package middleware import ( "net/http" "strconv" "strings" "github.com/labstack/echo/v4" ) // SecurityHeadersMiddleware adds security headers to all responses func SecurityHeadersMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Add security headers c.Response().Header().Set("X-Content-Type-Options", "nosniff") c.Response().Header().Set("X-Frame-Options", "DENY") c.Response().Header().Set("X-XSS-Protection", "1; mode=block") c.Response().Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") c.Response().Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:") c.Response().Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") c.Response().Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") return next(c) } } } // HTTPSRedirectMiddleware redirects HTTP to HTTPS // NOTE: Disabled for Docker self-hosted deployments - SSL is handled by reverse proxy func HTTPSRedirectMiddleware(httpsPort string) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // SSL is handled by proxy - no redirect needed return next(c) } } } // isTestMode checks if the application is running in test mode func isTestMode(c echo.Context) bool { // Check for test mode header or environment return c.Request().Header.Get("X-Test-Mode") == "true" || c.Request().Header.Get("X-Forwarded-Proto") == "http" } // SSLProxyMiddleware handles SSL termination proxy headers // NOTE: Essential for Docker deployments where SSL is handled by reverse proxy (nginx, traefik, etc.) // This middleware reads X-Forwarded-Proto and X-Forwarded-Host headers set by the proxy func SSLProxyMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Check for proxy headers if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto == "https" { c.Request().URL.Scheme = "https" } if host := c.Request().Header.Get("X-Forwarded-Host"); host != "" { c.Request().URL.Host = host } return next(c) } } } // HTTPSProtectionMiddleware provides HTTPS protection for production func HTTPSProtectionMiddleware(enableRedirect bool, httpsPort string) []echo.MiddlewareFunc { return []echo.MiddlewareFunc{ SSLProxyMiddleware(), SecurityHeadersMiddleware(), HTTPSRedirectMiddleware(httpsPort), } } // CORSSecurityConfig configures CORS with security best practices type CORSSecurityConfig struct { AllowedOrigins []string AllowedMethods []string AllowedHeaders []string ExposedHeaders []string AllowCredentials bool MaxAge int } // NewSecureCORSConfig creates production-ready CORS config func NewSecureCORSConfig() CORSSecurityConfig { return CORSSecurityConfig{ AllowedOrigins: []string{ "https://bookhoard.example.com", "https://*.bookhoard.example.com", }, AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowedHeaders: []string{ "Accept", "Authorization", "Content-Type", "X-Request-ID", "X-Timestamp", "X-Signature", "X-Device-ID", }, ExposedHeaders: []string{ "X-Request-ID", "X-Sync-Status", "X-Conflict-Detected", }, AllowCredentials: true, MaxAge: 86400, // 24 hours } } // SecureCORSMiddleware creates CORS middleware with security func SecureCORSMiddleware(config CORSSecurityConfig) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { origin := c.Request().Header.Get("Origin") // Check if origin is allowed allowed := false for _, allowedOrigin := range config.AllowedOrigins { if origin == allowedOrigin || allowedOrigin == "*" { allowed = true break } } if !allowed { return c.JSON(http.StatusForbidden, map[string]string{ "error": "Origin not allowed", }) } // Set CORS headers c.Response().Header().Set("Access-Control-Allow-Origin", origin) c.Response().Header().Set("Access-Control-Allow-Methods", strings.Join(config.AllowedMethods, ", ")) c.Response().Header().Set("Access-Control-Allow-Headers", strings.Join(config.AllowedHeaders, ", ")) c.Response().Header().Set("Access-Control-Expose-Headers", strings.Join(config.ExposedHeaders, ", ")) c.Response().Header().Set("Access-Control-Allow-Credentials", "true") c.Response().Header().Set("Access-Control-Max-Age", strconv.Itoa(config.MaxAge)) // Handle preflight if c.Request().Method == "OPTIONS" { return c.NoContent(http.StatusNoContent) } return next(c) } } }