mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-25 08:25:07 +03:00
Add password authentication to WebUI and implement session management
- Updated WebUI configuration to include a password field for authentication. - Enhanced the WebUI server to handle login and logout functionality with session management. - Added tests for authentication and session handling. - Updated README and example configuration to reflect new authentication features.
This commit is contained in:
parent
51e1ef3ed0
commit
113dcbb72a
17 changed files with 676 additions and 74 deletions
|
@ -1,27 +1,193 @@
|
|||
package webui
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||
)
|
||||
|
||||
type WebUIServer struct {
|
||||
server *http.Server
|
||||
log core.Logger
|
||||
listen string
|
||||
server *http.Server
|
||||
log core.Logger
|
||||
listen string
|
||||
password string
|
||||
sessions map[string]time.Time // sessionID -> expiry time
|
||||
sessionsMux sync.RWMutex
|
||||
}
|
||||
|
||||
func Server(listen string, log core.Logger) *WebUIServer {
|
||||
type LoginRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func Server(listen string, password string, log core.Logger) *WebUIServer {
|
||||
return &WebUIServer{
|
||||
listen: listen,
|
||||
log: log,
|
||||
listen: listen,
|
||||
password: password,
|
||||
log: log,
|
||||
sessions: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
// generateSessionID creates a random session ID
|
||||
func (w *WebUIServer) generateSessionID() string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// isValidSession checks if a session is valid and not expired
|
||||
func (w *WebUIServer) isValidSession(sessionID string) bool {
|
||||
w.sessionsMux.RLock()
|
||||
defer w.sessionsMux.RUnlock()
|
||||
|
||||
expiry, exists := w.sessions[sessionID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
if time.Now().After(expiry) {
|
||||
// Session expired, clean it up
|
||||
go func() {
|
||||
w.sessionsMux.Lock()
|
||||
delete(w.sessions, sessionID)
|
||||
w.sessionsMux.Unlock()
|
||||
}()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// createSession creates a new session for the user
|
||||
func (w *WebUIServer) createSession() string {
|
||||
sessionID := w.generateSessionID()
|
||||
expiry := time.Now().Add(24 * time.Hour) // Session valid for 24 hours
|
||||
|
||||
w.sessionsMux.Lock()
|
||||
w.sessions[sessionID] = expiry
|
||||
w.sessionsMux.Unlock()
|
||||
|
||||
return sessionID
|
||||
}
|
||||
|
||||
// cleanupExpiredSessions removes expired sessions
|
||||
func (w *WebUIServer) cleanupExpiredSessions() {
|
||||
w.sessionsMux.Lock()
|
||||
defer w.sessionsMux.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
for sessionID, expiry := range w.sessions {
|
||||
if now.After(expiry) {
|
||||
delete(w.sessions, sessionID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// authMiddleware checks for valid session or redirects to login
|
||||
func (w *WebUIServer) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Skip authentication if no password is set
|
||||
if w.password == "" {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for session cookie
|
||||
cookie, err := r.Cookie("ygg_session")
|
||||
if err != nil || !w.isValidSession(cookie.Value) {
|
||||
// No valid session - redirect to login page
|
||||
if r.URL.Path == "/login.html" || strings.HasPrefix(r.URL.Path, "/auth/") {
|
||||
// Allow access to login page and auth endpoints
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For API calls, return 401
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// For regular pages, redirect to login
|
||||
http.Redirect(rw, r, "/login.html", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
next(rw, r)
|
||||
}
|
||||
}
|
||||
|
||||
// loginHandler handles password authentication
|
||||
func (w *WebUIServer) loginHandler(rw http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var loginReq LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
|
||||
http.Error(rw, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check password
|
||||
if subtle.ConstantTimeCompare([]byte(loginReq.Password), []byte(w.password)) != 1 {
|
||||
w.log.Debugf("Authentication failed for request from %s", r.RemoteAddr)
|
||||
http.Error(rw, "Invalid password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Create session
|
||||
sessionID := w.createSession()
|
||||
|
||||
// Set session cookie
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "ygg_session",
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: r.TLS != nil, // Only set Secure flag if using HTTPS
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: 24 * 60 * 60, // 24 hours
|
||||
})
|
||||
|
||||
w.log.Debugf("Successful authentication for request from %s", r.RemoteAddr)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// logoutHandler handles logout
|
||||
func (w *WebUIServer) logoutHandler(rw http.ResponseWriter, r *http.Request) {
|
||||
// Get session cookie
|
||||
cookie, err := r.Cookie("ygg_session")
|
||||
if err == nil {
|
||||
// Remove session from server
|
||||
w.sessionsMux.Lock()
|
||||
delete(w.sessions, cookie.Value)
|
||||
w.sessionsMux.Unlock()
|
||||
}
|
||||
|
||||
// Clear session cookie
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
Name: "ygg_session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1, // Delete cookie
|
||||
})
|
||||
|
||||
// Redirect to login page
|
||||
http.Redirect(rw, r, "/login.html", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (w *WebUIServer) Start() error {
|
||||
// Validate listen address before starting
|
||||
if w.listen != "" {
|
||||
|
@ -30,17 +196,30 @@ func (w *WebUIServer) Start() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Start session cleanup routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
w.cleanupExpiredSessions()
|
||||
}
|
||||
}()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Authentication endpoints - no auth required
|
||||
mux.HandleFunc("/auth/login", w.loginHandler)
|
||||
mux.HandleFunc("/auth/logout", w.logoutHandler)
|
||||
|
||||
// Setup static files handler (implementation varies by build)
|
||||
setupStaticHandler(mux)
|
||||
setupStaticHandler(mux, w)
|
||||
|
||||
// Serve any file by path (implementation varies by build)
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Serve any file by path (implementation varies by build) - with auth
|
||||
mux.HandleFunc("/", w.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
|
||||
serveFile(rw, r, w.log)
|
||||
})
|
||||
}))
|
||||
|
||||
// Health check endpoint
|
||||
// Health check endpoint - no auth required
|
||||
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte("OK"))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue