diff --git a/deploy/manifests/nginx-conf.yaml b/deploy/manifests/nginx-conf.yaml index ddb49f42d4..c1747aeba9 100644 --- a/deploy/manifests/nginx-conf.yaml +++ b/deploy/manifests/nginx-conf.yaml @@ -15,6 +15,10 @@ data: http { include /etc/nginx/conf.d/*.conf; js_import /usr/lib/nginx/modules/njs/httpmatches.js; + proxy_headers_hash_bucket_size 512; + proxy_headers_hash_max_size 1024; server_names_hash_bucket_size 256; server_names_hash_max_size 1024; + variables_hash_bucket_size 512; + variables_hash_max_size 1024; } diff --git a/docs/gateway-api-compatibility.md b/docs/gateway-api-compatibility.md index 4c7ca36374..ccd0926bb9 100644 --- a/docs/gateway-api-compatibility.md +++ b/docs/gateway-api-compatibility.md @@ -111,7 +111,8 @@ Fields: * `filters` * `type` - supported. * `requestRedirect` - supported except for the experimental `path` field. If multiple filters with `requestRedirect` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest. - * `requestHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported. + * `requestHeaderModifier` - supported. If multiple filters with `requestHeaderModifier` are configured, NGINX Kubernetes Gateway will choose the first one and ignore the rest. + * `responseHeaderModifier`, `requestMirror`, `urlRewrite`, `extensionRef` - not supported. * `backendRefs` - partially supported. Backend ref `filters` are not supported. * `status` * `parents` diff --git a/examples/http-header-filter/README.md b/examples/http-header-filter/README.md new file mode 100644 index 0000000000..6267b7f73d --- /dev/null +++ b/examples/http-header-filter/README.md @@ -0,0 +1,70 @@ +# Example + +In this example we will deploy NGINX Kubernetes Gateway and configure traffic routing for a simple echo server. +We will use `HTTPRoute` resources to route traffic to the echo server, using the RequestHeaderModifier filter to modify +headers to the request. +## Running the Example + +## 1. Deploy NGINX Kubernetes Gateway + +1. Follow the [installation instructions](/docs/installation.md) to deploy NGINX Gateway. + +1. Save the public IP address of NGINX Kubernetes Gateway into a shell variable: + + ``` + GW_IP=XXX.YYY.ZZZ.III + ``` + +1. Save the port of NGINX Kubernetes Gateway: + + ``` + GW_PORT= + ``` + +## 2. Deploy the Cafe Application + +1. Create the headers Deployment and Service: + + ``` + kubectl apply -f headers.yaml + ``` + +1. Check that the Pod is running in the `default` namespace: + + ``` + kubectl -n default get pods + NAME READY STATUS RESTARTS AGE + headers-6f4b79b975-2sb28 1/1 Running 0 12s + ``` + +## 3. Configure Routing + +1. Create the `Gateway`: + + ``` + kubectl apply -f gateway.yaml + ``` + +1. Create the `HTTPRoute` resources: + + ``` + kubectl apply -f echo-route.yaml + ``` + +## 4. Test the Application + +To access the application, we will use `curl` to send requests to the `headers` Service, including sending headers with +our request. +Notice our configured header values can be seen in the `requestHeaders` section below, and that the `User-Agent` header +is absent. + +``` +curl -s --resolve echo.example.com:$GW_PORT:$GW_IP http://echo.example.com:$GW_PORT/headers -H "My-Cool-Header:my-client-value" -H "My-Overwrite-Header:dont-see-this" +Headers: + header 'Accept-Encoding' is 'compress' + header 'My-cool-header' is 'my-client-value, this-is-an-appended-value' + header 'My-Overwrite-Header' is 'this-is-the-only-value' + header 'Host' is 'echo.example.com' + header 'Connection' is 'close' + header 'Accept' is '*/*' +``` diff --git a/examples/http-header-filter/echo-route.yaml b/examples/http-header-filter/echo-route.yaml new file mode 100644 index 0000000000..b3f5da29dd --- /dev/null +++ b/examples/http-header-filter/echo-route.yaml @@ -0,0 +1,31 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: headers +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "echo.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /headers + filters: + - type: RequestHeaderModifier + requestHeaderModifier: + set: + - name: My-Overwrite-Header + value: this-is-the-only-value + add: + - name: Accept-Encoding + value: compress + - name: My-cool-header + value: this-is-an-appended-value + remove: + - User-Agent + backendRefs: + - name: headers + port: 80 diff --git a/examples/http-header-filter/gateway.yaml b/examples/http-header-filter/gateway.yaml new file mode 100644 index 0000000000..9fb0ebd1af --- /dev/null +++ b/examples/http-header-filter/gateway.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: Gateway +metadata: + name: gateway + labels: + domain: k8s-gateway.nginx.org +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP diff --git a/examples/http-header-filter/headers.yaml b/examples/http-header-filter/headers.yaml new file mode 100644 index 0000000000..a15cd03798 --- /dev/null +++ b/examples/http-header-filter/headers.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headers +spec: + replicas: 1 + selector: + matchLabels: + app: headers + template: + metadata: + labels: + app: headers + spec: + containers: + - name: headers + image: nginx + ports: + - containerPort: 8080 + volumeMounts: + - name: config-volume + mountPath: /etc/nginx + readOnly: true + volumes: + - name: config-volume + configMap: + name: headers-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: headers-config +data: + nginx.conf: |- + user nginx; + worker_processes 1; + + pid /var/run/nginx.pid; + + load_module /usr/lib/nginx/modules/ngx_http_js_module.so; + + events {} + + http { + default_type text/plain; + + js_import /etc/nginx/headers.js; + js_set $headers headers.getRequestHeaders; + + server { + listen 8080; + return 200 "$headers"; + } + } + headers.js: |- + function getRequestHeaders(r) { + let s = "Headers:\n"; + for (let h in r.headersIn) { + s += ` header '${h}' is '${r.headersIn[h]}'\n`; + } + return s; + } + export default {getRequestHeaders}; + +--- +apiVersion: v1 +kind: Service +metadata: + name: headers +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: headers diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 0e756d36dc..5af89b6acc 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -42,5 +42,6 @@ func getExecuteFuncs() []executeFunc { executeUpstreams, executeSplitClients, executeServers, + executeMaps, } } diff --git a/internal/nginx/config/http/config.go b/internal/nginx/config/http/config.go index f61b21de66..7b75004c6a 100644 --- a/internal/nginx/config/http/config.go +++ b/internal/nginx/config/http/config.go @@ -12,12 +12,19 @@ type Server struct { // Location holds all configuration for an HTTP location. type Location struct { - Return *Return - Path string - ProxyPass string - HTTPMatchVar string - Internal bool - Exact bool + Return *Return + Path string + ProxyPass string + HTTPMatchVar string + ProxySetHeaders []Header + Internal bool + Exact bool +} + +// Header defines a HTTP header to be passed to the proxied server. +type Header struct { + Name string + Value string } // Return represents an HTTP return. @@ -66,3 +73,16 @@ type SplitClientDistribution struct { Percent string Value string } + +// Map defines an NGINX map. +type Map struct { + Source string + Variable string + Parameters []MapParameter +} + +// Parameter defines a Value and Result pair in a Map. +type MapParameter struct { + Value string + Result string +} diff --git a/internal/nginx/config/maps.go b/internal/nginx/config/maps.go new file mode 100644 index 0000000000..4d1aa3a1b1 --- /dev/null +++ b/internal/nginx/config/maps.go @@ -0,0 +1,72 @@ +package config + +import ( + "strings" + gotemplate "text/template" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" +) + +var mapsTemplate = gotemplate.Must(gotemplate.New("maps").Parse(mapsTemplateText)) + +func executeMaps(conf dataplane.Configuration) []byte { + maps := createMaps(append(conf.HTTPServers, conf.SSLServers...)) + return execute(mapsTemplate, maps) +} + +func createMaps(servers []dataplane.VirtualServer) []http.Map { + return buildAddHeaderMaps(servers) +} + +func buildAddHeaderMaps(servers []dataplane.VirtualServer) []http.Map { + addHeaderNames := make(map[string]struct{}) + + for _, s := range servers { + for _, pr := range s.PathRules { + for _, mr := range pr.MatchRules { + if mr.Filters.RequestHeaderModifiers != nil { + for _, addHeader := range mr.Filters.RequestHeaderModifiers.Add { + lowerName := strings.ToLower(addHeader.Name) + if _, ok := addHeaderNames[lowerName]; !ok { + addHeaderNames[lowerName] = struct{}{} + } + } + } + } + } + } + + maps := make([]http.Map, 0, len(addHeaderNames)) + for m := range addHeaderNames { + maps = append(maps, createAddHeadersMap(m)) + } + return maps +} + +const ( + // In order to prepend any passed client header values to values specified in the add headers field of request + // header modifiers, we need to create a map parameter regex for any string value + anyStringFmt = `~.*` +) + +func createAddHeadersMap(name string) http.Map { + underscoreName := convertStringToSafeVariableName(name) + httpVarSource := "${http_" + underscoreName + "}" + mapVarName := generateAddHeaderMapVariableName(name) + params := []http.MapParameter{ + { + Value: "default", + Result: "''", + }, + { + Value: anyStringFmt, + Result: httpVarSource + ",", + }, + } + return http.Map{ + Source: httpVarSource, + Variable: "$" + mapVarName, + Parameters: params, + } +} diff --git a/internal/nginx/config/maps_template.go b/internal/nginx/config/maps_template.go new file mode 100644 index 0000000000..4f988fc8cf --- /dev/null +++ b/internal/nginx/config/maps_template.go @@ -0,0 +1,11 @@ +package config + +var mapsTemplateText = ` +{{ range $m := . }} +map {{ $m.Source }} {{ $m.Variable }} { + {{ range $p := $m.Parameters }} + {{ $p.Value }} {{ $p.Result }}; + {{ end }} +} +{{- end }} +` diff --git a/internal/nginx/config/maps_test.go b/internal/nginx/config/maps_test.go new file mode 100644 index 0000000000..39846b3b90 --- /dev/null +++ b/internal/nginx/config/maps_test.go @@ -0,0 +1,186 @@ +package config + +import ( + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" +) + +func TestExecuteMaps(t *testing.T) { + g := NewGomegaWithT(t) + pathRules := []dataplane.PathRule{ + { + MatchRules: []dataplane.MatchRule{ + { + Filters: dataplane.Filters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "my-add-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + { + Filters: dataplane.Filters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "my-second-add-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + { + Filters: dataplane.Filters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Set: []dataplane.HTTPHeader{ + { + Name: "my-set-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + }, + }, + } + + conf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ + { + PathRules: pathRules, + }, + { + PathRules: pathRules, + }, + { + IsDefault: true, + }, + }, + SSLServers: []dataplane.VirtualServer{ + { + PathRules: pathRules, + }, + { + IsDefault: true, + }, + }, + } + + expSubStrings := map[string]int{ + "map ${http_my_add_header} $my_add_header_header_var {": 1, + "default '';": 2, + "~.* ${http_my_add_header},": 1, + "map ${http_my_second_add_header} $my_second_add_header_header_var {": 1, + "~.* ${http_my_second_add_header},;": 1, + "map ${http_my_set_header} $my_set_header_header_var {": 0, + } + + maps := string(executeMaps(conf)) + for expSubStr, expCount := range expSubStrings { + g.Expect(expCount).To(Equal(strings.Count(maps, expSubStr))) + } +} + +func TestBuildAddHeaderMaps(t *testing.T) { + g := NewGomegaWithT(t) + pathRules := []dataplane.PathRule{ + { + MatchRules: []dataplane.MatchRule{ + { + Filters: dataplane.Filters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "my-add-header", + Value: "some-value-123", + }, + { + Name: "my-add-header", + Value: "some-value-123", + }, + { + Name: "my-second-add-header", + Value: "some-value-123", + }, + }, + Set: []dataplane.HTTPHeader{ + { + Name: "my-set-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + { + Filters: dataplane.Filters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Set: []dataplane.HTTPHeader{ + { + Name: "my-set-header", + Value: "some-value-123", + }, + }, + Add: []dataplane.HTTPHeader{ + { + Name: "my-add-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + }, + }, + } + + testServers := []dataplane.VirtualServer{ + { + PathRules: pathRules, + }, + { + PathRules: pathRules, + }, + { + IsDefault: true, + }, + } + expectedMap := []http.Map{ + { + Source: "${http_my_add_header}", + Variable: "$my_add_header_header_var", + Parameters: []http.MapParameter{ + {Value: "default", Result: "''"}, + { + Value: "~.*", + Result: "${http_my_add_header},", + }, + }, + }, + { + Source: "${http_my_second_add_header}", + Variable: "$my_second_add_header_header_var", + Parameters: []http.MapParameter{ + {Value: "default", Result: "''"}, + { + Value: "~.*", + Result: "${http_my_second_add_header},", + }, + }, + }, + } + maps := buildAddHeaderMaps(testServers) + + g.Expect(maps).To(ConsistOf(expectedMap)) +} diff --git a/internal/nginx/config/servers.go b/internal/nginx/config/servers.go index 2418ae122d..a449264081 100644 --- a/internal/nginx/config/servers.go +++ b/internal/nginx/config/servers.go @@ -142,6 +142,7 @@ func createLocations(pathRules []dataplane.PathRule, listenerPort int32) []http. } backendName := backendGroupName(r.BackendGroup) + loc.ProxySetHeaders = generateProxySetHeaders(r.Filters.RequestHeaderModifiers) if backendGroupNeedsSplit(r.BackendGroup) { loc.ProxyPass = createProxyPassForVar(backendName) @@ -308,6 +309,52 @@ func createMatchLocation(path string) http.Location { } } +func generateProxySetHeaders(filters *dataplane.HTTPHeaderFilter) []http.Header { + if filters == nil { + return nil + } + proxySetHeaders := make([]http.Header, 0, len(filters.Add)+len(filters.Set)+len(filters.Remove)) + if len(filters.Add) > 0 { + addHeaders := convertAddHeaders(filters.Add) + proxySetHeaders = append(proxySetHeaders, addHeaders...) + } + if len(filters.Set) > 0 { + setHeaders := convertSetHeaders(filters.Set) + proxySetHeaders = append(proxySetHeaders, setHeaders...) + } + // If the value of a header field is an empty string then this field will not be passed to a proxied server + for _, h := range filters.Remove { + proxySetHeaders = append(proxySetHeaders, http.Header{ + Name: h, + Value: "", + }) + } + return proxySetHeaders +} + +func convertAddHeaders(headers []dataplane.HTTPHeader) []http.Header { + locHeaders := make([]http.Header, 0, len(headers)) + for _, h := range headers { + mapVarName := "${" + generateAddHeaderMapVariableName(h.Name) + "}" + locHeaders = append(locHeaders, http.Header{ + Name: h.Name, + Value: mapVarName + h.Value, + }) + } + return locHeaders +} + +func convertSetHeaders(headers []dataplane.HTTPHeader) []http.Header { + locHeaders := make([]http.Header, 0, len(headers)) + for _, h := range headers { + locHeaders = append(locHeaders, http.Header{ + Name: h.Name, + Value: h.Value, + }) + } + return locHeaders +} + func createPathForMatch(path string, pathType dataplane.PathType, routeIdx int) string { return fmt.Sprintf("%s_%s_route%d", path, pathType, routeIdx) } diff --git a/internal/nginx/config/servers_template.go b/internal/nginx/config/servers_template.go index 24be76de7b..98a00e6997 100644 --- a/internal/nginx/config/servers_template.go +++ b/internal/nginx/config/servers_template.go @@ -47,6 +47,9 @@ server { {{ end }} {{- if $l.ProxyPass -}} + {{ range $h := $l.ProxySetHeaders }} + proxy_set_header {{ $h.Name }} "{{ $h.Value }}"; + {{- end }} proxy_set_header Host $host; proxy_pass {{ $l.ProxyPass }}$request_uri; {{- end }} diff --git a/internal/nginx/config/servers_test.go b/internal/nginx/config/servers_test.go index ee97d3f710..7fc388e475 100644 --- a/internal/nginx/config/servers_test.go +++ b/internal/nginx/config/servers_test.go @@ -315,6 +315,30 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + // A match with requestHeaderModifier filter set + Matches: []v1beta1.HTTPRouteMatch{ + { + Path: &v1beta1.HTTPPathMatch{ + Value: helpers.GetStringPointer("/proxy-set-headers"), + Type: helpers.GetPointer(v1beta1.PathMatchPathPrefix), + }, + }, + }, + Filters: []v1beta1.HTTPRouteFilter{ + { + Type: "RequestHeaderModifier", + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Add: []v1beta1.HTTPHeader{ + { + Name: "my-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + }, }, }, } @@ -493,6 +517,28 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "/proxy-set-headers", + PathType: dataplane.PathTypePrefix, + MatchRules: []dataplane.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 8, + Source: hr, + BackendGroup: fooGroup, + Filters: dataplane.Filters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "my-header", + Value: "some-value-123", + }, + }, + }, + }, + }, + }, + }, } httpServers := []dataplane.VirtualServer{ @@ -622,6 +668,16 @@ func TestCreateServers(t *testing.T) { HTTPMatchVar: expectedMatchString(exactMatches), Exact: true, }, + { + Path: "/proxy-set-headers", + ProxyPass: "http://test_foo_80", + ProxySetHeaders: []http.Header{ + { + Name: "my-header", + Value: "${my_header_header_var}some-value-123", + }, + }, + }, } } @@ -1173,15 +1229,15 @@ func TestCreateProxyPassForVar(t *testing.T) { } func TestCreateMatchLocation(t *testing.T) { + g := NewGomegaWithT(t) + expected := http.Location{ Path: "/path", Internal: true, } result := createMatchLocation("/path") - if result != expected { - t.Errorf("createMatchLocation() returned %v but expected %v", result, expected) - } + g.Expect(result).To(Equal(expected)) } func TestCreatePathForMatch(t *testing.T) { @@ -1207,3 +1263,40 @@ func TestCreatePathForMatch(t *testing.T) { g.Expect(result).To(Equal(tc.expected)) } } + +func TestGenerateProxySetHeaders(t *testing.T) { + g := NewGomegaWithT(t) + + filters := dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "Authorization", + Value: "my-auth", + }, + }, + Set: []dataplane.HTTPHeader{ + { + Name: "Accept-Encoding", + Value: "gzip", + }, + }, + Remove: []string{"my-header"}, + } + expectedHeaders := []http.Header{ + { + Name: "Authorization", + Value: "${authorization_header_var}my-auth", + }, + { + Name: "Accept-Encoding", + Value: "gzip", + }, + { + Name: "my-header", + Value: "", + }, + } + + headers := generateProxySetHeaders(&filters) + g.Expect(headers).To(Equal(expectedHeaders)) +} diff --git a/internal/nginx/config/validation/common.go b/internal/nginx/config/validation/common.go index dd409bc474..988e7848fe 100644 --- a/internal/nginx/config/validation/common.go +++ b/internal/nginx/config/validation/common.go @@ -3,6 +3,7 @@ package validation import ( "errors" "regexp" + "strings" k8svalidation "k8s.io/apimachinery/pkg/util/validation" ) @@ -46,3 +47,21 @@ func validateEscapedStringNoVarExpansion(value string, examples []string) error } return nil } + +const ( + invalidHostHeaderErrMsg string = "redefining the Host request header is not supported" + maxHeaderLength int = 256 +) + +func validateHeaderName(name string) error { + if len(name) > maxHeaderLength { + return errors.New(k8svalidation.MaxLenError(maxHeaderLength)) + } + if msg := k8svalidation.IsHTTPHeaderName(name); msg != nil { + return errors.New(msg[0]) + } + if strings.ToLower(name) == "host" { + return errors.New(invalidHostHeaderErrMsg) + } + return nil +} diff --git a/internal/nginx/config/validation/common_test.go b/internal/nginx/config/validation/common_test.go index 711b316b80..925d5ac8fc 100644 --- a/internal/nginx/config/validation/common_test.go +++ b/internal/nginx/config/validation/common_test.go @@ -1,6 +1,7 @@ package validation import ( + "strings" "testing" ) @@ -30,3 +31,23 @@ func TestValidateEscapedStringNoVarExpansion(t *testing.T) { `test"test`, `$test`) } + +func TestValidateValidHeaderName(t *testing.T) { + validator := func(value string) error { return validateHeaderName(value) } + + testValidValuesForSimpleValidator(t, validator, + `Content-Encoding`, + `X-Forwarded-For`, + // max supported length is 256, generate string with 16*16 chars (256) + strings.Repeat("very-long-header", 16)) + testInvalidValuesForSimpleValidator(t, validator, + `\`, + `test test`, + `test"test`, + `$test`, + "Host", + "host", + "my-header[]", + "my-header&", + strings.Repeat("very-long-header", 16)+"1") +} diff --git a/internal/nginx/config/validation/http_filters.go b/internal/nginx/config/validation/http_filters.go index 4875d28c3d..e505f94276 100644 --- a/internal/nginx/config/validation/http_filters.go +++ b/internal/nginx/config/validation/http_filters.go @@ -4,6 +4,10 @@ package validation // For example, return 302 "https://example.com:8080"; type HTTPRedirectValidator struct{} +// HTTPRequestHeaderValidator validates values for request headers, +// which in NGINX is done with the proxy_set_header directive. +type HTTPRequestHeaderValidator struct{} + var supportedRedirectSchemes = map[string]struct{}{ "http": {}, "https": {}, @@ -39,3 +43,14 @@ var supportedRedirectStatusCodes = map[int]struct{}{ func (HTTPRedirectValidator) ValidateRedirectStatusCode(statusCode int) (valid bool, supportedValues []string) { return validateInSupportedValues(statusCode, supportedRedirectStatusCodes) } + +func (HTTPRequestHeaderValidator) ValidateRequestHeaderName(name string) error { + return validateHeaderName(name) +} + +var requestHeaderValueExamples = []string{"my-header-value", "example/12345=="} + +func (HTTPRequestHeaderValidator) ValidateRequestHeaderValue(value string) error { + // Variables in header values are supported by NGINX but not required by the Gateway API. + return validateEscapedStringNoVarExpansion(value, requestHeaderValueExamples) +} diff --git a/internal/nginx/config/validation/http_filters_test.go b/internal/nginx/config/validation/http_filters_test.go index 7a11b061dc..d3abeb1093 100644 --- a/internal/nginx/config/validation/http_filters_test.go +++ b/internal/nginx/config/validation/http_filters_test.go @@ -44,3 +44,27 @@ func TestValidateRedirectStatusCode(t *testing.T) { testInvalidValuesForSupportedValuesValidator(t, validator.ValidateRedirectStatusCode, supportedRedirectStatusCodes, 404) } + +func TestValidateRequestHeaderName(t *testing.T) { + validator := HTTPRequestHeaderValidator{} + + testValidValuesForSimpleValidator(t, validator.ValidateRequestHeaderName, + "Content-Encoding", + "Connection") + + testInvalidValuesForSimpleValidator(t, validator.ValidateRequestHeaderName, "$Content-Encoding") +} + +func TestValidateRequestHeaderValue(t *testing.T) { + validator := HTTPRequestHeaderValidator{} + + testValidValuesForSimpleValidator(t, validator.ValidateRequestHeaderValue, + "my-cookie-name", + "ssl_(server_name}", + "example/1234==", + "1234:3456") + + testInvalidValuesForSimpleValidator(t, validator.ValidateRequestHeaderValue, + "$Content-Encoding", + `"example"`) +} diff --git a/internal/nginx/config/validation/http_validator.go b/internal/nginx/config/validation/http_validator.go index 263d3b7c3f..a206bb114d 100644 --- a/internal/nginx/config/validation/http_validator.go +++ b/internal/nginx/config/validation/http_validator.go @@ -10,6 +10,7 @@ import ( type HTTPValidator struct { HTTPNJSMatchValidator HTTPRedirectValidator + HTTPRequestHeaderValidator } var _ validation.HTTPFieldsValidator = HTTPValidator{} diff --git a/internal/nginx/config/variable_names.go b/internal/nginx/config/variable_names.go index 5ea718e79e..7e5680b8fb 100644 --- a/internal/nginx/config/variable_names.go +++ b/internal/nginx/config/variable_names.go @@ -9,3 +9,13 @@ import ( func convertStringToSafeVariableName(s string) string { return strings.ReplaceAll(s, "-", "_") } + +// generateAddHeaderMapVariableName Generate the variable name for a proxy add header map. +// We have increased the proxy_headers_hash_bucket_size and variables_hash_bucket_size to 512; and +// proxy_headers_hash_max_size and variables_hash_max_size to 1024 to support the longest header name as allowed +// by the schema (256 characters). This ensures NGINX will not fail to reload. +// FIXME(ciarams87): Investigate if any there are any performance related concerns with changing these directives. +// https://github.com/nginxinc/nginx-kubernetes-gateway/issues/772 +func generateAddHeaderMapVariableName(name string) string { + return strings.ToLower(convertStringToSafeVariableName(name)) + "_header_var" +} diff --git a/internal/nginx/config/variable_names_test.go b/internal/nginx/config/variable_names_test.go index 8e45a9fcb2..1e1843fc21 100644 --- a/internal/nginx/config/variable_names_test.go +++ b/internal/nginx/config/variable_names_test.go @@ -1,6 +1,10 @@ package config -import "testing" +import ( + "testing" + + . "github.com/onsi/gomega" +) func TestConvertStringToSafeVariableName(t *testing.T) { tests := []struct { @@ -30,3 +34,27 @@ func TestConvertStringToSafeVariableName(t *testing.T) { } } } + +func TestGenerateAddHeaderMapVariableName(t *testing.T) { + g := NewGomegaWithT(t) + tests := []struct { + msg string + headerName string + expected string + }{ + { + msg: "no hyphens", + headerName: "MyCoolHeader", + expected: "mycoolheader_header_var", + }, + { + msg: "with hyphens", + headerName: "My-Cool-Header", + expected: "my_cool_header_header_var", + }, + } + for _, tc := range tests { + actual := generateAddHeaderMapVariableName(tc.headerName) + g.Expect(actual).To(Equal(tc.expected)) + } +} diff --git a/internal/state/dataplane/configuration.go b/internal/state/dataplane/configuration.go index 0fa404df79..2f49b9c3ed 100644 --- a/internal/state/dataplane/configuration.go +++ b/internal/state/dataplane/configuration.go @@ -70,13 +70,25 @@ type PathRule struct { MatchRules []MatchRule } +type HTTPHeaderFilter struct { + Set []HTTPHeader + Add []HTTPHeader + Remove []string +} + +type HTTPHeader struct { + Name string + Value string +} + // InvalidFilter is a special filter for handling the case when configured filters are invalid. type InvalidFilter struct{} // Filters hold the filters for a MatchRule. type Filters struct { - InvalidFilter *InvalidFilter - RequestRedirect *v1beta1.HTTPRequestRedirectFilter + InvalidFilter *InvalidFilter + RequestRedirect *v1beta1.HTTPRequestRedirectFilter + RequestHeaderModifiers *HTTPHeaderFilter } // MatchRule represents a routing rule. It corresponds directly to a Match in the HTTPRoute resource. @@ -515,12 +527,32 @@ func createFilters(filters []v1beta1.HTTPRouteFilter) Filters { for _, f := range filters { switch f.Type { case v1beta1.HTTPRouteFilterRequestRedirect: - result.RequestRedirect = f.RequestRedirect - // using the first filter - return result + if result.RequestRedirect == nil { + // using the first filter + result.RequestRedirect = f.RequestRedirect + } + case v1beta1.HTTPRouteFilterRequestHeaderModifier: + if result.RequestHeaderModifiers == nil { + // using the first filter + result.RequestHeaderModifiers = convertHTTPFilter(f.RequestHeaderModifier) + } } } + return result +} +func convertHTTPFilter(httpFilter *v1beta1.HTTPHeaderFilter) *HTTPHeaderFilter { + result := &HTTPHeaderFilter{ + Remove: httpFilter.Remove, + Set: make([]HTTPHeader, 0, len(httpFilter.Set)), + Add: make([]HTTPHeader, 0, len(httpFilter.Add)), + } + for _, s := range httpFilter.Set { + result.Set = append(result.Set, HTTPHeader{Name: string(s.Name), Value: s.Value}) + } + for _, a := range httpFilter.Add { + result.Add = append(result.Add, HTTPHeader{Name: string(a.Name), Value: a.Value}) + } return result } diff --git a/internal/state/dataplane/configuration_test.go b/internal/state/dataplane/configuration_test.go index 1c343b67fc..f65497fa2a 100644 --- a/internal/state/dataplane/configuration_test.go +++ b/internal/state/dataplane/configuration_test.go @@ -1506,6 +1506,28 @@ func TestCreateFilters(t *testing.T) { Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), }, } + requestHeaderModifiers1 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Set: []v1beta1.HTTPHeader{ + { + Name: "Connection", + Value: "close", + }, + }, + }, + } + requestHeaderModifiers2 := v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Add: []v1beta1.HTTPHeader{ + { + Name: "Content-Accepted", + Value: "gzip", + }, + }, + }, + } tests := []struct { expected Filters @@ -1536,6 +1558,31 @@ func TestCreateFilters(t *testing.T) { }, msg: "two filters, first wins", }, + { + filters: []v1beta1.HTTPRouteFilter{ + redirect1, + redirect2, + requestHeaderModifiers1, + }, + expected: Filters{ + RequestRedirect: redirect1.RequestRedirect, + RequestHeaderModifiers: convertHTTPFilter(requestHeaderModifiers1.RequestHeaderModifier), + }, + msg: "two redirect filters, one request header modifier, first redirect wins", + }, + { + filters: []v1beta1.HTTPRouteFilter{ + redirect1, + redirect2, + requestHeaderModifiers1, + requestHeaderModifiers2, + }, + expected: Filters{ + RequestRedirect: redirect1.RequestRedirect, + RequestHeaderModifiers: convertHTTPFilter(requestHeaderModifiers1.RequestHeaderModifier), + }, + msg: "two redirect filters, two request header modifier, first value for each wins", + }, } for _, test := range tests { @@ -1993,3 +2040,34 @@ func TestHostnameMoreSpecific(t *testing.T) { g.Expect(listenerHostnameMoreSpecific(tc.host1, tc.host2)).To(Equal(tc.host1Wins), tc.msg) } } + +func TestConvertHTTPFilter(t *testing.T) { + g := NewGomegaWithT(t) + + httpFilter := &v1beta1.HTTPHeaderFilter{ + Set: []v1beta1.HTTPHeader{{ + Name: "My-Set-Header", + Value: "my-value", + }}, + Add: []v1beta1.HTTPHeader{{ + Name: "My-Add-Header", + Value: "my-value", + }}, + Remove: []string{"My-remove-header"}, + } + + expected := HTTPHeaderFilter{ + Set: []HTTPHeader{{ + Name: "My-Set-Header", + Value: "my-value", + }}, + Add: []HTTPHeader{{ + Name: "My-Add-Header", + Value: "my-value", + }}, + Remove: []string{"My-remove-header"}, + } + + result := convertHTTPFilter(httpFilter) + g.Expect(*result).To(Equal(expected)) +} diff --git a/internal/state/graph/httproute.go b/internal/state/graph/httproute.go index d4691add28..7b9f422fc8 100644 --- a/internal/state/graph/httproute.go +++ b/internal/state/graph/httproute.go @@ -613,15 +613,31 @@ func validateFilter( ) field.ErrorList { var allErrs field.ErrorList - if filter.Type != v1beta1.HTTPRouteFilterRequestRedirect { + switch filter.Type { + case v1beta1.HTTPRouteFilterRequestRedirect: + return validateFilterRedirect(validator, filter, filterPath) + case v1beta1.HTTPRouteFilterRequestHeaderModifier: + return validateFilterHeaderModifier(validator, filter, filterPath) + default: valErr := field.NotSupported( filterPath.Child("type"), filter.Type, - []string{string(v1beta1.HTTPRouteFilterRequestRedirect)}, + []string{ + string(v1beta1.HTTPRouteFilterRequestRedirect), + string(v1beta1.HTTPRouteFilterRequestHeaderModifier), + }, ) allErrs = append(allErrs, valErr) return allErrs } +} + +func validateFilterRedirect( + validator validation.HTTPFieldsValidator, + filter v1beta1.HTTPRouteFilter, + filterPath *field.Path, +) field.ErrorList { + var allErrs field.ErrorList if filter.RequestRedirect == nil { panicForBrokenWebhookAssumption(errors.New("requestRedirect cannot be nil")) @@ -666,3 +682,56 @@ func validateFilter( return allErrs } + +func validateFilterHeaderModifier( + validator validation.HTTPFieldsValidator, + filter v1beta1.HTTPRouteFilter, + filterPath *field.Path, +) field.ErrorList { + headerModifier := filter.RequestHeaderModifier + + headerModifierPath := filterPath.Child("requestHeaderModifier") + + if headerModifier == nil { + panicForBrokenWebhookAssumption(errors.New("requestHeaderModifier cannot be nil")) + } + + return validateFilterHeaderModifierFields(validator, headerModifier, headerModifierPath) +} + +func validateFilterHeaderModifierFields( + validator validation.HTTPFieldsValidator, + headerModifier *v1beta1.HTTPHeaderFilter, + headerModifierPath *field.Path, +) field.ErrorList { + var allErrs field.ErrorList + + for _, h := range headerModifier.Add { + if err := validator.ValidateRequestHeaderName(string(h.Name)); err != nil { + valErr := field.Invalid(headerModifierPath.Child("add"), h, err.Error()) + allErrs = append(allErrs, valErr) + } + if err := validator.ValidateRequestHeaderValue(h.Value); err != nil { + valErr := field.Invalid(headerModifierPath.Child("add"), h, err.Error()) + allErrs = append(allErrs, valErr) + } + } + for _, h := range headerModifier.Set { + if err := validator.ValidateRequestHeaderName(string(h.Name)); err != nil { + valErr := field.Invalid(headerModifierPath.Child("set"), h, err.Error()) + allErrs = append(allErrs, valErr) + } + if err := validator.ValidateRequestHeaderValue(h.Value); err != nil { + valErr := field.Invalid(headerModifierPath.Child("set"), h, err.Error()) + allErrs = append(allErrs, valErr) + } + } + for _, h := range headerModifier.Remove { + if err := validator.ValidateRequestHeaderName(h); err != nil { + valErr := field.Invalid(headerModifierPath.Child("remove"), h, err.Error()) + allErrs = append(allErrs, valErr) + } + } + + return allErrs +} diff --git a/internal/state/graph/httproute_test.go b/internal/state/graph/httproute_test.go index f7f96271b6..4e0f8c43bd 100644 --- a/internal/state/graph/httproute_test.go +++ b/internal/state/graph/httproute_test.go @@ -1596,6 +1596,48 @@ func TestValidateMatch(t *testing.T) { } func TestValidateFilter(t *testing.T) { + tests := []struct { + filter v1beta1.HTTPRouteFilter + name string + expectErrCount int + }{ + { + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestRedirect, + RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{}, + }, + expectErrCount: 0, + name: "valid redirect filter", + }, + { + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{}, + }, + expectErrCount: 0, + name: "valid request header modifiers filter", + }, + { + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterURLRewrite, + }, + expectErrCount: 1, + name: "unsupported filter", + }, + } + + filterPath := field.NewPath("test") + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + allErrs := validateFilter(&validationfakes.FakeHTTPFieldsValidator{}, test.filter, filterPath) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + }) + } +} + +func TestValidateFilterRedirect(t *testing.T) { createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator { v := &validationfakes.FakeHTTPFieldsValidator{} @@ -1634,14 +1676,6 @@ func TestValidateFilter(t *testing.T) { expectErrCount: 0, name: "valid redirect filter with no fields set", }, - { - validator: createAllValidValidator(), - filter: v1beta1.HTTPRouteFilter{ - Type: v1beta1.HTTPRouteFilterURLRewrite, - }, - expectErrCount: 1, - name: "unsupported filter", - }, { validator: func() *validationfakes.FakeHTTPFieldsValidator { validator := createAllValidValidator() @@ -1743,7 +1777,121 @@ func TestValidateFilter(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewGomegaWithT(t) - allErrs := validateFilter(test.validator, test.filter, filterPath) + allErrs := validateFilterRedirect(test.validator, test.filter, filterPath) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + }) + } +} + +func TestValidateFilterRequestHeaderModifier(t *testing.T) { + createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator { + v := &validationfakes.FakeHTTPFieldsValidator{} + return v + } + + tests := []struct { + filter v1beta1.HTTPRouteFilter + validator *validationfakes.FakeHTTPFieldsValidator + name string + expectErrCount int + }{ + { + validator: createAllValidValidator(), + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Set: []v1beta1.HTTPHeader{ + {Name: "Connection", Value: "close"}, + }, + Add: []v1beta1.HTTPHeader{ + {Name: "Accept-Encoding", Value: "gzip"}, + }, + Remove: []string{"Cache-Control"}, + }, + }, + expectErrCount: 0, + name: "valid request header modifier filter", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateRequestHeaderNameReturns(errors.New("Invalid header")) + return v + }(), + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Add: []v1beta1.HTTPHeader{ + {Name: "$var_name", Value: "gzip"}, + }, + }, + }, + expectErrCount: 1, + name: "request header modifier filter with invalid add", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateRequestHeaderNameReturns(errors.New("Invalid header")) + return v + }(), + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Remove: []string{"$var-name"}, + }, + }, + expectErrCount: 1, + name: "request header modifier filter with invalid remove", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateRequestHeaderValueReturns(errors.New("Invalid header value")) + return v + }(), + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Add: []v1beta1.HTTPHeader{ + {Name: "Accept-Encoding", Value: "yhu$"}, + }, + }, + }, + expectErrCount: 1, + name: "request header modifier filter with invalid header value", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateRequestHeaderValueReturns(errors.New("Invalid header value")) + v.ValidateRequestHeaderNameReturns(errors.New("Invalid header")) + return v + }(), + filter: v1beta1.HTTPRouteFilter{ + Type: v1beta1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &v1beta1.HTTPHeaderFilter{ + Set: []v1beta1.HTTPHeader{ + {Name: "Host", Value: "my_host"}, + }, + Add: []v1beta1.HTTPHeader{ + {Name: "}90yh&$", Value: "gzip$"}, + {Name: "}67yh&$", Value: "compress$"}, + }, + Remove: []string{"Cache-Control$}"}, + }, + }, + expectErrCount: 7, + name: "request header modifier filter all fields invalid", + }, + } + + filterPath := field.NewPath("test") + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + allErrs := validateFilterHeaderModifier(test.validator, test.filter, filterPath) g.Expect(allErrs).To(HaveLen(test.expectErrCount)) }) } diff --git a/internal/state/validation/validationfakes/fake_httpfields_validator.go b/internal/state/validation/validationfakes/fake_httpfields_validator.go index 98e6f9d4b3..b26ab548ff 100644 --- a/internal/state/validation/validationfakes/fake_httpfields_validator.go +++ b/internal/state/validation/validationfakes/fake_httpfields_validator.go @@ -124,6 +124,28 @@ type FakeHTTPFieldsValidator struct { result1 bool result2 []string } + ValidateRequestHeaderNameStub func(string) error + validateRequestHeaderNameMutex sync.RWMutex + validateRequestHeaderNameArgsForCall []struct { + arg1 string + } + validateRequestHeaderNameReturns struct { + result1 error + } + validateRequestHeaderNameReturnsOnCall map[int]struct { + result1 error + } + ValidateRequestHeaderValueStub func(string) error + validateRequestHeaderValueMutex sync.RWMutex + validateRequestHeaderValueArgsForCall []struct { + arg1 string + } + validateRequestHeaderValueReturns struct { + result1 error + } + validateRequestHeaderValueReturnsOnCall map[int]struct { + result1 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -747,6 +769,128 @@ func (fake *FakeHTTPFieldsValidator) ValidateRedirectStatusCodeReturnsOnCall(i i }{result1, result2} } +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderName(arg1 string) error { + fake.validateRequestHeaderNameMutex.Lock() + ret, specificReturn := fake.validateRequestHeaderNameReturnsOnCall[len(fake.validateRequestHeaderNameArgsForCall)] + fake.validateRequestHeaderNameArgsForCall = append(fake.validateRequestHeaderNameArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ValidateRequestHeaderNameStub + fakeReturns := fake.validateRequestHeaderNameReturns + fake.recordInvocation("ValidateRequestHeaderName", []interface{}{arg1}) + fake.validateRequestHeaderNameMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderNameCallCount() int { + fake.validateRequestHeaderNameMutex.RLock() + defer fake.validateRequestHeaderNameMutex.RUnlock() + return len(fake.validateRequestHeaderNameArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderNameCalls(stub func(string) error) { + fake.validateRequestHeaderNameMutex.Lock() + defer fake.validateRequestHeaderNameMutex.Unlock() + fake.ValidateRequestHeaderNameStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderNameArgsForCall(i int) string { + fake.validateRequestHeaderNameMutex.RLock() + defer fake.validateRequestHeaderNameMutex.RUnlock() + argsForCall := fake.validateRequestHeaderNameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderNameReturns(result1 error) { + fake.validateRequestHeaderNameMutex.Lock() + defer fake.validateRequestHeaderNameMutex.Unlock() + fake.ValidateRequestHeaderNameStub = nil + fake.validateRequestHeaderNameReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderNameReturnsOnCall(i int, result1 error) { + fake.validateRequestHeaderNameMutex.Lock() + defer fake.validateRequestHeaderNameMutex.Unlock() + fake.ValidateRequestHeaderNameStub = nil + if fake.validateRequestHeaderNameReturnsOnCall == nil { + fake.validateRequestHeaderNameReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateRequestHeaderNameReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValue(arg1 string) error { + fake.validateRequestHeaderValueMutex.Lock() + ret, specificReturn := fake.validateRequestHeaderValueReturnsOnCall[len(fake.validateRequestHeaderValueArgsForCall)] + fake.validateRequestHeaderValueArgsForCall = append(fake.validateRequestHeaderValueArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ValidateRequestHeaderValueStub + fakeReturns := fake.validateRequestHeaderValueReturns + fake.recordInvocation("ValidateRequestHeaderValue", []interface{}{arg1}) + fake.validateRequestHeaderValueMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueCallCount() int { + fake.validateRequestHeaderValueMutex.RLock() + defer fake.validateRequestHeaderValueMutex.RUnlock() + return len(fake.validateRequestHeaderValueArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueCalls(stub func(string) error) { + fake.validateRequestHeaderValueMutex.Lock() + defer fake.validateRequestHeaderValueMutex.Unlock() + fake.ValidateRequestHeaderValueStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueArgsForCall(i int) string { + fake.validateRequestHeaderValueMutex.RLock() + defer fake.validateRequestHeaderValueMutex.RUnlock() + argsForCall := fake.validateRequestHeaderValueArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueReturns(result1 error) { + fake.validateRequestHeaderValueMutex.Lock() + defer fake.validateRequestHeaderValueMutex.Unlock() + fake.ValidateRequestHeaderValueStub = nil + fake.validateRequestHeaderValueReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateRequestHeaderValueReturnsOnCall(i int, result1 error) { + fake.validateRequestHeaderValueMutex.Lock() + defer fake.validateRequestHeaderValueMutex.Unlock() + fake.ValidateRequestHeaderValueStub = nil + if fake.validateRequestHeaderValueReturnsOnCall == nil { + fake.validateRequestHeaderValueReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateRequestHeaderValueReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -770,6 +914,10 @@ func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { defer fake.validateRedirectSchemeMutex.RUnlock() fake.validateRedirectStatusCodeMutex.RLock() defer fake.validateRedirectStatusCodeMutex.RUnlock() + fake.validateRequestHeaderNameMutex.RLock() + defer fake.validateRequestHeaderNameMutex.RUnlock() + fake.validateRequestHeaderValueMutex.RLock() + defer fake.validateRequestHeaderValueMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/state/validation/validator.go b/internal/state/validation/validator.go index 75d68c1000..ad06077c65 100644 --- a/internal/state/validation/validator.go +++ b/internal/state/validation/validator.go @@ -23,4 +23,6 @@ type HTTPFieldsValidator interface { ValidateRedirectHostname(hostname string) error ValidateRedirectPort(port int32) error ValidateRedirectStatusCode(statusCode int) (valid bool, supportedValues []string) + ValidateRequestHeaderName(name string) error + ValidateRequestHeaderValue(value string) error }