package handler import ( "crypto/rand" "encoding/base64" "net/http" "time" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" ) // Session key constants const ( SessionName = "ddns_session" SessionUserKey = "user" SessionAuthKey = "authenticated" SessionCreatedAt = "created_at" SessionExpiresAt = "expires_at" ) // ShowLoginPage renders the login page func (h *Handler) ShowLoginPage(c echo.Context) error { // Check if already authenticated if h.IsAuthenticated(c) { return c.Redirect(http.StatusFound, "/@/hosts") } // Check if there's an error message from failed login errorMsg := c.QueryParam("error") return c.Render(http.StatusOK, "login", echo.Map{ "title": h.Title, "logoPath": h.LogoPath, "poweredBy": h.PoweredBy, "poweredByUrl": h.PoweredByUrl, "error": errorMsg, }) } // HandleLogin processes login form submission func (h *Handler) HandleLogin(c echo.Context) error { username := c.FormValue("username") password := c.FormValue("password") rememberMe := c.FormValue("remember_me") == "on" // Get client IP for logging clientIP := ExtractIPFromRequest( c.Request().RemoteAddr, c.Request().Header.Get("X-Forwarded-For"), c.Request().Header.Get("X-Real-IP"), ) // Validate credentials authenticated, err := h.authByEnv(username, password) if err != nil { log.Error("Authentication error:", err) h.LogFailedAuth(clientIP, c.Request().UserAgent(), c.Path(), username, password) return c.Redirect(http.StatusFound, "/@/login?error=authentication_error") } if !authenticated { log.Warnf("Failed login attempt from IP %s, username: %s", clientIP, username) h.LogFailedAuth(clientIP, c.Request().UserAgent(), c.Path(), username, password) return c.Redirect(http.StatusFound, "/@/login?error=invalid_credentials") } // Authentication successful - create session sess, err := h.GetSession(c) if err != nil { log.Error("Session creation error:", err) return c.Redirect(http.StatusFound, "/@/login?error=session_error") } // Set session values sess.Values[SessionUserKey] = username sess.Values[SessionAuthKey] = true sess.Values[SessionCreatedAt] = time.Now().Unix() // Set expiration based on remember me if rememberMe { sess.Options.MaxAge = 30 * 24 * 60 * 60 // 30 days sess.Values[SessionExpiresAt] = time.Now().Add(30 * 24 * time.Hour).Unix() } else { sess.Options.MaxAge = 24 * 60 * 60 // 24 hours sess.Values[SessionExpiresAt] = time.Now().Add(24 * time.Hour).Unix() } // Set secure flag if using HTTPS if h.IsHTTPS(c) { sess.Options.Secure = true } // Save session if err := sess.Save(c.Request(), c.Response()); err != nil { log.Error("Session save error:", err) return c.Redirect(http.StatusFound, "/@/login?error=session_error") } log.Infof("Successful login from IP %s, username: %s", clientIP, username) // Redirect to originally requested page or default to hosts redirect := c.QueryParam("redirect") if redirect == "" || redirect == "/@/login" { redirect = "/@/hosts" } return c.Redirect(http.StatusFound, redirect) } // HandleLogout destroys the session and logs out the user func (h *Handler) HandleLogout(c echo.Context) error { sess, err := h.GetSession(c) if err == nil { // Get username before destroying session username := "unknown" if user, ok := sess.Values[SessionUserKey].(string); ok { username = user } // Destroy session sess.Options.MaxAge = -1 sess.Values = make(map[interface{}]interface{}) sess.Save(c.Request(), c.Response()) if username != "" { log.Infof("User %s logged out", username) } } // Clear session cookie c.SetCookie(&http.Cookie{ Name: SessionName, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: h.IsHTTPS(c), SameSite: http.SameSiteStrictMode, }) // ALWAYS render logout page (not redirect) // Pass LogoutUrl so JavaScript can handle delayed redirect return c.Render(http.StatusOK, "logout", echo.Map{ "title": h.Title, "logoPath": h.LogoPath, "logoutUrl": h.LogoutUrl, // Pass the logout URL to template "poweredBy": h.PoweredBy, "poweredByUrl": h.PoweredByUrl, }) } // IsAuthenticated checks if the current session is authenticated func (h *Handler) IsAuthenticated(c echo.Context) bool { if h.DisableAdminAuth { return true } sess, err := h.GetSession(c) if err != nil { return false } // Check if authenticated authenticated, ok := sess.Values[SessionAuthKey].(bool) if !ok || !authenticated { return false } // Check if session has expired if expiresAt, ok := sess.Values[SessionExpiresAt].(int64); ok { if time.Now().Unix() > expiresAt { log.Info("Session expired") return false } } return true } // GetSession retrieves or creates a session for the request func (h *Handler) GetSession(c echo.Context) (*Session, error) { return h.SessionStore.Get(c.Request(), SessionName) } // GenerateCSRFToken generates a random CSRF token func GenerateCSRFToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return base64.URLEncoding.EncodeToString(b), nil } // IsHTTPS checks if the request came via HTTPS // Checks both direct HTTPS and reverse proxy headers func (h *Handler) IsHTTPS(c echo.Context) bool { // Check if direct HTTPS if c.Request().TLS != nil { return true } // Check reverse proxy headers proto := c.Request().Header.Get("X-Forwarded-Proto") if proto == "https" { return true } // Check other common headers if c.Request().Header.Get("X-Forwarded-Ssl") == "on" { return true } if c.Request().Header.Get("X-Url-Scheme") == "https" { return true } return false } // GetHTTPSRedirectURL constructs the HTTPS version of the current URL func (h *Handler) GetHTTPSRedirectURL(c echo.Context) string { host := c.Request().Host uri := c.Request().RequestURI return "https://" + host + uri }