Skip to content

Commit

Permalink
Add support for host_rewrite_header (#9608)
Browse files Browse the repository at this point in the history
* add it to the proto

* add plugin

* add changelog

* Adding changelog file to new location

* Deleting changelog file from old location

* add validation

* remove old entry

* Adding changelog file to new location

* Deleting changelog file from old location

* dont remove exposed method

* update changelog

* update changelog

* Update changelog/v1.18.0-beta1/add-host-rewrite-header.yaml

Co-authored-by: Nathan Fudenberg <nathan.fudenberg@solo.io>

* update changelog

* update changelog

* address comments

---------

Co-authored-by: changelog-bot <changelog-bot>
Co-authored-by: soloio-bulldozer[bot] <48420018+soloio-bulldozer[bot]@users.noreply.github.com>
Co-authored-by: Nathan Fudenberg <nathan.fudenberg@solo.io>
  • Loading branch information
3 people committed Jun 14, 2024
1 parent a6cd15b commit e2ebdeb
Show file tree
Hide file tree
Showing 16 changed files with 224 additions and 5 deletions.
11 changes: 11 additions & 0 deletions changelog/v1.18.0-beta1/add-host-rewrite-header.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
changelog:
- type: NEW_FEATURE
issueLink: https://github.com/solo-io/gloo/issues/9579
resolvesIssue: false
description: >-
Adds the `host_rewrite_header` to the route options to allow envoy to swapped the host header with the content of given downstream or custom header. Pay attention to the potential security implications of using this option. Provided header must come from trusted source.
- type: FIX
issueLink: https://github.com/solo-io/gloo/issues/9622
resolvesIssue: true
description: >-
Previously, header names consisting of invalid characters such as '()[]:;,<=>' were accepted when passed via the healthCheck or headerManipulation `requestHeadersToAdd` parameter. This resulted in envoy throwing an `invalid header name` error. Now, header names are validated according to RFC 9110, which is the same validation used by envoy. If a header name consisting of invalid characters is passed via the aforementioned parameters, it is caught and rejected in edge and does not propagate to envoy.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions install/helm/gloo/crds/gateway.solo.io_v1_RouteOption.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,9 @@ spec:
type: object
hostRewrite:
type: string
hostRewriteHeader:
nullable: true
type: string
hostRewritePathRegex:
properties:
pattern:
Expand Down
3 changes: 3 additions & 0 deletions install/helm/gloo/crds/gateway.solo.io_v1_RouteTable.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,9 @@ spec:
type: object
hostRewrite:
type: string
hostRewriteHeader:
nullable: true
type: string
hostRewritePathRegex:
properties:
pattern:
Expand Down
3 changes: 3 additions & 0 deletions install/helm/gloo/crds/gateway.solo.io_v1_VirtualService.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2907,6 +2907,9 @@ spec:
type: object
hostRewrite:
type: string
hostRewriteHeader:
nullable: true
type: string
hostRewritePathRegex:
properties:
pattern:
Expand Down
12 changes: 11 additions & 1 deletion pkg/utils/api_conversion/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
envoytype_gloo "github.com/solo-io/gloo/projects/gloo/pkg/api/external/envoy/type"
v1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins/utils/headers"
envoycore_sk "github.com/solo-io/solo-kit/pkg/api/external/envoy/api/v2/core"
"github.com/solo-io/solo-kit/pkg/errors"
)
Expand Down Expand Up @@ -71,6 +72,15 @@ func ToEnvoyHeaderValueOptionList(option []*envoycore_sk.HeaderValueOption, secr
return result, nil
}

// validateCustomHeaders checks whether the custom header is allowed to be modified as per https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#custom-request-response-headers
// and validates the whether the header will be accepted by envoy
func validateCustomHeaders(header envoycore_sk.HeaderValue) error {
if err := CheckForbiddenCustomHeaders(header); err != nil {
return err
}
return headers.ValidateHeaderKey(header.GetKey())
}

// CheckForbiddenCustomHeaders checks whether the custom header is allowed to be modified as per https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#custom-request-response-headers
func CheckForbiddenCustomHeaders(header envoycore_sk.HeaderValue) error {
key := header.GetKey()
Expand All @@ -90,7 +100,7 @@ func ToEnvoyHeaderValueOptions(option *envoycore_sk.HeaderValueOption, secrets *

switch typedOption := option.GetHeaderOption().(type) {
case *envoycore_sk.HeaderValueOption_Header:
if err := CheckForbiddenCustomHeaders(*typedOption.Header); err != nil {
if err := validateCustomHeaders(*typedOption.Header); err != nil {
return nil, err
}
return []*envoy_config_core_v3.HeaderValueOption{
Expand Down
5 changes: 5 additions & 0 deletions projects/gloo/api/v1/options.proto
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ message RouteOptions {
// Indicates that during forwarding, the host header will be swapped with the result of the regex
// substitution executed on path value with query and fragment removed.
.solo.io.envoy.type.matcher.v3.RegexMatchAndSubstitute host_rewrite_path_regex = 101;

// Indicates that during forwarding, the host header will be swapped with the content of given downstream or custom header.
// If header value is empty, host header is left intact.
// Using this option will append the x-forwarded-host header if append_x_forwarded_host is set.
google.protobuf.StringValue host_rewrite_header = 147;
};
// If true and there is a host rewrite, appends the x-forwarded-host header to requests.
google.protobuf.BoolValue append_x_forwarded_host = 146;
Expand Down
12 changes: 12 additions & 0 deletions projects/gloo/pkg/api/v1/options.pb.clone.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions projects/gloo/pkg/api/v1/options.pb.equal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions projects/gloo/pkg/api/v1/options.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions projects/gloo/pkg/api/v1/options.pb.hash.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion projects/gloo/pkg/plugins/basicroute/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/protocol_upgrade"
"github.com/solo-io/gloo/projects/gloo/pkg/api/v1/options/retries"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins/utils/headers"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins/utils/upgradeconfig"
"github.com/solo-io/solo-kit/pkg/errors"
)
Expand Down Expand Up @@ -227,7 +228,14 @@ func applyHostRewrite(ctx context.Context, in *v1.Route, out *envoy_config_route
routeAction.Route.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: rewriteType.AutoHostRewrite,
}

case *v1.RouteOptions_HostRewriteHeader:
err := headers.ValidateHeaderKey(rewriteType.HostRewriteHeader.GetValue())
if err != nil {
return err
}
routeAction.Route.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_HostRewriteHeader{
HostRewriteHeader: rewriteType.HostRewriteHeader.GetValue(),
}
case *v1.RouteOptions_HostRewritePathRegex:
regex, err := ConvertRegexMatchAndSubstitute(ctx, rewriteType.HostRewritePathRegex)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions projects/gloo/pkg/plugins/basicroute/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,28 @@ var _ = Describe("host rewrite", func() {
Expect(routeAction.GetAutoHostRewrite().GetValue()).To(Equal(true))
})

It("sets host_rewrite_header", func() {
hostRewriteHeader := "host-rewrite"
p := NewPlugin()
out := &envoy_config_route_v3.Route{
Action: &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{},
},
}
err := p.ProcessRoute(plugins.RouteParams{}, &v1.Route{
Options: &v1.RouteOptions{
HostRewriteType: &v1.RouteOptions_HostRewriteHeader{
HostRewriteHeader: &wrappers.StringValue{
Value: hostRewriteHeader,
},
},
},
Action: &v1.Route_RouteAction{},
}, out)
Expect(err).NotTo(HaveOccurred())
Expect(out.GetRoute().GetHostRewriteHeader()).To(Equal(hostRewriteHeader))
})

It("rewrites using regex", func() {
p := NewPlugin()
routeAction := &envoy_config_route_v3.RouteAction{}
Expand Down
13 changes: 13 additions & 0 deletions projects/gloo/pkg/plugins/utils/headers/headers_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package headers_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestHeaders(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Headers Suite")
}
23 changes: 23 additions & 0 deletions projects/gloo/pkg/plugins/utils/headers/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package headers

import (
"fmt"
"regexp"
)

var (
// Regex to check that header names consists of only valid ASCII characters
// https://github.com/envoyproxy/envoy/blob/b0f4332867267913d9aa80c5c0befda14a00d826/source/common/http/character_set_validation.h#L24-L35
validHeaderNameRegex = regexp.MustCompile("^([a-zA-Z0-9!#$%&'*+.^_`|~-])+$")
)

// ValidateHeaderKey checks whether a header is valid based on the RFC and envoy's regex to accept a header key
func ValidateHeaderKey(key string) error {
if len(key) == 0 {
return fmt.Errorf("empty HTTP header names are not allowed")
}
if !validHeaderNameRegex.MatchString(key) {
return fmt.Errorf("'%s' is an invalid HTTP header key", key)
}
return nil
}
49 changes: 49 additions & 0 deletions projects/gloo/pkg/plugins/utils/headers/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package headers_test

import (
"fmt"
"unicode"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/solo-io/gloo/projects/gloo/pkg/plugins/utils/headers"
)

var _ = Describe("Validate header keys", func() {

generateInvalidEntries := func(character rune) []TableEntry {
var entries []TableEntry
entries = append(entries, Entry(fmt.Sprintf("just '%s'", string(character)), fmt.Sprintf("%s", string(character)), true))
entries = append(entries, Entry(fmt.Sprintf("contains leading '%s'", string(character)), fmt.Sprintf("%sreserved", string(character)), true))
entries = append(entries, Entry(fmt.Sprintf("contains trailing '%s'", string(character)), fmt.Sprintf("reserved%s", string(character)), true))
entries = append(entries, Entry(fmt.Sprintf("contains '%s'", string(character)), fmt.Sprintf("rese%srved", string(character)), true))
return entries
}

DescribeTable("Validates header keys", func(key string, errored bool) {
Expect(headers.ValidateHeaderKey(key) != nil).To(Equal(errored))
},
generateInvalidEntries(':'),
generateInvalidEntries('"'),
generateInvalidEntries(' '),
generateInvalidEntries('('),
generateInvalidEntries(')'),
generateInvalidEntries(','),
generateInvalidEntries('/'),
generateInvalidEntries(':'),
generateInvalidEntries(';'),
generateInvalidEntries('<'),
generateInvalidEntries('='),
generateInvalidEntries('>'),
generateInvalidEntries('?'),
generateInvalidEntries('@'),
generateInvalidEntries('['),
generateInvalidEntries('\\'),
generateInvalidEntries(']'),
generateInvalidEntries('{'),
generateInvalidEntries('}'),
generateInvalidEntries('>'),
generateInvalidEntries(unicode.MaxASCII),
Entry("valid header", "valid-header", false),
)
})

0 comments on commit e2ebdeb

Please sign in to comment.