v2.31.0-dev21 b59533b

clientip_test.go RawHistoryFolder

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

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