package httpapp import ( "net/http" "strings" "gitea.avt.data-center.id/othman.suseno/atlas/internal/errors" ) // securityHeadersMiddleware adds security headers to responses func (a *App) securityHeadersMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Security headers w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") // HSTS (only for HTTPS) if r.TLS != nil { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") } // Content Security Policy (CSP) // Allow Tailwind CDN, unpkg (for htmx), and jsdelivr for external resources // Note: Tailwind CDN needs connect-src to fetch config and make network requests csp := "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net; connect-src 'self' https://cdn.tailwindcss.com https://unpkg.com https://cdn.jsdelivr.net;" w.Header().Set("Content-Security-Policy", csp) next.ServeHTTP(w, r) }) } // corsMiddleware handles CORS requests func (a *App) corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") // Allow specific origins or all (for development) allowedOrigins := []string{ "http://localhost:8080", "http://localhost:3000", "http://127.0.0.1:8080", } // Check if origin is allowed allowed := false for _, allowedOrigin := range allowedOrigins { if origin == allowedOrigin { allowed = true break } } // Allow requests from same origin if origin == "" || r.Header.Get("Referer") != "" { allowed = true } if allowed && origin != "" { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Max-Age", "3600") } // Handle preflight requests if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } // requestSizeMiddleware limits request body size func (a *App) requestSizeMiddleware(maxSize int64) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Limit request body size r.Body = http.MaxBytesReader(w, r.Body, maxSize) next.ServeHTTP(w, r) }) } } // validateContentTypeMiddleware validates Content-Type for POST/PUT/PATCH requests func (a *App) validateContentTypeMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip for GET, HEAD, OPTIONS, DELETE if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions || r.Method == http.MethodDelete { next.ServeHTTP(w, r) return } // Skip for public endpoints if a.isPublicEndpoint(r.URL.Path, r.Method) { next.ServeHTTP(w, r) return } // Check Content-Type for POST/PUT/PATCH contentType := r.Header.Get("Content-Type") if contentType == "" { writeError(w, errors.ErrBadRequest("Content-Type header is required")) return } // Allow JSON and form data if !strings.HasPrefix(contentType, "application/json") && !strings.HasPrefix(contentType, "application/x-www-form-urlencoded") && !strings.HasPrefix(contentType, "multipart/form-data") { writeError(w, errors.ErrBadRequest("Content-Type must be application/json")) return } next.ServeHTTP(w, r) }) }