v2.30.0 3ee801c

clientip_test.go RawHistoryFolder

1package middleware
2
3import (
4	"net"
5	"net/http"
6	"net/http/httptest"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10)
11
12func TestGetClientIP(t *testing.T) {
13	// Parse trusted proxies for tests
14	trustedNets := ParseTrustedProxies([]string{"127.0.0.1", "10.0.0.0/8"})
15	noTrustedNets := []*net.IPNet{}
16	// Include a Cloudflare IP in trusted proxies to test Cf-Connecting-Ip
17	trustedWithCF := ParseTrustedProxies([]string{"127.0.0.1", "10.0.0.0/8", "173.245.48.0/20"})
18
19	tests := []struct {
20		name        string
21		remoteAddr  string
22		xff         string
23		xRealIP     string
24		cfIP        string
25		trustedNets []*net.IPNet
26		wantIP      string
27	}{
28		{
29			name:        "from RemoteAddr with port",
30			remoteAddr:  "192.168.1.100:12345",
31			trustedNets: noTrustedNets,
32			wantIP:      "192.168.1.100",
33		},
34		{
35			name:        "from RemoteAddr without port",
36			remoteAddr:  "192.168.1.100",
37			trustedNets: noTrustedNets,
38			wantIP:      "192.168.1.100",
39		},
40		{
41			name:        "XFF ignored when no trusted proxies",
42			remoteAddr:  "192.168.1.100:12345",
43			xff:         "203.0.113.50",
44			trustedNets: noTrustedNets,
45			wantIP:      "192.168.1.100",
46		},
47		{
48			name:        "XFF ignored when remote is not trusted",
49			remoteAddr:  "192.168.1.100:12345",
50			xff:         "203.0.113.50",
51			trustedNets: trustedNets, // trusts 127.0.0.1 and 10.0.0.0/8
52			wantIP:      "192.168.1.100",
53		},
54		{
55			name:        "XFF honored from trusted proxy (single IP)",
56			remoteAddr:  "127.0.0.1:12345",
57			xff:         "203.0.113.50",
58			trustedNets: trustedNets,
59			wantIP:      "203.0.113.50",
60		},
61		{
62			name:        "XFF honored from trusted proxy (multiple IPs)",
63			remoteAddr:  "127.0.0.1:12345",
64			xff:         "203.0.113.50, 10.0.0.1",
65			trustedNets: trustedNets,
66			wantIP:      "203.0.113.50",
67		},
68		{
69			name:        "XFF spoof chain returns first untrusted from right",
70			remoteAddr:  "127.0.0.1:12345",
71			xff:         "198.51.100.123, 203.0.113.50, 10.0.0.9",
72			trustedNets: trustedNets,
73			wantIP:      "203.0.113.50",
74		},
75		{
76			name:        "XFF honored from trusted CIDR",
77			remoteAddr:  "10.1.2.3:12345",
78			xff:         "203.0.113.50",
79			trustedNets: trustedNets,
80			wantIP:      "203.0.113.50",
81		},
82		{
83			name:        "X-Real-IP honored from trusted proxy",
84			remoteAddr:  "127.0.0.1:12345",
85			xRealIP:     "203.0.113.50",
86			trustedNets: trustedNets,
87			wantIP:      "203.0.113.50",
88		},
89		{
90			name:        "X-Real-IP ignored when not trusted",
91			remoteAddr:  "192.168.1.100:12345",
92			xRealIP:     "203.0.113.50",
93			trustedNets: trustedNets,
94			wantIP:      "192.168.1.100",
95		},
96		{
97			name:        "XFF takes precedence over X-Real-IP",
98			remoteAddr:  "127.0.0.1:12345",
99			xff:         "203.0.113.50",
100			xRealIP:     "203.0.113.60",
101			trustedNets: trustedNets,
102			wantIP:      "203.0.113.50",
103		},
104		{
105			name:        "Cf-Connecting-Ip honored from Cloudflare IP",
106			remoteAddr:  "173.245.48.1:12345",
107			cfIP:        "203.0.113.99",
108			trustedNets: trustedWithCF,
109			wantIP:      "203.0.113.99",
110		},
111		{
112			name:        "Cf-Connecting-Ip ignored from non-Cloudflare trusted proxy",
113			remoteAddr:  "127.0.0.1:12345",
114			cfIP:        "203.0.113.99",
115			xff:         "203.0.113.50",
116			trustedNets: trustedNets,
117			wantIP:      "203.0.113.50",
118		},
119		{
120			name:        "Cf-Connecting-Ip ignored from untrusted IP",
121			remoteAddr:  "192.168.1.100:12345",
122			cfIP:        "203.0.113.99",
123			trustedNets: trustedNets,
124			wantIP:      "192.168.1.100",
125		},
126		{
127			name:        "IPv6 RemoteAddr",
128			remoteAddr:  "[::1]:12345",
129			trustedNets: noTrustedNets,
130			wantIP:      "::1",
131		},
132	}
133
134	for _, tt := range tests {
135		t.Run(tt.name, func(t *testing.T) {
136			req := httptest.NewRequest(http.MethodGet, "/", nil)
137			req.RemoteAddr = tt.remoteAddr
138			if tt.xff != "" {
139				req.Header.Set("X-Forwarded-For", tt.xff)
140			}
141			if tt.xRealIP != "" {
142				req.Header.Set("X-Real-IP", tt.xRealIP)
143			}
144			if tt.cfIP != "" {
145				req.Header.Set("Cf-Connecting-Ip", tt.cfIP)
146			}
147
148			ip := GetClientIP(req, tt.trustedNets)
149			assert.Equal(t, tt.wantIP, ip)
150		})
151	}
152}
153
154func TestParseTrustedProxies(t *testing.T) {
155	tests := []struct {
156		name    string
157		proxies []string
158		testIP  string
159		want    bool
160	}{
161		{
162			name:    "empty list",
163			proxies: []string{},
164			testIP:  "192.168.1.1",
165			want:    false,
166		},
167		{
168			name:    "single IP match",
169			proxies: []string{"192.168.1.1"},
170			testIP:  "192.168.1.1",
171			want:    true,
172		},
173		{
174			name:    "single IP no match",
175			proxies: []string{"192.168.1.1"},
176			testIP:  "192.168.1.2",
177			want:    false,
178		},
179		{
180			name:    "CIDR match",
181			proxies: []string{"10.0.0.0/8"},
182			testIP:  "10.1.2.3",
183			want:    true,
184		},
185		{
186			name:    "CIDR no match",
187			proxies: []string{"10.0.0.0/8"},
188			testIP:  "192.168.1.1",
189			want:    false,
190		},
191		{
192			name:    "mixed IP and CIDR",
193			proxies: []string{"127.0.0.1", "10.0.0.0/8", "172.16.0.0/12"},
194			testIP:  "172.20.1.1",
195			want:    true,
196		},
197		{
198			name:    "invalid entries ignored",
199			proxies: []string{"not-an-ip", "10.0.0.0/8"},
200			testIP:  "10.1.2.3",
201			want:    true,
202		},
203		{
204			name:    "IPv6 single IP",
205			proxies: []string{"::1"},
206			testIP:  "::1",
207			want:    true,
208		},
209		{
210			name:    "IPv6 CIDR",
211			proxies: []string{"fd00::/8"},
212			testIP:  "fd12:3456::1",
213			want:    true,
214		},
215	}
216
217	for _, tt := range tests {
218		t.Run(tt.name, func(t *testing.T) {
219			nets := ParseTrustedProxies(tt.proxies)
220			got := IsTrustedProxy(tt.testIP, nets)
221			assert.Equal(t, tt.want, got)
222		})
223	}
224}
225
226func TestIsTrustedProxy(t *testing.T) {
227	tests := []struct {
228		name        string
229		ip          string
230		trustedNets []*net.IPNet
231		want        bool
232	}{
233		{
234			name:        "empty trusted nets",
235			ip:          "192.168.1.1",
236			trustedNets: []*net.IPNet{},
237			want:        false,
238		},
239		{
240			name:        "nil trusted nets",
241			ip:          "192.168.1.1",
242			trustedNets: nil,
243			want:        false,
244		},
245		{
246			name:        "invalid IP",
247			ip:          "not-an-ip",
248			trustedNets: ParseTrustedProxies([]string{"192.168.1.0/24"}),
249			want:        false,
250		},
251		{
252			name:        "empty IP",
253			ip:          "",
254			trustedNets: ParseTrustedProxies([]string{"192.168.1.0/24"}),
255			want:        false,
256		},
257	}
258
259	for _, tt := range tests {
260		t.Run(tt.name, func(t *testing.T) {
261			got := IsTrustedProxy(tt.ip, tt.trustedNets)
262			assert.Equal(t, tt.want, got)
263		})
264	}
265}

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