v2.30.0 3ee801c

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/stretchr/testify/assert"
12	"github.com/stretchr/testify/require"
13)
14
15func TestRegistryCIDRAllowlist(t *testing.T) {
16	log := testLogger()
17	ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
18		w.WriteHeader(http.StatusOK)
19	})
20
21	tests := []struct {
22		name        string
23		allowedNets []*net.IPNet
24		trustedNets []*net.IPNet
25		remoteAddr  string
26		xff         string
27		wantStatus  int
28	}{
29		{
30			name:        "no CIDRs configured passes through",
31			allowedNets: nil,
32			remoteAddr:  "203.0.113.50:1234",
33			wantStatus:  http.StatusOK,
34		},
35		{
36			name:        "empty CIDRs passes through",
37			allowedNets: []*net.IPNet{},
38			remoteAddr:  "203.0.113.50:1234",
39			wantStatus:  http.StatusOK,
40		},
41		{
42			name:        "allowed IP returns 200",
43			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
44			remoteAddr:  "100.100.1.1:1234",
45			wantStatus:  http.StatusOK,
46		},
47		{
48			name:        "denied IP returns 403",
49			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
50			remoteAddr:  "203.0.113.50:1234",
51			wantStatus:  http.StatusForbidden,
52		},
53		{
54			name:        "localhost always allowed even when not in allowlist",
55			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
56			remoteAddr:  "127.0.0.1:1234",
57			wantStatus:  http.StatusOK,
58		},
59		{
60			name:        "127.x.x.x always allowed",
61			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
62			remoteAddr:  "127.0.0.2:1234",
63			wantStatus:  http.StatusOK,
64		},
65		{
66			name:        "IPv6 localhost always allowed",
67			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
68			remoteAddr:  "[::1]:1234",
69			wantStatus:  http.StatusOK,
70		},
71		{
72			name:        "trusted proxy forwards allowed client IP",
73			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
74			trustedNets: ParseTrustedProxies([]string{"10.0.0.1"}),
75			remoteAddr:  "10.0.0.1:1234",
76			xff:         "100.100.1.1",
77			wantStatus:  http.StatusOK,
78		},
79		{
80			name:        "trusted proxy forwards denied client IP",
81			allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}),
82			trustedNets: ParseTrustedProxies([]string{"10.0.0.1"}),
83			remoteAddr:  "10.0.0.1:1234",
84			xff:         "203.0.113.50",
85			wantStatus:  http.StatusForbidden,
86		},
87	}
88
89	for _, tt := range tests {
90		t.Run(tt.name, func(t *testing.T) {
91			handler := RegistryCIDRAllowlist(tt.allowedNets, tt.trustedNets, log)(ok)
92
93			req := httptest.NewRequest(http.MethodGet, "/v2/", nil)
94			req.RemoteAddr = tt.remoteAddr
95			if tt.xff != "" {
96				req.Header.Set("X-Forwarded-For", tt.xff)
97			}
98			rr := httptest.NewRecorder()
99
100			handler.ServeHTTP(rr, req)
101
102			assert.Equal(t, tt.wantStatus, rr.Code)
103
104			if tt.wantStatus == http.StatusForbidden {
105				var errResp dto.ErrorResponse
106				require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
107				assert.Equal(t, "Forbidden", errResp.Error)
108			}
109		})
110	}
111}
112
113func TestProxyCIDRAllowlist(t *testing.T) {
114	log := testLogger()
115	redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116		http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect)
117	})
118
119	tests := []struct {
120		name        string
121		allowedNets []*net.IPNet
122		remoteAddr  string
123		wantStatus  int
124	}{
125		{
126			name:        "no CIDRs configured passes through",
127			allowedNets: nil,
128			remoteAddr:  "203.0.113.50:1234",
129			wantStatus:  http.StatusPermanentRedirect,
130		},
131		{
132			name:        "allowed IP gets redirect",
133			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
134			remoteAddr:  "173.245.48.1:1234",
135			wantStatus:  http.StatusPermanentRedirect,
136		},
137		{
138			name:        "blocked IP gets 403",
139			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
140			remoteAddr:  "203.0.113.50:1234",
141			wantStatus:  http.StatusForbidden,
142		},
143		{
144			name:        "localhost always allowed",
145			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
146			remoteAddr:  "127.0.0.1:1234",
147			wantStatus:  http.StatusPermanentRedirect,
148		},
149		{
150			name:        "ignores XFF (uses RemoteAddr only)",
151			allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}),
152			remoteAddr:  "203.0.113.50:1234",
153			wantStatus:  http.StatusForbidden,
154		},
155	}
156
157	for _, tt := range tests {
158		t.Run(tt.name, func(t *testing.T) {
159			handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect)
160
161			req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
162			req.RemoteAddr = tt.remoteAddr
163			// Even with XFF from an "allowed" range, the proxy middleware
164			// must check RemoteAddr, not forwarded headers.
165			req.Header.Set("X-Forwarded-For", "173.245.48.1")
166			rr := httptest.NewRecorder()
167
168			handler.ServeHTTP(rr, req)
169
170			assert.Equal(t, tt.wantStatus, rr.Code)
171
172			if tt.wantStatus == http.StatusForbidden {
173				var errResp dto.ErrorResponse
174				require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
175				assert.Equal(t, "Forbidden", errResp.Error)
176			}
177		})
178	}
179}

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