package reverseproxy import ( "context" "net/http" "net/http/httptest" "net/http/httptrace" "net/textproto" "testing" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func TestAddForwardedHeadersNonIP(t *testing.T) { h := Handler{} // Simulate a request with a non-IP remote address (e.g. SCION, abstract socket, or hostname) req := httptest.NewRequest("GET", "/", nil) req.RemoteAddr = "my-weird-network:12345" // Mock the context variables required by Caddy. // We need to inject the variable map manually since we aren't running the full server. vars := map[string]any{ caddyhttp.TrustedProxyVarKey: false, } ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) req = req.WithContext(ctx) // Execute the unexported function err := h.addForwardedHeaders(req) // Expectation: No error should be returned for non-IP addresses. // The function should simply skip the trusted proxy check. if err != nil { t.Errorf("expected no error for non-IP address, got: %v", err) } } func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) { h := Handler{} req := httptest.NewRequest("GET", "http://example.com/", nil) req.RemoteAddr = "@" req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.0.0.1") req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", "original.example.com") vars := map[string]any{ caddyhttp.TrustedProxyVarKey: true, caddyhttp.ClientIPVarKey: "1.2.3.4", } ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) req = req.WithContext(ctx) err := h.addForwardedHeaders(req) if err != nil { t.Fatalf("expected no error, got: %v", err) } if got := req.Header.Get("X-Forwarded-For"); got != "1.2.3.4, 10.0.0.1" { t.Errorf("X-Forwarded-For = %q, want %q", got, "1.2.3.4, 10.0.0.1") } if got := req.Header.Get("X-Forwarded-Proto"); got != "https" { t.Errorf("X-Forwarded-Proto = %q, want %q", got, "https") } if got := req.Header.Get("X-Forwarded-Host"); got != "original.example.com" { t.Errorf("X-Forwarded-Host = %q, want %q", got, "original.example.com") } } func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) { h := Handler{} req := httptest.NewRequest("GET", "http://example.com/", nil) req.RemoteAddr = "@" req.Header.Set("X-Forwarded-For", "1.2.3.4") req.Header.Set("X-Forwarded-Proto", "https") req.Header.Set("X-Forwarded-Host", "spoofed.example.com") vars := map[string]any{ caddyhttp.TrustedProxyVarKey: false, caddyhttp.ClientIPVarKey: "", } ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) req = req.WithContext(ctx) err := h.addForwardedHeaders(req) if err != nil { t.Fatalf("expected no error, got: %v", err) } if got := req.Header.Get("X-Forwarded-For"); got != "" { t.Errorf("X-Forwarded-For should be deleted, got %q", got) } if got := req.Header.Get("X-Forwarded-Proto"); got != "" { t.Errorf("X-Forwarded-Proto should be deleted, got %q", got) } if got := req.Header.Get("X-Forwarded-Host"); got != "" { t.Errorf("X-Forwarded-Host should be deleted, got %q", got) } } func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) { h := Handler{} req := httptest.NewRequest("GET", "http://example.com/", nil) req.RemoteAddr = "@" vars := map[string]any{ caddyhttp.TrustedProxyVarKey: true, caddyhttp.ClientIPVarKey: "5.6.7.8", } ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars) req = req.WithContext(ctx) err := h.addForwardedHeaders(req) if err != nil { t.Fatalf("expected no error, got: %v", err) } if got := req.Header.Get("X-Forwarded-For"); got != "" { t.Errorf("X-Forwarded-For should be empty when no prior XFF exists, got %q", got) } if got := req.Header.Get("X-Forwarded-Proto"); got != "http" { t.Errorf("X-Forwarded-Proto = %q, want %q", got, "http") } if got := req.Header.Get("X-Forwarded-Host"); got != "example.com" { t.Errorf("X-Forwarded-Host = %q, want %q", got, "example.com") } } // TestInformationalResponseKeepsHandlerHeaders verifies that a 1xx response // from the upstream does not remove response headers set before proxying. func TestInformationalResponseKeepsHandlerHeaders(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Link", "; rel=preload") w.WriteHeader(http.StatusEarlyHints) w.Header().Del("Link") w.WriteHeader(http.StatusOK) })) t.Cleanup(backend.Close) h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()}) front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Strict-Transport-Security", "max-age=31536000") _ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })) })) t.Cleanup(front.Close) var earlyHints []textproto.MIMEHeader trace := &httptrace.ClientTrace{ Got1xxResponse: func(code int, header textproto.MIMEHeader) error { if code == http.StatusEarlyHints { earlyHints = append(earlyHints, header) } return nil }, } req, err := http.NewRequestWithContext(httptrace.WithClientTrace(context.Background(), trace), http.MethodGet, front.URL, nil) if err != nil { t.Fatal(err) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("request failed: %v", err) } resp.Body.Close() if len(earlyHints) != 1 { t.Fatalf("103 responses: got %d, want 1", len(earlyHints)) } if got := earlyHints[0].Get("Link"); got != "; rel=preload" { t.Errorf("Link on the 103: got %q, want %q", got, "; rel=preload") } if resp.StatusCode != http.StatusOK { t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK) } if got := resp.Header.Get("Strict-Transport-Security"); got != "max-age=31536000" { t.Errorf("Strict-Transport-Security: got %q, want %q", got, "max-age=31536000") } if got := resp.Header.Get("Link"); got != "" { t.Errorf("Link from the 103 leaked into the final response: %q", got) } }