// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "encoding/json" "fmt" "log/slog" "net" "net/http" "time" "github.com/gorilla/mux" ) // HTTP server timeouts const ( readHeaderTimeout = 10 * time.Second readTimeout = 30 * time.Second writeTimeout = 30 * time.Second idleTimeout = 120 * time.Second ) // defaultShutdownTimeout bounds the graceful shutdown const defaultShutdownTimeout = 15 * time.Second // WebServer holds the HTTP server and its dependencies type WebServer struct { HTTPServer *http.Server log *slog.Logger tokens TokenLookup audit AuditRecorder docker ContainerLister encode func(v any) ([]byte, error) } // NewWebServer builds the server, its router and its middleware func NewWebServer(log *slog.Logger, tokens TokenLookup, audit AuditRecorder, docker ContainerLister) *WebServer { s := &WebServer{ log: log, tokens: tokens, audit: audit, docker: docker, encode: json.Marshal, } s.HTTPServer = &http.Server{ Handler: s.Handler(), ReadHeaderTimeout: readHeaderTimeout, ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn), } return s } // Handler returns the complete request handler: logging middleware around the router func (s *WebServer) Handler() http.Handler { defer traceFunc(context.Background(), s.log, "WebServer.Handler")() // Create the router; paths are matched exactly (no cleaning redirects, no strict-slash redirects) r := mux.NewRouter() r.SkipClean(true) r.NotFoundHandler = http.HandlerFunc(s.handleNotFound) r.MethodNotAllowedHandler = http.HandlerFunc(s.handleMethodNotAllowed) // Associate the various handlers (routes) s.Routes(r) s.log.Debug("routes registered", "route", http.MethodGet+" "+containersPath) return s.logRequests(r) } // Serve serves HTTP on ln until ctx is cancelled, then shuts down gracefully within shutdownTimeout // It returns an error if the server stops unexpectedly or the graceful shutdown fails func (s *WebServer) Serve(ctx context.Context, ln net.Listener, shutdownTimeout time.Duration) error { defer traceFunc(ctx, s.log, "WebServer.Serve")() // Start serving in the background s.log.InfoContext(ctx, "server listening", "addr", ln.Addr().String(), "version", APP_VERSION, "commit", COMMIT_ID) errCh := make(chan error, 1) go func() { errCh <- s.HTTPServer.Serve(ln) }() // Wait for either a server failure or the shutdown request select { case err := <-errCh: return fmt.Errorf("http server: %w", err) case <-ctx.Done(): } // Graceful shutdown s.log.DebugContext(ctx, "shutdown requested, draining http server", "timeout", shutdownTimeout) shCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), shutdownTimeout) defer cancel() if err := s.HTTPServer.Shutdown(shCtx); err != nil { s.HTTPServer.Close() return fmt.Errorf("http server shutdown: %w", err) } <-errCh s.log.DebugContext(ctx, "http server shut down") return nil }