From 73f655d2eab66236dcd47589f1d7f92be367d4f2 Mon Sep 17 00:00:00 2001 From: Gilbert Gilb's Date: Sun, 18 Oct 2020 21:16:38 +0200 Subject: [PATCH] implement default values for header directive closes #3804 --- modules/caddyhttp/headers/caddyfile.go | 24 +++-- modules/caddyhttp/headers/headers.go | 15 +++ modules/caddyhttp/headers/headers_test.go | 119 +++++++++++++++++++++- 3 files changed, 148 insertions(+), 10 deletions(-) diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go index d893cab3dde3..d9be1fb8f223 100644 --- a/modules/caddyhttp/headers/caddyfile.go +++ b/modules/caddyhttp/headers/caddyfile.go @@ -30,8 +30,9 @@ func init() { // parseCaddyfile sets up the handler for response headers from // Caddyfile tokens. Syntax: // -// header [] [[+|-] [] []] { +// header [] [[+|-|?] [] []] { // [+] [ []] +// ? // - // [defer] // } @@ -102,7 +103,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // parseReqHdrCaddyfile sets up the handler for request headers // from Caddyfile tokens. Syntax: // -// request_header [] [[+|-] [] []] +// request_header [] [[+|-|?] [] []] // func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { hdr := new(Handler) @@ -142,12 +143,12 @@ func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, // CaddyfileHeaderOp applies a new header operation according to // field, value, and replacement. The field can be prefixed with -// "+" or "-" to specify adding or removing; otherwise, the value -// will be set (overriding any previous value). If replacement is -// non-empty, value will be treated as a regular expression which -// will be used to search and then replacement will be used to -// complete the substring replacement; in that case, any + or - -// prefix to field will be ignored. +// "+", "-" or "?" to specify adding, removing or setting a default +// value; otherwise, the value will be set (overriding any previous +// value). If replacement is non-empty, value will be treated as a +// regular expression which will be used to search and then replacement +// will be used to complete the substring replacement; in that case, +// any "+", "-" or "?" prefix to field will be ignored. func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) { if strings.HasPrefix(field, "+") { if ops.Add == nil { @@ -156,6 +157,11 @@ func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) { ops.Add.Set(field[1:], value) } else if strings.HasPrefix(field, "-") { ops.Delete = append(ops.Delete, field[1:]) + } else if strings.HasPrefix(field, "?") { + if ops.SetDefault == nil { + ops.SetDefault = make(http.Header) + } + ops.SetDefault.Set(field[1:], value) } else { if replacement == "" { if ops.Set == nil { @@ -166,7 +172,7 @@ func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) { if ops.Replace == nil { ops.Replace = make(map[string][]Replacement) } - field = strings.TrimLeft(field, "+-") + field = strings.TrimLeft(field, "+-?") ops.Replace[field] = append( ops.Replace[field], Replacement{ diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 3571dd929aed..b9503bd58f60 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -118,6 +118,9 @@ type HeaderOps struct { // Sets HTTP headers; replaces existing header fields. Set http.Header `json:"set,omitempty"` + // Sets a default value for HTTP headers. + SetDefault http.Header `json:"set_default,omitempty"` + // Names of HTTP header fields to delete. Delete []string `json:"delete,omitempty"` @@ -206,6 +209,18 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { hdr.Set(fieldName, strings.Join(newVals, ",")) } + // set default + for fieldName, vals := range ops.SetDefault { + fieldName = repl.ReplaceAll(fieldName, "") + if hdr.Get(fieldName) != "" { + continue + } + + for _, v := range vals { + hdr.Add(fieldName, repl.ReplaceAll(v, "")) + } + } + // delete for _, fieldName := range ops.Delete { hdr.Del(repl.ReplaceAll(fieldName, "")) diff --git a/modules/caddyhttp/headers/headers_test.go b/modules/caddyhttp/headers/headers_test.go index e4f03adc9c62..751e1918ba78 100644 --- a/modules/caddyhttp/headers/headers_test.go +++ b/modules/caddyhttp/headers/headers_test.go @@ -14,8 +14,125 @@ package headers -import "testing" +import ( + "context" + "net/http" + "reflect" + "testing" + + "github.com/caddyserver/caddy/v2" +) func TestReqHeaders(t *testing.T) { // TODO: write tests } + +func TestHeaderOps(t *testing.T) { + for i, tc := range []struct { + headerOps HeaderOps + input http.Header + expected http.Header + }{ + { + headerOps: HeaderOps{ + Add: http.Header{ + "Expose-Secrets": []string{"always"}, + }, + }, + input: http.Header{ + "Expose-Secrets": []string{"i'm serious"}, + }, + expected: http.Header{ + "Expose-Secrets": []string{"i'm serious", "always"}, + }, + }, + { + headerOps: HeaderOps{ + Set: http.Header{ + "Who-Wins": []string{"batman"}, + }, + }, + input: http.Header{ + "Who-Wins": []string{"joker"}, + }, + expected: http.Header{ + "Who-Wins": []string{"batman"}, + }, + }, + { + headerOps: HeaderOps{ + SetDefault: http.Header{ + "Cache-Control": []string{"default"}, + }, + }, + input: http.Header{ + "Not-Cache-Control": []string{"cache-cache"}, + }, + expected: http.Header{ + "Cache-Control": []string{"default"}, + "Not-Cache-Control": []string{"cache-cache"}, + }, + }, + { + headerOps: HeaderOps{ + SetDefault: http.Header{ + "Cache-Control": []string{"no-store"}, + }, + }, + input: http.Header{ + "Cache-Control": []string{"max-age=3600"}, + }, + expected: http.Header{ + "Cache-Control": []string{"max-age=3600"}, + }, + }, + { + headerOps: HeaderOps{ + Delete: []string{"Kick-Me"}, + }, + input: http.Header{ + "Kick-Me": []string{"if you can"}, + "Keep-Me": []string{"i swear i'm innocent"}, + }, + expected: http.Header{ + "Keep-Me": []string{"i swear i'm innocent"}, + }, + }, + { + headerOps: HeaderOps{ + Replace: map[string][]Replacement{ + "Best-Server": []Replacement{ + Replacement{ + Search: "NGINX", + Replace: "the Caddy web server", + }, + Replacement{ + SearchRegexp: `Apache(\d+)`, + Replace: "Caddy", + }, + }, + }, + }, + input: http.Header{ + "Best-Server": []string{"it's NGINX, undoubtedly", "I love Apache2"}, + }, + expected: http.Header{ + "Best-Server": []string{"it's the Caddy web server, undoubtedly", "I love Caddy"}, + }, + }, + } { + req := &http.Request{Header: tc.input} + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + tc.headerOps.Provision(caddy.Context{}) + tc.headerOps.ApplyToRequest(req) + actual := req.Header + + if !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("Test %d: Expected %v, got %v", i, tc.expected, actual) + continue + } + } +}