mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-31 16:32:45 -04:00
* 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
188 lines
6.0 KiB
Go
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)
|
|
}
|
|
}
|