package middleware import ( "bytes" "log/slog" "net/http" "net/http/httptest" "net/netip" "strings" "testing" "sneak.berlin/go/pixa/internal/clientip" "sneak.berlin/go/pixa/internal/config" ) // testForwardedClient is the client address the proxy forwards. const testForwardedClient = "203.0.113.7" // newTestMiddleware builds a Middleware whose resolver trusts the given // CIDRs and whose logger writes JSON to buf. func newTestMiddleware(t *testing.T, buf *bytes.Buffer, trusted ...string) *Middleware { t.Helper() prefixes := make([]netip.Prefix, 0, len(trusted)) for _, c := range trusted { p, err := netip.ParsePrefix(c) if err != nil { t.Fatalf("netip.ParsePrefix(%q) error = %v", c, err) } prefixes = append(prefixes, p) } return &Middleware{ log: slog.New(slog.NewJSONHandler(buf, nil)), config: &config.Config{TrustedProxies: prefixes}, clientIP: clientip.NewResolver(prefixes), } } // TestClientIPMiddlewareStoresResolvedIP verifies the ClientIP middleware // puts the resolved address into the request context for a trusted and an // untrusted peer. func TestClientIPMiddlewareStoresResolvedIP(t *testing.T) { t.Parallel() tests := []struct { name string remoteAddr string forwarded string want string }{ { name: "trusted peer honors forwarded client", remoteAddr: "10.0.0.1:5000", forwarded: testForwardedClient, want: testForwardedClient, }, { name: "untrusted peer ignores forwarded header", remoteAddr: "198.51.100.9:5000", forwarded: testForwardedClient, want: "198.51.100.9", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mw := newTestMiddleware(t, &bytes.Buffer{}, "10.0.0.0/8") var got string handler := mw.ClientIP()(http.HandlerFunc( func(_ http.ResponseWriter, r *http.Request) { got = clientip.FromContext(r.Context()) })) req := httptest.NewRequestWithContext( t.Context(), http.MethodGet, "/", nil) req.RemoteAddr = tt.remoteAddr req.Header.Set("X-Forwarded-For", tt.forwarded) handler.ServeHTTP(httptest.NewRecorder(), req) if got != tt.want { t.Errorf("client IP in context = %q, want %q", got, tt.want) } }) } } // TestLoggingUsesResolvedClientIP verifies the logging middleware records // the resolved forwarded client IP rather than the proxy peer address. func TestLoggingUsesResolvedClientIP(t *testing.T) { t.Parallel() var buf bytes.Buffer mw := newTestMiddleware(t, &buf, "10.0.0.0/8") handler := mw.ClientIP()(mw.Logging()(http.HandlerFunc( func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))) req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) req.RemoteAddr = "10.0.0.1:5000" req.Header.Set("X-Forwarded-For", testForwardedClient) handler.ServeHTTP(httptest.NewRecorder(), req) if !strings.Contains(buf.String(), `"remoteIP":"`+testForwardedClient+`"`) { t.Errorf("log output missing resolved client IP; got %q", buf.String()) } }