Skip to content

Commit

Permalink
Add matchtype 'none' (dkijkuit#9)
Browse files Browse the repository at this point in the history
* fix require check when value is not empty

* add matchtype none

* documention how to use matchtype none

* unescape example

---------

Co-authored-by: Frank Peters <frank.peters@theoplayer.com>
  • Loading branch information
frankforpresident and Frank Peters authored Dec 18, 2023
1 parent 7380ca9 commit ba652bb
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 31 deletions.
12 changes: 6 additions & 6 deletions .traefik.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,39 @@ type: middleware

import: github.com/dkijkuit/checkheadersplugin

summary: 'Checks the incoming request for specific headers and their values to be present and matching the configuration.'
summary: "Checks the incoming request for specific headers and their values to be present and matching the configuration."

testData:
headers:
- header:
name: "HEADER_1"
matchtype: one
values:
values:
- "VALUE_1"
- "VALUE_99"
- header:
name: "HEADER_2"
matchtype: one
values:
values:
- "VALUE_2"
- header:
name: "HEADER_3"
matchtype: one
values:
values:
- "VALUE_3"
required: false
- header:
name: "HEADER_4"
matchtype: all
values:
values:
- "LUE_4"
- "VALUE_5"
contains: true
required: true
- header:
name: "HEADER_4"
matchtype: one
values:
values:
- "VALUE_\\d"
regex: true
required: true
57 changes: 47 additions & 10 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,16 @@ Should return a 200 showing details about the request.

Supported configurations per header

| Setting | Allowed values | Description |
| :-- | :-- | :-- |
| name | string | Name of the request header |
| matchtype | one, all | Match on all values or one of the values specified. The value 'all' is only allowed in combination with the 'contains' setting.|
| values | []string | A list of allowed values which are matched against the request header value|
| contains | boolean | If set to true (default false), the request is allowed if the rtequest header value contains the value specified in the configuration |
| regex | boolean | If set to true (default false), the match is done using a regular expression. The value of the request header is matched against the value specified in the configuration. |
| required | boolean | If set to false (default true), the request is allowed if the header is absent or the value is empty|
| urldecode | boolean | If set to true (default false), the value of the request header will be URL decoded before further processing with the plugin. This is useful when using this plugin with the [PassTLSClientCert](https://doc.traefik.io/traefik/middlewares/passtlsclientcert/) middleware that Traefik offers.
| debug | boolean | If set to true (default false), the request headers, values and validation will be printed to the console|
| Setting | Allowed values | Description |
| :-------- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | Name of the request header |
| matchtype | one, all, none | Match on all values, one of the values specified or none of the values. The value 'all' is only allowed in combination with the 'contains' and 'regex' setting. |
| values | []string | A list of allowed values which are matched against the request header value |
| contains | boolean | If set to true (default false), the request is allowed if the request header value contains the value specified in the configuration |
| regex | boolean | If set to true (default false), the match is done using a regular expression. The value of the request header is matched against the value specified in the configuration. via the [regexp](https://pkg.go.dev/regexp) package |
| required | boolean | If set to false (default true), the request is allowed if the header is absent or the value is empty |
| urldecode | boolean | If set to true (default false), the value of the request header will be URL decoded before further processing with the plugin. This is useful when using this plugin with the [PassTLSClientCert](https://doc.traefik.io/traefik/middlewares/passtlsclientcert/) middleware that Traefik offers. |
| debug | boolean | If set to true (default false), the request headers, values and validation will be printed to the console |

#

Expand Down Expand Up @@ -190,3 +190,40 @@ middlewares:
required: true
urldecode: true
```

## Example 3 config

This plugin give you also the possibility to validate header via a regular expression. This can be useful when you want to validate a header value against a pattern. For example, you want to validate a JWT token in the `Authorization` header. The JWT token has a specific format and you can validate this with a regular expression.

```yaml
middlewares:
my-checkheadersplugin:
plugin:
checkheadersplugin:
headers:
- header:
name: "Authorization"
matchtype: one
values:
- "^Bearer .*"
regex: true
```

## Example 4 config

You can also use this plugin to check if header has a certain value that is not allowed. This way you can allow every value except a the provide ones, acting as blacklist. For example, you want to block requests that have `Content-Language` header that are set to `de-DE` or `de-AT`. You can use 'none' in combination with matchtype 'regex' or 'contains'.

```yaml
middlewares:
my-checkheadersplugin:
plugin:
checkheadersplugin:
headers:
- header:
name: "Content-Language"
matchtype: none
values:
- "de-DE"
- "de-AT"
required: true
```
16 changes: 8 additions & 8 deletions dynamic-dev-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ http:
- checkheaders

services:
service-whoami:
service-whoami:
loadBalancer:
servers:
- url: http://127.0.0.1:5000

middlewares:
checkheaders:
plugin:
Expand All @@ -24,32 +24,32 @@ http:
- header:
name: "HEADER_1"
matchtype: one
values:
values:
- "VALUE_1"
- "VALUE_99"
- header:
name: "HEADER_2"
matchtype: one
values:
values:
- "VALUE_2"
- header:
name: "HEADER_3"
matchtype: one
values:
values:
- "VALUE_3"
required: false
- header:
name: "HEADER_4"
matchtype: all
values:
values:
- "LUE_4"
- "VALUE_5"
contains: true
required: true
- header:
name: "HEADER_4"
matchtype: one
values:
values:
- "VALUE_\\d"
regex: true
required: true
required: true
62 changes: 56 additions & 6 deletions header_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const (
MatchAll MatchType = "all"
//MatchOne requires only one value to be matched
MatchOne MatchType = "one"
//MatchNone requires none of the values to be matched
MatchNone MatchType = "none"
)

// CreateConfig creates the default plugin configuration.
Expand Down Expand Up @@ -101,13 +103,17 @@ func (a *HeaderMatch) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
headersValid = checkContains(&reqHeaderVal, &vHeader)
} else if vHeader.IsRegex() {
headersValid = checkRegex(&reqHeaderVal, &vHeader)
} else {
headersValid = checkRequired(&reqHeaderVal, &vHeader)
}
} else {
headersValid = checkRequired(&reqHeaderVal, &vHeader)
}

if vHeader.IsDebug() {
fmt.Println("checkheaders (debug):\n\tHeaders valid:", headersValid, "\n\tRequest headers:", reqHeaderVal, "\n\tConfigured headers:", vHeader.Values)
fmt.Println("checkheaders (debug): Headers valid:", headersValid)
fmt.Println("checkheaders (debug): Request headers:", reqHeaderVal)
fmt.Println("checkheaders (debug): Configured headers:", vHeader.Values)
}

if !headersValid {
Expand All @@ -122,14 +128,27 @@ func (a *HeaderMatch) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}

// checkContains checks whether a header value contains the configured value
func checkContains(requestValue *string, vHeader *SingleHeader) bool {

if vHeader.IsDebug() {
fmt.Println("checkheaders (debug): Validating contains:", *requestValue, vHeader.Values)
}

matchCount := 0
for _, value := range vHeader.Values {
if strings.Contains(*requestValue, value) {
matchCount++
}
}

if vHeader.MatchType == string(MatchNone) {
if matchCount == 0 {
return true
}
return false
}

if matchCount == 0 {
return false
} else if vHeader.MatchType == string(MatchAll) && matchCount != len(vHeader.Values) {
Expand All @@ -139,33 +158,57 @@ func checkContains(requestValue *string, vHeader *SingleHeader) bool {
return true
}

// checkRegex checks whether a header value matches the configured regex
func checkRegex(requestValue *string, vHeader *SingleHeader) bool {

if vHeader.IsDebug() {
fmt.Println("checkheaders (debug): Validating:", *requestValue, "with regex:", vHeader.Values)
}

matchCount := 0
for _, value := range vHeader.Values {
match, err := regexp.MatchString(value, *requestValue)
if err != nil {

if err == nil {
if match {
matchCount++
}
} else {
if vHeader.IsDebug() {
fmt.Println("Error matching regex:", err)
fmt.Println("checkheaders (debug): ERROR matching regex:", err)
}
return false
}
if match {
matchCount++

}

if vHeader.MatchType == string(MatchNone) {
if matchCount == 0 {
return true
}
return false
}

if matchCount == 0 {
return false

} else if vHeader.MatchType == string(MatchAll) && matchCount != len(vHeader.Values) {
return false
}

return true
}

// checkRequired checks whether a header value is required in the request
// if the header is not required, it will also return true if the header is not present in the request
func checkRequired(requestValue *string, vHeader *SingleHeader) bool {

if vHeader.IsDebug() {
fmt.Println("checkheaders (debug): Validating required:", *requestValue, vHeader.Values)
}

matchCount := 0
for _, value := range vHeader.Values {
// if the header is required, it should match the configured value
if *requestValue == value {
matchCount++
}
Expand All @@ -175,6 +218,13 @@ func checkRequired(requestValue *string, vHeader *SingleHeader) bool {
}
}

if vHeader.MatchType == string(MatchNone) {
if matchCount == 0 {
return true
}
return false
}

if matchCount == 0 {
return false
}
Expand Down
33 changes: 32 additions & 1 deletion header_match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestHeadersMatch(t *testing.T) {
"testCountryCodeRegex": "NL",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "value5_or_value1_or_value_2_or_value_3",
"testNoneMatch": "none_value",
}

executeTest(t, requestHeaders, http.StatusOK)
Expand All @@ -44,6 +45,7 @@ func TestHeadersOneMatch(t *testing.T) {
"testCountryCodeRegex": "GB",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "test_or_value2",
"testNoneMatch": "none_value",
}

executeTest(t, requestHeaders, http.StatusOK)
Expand All @@ -59,6 +61,25 @@ func TestHeadersNotMatch(t *testing.T) {
"testCountryCodeRegex": "DE",
"X-Forwarded-Tls-Client-Cert-Info": "wrongvalue",
"testMultipleContainsValues": "wrongvalues",
"testNoneMatch": "not_allowed_value_1",
}

executeTest(t, requestHeaders, http.StatusForbidden)
}

func TestHeadersNotMatchWhenSomeAreCorrect(t *testing.T) {
requestHeaders := map[string]string{
//wrong values
"test1": "should_not_match",
"test2": "should_not_match",
"test3": "should_not_match",
//correct values
"test4": "value4",
"testNumberRegex": "12345",
"testCountryCodeRegex": "NL",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "value5_or_value1_or_value_2_or_value_3",
"testNoneMatch": "none_value",
}

executeTest(t, requestHeaders, http.StatusForbidden)
Expand All @@ -73,6 +94,7 @@ func TestHeadersNotRequired(t *testing.T) {
"testCountryCodeRegex": "FR",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "value5_or_value1_or_value_2_or_value_3",
"testNoneMatch": "none_value",
}

executeTest(t, requestHeaders, http.StatusOK)
Expand Down Expand Up @@ -138,7 +160,6 @@ func executeTest(t *testing.T, requestHeaders map[string]string, expectedResultC
Contains: &contains,
URLDecode: &urlDecode,
},

// Adding headers with regex support
{
Name: "testNumberRegex",
Expand All @@ -155,6 +176,16 @@ func executeTest(t *testing.T, requestHeaders map[string]string, expectedResultC
Regex: &regex,
Required: &required,
},
//match none
{
Name: "testNoneMatch",
MatchType: string(checkheaders.MatchNone),
Values: []string{
"not_allowed_value_1",
"not_allowed_value_2",
},
Required: &required,
},
}

ctx := context.Background()
Expand Down

0 comments on commit ba652bb

Please sign in to comment.