package httpmiddleware import ( "net" "net/http" "os" "strings" ) // ClientIP returns the best-known client IP for a request. // // Forwarded headers are only trusted when the immediate remote address belongs // to an explicitly trusted proxy listed in TRUST_PROXY_IPS and/or // TRUST_PROXY_CIDRS. func ClientIP(r *http.Request) string { remoteIP := parseRemoteIP(r.RemoteAddr) if remoteIP == "" { remoteIP = strings.TrimSpace(r.RemoteAddr) } if !isTrustedProxy(remoteIP) { return remoteIP } if forwarded := forwardedClientIP(r); forwarded != "" { return forwarded } return remoteIP } func parseRemoteIP(raw string) string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return "" } if host, _, err := net.SplitHostPort(trimmed); err == nil { return host } if ip := net.ParseIP(trimmed); ip != nil { return ip.String() } return trimmed } func forwardedClientIP(r *http.Request) string { for _, header := range []string{"X-Forwarded-For", "X-Real-IP"} { raw := strings.TrimSpace(r.Header.Get(header)) if raw == "" { continue } if header == "X-Forwarded-For" { for _, part := range strings.Split(raw, ",") { candidate := strings.TrimSpace(part) if ip := net.ParseIP(candidate); ip != nil { return ip.String() } } continue } if ip := net.ParseIP(raw); ip != nil { return ip.String() } } return "" } func isTrustedProxy(remoteIP string) bool { ip := net.ParseIP(strings.TrimSpace(remoteIP)) if ip == nil { return false } for _, exact := range splitEnvList("TRUST_PROXY_IPS") { if trusted := net.ParseIP(exact); trusted != nil && trusted.Equal(ip) { return true } } for _, cidr := range splitEnvList("TRUST_PROXY_CIDRS") { _, network, err := net.ParseCIDR(cidr) if err == nil && network.Contains(ip) { return true } } return false } func splitEnvList(key string) []string { raw := strings.TrimSpace(os.Getenv(key)) if raw == "" { return nil } parts := strings.Split(raw, ",") values := make([]string, 0, len(parts)) for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { values = append(values, trimmed) } } return values }