Files
caddy/modules/caddyhttp/reverseproxy/headers_test.go
Jay Hemnani 519be12306 reverseproxy: Keep own headers after 1xx responses (#8169)
* reverseproxy: Keep own headers after 1xx responses

Since #6656 the 1xx handler clears the whole header map after writing the
informational response, which also drops headers set before proxying (for
example by the header handler). Restore those after clearing, as #5091 did.

* reverseproxy: Use maps.Copy to restore own headers

* reverseproxy: Check the 103 reaches the client in the 1xx headers test
2026-10-09 16:42:26 +10:00

188 lines
6.0 KiB
Go

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", "</style.css>; 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 != "</style.css>; rel=preload" {
t.Errorf("Link on the 103: got %q, want %q", got, "</style.css>; 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)
}
}