Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
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
|
|
}
|