diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 97eee07ba..a1afaa548 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -18,6 +18,7 @@ import ( "fmt" "net/http" "regexp" + "sort" "strings" "github.com/caddyserver/caddy/v2" @@ -136,6 +137,19 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error { if ops == nil { return nil // it's possible no ops are configured; fix #6893 } + if len(ops.Set) > 0 { + set := make(http.Header, len(ops.Set)) + var fields []string + for fieldName := range ops.Set { + fields = append(fields, fieldName) + } + sort.Strings(fields) + for _, fieldName := range fields { + canonical := http.CanonicalHeaderKey(fieldName) + set[canonical] = append(set[canonical], ops.Set[fieldName]...) + } + ops.Set = set + } for fieldName, replacements := range ops.Replace { for i, r := range replacements { if r.SearchRegexp == "" { @@ -242,13 +256,22 @@ func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { // set for fieldName, vals := range ops.Set { - fieldName = repl.ReplaceKnown(fieldName, "") + if expanded := repl.ReplaceKnown(fieldName, ""); expanded != fieldName { + fieldName = http.CanonicalHeaderKey(expanded) + } var newVals []string for i := range vals { // append to new slice so we don't overwrite // the original values in ops.Set newVals = append(newVals, repl.ReplaceKnown(vals[i], "")) } + if fieldName == "Set-Cookie" && len(newVals) > 1 { + hdr.Del(fieldName) + for _, val := range newVals { + hdr.Add(fieldName, val) + } + continue + } hdr.Set(fieldName, strings.Join(newVals, ",")) } diff --git a/modules/caddyhttp/headers/headers_test.go b/modules/caddyhttp/headers/headers_test.go index a303c92dd..f74cf4ac9 100644 --- a/modules/caddyhttp/headers/headers_test.go +++ b/modules/caddyhttp/headers/headers_test.go @@ -267,6 +267,64 @@ func TestHandler(t *testing.T) { } } +func TestHeaderOpsSetMultipleCookies(t *testing.T) { + want := []string{"a=1; Path=/", "b=2; Path=/"} + for _, tc := range []struct { + fieldName, provisionedName string + }{ + {"Set-Cookie", "Set-Cookie"}, + {"set-cookie", "Set-Cookie"}, + {"sEt-CoOkIe", "Set-Cookie"}, + {"{cookie_header}", "{cookie_header}"}, + {"set-{cookie_suffix}", "set-{cookie_suffix}"}, + } { + t.Run(tc.fieldName, func(t *testing.T) { + ops := HeaderOps{Set: http.Header{tc.fieldName: want}} + if err := ops.Provision(caddy.Context{}); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(ops.Set, http.Header{tc.provisionedName: want}) { + t.Fatalf("provisioned Set = %v, want field %q", ops.Set, tc.provisionedName) + } + repl := caddy.NewReplacer() + repl.Set("cookie_header", "set-cookie") + repl.Set("cookie_suffix", "cookie") + hdr := http.Header{"Set-Cookie": {"old=1"}} + ops.ApplyTo(hdr, repl) + if got := hdr.Values("Set-Cookie"); !reflect.DeepEqual(got, want) { + t.Errorf("Set-Cookie values = %q, want %q", got, want) + } + }) + } +} + +func TestHandlerProvisionSetCollision(t *testing.T) { + for _, name := range []string{"request", "response"} { + t.Run(name, func(t *testing.T) { + ops := &HeaderOps{Set: http.Header{ + "set-cookie": {"b=2; Path=/"}, + "Set-Cookie": {"a=1; Path=/"}, + }} + h := Handler{Request: ops} + if name == "response" { + h = Handler{Response: &RespHeaderOps{HeaderOps: ops}} + } + if err := h.Provision(caddy.Context{}); err != nil { + t.Fatal(err) + } + want := http.Header{"Set-Cookie": {"a=1; Path=/", "b=2; Path=/"}} + if !reflect.DeepEqual(ops.Set, want) { + t.Fatalf("provisioned Set = %v, want %v", ops.Set, want) + } + hdr := make(http.Header) + ops.ApplyTo(hdr, caddy.NewReplacer()) + if !reflect.DeepEqual(hdr, want) { + t.Errorf("headers = %v, want %v", hdr, want) + } + }) + } +} + type nextHandler func(http.ResponseWriter, *http.Request) error func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {