From 42986dec2f3a5819ab695daa46b961c0dfff3ff0 Mon Sep 17 00:00:00 2001 From: Clayton Gonsalves Date: Tue, 15 Aug 2023 11:09:32 +0200 Subject: [PATCH] If applied this PR will allow dynamic host header rewrite on the route level for HTTPProxies. Fixes #5673 Signed-off-by: Clayton Gonsalves --- apis/projectcontour/v1/httpproxy.go | 6 +- .../XXXX-clayton-gonsalves-minor.md | 24 +++ examples/contour/01-crds.yaml | 6 +- examples/render/contour-deployment.yaml | 6 +- .../render/contour-gateway-provisioner.yaml | 6 +- examples/render/contour-gateway.yaml | 6 +- examples/render/contour.yaml | 6 +- internal/dag/dag.go | 4 + internal/dag/policy.go | 35 +++- internal/dag/policy_test.go | 152 ++++++++++++++ internal/envoy/route.go | 9 +- internal/envoy/v3/route.go | 8 +- internal/featuretests/v3/envoy.go | 9 + internal/featuretests/v3/headerpolicy_test.go | 120 +++++++++++ .../docs/main/config/api-reference.html | 7 +- .../docs/main/config/request-rewriting.md | 71 +++++++ .../e2e/httpproxy/host_header_rewrite_test.go | 194 +++++++++++++++++- test/e2e/httpproxy/httpproxy_test.go | 16 +- 18 files changed, 662 insertions(+), 23 deletions(-) create mode 100644 changelogs/unreleased/XXXX-clayton-gonsalves-minor.md diff --git a/apis/projectcontour/v1/httpproxy.go b/apis/projectcontour/v1/httpproxy.go index db77734597f..d7f63ce1367 100644 --- a/apis/projectcontour/v1/httpproxy.go +++ b/apis/projectcontour/v1/httpproxy.go @@ -551,6 +551,10 @@ type Route struct { // +optional PathRewritePolicy *PathRewritePolicy `json:"pathRewritePolicy,omitempty"` // The policy for managing request headers during proxying. + // NOTE: You can set the custom values to the host header on a request using + // the below format "%REQ(X-Header-Namee)%". If the value of the header + // is empty, it is ignored. + // // +optional RequestHeadersPolicy *HeadersPolicy `json:"requestHeadersPolicy,omitempty"` // The policy for managing response headers during proxying. @@ -1268,7 +1272,7 @@ type LoadBalancerPolicy struct { } // HeadersPolicy defines how headers are managed during forwarding. -// The `Host` header is treated specially and if set in a HTTP response +// The `Host` header is treated specially and if set in a HTTP request // will be used as the SNI server name when forwarding over TLS. It is an // error to attempt to set the `Host` header in a HTTP response. type HeadersPolicy struct { diff --git a/changelogs/unreleased/XXXX-clayton-gonsalves-minor.md b/changelogs/unreleased/XXXX-clayton-gonsalves-minor.md new file mode 100644 index 00000000000..7b83af64e22 --- /dev/null +++ b/changelogs/unreleased/XXXX-clayton-gonsalves-minor.md @@ -0,0 +1,24 @@ +## HTTPProxy:Allow Host header rewrite with dynamic headers. + +This Change allows the host header to be rewritten on requests using dynamic headers on the only route level. + +#### Example +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: dynamic-host-header-rewrite +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: "%REQ(x-rewrite-header)%" +``` + diff --git a/examples/contour/01-crds.yaml b/examples/contour/01-crds.yaml index ccb1d01adc4..c60b3d2189c 100644 --- a/examples/contour/01-crds.yaml +++ b/examples/contour/01-crds.yaml @@ -5883,8 +5883,10 @@ spec: type: object type: object requestHeadersPolicy: - description: The policy for managing request headers during - proxying. + description: 'The policy for managing request headers during + proxying. NOTE: You can set the custom values to the host + header on a request using the below format "%REQ(X-Header-Namee)%". + If the value of the header is empty, it is ignored.' properties: remove: description: Remove specifies a list of HTTP header names diff --git a/examples/render/contour-deployment.yaml b/examples/render/contour-deployment.yaml index cf1cf5c2886..3966993304e 100644 --- a/examples/render/contour-deployment.yaml +++ b/examples/render/contour-deployment.yaml @@ -6096,8 +6096,10 @@ spec: type: object type: object requestHeadersPolicy: - description: The policy for managing request headers during - proxying. + description: 'The policy for managing request headers during + proxying. NOTE: You can set the custom values to the host + header on a request using the below format "%REQ(X-Header-Namee)%". + If the value of the header is empty, it is ignored.' properties: remove: description: Remove specifies a list of HTTP header names diff --git a/examples/render/contour-gateway-provisioner.yaml b/examples/render/contour-gateway-provisioner.yaml index 39103148552..5a782c54973 100644 --- a/examples/render/contour-gateway-provisioner.yaml +++ b/examples/render/contour-gateway-provisioner.yaml @@ -5897,8 +5897,10 @@ spec: type: object type: object requestHeadersPolicy: - description: The policy for managing request headers during - proxying. + description: 'The policy for managing request headers during + proxying. NOTE: You can set the custom values to the host + header on a request using the below format "%REQ(X-Header-Namee)%". + If the value of the header is empty, it is ignored.' properties: remove: description: Remove specifies a list of HTTP header names diff --git a/examples/render/contour-gateway.yaml b/examples/render/contour-gateway.yaml index aa3ee90539d..74702f0f2a8 100644 --- a/examples/render/contour-gateway.yaml +++ b/examples/render/contour-gateway.yaml @@ -6102,8 +6102,10 @@ spec: type: object type: object requestHeadersPolicy: - description: The policy for managing request headers during - proxying. + description: 'The policy for managing request headers during + proxying. NOTE: You can set the custom values to the host + header on a request using the below format "%REQ(X-Header-Namee)%". + If the value of the header is empty, it is ignored.' properties: remove: description: Remove specifies a list of HTTP header names diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml index d542a73f2d6..3cf7362696c 100644 --- a/examples/render/contour.yaml +++ b/examples/render/contour.yaml @@ -6096,8 +6096,10 @@ spec: type: object type: object requestHeadersPolicy: - description: The policy for managing request headers during - proxying. + description: 'The policy for managing request headers during + proxying. NOTE: You can set the custom values to the host + header on a request using the below format "%REQ(X-Header-Namee)%". + If the value of the header is empty, it is ignored.' properties: remove: description: Remove specifies a list of HTTP header names diff --git a/internal/dag/dag.go b/internal/dag/dag.go index 09918e17b21..42ea56461c3 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -454,6 +454,10 @@ type HeadersPolicy struct { // HostRewrite defines if a host should be rewritten on upstream requests HostRewrite string + // HostRewriteHeader defines if a host should be rewritten on upstream requests + // via a header value. only applicable for routes. + HostRewriteHeader string + Add map[string]string Set map[string]string Remove []string diff --git a/internal/dag/policy.go b/internal/dag/policy.go index 8f6966ccbbf..de929356676 100644 --- a/internal/dag/policy.go +++ b/internal/dag/policy.go @@ -127,6 +127,11 @@ func headersPolicyService(defaultPolicy *HeadersPolicy, policy *contour_api_v1.H return nil, fmt.Errorf("rewriting %q header is not supported", key) } if len(userPolicy.HostRewrite) == 0 { + // check for the hostRewriteHeader on the service. Return error if set since this + // is not supported on envoy. + if HostRewriteHeader := extractHostRewriteHeaderValue(v); HostRewriteHeader != "" { + return nil, fmt.Errorf("rewriting %q host header with dynamic value is not supported on service", key) + } userPolicy.HostRewrite = v } continue @@ -164,6 +169,7 @@ func headersPolicyRoute(policy *contour_api_v1.HeadersPolicy, allowHostRewrite b set := make(map[string]string, len(policy.Set)) hostRewrite := "" + hostRewriteHeader := "" for _, entry := range policy.Set { key := http.CanonicalHeaderKey(entry.Name) if _, ok := set[key]; ok { @@ -173,8 +179,13 @@ func headersPolicyRoute(policy *contour_api_v1.HeadersPolicy, allowHostRewrite b if !allowHostRewrite { return nil, fmt.Errorf("rewriting %q header is not supported", key) } - hostRewrite = entry.Value - continue + if extractedHostRewriteHeader := extractHostRewriteHeaderValue(entry.Value); extractedHostRewriteHeader != "" { + hostRewriteHeader = extractedHostRewriteHeader + continue + } else { + hostRewrite = entry.Value + continue + } } if msgs := validation.IsHTTPHeaderName(key); len(msgs) != 0 { return nil, fmt.Errorf("invalid set header %q: %v", key, msgs) @@ -203,12 +214,26 @@ func headersPolicyRoute(policy *contour_api_v1.HeadersPolicy, allowHostRewrite b } return &HeadersPolicy{ - Set: set, - HostRewrite: hostRewrite, - Remove: rl, + Set: set, + HostRewrite: hostRewrite, + HostRewriteHeader: hostRewriteHeader, + Remove: rl, }, nil } +// extractHostRewriteHeaderValue returns the value of the header +func extractHostRewriteHeaderValue(s string) string { + // match "%REQ()%" + re := regexp.MustCompile(`%REQ\((\S+)\)%`) + matches := re.FindStringSubmatch(s) + + if len(matches) == 2 { + return strings.TrimSpace(matches[1]) + } + + return "" +} + // headersPolicyGatewayAPI builds a *HeaderPolicy for the supplied HTTPHeaderFilter. // TODO: Take care about the order of operators once https://github.com/kubernetes-sigs/gateway-api/issues/480 was solved. func headersPolicyGatewayAPI(hf *gatewayapi_v1beta1.HTTPHeaderFilter, headerPolicyType string) (*HeadersPolicy, error) { diff --git a/internal/dag/policy_test.go b/internal/dag/policy_test.go index f222ff109e2..2c923a31e08 100644 --- a/internal/dag/policy_test.go +++ b/internal/dag/policy_test.go @@ -15,6 +15,7 @@ package dag import ( "errors" + "fmt" "io" "testing" "time" @@ -1249,6 +1250,36 @@ func TestValidateHeaderAlteration(t *testing.T) { "K-Foo": "100%%", }, }, + }, { + name: "rewrite host header with dynamic headers", + in: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{ + Name: "K-Foo", + Value: "100%", + }}, + }, + dyn: map[string]string{ + "CONTOUR_NAMESPACE": "myns", + }, + dhp: &HeadersPolicy{ + Set: map[string]string{ + "K-Foo": "50%", + }, + }, + want: &HeadersPolicy{ + Set: map[string]string{ + "K-Foo": "100%%", + }, + }, + }, { + name: "Host header rewrite via dynamic header", + in: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{ + Name: "Host", + Value: "%REQ(foo)%", + }}, + }, + wantErr: fmt.Errorf("rewriting \"Host\" header is not supported"), }} for _, test := range tests { @@ -1259,3 +1290,124 @@ func TestValidateHeaderAlteration(t *testing.T) { }) } } + +func TestExtractHeaderValue(t *testing.T) { + tests := map[string]string{ + "%REQ(X-Header-Name)%": "X-Header-Name", + "%req(X-Header-Name)%": "", + "%REQ( Content-Type )%": "", + "REQ(Content-Type)": "", + "%REQ(Content-Type%": "", + "SomeOtherValue": "", + } + + for input, expected := range tests { + t.Run(input, func(t *testing.T) { + actual := extractHostRewriteHeaderValue(input) + if actual != expected { + t.Errorf("For input %q, expected %q, got %q", input, expected, actual) + } + }) + } +} + +func TestHeadersPolicyRoute(t *testing.T) { + tests := []struct { + name string + policy *contour_api_v1.HeadersPolicy + allowRewrite bool + dynHeaders map[string]string + expected *HeadersPolicy + expectedErr error + }{ + { + name: "nil policy", + policy: nil, + expected: nil, + }, + { + name: "duplicate set headers", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: "X-Header", Value: "Test"}, {Name: "X-Header", Value: "Test2"}}, + }, + expectedErr: fmt.Errorf("duplicate header addition: %q", "X-Header"), + }, + { + name: "host rewrite not allowed", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: "Host", Value: "Test"}}, + }, + allowRewrite: false, + expectedErr: fmt.Errorf("rewriting %q header is not supported", "Host"), + }, + { + name: "host rewrite allowed", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: "Host", Value: "Test"}}, + }, + allowRewrite: true, + expected: &HeadersPolicy{ + HostRewrite: "Test", + Remove: nil, + }, + }, + { + name: "host rewrite allowed, by header", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: "Host", Value: "%REQ(Test)%"}}, + }, + allowRewrite: true, + expected: &HeadersPolicy{ + HostRewrite: "", + HostRewriteHeader: "Test", + Remove: nil, + }, + }, + { + name: "host rewrite allowed, by header. invalid", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: "Host", Value: "%REQ (Test"}}, + }, + allowRewrite: true, + expected: &HeadersPolicy{ + HostRewrite: "%REQ (Test", + HostRewriteHeader: "", + Remove: nil, + }, + }, + { + name: "invalid header name", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: " Invalid-Header ", Value: "Test"}}, + }, + expectedErr: fmt.Errorf(`invalid set header " Invalid-Header ": [a valid HTTP header must consist of alphanumeric characters or '-' (e.g. 'X-Header-Name', regex used for validation is '[-A-Za-z0-9]+')]`), + }, + { + name: "duplicate remove headers", + policy: &contour_api_v1.HeadersPolicy{ + Remove: []string{"X-Header", "X-Header"}, + }, + expectedErr: fmt.Errorf("duplicate header removal: %q", "X-Header"), + }, + { + name: "valid set and remove headers", + policy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{Name: "X-Header", Value: "Test"}}, + Remove: []string{"Y-Header"}, + }, + expected: &HeadersPolicy{ + Set: map[string]string{"X-Header": "Test"}, + HostRewrite: "", + Remove: []string{"Y-Header"}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := headersPolicyRoute(tc.policy, tc.allowRewrite, tc.dynHeaders) + assert.Equal(t, tc.expected, result) + assert.Equal(t, tc.expectedErr, err) + }) + } +} diff --git a/internal/envoy/route.go b/internal/envoy/route.go index 03d720242b6..c10817fe306 100644 --- a/internal/envoy/route.go +++ b/internal/envoy/route.go @@ -19,13 +19,20 @@ import ( "google.golang.org/protobuf/types/known/durationpb" ) -func HostReplaceHeader(hp *dag.HeadersPolicy) string { +func HostRewriteLiteral(hp *dag.HeadersPolicy) string { if hp == nil { return "" } return hp.HostRewrite } +func HostRewriteHeader(hp *dag.HeadersPolicy) string { + if hp == nil { + return "" + } + return hp.HostRewriteHeader +} + // Timeout converts a timeout.Setting to a protobuf.Duration // that's appropriate for Envoy. In general (though there are // exceptions), Envoy uses the following semantics: diff --git a/internal/envoy/v3/route.go b/internal/envoy/v3/route.go index 609dcc2c76d..a8e3c643a97 100644 --- a/internal/envoy/v3/route.go +++ b/internal/envoy/v3/route.go @@ -415,10 +415,14 @@ func routeRoute(r *dag.Route) *envoy_route_v3.Route_Route { } // Check for host header policy and set if found - if val := envoy.HostReplaceHeader(r.RequestHeadersPolicy); val != "" { + if val := envoy.HostRewriteLiteral(r.RequestHeadersPolicy); val != "" { ra.HostRewriteSpecifier = &envoy_route_v3.RouteAction_HostRewriteLiteral{ HostRewriteLiteral: val, } + } else if val := envoy.HostRewriteHeader(r.RequestHeadersPolicy); val != "" { + ra.HostRewriteSpecifier = &envoy_route_v3.RouteAction_HostRewriteHeader{ + HostRewriteHeader: val, + } } if r.Websocket { @@ -616,7 +620,7 @@ func weightedClusters(route *dag.Route) *envoy_route_v3.WeightedCluster { c.RequestHeadersToAdd = append(headerValueList(cluster.RequestHeadersPolicy.Set, false), headerValueList(cluster.RequestHeadersPolicy.Add, true)...) c.RequestHeadersToRemove = cluster.RequestHeadersPolicy.Remove // Check for host header policy and set if found - if val := envoy.HostReplaceHeader(cluster.RequestHeadersPolicy); val != "" { + if val := envoy.HostRewriteLiteral(cluster.RequestHeadersPolicy); val != "" { c.HostRewriteSpecifier = &envoy_route_v3.WeightedCluster_ClusterWeight_HostRewriteLiteral{ HostRewriteLiteral: val, } diff --git a/internal/featuretests/v3/envoy.go b/internal/featuretests/v3/envoy.go index ea60b26ef1b..425afef2a5c 100644 --- a/internal/featuretests/v3/envoy.go +++ b/internal/featuretests/v3/envoy.go @@ -156,6 +156,15 @@ func routeHostRewrite(cluster, newHostName string) *envoy_route_v3.Route_Route { } } +func routeHostRewriteHeader(cluster, hostnameHeader string) *envoy_route_v3.Route_Route { + return &envoy_route_v3.Route_Route{ + Route: &envoy_route_v3.RouteAction{ + ClusterSpecifier: &envoy_route_v3.RouteAction_Cluster{Cluster: cluster}, + HostRewriteSpecifier: &envoy_route_v3.RouteAction_HostRewriteHeader{HostRewriteHeader: hostnameHeader}, + }, + } +} + func upgradeHTTPS(match *envoy_route_v3.RouteMatch) *envoy_route_v3.Route { return &envoy_route_v3.Route{ Match: match, diff --git a/internal/featuretests/v3/headerpolicy_test.go b/internal/featuretests/v3/headerpolicy_test.go index bd0696fb3cd..f74fa31c563 100644 --- a/internal/featuretests/v3/headerpolicy_test.go +++ b/internal/featuretests/v3/headerpolicy_test.go @@ -219,3 +219,123 @@ func TestHeaderPolicy_ReplaceHeader_HTTProxy(t *testing.T) { TypeUrl: clusterType, }) } + +func TestHeaderPolicy_ReplaceHostHeader_HTTProxy(t *testing.T) { + // Enable ExternalName processing here because + // we need to check that host rewrites work in combination + // with ExternalName. + rh, c, done := setup(t, enableExternalNameService(t)) + defer done() + + rh.OnAdd(fixture.NewService("svc1"). + WithPorts(v1.ServicePort{Port: 80, TargetPort: intstr.FromInt(8080)}), + ) + + rh.OnAdd(fixture.NewProxy("simple").WithSpec( + contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{Fqdn: "hello.world"}, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: "svc1", + Port: 80, + }}, + RequestHeadersPolicy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{ + Name: "Host", + Value: "%REQ(x-goodbye-planet)%", + }}, + }, + }}, + }), + ) + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + envoy_v3.RouteConfiguration("ingress_http", + envoy_v3.VirtualHost("hello.world", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeHostRewriteHeader("default/svc1/80/da39a3ee5e", "x-goodbye-planet"), + }, + ), + ), + ), + TypeUrl: routeType, + }) + + rh.OnAdd(fixture.NewService("externalname"). + Annotate("projectcontour.io/upstream-protocol.tls", "https,443"). + WithSpec(v1.ServiceSpec{ + ExternalName: "goodbye.planet", + Type: v1.ServiceTypeExternalName, + Ports: []v1.ServicePort{{ + Port: 443, + Name: "https", + }}, + }), + ) + + rh.OnAdd(&v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Type: "kubernetes.io/tls", + Data: featuretests.Secretdata(featuretests.CERTIFICATE, featuretests.RSA_PRIVATE_KEY), + }) + + // Proxy with SNI + rh.OnAdd(fixture.NewProxy("simple").WithSpec( + contour_api_v1.HTTPProxySpec{ + VirtualHost: &contour_api_v1.VirtualHost{ + Fqdn: "hello.world", + TLS: &contour_api_v1.TLS{SecretName: "foo"}, + }, + Routes: []contour_api_v1.Route{{ + Services: []contour_api_v1.Service{{ + Name: "externalname", + Port: 443, + }}, + RequestHeadersPolicy: &contour_api_v1.HeadersPolicy{ + Set: []contour_api_v1.HeaderValue{{ + Name: "Host", + Value: "%REQ(x-goodbye-planet)%", + }}, + }, + }}, + }), + ) + + c.Request(routeType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: routeResources(t, + envoy_v3.RouteConfiguration("ingress_http", + envoy_v3.VirtualHost("hello.world", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: &envoy_route_v3.Route_Redirect{ + Redirect: &envoy_route_v3.RedirectAction{ + SchemeRewriteSpecifier: &envoy_route_v3.RedirectAction_HttpsRedirect{ + HttpsRedirect: true, + }, + }, + }, + }), + ), + envoy_v3.RouteConfiguration("https/hello.world", + envoy_v3.VirtualHost("hello.world", + &envoy_route_v3.Route{ + Match: routePrefix("/"), + Action: routeHostRewriteHeader("default/externalname/443/9ebffe8f28", "x-goodbye-planet"), + }, + )), + ), + TypeUrl: routeType, + }) + + c.Request(clusterType).Equals(&envoy_discovery_v3.DiscoveryResponse{ + Resources: resources(t, + tlsCluster(externalNameCluster("default/externalname/443/9ebffe8f28", "default/externalname/https", "default_externalname_443", "goodbye.planet", 443), nil, "goodbye.planet", "goodbye.planet", nil), + ), + TypeUrl: clusterType, + }) +} diff --git a/site/content/docs/main/config/api-reference.html b/site/content/docs/main/config/api-reference.html index c9f2508f65b..ef57b9a38eb 100644 --- a/site/content/docs/main/config/api-reference.html +++ b/site/content/docs/main/config/api-reference.html @@ -2097,7 +2097,7 @@

HeadersPolicy

HeadersPolicy defines how headers are managed during forwarding. -The Host header is treated specially and if set in a HTTP response +The Host header is treated specially and if set in a HTTP request will be used as the SNI server name when forwarding over TLS. It is an error to attempt to set the Host header in a HTTP response.

@@ -3704,7 +3704,10 @@

Route (Optional) -

The policy for managing request headers during proxying.

+

The policy for managing request headers during proxying. +NOTE: You can set the custom values to the host header on a request using +the below format “%REQ(X-Header-Namee)%”. If the value of the header +is empty, it is ignored.

diff --git a/site/content/docs/main/config/request-rewriting.md b/site/content/docs/main/config/request-rewriting.md index a01dd78bcae..7a82816128f 100644 --- a/site/content/docs/main/config/request-rewriting.md +++ b/site/content/docs/main/config/request-rewriting.md @@ -257,3 +257,74 @@ For per-Route requestHeadersPolicy only `%CONTOUR_NAMESPACE%` is set and using `%CONTOUR_SERVICE_NAME%` and `%CONTOUR_SERVICE_PORT%` will end up as the literal values `%%CONTOUR_SERVICE_NAME%%` and `%%CONTOUR_SERVICE_PORT%%`, respectively. + +### Manipulating the Host header. + +Contour allows users to manipulate the host header in two ways, using the `requestHeadersPolicy`. +#### static rewrite + +You can set the host to a static value. This can be done on the route and service level. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: static-host-header-rewrite-route +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: foo.com +``` + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: static-host-header-rewrite-service +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: "foo.com" +``` + +#### dynamic rewrite + +You can also set the host header dynamically with the content of a existing header. +The format has to be `"%REQ()%"`. If the header is empty, it is ignored. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: dynamic-host-header-rewrite-route +spec: + fqdn: local.projectcontour.io + routes: + - conditions: + - prefix: / + services: + - name: s1 + port: 80 + - requestHeaderPolicy: + set: + - name: host + value: "%REQ(x-rewrite-header)%" +``` + +Note: Only one of static or dynamic host rewrite can be specified. diff --git a/test/e2e/httpproxy/host_header_rewrite_test.go b/test/e2e/httpproxy/host_header_rewrite_test.go index 6e53711c6dc..651d75b053f 100644 --- a/test/e2e/httpproxy/host_header_rewrite_test.go +++ b/test/e2e/httpproxy/host_header_rewrite_test.go @@ -16,15 +16,19 @@ package httpproxy import ( + "context" + "net/http" + . "github.com/onsi/ginkgo/v2" contourv1 "github.com/projectcontour/contour/apis/projectcontour/v1" "github.com/projectcontour/contour/test/e2e" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func testHostHeaderRewrite(namespace string) { +func testHostRewriteLiteral(namespace string) { Specify("hostname can be rewritten with policy on route", func() { t := f.T() @@ -71,3 +75,191 @@ func testHostHeaderRewrite(namespace string) { assert.Equal(t, "rewritten.com", f.GetEchoResponseBody(res.Body).Host) }) } + +func testHostRewriteHeaderHTTPService(namespace string) { + opts := []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{ + "x-host-rewrite": "newhostrewritten.com", + }), + } + + Specify("hostname can be rewritten from header with policy on route", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "ingress-conformance-echo") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "host-header-rewrite", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "hostheaderrewrite.projectcontour.io", + }, + Routes: []contourv1.Route{ + { + Services: []contourv1.Service{ + { + Name: "ingress-conformance-echo", + Port: 80, + }, + }, + RequestHeadersPolicy: &contourv1.HeadersPolicy{ + Set: []contourv1.HeaderValue{ + { + Name: "Host", + Value: "%REQ(x-host-rewrite)%", + }, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + RequestOpts: opts, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + assert.Equal(t, "newhostrewritten.com", f.GetEchoResponseBody(res.Body).Host) + }) +} + +func testHostRewriteHeaderHTTPSService(namespace string) { + opts := []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{ + "x-host-rewrite": "newhostrewritten.com", + }), + } + + Specify("hostname can be rewritten with policy on route with https", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "ingress-conformance-echo") + f.Certs.CreateSelfSignedCert(namespace, "ingress-conformance-echo", "ingress-conformance-echo", "https.hostheaderrewrite.projectcontour.io") + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "host-header-rewrite", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "https.hostheaderrewrite.projectcontour.io", + TLS: &contourv1.TLS{ + SecretName: "ingress-conformance-echo", + }, + }, + Routes: []contourv1.Route{ + { + Services: []contourv1.Service{ + { + Name: "ingress-conformance-echo", + Port: 80, + }, + }, + RequestHeadersPolicy: &contourv1.HeadersPolicy{ + Set: []contourv1.HeaderValue{ + { + Name: "Host", + Value: "%REQ(x-host-rewrite)%", + }, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + res, ok := f.HTTP.SecureRequestUntil(&e2e.HTTPSRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + RequestOpts: opts, + }) + + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + assert.Equal(t, "newhostrewritten.com", f.GetEchoResponseBody(res.Body).Host) + }) +} + +func testHostRewriteHeadeExternalNameService(namespace string) { + opts := []func(*http.Request){ + e2e.OptSetHeaders(map[string]string{ + "x-host-rewrite": "newhostrewritten.com", + }), + } + + Specify("hostname can be rewritten from header with policy on route", func() { + t := f.T() + + f.Fixtures.Echo.Deploy(namespace, "ingress-conformance-echo") + + externalNameService := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "external-name-service", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + ExternalName: "ingress-conformance-echo." + namespace, + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + }, + } + require.NoError(t, f.Client.Create(context.TODO(), externalNameService)) + + p := &contourv1.HTTPProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: "host-header-rewrite", + }, + Spec: contourv1.HTTPProxySpec{ + VirtualHost: &contourv1.VirtualHost{ + Fqdn: "hostheaderrewrite.projectcontour.io", + }, + Routes: []contourv1.Route{ + { + Services: []contourv1.Service{ + { + Name: externalNameService.Name, + Port: 80, + }, + }, + RequestHeadersPolicy: &contourv1.HeadersPolicy{ + Set: []contourv1.HeaderValue{ + { + Name: "Host", + Value: "%REQ(x-host-rewrite)%", + }, + }, + }, + }, + }, + }, + } + f.CreateHTTPProxyAndWaitFor(p, e2e.HTTPProxyValid) + + res, ok := f.HTTP.RequestUntil(&e2e.HTTPRequestOpts{ + Host: p.Spec.VirtualHost.Fqdn, + Condition: e2e.HasStatusCode(200), + RequestOpts: opts, + }) + require.NotNil(t, res, "request never succeeded") + require.Truef(t, ok, "expected 200 response code, got %d", res.StatusCode) + + assert.Equal(t, "newhostrewritten.com", f.GetEchoResponseBody(res.Body).Host) + }) +} diff --git a/test/e2e/httpproxy/httpproxy_test.go b/test/e2e/httpproxy/httpproxy_test.go index fa327366305..a721a5467ad 100644 --- a/test/e2e/httpproxy/httpproxy_test.go +++ b/test/e2e/httpproxy/httpproxy_test.go @@ -288,7 +288,21 @@ var _ = Describe("HTTPProxy", func() { f.NamespacedTest("httpproxy-dynamic-headers", testDynamicHeaders) - f.NamespacedTest("httpproxy-host-header-rewrite", testHostHeaderRewrite) + f.NamespacedTest("httpproxy-host-header-rewrite-literal", testHostRewriteLiteral) + + f.NamespacedTest("httpproxy-host-header-rewrite-header", testHostRewriteHeaderHTTPService) + + f.NamespacedTest("httpproxy-host-header-rewrite-header-https", testHostRewriteHeaderHTTPSService) + + f.NamespacedTest("httpproxy-host-header-rewrite-header-externalname-service", func(namespace string) { + Context("with ExternalName Services enabled", func() { + BeforeEach(func() { + contourConfig.EnableExternalNameService = true + contourConfiguration.Spec.EnableExternalNameService = ref.To(true) + }) + testHostRewriteHeadeExternalNameService(namespace) + }) + }) f.NamespacedTest("httpproxy-ip-filters", func(namespace string) { // ip filter tests rely on the ability to forge x-forwarded-for