Skip to content

Commit

Permalink
Fix: double slashes in redirect URL (#2998)
Browse files Browse the repository at this point in the history
* fix: double trailing splashs in redirect URL

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* add e2e tests

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* fix lint

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* fix test

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* fix test

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* fix test

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* fix test

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* add e2e tests

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* fix test

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* revert

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* use regex rewrite to generate the redirect url

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* use regex rewrite to generate the redirect url

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* use regex rewrite to generate the redirect url

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* remove comments

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* extract method

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

* address comments

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>

---------

Signed-off-by: huabing zhao <zhaohuabing@gmail.com>
  • Loading branch information
zhaohuabing authored Mar 29, 2024
1 parent deea895 commit ceb697f
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 15 deletions.
47 changes: 34 additions & 13 deletions internal/xds/translator/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func buildXdsRoute(httpRoute *ir.HTTPRoute) (*routev3.Route, error) {
case httpRoute.DirectResponse != nil:
router.Action = &routev3.Route_DirectResponse{DirectResponse: buildXdsDirectResponseAction(httpRoute.DirectResponse)}
case httpRoute.Redirect != nil:
router.Action = &routev3.Route_Redirect{Redirect: buildXdsRedirectAction(httpRoute.Redirect)}
router.Action = &routev3.Route_Redirect{Redirect: buildXdsRedirectAction(httpRoute)}
case httpRoute.URLRewrite != nil:
routeAction := buildXdsURLRewriteAction(httpRoute.Destination.Name, httpRoute.URLRewrite, httpRoute.PathMatch)
if httpRoute.Mirrors != nil {
Expand Down Expand Up @@ -279,8 +279,11 @@ func idleTimeout(httpRoute *ir.HTTPRoute) *durationpb.Duration {
return nil
}

func buildXdsRedirectAction(redirection *ir.Redirect) *routev3.RedirectAction {
routeAction := &routev3.RedirectAction{}
func buildXdsRedirectAction(httpRoute *ir.HTTPRoute) *routev3.RedirectAction {
var (
redirection = httpRoute.Redirect
routeAction = &routev3.RedirectAction{}
)

if redirection.Scheme != nil {
routeAction.SchemeRewriteSpecifier = &routev3.RedirectAction_SchemeRedirect{
Expand All @@ -293,8 +296,14 @@ func buildXdsRedirectAction(redirection *ir.Redirect) *routev3.RedirectAction {
PathRedirect: *redirection.Path.FullReplace,
}
} else if redirection.Path.PrefixMatchReplace != nil {
routeAction.PathRewriteSpecifier = &routev3.RedirectAction_PrefixRewrite{
PrefixRewrite: *redirection.Path.PrefixMatchReplace,
if useRegexRewriteForPrefixMatchReplace(httpRoute.PathMatch, *redirection.Path.PrefixMatchReplace) {
routeAction.PathRewriteSpecifier = &routev3.RedirectAction_RegexRewrite{
RegexRewrite: prefix2RegexRewrite(*httpRoute.PathMatch.Prefix),
}
} else {
routeAction.PathRewriteSpecifier = &routev3.RedirectAction_PrefixRewrite{
PrefixRewrite: *redirection.Path.PrefixMatchReplace,
}
}
}
}
Expand All @@ -313,6 +322,24 @@ func buildXdsRedirectAction(redirection *ir.Redirect) *routev3.RedirectAction {
return routeAction
}

// useRegexRewriteForPrefixMatchReplace checks if the regex rewrite should be used for prefix match replace
// due to the issue with Envoy not handling the case of "//" when the replace string is "/".
// See: https://github.com/envoyproxy/envoy/issues/26055
func useRegexRewriteForPrefixMatchReplace(pathMatch *ir.StringMatch, prefixMatchReplace string) bool {
return pathMatch != nil &&
pathMatch.Prefix != nil &&
(prefixMatchReplace == "" || prefixMatchReplace == "/")
}

func prefix2RegexRewrite(prefix string) *matcherv3.RegexMatchAndSubstitute {
return &matcherv3.RegexMatchAndSubstitute{
Pattern: &matcherv3.RegexMatcher{
Regex: "^" + prefix + `\/*`,
},
Substitution: "/",
}
}

func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMatch *ir.StringMatch) *routev3.RouteAction {
routeAction := &routev3.RouteAction{
ClusterSpecifier: &routev3.RouteAction_Cluster{
Expand All @@ -333,14 +360,8 @@ func buildXdsURLRewriteAction(destName string, urlRewrite *ir.URLRewrite, pathMa
// An empty replace string does not seem to solve the issue so we are using
// a regex match and replace instead
// Remove this workaround once https://github.com/envoyproxy/envoy/issues/26055 is fixed
if pathMatch != nil && pathMatch.Prefix != nil &&
(*urlRewrite.Path.PrefixMatchReplace == "" || *urlRewrite.Path.PrefixMatchReplace == "/") {
routeAction.RegexRewrite = &matcherv3.RegexMatchAndSubstitute{
Pattern: &matcherv3.RegexMatcher{
Regex: "^" + *pathMatch.Prefix + `\/*`,
},
Substitution: "/",
}
if useRegexRewriteForPrefixMatchReplace(pathMatch, *urlRewrite.Path.PrefixMatchReplace) {
routeAction.RegexRewrite = prefix2RegexRewrite(*pathMatch.Prefix)
} else {
routeAction.PrefixRewrite = *urlRewrite.Path.PrefixMatchReplace
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ http:
mergeSlashes: true
escapedSlashesAction: UnescapeAndRedirect
routes:
- name: "redirect-route"
- name: "redirect-route-1"
hostname: "*"
destination:
name: "redirect-route-dest"
Expand All @@ -24,3 +24,29 @@ http:
port: 8443
path:
prefixMatchReplace: /redirected
- name: "redirect-route-2"
hostname: "*"
pathMatch:
prefix: "/redirect"
destination:
name: "redirect-route-dest"
settings:
- endpoints:
- host: "1.2.3.4"
port: 50000
redirect:
path:
prefixMatchReplace: /
- name: "redirect-route-3"
hostname: "*"
pathMatch:
prefix: "/redirect/"
destination:
name: "redirect-route-dest"
settings:
- endpoints:
- host: "1.2.3.4"
port: 50000
redirect:
path:
prefixMatchReplace: /
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@
routes:
- match:
prefix: /
name: redirect-route
name: redirect-route-1
redirect:
hostRedirect: redirected.com
portRedirect: 8443
prefixRewrite: /redirected
responseCode: FOUND
schemeRedirect: https
- match:
pathSeparatedPrefix: /redirect
name: redirect-route-2
redirect:
regexRewrite:
pattern:
regex: ^/redirect\/*
substitution: /
- match:
pathSeparatedPrefix: /redirect
name: redirect-route-3
redirect:
regexRewrite:
pattern:
regex: ^/redirect/\/*
substitution: /
29 changes: 29 additions & 0 deletions test/e2e/testdata/redirect-replaceprefixmatch-slash.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: redirect-replaceprefixmatch-slash
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- matches:
- path:
type: PathPrefix
value: /api/foo/
filters:
- requestRedirect:
path:
replacePrefixMatch: /
type: ReplacePrefixMatch
type: RequestRedirect
- matches:
- path:
type: PathPrefix
value: /api/bar
filters:
- requestRedirect:
path:
replacePrefixMatch: /
type: ReplacePrefixMatch
type: RequestRedirect
125 changes: 125 additions & 0 deletions test/e2e/tests/redirect_replaceprefixmatch_slash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// 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 (
"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/suite"
)

func init() {
ConformanceTests = append(ConformanceTests, RedirectTrailingSlashTest)

}

// RedirectTrailingSlashTest tests that only one slash in the redirect URL
// See https://github.com/envoyproxy/gateway/issues/2976
var RedirectTrailingSlashTest = suite.ConformanceTest{
ShortName: "RedirectTrailingSlash",
Description: "Test that only one slash in the redirect URL",
Manifests: []string{"testdata/redirect-replaceprefixmatch-slash.yaml"},

Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testCases := []struct {
name string
path string
statusCode int
expectedLocation string
}{
// Test cases for the HTTPRoute match /api/foo/
{
name: "match: /api/foo/, request: /api/foo/redirect",
path: "/api/foo/redirect",
statusCode: 302,
expectedLocation: "/redirect",
},
{
name: "match: /api/foo/, request: /api/foo/",
path: "/api/foo/",
statusCode: 302,
expectedLocation: "/",
},
{
name: "match: /api/foo/, request: /api/foo",
path: "/api/foo",
statusCode: 302,
expectedLocation: "/",
},
{
name: "match: /api/foo/, request: /api/foo-bar",
path: "/api/foo-bar",
statusCode: 404,
},

// Test cases for the HTTPRoute match /api/bar
{
name: "match: /api/bar, request: /api/bar/redirect",
path: "/api/bar/redirect",
statusCode: 302,
expectedLocation: "/redirect",
},
{
name: "match: /api/bar, request: /api/bar/",
path: "/api/bar/",
statusCode: 302,
expectedLocation: "/",
},
{
name: "match: /api/bar, request: /api/bar",
path: "/api/bar",
statusCode: 302,
expectedLocation: "/",
},
{
name: "match: /api/bar, request: /api/bar-foo",
path: "/api/bar-foo",
statusCode: 404,
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "redirect-replaceprefixmatch-slash", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)

expectedResponse := http.ExpectedResponse{
Request: http.Request{
Path: testCase.path,
UnfollowRedirect: true,
},
Response: http.Response{
StatusCode: testCase.statusCode,
},
Namespace: ns,
}
if testCase.expectedLocation != "" {
expectedResponse.Response.Headers = map[string]string{
"Location": testCase.expectedLocation,
}
}

req := http.MakeRequest(t, &expectedResponse, gwAddr, "HTTP", "http")
cReq, cResp, err := suite.RoundTripper.CaptureRoundTrip(req)
if err != nil {
t.Errorf("failed to get expected response: %v", err)
}

if err := http.CompareRequest(t, &req, cReq, cResp, expectedResponse); err != nil {
t.Errorf("failed to compare request and response: %v", err)
}
})
}
},
}

0 comments on commit ceb697f

Please sign in to comment.