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 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