Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user