diff --git a/.traefik.yml b/.traefik.yml index 7aa624e..7a92cdb 100644 --- a/.traefik.yml +++ b/.traefik.yml @@ -3,31 +3,31 @@ 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 @@ -35,7 +35,7 @@ testData: - header: name: "HEADER_4" matchtype: one - values: + values: - "VALUE_\\d" regex: true required: true diff --git a/README.MD b/README.MD index 007919e..622b77c 100644 --- a/README.MD +++ b/README.MD @@ -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 | # @@ -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 +``` diff --git a/dynamic-dev-config.yaml b/dynamic-dev-config.yaml index cc41288..725b470 100644 --- a/dynamic-dev-config.yaml +++ b/dynamic-dev-config.yaml @@ -11,11 +11,11 @@ http: - checkheaders services: - service-whoami: + service-whoami: loadBalancer: servers: - url: http://127.0.0.1:5000 - + middlewares: checkheaders: plugin: @@ -24,24 +24,24 @@ 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 @@ -49,7 +49,7 @@ http: - header: name: "HEADER_4" matchtype: one - values: + values: - "VALUE_\\d" regex: true - required: true \ No newline at end of file + required: true diff --git a/header_match.go b/header_match.go index b77df97..94b2297 100644 --- a/header_match.go +++ b/header_match.go @@ -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. @@ -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 { @@ -122,7 +128,13 @@ 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) { @@ -130,6 +142,13 @@ func checkContains(requestValue *string, vHeader *SingleHeader) bool { } } + 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) { @@ -139,23 +158,39 @@ 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 } @@ -163,9 +198,17 @@ func checkRegex(requestValue *string, vHeader *SingleHeader) bool { 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++ } @@ -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 } diff --git a/header_match_test.go b/header_match_test.go index 4b14d19..f7a91c5 100644 --- a/header_match_test.go +++ b/header_match_test.go @@ -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) @@ -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) @@ -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) @@ -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) @@ -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", @@ -155,6 +176,16 @@ func executeTest(t *testing.T, requestHeaders map[string]string, expectedResultC Regex: ®ex, 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()