Skip to content

Commit

Permalink
fix: Header value matching using wildcards fixed (#485)
Browse files Browse the repository at this point in the history
  • Loading branch information
dadrus authored Feb 8, 2023
1 parent 6d31d01 commit cf3ed57
Showing 3 changed files with 188 additions and 15 deletions.
8 changes: 5 additions & 3 deletions docs/content/docs/configuration/reference/types.adoc
Original file line number Diff line number Diff line change
@@ -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
75 changes: 67 additions & 8 deletions internal/rules/mechanisms/errorhandlers/matcher/header_matcher.go
Original file line number Diff line number Diff line change
@@ -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
}
120 changes: 116 additions & 4 deletions internal/rules/mechanisms/errorhandlers/matcher/header_matcher_test.go
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit cf3ed57

Please sign in to comment.