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}