v2.31.0-dev19 35467d7

cidr_test.go RawHistoryFolder

1package middleware
2
3import (
4	"encoding/json"
5	"net"
6	"net/http"
7	"net/http/httptest"
8	"testing"
9
10	"github.com/bnema/gordon/internal/adapters/dto"
11	"github.com/bnema/gordon/internal/adapters/in/http/httphelper"
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/require"
14)
15
16func TestRegistryCIDRAllowlist(t *testing.T) {
17	log := testLogger()
18	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
19		w.WriteHeader(http.StatusOK)
20	})
21
22	tests := []struct {
23		name        string
24		allowedNets []*net.IPNet
25		trustedNets []*net.IPNet
26		remoteAddr  string
27		xff         string
28		wantStatus  int
29	}{
30		{
31			name:        "no CIDRs configured passes through",
32			allowedNets: nil,
33			remoteAddr:  "203.0.113.50:1234",
34			wantStatus:  http.StatusOK,
35		},
36		{
37			name:        "empty CIDRs passes through",
38			allowedNets: []*net.IPNet{},
39			remoteAddr:  "203.0.113.50:1234",
40			wantStatus:  http.StatusOK,
41		},
42		{
43			name:        "allowed IP returns 200",
44			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
45			remoteAddr:  "100.100.1.1:1234",
46			wantStatus:  http.StatusOK,
47		},
48		{
49			name:        "denied IP returns 403",
50			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
51			remoteAddr:  "203.0.113.50:1234",
52			wantStatus:  http.StatusForbidden,
53		},
54		{
55			name:        "localhost always allowed even when not in allowlist",
56			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
57			remoteAddr:  "127.0.0.1:1234",
58			wantStatus:  http.StatusOK,
59		},
60		{
61			name:        "127.x.x.x always allowed",
62			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
63			remoteAddr:  "127.0.0.2:1234",
64			wantStatus:  http.StatusOK,
65		},
66		{
67			name:        "IPv6 localhost always allowed",
68			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
69			remoteAddr:  "[::1]:1234",
70			wantStatus:  http.StatusOK,
71		},
72		{
73			name:        "trusted proxy forwards allowed client IP",
74			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
75			trustedNets: httphelper.ParseTrustedProxies([]string{"10.0.0.1"}),
76			remoteAddr:  "10.0.0.1:1234",
77			xff:         "100.100.1.1",
78			wantStatus:  http.StatusOK,
79		},
80		{
81			name:        "trusted proxy forwards denied client IP",
82			allowedNets: httphelper.ParseTrustedProxies([]string{"100.64.0.0/10"}),
83			trustedNets: httphelper.ParseTrustedProxies([]string{"10.0.0.1"}),
84			remoteAddr:  "10.0.0.1:1234",
85			xff:         "203.0.113.50",
86			wantStatus:  http.StatusForbidden,
87		},
88	}
89
90	for _, tt := range tests {
91		t.Run(tt.name, func(t *testing.T) {
92			handler := RegistryCIDRAllowlist(tt.allowedNets, tt.trustedNets, log)(ok)
93
94			req := httptest.NewRequest(http.MethodGet, "/v2/", nil)
95			req.RemoteAddr = tt.remoteAddr
96			if tt.xff != "" {
97				req.Header.Set("X-Forwarded-For", tt.xff)
98			}
99			rr := httptest.NewRecorder()
100
101			handler.ServeHTTP(rr, req)
102
103			assert.Equal(t, tt.wantStatus, rr.Code)
104
105			if tt.wantStatus == http.StatusForbidden {
106				var errResp dto.ErrorResponse
107				require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
108				assert.Equal(t, "Forbidden", errResp.Error)
109			}
110		})
111	}
112}
113
114func TestHTTPSRedirect_NoPortHost_OmitsTLSPort(t *testing.T) {
115	log := testLogger()
116	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
117		w.WriteHeader(http.StatusOK)
118	})
119
120	handler := HTTPSRedirect(nil, 8088, 8443, true, log, func(host string) bool { return host == "o2.bnema.dev" })(ok)
121	srv := httptest.NewServer(handler)
122	defer srv.Close()
123
124	client := srv.Client()
125	client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
126
127	req, _ := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
128	req.Host = "o2.bnema.dev"
129	resp, err := client.Do(req)
130	require.NoError(t, err)
131	defer resp.Body.Close()
132
133	assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode)
134	assert.Equal(t, "https://o2.bnema.dev/", resp.Header.Get("Location"))
135}
136
137func TestHTTPSRedirect_HTTPListenerPort_MapsToTLSPort(t *testing.T) {
138	log := testLogger()
139	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
140		w.WriteHeader(http.StatusOK)
141	})
142
143	handler := HTTPSRedirect(nil, 8088, 8443, true, log, func(host string) bool { return host == "o2.bnema.dev" })(ok)
144	srv := httptest.NewServer(handler)
145	defer srv.Close()
146
147	client := srv.Client()
148	client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
149
150	req, _ := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
151	req.Host = "o2.bnema.dev:8088"
152	resp, err := client.Do(req)
153	require.NoError(t, err)
154	defer resp.Body.Close()
155
156	assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode)
157	assert.Equal(t, "https://o2.bnema.dev:8443/", resp.Header.Get("Location"))
158}
159
160func TestHTTPSRedirect_UnknownExplicitPort_IsPreserved(t *testing.T) {
161	log := testLogger()
162	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
163		w.WriteHeader(http.StatusOK)
164	})
165
166	handler := HTTPSRedirect(nil, 8088, 8443, true, log, func(host string) bool { return host == "o2.bnema.dev" })(ok)
167	srv := httptest.NewServer(handler)
168	defer srv.Close()
169
170	client := srv.Client()
171	client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
172
173	req, _ := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
174	req.Host = "o2.bnema.dev:9999"
175	resp, err := client.Do(req)
176	require.NoError(t, err)
177	defer resp.Body.Close()
178
179	assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode)
180	assert.Equal(t, "https://o2.bnema.dev:9999/", resp.Header.Get("Location"))
181}
182
183func TestHTTPSRedirect_TrailingDotHostRedirectsToCanonicalHost(t *testing.T) {
184	log := testLogger()
185	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
186		w.WriteHeader(http.StatusOK)
187	})
188	handler := HTTPSRedirect(nil, 8088, 8443, true, log, func(host string) bool { return host == "o2.bnema.dev" })(ok)
189
190	srv := httptest.NewServer(handler)
191	defer srv.Close()
192	client := srv.Client()
193	client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
194
195	req, err := http.NewRequest(http.MethodGet, srv.URL+"/path", nil)
196	require.NoError(t, err)
197	req.Host = "O2.Bnema.Dev.:8088"
198	resp, err := client.Do(req)
199	require.NoError(t, err)
200	defer resp.Body.Close()
201
202	assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode)
203	assert.Equal(t, "https://o2.bnema.dev:8443/path", resp.Header.Get("Location"))
204}
205
206func TestHTTPSRedirect_RejectsInvalidHost(t *testing.T) {
207	log := testLogger()
208	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
209		w.WriteHeader(http.StatusOK)
210	})
211	handler := HTTPSRedirect(nil, 8088, 8443, true, log, func(host string) bool { return host == "o2.bnema.dev" })(ok)
212
213	tests := []struct {
214		name string
215		host string
216	}{
217		{name: "invalid port", host: "o2.bnema.dev:abcd"},
218
219		{name: "localhost", host: "localhost"},
220		{name: "ipv6", host: "[::1]:8088"},
221		{name: "unconfigured public host", host: "evil.example.com"},
222	}
223
224	for _, tt := range tests {
225		t.Run(tt.name, func(t *testing.T) {
226			srv := httptest.NewServer(handler)
227			defer srv.Close()
228			req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
229			require.NoError(t, err)
230			req.Host = tt.host
231			resp, err := srv.Client().Do(req)
232			require.NoError(t, err)
233			defer resp.Body.Close()
234
235			assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
236			assert.Empty(t, resp.Header.Get("Location"))
237		})
238	}
239}
240
241func TestProxyCIDRAllowlist(t *testing.T) {
242	log := testLogger()
243	redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
244		http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect)
245	})
246
247	tests := []struct {
248		name        string
249		allowedNets []*net.IPNet
250		remoteAddr  string
251		wantStatus  int
252	}{
253		{
254			name:        "no CIDRs configured passes through",
255			allowedNets: nil,
256			remoteAddr:  "203.0.113.50:1234",
257			wantStatus:  http.StatusPermanentRedirect,
258		},
259		{
260			name:        "allowed IP gets redirect",
261			allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
262			remoteAddr:  "173.245.48.1:1234",
263			wantStatus:  http.StatusPermanentRedirect,
264		},
265		{
266			name:        "blocked IP gets 403",
267			allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
268			remoteAddr:  "203.0.113.50:1234",
269			wantStatus:  http.StatusForbidden,
270		},
271		{
272			name:        "localhost always allowed",
273			allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
274			remoteAddr:  "127.0.0.1:1234",
275			wantStatus:  http.StatusPermanentRedirect,
276		},
277		{
278			name:        "ignores XFF (uses RemoteAddr only)",
279			allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
280			remoteAddr:  "203.0.113.50:1234",
281			wantStatus:  http.StatusForbidden,
282		},
283	}
284
285	for _, tt := range tests {
286		t.Run(tt.name, func(t *testing.T) {
287			handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect)
288
289			req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
290			req.RemoteAddr = tt.remoteAddr
291			// Even with XFF from an "allowed" range, the proxy middleware
292			// must check RemoteAddr, not forwarded headers.
293			req.Header.Set("X-Forwarded-For", "173.245.48.1")
294			rr := httptest.NewRecorder()
295
296			handler.ServeHTTP(rr, req)
297
298			assert.Equal(t, tt.wantStatus, rr.Code)
299
300			if tt.wantStatus == http.StatusForbidden {
301				var errResp dto.ErrorResponse
302				require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
303				assert.Equal(t, "Forbidden", errResp.Error)
304			}
305		})
306	}
307}

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