package httpapi import ( "net" "net/http" "strings" ) // extractClientIP extracts and validates the client's IP address from the HTTP request. // It inspects standard reverse proxy and CDN headers in priority order before falling // back to the remote socket address. Returns nil if no valid IP address can be found. func extractClientIP(r *http.Request) *string { if r == nil { return nil } // Priority headers commonly set by reverse proxies, load balancers, and CDNs. headers := []string{ "CF-Connecting-IP", // Cloudflare "True-Client-IP", // Cloudflare Enterprise, Akamai "X-Real-IP", // Nginx, Traefik } for _, header := range headers { if val := strings.TrimSpace(r.Header.Get(header)); val != "" { if ip := parseIP(val); ip != "" { return &ip } } } // X-Forwarded-For can contain multiple comma-separated IPs: client, proxy1, proxy2. // The original client IP is the first (leftmost) entry. if xff := r.Header.Get("X-Forwarded-For"); xff != "" { for _, part := range strings.Split(xff, ",") { if ip := parseIP(part); ip != "" { return &ip } } } // Fallback to RemoteAddr (typically "host:port" or "[ipv6]:port"). if r.RemoteAddr != "" { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { host = r.RemoteAddr } if ip := parseIP(host); ip != "" { return &ip } } return nil } func parseIP(raw string) string { raw = strings.TrimSpace(raw) raw = strings.TrimPrefix(raw, "[") raw = strings.TrimSuffix(raw, "]") ip := net.ParseIP(raw) if ip == nil { return "" } return ip.String() }