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 == "app.example.com" })(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 = "app.example.com"
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://app.example.com/", 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 == "app.example.com" })(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 = "app.example.com: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://app.example.com: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 == "app.example.com" })(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 = "app.example.com: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://app.example.com: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 == "app.example.com" })(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 = "App.Example.Com.: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://app.example.com: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 == "app.example.com" })(ok)
212
213 tests := []struct {
214 name string
215 host string
216 }{
217 {name: "invalid port", host: "app.example.com: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
241// TestHTTPSRedirect_TLSAlwaysNeverServesPlaintext proves a tls=always host
242// is redirected when an HTTPS endpoint exists and refused when it does not:
243// the backend is never reached over plaintext.
244func TestHTTPSRedirect_TLSAlwaysNeverServesPlaintext(t *testing.T) {
245 log := testLogger()
246 served := false
247 backend := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
248 served = true
249 w.WriteHeader(http.StatusOK)
250 })
251 isHostAllowed := func(host string) bool { return host == "secure.example.com" }
252 isTLSAlways := func(host string) bool { return host == "secure.example.com" }
253
254 redirected := HTTPSRedirectWithEligibility(nil, 8088, 8443, false, log, isHostAllowed, nil, isTLSAlways)(backend)
255 srv := httptest.NewServer(redirected)
256 defer srv.Close()
257 client := srv.Client()
258 client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
259
260 req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
261 require.NoError(t, err)
262 req.Host = "secure.example.com"
263 resp, err := client.Do(req)
264 require.NoError(t, err)
265 defer resp.Body.Close()
266 assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode)
267 assert.Equal(t, "https://secure.example.com/", resp.Header.Get("Location"))
268 assert.False(t, served, "the plaintext backend must never be reached")
269
270 // No HTTPS endpoint configured: the request is refused, not served.
271 served = false
272 refused := HTTPSRedirectWithEligibility(nil, 8088, 0, false, log, isHostAllowed, nil, isTLSAlways)(backend)
273 srv2 := httptest.NewServer(refused)
274 defer srv2.Close()
275 req2, err := http.NewRequest(http.MethodGet, srv2.URL+"/", nil)
276 require.NoError(t, err)
277 req2.Host = "secure.example.com"
278 refusedClient := srv2.Client()
279 refusedClient.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
280 resp2, err := refusedClient.Do(req2)
281 require.NoError(t, err)
282 defer resp2.Body.Close()
283 assert.Equal(t, http.StatusMisdirectedRequest, resp2.StatusCode)
284 assert.False(t, served, "plaintext must not be served without an HTTPS endpoint")
285}
286
287// TestHTTPSRedirect_TLSAlwaysIgnoresDisabledRedirectConfig proves tls=always
288// still redirects when redirects are otherwise disabled for untrusted
289// clients.
290func TestHTTPSRedirect_TLSAlwaysIgnoresDisabledRedirectConfig(t *testing.T) {
291 log := testLogger()
292 served := false
293 backend := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
294 served = true
295 w.WriteHeader(http.StatusOK)
296 })
297 isHostAllowed := func(host string) bool { return host == "secure.example.com" }
298 isTLSAlways := func(host string) bool { return host == "secure.example.com" }
299
300 handler := HTTPSRedirectWithEligibility(nil, 8088, 8443, false, log, isHostAllowed, nil, isTLSAlways)(backend)
301 srv := httptest.NewServer(handler)
302 defer srv.Close()
303 client := srv.Client()
304 client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
305
306 req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
307 require.NoError(t, err)
308 req.Host = "secure.example.com"
309 resp, err := client.Do(req)
310 require.NoError(t, err)
311 defer resp.Body.Close()
312 assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode)
313 assert.False(t, served)
314}
315
316func TestHTTPSRedirect_IneligibleKnownHostPassesThrough(t *testing.T) {
317 log := testLogger()
318 ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
319 w.WriteHeader(http.StatusOK)
320 })
321 // plain.example.com is known but opted out of TLS (tls=never).
322 handler := HTTPSRedirectWithEligibility(nil, 8088, 8443, true, log,
323 func(host string) bool { return host == "plain.example.com" || host == "app.example.com" },
324 func(host string) bool { return host != "plain.example.com" }, nil)(ok)
325
326 srv := httptest.NewServer(handler)
327 defer srv.Close()
328 client := srv.Client()
329 client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
330
331 // Ineligible known host: served plain HTTP, no redirect.
332 req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
333 require.NoError(t, err)
334 req.Host = "plain.example.com"
335 resp, err := client.Do(req)
336 require.NoError(t, err)
337 defer resp.Body.Close()
338 assert.Equal(t, http.StatusOK, resp.StatusCode)
339 assert.Empty(t, resp.Header.Get("Location"))
340
341 // Eligible known host: still redirected.
342 req, err = http.NewRequest(http.MethodGet, srv.URL+"/", nil)
343 require.NoError(t, err)
344 req.Host = "app.example.com"
345 resp2, err := client.Do(req)
346 require.NoError(t, err)
347 defer resp2.Body.Close()
348 assert.Equal(t, http.StatusPermanentRedirect, resp2.StatusCode)
349 assert.Equal(t, "https://app.example.com/", resp2.Header.Get("Location"))
350
351 // Unknown host: still 400.
352 req, err = http.NewRequest(http.MethodGet, srv.URL+"/", nil)
353 require.NoError(t, err)
354 req.Host = "evil.example.com"
355 resp3, err := client.Do(req)
356 require.NoError(t, err)
357 defer resp3.Body.Close()
358 assert.Equal(t, http.StatusBadRequest, resp3.StatusCode)
359}
360
361func TestProxyCIDRAllowlist(t *testing.T) {
362 log := testLogger()
363 redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
364 http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect)
365 })
366
367 tests := []struct {
368 name string
369 allowedNets []*net.IPNet
370 remoteAddr string
371 wantStatus int
372 }{
373 {
374 name: "no CIDRs configured passes through",
375 allowedNets: nil,
376 remoteAddr: "203.0.113.50:1234",
377 wantStatus: http.StatusPermanentRedirect,
378 },
379 {
380 name: "allowed IP gets redirect",
381 allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
382 remoteAddr: "173.245.48.1:1234",
383 wantStatus: http.StatusPermanentRedirect,
384 },
385 {
386 name: "blocked IP gets 403",
387 allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
388 remoteAddr: "203.0.113.50:1234",
389 wantStatus: http.StatusForbidden,
390 },
391 {
392 name: "localhost always allowed",
393 allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
394 remoteAddr: "127.0.0.1:1234",
395 wantStatus: http.StatusPermanentRedirect,
396 },
397 {
398 name: "ignores XFF (uses RemoteAddr only)",
399 allowedNets: httphelper.ParseTrustedProxies([]string{"173.245.48.0/20"}),
400 remoteAddr: "203.0.113.50:1234",
401 wantStatus: http.StatusForbidden,
402 },
403 }
404
405 for _, tt := range tests {
406 t.Run(tt.name, func(t *testing.T) {
407 handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect)
408
409 req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
410 req.RemoteAddr = tt.remoteAddr
411 // Even with XFF from an "allowed" range, the proxy middleware
412 // must check RemoteAddr, not forwarded headers.
413 req.Header.Set("X-Forwarded-For", "173.245.48.1")
414 rr := httptest.NewRecorder()
415
416 handler.ServeHTTP(rr, req)
417
418 assert.Equal(t, tt.wantStatus, rr.Code)
419
420 if tt.wantStatus == http.StatusForbidden {
421 var errResp dto.ErrorResponse
422 require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp))
423 assert.Equal(t, "Forbidden", errResp.Error)
424 }
425 })
426 }
427}