v2.31.0-dev2 4b72742

cidr_test.go RawHistoryFolder

1package middleware
2
3import (
4	"encoding/json"
5	"fmt"
6	"net"
7	"net/http"
8	"net/http/httptest"
9	"testing"
10
11	"github.com/bnema/gordon/internal/adapters/dto"
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: 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: 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: 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: 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: 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: ParseTrustedProxies([]string{"100.64.0.0/10"}),
75			trustedNets: 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: ParseTrustedProxies([]string{"100.64.0.0/10"}),
83			trustedNets: 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)(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)(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)(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_InvalidExplicitPort_IsPreserved(t *testing.T) {
184	log := testLogger()
185	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
186		w.WriteHeader(http.StatusOK)
187	})
188
189	handler := HTTPSRedirect(nil, 8088, 8443, true, log)(ok)
190	srv := httptest.NewServer(handler)
191	defer srv.Close()
192
193	// Use a raw TCP connection because Go's HTTP client rejects the
194	// invalid port in the Location header before CheckRedirect fires.
195	conn, err := net.Dial("tcp", srv.Listener.Addr().String())
196	require.NoError(t, err)
197	defer conn.Close()
198
199	_, err = fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: o2.bnema.dev:abcd\r\nConnection: close\r\n\r\n")
200	require.NoError(t, err)
201
202	var buf [4096]byte
203	n, _ := conn.Read(buf[:])
204	raw := string(buf[:n])
205
206	assert.Contains(t, raw, "308")
207	assert.Contains(t, raw, "Location: https://o2.bnema.dev:abcd/")
208}
209
210func TestProxyCIDRAllowlist(t *testing.T) {
211	log := testLogger()
212	redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
213		http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect)
214	})
215
216	tests := []struct {
217		name        string
218		allowedNets []*net.IPNet
219		remoteAddr  string
220		wantStatus  int
221	}{
222		{
223			name:        "no CIDRs configured passes through",
224			allowedNets: nil,
225			remoteAddr:  "203.0.113.50:1234",
226			wantStatus:  http.StatusPermanentRedirect,
227		},
228		{
229			name:        "allowed IP gets redirect",
230			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
231			remoteAddr:  "173.245.48.1:1234",
232			wantStatus:  http.StatusPermanentRedirect,
233		},
234		{
235			name:        "blocked IP gets 403",
236			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
237			remoteAddr:  "203.0.113.50:1234",
238			wantStatus:  http.StatusForbidden,
239		},
240		{
241			name:        "localhost always allowed",
242			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
243			remoteAddr:  "127.0.0.1:1234",
244			wantStatus:  http.StatusPermanentRedirect,
245		},
246		{
247			name:        "ignores XFF (uses RemoteAddr only)",
248			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
249			remoteAddr:  "203.0.113.50:1234",
250			wantStatus:  http.StatusForbidden,
251		},
252	}
253
254	for _, tt := range tests {
255		t.Run(tt.name, func(t *testing.T) {
256			handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect)
257
258			req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
259			req.RemoteAddr = tt.remoteAddr
260			// Even with XFF from an "allowed" range, the proxy middleware
261			// must check RemoteAddr, not forwarded headers.
262			req.Header.Set("X-Forwarded-For", "173.245.48.1")
263			rr := httptest.NewRecorder()
264
265			handler.ServeHTTP(rr, req)
266
267			assert.Equal(t, tt.wantStatus, rr.Code)
268
269			if tt.wantStatus == http.StatusForbidden {
270				var errResp dto.ErrorResponse
271				require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
272				assert.Equal(t, "Forbidden", errResp.Error)
273			}
274		})
275	}
276}

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