// Package middleware provides HTTP middleware for logging, // CORS, and other cross-cutting concerns. package middleware import ( "log/slog" "net" "net/http" "time" "sneak.berlin/go/netwatch/internal/config" "sneak.berlin/go/netwatch/internal/globals" "sneak.berlin/go/netwatch/internal/logger" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "go.uber.org/fx" ) const corsMaxAgeSec = 300 // Params defines the dependencies for Middleware. type Params struct { fx.In Config *config.Config Globals *globals.Globals Logger *logger.Logger } // Middleware holds shared state for middleware factories. type Middleware struct { log *slog.Logger params *Params } // New creates a Middleware instance. func New( _ fx.Lifecycle, params Params, ) (*Middleware, error) { s := new(Middleware) s.params = ¶ms s.log = params.Logger.Get() return s, nil } type loggingResponseWriter struct { http.ResponseWriter statusCode int } func newLoggingResponseWriter( w http.ResponseWriter, ) *loggingResponseWriter { return &loggingResponseWriter{w, http.StatusOK} } func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.statusCode = code lrw.ResponseWriter.WriteHeader(code) } func ipFromHostPort(hostPort string) string { host, _, err := net.SplitHostPort(hostPort) if err != nil { return hostPort } return host } // Logging returns middleware that logs each request with // timing, status code, and client information. func (s *Middleware) Logging() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { start := time.Now().UTC() lrw := newLoggingResponseWriter(w) ctx := r.Context() defer func() { latency := time.Since(start) s.log.InfoContext(ctx, "request", "request_start", start, "method", r.Method, "url", r.URL.String(), "useragent", r.UserAgent(), "request_id", ctx.Value( middleware.RequestIDKey, ), "referer", r.Referer(), "proto", r.Proto, "remote_ip", ipFromHostPort(r.RemoteAddr), "status", lrw.statusCode, "latency_ms", latency.Milliseconds(), ) }() next.ServeHTTP(lrw, r) }, ) } } // CORS returns middleware that adds permissive CORS headers. func (s *Middleware) CORS() func(http.Handler) http.Handler { return cors.Handler(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: []string{ "GET", "POST", "PUT", "DELETE", "OPTIONS", }, AllowedHeaders: []string{ "Accept", "Authorization", "Content-Type", "X-CSRF-Token", }, ExposedHeaders: []string{"Link"}, AllowCredentials: false, MaxAge: corsMaxAgeSec, }) }