From fc717bbca992fe8b14f8bd357e1f4e5e128ddca9 Mon Sep 17 00:00:00 2001 From: Alexander Yastrebov Date: Mon, 24 Mar 2025 20:29:21 +0100 Subject: [PATCH] filters/builtin: drop specified header value Extend `dropRequestHeader` and `dropResponseHeader` to support optional second value argument and drop only this value when specified. Signed-off-by: Alexander Yastrebov --- docs/reference/filters.md | 9 +- filters/builtin/header_test.go | 151 ++++++++++++++++++++++---------- filters/builtin/headerfilter.go | 18 +++- 3 files changed, 127 insertions(+), 51 deletions(-) diff --git a/docs/reference/filters.md b/docs/reference/filters.md index 3548352142..e60a4e257b 100644 --- a/docs/reference/filters.md +++ b/docs/reference/filters.md @@ -163,11 +163,12 @@ but appends the provided value to the already existing ones. ### dropRequestHeader -Removes a header from the request +Removes a header or a specific value from the request. Parameters: * header name (string) +* header value (string) - optional Example: @@ -175,6 +176,12 @@ Example: foo: * -> dropRequestHeader("User-Agent") -> "https://backend.example.org"; ``` +Drop exactly matching value and keep others: + +``` +foo: * -> dropRequestHeader("Connection", "Upgrade") -> "https://backend.example.org"; +``` + ### modResponseHeader Same as [modRequestHeader](#modrequestheader), only for responses diff --git a/filters/builtin/header_test.go b/filters/builtin/header_test.go index 6ae6db2bc2..008aefe23a 100644 --- a/filters/builtin/header_test.go +++ b/filters/builtin/header_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/zalando/skipper/eskip" "github.com/zalando/skipper/filters" "github.com/zalando/skipper/filters/filtertest" @@ -85,12 +86,7 @@ func testHeaders(t *testing.T, got, expected http.Header) { delete(got, n) } } - - if !compareHeaders(got, expected) { - printHeader(t, expected, "invalid header", "expected") - printHeader(t, got, "invalid header", "got") - t.Error("invalid header") - } + assert.Equal(t, expected, got) } func TestHeader(t *testing.T) { @@ -132,10 +128,11 @@ func TestHeader(t *testing.T) { requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, expectedHeader: http.Header{"X-Test-Request-Name": []string{"value"}}, }, { - msg: "set outgoing host on set", - args: []interface{}{"Host", "www.example.org"}, - valid: true, - host: "www.example.org", + msg: "set outgoing host on set", + args: []interface{}{"Host", "www.example.org"}, + valid: true, + host: "www.example.org", + expectedHeader: http.Header{}, }, { msg: "set request header from path params", args: []interface{}{"X-Test-Name", "Mit ${was} zu ${wo}"}, @@ -161,10 +158,11 @@ func TestHeader(t *testing.T) { requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, expectedHeader: http.Header{"X-Test-Request-Name": []string{"value0", "value1", "value"}}, }, { - msg: "append outgoing host on set", - args: []interface{}{"Host", "www.example.org"}, - valid: true, - host: "www.example.org", + msg: "append outgoing host on set", + args: []interface{}{"Host", "www.example.org"}, + valid: true, + host: "www.example.org", + expectedHeader: http.Header{}, }, { msg: "append request header from path params", args: []interface{}{"X-Test-Name", "a ${foo}ter"}, @@ -186,19 +184,46 @@ func TestHeader(t *testing.T) { expectedHeader: http.Header{"X-Test-Request-Name": []string{"Value"}}, }}, "dropRequestHeader": {{ - msg: "drop request header when none", - args: []interface{}{"X-Test-Name"}, - valid: true, + msg: "drop request header when none", + args: []interface{}{"X-Test-Name"}, + valid: true, + expectedHeader: http.Header{}, }, { - msg: "drop request header when exists", - args: []interface{}{"X-Test-Name"}, - valid: true, - requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + msg: "drop request header when exists", + args: []interface{}{"X-Test-Name"}, + valid: true, + requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + expectedHeader: http.Header{}, }, { - msg: "name parameter is case-insensitive", - args: []interface{}{"x-test-name"}, - valid: true, - requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + msg: "drop request header when does not exist", + args: []interface{}{"X-Test-Does-Not-Exist"}, + valid: true, + requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + expectedHeader: http.Header{"X-Test-Request-Name": []string{"value0", "value1"}}, + }, { + msg: "name parameter is case-insensitive", + args: []interface{}{"x-test-name"}, + valid: true, + requestHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + expectedHeader: http.Header{}, + }, { + msg: "drop matching value", + args: []interface{}{"X-Test-Name", "bar"}, + valid: true, + requestHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + expectedHeader: http.Header{"X-Test-Request-Name": []string{"foo", "baz"}, "X-Test-Request-Name2": []string{"qux"}}, + }, { + msg: "ignore non-matching", + args: []interface{}{"X-Test-Name", "qux"}, + valid: true, + requestHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + expectedHeader: http.Header{"X-Test-Request-Name": []string{"foo", "bar", "baz"}, "X-Test-Request-Name2": []string{"qux"}}, + }, { + msg: "drop matching value name parameter is case-insensitive", + args: []interface{}{"x-test-name", "bar"}, + valid: true, + requestHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + expectedHeader: http.Header{"X-Test-Request-Name": []string{"foo", "baz"}, "X-Test-Request-Name2": []string{"qux"}}, }}, "setResponseHeader": {{ msg: "set response header when none", @@ -220,9 +245,10 @@ func TestHeader(t *testing.T) { valid: true, expectedHeader: http.Header{"X-Test-Name": []string{"a small barter"}}, }, { - msg: "set response header from path params when missing", - args: []interface{}{"X-Test-Name", "a ${foo}ter"}, - valid: true, + msg: "set response header from path params when missing", + args: []interface{}{"X-Test-Name", "a ${foo}ter"}, + valid: true, + expectedHeader: http.Header{}, }, { msg: "name parameter is case-insensitive", args: []interface{}{"x-test-name", "Value"}, @@ -261,19 +287,46 @@ func TestHeader(t *testing.T) { expectedHeader: http.Header{"X-Test-Name": []string{"Value"}}, }}, "dropResponseHeader": {{ - msg: "drop response header when none", - args: []interface{}{"X-Test-Name"}, - valid: true, + msg: "drop response header when none", + args: []interface{}{"X-Test-Name"}, + valid: true, + expectedHeader: http.Header{}, }, { msg: "drop response header when exists", args: []interface{}{"X-Test-Name"}, valid: true, responseHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + expectedHeader: http.Header{}, + }, { + msg: "drop response header when does not exist", + args: []interface{}{"X-Test-Does-Not-Exist"}, + valid: true, + responseHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + expectedHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, }, { msg: "name parameter is case-insensitive", args: []interface{}{"x-test-name"}, valid: true, responseHeader: http.Header{"X-Test-Name": []string{"value0", "value1"}}, + expectedHeader: http.Header{}, + }, { + msg: "drop matching value", + args: []interface{}{"X-Test-Name", "bar"}, + valid: true, + responseHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + expectedHeader: http.Header{"X-Test-Name": []string{"foo", "baz"}, "X-Test-Name2": []string{"qux"}}, + }, { + msg: "ignore non-matching", + args: []interface{}{"X-Test-Name", "qux"}, + valid: true, + responseHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + expectedHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + }, { + msg: "drop matching value name parameter is case-insensitive", + args: []interface{}{"x-test-name", "bar"}, + valid: true, + responseHeader: http.Header{"X-Test-Name": []string{"foo", "bar", "baz"}, "X-Test-Name2": []string{"qux"}}, + expectedHeader: http.Header{"X-Test-Name": []string{"foo", "baz"}, "X-Test-Name2": []string{"qux"}}, }}, "setContextRequestHeader": {{ msg: "set request header from context", @@ -282,11 +335,12 @@ func TestHeader(t *testing.T) { valid: true, expectedHeader: http.Header{"X-Test-Request-Foo": []string{"bar"}}, }, { - msg: "set request host header from context", - args: []interface{}{"Host", "foo"}, - context: map[string]interface{}{"foo": "www.example.org"}, - valid: true, - host: "www.example.org", + msg: "set request host header from context", + args: []interface{}{"Host", "foo"}, + context: map[string]interface{}{"foo": "www.example.org"}, + valid: true, + host: "www.example.org", + expectedHeader: http.Header{}, }, { msg: "name parameter is case-insensitive", args: []interface{}{"x-test-foo", "foo"}, @@ -302,11 +356,12 @@ func TestHeader(t *testing.T) { requestHeader: http.Header{"X-Test-Foo": []string{"bar"}}, expectedHeader: http.Header{"X-Test-Request-Foo": []string{"bar", "baz"}}, }, { - msg: "append request host header from context", - args: []interface{}{"Host", "foo"}, - context: map[string]interface{}{"foo": "www.example.org"}, - valid: true, - host: "www.example.org", + msg: "append request host header from context", + args: []interface{}{"Host", "foo"}, + context: map[string]interface{}{"foo": "www.example.org"}, + valid: true, + host: "www.example.org", + expectedHeader: http.Header{}, }, { msg: "name parameter is case-insensitive", args: []interface{}{"x-test-foo", "foo"}, @@ -356,9 +411,10 @@ func TestHeader(t *testing.T) { msg: "invalid target header name", args: []interface{}{"X-Test-Foo", 42}, }, { - msg: "no header to copy", - args: []interface{}{"X-Test-Foo", "X-Test-Bar"}, - valid: true, + msg: "no header to copy", + args: []interface{}{"X-Test-Foo", "X-Test-Bar"}, + valid: true, + expectedHeader: http.Header{}, }, { msg: "copy header", args: []interface{}{"X-Test-Foo", "X-Test-Bar"}, @@ -414,9 +470,10 @@ func TestHeader(t *testing.T) { msg: "invalid target header name", args: []interface{}{"X-Test-Foo", 42}, }, { - msg: "no header to copy", - args: []interface{}{"X-Test-Foo", "X-Test-Bar"}, - valid: true, + msg: "no header to copy", + args: []interface{}{"X-Test-Foo", "X-Test-Bar"}, + valid: true, + expectedHeader: http.Header{}, }, { msg: "copy header", args: []interface{}{"X-Test-Foo", "X-Test-Bar"}, diff --git a/filters/builtin/headerfilter.go b/filters/builtin/headerfilter.go index e9b5be770a..380c525abf 100644 --- a/filters/builtin/headerfilter.go +++ b/filters/builtin/headerfilter.go @@ -2,6 +2,8 @@ package builtin import ( "fmt" + "net/textproto" + "slices" "strings" "github.com/zalando/skipper/eskip" @@ -47,7 +49,7 @@ type headerFilter struct { func headerFilterConfig(typ headerType, config []interface{}) (string, string, *eskip.Template, error) { switch typ { case dropRequestHeader, dropResponseHeader: - if len(config) != 1 { + if len(config) < 1 || len(config) > 2 { return "", "", nil, filters.ErrInvalidFilterParameters } default: @@ -281,7 +283,12 @@ func (f *headerFilter) Request(ctx filters.FilterContext) { ctx.SetOutgoingHost(f.value) } case dropRequestHeader: - header.Del(f.key) + if f.value == "" { + header.Del(f.key) + } else { + k := textproto.CanonicalMIMEHeaderKey(f.key) + header[k] = slices.DeleteFunc(header[k], func(v string) bool { return v == f.value }) + } case setContextRequestHeader: valueFromContext(ctx, f.key, f.value, true, header.Set) case appendContextRequestHeader: @@ -313,7 +320,12 @@ func (f *headerFilter) Response(ctx filters.FilterContext) { case depResponseHeader: header.Add(f.key, f.value) case dropResponseHeader: - header.Del(f.key) + if f.value == "" { + header.Del(f.key) + } else { + k := textproto.CanonicalMIMEHeaderKey(f.key) + header[k] = slices.DeleteFunc(header[k], func(v string) bool { return v == f.value }) + } case setContextResponseHeader: valueFromContext(ctx, f.key, f.value, false, header.Set) case appendContextResponseHeader: