mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-19 16:27:37 +00:00
Harden public URL detection and setup token handling
This commit is contained in:
parent
17d55b8cf4
commit
d6cbfc23ec
2 changed files with 82 additions and 13 deletions
|
|
@ -185,8 +185,7 @@ func (h *ConfigHandlers) ValidateSetupToken(token string) bool {
|
||||||
defer h.codeMutex.RUnlock()
|
defer h.codeMutex.RUnlock()
|
||||||
|
|
||||||
if code, exists := h.setupCodes[tokenHash]; exists {
|
if code, exists := h.setupCodes[tokenHash]; exists {
|
||||||
// Allow tokens while they are valid or within a short grace period after use.
|
if !code.Used && now.Before(code.ExpiresAt) {
|
||||||
if now.Before(code.ExpiresAt.Add(2 * time.Minute)) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5555,8 +5554,8 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("authToken", req.AuthToken).
|
Bool("hasAuthToken", strings.TrimSpace(req.AuthToken) != "").
|
||||||
Str("authCode", authCode).
|
Bool("hasSetupCode", strings.TrimSpace(authCode) != "").
|
||||||
Bool("hasConfigToken", h.config.HasAPITokens()).
|
Bool("hasConfigToken", h.config.HasAPITokens()).
|
||||||
Msg("Checking authentication for auto-register")
|
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
|
// Not the API token, check if it's a temporary setup code
|
||||||
codeHash := internalauth.HashAPIToken(authCode)
|
codeHash := internalauth.HashAPIToken(authCode)
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("authCode", authCode).
|
Bool("hasAuthCode", true).
|
||||||
Str("codeHash", codeHash[:8]+"...").
|
Str("codeHash", codeHash[:8]+"...").
|
||||||
Msg("Checking auth token as setup code")
|
Msg("Checking auth token as setup code")
|
||||||
h.codeMutex.Lock()
|
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
|
// what's entered in the UI and what's provided in the setup script URL
|
||||||
if setupCode.NodeType == req.Type {
|
if setupCode.NodeType == req.Type {
|
||||||
setupCode.Used = true // Mark as used immediately
|
setupCode.Used = true // Mark as used immediately
|
||||||
// Allow the token to be reused for a brief grace period so the setup
|
// Allow a short grace period for follow-up actions without keeping tokens alive too long
|
||||||
// script can complete follow-up actions (temperature verification, etc).
|
graceExpiry := time.Now().Add(1 * time.Minute)
|
||||||
graceExpiry := time.Now().Add(5 * time.Minute)
|
if setupCode.ExpiresAt.Before(graceExpiry) {
|
||||||
if setupCode.ExpiresAt.After(graceExpiry) {
|
|
||||||
graceExpiry = setupCode.ExpiresAt
|
graceExpiry = setupCode.ExpiresAt
|
||||||
}
|
}
|
||||||
h.recentSetupTokens[codeHash] = graceExpiry
|
h.recentSetupTokens[codeHash] = graceExpiry
|
||||||
|
|
|
||||||
|
|
@ -1524,11 +1524,21 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !canCapturePublicURL(r.config, req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if r.config.EnvOverrides != nil && r.config.EnvOverrides["publicURL"] {
|
if r.config.EnvOverrides != nil && r.config.EnvOverrides["publicURL"] {
|
||||||
return
|
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 == "" {
|
if rawHost == "" {
|
||||||
rawHost = req.Host
|
rawHost = req.Host
|
||||||
}
|
}
|
||||||
|
|
@ -1540,9 +1550,12 @@ func (r *Router) capturePublicURLFromRequest(req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rawProto := firstForwardedValue(req.Header.Get("X-Forwarded-Proto"))
|
rawProto := ""
|
||||||
if rawProto == "" {
|
if trustedProxy {
|
||||||
rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Scheme"))
|
rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Proto"))
|
||||||
|
if rawProto == "" {
|
||||||
|
rawProto = firstForwardedValue(req.Header.Get("X-Forwarded-Scheme"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scheme := strings.ToLower(strings.TrimSpace(rawProto))
|
scheme := strings.ToLower(strings.TrimSpace(rawProto))
|
||||||
switch scheme {
|
switch scheme {
|
||||||
|
|
@ -1668,6 +1681,64 @@ func shouldAppendForwardedPort(port, scheme string) bool {
|
||||||
return true
|
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
|
// handleHealth handles health check requests
|
||||||
func (r *Router) handleHealth(w http.ResponseWriter, req *http.Request) {
|
func (r *Router) handleHealth(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
if req.Method != http.MethodGet && req.Method != http.MethodHead {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue