1
0
Fork 0

Use routing path in v3 matchers

Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
Kevin Pollet 2025-05-27 11:06:05 +02:00 committed by GitHub
parent de1802d849
commit 859f4e8868
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 338 additions and 209 deletions

View file

@ -290,3 +290,32 @@ and to help with the migration from v2 to v3.
The `ruleSyntax` router's option was used to override the default rule syntax for a specific router. The `ruleSyntax` router's option was used to override the default rule syntax for a specific router.
In preparation for the next major release, please remove any use of these two options and use the v3 syntax for writing the router's rules. In preparation for the next major release, please remove any use of these two options and use the v3 syntax for writing the router's rules.
## v3.4.1
### Request Path Normalization
Since `v3.4.1`, the request path is now normalized by decoding unreserved characters in the request path,
and also uppercasing the percent-encoded characters.
This follows [RFC 3986 percent-encoding normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.2),
and [RFC 3986 case normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1).
The normalization happens before the request path is sanitized,
and cannot be disabled.
This notably helps with encoded dots characters (which are unreserved characters) to be sanitized properly.
### Routing Path
Since `v3.4.1`, the reserved characters [(as per RFC 3986)](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) are kept encoded in the request path when matching the router rules.
Those characters, when decoded, change the meaning of the request path for routing purposes,
and Traefik now keeps them encoded to avoid any ambiguity.
### Request Path Matching Examples
| Request Path | Router Rule | Traefik v3.4.0 | Traefik v3.4.1 |
|-------------------|------------------------|----------------|----------------|
| `/foo%2Fbar` | PathPrefix(`/foo/bar`) | Match | No match |
| `/foo/../bar` | PathPrefix(`/foo`) | No match | No match |
| `/foo/../bar` | PathPrefix(`/bar`) | Match | Match |
| `/foo/%2E%2E/bar` | PathPrefix(`/foo`) | Match | No match |
| `/foo/%2E%2E/bar` | PathPrefix(`/bar`) | No match | Match |

View file

@ -19,6 +19,9 @@
[providers.file] [providers.file]
filename = "{{ .SelfFilename }}" filename = "{{ .SelfFilename }}"
[core]
defaultRuleSyntax = "{{ .DefaultRuleSyntax }}"
# dynamic configuration # dynamic configuration
[http.routers] [http.routers]
[http.routers.without] [http.routers.without]

View file

@ -1606,9 +1606,108 @@ func (s *SimpleSuite) TestSanitizePath() {
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80") whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80")
file := s.adaptFile("fixtures/simple_clean_path.toml", struct { file := s.adaptFile("fixtures/simple_sanitize_path.toml", struct {
Server1 string Server1 string
}{whoami1URL}) DefaultRuleSyntax string
}{whoami1URL, "v3"})
s.traefikCmd(withConfigFile(file))
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("PathPrefix(`/with`)"))
require.NoError(s.T(), err)
testCases := []struct {
desc string
request string
target string
body string
expected int
}{
{
desc: "Explicit call to the route with a middleware",
request: "GET /with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8000",
expected: http.StatusFound,
},
{
desc: "Explicit call to the route without a middleware",
request: "GET /without HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8000",
expected: http.StatusOK,
body: "GET /without HTTP/1.1",
},
{
desc: "Implicit call to the route with a middleware",
request: "GET /without/../with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8000",
expected: http.StatusFound,
},
{
desc: "Implicit encoded dot dots call to the route with a middleware",
request: "GET /without/%2E%2E/with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8000",
expected: http.StatusFound,
},
{
desc: "Implicit with encoded unreserved character call to the route with a middleware",
request: "GET /%77ith HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8000",
expected: http.StatusFound,
},
{
desc: "Explicit call to the route with a middleware, and disable path sanitization",
request: "GET /with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8001",
expected: http.StatusFound,
},
{
desc: "Explicit call to the route without a middleware, and disable path sanitization",
request: "GET /without HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8001",
expected: http.StatusOK,
body: "GET /without HTTP/1.1",
},
{
desc: "Implicit call to the route with a middleware, and disable path sanitization",
request: "GET /without/../with HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
target: "127.0.0.1:8001",
// The whoami is redirecting to /with, but the path is not sanitized.
expected: http.StatusMovedPermanently,
},
}
for _, test := range testCases {
conn, err := net.Dial("tcp", test.target)
require.NoError(s.T(), err)
_, err = conn.Write([]byte(test.request))
require.NoError(s.T(), err)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
require.NoError(s.T(), err)
assert.Equalf(s.T(), test.expected, resp.StatusCode, "%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected)
if test.body != "" {
body, err := io.ReadAll(resp.Body)
require.NoError(s.T(), err)
assert.Contains(s.T(), string(body), test.body)
}
}
}
func (s *SimpleSuite) TestSanitizePathSyntaxV2() {
s.createComposeProject("base")
s.composeUp()
defer s.composeDown()
whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80")
file := s.adaptFile("fixtures/simple_sanitize_path.toml", struct {
Server1 string
DefaultRuleSyntax string
}{whoami1URL, "v2"})
s.traefikCmd(withConfigFile(file)) s.traefikCmd(withConfigFile(file))

View file

@ -142,7 +142,8 @@ func path(tree *matchersTree, paths ...string) error {
} }
tree.matcher = func(req *http.Request) bool { tree.matcher = func(req *http.Request) bool {
return req.URL.Path == path routingPath := getRoutingPath(req)
return routingPath != nil && *routingPath == path
} }
return nil return nil
@ -157,7 +158,8 @@ func pathRegexp(tree *matchersTree, paths ...string) error {
} }
tree.matcher = func(req *http.Request) bool { tree.matcher = func(req *http.Request) bool {
return re.MatchString(req.URL.Path) routingPath := getRoutingPath(req)
return routingPath != nil && re.MatchString(*routingPath)
} }
return nil return nil
@ -171,7 +173,8 @@ func pathPrefix(tree *matchersTree, paths ...string) error {
} }
tree.matcher = func(req *http.Request) bool { tree.matcher = func(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, path) routingPath := getRoutingPath(req)
return routingPath != nil && strings.HasPrefix(*routingPath, path)
} }
return nil return nil

View file

@ -28,7 +28,7 @@ func pathV2(tree *matchersTree, paths ...string) error {
var routes []*mux.Route var routes []*mux.Route
for _, path := range paths { for _, path := range paths {
route := mux.NewRouter().NewRoute() route := mux.NewRouter().UseRoutingPath().NewRoute()
if err := route.Path(path).GetError(); err != nil { if err := route.Path(path).GetError(); err != nil {
return err return err
@ -54,7 +54,7 @@ func pathPrefixV2(tree *matchersTree, paths ...string) error {
var routes []*mux.Route var routes []*mux.Route
for _, path := range paths { for _, path := range paths {
route := mux.NewRouter().NewRoute() route := mux.NewRouter().UseRoutingPath().NewRoute()
if err := route.PathPrefix(path).GetError(); err != nil { if err := route.PathPrefix(path).GetError(); err != nil {
return err return err

View file

@ -1,10 +1,15 @@
package http package http
import ( import (
"context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"sort" "sort"
"strings"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/rules" "github.com/traefik/traefik/v3/pkg/rules"
) )
@ -33,6 +38,16 @@ func NewMuxer(parser SyntaxParser) *Muxer {
// ServeHTTP forwards the connection to the matching HTTP handler. // ServeHTTP forwards the connection to the matching HTTP handler.
// Serves 404 if no handler is found. // Serves 404 if no handler is found.
func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (m *Muxer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := log.Ctx(req.Context())
var err error
req, err = withRoutingPath(req)
if err != nil {
logger.Debug().Err(err).Msg("Unable to add routing path to request context")
rw.WriteHeader(http.StatusBadRequest)
return
}
for _, route := range m.routes { for _, route := range m.routes {
if route.matchers.match(req) { if route.matchers.match(req) {
route.handler.ServeHTTP(rw, req) route.handler.ServeHTTP(rw, req)
@ -72,6 +87,86 @@ func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http.
return nil return nil
} }
// reservedCharacters contains the mapping of the percent-encoded form to the ASCII form
// of the reserved characters according to https://datatracker.ietf.org/doc/html/rfc3986#section-2.2.
// By extension to https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 the percent character is also considered a reserved character.
// Because decoding the percent character would change the meaning of the URL.
var reservedCharacters = map[string]rune{
"%3A": ':',
"%2F": '/',
"%3F": '?',
"%23": '#',
"%5B": '[',
"%5D": ']',
"%40": '@',
"%21": '!',
"%24": '$',
"%26": '&',
"%27": '\'',
"%28": '(',
"%29": ')',
"%2A": '*',
"%2B": '+',
"%2C": ',',
"%3B": ';',
"%3D": '=',
"%25": '%',
}
// getRoutingPath retrieves the routing path from the request context.
// It returns nil if the routing path is not set in the context.
func getRoutingPath(req *http.Request) *string {
routingPath := req.Context().Value(mux.RoutingPathKey)
if routingPath != nil {
rp := routingPath.(string)
return &rp
}
return nil
}
// withRoutingPath decodes non-allowed characters in the EscapedPath and stores it in the request context to be able to use it for routing.
// This allows using the decoded version of the non-allowed characters in the routing rules for a better UX.
// For example, the rule PathPrefix(`/foo bar`) will match the following request path `/foo%20bar`.
func withRoutingPath(req *http.Request) (*http.Request, error) {
escapedPath := req.URL.EscapedPath()
var routingPathBuilder strings.Builder
for i := 0; i < len(escapedPath); i++ {
if escapedPath[i] != '%' {
routingPathBuilder.WriteString(string(escapedPath[i]))
continue
}
// This should never happen as the standard library will reject requests containing invalid percent-encodings.
// This discards URLs with a percent character at the end.
if i+2 >= len(escapedPath) {
return nil, errors.New("invalid percent-encoding at the end of the URL path")
}
encodedCharacter := escapedPath[i : i+3]
if _, reserved := reservedCharacters[encodedCharacter]; reserved {
routingPathBuilder.WriteString(encodedCharacter)
} else {
// This should never happen as the standard library will reject requests containing invalid percent-encodings.
decodedCharacter, err := url.PathUnescape(encodedCharacter)
if err != nil {
return nil, errors.New("invalid percent-encoding in URL path")
}
routingPathBuilder.WriteString(decodedCharacter)
}
i += 2
}
return req.WithContext(
context.WithValue(
req.Context(),
mux.RoutingPathKey,
routingPathBuilder.String(),
),
), nil
}
// ParseDomains extract domains from rule. // ParseDomains extract domains from rule.
func ParseDomains(rule string) ([]string, error) { func ParseDomains(rule string) ([]string, error) {
var matchers []string var matchers []string

View file

@ -557,3 +557,43 @@ func TestGetRulePriority(t *testing.T) {
}) })
} }
} }
func TestRoutingPath(t *testing.T) {
tests := []struct {
desc string
path string
expectedRoutingPath string
}{
{
desc: "unallowed percent-encoded character is decoded",
path: "/foo%20bar",
expectedRoutingPath: "/foo bar",
},
{
desc: "reserved percent-encoded character is kept encoded",
path: "/foo%2Fbar",
expectedRoutingPath: "/foo%2Fbar",
},
{
desc: "multiple mixed characters",
path: "/foo%20bar%2Fbaz%23qux",
expectedRoutingPath: "/foo bar%2Fbaz%23qux",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "http://foo"+test.path, http.NoBody)
var err error
req, err = withRoutingPath(req)
require.NoError(t, err)
gotRoutingPath := getRoutingPath(req)
assert.NotNil(t, gotRoutingPath)
assert.Equal(t, test.expectedRoutingPath, *gotRoutingPath)
})
}
}

View file

@ -17,7 +17,6 @@ import (
"github.com/containous/alice" "github.com/containous/alice"
gokitmetrics "github.com/go-kit/kit/metrics" gokitmetrics "github.com/go-kit/kit/metrics"
"github.com/gorilla/mux"
"github.com/pires/go-proxyproto" "github.com/pires/go-proxyproto"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -630,8 +629,6 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
handler = http.AllowQuerySemicolons(handler) handler = http.AllowQuerySemicolons(handler)
} }
handler = routingPath(handler)
// Note that the Path sanitization has to be done after the path normalization, // Note that the Path sanitization has to be done after the path normalization,
// hence the wrapping has to be done before the normalize path wrapping. // hence the wrapping has to be done before the normalize path wrapping.
if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath { if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath {
@ -866,76 +863,3 @@ func normalizePath(h http.Handler) http.Handler {
h.ServeHTTP(rw, r2) h.ServeHTTP(rw, r2)
}) })
} }
// reservedCharacters contains the mapping of the percent-encoded form to the ASCII form
// of the reserved characters according to https://datatracker.ietf.org/doc/html/rfc3986#section-2.2.
// By extension to https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 the percent character is also considered a reserved character.
// Because decoding the percent character would change the meaning of the URL.
var reservedCharacters = map[string]rune{
"%3A": ':',
"%2F": '/',
"%3F": '?',
"%23": '#',
"%5B": '[',
"%5D": ']',
"%40": '@',
"%21": '!',
"%24": '$',
"%26": '&',
"%27": '\'',
"%28": '(',
"%29": ')',
"%2A": '*',
"%2B": '+',
"%2C": ',',
"%3B": ';',
"%3D": '=',
"%25": '%',
}
// routingPath decodes non-allowed characters in the EscapedPath and stores it in the context to be able to use it for routing.
// This allows using the decoded version of the non-allowed characters in the routing rules for a better UX.
// For example, the rule PathPrefix(`/foo bar`) will match the following request path `/foo%20bar`.
func routingPath(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
escapedPath := req.URL.EscapedPath()
var routingPathBuilder strings.Builder
for i := 0; i < len(escapedPath); i++ {
if escapedPath[i] != '%' {
routingPathBuilder.WriteString(string(escapedPath[i]))
continue
}
// This should never happen as the standard library will reject requests containing invalid percent-encodings.
// This discards URLs with a percent character at the end.
if i+2 >= len(escapedPath) {
rw.WriteHeader(http.StatusBadRequest)
return
}
encodedCharacter := escapedPath[i : i+3]
if _, reserved := reservedCharacters[encodedCharacter]; reserved {
routingPathBuilder.WriteString(encodedCharacter)
} else {
// This should never happen as the standard library will reject requests containing invalid percent-encodings.
decodedCharacter, err := url.PathUnescape(encodedCharacter)
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
return
}
routingPathBuilder.WriteString(decodedCharacter)
}
i += 2
}
h.ServeHTTP(rw, req.WithContext(
context.WithValue(
req.Context(),
mux.RoutingPathKey,
routingPathBuilder.String(),
),
))
})
}

View file

@ -13,7 +13,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types" ptypes "github.com/traefik/paerser/types"
@ -510,56 +509,7 @@ func TestNormalizePath_malformedPercentEncoding(t *testing.T) {
} }
} }
func TestRoutingPath(t *testing.T) { // TestPathOperations tests the whole behavior of normalizePath, and sanitizePath combined through the use of the createHTTPServer func.
tests := []struct {
desc string
path string
expRoutingPath string
expStatus int
}{
{
desc: "unallowed percent-encoded character is decoded",
path: "/foo%20bar",
expRoutingPath: "/foo bar",
expStatus: http.StatusOK,
},
{
desc: "reserved percent-encoded character is kept encoded",
path: "/foo%2Fbar",
expRoutingPath: "/foo%2Fbar",
expStatus: http.StatusOK,
},
{
desc: "multiple mixed characters",
path: "/foo%20bar%2Fbaz%23qux",
expRoutingPath: "/foo bar%2Fbaz%23qux",
expStatus: http.StatusOK,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var gotRoute string
handler := routingPath(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotRoute, _ = r.Context().Value(mux.RoutingPathKey).(string)
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "http://foo"+test.path, http.NoBody)
res := httptest.NewRecorder()
handler.ServeHTTP(res, req)
assert.Equal(t, test.expStatus, res.Code)
assert.Equal(t, test.expRoutingPath, gotRoute)
})
}
}
// TestPathOperations tests the whole behavior of normalizePath, sanitizePath, and routingPath combined through the use of the createHTTPServer func.
// It aims to guarantee the server entrypoint handler is secure regarding a large variety of cases that could lead to path traversal attacks. // It aims to guarantee the server entrypoint handler is secure regarding a large variety of cases that could lead to path traversal attacks.
func TestPathOperations(t *testing.T) { func TestPathOperations(t *testing.T) {
// Create a listener for the server. // Create a listener for the server.
@ -580,7 +530,6 @@ func TestPathOperations(t *testing.T) {
server.Switcher.UpdateHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server.Switcher.UpdateHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Path", r.URL.Path) w.Header().Set("Path", r.URL.Path)
w.Header().Set("RawPath", r.URL.EscapedPath()) w.Header().Set("RawPath", r.URL.EscapedPath())
w.Header().Set("RoutingPath", r.Context().Value(mux.RoutingPathKey).(string))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
})) }))
@ -596,100 +545,88 @@ func TestPathOperations(t *testing.T) {
} }
tests := []struct { tests := []struct {
desc string desc string
rawPath string rawPath string
expectedPath string expectedPath string
expectedRaw string expectedRaw string
expectedRoutingPath string expectedStatus int
expectedStatus int
}{ }{
{ {
desc: "normalize and sanitize path", desc: "normalize and sanitize path",
rawPath: "/a/../b/%41%42%43//%2f/", rawPath: "/a/../b/%41%42%43//%2f/",
expectedPath: "/b/ABC///", expectedPath: "/b/ABC///",
expectedRaw: "/b/ABC/%2F/", expectedRaw: "/b/ABC/%2F/",
expectedRoutingPath: "/b/ABC/%2F/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with traversal attempt", desc: "path with traversal attempt",
rawPath: "/../../b/", rawPath: "/../../b/",
expectedPath: "/b/", expectedPath: "/b/",
expectedRaw: "/b/", expectedRaw: "/b/",
expectedRoutingPath: "/b/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with multiple traversal attempts", desc: "path with multiple traversal attempts",
rawPath: "/a/../../b/../c/", rawPath: "/a/../../b/../c/",
expectedPath: "/c/", expectedPath: "/c/",
expectedRaw: "/c/", expectedRaw: "/c/",
expectedRoutingPath: "/c/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with mixed traversal and valid segments", desc: "path with mixed traversal and valid segments",
rawPath: "/a/../b/./c/../d/", rawPath: "/a/../b/./c/../d/",
expectedPath: "/b/d/", expectedPath: "/b/d/",
expectedRaw: "/b/d/", expectedRaw: "/b/d/",
expectedRoutingPath: "/b/d/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with trailing slash and traversal", desc: "path with trailing slash and traversal",
rawPath: "/a/b/../", rawPath: "/a/b/../",
expectedPath: "/a/", expectedPath: "/a/",
expectedRaw: "/a/", expectedRaw: "/a/",
expectedRoutingPath: "/a/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with encoded traversal sequences", desc: "path with encoded traversal sequences",
rawPath: "/a/%2E%2E/%2E%2E/b/", rawPath: "/a/%2E%2E/%2E%2E/b/",
expectedPath: "/b/", expectedPath: "/b/",
expectedRaw: "/b/", expectedRaw: "/b/",
expectedRoutingPath: "/b/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with over-encoded traversal sequences", desc: "path with over-encoded traversal sequences",
rawPath: "/a/%252E%252E/%252E%252E/b/", rawPath: "/a/%252E%252E/%252E%252E/b/",
expectedPath: "/a/%2E%2E/%2E%2E/b/", expectedPath: "/a/%2E%2E/%2E%2E/b/",
expectedRaw: "/a/%252E%252E/%252E%252E/b/", expectedRaw: "/a/%252E%252E/%252E%252E/b/",
expectedRoutingPath: "/a/%252E%252E/%252E%252E/b/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "routing path with unallowed percent-encoded character", desc: "routing path with unallowed percent-encoded character",
rawPath: "/foo%20bar", rawPath: "/foo%20bar",
expectedPath: "/foo bar", expectedPath: "/foo bar",
expectedRaw: "/foo%20bar", expectedRaw: "/foo%20bar",
expectedRoutingPath: "/foo bar", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "routing path with reserved percent-encoded character", desc: "routing path with reserved percent-encoded character",
rawPath: "/foo%2Fbar", rawPath: "/foo%2Fbar",
expectedPath: "/foo/bar", expectedPath: "/foo/bar",
expectedRaw: "/foo%2Fbar", expectedRaw: "/foo%2Fbar",
expectedRoutingPath: "/foo%2Fbar", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "routing path with unallowed and reserved percent-encoded character", desc: "routing path with unallowed and reserved percent-encoded character",
rawPath: "/foo%20%2Fbar", rawPath: "/foo%20%2Fbar",
expectedPath: "/foo /bar", expectedPath: "/foo /bar",
expectedRaw: "/foo%20%2Fbar", expectedRaw: "/foo%20%2Fbar",
expectedRoutingPath: "/foo %2Fbar", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
{ {
desc: "path with traversal and encoded slash", desc: "path with traversal and encoded slash",
rawPath: "/a/..%2Fb/", rawPath: "/a/..%2Fb/",
expectedPath: "/a/../b/", expectedPath: "/a/../b/",
expectedRaw: "/a/..%2Fb/", expectedRaw: "/a/..%2Fb/",
expectedRoutingPath: "/a/..%2Fb/", expectedStatus: http.StatusOK,
expectedStatus: http.StatusOK,
}, },
} }
@ -706,7 +643,6 @@ func TestPathOperations(t *testing.T) {
assert.Equal(t, test.expectedStatus, res.StatusCode) assert.Equal(t, test.expectedStatus, res.StatusCode)
assert.Equal(t, test.expectedPath, res.Header.Get("Path")) assert.Equal(t, test.expectedPath, res.Header.Get("Path"))
assert.Equal(t, test.expectedRaw, res.Header.Get("RawPath")) assert.Equal(t, test.expectedRaw, res.Header.Get("RawPath"))
assert.Equal(t, test.expectedRoutingPath, res.Header.Get("RoutingPath"))
}) })
} }
} }