package middleware import ( "encoding/json" "fmt" "net" "net/http" "net/http/httptest" "testing" "github.com/bnema/gordon/internal/adapters/dto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRegistryCIDRAllowlist(t *testing.T) { log := testLogger() ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) tests := []struct { name string allowedNets []*net.IPNet trustedNets []*net.IPNet remoteAddr string xff string wantStatus int }{ { name: "no CIDRs configured passes through", allowedNets: nil, remoteAddr: "203.0.113.50:1234", wantStatus: http.StatusOK, }, { name: "empty CIDRs passes through", allowedNets: []*net.IPNet{}, remoteAddr: "203.0.113.50:1234", wantStatus: http.StatusOK, }, { name: "allowed IP returns 200", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), remoteAddr: "100.100.1.1:1234", wantStatus: http.StatusOK, }, { name: "denied IP returns 403", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), remoteAddr: "203.0.113.50:1234", wantStatus: http.StatusForbidden, }, { name: "localhost always allowed even when not in allowlist", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), remoteAddr: "127.0.0.1:1234", wantStatus: http.StatusOK, }, { name: "127.x.x.x always allowed", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), remoteAddr: "127.0.0.2:1234", wantStatus: http.StatusOK, }, { name: "IPv6 localhost always allowed", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), remoteAddr: "[::1]:1234", wantStatus: http.StatusOK, }, { name: "trusted proxy forwards allowed client IP", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), trustedNets: ParseTrustedProxies([]string{"10.0.0.1"}), remoteAddr: "10.0.0.1:1234", xff: "100.100.1.1", wantStatus: http.StatusOK, }, { name: "trusted proxy forwards denied client IP", allowedNets: ParseTrustedProxies([]string{"100.64.0.0/10"}), trustedNets: ParseTrustedProxies([]string{"10.0.0.1"}), remoteAddr: "10.0.0.1:1234", xff: "203.0.113.50", wantStatus: http.StatusForbidden, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := RegistryCIDRAllowlist(tt.allowedNets, tt.trustedNets, log)(ok) req := httptest.NewRequest(http.MethodGet, "/v2/", nil) req.RemoteAddr = tt.remoteAddr if tt.xff != "" { req.Header.Set("X-Forwarded-For", tt.xff) } rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, tt.wantStatus, rr.Code) if tt.wantStatus == http.StatusForbidden { var errResp dto.ErrorResponse require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp)) assert.Equal(t, "Forbidden", errResp.Error) } }) } } func TestHTTPSRedirect_NoPortHost_OmitsTLSPort(t *testing.T) { log := testLogger() ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) handler := HTTPSRedirect(nil, 8088, 8443, true, log)(ok) srv := httptest.NewServer(handler) defer srv.Close() client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } req, _ := http.NewRequest(http.MethodGet, srv.URL+"/", nil) req.Host = "o2.bnema.dev" resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode) assert.Equal(t, "https://o2.bnema.dev/", resp.Header.Get("Location")) } func TestHTTPSRedirect_HTTPListenerPort_MapsToTLSPort(t *testing.T) { log := testLogger() ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) handler := HTTPSRedirect(nil, 8088, 8443, true, log)(ok) srv := httptest.NewServer(handler) defer srv.Close() client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } req, _ := http.NewRequest(http.MethodGet, srv.URL+"/", nil) req.Host = "o2.bnema.dev:8088" resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode) assert.Equal(t, "https://o2.bnema.dev:8443/", resp.Header.Get("Location")) } func TestHTTPSRedirect_UnknownExplicitPort_IsPreserved(t *testing.T) { log := testLogger() ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) handler := HTTPSRedirect(nil, 8088, 8443, true, log)(ok) srv := httptest.NewServer(handler) defer srv.Close() client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } req, _ := http.NewRequest(http.MethodGet, srv.URL+"/", nil) req.Host = "o2.bnema.dev:9999" resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, http.StatusPermanentRedirect, resp.StatusCode) assert.Equal(t, "https://o2.bnema.dev:9999/", resp.Header.Get("Location")) } func TestHTTPSRedirect_InvalidExplicitPort_IsPreserved(t *testing.T) { log := testLogger() ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) handler := HTTPSRedirect(nil, 8088, 8443, true, log)(ok) srv := httptest.NewServer(handler) defer srv.Close() // Use a raw TCP connection because Go's HTTP client rejects the // invalid port in the Location header before CheckRedirect fires. conn, err := net.Dial("tcp", srv.Listener.Addr().String()) require.NoError(t, err) defer conn.Close() _, err = fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: o2.bnema.dev:abcd\r\nConnection: close\r\n\r\n") require.NoError(t, err) var buf [4096]byte n, _ := conn.Read(buf[:]) raw := string(buf[:n]) assert.Contains(t, raw, "308") assert.Contains(t, raw, "Location: https://o2.bnema.dev:abcd/") } func TestProxyCIDRAllowlist(t *testing.T) { log := testLogger() redirect := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusPermanentRedirect) }) tests := []struct { name string allowedNets []*net.IPNet remoteAddr string wantStatus int }{ { name: "no CIDRs configured passes through", allowedNets: nil, remoteAddr: "203.0.113.50:1234", wantStatus: http.StatusPermanentRedirect, }, { name: "allowed IP gets redirect", allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}), remoteAddr: "173.245.48.1:1234", wantStatus: http.StatusPermanentRedirect, }, { name: "blocked IP gets 403", allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}), remoteAddr: "203.0.113.50:1234", wantStatus: http.StatusForbidden, }, { name: "localhost always allowed", allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}), remoteAddr: "127.0.0.1:1234", wantStatus: http.StatusPermanentRedirect, }, { name: "ignores XFF (uses RemoteAddr only)", allowedNets: ParseTrustedProxies([]string{"173.245.48.0/20"}), remoteAddr: "203.0.113.50:1234", wantStatus: http.StatusForbidden, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := ProxyCIDRAllowlist(tt.allowedNets, log)(redirect) req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) req.RemoteAddr = tt.remoteAddr // Even with XFF from an "allowed" range, the proxy middleware // must check RemoteAddr, not forwarded headers. req.Header.Set("X-Forwarded-For", "173.245.48.1") rr := httptest.NewRecorder() handler.ServeHTTP(rr, req) assert.Equal(t, tt.wantStatus, rr.Code) if tt.wantStatus == http.StatusForbidden { var errResp dto.ErrorResponse require.NoError(t, json.NewDecoder(rr.Body).Decode(&errResp)) assert.Equal(t, "Forbidden", errResp.Error) } }) } }