v2.16.0 ebd2ab3

clientip.go RawHistoryFolder

1package middleware
2
3import (
4	"net"
5	"net/http"
6	"strings"
7)
8
9func normalizeIP(raw string) string {
10	ip := strings.TrimSpace(raw)
11	if ip == "" {
12		return ""
13	}
14
15	if host, _, err := net.SplitHostPort(ip); err == nil {
16		ip = host
17	}
18
19	parsed := net.ParseIP(strings.TrimSpace(ip))
20	if parsed == nil {
21		return ""
22	}
23
24	return parsed.String()
25}
26
27// ParseTrustedProxies converts a list of IP addresses and CIDR ranges to net.IPNet.
28// It accepts both single IPs (e.g., "10.0.0.1") and CIDR notation (e.g., "10.0.0.0/8").
29// Single IPs are converted to /32 (IPv4) or /128 (IPv6) CIDR blocks.
30func ParseTrustedProxies(proxies []string) []*net.IPNet {
31	var nets []*net.IPNet
32	for _, proxy := range proxies {
33		// Try parsing as CIDR
34		_, ipNet, err := net.ParseCIDR(proxy)
35		if err == nil {
36			nets = append(nets, ipNet)
37			continue
38		}
39		// Try parsing as single IP
40		ip := net.ParseIP(proxy)
41		if ip != nil {
42			// Convert single IP to /32 or /128 CIDR
43			bits := 32
44			if ip.To4() == nil {
45				bits = 128
46			}
47			nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
48		}
49	}
50	return nets
51}
52
53// IsTrustedProxy checks if the given IP address is from a trusted proxy.
54// Returns false if trustedNets is empty or the IP cannot be parsed.
55func IsTrustedProxy(ip string, trustedNets []*net.IPNet) bool {
56	if len(trustedNets) == 0 {
57		return false
58	}
59	parsedIP := net.ParseIP(ip)
60	if parsedIP == nil {
61		return false
62	}
63	for _, ipNet := range trustedNets {
64		if ipNet.Contains(parsedIP) {
65			return true
66		}
67	}
68	return false
69}
70
71// GetClientIP extracts the client IP address from the request.
72// It only honors X-Forwarded-For and X-Real-IP headers when the request
73// originates from a trusted proxy. This prevents attackers from spoofing
74// their IP address when Gordon is exposed directly without a reverse proxy.
75func GetClientIP(r *http.Request, trustedNets []*net.IPNet) string {
76	// Extract the direct connection IP from RemoteAddr
77	remoteIP, _, err := net.SplitHostPort(r.RemoteAddr)
78	if err != nil {
79		remoteIP = r.RemoteAddr
80	}
81	if normalized := normalizeIP(remoteIP); normalized != "" {
82		remoteIP = normalized
83	}
84
85	// Only honor proxy headers if the request comes from a trusted proxy
86	if IsTrustedProxy(remoteIP, trustedNets) {
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 !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