v2.31.0-dev9 062dcae

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		// Check X-Forwarded-For for proxied requests.
88		// SECURITY: Parse from right to left and return the first untrusted IP.
89		// This prevents spoofing when a trusted proxy appends to an attacker-
90		// supplied XFF chain.
91		if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
92			parts := strings.Split(xff, ",")
93			validChain := make([]string, 0, len(parts))
94			for _, part := range parts {
95				if ip := normalizeIP(part); ip != "" {
96					validChain = append(validChain, ip)
97				}
98			}
99
100			for i := len(validChain) - 1; i >= 0; i-- {
101				if !httphelper.IsTrustedProxy(validChain[i], trustedNets) {
102					return validChain[i]
103				}
104			}
105
106			if len(validChain) > 0 {
107				return validChain[0]
108			}
109		}
110
111		// Check X-Real-IP
112		if xri := r.Header.Get("X-Real-IP"); xri != "" {
113			if ip := normalizeIP(xri); ip != "" {
114				return ip
115			}
116		}
117	}
118
119	// Fall back to RemoteAddr (direct connection IP)
120	return remoteIP
121}

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