Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(translator): early request header modifier #4004

Merged
merged 14 commits into from
Aug 14, 2024
7 changes: 7 additions & 0 deletions api/v1alpha1/clienttrafficpolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
)

Expand Down Expand Up @@ -134,6 +135,12 @@ type HeaderSettings struct {
//
// +optional
PreserveXRequestID *bool `json:"preserveXRequestID,omitempty"`

// EarlyRequestHeaders defines settings for early request header modification, before envoy performs
// routing, tracing and built-in header manipulation.
//
// +optional
EarlyRequestHeaders *gwapiv1.HTTPHeaderFilter `json:"earlyRequestHeaders,omitempty"`
}

// WithUnderscoresAction configures the action to take when an HTTP header with underscores
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,148 @@ spec:
DisableRateLimitHeaders configures Envoy Proxy to omit the "X-RateLimit-" response headers
when rate limiting is enabled.
type: boolean
earlyRequestHeaders:
description: |-
EarlyRequestHeaders defines settings for early request header modification, before envoy performs
routing, tracing and built-in header manipulation.
properties:
add:
description: |-
Add adds the given header(s) (name, value) to the request
before the action. It appends to any existing values associated
with the header name.


Input:
GET /foo HTTP/1.1
my-header: foo


Config:
add:
- name: "my-header"
value: "bar,baz"


Output:
GET /foo HTTP/1.1
my-header: foo,bar,baz
items:
description: HTTPHeader represents an HTTP Header name and
value as defined by RFC 7230.
properties:
name:
description: |-
Name is the name of the HTTP Header to be matched. Name matching MUST be
case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).


If multiple entries specify equivalent header names, the first entry with
an equivalent name MUST be considered for a match. Subsequent entries
with an equivalent header name MUST be ignored. Due to the
case-insensitivity of header names, "foo" and "Foo" are considered
equivalent.
maxLength: 256
minLength: 1
pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
type: string
value:
description: Value is the value of HTTP Header to be
matched.
maxLength: 4096
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 16
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
remove:
description: |-
Remove the given header(s) from the HTTP request before the action. The
value of Remove is a list of HTTP header names. Note that the header
names are case-insensitive (see
https://datatracker.ietf.org/doc/html/rfc2616#section-4.2).


Input:
GET /foo HTTP/1.1
my-header1: foo
my-header2: bar
my-header3: baz


Config:
remove: ["my-header1", "my-header3"]


Output:
GET /foo HTTP/1.1
my-header2: bar
items:
type: string
maxItems: 16
type: array
x-kubernetes-list-type: set
set:
description: |-
Set overwrites the request with the given header (name, value)
before the action.


Input:
GET /foo HTTP/1.1
my-header: foo


Config:
set:
- name: "my-header"
value: "bar"


Output:
GET /foo HTTP/1.1
my-header: bar
items:
description: HTTPHeader represents an HTTP Header name and
value as defined by RFC 7230.
properties:
name:
description: |-
Name is the name of the HTTP Header to be matched. Name matching MUST be
case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).


If multiple entries specify equivalent header names, the first entry with
an equivalent name MUST be considered for a match. Subsequent entries
with an equivalent header name MUST be ignored. Due to the
case-insensitivity of header names, "foo" and "Foo" are considered
equivalent.
maxLength: 256
minLength: 1
pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$
type: string
value:
description: Value is the value of HTTP Header to be
matched.
maxLength: 4096
minLength: 1
type: string
required:
- name
- value
type: object
maxItems: 16
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
type: object
enableEnvoyHeaders:
description: |-
EnableEnvoyHeaders configures Envoy Proxy to add the "X-Envoy-" headers to requests
Expand Down
147 changes: 144 additions & 3 deletions internal/gatewayapi/clienttrafficpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/utils/ptr"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
Expand Down Expand Up @@ -432,7 +433,10 @@
translateClientIPDetection(policy.Spec.ClientIPDetection, httpIR)

// Translate Header Settings
translateListenerHeaderSettings(policy.Spec.Headers, httpIR)
if err = translateListenerHeaderSettings(policy.Spec.Headers, httpIR); err != nil {
err = perr.WithMessage(err, "Headers")
errs = errors.Join(errs, err)
}

// Translate Path Settings
translatePathSettings(policy.Spec.Path, httpIR)
Expand Down Expand Up @@ -613,9 +617,9 @@
httpIR.ClientIPDetection = (*ir.ClientIPDetectionSettings)(clientIPDetection)
}

func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, httpIR *ir.HTTPListener) {
func translateListenerHeaderSettings(headerSettings *egv1a1.HeaderSettings, httpIR *ir.HTTPListener) error {
if headerSettings == nil {
return
return nil
}
httpIR.Headers = &ir.HeaderSettings{
EnableEnvoyHeaders: ptr.Deref(headerSettings.EnableEnvoyHeaders, false),
Expand All @@ -634,6 +638,16 @@
httpIR.Headers.XForwardedClientCert.CertDetailsToAdd = headerSettings.XForwardedClientCert.CertDetailsToAdd
}
}

if headerSettings.EarlyRequestHeaders != nil {
headersToAdd, headersToRemove, err := translateEarlyRequestHeaders(headerSettings.EarlyRequestHeaders)
if err != nil {
return err
}
httpIR.Headers.EarlyAddRequestHeaders = headersToAdd
httpIR.Headers.EarlyRemoveRequestHeaders = headersToRemove
}
return nil
}

func translateHTTP1Settings(http1Settings *egv1a1.HTTP1Settings, httpIR *ir.HTTPListener) error {
Expand Down Expand Up @@ -869,3 +883,130 @@

return irConnection, nil
}

func translateEarlyRequestHeaders(headerModifier *gwapiv1.HTTPHeaderFilter) ([]ir.AddHeader, []string, error) {
// Make sure the header modifier config actually exists
if headerModifier == nil {
return nil, nil, nil

Check warning on line 890 in internal/gatewayapi/clienttrafficpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/clienttrafficpolicy.go#L890

Added line #L890 was not covered by tests
}
var errs error
emptyFilterConfig := true // keep track of whether the provided config is empty or not

var AddRequestHeaders []ir.AddHeader
var RemoveRequestHeaders []string

// Add request headers
if headersToAdd := headerModifier.Add; headersToAdd != nil {
if len(headersToAdd) > 0 {
emptyFilterConfig = false
}
for _, addHeader := range headersToAdd {
emptyFilterConfig = false
if addHeader.Name == "" {
errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot add a header with an empty name"))
// try to process the rest of the headers and produce a valid config.
continue
}
// Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names
if strings.ContainsAny(string(addHeader.Name), "/:") {
errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders Filter cannot set headers with a '/' or ':' character in them. Header: %q", string(addHeader.Name)))
continue

Check warning on line 913 in internal/gatewayapi/clienttrafficpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/clienttrafficpolicy.go#L912-L913

Added lines #L912 - L913 were not covered by tests
}
// Check if the header is a duplicate
headerKey := string(addHeader.Name)
canAddHeader := true
for _, h := range AddRequestHeaders {
if strings.EqualFold(h.Name, headerKey) {
canAddHeader = false
break
}
}

if !canAddHeader {
continue
}

newHeader := ir.AddHeader{
Name: headerKey,
Append: true,
Value: strings.Split(addHeader.Value, ","),
}

AddRequestHeaders = append(AddRequestHeaders, newHeader)
}
}

// Set headers
if headersToSet := headerModifier.Set; headersToSet != nil {
if len(headersToSet) > 0 {
emptyFilterConfig = false
}
for _, setHeader := range headersToSet {

if setHeader.Name == "" {
errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set a header with an empty name"))
continue
}
// Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names
if strings.ContainsAny(string(setHeader.Name), "/:") {
errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot set headers with a '/' or ':' character in them. Header: '%s'", string(setHeader.Name)))
continue

Check warning on line 953 in internal/gatewayapi/clienttrafficpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/clienttrafficpolicy.go#L952-L953

Added lines #L952 - L953 were not covered by tests
}

// Check if the header to be set has already been configured
headerKey := string(setHeader.Name)
canAddHeader := true
for _, h := range AddRequestHeaders {
if strings.EqualFold(h.Name, headerKey) {
canAddHeader = false
break
}
}
if !canAddHeader {
continue
}
newHeader := ir.AddHeader{
Name: string(setHeader.Name),
Append: false,
Value: strings.Split(setHeader.Value, ","),
}

AddRequestHeaders = append(AddRequestHeaders, newHeader)
}
}

// Remove request headers
// As far as Envoy is concerned, it is ok to configure a header to be added/set and also in the list of
// headers to remove. It will remove the original header if present and then add/set the header after.
if headersToRemove := headerModifier.Remove; headersToRemove != nil {
if len(headersToRemove) > 0 {
emptyFilterConfig = false
}
for _, removedHeader := range headersToRemove {
if removedHeader == "" {
errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders cannot remove a header with an empty name"))
continue
}

canRemHeader := true
for _, h := range RemoveRequestHeaders {
if strings.EqualFold(h, removedHeader) {
canRemHeader = false
break
}
}
if !canRemHeader {
continue
}

RemoveRequestHeaders = append(RemoveRequestHeaders, removedHeader)
}
}

// Update the status if the filter failed to configure any valid headers to add/remove
if len(AddRequestHeaders) == 0 && len(RemoveRequestHeaders) == 0 && !emptyFilterConfig {
errs = errors.Join(errs, fmt.Errorf("EarlyRequestHeaders did not provide valid configuration to add/set/remove any headers"))

Check warning on line 1008 in internal/gatewayapi/clienttrafficpolicy.go

View check run for this annotation

Codecov / codecov/patch

internal/gatewayapi/clienttrafficpolicy.go#L1008

Added line #L1008 was not covered by tests
}

return AddRequestHeaders, RemoveRequestHeaders, errs
}
Loading
Loading