v3.4.1 de8f68e

clientip.go RawHistoryFolder

1package middleware
2
3import (
4	"net"
5	"net/http"
6	"strings"
7
8	"github.com/bnema/gordon/internal/adapters/in/http/httphelper"
9)
10
11// cloudflareNets contains Cloudflare's published IPv4 and IPv6 ranges.
12// Cf-Connecting-Ip is only trusted when the immediate upstream matches these.
13// Source: https://www.cloudflare.com/ips/
14var cloudflareNets = httphelper.ParseTrustedProxies([]string{
15	// IPv4
16	"173.245.48.0/20",
17	"103.21.244.0/22",
18	"103.22.200.0/22",
19	"103.31.4.0/22",
20	"141.101.64.0/18",
21	"108.162.192.0/18",
22	"190.93.240.0/20",
23	"188.114.96.0/20",
24	"197.234.240.0/22",
25	"198.41.128.0/17",
26	"162.158.0.0/15",
27	"104.16.0.0/13",
28	"104.24.0.0/14",
29	"172.64.0.0/13",
30	"131.0.72.0/22",
31	// IPv6
32	"2400:cb00::/32",
33	"2606:4700::/32",
34	"2803:f800::/32",
35	"2405:b500::/32",
36	"2405:8100::/32",
37	"2a06:98c0::/29",
38	"2c0f:f248::/32",
39})
40
41func normalizeIP(raw string) string {
42	ip := strings.TrimSpace(raw)
43	if ip == "" {
44		return ""
45	}
46
47	if host, _, err := net.SplitHostPort(ip); err == nil {
48		ip = host
49	}
50
51	parsed := net.ParseIP(ip)
52	if parsed == nil {
53		return ""
54	}
55
56	return parsed.String()
57}
58
59// GetClientIP extracts the client IP address from the request.
60// It only honors X-Forwarded-For and X-Real-IP headers when the request
61// originates from a trusted proxy. This prevents attackers from spoofing
62// their IP address when Gordon is exposed directly without a reverse proxy.
63func GetClientIP(r *http.Request, trustedNets []*net.IPNet) string {
64	// Extract the direct connection IP from RemoteAddr
65	remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
66	if err != nil {
67		remoteIP = r.RemoteAddr
68	}
69	if normalized := normalizeIP(remoteIP); normalized != "" {
70		remoteIP = normalized
71	}
72
73	// Only honor proxy headers if the request comes from a trusted proxy
74	if httphelper.IsTrustedProxy(remoteIP, trustedNets) {
75		// Cf-Connecting-IP is set by Cloudflare to the true client IP.
76		// Only trust it when the immediate upstream is a known Cloudflare IP,
77		// not any generic trusted proxy (e.g. local LB/nginx), because a
78		// non-Cloudflare proxy would blindly forward a spoofed header.
79		if httphelper.IsTrustedProxy(remoteIP, cloudflareNets) {
80			if cfIP := r.Header.Get("Cf-Connecting-Ip"); cfIP != "" {
81				if ip := normalizeIP(cfIP); ip != "" {
82					return ip
83				}
84			}
85		}
86
87		if ip := forwardedForClientIP(r, trustedNets); ip != "" {
88			return ip
89		}
90
91		if ip := realIPHeader(r); ip != "" {
92			return ip
93		}
94	}
95
96	// Fall back to RemoteAddr (direct connection IP)
97	return remoteIP
98}
99
100// forwardedForClientIP returns the right-most untrusted X-Forwarded-For hop.
101// SECURITY: Parse from right to left so a trusted proxy appending to an
102// attacker-supplied chain cannot be spoofed. All header lines are joined, since
103// some proxies add a new X-Forwarded-For line instead of appending to the last.
104func forwardedForClientIP(r *http.Request, trustedNets []*net.IPNet) string {
105	values := r.Header.Values("X-Forwarded-For")
106	if len(values) == 0 {
107		return ""
108	}
109
110	parts := strings.Split(strings.Join(values, ","), ",")
111	validChain := make([]string, 0, len(parts))
112	for _, part := range parts {
113		if ip := normalizeIP(part); ip != "" {
114			validChain = append(validChain, ip)
115		}
116	}
117
118	for i := len(validChain) - 1; i >= 0; i-- {
119		if !httphelper.IsTrustedProxy(validChain[i], trustedNets) {
120			return validChain[i]
121		}
122	}
123
124	if len(validChain) > 0 {
125		return validChain[0]
126	}
127	return ""
128}
129
130// realIPHeader returns X-Real-IP only when it is set exactly once, since a
131// repeated header means one of the values may come from the client.
132func realIPHeader(r *http.Request) string {
133	values := r.Header.Values("X-Real-IP")
134	if len(values) != 1 {
135		return ""
136	}
137	return normalizeIP(values[0])
138}
139
140// IsTrustedCloudflarePeer reports whether the request's immediate peer is both
141// a trusted proxy and a published Cloudflare address. Only then does
142// Cf-Connecting-Ip carry a client IP set by Cloudflare rather than by the client.
143func IsTrustedCloudflarePeer(r *http.Request, trustedNets []*net.IPNet) bool {
144	return httphelper.IsTrustedSource(r, trustedNets) && httphelper.IsTrustedSource(r, cloudflareNets)
145}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close