// kafka — a privacy-respecting metasearch engine // Copyright (C) 2026-present metamorphosis-dev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. package middleware import ( "net/http" "strconv" "strings" ) // SecurityHeadersConfig controls which security headers are set. type SecurityHeadersConfig struct { // FrameOptions controls X-Frame-Options. Default: "DENY". FrameOptions string // HSTSMaxAge controls the max-age for Strict-Transport-Security. // Set to 0 to disable HSTS (useful for local dev). Default: 31536000 (1 year). HSTSMaxAge int // HSTSPreloadDomains adds "includeSubDomains; preload" to HSTS. HSTSPreloadDomains bool // ReferrerPolicy controls the Referrer-Policy header. Default: "no-referrer". ReferrerPolicy string // CSP controls Content-Security-Policy. Default: a restrictive policy. // Set to "" to disable CSP entirely. CSP string } // SecurityHeaders returns middleware that sets standard HTTP security headers // on every response. func SecurityHeaders(cfg SecurityHeadersConfig) func(http.Handler) http.Handler { frameOpts := cfg.FrameOptions if frameOpts == "" { frameOpts = "DENY" } hstsAge := cfg.HSTSMaxAge if hstsAge == 0 { hstsAge = 31536000 // 1 year } refPol := cfg.ReferrerPolicy if refPol == "" { refPol = "no-referrer" } csp := cfg.CSP if csp == "" { csp = defaultCSP() } hstsValue := "max-age=" + strconv.Itoa(hstsAge) if cfg.HSTSPreloadDomains { hstsValue += "; includeSubDomains; preload" } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", frameOpts) w.Header().Set("Referrer-Policy", refPol) w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") w.Header().Set("Content-Security-Policy", csp) if hstsAge > 0 { w.Header().Set("Strict-Transport-Security", hstsValue) } next.ServeHTTP(w, r) }) } } // defaultCSP returns a restrictive Content-Security-Policy for the // metasearch engine. func defaultCSP() string { return strings.Join([]string{ "default-src 'self'", "script-src 'self' 'unsafe-inline' https://unpkg.com", "style-src 'self' 'unsafe-inline'", "img-src 'self' https: data:", "connect-src 'self'", "font-src 'self'", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", }, "; ") }