mirror of
https://github.com/caddyserver/caddy.git
synced 2026-11-02 08:15:04 -05:00
262 lines
8.4 KiB
Go
262 lines
8.4 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)
|
|
}
|
|
}
|
|
|
|
// TestInformationalResponseStripsHopByHopHeaders verifies that hop-by-hop
|
|
// headers from a 1xx response are not forwarded to the client, including
|
|
// headers nominated by the Connection header.
|
|
func TestInformationalResponseStripsHopByHopHeaders(t *testing.T) {
|
|
hopByHop := map[string]string{
|
|
"Alt-Svc": `h2="attacker.example:443"; ma=86400`,
|
|
"Keep-Alive": "timeout=999",
|
|
"Proxy-Authenticate": `Basic realm="phish"`,
|
|
"X-Internal": "secret",
|
|
}
|
|
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Link", "</style.css>; rel=preload")
|
|
for k, v := range hopByHop {
|
|
w.Header().Set(k, v)
|
|
}
|
|
w.Header().Set("Connection", "X-Internal")
|
|
w.WriteHeader(http.StatusEarlyHints)
|
|
for k := range hopByHop {
|
|
w.Header().Del(k)
|
|
}
|
|
w.Header().Del("Link")
|
|
w.Header().Del("Connection")
|
|
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) {
|
|
_ = 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")
|
|
}
|
|
for k := range hopByHop {
|
|
if got := earlyHints[0].Get(k); got != "" {
|
|
t.Errorf("hop-by-hop header %s on the 103: got %q, want it stripped", k, got)
|
|
}
|
|
}
|
|
if got := earlyHints[0].Get("Connection"); got != "" {
|
|
t.Errorf("Connection on the 103: got %q, want it stripped", got)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status: got %d, want %d", resp.StatusCode, http.StatusOK)
|
|
}
|
|
}
|