From 4d0a3deec65c357c3a5a7570db2df739a654e270 Mon Sep 17 00:00:00 2001 From: salonichf5 <146118978+salonichf5@users.noreply.github.com> Date: Tue, 14 May 2024 10:41:10 -0600 Subject: [PATCH] Add support for ResponseHeaderModifier for HTTPRouteRule objects (#1880) Add support for ResponseHeaderModifier for HTTPRouteRule objects Problem: Users want to add, set, and remove response headers. Solution: Use add_header NGINX directive to support ResponseHeaderModifier. ---- Co-authored-by: kaihsun --- conformance/Makefile | 2 +- .../README.md | 2 +- .../echo-route.yaml | 0 .../gateway.yaml | 0 .../headers.yaml | 0 .../http-response-header-filter/README.md | 3 + .../http-response-header-filter/gateway.yaml | 10 + .../http-response-header-filter/headers.yaml | 70 +++++ .../http-route-filters.yaml | 29 ++ .../http-route.yaml | 18 ++ .../mode/static/nginx/config/http/config.go | 11 +- internal/mode/static/nginx/config/servers.go | 28 +- .../static/nginx/config/servers_template.go | 10 + .../mode/static/nginx/config/servers_test.go | 118 +++++++ .../nginx/config/validation/http_filters.go | 8 +- .../config/validation/http_filters_test.go | 16 +- .../nginx/config/validation/http_validator.go | 2 +- .../static/state/dataplane/configuration.go | 5 + .../state/dataplane/configuration_test.go | 42 ++- internal/mode/static/state/dataplane/types.go | 2 + internal/mode/static/state/graph/httproute.go | 108 +++++-- .../mode/static/state/graph/httproute_test.go | 200 +++++++++++- .../fake_httpfields_validator.go | 290 +++++++++--------- .../mode/static/state/validation/validator.go | 4 +- .../traffic-management/response-headers.md | 212 +++++++++++++ .../overview/gateway-api-compatibility.md | 3 +- 26 files changed, 995 insertions(+), 198 deletions(-) rename examples/{http-header-filter => http-request-header-filter}/README.md (98%) rename examples/{http-header-filter => http-request-header-filter}/echo-route.yaml (100%) rename examples/{http-header-filter => http-request-header-filter}/gateway.yaml (100%) rename examples/{http-header-filter => http-request-header-filter}/headers.yaml (100%) create mode 100644 examples/http-response-header-filter/README.md create mode 100644 examples/http-response-header-filter/gateway.yaml create mode 100644 examples/http-response-header-filter/headers.yaml create mode 100644 examples/http-response-header-filter/http-route-filters.yaml create mode 100644 examples/http-response-header-filter/http-route.yaml create mode 100644 site/content/how-to/traffic-management/response-headers.md diff --git a/conformance/Makefile b/conformance/Makefile index 27de8f1786..91ce92da7e 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' ``` -## 2. Deploy the Cafe Application +## 2. Deploy the Headers Application 1. Create the headers Deployment and Service: diff --git a/examples/http-header-filter/echo-route.yaml b/examples/http-request-header-filter/echo-route.yaml similarity index 100% rename from examples/http-header-filter/echo-route.yaml rename to examples/http-request-header-filter/echo-route.yaml diff --git a/examples/http-header-filter/gateway.yaml b/examples/http-request-header-filter/gateway.yaml similarity index 100% rename from examples/http-header-filter/gateway.yaml rename to examples/http-request-header-filter/gateway.yaml diff --git a/examples/http-header-filter/headers.yaml b/examples/http-request-header-filter/headers.yaml similarity index 100% rename from examples/http-header-filter/headers.yaml rename to examples/http-request-header-filter/headers.yaml diff --git a/examples/http-response-header-filter/README.md b/examples/http-response-header-filter/README.md new file mode 100644 index 0000000000..ace5ec59df --- /dev/null +++ b/examples/http-response-header-filter/README.md @@ -0,0 +1,3 @@ +# HTTP Response Headers + +This directory contains the YAML files used in the [HTTP Response Headers](https://docs.nginx.com/nginx-gateway-fabric/how-to/traffic-management/response-headers.md) guide. diff --git a/examples/http-response-header-filter/gateway.yaml b/examples/http-response-header-filter/gateway.yaml new file mode 100644 index 0000000000..9d402bd5a1 --- /dev/null +++ b/examples/http-response-header-filter/gateway.yaml @@ -0,0 +1,10 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP diff --git a/examples/http-response-header-filter/headers.yaml b/examples/http-response-header-filter/headers.yaml new file mode 100644 index 0000000000..7e6849f9b0 --- /dev/null +++ b/examples/http-response-header-filter/headers.yaml @@ -0,0 +1,70 @@ +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 +# yamllint disable rule:indentation +data: + nginx.conf: |- + user nginx; + worker_processes 1; + + pid /var/run/nginx.pid; + + events {} + + http { + default_type text/plain; + + server { + listen 8080; + + add_header X-Header-Unmodified "unmodified"; + add_header X-Header-Add "add-to"; + add_header X-Header-Set "overwrite"; + add_header X-Header-Remove "remove"; + + return 200 "ok"; + } + } +# yamllint enable rule:indentation +--- +apiVersion: v1 +kind: Service +metadata: + name: headers +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: headers diff --git a/examples/http-response-header-filter/http-route-filters.yaml b/examples/http-response-header-filter/http-route-filters.yaml new file mode 100644 index 0000000000..c48b760f8b --- /dev/null +++ b/examples/http-response-header-filter/http-route-filters.yaml @@ -0,0 +1,29 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: headers +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /headers + filters: + - type: ResponseHeaderModifier + responseHeaderModifier: + set: + - name: X-Header-Set + value: overwritten-value + add: + - name: X-Header-Add + value: this-is-the-appended-value + remove: + - X-Header-Remove + backendRefs: + - name: headers + port: 80 diff --git a/examples/http-response-header-filter/http-route.yaml b/examples/http-response-header-filter/http-route.yaml new file mode 100644 index 0000000000..6777308a19 --- /dev/null +++ b/examples/http-response-header-filter/http-route.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: headers +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /headers + backendRefs: + - name: headers + port: 80 diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index f20a94f2cf..cae46e1b8d 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -16,10 +16,12 @@ type Location struct { Path string ProxyPass string HTTPMatchKey string + HTTPMatchVar string + Rewrites []string ProxySetHeaders []Header ProxySSLVerify *ProxySSLVerify Return *Return - Rewrites []string + ResponseHeaders ResponseHeaders GRPC bool } @@ -29,6 +31,13 @@ type Header struct { Value string } +// ResponseHeaders holds all response headers to be added, set, or removed. +type ResponseHeaders struct { + Add []Header + Set []Header + Remove []string +} + // Return represents an HTTP return. type Return struct { Body string diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 6884007630..9e3fe06177 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -294,6 +294,7 @@ func updateLocationsForFilters( rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path) proxySetHeaders := generateProxySetHeaders(&matchRule.Filters, grpc) + responseHeaders := generateResponseHeaders(&matchRule.Filters) for i := range buildLocations { if rewrites != nil { if rewrites.Rewrite != "" { @@ -308,6 +309,7 @@ func updateLocationsForFilters( generateProtocolString(buildLocations[i].ProxySSLVerify, grpc), grpc, ) + buildLocations[i].ResponseHeaders = responseHeaders buildLocations[i].ProxyPass = proxyPass buildLocations[i].GRPC = grpc } @@ -578,11 +580,11 @@ func generateProxySetHeaders(filters *dataplane.HTTPFilters, grpc bool) []http.H headerLen := len(headerFilter.Add) + len(headerFilter.Set) + len(headerFilter.Remove) + len(headers) proxySetHeaders := make([]http.Header, 0, headerLen) if len(headerFilter.Add) > 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 @@ -596,7 +598,25 @@ func generateProxySetHeaders(filters *dataplane.HTTPFilters, grpc bool) []http.H 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) + "}" @@ -608,7 +628,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 cbbc36f196..12ebb60261 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -58,6 +58,16 @@ server { {{ $proxyOrGRPC }}_set_header {{ $h.Name }} "{{ $h.Value }}"; {{- end }} {{ $proxyOrGRPC }}_pass {{ $l.ProxyPass }}; + {{ 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; {{- if $l.ProxySSLVerify }} {{ $proxyOrGRPC }}_ssl_server_name on; diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 163b5beccd..5f039a7a86 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -512,6 +512,14 @@ func TestCreateServers(t *testing.T) { }, }, }, + ResponseHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "my-header-response", + Value: "some-value-response-123", + }, + }, + }, }, }, }, @@ -642,16 +650,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: "/", @@ -661,6 +672,7 @@ func TestCreateServers(t *testing.T) { Path: "@rule1-route0", ProxyPass: "http://$test__route1_rule1$request_uri", ProxySetHeaders: baseHeaders, + ResponseHeaders: http.ResponseHeaders{}, }, { Path: "/test/", @@ -670,11 +682,13 @@ 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/", @@ -742,18 +756,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/", @@ -793,11 +810,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", @@ -828,6 +847,16 @@ func TestCreateServers(t *testing.T) { Value: "$connection_upgrade", }, }, + ResponseHeaders: http.ResponseHeaders{ + Add: []http.Header{ + { + Name: "my-header-response", + Value: "some-value-response-123", + }, + }, + Set: []http.Header{}, + Remove: []string{}, + }, }, { Path: "= /proxy-set-headers", @@ -854,6 +883,16 @@ func TestCreateServers(t *testing.T) { Value: "$connection_upgrade", }, }, + ResponseHeaders: http.ResponseHeaders{ + Add: []http.Header{ + { + Name: "my-header-response", + Value: "some-value-response-123", + }, + }, + Set: []http.Header{}, + Remove: []string{}, + }, }, { Path: "= /grpc/method", @@ -990,11 +1029,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(), }, @@ -1028,11 +1069,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(), }, @@ -1076,11 +1119,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(), }, @@ -1199,11 +1244,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: "/", @@ -1249,16 +1296,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{}, }, }, }, @@ -2081,3 +2131,71 @@ func TestConvertBackendTLSFromGroup(t *testing.T) { }) } } + +func TestGenerateResponseHeaders(t *testing.T) { + tests := []struct { + filters *dataplane.HTTPFilters + msg string + expectedHeaders http.ResponseHeaders + }{ + { + msg: "no filter set", + filters: &dataplane.HTTPFilters{ + RequestHeaderModifiers: &dataplane.HTTPHeaderFilter{}, + }, + expectedHeaders: http.ResponseHeaders{}, + }, + { + msg: "set filters correctly", + filters: &dataplane.HTTPFilters{ + ResponseHeaderModifiers: &dataplane.HTTPHeaderFilter{ + Add: []dataplane.HTTPHeader{ + { + Name: "Accept-Encoding", + Value: "gzip", + }, + { + Name: "Authorization", + Value: "my-auth", + }, + }, + Set: []dataplane.HTTPHeader{ + { + Name: "Accept-Encoding", + Value: "my-new-overwritten-value", + }, + }, + Remove: []string{"Transfer-Encoding"}, + }, + }, + expectedHeaders: http.ResponseHeaders{ + Add: []http.Header{ + { + Name: "Accept-Encoding", + Value: "gzip", + }, + { + Name: "Authorization", + Value: "my-auth", + }, + }, + Set: []http.Header{ + { + Name: "Accept-Encoding", + Value: "my-new-overwritten-value", + }, + }, + Remove: []string{"Transfer-Encoding"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.msg, func(t *testing.T) { + g := NewWithT(t) + + headers := generateResponseHeaders(tc.filters) + g.Expect(headers).To(Equal(tc.expectedHeaders)) + }) + } +} diff --git a/internal/mode/static/nginx/config/validation/http_filters.go b/internal/mode/static/nginx/config/validation/http_filters.go index 3fc638108e..0a8de17957 100644 --- a/internal/mode/static/nginx/config/validation/http_filters.go +++ b/internal/mode/static/nginx/config/validation/http_filters.go @@ -14,9 +14,9 @@ type HTTPRedirectValidator struct{} // HTTPURLRewriteValidator validates values for a URL rewrite. type HTTPURLRewriteValidator struct{} -// HTTPRequestHeaderValidator validates values for request headers, +// HTTPHeaderValidator validates values for request headers, // which in NGINX is done with the proxy_set_header directive. -type HTTPRequestHeaderValidator struct{} +type HTTPHeaderValidator struct{} var supportedRedirectSchemes = map[string]struct{}{ "http": {}, @@ -72,13 +72,13 @@ func (HTTPURLRewriteValidator) ValidateRewritePath(path string) error { return nil } -func (HTTPRequestHeaderValidator) ValidateRequestHeaderName(name string) error { +func (HTTPHeaderValidator) ValidateFilterHeaderName(name string) error { return validateHeaderName(name) } var requestHeaderValueExamples = []string{"my-header-value", "example/12345=="} -func (HTTPRequestHeaderValidator) ValidateRequestHeaderValue(value string) error { +func (HTTPHeaderValidator) 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..d58716599c 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) { - validator := HTTPRequestHeaderValidator{} +func TestValidateFilterHeaderName(t *testing.T) { + validator := HTTPHeaderValidator{} 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) { - validator := HTTPRequestHeaderValidator{} +func TestValidateFilterHeaderValue(t *testing.T) { + validator := HTTPHeaderValidator{} 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/nginx/config/validation/http_validator.go b/internal/mode/static/nginx/config/validation/http_validator.go index f33adb8434..4e744124c2 100644 --- a/internal/mode/static/nginx/config/validation/http_validator.go +++ b/internal/mode/static/nginx/config/validation/http_validator.go @@ -11,7 +11,7 @@ type HTTPValidator struct { HTTPNJSMatchValidator HTTPRedirectValidator HTTPURLRewriteValidator - HTTPRequestHeaderValidator + HTTPHeaderValidator } var _ validation.HTTPFieldsValidator = HTTPValidator{} diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index 36999dcd4e..3978438a73 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -541,6 +541,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/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index def4563ab8..ab78a46388 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -2083,6 +2083,30 @@ func TestCreateFilters(t *testing.T) { }, } + responseHeaderModifiers1 := v1.HTTPRouteFilter{ + Type: v1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &v1.HTTPHeaderFilter{ + Add: []v1.HTTPHeader{ + { + Name: "X-Server-Version", + Value: "2.3", + }, + }, + }, + } + + responseHeaderModifiers2 := v1.HTTPRouteFilter{ + Type: v1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &v1.HTTPHeaderFilter{ + Set: []v1.HTTPHeader{ + { + Name: "X-Route", + Value: "new-response-value", + }, + }, + }, + } + expectedRedirect1 := HTTPRequestRedirectFilter{ Hostname: helpers.GetPointer("foo.example.com"), } @@ -2098,6 +2122,15 @@ func TestCreateFilters(t *testing.T) { }, } + expectedresponseHeaderModifier := HTTPHeaderFilter{ + Add: []HTTPHeader{ + { + Name: "X-Server-Version", + Value: "2.3", + }, + }, + } + tests := []struct { expected HTTPFilters msg string @@ -2147,11 +2180,14 @@ func TestCreateFilters(t *testing.T) { rewrite2, requestHeaderModifiers1, requestHeaderModifiers2, + responseHeaderModifiers1, + responseHeaderModifiers2, }, expected: HTTPFilters{ - RequestRedirect: &expectedRedirect1, - RequestURLRewrite: &expectedRewrite1, - RequestHeaderModifiers: &expectedHeaderModifier1, + RequestRedirect: &expectedRedirect1, + RequestURLRewrite: &expectedRewrite1, + RequestHeaderModifiers: &expectedHeaderModifier1, + ResponseHeaderModifiers: &expectedresponseHeaderModifier, }, msg: "two of each filter, first value for each wins", }, diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 53453b0d82..3e7a861afc 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -114,6 +114,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 5a61d92185..bd5aaefdce 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -12,6 +12,12 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) +var ( + add = "add" + set = "set" + remove = "remove" +) + func buildHTTPRoute( validator validation.HTTPFieldsValidator, ghr *v1.HTTPRoute, @@ -247,7 +253,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"), @@ -256,6 +266,7 @@ func validateFilter( string(v1.HTTPRouteFilterRequestRedirect), string(v1.HTTPRouteFilterURLRewrite), string(v1.HTTPRouteFilterRequestHeaderModifier), + string(v1.HTTPRouteFilterResponseHeaderModifier), }, ) allErrs = append(allErrs, valErr) @@ -358,18 +369,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( @@ -382,40 +389,99 @@ func validateFilterHeaderModifierFields( // Ensure that the header names are case-insensitive unique allErrs = append(allErrs, validateRequestHeadersCaseInsensitiveUnique( headerModifier.Add, - headerModifierPath.Child("add"))..., + headerModifierPath.Child(add))..., ) allErrs = append(allErrs, validateRequestHeadersCaseInsensitiveUnique( headerModifier.Set, - headerModifierPath.Child("set"))..., + headerModifierPath.Child(set))..., ) allErrs = append(allErrs, validateRequestHeaderStringCaseInsensitiveUnique( headerModifier.Remove, - headerModifierPath.Child("remove"))..., + headerModifierPath.Child(remove))..., ) for _, h := range headerModifier.Add { - if err := validator.ValidateRequestHeaderName(string(h.Name)); err != nil { - valErr := field.Invalid(headerModifierPath.Child("add"), h, err.Error()) + 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 { - valErr := field.Invalid(headerModifierPath.Child("add"), h, err.Error()) + 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 { - valErr := field.Invalid(headerModifierPath.Child("set"), h, err.Error()) + 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 { - valErr := field.Invalid(headerModifierPath.Child("set"), h, err.Error()) + 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 { - valErr := field.Invalid(headerModifierPath.Child("remove"), h, err.Error()) + if err := validator.ValidateFilterHeaderName(h); err != nil { + valErr := field.Invalid(headerModifierPath.Child(remove), h, err.Error()) + allErrs = append(allErrs, valErr) + } + } + + 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 + + allErrs = append(allErrs, validateResponseHeaders( + responseHeaderModifier.Add, + filterPath.Child(add))..., + ) + + allErrs = append(allErrs, validateResponseHeaders( + responseHeaderModifier.Set, + filterPath.Child(set))..., + ) + + var removeHeaders []v1.HTTPHeader + for _, h := range responseHeaderModifier.Remove { + removeHeaders = append(removeHeaders, v1.HTTPHeader{Name: v1.HTTPHeaderName(h)}) + } + + allErrs = append(allErrs, validateResponseHeaders( + removeHeaders, + filterPath.Child(remove))..., + ) + + return allErrs +} + +func validateResponseHeaders( + headers []v1.HTTPHeader, + path *field.Path, +) field.ErrorList { + var allErrs field.ErrorList + disallowedResponseHeaderSet := map[string]struct{}{ + "server": {}, + "date": {}, + "x-pad": {}, + "content-type": {}, + "content-length": {}, + "connection": {}, + } + invalidPrefix := "x-accel" + + for _, h := range headers { + valErr := field.Invalid(path, h, "header name is not allowed") + name := strings.ToLower(string(h.Name)) + if _, exists := disallowedResponseHeaderSet[name]; exists || strings.HasPrefix(name, strings.ToLower(invalidPrefix)) { allErrs = append(allErrs, valErr) } } diff --git a/internal/mode/static/state/graph/httproute_test.go b/internal/mode/static/state/graph/httproute_test.go index 36bddc430c..ede2abcbd7 100644 --- a/internal/mode/static/state/graph/httproute_test.go +++ b/internal/mode/static/state/graph/httproute_test.go @@ -865,6 +865,14 @@ func TestValidateFilter(t *testing.T) { expectErrCount: 0, name: "valid request header modifiers filter", }, + { + filter: gatewayv1.HTTPRouteFilter{ + Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, + ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{}, + }, + expectErrCount: 0, + name: "valid response header modifiers filter", + }, { filter: gatewayv1.HTTPRouteFilter{ Type: gatewayv1.HTTPRouteFilterRequestMirror, @@ -1223,7 +1231,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{ @@ -1240,7 +1248,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{ @@ -1255,7 +1263,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{ @@ -1272,8 +1280,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{ @@ -1318,7 +1326,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: "response header modifier filter with disallowed header name", + }, + { + 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: "response header modifier filter with disallowed header name prefix", + }, + } + + 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 52e20bb47f..3bc5847004 100644 --- a/internal/mode/static/state/validation/validator.go +++ b/internal/mode/static/state/validation/validator.go @@ -25,8 +25,8 @@ 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 } // GenericValidator validates any generic values from NGF API resources from the perspective of a data-plane. diff --git a/site/content/how-to/traffic-management/response-headers.md b/site/content/how-to/traffic-management/response-headers.md new file mode 100644 index 0000000000..03d6f60da0 --- /dev/null +++ b/site/content/how-to/traffic-management/response-headers.md @@ -0,0 +1,212 @@ +--- +title: "HTTP Response Headers" +description: "Learn how to modify the response headers of your application using NGINX Gateway Fabric." +weight: 700 +toc: true +--- + +[HTTP Header Modifiers](https://gateway-api.sigs.k8s.io/guides/http-header-modifier/?h=request#http-header-modifiers) can be used to add, modify or remove headers during the request-response lifecycle. The [ResponseHeaderModifier](https://gateway-api.sigs.k8s.io/guides/http-header-modifier/#http-response-header-modifier) is used to alter headers in a response to the client. + +In this guide we will modify the headers for HTTP responses when client requests are made. For an introduction to exposing your application, we recommend that you follow the [basic guide]({{< relref "/how-to/traffic-management/routing-traffic-to-your-app.md" >}}) first. + +We'll begin by configuring an app with custom headers and a straightforward HTTPRoute. We'll then observe the server response in relation to header responses. Next, we'll delve into modifying some of those headers using an HTTPRoute with filters to modify *response* headers. Our aim will be to verify whether the server responds with the modified headers. + + +## Prerequisites + +- [Install]({{< relref "/installation/" >}}) NGINX Gateway Fabric. +- [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP + address and port of NGINX Gateway Fabric into shell variables: + + ```text + GW_IP=XXX.YYY.ZZZ.III + GW_PORT= + ``` + +{{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}} + +### Deploy the Headers application + +Begin by deploying the example application `headers`. It is a simple application that adds response headers which we'll later tweak and customize. + +```shell +kubectl apply -f https://raw.githubusercontent.com/nginxinc/nginx-gateway-fabric/v1.3.0/examples/http-response-header-filter/headers.yaml +``` + +This will create the headers Service and a Deployment with one Pod. Run the following command to verify the resources were created: + +```shell +kubectl get pods,svc +``` + +```text +NAME READY STATUS RESTARTS AGE +pod/headers-6f854c478-hd2jr 1/1 Running 0 95s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/headers ClusterIP 10.96.15.12 80/TCP 95s +``` + +### Deploy the Gateway API Resources for the Header Application + +The [Gateway](https://gateway-api.sigs.k8s.io/api-types/gateway/) resource is typically deployed by the [Cluster Operator](https://gateway-api.sigs.k8s.io/concepts/roles-and-personas/#roles-and-personas_1). This Gateway defines a single listener on port 80. Since no hostname is specified, this listener matches on all hostnames. To deploy the Gateway: + +```yaml +kubectl apply -f - <}}) 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`