diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index a1e15abe1..db66fd38b 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -185,8 +185,7 @@ func (h *ConfigHandlers) ValidateSetupToken(token string) bool { defer h.codeMutex.RUnlock() if code, exists := h.setupCodes[tokenHash]; exists { - // Allow tokens while they are valid or within a short grace period after use. - if now.Before(code.ExpiresAt.Add(2 * time.Minute)) { + if !code.Used && now.Before(code.ExpiresAt) { return true } } @@ -5555,8 +5554,8 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque } log.Debug(). - Str("authToken", req.AuthToken). - Str("authCode", authCode). + Bool("hasAuthToken", strings.TrimSpace(req.AuthToken) != ""). + Bool("hasSetupCode", strings.TrimSpace(authCode) != ""). Bool("hasConfigToken", h.config.HasAPITokens()). Msg("Checking authentication for auto-register") @@ -5578,7 +5577,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque // Not the API token, check if it's a temporary setup code codeHash := internalauth.HashAPIToken(authCode) log.Debug(). - Str("authCode", authCode). + Bool("hasAuthCode", true). Str("codeHash", codeHash[:8]+"..."). Msg("Checking auth token as setup code") h.codeMutex.Lock() @@ -5593,10 +5592,9 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque // what's entered in the UI and what's provided in the setup script URL if setupCode.NodeType == req.Type { setupCode.Used = true // Mark as used immediately - // Allow the token to be reused for a brief grace period so the setup - // script can complete follow-up actions (temperature verification, etc). - graceExpiry := time.Now().Add(5 * time.Minute) - if setupCode.ExpiresAt.After(graceExpiry) { + // Allow a short grace period for follow-up actions without keeping tokens alive too long + graceExpiry := time.Now().Add(1 * time.Minute) + if setupCode.ExpiresAt.Before(graceExpiry) { graceExpiry = setupCode.ExpiresAt } h.recentSetupTokens[codeHash] = graceExpiry diff --git a/internal/api/router.go b/internal/api/router.go index e45b574a6..77ea86b04 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1524,11 +1524,21 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) { return } + if !canCapturePublicURL(r.config, req) { + return + } + if r.config.EnvOverrides != nil && r.config.EnvOverrides["publicURL"] { return } - rawHost := firstForwardedValue(req.Header.Get("X-Forwarded-Host")) + peerIP := extractRemoteIP(req.RemoteAddr) + trustedProxy := isTrustedProxyIP(peerIP) + + rawHost := "" + if trustedProxy { + rawHost = firstForwardedValue(req.Header.Get("X-Forwarded-Host")) + } if rawHost == "" { rawHost = req.Host } @@ -1540,9 +1550,12 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) { return } - rawProto := firstForwardedValue(req.Header.Get("X-Forwarded-Proto")) - if rawProto == "" { - rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Scheme")) + rawProto := "" + if trustedProxy { + rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Proto")) + if rawProto == "" { + rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Scheme")) + } } scheme := strings.ToLower(strings.TrimSpace(rawProto)) switch scheme { @@ -1668,6 +1681,64 @@ func shouldAppendForwardedPort(port, scheme string) bool { return true } +func canCapturePublicURL(cfg *config.Config, req *http.Request) bool { + if cfg == nil || req == nil { + return false + } + + if isDirectLoopbackRequest(req) { + return true + } + + return isRequestAuthenticated(cfg, req) +} + +func isRequestAuthenticated(cfg *config.Config, req *http.Request) bool { + if cfg == nil || req == nil { + return false + } + + if cfg.ProxyAuthSecret != "" { + if ok, _, _ := CheckProxyAuth(cfg, req); ok { + return true + } + } + + if cfg.HasAPITokens() { + if token := strings.TrimSpace(req.Header.Get("X-API-Token")); token != "" { + if _, ok := cfg.ValidateAPIToken(token); ok { + return true + } + } + if authHeader := strings.TrimSpace(req.Header.Get("Authorization")); strings.HasPrefix(strings.ToLower(authHeader), "bearer ") { + if _, ok := cfg.ValidateAPIToken(strings.TrimSpace(authHeader[7:])); ok { + return true + } + } + } + + if cookie, err := req.Cookie("pulse_session"); err == nil && cookie.Value != "" { + if ValidateSession(cookie.Value) { + return true + } + } + + if cfg.AuthUser != "" && cfg.AuthPass != "" { + const prefix = "Basic " + if authHeader := req.Header.Get("Authorization"); strings.HasPrefix(authHeader, prefix) { + if decoded, err := base64.StdEncoding.DecodeString(authHeader[len(prefix):]); err == nil { + if parts := strings.SplitN(string(decoded), ":", 2); len(parts) == 2 { + if parts[0] == cfg.AuthUser && auth.CheckPasswordHash(parts[1], cfg.AuthPass) { + return true + } + } + } + } + } + + return false +} + // handleHealth handles health check requests func (r *Router) handleHealth(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodGet && req.Method != http.MethodHead {