diff --git a/docs/content/docs/configuration/reference/types.adoc b/docs/content/docs/configuration/reference/types.adoc index f68e7deea..8ef9a7006 100644 --- a/docs/content/docs/configuration/reference/types.adoc +++ b/docs/content/docs/configuration/reference/types.adoc @@ -426,7 +426,9 @@ A list with CIDR entries to match. Configured entries are evaluated using a bool * *`request_headers`*: _string array map_ (optional) + -A map with header names and the corresponding values to match. Configured entries are evaluated using a boolean `or` logic. This holds also true for the header values. +A map with header names and the corresponding values to match. Configured entries are evaluated using a boolean `or` logic. This holds also true for the header values. Wildcards like `\*` or `*/\*` can be used as well, where `*` matches any value and `\*/*` matches only those value, which consist of a type and a subtype. E.g `\*/*` would match `application/json` and `text/plain`, but not `text`. The actual matching happens always case-insensitive. ++ +NOTE: Some HTTP headers, like `Accept` or `Content-Type` hold not only the actual MIME type, but may also contain optional parameters. These parameters are ignored while matching the values. .Complex Error Condition configuration ==== @@ -446,10 +448,10 @@ request_cidr: - 10.0.0.0/8 # AND request_headers: - Accept: + accept: - text/html # OR - - "*/*" + - text/plain # OR Content-Type: - application/json diff --git a/internal/rules/mechanisms/errorhandlers/matcher/header_matcher.go b/internal/rules/mechanisms/errorhandlers/matcher/header_matcher.go index fbc0f72fe..c0cbd53a5 100644 --- a/internal/rules/mechanisms/errorhandlers/matcher/header_matcher.go +++ b/internal/rules/mechanisms/errorhandlers/matcher/header_matcher.go @@ -17,23 +17,82 @@ package matcher import ( - "net/textproto" + "net/http" "strings" ) type HeaderMatcher map[string][]string func (hm HeaderMatcher) Match(headers map[string]string) bool { - for name, valueList := range hm { - key := textproto.CanonicalMIMEHeaderKey(name) - if headerVal, found := headers[key]; found { - for _, val := range valueList { - if strings.Contains(headerVal, val) { - return true - } + for name, patterns := range hm { + key := http.CanonicalHeaderKey(name) + if value, found := headers[key]; found && hm.matchesAnyPattern(value, patterns) { + return true + } + } + + return false +} + +func (hm HeaderMatcher) matchesAnyPattern(value string, patterns []string) bool { + for _, headerValue := range hm.headerValuesFrom(value) { + for _, pattern := range patterns { + if headerValue.match(pattern) { + return true } } } return false } + +func (hm HeaderMatcher) headerValuesFrom(received string) []*headerValue { + values := strings.Split(strings.ToLower(received), ",") + headerValues := make([]*headerValue, len(values)) + + for idx, value := range values { + headerValues[idx] = newHeaderValue(strings.TrimSpace(value)) + } + + return headerValues +} + +type headerValue struct { + Type string + Subtype string +} + +func newHeaderValue(val string) *headerValue { + if paramsIdx := strings.IndexRune(val, ';'); paramsIdx != -1 { + val = val[:paramsIdx] + } + + typeSubtype := strings.Split(val, "/") + mediaType := typeSubtype[0] + mediaSubtype := "" + + if len(typeSubtype) > 1 { + mediaSubtype = typeSubtype[1] + } + + return &headerValue{ + Type: mediaType, + Subtype: mediaSubtype, + } +} + +func (h *headerValue) match(pattern string) bool { + if pattern == "*" { + return true + } + + pattern = strings.ToLower(pattern) + typeSubtype := strings.Split(pattern, "/") + + typeMatched := typeSubtype[0] == "*" || h.Type == typeSubtype[0] + + subtypeMatched := (len(h.Subtype) == 0 && len(typeSubtype) == 1) || + (len(h.Subtype) != 0 && len(typeSubtype) != 1 && (typeSubtype[1] == "*" || h.Subtype == typeSubtype[1])) + + return typeMatched && subtypeMatched +} diff --git a/internal/rules/mechanisms/errorhandlers/matcher/header_matcher_test.go b/internal/rules/mechanisms/errorhandlers/matcher/header_matcher_test.go index b23950871..18cb85813 100644 --- a/internal/rules/mechanisms/errorhandlers/matcher/header_matcher_test.go +++ b/internal/rules/mechanisms/errorhandlers/matcher/header_matcher_test.go @@ -42,12 +42,14 @@ func TestHeaderMatcher(t *testing.T) { { uc: "match multiple header", headers: map[string][]string{ - "foobar": {"foo", "bar"}, - "some-header": {"value1", "value2"}, + "foobar": {"foo", "bar"}, + "some-header": {"value1", "value2"}, + "x-yet-another-header": {"application/json"}, }, match: map[string]string{ - "Foobar": "bar,foo", - "Some-Header": "value1,val3", + "Foobar": "bar,foo", + "Some-Header": "value1,val3", + "X-Yet-Another-Header": "application/xml;q=0.8, application/json;v=1.2", }, matching: true, }, @@ -81,6 +83,116 @@ func TestHeaderMatcher(t *testing.T) { }, matching: true, }, + { + uc: "match simple header value using *", + headers: map[string][]string{ + "x-foo-bar": {"*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar", + }, + matching: true, + }, + { + uc: "match structured header value using *", + headers: map[string][]string{ + "x-foo-bar": {"*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,foo/bar", + }, + matching: true, + }, + { + uc: "do not match simple header value using */*", + headers: map[string][]string{ + "x-foo-bar": {"*/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar", + }, + matching: false, + }, + { + uc: "match structured header value using */*", + headers: map[string][]string{ + "x-foo-bar": {"*/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,foo/bar", + }, + matching: true, + }, + { + uc: "match structured wildcard header value using */*", + headers: map[string][]string{ + "x-foo-bar": {"*/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,*/*", + }, + matching: true, + }, + { + uc: "do not match structured header value using text/*", + headers: map[string][]string{ + "x-foo-bar": {"text/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,foo/bar", + }, + matching: false, + }, + { + uc: "do not match structured wildcard header value using text/*", + headers: map[string][]string{ + "x-foo-bar": {"text/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,*/*", + }, + matching: false, + }, + { + uc: "do not match structured wildcard header value using */plain", + headers: map[string][]string{ + "x-foo-bar": {"*/plain"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,*/*", + }, + matching: false, + }, + { + uc: "match structured header value using text/*", + headers: map[string][]string{ + "x-foo-bar": {"text/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,text/*", + }, + matching: true, + }, + { + uc: "do not match structured header value using application/*", + headers: map[string][]string{ + "x-foo-bar": {"application/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,text/*", + }, + matching: false, + }, + { + uc: "match structured header value using text/*", + headers: map[string][]string{ + "x-foo-bar": {"text/*"}, + }, + match: map[string]string{ + "X-Foo-Bar": "bar/foo;q=0.1;v=1,text/plain", + }, + matching: true, + }, } { t.Run("case="+tc.uc, func(t *testing.T) { matcher := HeaderMatcher(tc.headers)