| 274 | } |
| 275 | |
| 276 | func (s *Server) Handler() (*chi.Mux, error) { |
| 277 | r := chi.NewRouter() |
| 278 | |
| 279 | // Global middleware |
| 280 | r.Use(middleware.RequestID) // Must be before logger to capture request ID |
| 281 | // r.Use(middleware.Logger(s.logger)) |
| 282 | r.Use(middleware.Recoverer) |
| 283 | // Enforce auth-disabled IP allowlist against the direct TCP peer. |
| 284 | // This runs before RealIP so forwarded headers cannot bypass restrictions. |
| 285 | r.Use(middleware.RequireAuthDisabledIPAllowlist(s.config.Config)) |
| 286 | r.Use(middleware.RealIP) |
| 287 | |
| 288 | // HTTP compression - handles gzip, brotli, zstd, deflate automatically |
| 289 | // Use faster compression levels for better proxy performance |
| 290 | compressor, err := httpcompression.DefaultAdapter( |
| 291 | httpcompression.MinSize(1024), // Only compress responses >= 1KB |
| 292 | httpcompression.GzipCompressionLevel(2), // Use gzip level 2 (fast) instead of 6 (default) |
| 293 | httpcompression.Prefer(httpcompression.PreferServer), // Let server choose best compression |
| 294 | ) |
| 295 | if err != nil { |
| 296 | log.Error().Err(err).Msg("Failed to create HTTP compression adapter") |
| 297 | } else { |
| 298 | // SSE responses must never be compressed. The compressor's writer buffers |
| 299 | // until MinSize (delaying event flushes) and lacks Unwrap(), which prevents |
| 300 | // the stream handler from clearing the server WriteTimeout via |
| 301 | // http.NewResponseController. Bypass compression for event-stream requests |
| 302 | // (EventSource always sends Accept: text/event-stream), covering /stream and |
| 303 | // the RSS /events endpoint without coupling to specific paths. |
| 304 | r.Use(func(next http.Handler) http.Handler { |
| 305 | compressed := compressor(next) |
| 306 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| 307 | if strings.Contains(req.Header.Get("Accept"), "text/event-stream") { |
| 308 | next.ServeHTTP(w, req) |
| 309 | return |
| 310 | } |
| 311 | compressed.ServeHTTP(w, req) |
| 312 | }) |
| 313 | }) |
| 314 | } |
| 315 | |
| 316 | // CORS is disabled by default. Enable only for explicit trusted origins. |
| 317 | if len(s.config.Config.CORSAllowedOrigins) > 0 { |
| 318 | corsMiddleware := cors.New(cors.Options{ |
| 319 | AllowCredentials: true, |
| 320 | AllowedOrigins: s.config.Config.CORSAllowedOrigins, |
| 321 | AllowedMethods: []string{"HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"}, |
| 322 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-API-Key", "X-Requested-With"}, |
| 323 | MaxAge: 300, |
| 324 | Debug: false, |
| 325 | }) |
| 326 | r.Use(corsMiddleware.Handler) |
| 327 | } |
| 328 | |
| 329 | // Session middleware - must be added before any session-dependent middleware |
| 330 | r.Use(s.sessionManager.LoadAndSave) |
| 331 | |
| 332 | // Create handlers |
| 333 | healthHandler := handlers.NewHealthHandler() |