// Package httpserver provides TLS HTTP server setup with chi, standard // middleware, and graceful shutdown for Metacircular services. package httpserver import ( "context" "crypto/tls" "encoding/json" "fmt" "log/slog" "net/http" "time" "github.com/go-chi/chi/v5" "git.wntrmute.dev/mc/mcdsl/config" ) // Server wraps a chi router and an http.Server with the standard // Metacircular TLS configuration. type Server struct { // Router is the chi router. Services register their routes on it. Router *chi.Mux // Logger is used by the logging middleware. Logger *slog.Logger httpSrv *http.Server cfg config.ServerConfig } // New creates a Server configured from cfg. The underlying http.Server // is configured with TLS 1.3 minimum and timeouts from the config. // Services access s.Router to register routes before calling // ListenAndServeTLS. func New(cfg config.ServerConfig, logger *slog.Logger) *Server { r := chi.NewRouter() s := &Server{ Router: r, Logger: logger, cfg: cfg, } s.httpSrv = &http.Server{ Addr: cfg.ListenAddr, Handler: r, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS13, }, ReadTimeout: cfg.ReadTimeout.Duration, WriteTimeout: cfg.WriteTimeout.Duration, IdleTimeout: cfg.IdleTimeout.Duration, } return s } // ListenAndServeTLS starts the HTTPS server using the TLS certificate and // key from the config. It blocks until the server is shut down. Returns // nil if the server was shut down gracefully via [Server.Shutdown]. func (s *Server) ListenAndServeTLS() error { s.Logger.Info("starting server", "addr", s.cfg.ListenAddr) err := s.httpSrv.ListenAndServeTLS(s.cfg.TLSCert, s.cfg.TLSKey) if err != nil && err != http.ErrServerClosed { return fmt.Errorf("httpserver: %w", err) } return nil } // Shutdown gracefully shuts down the server, waiting for in-flight // requests to complete. The provided context controls the shutdown // timeout. func (s *Server) Shutdown(ctx context.Context) error { return s.httpSrv.Shutdown(ctx) } // LoggingMiddleware logs each HTTP request after it completes, including // method, path, status code, duration, and remote address. func (s *Server) LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() sw := &StatusWriter{ResponseWriter: w, Status: http.StatusOK} next.ServeHTTP(sw, r) s.Logger.Info("http", "method", r.Method, "path", r.URL.Path, "status", sw.Status, "duration", time.Since(start), "remote", r.RemoteAddr, ) }) } // StatusWriter wraps an http.ResponseWriter to capture the status code. // It is exported for use in custom middleware. type StatusWriter struct { http.ResponseWriter // Status is the HTTP status code written to the response. Status int } // WriteHeader captures the status code and delegates to the underlying // ResponseWriter. func (w *StatusWriter) WriteHeader(code int) { w.Status = code w.ResponseWriter.WriteHeader(code) } // WriteJSON writes v as JSON with the given HTTP status code. func WriteJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } // WriteError writes a standard Metacircular error response: // {"error": "message"}. func WriteError(w http.ResponseWriter, status int, message string) { WriteJSON(w, status, map[string]string{"error": message}) }