diff options
Diffstat (limited to 'internal/server/server.go')
| -rw-r--r-- | internal/server/server.go | 105 |
1 files changed, 79 insertions, 26 deletions
diff --git a/internal/server/server.go b/internal/server/server.go index b47bf79..ab9ff24 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -28,14 +28,18 @@ var templatesFS embed.FS //go:embed static/* var staticFS embed.FS +// ReloadFunc is a callback function for reloading configuration +type ReloadFunc func() error + // Server handles HTTP requests for the status page type Server struct { - config *config.Config - storage *storage.Storage - scheduler *monitor.Scheduler - logger *slog.Logger - server *http.Server - templates *template.Template + config *config.Config + storage *storage.Storage + scheduler *monitor.Scheduler + logger *slog.Logger + server *http.Server + templates *template.Template + reloadConfig ReloadFunc } // New creates a new HTTP server @@ -89,6 +93,9 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l mux.HandleFunc("GET /api/page", s.withAPIAuth(s.handleAPIPage)) } + // Config reload endpoint - always requires authentication + mux.HandleFunc("POST /api/reload", s.withStrictAuth(s.handleAPIReload)) + // Create HTTP server s.server = &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port), @@ -145,39 +152,59 @@ func (s *Server) withAPIAuth(handler http.HandlerFunc) http.HandlerFunc { return case "authenticated": - // Check for API key in header or query param - apiKey := r.Header.Get("X-API-Key") - if apiKey == "" { - apiKey = r.URL.Query().Get("api_key") - } - - if apiKey == "" { + if !s.checkAPIKey(r) { w.Header().Set("WWW-Authenticate", "API-Key") s.jsonError(w, "API key required", http.StatusUnauthorized) return } - // Check if key is valid - valid := false - for _, key := range s.config.API.Keys { - if key == apiKey { - valid = true - break - } - } + // case "public" or default: allow access + } - if !valid { - s.jsonError(w, "Invalid API key", http.StatusUnauthorized) - return - } + handler(w, r) + } +} - // case "public" or default: allow access +// withStrictAuth wraps an API handler that always requires authentication, +// regardless of the api.access setting. Used for sensitive operations like config reload. +func (s *Server) withStrictAuth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Always require API key, even if api.access is "public" + if len(s.config.API.Keys) == 0 { + s.jsonError(w, "No API keys configured. Add keys to api.keys in config to use this endpoint.", http.StatusForbidden) + return + } + + if !s.checkAPIKey(r) { + w.Header().Set("WWW-Authenticate", "API-Key") + s.jsonError(w, "API key required", http.StatusUnauthorized) + return } handler(w, r) } } +// checkAPIKey validates the API key from request header or query parameter +func (s *Server) checkAPIKey(r *http.Request) bool { + apiKey := r.Header.Get("X-API-Key") + if apiKey == "" { + apiKey = r.URL.Query().Get("api_key") + } + + if apiKey == "" { + return false + } + + for _, key := range s.config.API.Keys { + if key == apiKey { + return true + } + } + + return false +} + // PageData contains data for rendering the status page type PageData struct { Site config.SiteConfig @@ -991,6 +1018,32 @@ func (s *Server) handleAPIIncidents(w http.ResponseWriter, r *http.Request) { }) } +// SetReloadFunc sets the callback function for reloading configuration +func (s *Server) SetReloadFunc(fn ReloadFunc) { + s.reloadConfig = fn +} + +// handleAPIReload triggers a configuration reload (always requires authentication) +func (s *Server) handleAPIReload(w http.ResponseWriter, r *http.Request) { + if s.reloadConfig == nil { + s.jsonError(w, "Reload function not configured", http.StatusServiceUnavailable) + return + } + + s.logger.Info("config reload triggered via API", "remote_addr", r.RemoteAddr) + + if err := s.reloadConfig(); err != nil { + s.logger.Error("config reload failed", "error", err) + s.jsonError(w, fmt.Sprintf("Reload failed: %v", err), http.StatusInternalServerError) + return + } + + s.jsonResponse(w, map[string]string{ + "status": "ok", + "message": "Configuration reloaded successfully", + }) +} + // jsonResponse writes a JSON response func (s *Server) jsonResponse(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") |