package server import ( "net" "net/http" "strings" ) // trustedProxies is a parsed list of CIDR ranges whose X-Forwarded-* headers // we are willing to honor. A zero-length list disables proxy-header handling // entirely, which is the safe default for direct exposure. type trustedProxies []*net.IPNet // localProxyCIDRs are the loopback and private/link-local ranges trusted when // the configuration uses the "local"/"private" keyword — the common case for // an edge proxy running on the same host or LAN. var localProxyCIDRs = []string{ "127.0.0.0/8", "::1/128", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "fc00::/7", "fe80::/10", } func parseTrustedProxies(cidrs []string) (trustedProxies, error) { // Expand keywords first: "local"/"private" -> local ranges; "all"/"*" -> // everything; "none" -> nothing. var expanded []string for _, raw := range cidrs { switch strings.ToLower(strings.TrimSpace(raw)) { case "": continue case "none", "off", "false": return nil, nil case "local", "private": expanded = append(expanded, localProxyCIDRs...) case "all", "any", "*": expanded = append(expanded, "0.0.0.0/0", "::/0") default: expanded = append(expanded, raw) } } var nets trustedProxies for _, raw := range expanded { raw = strings.TrimSpace(raw) if raw == "" { continue } // Bare IPs are accepted as /32 or /128. if !strings.Contains(raw, "/") { if ip := net.ParseIP(raw); ip != nil { if ip.To4() != nil { raw += "/32" } else { raw += "/128" } } } _, ipnet, err := net.ParseCIDR(raw) if err != nil { return nil, err } nets = append(nets, ipnet) } return nets, nil } func (tp trustedProxies) contains(ip net.IP) bool { if ip == nil { return false } for _, n := range tp { if n.Contains(ip) { return true } } return false } // proxyHeadersMiddleware rewrites r.RemoteAddr, r.URL.Scheme, and r.Host from // X-Forwarded-* headers only when the immediate TCP peer sits inside one of // the configured trusted CIDR ranges. Requests arriving from untrusted peers // carry their headers through untouched but are never used for identity or // link-building decisions. If no proxies are configured the middleware is a // no-op and raw RemoteAddr wins, which is the correct behavior for a binary // exposed directly on the network. func proxyHeadersMiddleware(tp trustedProxies) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if len(tp) > 0 { peerIP := remoteIPFromAddr(r.RemoteAddr) if tp.contains(peerIP) { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { if client := firstNonEmpty(splitAndTrim(xff, ",")); client != "" { r.RemoteAddr = client } } else if xrip := strings.TrimSpace(r.Header.Get("X-Real-IP")); xrip != "" { r.RemoteAddr = xrip } if proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); proto != "" { r.URL.Scheme = strings.ToLower(proto) } if host := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")); host != "" { r.Host = host } } } next.ServeHTTP(w, r) }) } } // PublicBaseURL returns the externally-visible base URL (scheme://host) for the // current request. It honors X-Forwarded-* headers only when they were already // applied by proxyHeadersMiddleware upstream, so callers do not need to know // whether a proxy is in front. Useful for building OIDC redirect URIs, email // links, or any absolute URL that must round-trip back to the user. func PublicBaseURL(r *http.Request) string { scheme := r.URL.Scheme if scheme == "" { if r.TLS != nil { scheme = "https" } else { scheme = "http" } } host := r.Host if host == "" { host = r.URL.Host } return scheme + "://" + host } func remoteIPFromAddr(addr string) net.IP { if addr == "" { return nil } if host, _, err := net.SplitHostPort(addr); err == nil { return net.ParseIP(host) } return net.ParseIP(addr) } func splitAndTrim(s, sep string) []string { parts := strings.Split(s, sep) out := make([]string, 0, len(parts)) for _, p := range parts { if t := strings.TrimSpace(p); t != "" { out = append(out, t) } } return out } func firstNonEmpty(parts []string) string { for _, p := range parts { if p != "" { return p } } return "" }