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}