package httpapi import ( "net" "net/http" "strings" "github.com/descrybe/descrybe-v2/apps/api/internal/config" ) // TrustedRealIP rewrites RemoteAddr from client IP headers only when the // immediate peer is listed in TRUSTED_PROXIES. Empty allowlist leaves // RemoteAddr unchanged (ignores spoofable X-Forwarded-For / X-Real-IP). func TrustedRealIP(trusted []string) func(http.Handler) http.Handler { nets, err := config.ParseTrustedProxyNets(trusted) if err != nil || len(nets) == 0 { return func(next http.Handler) http.Handler { return next } } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if isTrustedPeer(r.RemoteAddr, nets) { if rip := clientIPFromProxyHeaders(r); rip != "" { r.RemoteAddr = rip } } next.ServeHTTP(w, r) }) } } // apiContentSecurityPolicy is a strict CSP for JSON API responses (no HTML/scripts). const apiContentSecurityPolicy = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" // SecurityHeaders sets baseline API response headers. HSTS is only emitted // when session cookies are marked Secure (HTTPS deployments). func SecurityHeaders(sessionSecure bool) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h := w.Header() h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Frame-Options", "DENY") h.Set("Referrer-Policy", "strict-origin-when-cross-origin") h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") h.Set("Content-Security-Policy", apiContentSecurityPolicy) if sessionSecure { h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") } next.ServeHTTP(w, r) }) } } func isTrustedPeer(remoteAddr string, nets []*net.IPNet) bool { ip := peerIP(remoteAddr) if ip == nil { return false } for _, n := range nets { if n.Contains(ip) { return true } } return false } func peerIP(remoteAddr string) net.IP { host := strings.TrimSpace(remoteAddr) if h, _, err := net.SplitHostPort(host); err == nil { host = h } return net.ParseIP(host) } func clientIPFromProxyHeaders(r *http.Request) string { var ip string if tcip := r.Header.Get("True-Client-IP"); tcip != "" { ip = tcip } else if xrip := r.Header.Get("X-Real-IP"); xrip != "" { ip = xrip } else if xff := r.Header.Get("X-Forwarded-For"); xff != "" { i := strings.Index(xff, ",") if i == -1 { i = len(xff) } ip = xff[:i] } ip = strings.TrimSpace(ip) if ip == "" || net.ParseIP(ip) == nil { return "" } return ip }