mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-31 08:21:39 -04:00
* reverseproxy: strip hop-by-hop headers when copying response headers copy_response_headers copied the upstream header map before finalizeResponse could strip hop-by-hop fields. Clone and strip first, so include and exclude cannot forward them, and leave the upstream response unchanged for later handlers in the route. Co-authored-by: Grok 4.7 <noreply@x.ai>
184 lines
5.7 KiB
Go
184 lines
5.7 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package reverseproxy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
func TestCopyResponseHeadersStripsHopByHop(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Alt-Svc", `h2="evil.example:443"; ma=86400`)
|
|
w.Header().Set("Keep-Alive", "timeout=999")
|
|
w.Header().Set("Proxy-Authenticate", `Basic realm="phish"`)
|
|
w.Header().Set("X-Upstream", "kept")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = io.WriteString(w, "from-upstream")
|
|
}))
|
|
t.Cleanup(backend.Close)
|
|
|
|
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
t.Cleanup(cancel)
|
|
|
|
newHandler := func(t *testing.T, handles ...json.RawMessage) *Handler {
|
|
t.Helper()
|
|
h := minimalHandler(0, &Upstream{Host: new(Host), Dial: backend.Listener.Addr().String()})
|
|
if len(handles) == 0 {
|
|
return h
|
|
}
|
|
route := caddyhttp.Route{HandlersRaw: handles}
|
|
if err := route.ProvisionHandlers(ctx, nil); err != nil {
|
|
t.Fatalf("provision handle_response: %v", err)
|
|
}
|
|
h.HandleResponse = []caddyhttp.ResponseHandler{{
|
|
Routes: caddyhttp.RouteList{route},
|
|
}}
|
|
return h
|
|
}
|
|
|
|
// The replacement body is the same length as the upstream body.
|
|
// copy_response_headers also copies Content-Length, and a shorter
|
|
// body makes the client report unexpected EOF. X-Replaced shows
|
|
// that static_response ran.
|
|
const replaced = `{"handler":"static_response","body":"from-upstream","headers":{"X-Replaced":["yes"]}}`
|
|
cases := []struct {
|
|
name string
|
|
handles []json.RawMessage
|
|
wantReplace bool
|
|
}{
|
|
{
|
|
name: "default finalize",
|
|
},
|
|
{
|
|
name: "copy_response",
|
|
handles: []json.RawMessage{
|
|
json.RawMessage(`{"handler":"copy_response"}`),
|
|
},
|
|
},
|
|
{
|
|
name: "copy_response_headers",
|
|
handles: []json.RawMessage{
|
|
json.RawMessage(`{"handler":"copy_response_headers"}`),
|
|
json.RawMessage(replaced),
|
|
},
|
|
wantReplace: true,
|
|
},
|
|
{
|
|
name: "include cannot copy hop-by-hop fields",
|
|
handles: []json.RawMessage{
|
|
json.RawMessage(`{"handler":"copy_response_headers","include":["Alt-Svc","X-Upstream","Keep-Alive"]}`),
|
|
json.RawMessage(replaced),
|
|
},
|
|
wantReplace: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
h := newHandler(t, tc.handles...)
|
|
front := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
err := h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadGateway)
|
|
}
|
|
}))
|
|
t.Cleanup(front.Close)
|
|
|
|
resp, err := front.Client().Get(front.URL)
|
|
if err != nil {
|
|
t.Fatalf("GET: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("read body: %v", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, body = %q", resp.StatusCode, body)
|
|
}
|
|
if string(body) != "from-upstream" {
|
|
t.Fatalf("body = %q", body)
|
|
}
|
|
if got := resp.Header.Get("X-Upstream"); got != "kept" {
|
|
t.Errorf("X-Upstream = %q, want kept", got)
|
|
}
|
|
if got, want := resp.Header.Get("X-Replaced") == "yes", tc.wantReplace; got != want {
|
|
t.Errorf("X-Replaced present = %v, want %v", got, want)
|
|
}
|
|
for _, field := range []string{"Alt-Svc", "Keep-Alive", "Proxy-Authenticate"} {
|
|
if got := resp.Header.Get(field); got != "" {
|
|
t.Errorf("%s = %q, want absent", field, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCopyResponseHeadersStripLeavesUpstreamHeader(t *testing.T) {
|
|
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
t.Cleanup(cancel)
|
|
|
|
h := &CopyResponseHeadersHandler{Include: []string{"Alt-Svc", "X-Upstream", "X-Hop"}}
|
|
if err := h.Provision(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
res := &http.Response{Header: make(http.Header), StatusCode: http.StatusOK}
|
|
res.Header.Set("Alt-Svc", `h2="evil.example:443"`)
|
|
res.Header.Set("X-Upstream", "kept")
|
|
res.Header.Set("X-Hop", "secret")
|
|
res.Header.Set("Connection", "close, X-Hop")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), proxyHandleResponseContextCtxKey, &handleResponseContext{
|
|
response: res,
|
|
}))
|
|
rec := httptest.NewRecorder()
|
|
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
|
|
return nil
|
|
}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got := rec.Header().Get("X-Upstream"); got != "kept" {
|
|
t.Errorf("X-Upstream = %q, want kept", got)
|
|
}
|
|
for _, field := range []string{"Alt-Svc", "X-Hop", "Connection"} {
|
|
if got := rec.Header().Get(field); got != "" {
|
|
t.Errorf("copied %s = %q, want absent", field, got)
|
|
}
|
|
}
|
|
if got := res.Header.Get("Alt-Svc"); got != `h2="evil.example:443"` {
|
|
t.Errorf("upstream Alt-Svc = %q, want it unchanged", got)
|
|
}
|
|
if got := res.Header.Get("X-Hop"); got != "secret" {
|
|
t.Errorf("upstream X-Hop = %q, want it unchanged", got)
|
|
}
|
|
if got := res.Header.Get("Connection"); got != "close, X-Hop" {
|
|
t.Errorf("upstream Connection = %q, want it unchanged", got)
|
|
}
|
|
}
|