diff --git a/conformance/Makefile b/conformance/Makefile index 30f772ceb4..da8cb8da1c 100644 --- a/conformance/Makefile +++ b/conformance/Makefile @@ -5,7 +5,7 @@ NGINX_PREFIX = $(PREFIX)/nginx NGINX_PLUS_PREFIX ?= $(PREFIX)/nginx-plus GW_API_VERSION ?= 1.0.0 GATEWAY_CLASS = nginx -SUPPORTED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080 +SUPPORTED_FEATURES = HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteSchemeRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,GatewayPort8080,HTTPRouteResponseHeaderModification KIND_IMAGE ?= $(shell grep -m1 'FROM kindest/node' 0 { - addHeaders := convertAddHeaders(headerFilter.Add) + addHeaders := createHeadersWithVarName(headerFilter.Add) proxySetHeaders = append(proxySetHeaders, addHeaders...) } if len(headerFilter.Set) > 0 { - setHeaders := convertSetHeaders(headerFilter.Set) + setHeaders := createHeaders(headerFilter.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 @@ -535,7 +537,25 @@ func generateProxySetHeaders(filters *dataplane.HTTPFilters) []http.Header { return append(proxySetHeaders, headers...) } -func convertAddHeaders(headers []dataplane.HTTPHeader) []http.Header { +func generateResponseHeaders(filters *dataplane.HTTPFilters) http.ResponseHeaders { + if filters == nil || filters.ResponseHeaderModifiers == nil { + return http.ResponseHeaders{} + } + + headerFilter := filters.ResponseHeaderModifiers + responseRemoveHeaders := make([]string, len(headerFilter.Remove)) + + // Make a deep copy to prevent the slice from being accidentally modified. + copy(responseRemoveHeaders, headerFilter.Remove) + + return http.ResponseHeaders{ + Add: createHeaders(headerFilter.Add), + Set: createHeaders(headerFilter.Set), + Remove: responseRemoveHeaders, + } +} + +func createHeadersWithVarName(headers []dataplane.HTTPHeader) []http.Header { locHeaders := make([]http.Header, 0, len(headers)) for _, h := range headers { mapVarName := "${" + generateAddHeaderMapVariableName(h.Name) + "}" @@ -547,7 +567,7 @@ func convertAddHeaders(headers []dataplane.HTTPHeader) []http.Header { return locHeaders } -func convertSetHeaders(headers []dataplane.HTTPHeader) []http.Header { +func createHeaders(headers []dataplane.HTTPHeader) []http.Header { locHeaders := make([]http.Header, 0, len(headers)) for _, h := range headers { locHeaders = append(locHeaders, http.Header{ diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index dbf37575ae..c3b70f6729 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -50,6 +50,16 @@ server { {{ range $h := $l.ProxySetHeaders }} proxy_set_header {{ $h.Name }} "{{ $h.Value }}"; {{- end }} + {{ range $h := $l.ResponseHeaders.Add }} + add_header {{ $h.Name }} "{{ $h.Value }}" always; + {{- end }} + {{ range $h := $l.ResponseHeaders.Set }} + proxy_hide_header {{ $h.Name }}; + add_header {{ $h.Name }} "{{ $h.Value }}" always; + {{- end }} + {{ range $h := $l.ResponseHeaders.Remove }} + proxy_hide_header {{ $h }}; + {{- end }} proxy_http_version 1.1; proxy_pass {{ $l.ProxyPass }}; {{- if $l.ProxySSLVerify }} diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 218e59d9d3..e1cc9c9fe4 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -582,16 +582,19 @@ func TestCreateServers(t *testing.T) { Path: "@rule0-route0", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "@rule0-route1", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "@rule0-route2", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/", @@ -601,6 +604,7 @@ func TestCreateServers(t *testing.T) { Path: "@rule1-route0", ProxyPass: "http://$test__route1_rule1$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/test/", @@ -610,16 +614,19 @@ func TestCreateServers(t *testing.T) { Path: "/path-only/", ProxyPass: "http://invalid-backend-ref$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "= /path-only", ProxyPass: "http://invalid-backend-ref$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/backend-tls-policy/", ProxyPass: "https://test_btp_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, ProxySSLVerify: &http.ProxySSLVerify{ Name: "test-btp.example.com", TrustedCertificate: "/etc/nginx/secrets/test-btp.crt", @@ -629,6 +636,7 @@ func TestCreateServers(t *testing.T) { Path: "= /backend-tls-policy", ProxyPass: "https://test_btp_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, ProxySSLVerify: &http.ProxySSLVerify{ Name: "test-btp.example.com", TrustedCertificate: "/etc/nginx/secrets/test-btp.crt", @@ -682,18 +690,21 @@ func TestCreateServers(t *testing.T) { Rewrites: []string{"^ /replacement break"}, ProxyPass: "http://test_foo_80", ProxySetHeaders: rewriteProxySetHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "= /rewrite", Rewrites: []string{"^ /replacement break"}, ProxyPass: "http://test_foo_80", ProxySetHeaders: rewriteProxySetHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "@rule8-route0", Rewrites: []string{"^/rewrite-with-headers(.*)$ /prefix-replacement$1 break"}, ProxyPass: "http://test_foo_80", ProxySetHeaders: rewriteProxySetHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/rewrite-with-headers/", @@ -733,11 +744,13 @@ func TestCreateServers(t *testing.T) { Path: "= /exact", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "@rule12-route0", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "= /test", @@ -768,6 +781,7 @@ func TestCreateServers(t *testing.T) { Value: "$connection_upgrade", }, }, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "= /proxy-set-headers", @@ -794,6 +808,7 @@ func TestCreateServers(t *testing.T) { Value: "$connection_upgrade", }, }, + ResponseHeaders: http.ResponseHeaders{}, }, } } @@ -900,11 +915,13 @@ func TestCreateServersConflicts(t *testing.T) { Path: "/coffee/", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "= /coffee", ProxyPass: "http://test_bar_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, createDefaultRootLocation(), }, @@ -938,11 +955,13 @@ func TestCreateServersConflicts(t *testing.T) { Path: "= /coffee", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/coffee/", ProxyPass: "http://test_bar_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, createDefaultRootLocation(), }, @@ -986,11 +1005,13 @@ func TestCreateServersConflicts(t *testing.T) { Path: "/coffee/", ProxyPass: "http://test_bar_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "= /coffee", ProxyPass: "http://test_baz_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, createDefaultRootLocation(), }, @@ -1095,11 +1116,13 @@ func TestCreateLocationsRootPath(t *testing.T) { Path: "/path-1", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/path-2", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/", @@ -1117,16 +1140,19 @@ func TestCreateLocationsRootPath(t *testing.T) { Path: "/path-1", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/path-2", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, }, }, diff --git a/internal/mode/static/nginx/config/validation/http_filters.go b/internal/mode/static/nginx/config/validation/http_filters.go index 3fc638108e..ba12a86257 100644 --- a/internal/mode/static/nginx/config/validation/http_filters.go +++ b/internal/mode/static/nginx/config/validation/http_filters.go @@ -72,13 +72,13 @@ func (HTTPURLRewriteValidator) ValidateRewritePath(path string) error { return nil } -func (HTTPRequestHeaderValidator) ValidateRequestHeaderName(name string) error { +func (HTTPRequestHeaderValidator) ValidateFilterHeaderName(name string) error { return validateHeaderName(name) } var requestHeaderValueExamples = []string{"my-header-value", "example/12345=="} -func (HTTPRequestHeaderValidator) ValidateRequestHeaderValue(value string) error { +func (HTTPRequestHeaderValidator) ValidateFilterHeaderValue(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/mode/static/nginx/config/validation/http_filters_test.go b/internal/mode/static/nginx/config/validation/http_filters_test.go index c216a30224..4bd8fe6589 100644 --- a/internal/mode/static/nginx/config/validation/http_filters_test.go +++ b/internal/mode/static/nginx/config/validation/http_filters_test.go @@ -88,25 +88,25 @@ func TestValidateRewritePath(t *testing.T) { ) } -func TestValidateRequestHeaderName(t *testing.T) { +func TestValidateFilterHeaderName(t *testing.T) { validator := HTTPRequestHeaderValidator{} testValidValuesForSimpleValidator( t, - validator.ValidateRequestHeaderName, + validator.ValidateFilterHeaderName, "Content-Encoding", "MyBespokeHeader", ) - testInvalidValuesForSimpleValidator(t, validator.ValidateRequestHeaderName, "$Content-Encoding") + testInvalidValuesForSimpleValidator(t, validator.ValidateFilterHeaderName, "$Content-Encoding") } -func TestValidateRequestHeaderValue(t *testing.T) { +func TestValidateFilterHeaderValue(t *testing.T) { validator := HTTPRequestHeaderValidator{} testValidValuesForSimpleValidator( t, - validator.ValidateRequestHeaderValue, + validator.ValidateFilterHeaderValue, "my-cookie-name", "ssl_(server_name}", "example/1234==", @@ -115,7 +115,7 @@ func TestValidateRequestHeaderValue(t *testing.T) { testInvalidValuesForSimpleValidator( t, - validator.ValidateRequestHeaderValue, + validator.ValidateFilterHeaderValue, "$Content-Encoding", `"example"`, ) diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index 25ed7e217a..c77c898852 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -527,6 +527,11 @@ func createHTTPFilters(filters []v1.HTTPRouteFilter) HTTPFilters { // using the first filter result.RequestHeaderModifiers = convertHTTPHeaderFilter(f.RequestHeaderModifier) } + case v1.HTTPRouteFilterResponseHeaderModifier: + if result.ResponseHeaderModifiers == nil { + // using the first filter + result.ResponseHeaderModifiers = convertHTTPHeaderFilter(f.ResponseHeaderModifier) + } } } return result diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index da1b00afaa..44799de3cd 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -110,6 +110,8 @@ type HTTPFilters struct { RequestURLRewrite *HTTPURLRewriteFilter // RequestHeaderModifiers holds the HTTPHeaderFilter. RequestHeaderModifiers *HTTPHeaderFilter + // ResponseHeaderModifiers holds the HTTPHeaderFilter. + ResponseHeaderModifiers *HTTPHeaderFilter } // HTTPHeader represents an HTTP header. diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go index d16e833f00..3fc9e4135d 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -703,7 +703,11 @@ func validateFilter( case v1.HTTPRouteFilterURLRewrite: return validateFilterRewrite(validator, filter, filterPath) case v1.HTTPRouteFilterRequestHeaderModifier: - return validateFilterHeaderModifier(validator, filter, filterPath) + return validateFilterHeaderModifier(validator, filter.RequestHeaderModifier, filterPath.Child(string(filter.Type))) + case v1.HTTPRouteFilterResponseHeaderModifier: + return validateFilterResponseHeaderModifier( + validator, filter.ResponseHeaderModifier, filterPath.Child(string(filter.Type)), + ) default: valErr := field.NotSupported( filterPath.Child("type"), @@ -712,6 +716,7 @@ func validateFilter( string(v1.HTTPRouteFilterRequestRedirect), string(v1.HTTPRouteFilterURLRewrite), string(v1.HTTPRouteFilterRequestHeaderModifier), + string(v1.HTTPRouteFilterResponseHeaderModifier), }, ) allErrs = append(allErrs, valErr) @@ -814,18 +819,14 @@ func validateFilterRewrite( func validateFilterHeaderModifier( validator validation.HTTPFieldsValidator, - filter v1.HTTPRouteFilter, + headerModifier *v1.HTTPHeaderFilter, filterPath *field.Path, ) field.ErrorList { - headerModifier := filter.RequestHeaderModifier - - headerModifierPath := filterPath.Child("requestHeaderModifier") - if headerModifier == nil { - return field.ErrorList{field.Required(headerModifierPath, "requestHeaderModifier cannot be nil")} + return field.ErrorList{field.Required(filterPath, "cannot be nil")} } - return validateFilterHeaderModifierFields(validator, headerModifier, headerModifierPath) + return validateFilterHeaderModifierFields(validator, headerModifier, filterPath) } func validateFilterHeaderModifierFields( @@ -850,27 +851,27 @@ func validateFilterHeaderModifierFields( ) for _, h := range headerModifier.Add { - if err := validator.ValidateRequestHeaderName(string(h.Name)); err != nil { + if err := validator.ValidateFilterHeaderName(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 { + if err := validator.ValidateFilterHeaderValue(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 { + if err := validator.ValidateFilterHeaderName(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 { + if err := validator.ValidateFilterHeaderValue(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 { + if err := validator.ValidateFilterHeaderName(h); err != nil { valErr := field.Invalid(headerModifierPath.Child("remove"), h, err.Error()) allErrs = append(allErrs, valErr) } @@ -879,6 +880,61 @@ func validateFilterHeaderModifierFields( return allErrs } +func validateFilterResponseHeaderModifier( + validator validation.HTTPFieldsValidator, + responseHeaderModifier *v1.HTTPHeaderFilter, + filterPath *field.Path, +) field.ErrorList { + if errList := validateFilterHeaderModifier(validator, responseHeaderModifier, filterPath); errList != nil { + return errList + } + var allErrs field.ErrorList + disallowedResponseHeaderSet := map[string]struct{}{ + "server": {}, + "date": {}, + "x-pad": {}, + "content-type": {}, + "content-length": {}, + "connection": {}, + } + invalidPrefix := "x-accel" + for _, h := range responseHeaderModifier.Add { + valErr := field.Invalid(filterPath.Child("add"), h, "header name is not allowed") + name := strings.ToLower(string(h.Name)) + if _, exists := disallowedResponseHeaderSet[name]; exists { + allErrs = append(allErrs, valErr) + } else { + if strings.HasPrefix(name, strings.ToLower(invalidPrefix)) { + allErrs = append(allErrs, valErr) + } + } + } + for _, h := range responseHeaderModifier.Set { + valErr := field.Invalid(filterPath.Child("set"), h, "header name is not allowed") + name := strings.ToLower(string(h.Name)) + if _, exists := disallowedResponseHeaderSet[name]; exists { + allErrs = append(allErrs, valErr) + } else { + if strings.HasPrefix(name, strings.ToLower(invalidPrefix)) { + allErrs = append(allErrs, valErr) + } + } + } + for _, h := range responseHeaderModifier.Remove { + valErr := field.Invalid(filterPath.Child("remove"), h, "header name is not allowed") + name := strings.ToLower(h) + if _, exists := disallowedResponseHeaderSet[name]; exists { + allErrs = append(allErrs, valErr) + } else { + if strings.HasPrefix(name, strings.ToLower(invalidPrefix)) { + allErrs = append(allErrs, valErr) + } + } + } + + return allErrs +} + func validateRequestHeadersCaseInsensitiveUnique( headers []v1.HTTPHeader, path *field.Path, diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go index 7f4b99d376..853b3a0c5e 100644 --- a/internal/mode/static/state/graph/httproute_test.go +++ b/internal/mode/static/state/graph/httproute_test.go @@ -2316,7 +2316,7 @@ func TestValidateFilterRequestHeaderModifier(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { v := createAllValidValidator() - v.ValidateRequestHeaderNameReturns(errors.New("Invalid header")) + v.ValidateFilterHeaderNameReturns(errors.New("Invalid header")) return v }(), filter: gatewayv1.HTTPRouteFilter{ @@ -2333,7 +2333,7 @@ func TestValidateFilterRequestHeaderModifier(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { v := createAllValidValidator() - v.ValidateRequestHeaderNameReturns(errors.New("Invalid header")) + v.ValidateFilterHeaderNameReturns(errors.New("Invalid header")) return v }(), filter: gatewayv1.HTTPRouteFilter{ @@ -2348,7 +2348,7 @@ func TestValidateFilterRequestHeaderModifier(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { v := createAllValidValidator() - v.ValidateRequestHeaderValueReturns(errors.New("Invalid header value")) + v.ValidateFilterHeaderValueReturns(errors.New("Invalid header value")) return v }(), filter: gatewayv1.HTTPRouteFilter{ @@ -2365,8 +2365,8 @@ func TestValidateFilterRequestHeaderModifier(t *testing.T) { { validator: func() *validationfakes.FakeHTTPFieldsValidator { v := createAllValidValidator() - v.ValidateRequestHeaderValueReturns(errors.New("Invalid header value")) - v.ValidateRequestHeaderNameReturns(errors.New("Invalid header")) + v.ValidateFilterHeaderValueReturns(errors.New("Invalid header value")) + v.ValidateFilterHeaderNameReturns(errors.New("Invalid header")) return v }(), filter: gatewayv1.HTTPRouteFilter{ @@ -2411,7 +2411,187 @@ func TestValidateFilterRequestHeaderModifier(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - allErrs := validateFilterHeaderModifier(test.validator, test.filter, filterPath) + allErrs := validateFilterHeaderModifier( + test.validator, test.filter.RequestHeaderModifier, filterPath, + ) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + }) + } +} + +func TestValidateFilterResponseHeaderModifier(t *testing.T) { + createAllValidValidator := func() *validationfakes.FakeHTTPFieldsValidator { + v := &validationfakes.FakeHTTPFieldsValidator{} + return v + } + + tests := []struct { + filter gatewayv1.HTTPRouteFilter + validator *validationfakes.FakeHTTPFieldsValidator + name string + expectErrCount int + }{ + { + validator: createAllValidValidator(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Set: []gatewayv1.HTTPHeader{ + {Name: "MyBespokeHeader", Value: "my-value"}, + }, + Add: []gatewayv1.HTTPHeader{ + {Name: "Accept-Encoding", Value: "gzip"}, + }, + Remove: []string{"Cache-Control"}, + }, + }, + expectErrCount: 0, + name: "valid response header modifier filter", + }, + { + validator: createAllValidValidator(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: nil, + }, + expectErrCount: 1, + name: "nil response header modifier filter", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateFilterHeaderNameReturns(errors.New("Invalid header")) + return v + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Add: []gatewayv1.HTTPHeader{ + {Name: "$var_name", Value: "gzip"}, + }, + }, + }, + expectErrCount: 1, + name: "response header modifier filter with invalid add", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateFilterHeaderNameReturns(errors.New("Invalid header")) + return v + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Remove: []string{"$var-name"}, + }, + }, + expectErrCount: 1, + name: "response header modifier filter with invalid remove", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateFilterHeaderValueReturns(errors.New("Invalid header value")) + return v + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Add: []gatewayv1.HTTPHeader{ + {Name: "Accept-Encoding", Value: "yhu$"}, + }, + }, + }, + expectErrCount: 1, + name: "response header modifier filter with invalid header value", + }, + { + validator: func() *validationfakes.FakeHTTPFieldsValidator { + v := createAllValidValidator() + v.ValidateFilterHeaderValueReturns(errors.New("Invalid header value")) + v.ValidateFilterHeaderNameReturns(errors.New("Invalid header")) + return v + }(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Set: []gatewayv1.HTTPHeader{ + {Name: "Host", Value: "my_host"}, + }, + Add: []gatewayv1.HTTPHeader{ + {Name: "}90yh&$", Value: "gzip$"}, + {Name: "}67yh&$", Value: "compress$"}, + }, + Remove: []string{"Cache-Control$}"}, + }, + }, + expectErrCount: 7, + name: "response header modifier filter all fields invalid", + }, + { + validator: createAllValidValidator(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Set: []gatewayv1.HTTPHeader{ + {Name: "MyBespokeHeader", Value: "my-value"}, + {Name: "mYbespokeHEader", Value: "duplicate"}, + }, + Add: []gatewayv1.HTTPHeader{ + {Name: "Accept-Encoding", Value: "gzip"}, + {Name: "accept-encodING", Value: "gzip"}, + }, + Remove: []string{"Cache-Control", "cache-control"}, + }, + }, + expectErrCount: 3, + name: "response header modifier filter not unique names", + }, + { + validator: createAllValidValidator(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Set: []gatewayv1.HTTPHeader{ + {Name: "Content-Length", Value: "163"}, + }, + Add: []gatewayv1.HTTPHeader{ + {Name: "Content-Type", Value: "text/plain"}, + }, + Remove: []string{"X-Pad"}, + }, + }, + expectErrCount: 3, + name: "invalid response header modifier filter", + }, + { + validator: createAllValidValidator(), + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Set: []gatewayv1.HTTPHeader{ + {Name: "X-Accel-Redirect", Value: "/protected/iso.img"}, + }, + Add: []gatewayv1.HTTPHeader{ + {Name: "X-Accel-Limit-Rate", Value: "1024"}, + }, + Remove: []string{"X-Accel-Charset"}, + }, + }, + expectErrCount: 3, + name: "invalid response header modifier filter", + }, + } + + filterPath := field.NewPath("test") + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + allErrs := validateFilterResponseHeaderModifier( + test.validator, test.filter.ResponseHeaderModifier, filterPath, + ) g.Expect(allErrs).To(HaveLen(test.expectErrCount)) }) } diff --git a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go index 05b4c620b6..64d9b09349 100644 --- a/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go +++ b/internal/mode/static/state/validation/validationfakes/fake_httpfields_validator.go @@ -8,6 +8,28 @@ import ( ) type FakeHTTPFieldsValidator struct { + ValidateFilterHeaderNameStub func(string) error + validateFilterHeaderNameMutex sync.RWMutex + validateFilterHeaderNameArgsForCall []struct { + arg1 string + } + validateFilterHeaderNameReturns struct { + result1 error + } + validateFilterHeaderNameReturnsOnCall map[int]struct { + result1 error + } + ValidateFilterHeaderValueStub func(string) error + validateFilterHeaderValueMutex sync.RWMutex + validateFilterHeaderValueArgsForCall []struct { + arg1 string + } + validateFilterHeaderValueReturns struct { + result1 error + } + validateFilterHeaderValueReturnsOnCall map[int]struct { + result1 error + } ValidateHeaderNameInMatchStub func(string) error validateHeaderNameInMatchMutex sync.RWMutex validateHeaderNameInMatchArgsForCall []struct { @@ -124,41 +146,141 @@ type FakeHTTPFieldsValidator struct { result1 bool result2 []string } - ValidateRequestHeaderNameStub func(string) error - validateRequestHeaderNameMutex sync.RWMutex - validateRequestHeaderNameArgsForCall []struct { + ValidateRewritePathStub func(string) error + validateRewritePathMutex sync.RWMutex + validateRewritePathArgsForCall []struct { arg1 string } - validateRequestHeaderNameReturns struct { + validateRewritePathReturns struct { result1 error } - validateRequestHeaderNameReturnsOnCall map[int]struct { + validateRewritePathReturnsOnCall map[int]struct { result1 error } - ValidateRequestHeaderValueStub func(string) error - validateRequestHeaderValueMutex sync.RWMutex - validateRequestHeaderValueArgsForCall []struct { + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderName(arg1 string) error { + fake.validateFilterHeaderNameMutex.Lock() + ret, specificReturn := fake.validateFilterHeaderNameReturnsOnCall[len(fake.validateFilterHeaderNameArgsForCall)] + fake.validateFilterHeaderNameArgsForCall = append(fake.validateFilterHeaderNameArgsForCall, struct { arg1 string + }{arg1}) + stub := fake.ValidateFilterHeaderNameStub + fakeReturns := fake.validateFilterHeaderNameReturns + fake.recordInvocation("ValidateFilterHeaderName", []interface{}{arg1}) + fake.validateFilterHeaderNameMutex.Unlock() + if stub != nil { + return stub(arg1) } - validateRequestHeaderValueReturns struct { - result1 error + if specificReturn { + return ret.result1 } - validateRequestHeaderValueReturnsOnCall map[int]struct { + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderNameCallCount() int { + fake.validateFilterHeaderNameMutex.RLock() + defer fake.validateFilterHeaderNameMutex.RUnlock() + return len(fake.validateFilterHeaderNameArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderNameCalls(stub func(string) error) { + fake.validateFilterHeaderNameMutex.Lock() + defer fake.validateFilterHeaderNameMutex.Unlock() + fake.ValidateFilterHeaderNameStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderNameArgsForCall(i int) string { + fake.validateFilterHeaderNameMutex.RLock() + defer fake.validateFilterHeaderNameMutex.RUnlock() + argsForCall := fake.validateFilterHeaderNameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderNameReturns(result1 error) { + fake.validateFilterHeaderNameMutex.Lock() + defer fake.validateFilterHeaderNameMutex.Unlock() + fake.ValidateFilterHeaderNameStub = nil + fake.validateFilterHeaderNameReturns = struct { result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderNameReturnsOnCall(i int, result1 error) { + fake.validateFilterHeaderNameMutex.Lock() + defer fake.validateFilterHeaderNameMutex.Unlock() + fake.ValidateFilterHeaderNameStub = nil + if fake.validateFilterHeaderNameReturnsOnCall == nil { + fake.validateFilterHeaderNameReturnsOnCall = make(map[int]struct { + result1 error + }) } - ValidateRewritePathStub func(string) error - validateRewritePathMutex sync.RWMutex - validateRewritePathArgsForCall []struct { + fake.validateFilterHeaderNameReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderValue(arg1 string) error { + fake.validateFilterHeaderValueMutex.Lock() + ret, specificReturn := fake.validateFilterHeaderValueReturnsOnCall[len(fake.validateFilterHeaderValueArgsForCall)] + fake.validateFilterHeaderValueArgsForCall = append(fake.validateFilterHeaderValueArgsForCall, struct { arg1 string + }{arg1}) + stub := fake.ValidateFilterHeaderValueStub + fakeReturns := fake.validateFilterHeaderValueReturns + fake.recordInvocation("ValidateFilterHeaderValue", []interface{}{arg1}) + fake.validateFilterHeaderValueMutex.Unlock() + if stub != nil { + return stub(arg1) } - validateRewritePathReturns struct { - result1 error + if specificReturn { + return ret.result1 } - validateRewritePathReturnsOnCall map[int]struct { + return fakeReturns.result1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderValueCallCount() int { + fake.validateFilterHeaderValueMutex.RLock() + defer fake.validateFilterHeaderValueMutex.RUnlock() + return len(fake.validateFilterHeaderValueArgsForCall) +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderValueCalls(stub func(string) error) { + fake.validateFilterHeaderValueMutex.Lock() + defer fake.validateFilterHeaderValueMutex.Unlock() + fake.ValidateFilterHeaderValueStub = stub +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderValueArgsForCall(i int) string { + fake.validateFilterHeaderValueMutex.RLock() + defer fake.validateFilterHeaderValueMutex.RUnlock() + argsForCall := fake.validateFilterHeaderValueArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderValueReturns(result1 error) { + fake.validateFilterHeaderValueMutex.Lock() + defer fake.validateFilterHeaderValueMutex.Unlock() + fake.ValidateFilterHeaderValueStub = nil + fake.validateFilterHeaderValueReturns = struct { result1 error + }{result1} +} + +func (fake *FakeHTTPFieldsValidator) ValidateFilterHeaderValueReturnsOnCall(i int, result1 error) { + fake.validateFilterHeaderValueMutex.Lock() + defer fake.validateFilterHeaderValueMutex.Unlock() + fake.ValidateFilterHeaderValueStub = nil + if fake.validateFilterHeaderValueReturnsOnCall == nil { + fake.validateFilterHeaderValueReturnsOnCall = make(map[int]struct { + result1 error + }) } - invocations map[string][][]interface{} - invocationsMutex sync.RWMutex + fake.validateFilterHeaderValueReturnsOnCall[i] = struct { + result1 error + }{result1} } func (fake *FakeHTTPFieldsValidator) ValidateHeaderNameInMatch(arg1 string) error { @@ -780,128 +902,6 @@ 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) ValidateRewritePath(arg1 string) error { fake.validateRewritePathMutex.Lock() ret, specificReturn := fake.validateRewritePathReturnsOnCall[len(fake.validateRewritePathArgsForCall)] @@ -966,6 +966,10 @@ func (fake *FakeHTTPFieldsValidator) ValidateRewritePathReturnsOnCall(i int, res func (fake *FakeHTTPFieldsValidator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() + fake.validateFilterHeaderNameMutex.RLock() + defer fake.validateFilterHeaderNameMutex.RUnlock() + fake.validateFilterHeaderValueMutex.RLock() + defer fake.validateFilterHeaderValueMutex.RUnlock() fake.validateHeaderNameInMatchMutex.RLock() defer fake.validateHeaderNameInMatchMutex.RUnlock() fake.validateHeaderValueInMatchMutex.RLock() @@ -986,10 +990,6 @@ 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() fake.validateRewritePathMutex.RLock() defer fake.validateRewritePathMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go index d6433ad363..91b87186a0 100644 --- a/internal/mode/static/state/validation/validator.go +++ b/internal/mode/static/state/validation/validator.go @@ -24,6 +24,6 @@ type HTTPFieldsValidator interface { ValidateRedirectStatusCode(statusCode int) (valid bool, supportedValues []string) ValidateHostname(hostname string) error ValidateRewritePath(path string) error - ValidateRequestHeaderName(name string) error - ValidateRequestHeaderValue(value string) error + ValidateFilterHeaderName(name string) error + ValidateFilterHeaderValue(value string) error } diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index 2673543e91..72d0ed9069 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -157,7 +157,8 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman - `requestRedirect`: Supported except for the experimental `path` field. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `urlRewrite`. - `requestHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. - `urlRewrite`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. Incompatible with `requestRedirect`. - - `responseHeaderModifier`, `requestMirror`, `extensionRef`: Not supported. + - `responseHeaderModifier`: Supported. If multiple filters are configured, NGINX Gateway Fabric will choose the first and ignore the rest. + - `requestMirror`, `extensionRef`: Not supported. - `backendRefs`: Partially supported. Backend ref `filters` are not supported. - `status` - `parents`