package requestip import ( "net" "net/http" "strings" ) // FromRequest returns the best-effort client IP/host for a request, preferring // proxy headers before falling back to RemoteAddr. // // Header precedence: // 1) X-Real-IP // 2) X-Forwarded-For (first value) // 3) Forwarded (for=...) // 4) RemoteAddr (host part) func FromRequest(r *http.Request) string { if r == nil { return "" } if v := firstAddressToken(r.Header.Get("X-Real-IP")); v != "" { return stripPort(v) } if v := firstAddressToken(r.Header.Get("X-Forwarded-For")); v != "" { return stripPort(v) } if v := forwardedForValue(r.Header.Get("Forwarded")); v != "" { return stripPort(v) } return stripPort(strings.TrimSpace(r.RemoteAddr)) } func firstAddressToken(v string) string { if v == "" { return "" } part := strings.TrimSpace(strings.Split(v, ",")[0]) part = strings.Trim(part, `"`) return strings.TrimSpace(part) } func forwardedForValue(v string) string { for _, part := range strings.Split(v, ",") { for _, kv := range strings.Split(part, ";") { k, raw, ok := strings.Cut(strings.TrimSpace(kv), "=") if !ok || !strings.EqualFold(strings.TrimSpace(k), "for") { continue } candidate := strings.Trim(strings.TrimSpace(raw), `"`) if candidate == "" { continue } return candidate } } return "" } func stripPort(addr string) string { addr = strings.TrimSpace(addr) if addr == "" { return "" } // RFC 7239 quoted values may wrap IPv6 with brackets. addr = strings.Trim(addr, "[]") host, _, err := net.SplitHostPort(addr) if err == nil { return host } return addr }