Skip to content

Commit

Permalink
add regex support (#8)
Browse files Browse the repository at this point in the history
* add regex support

* update testing

---------

Co-authored-by: Frank Peters <[email protected]>
  • Loading branch information
frankforpresident and Frank Peters authored Nov 26, 2023
1 parent 22d3f0f commit 7380ca9
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 25 deletions.
9 changes: 8 additions & 1 deletion .traefik.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ testData:
- "LUE_4"
- "VALUE_5"
contains: true
required: true
required: true
- header:
name: "HEADER_4"
matchtype: one
values:
- "VALUE_\\d"
regex: true
required: true
50 changes: 35 additions & 15 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ entryPoints:
providers:
file:
filename: dynamic-dev-config.yaml

```
```yaml
Expand All @@ -44,11 +43,11 @@ http:
- checkheaders

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

middlewares:
checkheaders:
plugin:
Expand All @@ -57,43 +56,53 @@ 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:
- "VALUE_\\d"
required: true
regex: true
```
## Launch Traefik using dev config (config of plugin can be found in dynamic-dev-config.yaml)
```bash
$ docker run --rm -d -p 5000:80 containous/whoami
```

## Test using cURL

```bash
curl --location --insecure --request GET "http://localhost:4000/whoami" --header "HEADER_1: VALUE_99" --header "HEADER_2: VALUE_2" --header "HEADER_3: VALUE_3" --header "HEADER_4: VALUE_X_and_VALUE_4_and_VALUE_5_AND_6"
curl --location --insecure --request GET "http://localhost:4000/whoami" --header "HEADER_1: VALUE_99" --header "HEADER_2: VALUE_2" --header "HEADER_3: VALUE_3" --header "HEADER_4: VALUE_X_and_VALUE_4_and_VALUE_5_AND_6"
```

Should return a 200 showing details about the request.

#

## Configuration documentation

Supported configurations per header
Expand All @@ -104,12 +113,15 @@ Supported configurations per 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|

#

## Example 1 config

```yaml
middlewares:
my-checkheadersplugin:
Expand All @@ -119,39 +131,49 @@ middlewares:
- 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:
- "VALUE_\\d"
regex: true
required: true
```
## Example 2 config
You can also use this plugin to check on client certificate fields when using mTLS configuration. The [PassTLSClientCert](https://doc.traefik.io/traefik/middlewares/passtlsclientcert/) Traefik middleware adds the client certificate information to the request header `X-Forwarded-Tls-Client-Cert-Info` in a URL encoded format. Using this plugin as second middleware for route, you can verify the client certificate fields.

Example client certificate request header:

```http
X-Forwarded-Tls-Client-Cert-Info: Subject="C=US,ST=Ohio,L=Akron,O=Google,CN=server0.google.com";Issuer="DC=us,DC=google.com,DC=com,CN=GoogleRootCA";NB="1687386830";NA="1750458830";SAN="server0.google.com"
```

You could configure the plugin to check for the `CN` and the `DC` fields:

```yaml
middlewares:
my-checkheadersplugin:
Expand All @@ -161,12 +183,10 @@ middlewares:
- header:
name: "X-Forwarded-Tls-Client-Cert-Info"
matchtype: all
values:
values:
- "CN=server0.google.com"
- "DC=google.com"
contains: true
required: true
urldecode: true
```


9 changes: 8 additions & 1 deletion dynamic-dev-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,11 @@ http:
- "LUE_4"
- "VALUE_5"
contains: true
required: true
required: true
- header:
name: "HEADER_4"
matchtype: one
values:
- "VALUE_\\d"
regex: true
required: true
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/dkijkuit/checkheadersplugin

go 1.15
go 1.19
53 changes: 46 additions & 7 deletions header_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
)

//SingleHeader contains a single header keypair
// SingleHeader contains a single header keypair
type SingleHeader struct {
Name string `json:"name,omitempty"`
Values []string `json:"values,omitempty"`
Expand All @@ -18,6 +19,7 @@ type SingleHeader struct {
Contains *bool `json:"contains,omitempty"`
URLDecode *bool `json:"urldecode,omitempty"`
Debug *bool `json:"debug,omitempty"`
Regex *bool `json:"regex,omitempty"` // New field for regex support
}

// Config the plugin configuration.
Expand Down Expand Up @@ -94,8 +96,12 @@ func (a *HeaderMatch) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
reqHeaderVal, _ = url.QueryUnescape(reqHeaderVal)
}

if vHeader.IsContains() && reqHeaderVal != "" {
headersValid = checkContains(&reqHeaderVal, &vHeader)
if reqHeaderVal != "" {
if vHeader.IsContains() {
headersValid = checkContains(&reqHeaderVal, &vHeader)
} else if vHeader.IsRegex() {
headersValid = checkRegex(&reqHeaderVal, &vHeader)
}
} else {
headersValid = checkRequired(&reqHeaderVal, &vHeader)
}
Expand Down Expand Up @@ -133,6 +139,30 @@ func checkContains(requestValue *string, vHeader *SingleHeader) bool {
return true
}

func checkRegex(requestValue *string, vHeader *SingleHeader) bool {
matchCount := 0
for _, value := range vHeader.Values {
match, err := regexp.MatchString(value, *requestValue)
if err != nil {
if vHeader.IsDebug() {
fmt.Println("Error matching regex:", err)
}
return false
}
if match {
matchCount++
}
}

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

return true
}

func checkRequired(requestValue *string, vHeader *SingleHeader) bool {
matchCount := 0
for _, value := range vHeader.Values {
Expand All @@ -152,7 +182,7 @@ func checkRequired(requestValue *string, vHeader *SingleHeader) bool {
return true
}

//IsURLDecode checks whether a header value should be url decoded first before testing it
// IsURLDecode checks whether a header value should be url decoded first before testing it
func (s *SingleHeader) IsURLDecode() bool {
if s.URLDecode == nil || *s.URLDecode == false {
return false
Expand All @@ -161,7 +191,7 @@ func (s *SingleHeader) IsURLDecode() bool {
return true
}

//IsDebug checks whether a header value should print debug information in the log
// IsDebug checks whether a header value should print debug information in the log
func (s *SingleHeader) IsDebug() bool {
if s.Debug == nil || *s.Debug == false {
return false
Expand All @@ -170,7 +200,7 @@ func (s *SingleHeader) IsDebug() bool {
return true
}

//IsContains checks whether a header value should contain the configured value
// IsContains checks whether a header value should contain the configured value
func (s *SingleHeader) IsContains() bool {
if s.Contains == nil || *s.Contains == false {
return false
Expand All @@ -179,11 +209,20 @@ func (s *SingleHeader) IsContains() bool {
return true
}

//IsRequired checks whether a header is mandatory in the request, defaults to 'true'
// IsRequired checks whether a header is mandatory in the request, defaults to 'true'
func (s *SingleHeader) IsRequired() bool {
if s.Required == nil || *s.Required != false {
return true
}

return false
}

// IsRegex checks whether a header value should be matched using regular expressions
func (s *SingleHeader) IsRegex() bool {
if s.Regex == nil || *s.Regex == false {
return false
}

return true
}
26 changes: 26 additions & 0 deletions header_match_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

var required = true
var regex = true
var not_required = false
var contains = true
var urlDecode = true
Expand All @@ -24,6 +25,8 @@ func TestHeadersMatch(t *testing.T) {
"test2": "testvalue2",
"test3": "testvalue3",
"test4": "value4",
"testNumberRegex": "12345",
"testCountryCodeRegex": "NL",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "value5_or_value1_or_value_2_or_value_3",
}
Expand All @@ -37,6 +40,8 @@ func TestHeadersOneMatch(t *testing.T) {
"test2": "testvalue2",
"test3": "testvalue3",
"test4": "value4",
"testNumberRegex": "12345",
"testCountryCodeRegex": "GB",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "test_or_value2",
}
Expand All @@ -50,6 +55,8 @@ func TestHeadersNotMatch(t *testing.T) {
"test2": "wrongvalue2",
"test3": "wrongvalue3",
"test4": "correctvalue4",
"testNumberRegex": "abcde",
"testCountryCodeRegex": "DE",
"X-Forwarded-Tls-Client-Cert-Info": "wrongvalue",
"testMultipleContainsValues": "wrongvalues",
}
Expand All @@ -62,6 +69,8 @@ func TestHeadersNotRequired(t *testing.T) {
"test1": "testvalue1",
"test2": "testvalue2",
"test4": "ue4",
"testNumberRegex": "12345",
"testCountryCodeRegex": "FR",
"X-Forwarded-Tls-Client-Cert-Info": testcert,
"testMultipleContainsValues": "value5_or_value1_or_value_2_or_value_3",
}
Expand Down Expand Up @@ -129,6 +138,23 @@ func executeTest(t *testing.T, requestHeaders map[string]string, expectedResultC
Contains: &contains,
URLDecode: &urlDecode,
},

// Adding headers with regex support
{
Name: "testNumberRegex",
MatchType: string(checkheaders.MatchOne),
Values: []string{"\\d{5}"},
Regex: &regex,
Required: &required,
},
//match country codes
{
Name: "testCountryCodeRegex",
MatchType: string(checkheaders.MatchOne),
Values: []string{"^NL|GB|FR$"},
Regex: &regex,
Required: &required,
},
}

ctx := context.Background()
Expand Down

0 comments on commit 7380ca9

Please sign in to comment.