| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package ratelimit |
| 4 | |
| 5 | import ( |
| 6 | "net" |
| 7 | "net/http" |
| 8 | "net/netip" |
| 9 | "strings" |
| 10 | ) |
| 11 | |
| 12 | // IPKey is the canonical anonymous-request keyer: extracts the |
| 13 | // client IP from r.RemoteAddr or the X-Forwarded-For header. The |
| 14 | // trust-XFF flag should be set ONLY when the deployment runs |
| 15 | // behind a CDN/proxy we control; otherwise an attacker can spoof |
| 16 | // the header and dodge IP-keyed limits. |
| 17 | func IPKey(trustForwarded bool) KeyFunc { |
| 18 | return func(r *http.Request) string { |
| 19 | if trustForwarded { |
| 20 | if v := r.Header.Get("X-Forwarded-For"); v != "" { |
| 21 | // First entry is the client; downstream proxies append. |
| 22 | if comma := strings.IndexByte(v, ','); comma > 0 { |
| 23 | v = v[:comma] |
| 24 | } |
| 25 | return "ip:" + strings.TrimSpace(v) |
| 26 | } |
| 27 | } |
| 28 | host, _, err := net.SplitHostPort(r.RemoteAddr) |
| 29 | if err != nil { |
| 30 | host = r.RemoteAddr |
| 31 | } |
| 32 | return "ip:" + host |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | // ClientIP returns the parsed client IP using the same rules as |
| 37 | // IPKey. Used by signup-throttle's CIDR keying. |
| 38 | func ClientIP(r *http.Request, trustForwarded bool) (netip.Addr, bool) { |
| 39 | raw := "" |
| 40 | if trustForwarded { |
| 41 | if v := r.Header.Get("X-Forwarded-For"); v != "" { |
| 42 | if comma := strings.IndexByte(v, ','); comma > 0 { |
| 43 | v = v[:comma] |
| 44 | } |
| 45 | raw = strings.TrimSpace(v) |
| 46 | } |
| 47 | } |
| 48 | if raw == "" { |
| 49 | host, _, err := net.SplitHostPort(r.RemoteAddr) |
| 50 | if err == nil { |
| 51 | raw = host |
| 52 | } else { |
| 53 | raw = r.RemoteAddr |
| 54 | } |
| 55 | } |
| 56 | addr, err := netip.ParseAddr(raw) |
| 57 | if err != nil { |
| 58 | return netip.Addr{}, false |
| 59 | } |
| 60 | return addr, true |
| 61 | } |
| 62 |