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}