v2.31.0-dev6 8056b57

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

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