Skip to content

Commit

Permalink
e2e: Add an e2e test for the header case-preserving feature (envoypro…
Browse files Browse the repository at this point in the history
…xy#2516)

* Added an E2E test for checking header case preservation

Signed-off-by: Lior Okman <[email protected]>

* Fix a typo in a comment

Signed-off-by: Lior Okman <[email protected]>

* Make the linter happy

Signed-off-by: Lior Okman <[email protected]>

* Use the TimeoutConfig and Debug configuration from the suite

Signed-off-by: Lior Okman <[email protected]>

---------

Signed-off-by: Lior Okman <[email protected]>
  • Loading branch information
liorokman authored Feb 8, 2024
1 parent c3a2bd9 commit d44f4be
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 0 deletions.
107 changes: 107 additions & 0 deletions test/e2e/base/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,110 @@ spec:
allowedRoutes:
kinds:
- kind: UDPRoute
---
apiVersion: v1
kind: Namespace
metadata:
name: gateway-preserve-case-backend
---
apiVersion: v1
kind: ConfigMap
metadata:
name: go-server
namespace: gateway-preserve-case-backend
data:
go.mod: |
module srvr
go 1.21.3
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
)
go.sum: |
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
main.go: |
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/valyala/fasthttp"
)
func HandleFastHTTP(ctx *fasthttp.RequestCtx) {
ctx.QueryArgs().VisitAll(func(key, value []byte) {
if string(key) == "headers" {
ctx.Response.Header.Add(string(value), "PrEsEnT")
}
})
headers := map[string][]string{}
ctx.Request.Header.VisitAll(func(key, value []byte) {
headers[string(key)] = append(headers[string(key)], string(value))
})
if d, err := json.MarshalIndent(headers, "", " "); err != nil {
ctx.Error(fmt.Sprintf("%s", err), fasthttp.StatusBadRequest)
} else {
fmt.Fprintf(ctx, string(d)+"\n")
}
}
func main() {
s := fasthttp.Server{
Handler: HandleFastHTTP,
DisableHeaderNamesNormalizing: true,
}
log.Printf("Starting on port 8000")
log.Fatal(s.ListenAndServe(":8000"))
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: golang-app-deployment
namespace: gateway-preserve-case-backend
spec:
replicas: 1
selector:
matchLabels:
app: golang-app
template:
metadata:
labels:
app: golang-app
spec:
containers:
- name: golang-app-container
command:
- sh
- "-c"
- "cp -a /app /app-live && cd /app-live && go run . "
image: golang:1.21.3-alpine
ports:
- containerPort: 8000
volumeMounts:
- name: go-server
mountPath: /app
volumes:
- name: go-server
configMap:
name: go-server
---
apiVersion: v1
kind: Service
metadata:
name: fasthttp-backend
namespace: gateway-preserve-case-backend
spec:
selector:
app: golang-app
ports:
- protocol: TCP
port: 8000
targetPort: 8000
45 changes: 45 additions & 0 deletions test/e2e/testdata/preserve-case.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-preserve-case
namespace: gateway-preserve-case-backend
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: gateway-conformance-infra
to:
- group: ""
kind: Service
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
name: preserve-case
namespace: gateway-conformance-infra
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: same-namespace
namespace: gateway-conformance-infra
http1:
preserveHeaderCase: true
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: preserve-case
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- matches:
- path:
type: PathPrefix
value: /preserve
backendRefs:
- name: fasthttp-backend
namespace: gateway-preserve-case-backend
port: 8000
140 changes: 140 additions & 0 deletions test/e2e/tests/preservecase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

//go:build e2e
// +build e2e

package tests

import (
"context"
"encoding/json"
"fmt"
"io"
nethttp "net/http"
"net/http/httputil"
"regexp"
"testing"

"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/gateway-api/conformance/utils/http"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
)

func init() {
ConformanceTests = append(ConformanceTests, PreserveCaseTest)
}

// Copied from the conformance suite because it's needed in casePreservingRoundTrip
var startLineRegex = regexp.MustCompile(`(?m)^`)

func formatDump(data []byte, prefix string) string {
data = startLineRegex.ReplaceAllLiteral(data, []byte(prefix))
return string(data)
}

// Copied from the conformance suite and modified to not normalize headers before sending them
// to the remote side.
// The default HTTP client implementation in Golang also automatically normalizes received
// headers as they are parsed , so it's not possible to verify that returned headers were not normalized
func casePreservingRoundTrip(request roundtripper.Request, transport nethttp.RoundTripper, suite *suite.ConformanceTestSuite) (map[string]any, error) {
client := &nethttp.Client{}
client.Transport = transport

method := "GET"
ctx, cancel := context.WithTimeout(context.Background(), suite.TimeoutConfig.RequestTimeout)
defer cancel()
req, err := nethttp.NewRequestWithContext(ctx, method, request.URL.String(), nil)
if err != nil {
return nil, err
}
if request.Host != "" {
req.Host = request.Host
}
if request.Headers != nil {
for name, value := range request.Headers {
req.Header[name] = value
}
}
if suite.Debug {
var dump []byte
dump, err = httputil.DumpRequestOut(req, true)
if err != nil {
return nil, err
}

fmt.Printf("Sending Request:\n%s\n\n", formatDump(dump, "< "))
}

resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if suite.Debug {
var dump []byte
dump, err = httputil.DumpResponse(resp, true)
if err != nil {
return nil, err
}

fmt.Printf("Received Response:\n%s\n\n", formatDump(dump, "< "))
}

cReq := map[string]any{}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

err = json.Unmarshal(body, &cReq)
if err != nil {
return nil, fmt.Errorf("unexpected error reading response: %w", err)
}

return cReq, nil
}

var PreserveCaseTest = suite.ConformanceTest{
ShortName: "Preserve Case",
Description: "Preserve header cases",
Manifests: []string{"testdata/preserve-case.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
t.Run("should preserve header cases in both directions", func(t *testing.T) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "preserve-case", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)

// Can't use the standard method for checking the response, since the remote side isn't the
// conformance echo server and it returns a differently formatted response.
expectedResponse := http.ExpectedResponse{
Request: http.Request{
Path: "/preserve?headers=ReSpOnSeHeAdEr",
Headers: map[string]string{
"SpEcIaL": "Header",
},
},
Namespace: ns,
}

var rt nethttp.RoundTripper
req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http")
respBody, err := casePreservingRoundTrip(req, rt, suite)
if err != nil {
t.Errorf("failed to get expected response: %v", err)
}

if _, found := respBody["SpEcIaL"]; !found {
t.Errorf("case was not preserved for test header: %+v", respBody)
}
})

},
}

0 comments on commit d44f4be

Please sign in to comment.