From 859f4e886860442207d7ebc3b047d0bad44ba461 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Tue, 27 May 2025 11:06:05 +0200 Subject: [PATCH] Use routing path in v3 matchers Co-authored-by: Romain --- docs/content/migration/v3.md | 29 +++ ...an_path.toml => simple_sanitize_path.toml} | 3 + integration/simple_test.go | 105 +++++++++- pkg/muxer/http/matcher.go | 9 +- pkg/muxer/http/matcher_v2.go | 4 +- pkg/muxer/http/mux.go | 95 +++++++++ pkg/muxer/http/mux_test.go | 40 ++++ pkg/server/server_entrypoint_tcp.go | 76 ------- pkg/server/server_entrypoint_tcp_test.go | 186 ++++++------------ 9 files changed, 338 insertions(+), 209 deletions(-) rename integration/fixtures/{simple_clean_path.toml => simple_sanitize_path.toml} (93%) diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md index 48696d010..6581b5f39 100644 --- a/docs/content/migration/v3.md +++ b/docs/content/migration/v3.md @@ -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. 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 | diff --git a/integration/fixtures/simple_clean_path.toml b/integration/fixtures/simple_sanitize_path.toml similarity index 93% rename from integration/fixtures/simple_clean_path.toml rename to integration/fixtures/simple_sanitize_path.toml index b889b750b..bf97bc8ea 100644 --- a/integration/fixtures/simple_clean_path.toml +++ b/integration/fixtures/simple_sanitize_path.toml @@ -19,6 +19,9 @@ [providers.file] filename = "{{ .SelfFilename }}" +[core] + defaultRuleSyntax = "{{ .DefaultRuleSyntax }}" + # dynamic configuration [http.routers] [http.routers.without] diff --git a/integration/simple_test.go b/integration/simple_test.go index 4a70bd79b..791ee514c 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1606,9 +1606,108 @@ func (s *SimpleSuite) TestSanitizePath() { whoami1URL := "http://" + net.JoinHostPort(s.getComposeServiceIP("whoami1"), "80") - file := s.adaptFile("fixtures/simple_clean_path.toml", struct { - Server1 string - }{whoami1URL}) + file := s.adaptFile("fixtures/simple_sanitize_path.toml", struct { + Server1 string + 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)) diff --git a/pkg/muxer/http/matcher.go b/pkg/muxer/http/matcher.go index 98ebc8553..74f5d97f0 100644 --- a/pkg/muxer/http/matcher.go +++ b/pkg/muxer/http/matcher.go @@ -142,7 +142,8 @@ func path(tree *matchersTree, paths ...string) error { } tree.matcher = func(req *http.Request) bool { - return req.URL.Path == path + routingPath := getRoutingPath(req) + return routingPath != nil && *routingPath == path } return nil @@ -157,7 +158,8 @@ func pathRegexp(tree *matchersTree, paths ...string) error { } tree.matcher = func(req *http.Request) bool { - return re.MatchString(req.URL.Path) + routingPath := getRoutingPath(req) + return routingPath != nil && re.MatchString(*routingPath) } return nil @@ -171,7 +173,8 @@ func pathPrefix(tree *matchersTree, paths ...string) error { } 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 diff --git a/pkg/muxer/http/matcher_v2.go b/pkg/muxer/http/matcher_v2.go index b6635220f..36f426f48 100644 --- a/pkg/muxer/http/matcher_v2.go +++ b/pkg/muxer/http/matcher_v2.go @@ -28,7 +28,7 @@ func pathV2(tree *matchersTree, paths ...string) error { var routes []*mux.Route for _, path := range paths { - route := mux.NewRouter().NewRoute() + route := mux.NewRouter().UseRoutingPath().NewRoute() if err := route.Path(path).GetError(); err != nil { return err @@ -54,7 +54,7 @@ func pathPrefixV2(tree *matchersTree, paths ...string) error { var routes []*mux.Route for _, path := range paths { - route := mux.NewRouter().NewRoute() + route := mux.NewRouter().UseRoutingPath().NewRoute() if err := route.PathPrefix(path).GetError(); err != nil { return err diff --git a/pkg/muxer/http/mux.go b/pkg/muxer/http/mux.go index 2adad60bc..d3ab6a45d 100644 --- a/pkg/muxer/http/mux.go +++ b/pkg/muxer/http/mux.go @@ -1,10 +1,15 @@ package http import ( + "context" + "errors" "fmt" "net/http" + "net/url" "sort" + "strings" + "github.com/gorilla/mux" "github.com/rs/zerolog/log" "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. // Serves 404 if no handler is found. 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 { if route.matchers.match(req) { route.handler.ServeHTTP(rw, req) @@ -72,6 +87,86 @@ func (m *Muxer) AddRoute(rule string, syntax string, priority int, handler http. 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. func ParseDomains(rule string) ([]string, error) { var matchers []string diff --git a/pkg/muxer/http/mux_test.go b/pkg/muxer/http/mux_test.go index d0b4a08bf..36cc99221 100644 --- a/pkg/muxer/http/mux_test.go +++ b/pkg/muxer/http/mux_test.go @@ -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) + }) + } +} diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 5dcc64830..039d06659 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -17,7 +17,6 @@ import ( "github.com/containous/alice" gokitmetrics "github.com/go-kit/kit/metrics" - "github.com/gorilla/mux" "github.com/pires/go-proxyproto" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -630,8 +629,6 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati handler = http.AllowQuerySemicolons(handler) } - handler = routingPath(handler) - // 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. if configuration.HTTP.SanitizePath != nil && *configuration.HTTP.SanitizePath { @@ -866,76 +863,3 @@ func normalizePath(h http.Handler) http.Handler { 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(), - ), - )) - }) -} diff --git a/pkg/server/server_entrypoint_tcp_test.go b/pkg/server/server_entrypoint_tcp_test.go index 6ce62639b..ecb0c06ae 100644 --- a/pkg/server/server_entrypoint_tcp_test.go +++ b/pkg/server/server_entrypoint_tcp_test.go @@ -13,7 +13,6 @@ import ( "testing" "time" - "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ptypes "github.com/traefik/paerser/types" @@ -510,56 +509,7 @@ func TestNormalizePath_malformedPercentEncoding(t *testing.T) { } } -func TestRoutingPath(t *testing.T) { - 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. +// TestPathOperations tests the whole behavior of normalizePath, and sanitizePath 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. func TestPathOperations(t *testing.T) { // 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) { w.Header().Set("Path", r.URL.Path) w.Header().Set("RawPath", r.URL.EscapedPath()) - w.Header().Set("RoutingPath", r.Context().Value(mux.RoutingPathKey).(string)) w.WriteHeader(http.StatusOK) })) @@ -596,100 +545,88 @@ func TestPathOperations(t *testing.T) { } tests := []struct { - desc string - rawPath string - expectedPath string - expectedRaw string - expectedRoutingPath string - expectedStatus int + desc string + rawPath string + expectedPath string + expectedRaw string + expectedStatus int }{ { - desc: "normalize and sanitize path", - rawPath: "/a/../b/%41%42%43//%2f/", - expectedPath: "/b/ABC///", - expectedRaw: "/b/ABC/%2F/", - expectedRoutingPath: "/b/ABC/%2F/", - expectedStatus: http.StatusOK, + desc: "normalize and sanitize path", + rawPath: "/a/../b/%41%42%43//%2f/", + expectedPath: "/b/ABC///", + expectedRaw: "/b/ABC/%2F/", + expectedStatus: http.StatusOK, }, { - desc: "path with traversal attempt", - rawPath: "/../../b/", - expectedPath: "/b/", - expectedRaw: "/b/", - expectedRoutingPath: "/b/", - expectedStatus: http.StatusOK, + desc: "path with traversal attempt", + rawPath: "/../../b/", + expectedPath: "/b/", + expectedRaw: "/b/", + expectedStatus: http.StatusOK, }, { - desc: "path with multiple traversal attempts", - rawPath: "/a/../../b/../c/", - expectedPath: "/c/", - expectedRaw: "/c/", - expectedRoutingPath: "/c/", - expectedStatus: http.StatusOK, + desc: "path with multiple traversal attempts", + rawPath: "/a/../../b/../c/", + expectedPath: "/c/", + expectedRaw: "/c/", + expectedStatus: http.StatusOK, }, { - desc: "path with mixed traversal and valid segments", - rawPath: "/a/../b/./c/../d/", - expectedPath: "/b/d/", - expectedRaw: "/b/d/", - expectedRoutingPath: "/b/d/", - expectedStatus: http.StatusOK, + desc: "path with mixed traversal and valid segments", + rawPath: "/a/../b/./c/../d/", + expectedPath: "/b/d/", + expectedRaw: "/b/d/", + expectedStatus: http.StatusOK, }, { - desc: "path with trailing slash and traversal", - rawPath: "/a/b/../", - expectedPath: "/a/", - expectedRaw: "/a/", - expectedRoutingPath: "/a/", - expectedStatus: http.StatusOK, + desc: "path with trailing slash and traversal", + rawPath: "/a/b/../", + expectedPath: "/a/", + expectedRaw: "/a/", + expectedStatus: http.StatusOK, }, { - desc: "path with encoded traversal sequences", - rawPath: "/a/%2E%2E/%2E%2E/b/", - expectedPath: "/b/", - expectedRaw: "/b/", - expectedRoutingPath: "/b/", - expectedStatus: http.StatusOK, + desc: "path with encoded traversal sequences", + rawPath: "/a/%2E%2E/%2E%2E/b/", + expectedPath: "/b/", + expectedRaw: "/b/", + expectedStatus: http.StatusOK, }, { - desc: "path with over-encoded traversal sequences", - rawPath: "/a/%252E%252E/%252E%252E/b/", - expectedPath: "/a/%2E%2E/%2E%2E/b/", - expectedRaw: "/a/%252E%252E/%252E%252E/b/", - expectedRoutingPath: "/a/%252E%252E/%252E%252E/b/", - expectedStatus: http.StatusOK, + desc: "path with over-encoded traversal sequences", + rawPath: "/a/%252E%252E/%252E%252E/b/", + expectedPath: "/a/%2E%2E/%2E%2E/b/", + expectedRaw: "/a/%252E%252E/%252E%252E/b/", + expectedStatus: http.StatusOK, }, { - desc: "routing path with unallowed percent-encoded character", - rawPath: "/foo%20bar", - expectedPath: "/foo bar", - expectedRaw: "/foo%20bar", - expectedRoutingPath: "/foo bar", - expectedStatus: http.StatusOK, + desc: "routing path with unallowed percent-encoded character", + rawPath: "/foo%20bar", + expectedPath: "/foo bar", + expectedRaw: "/foo%20bar", + expectedStatus: http.StatusOK, }, { - desc: "routing path with reserved percent-encoded character", - rawPath: "/foo%2Fbar", - expectedPath: "/foo/bar", - expectedRaw: "/foo%2Fbar", - expectedRoutingPath: "/foo%2Fbar", - expectedStatus: http.StatusOK, + desc: "routing path with reserved percent-encoded character", + rawPath: "/foo%2Fbar", + expectedPath: "/foo/bar", + expectedRaw: "/foo%2Fbar", + expectedStatus: http.StatusOK, }, { - desc: "routing path with unallowed and reserved percent-encoded character", - rawPath: "/foo%20%2Fbar", - expectedPath: "/foo /bar", - expectedRaw: "/foo%20%2Fbar", - expectedRoutingPath: "/foo %2Fbar", - expectedStatus: http.StatusOK, + desc: "routing path with unallowed and reserved percent-encoded character", + rawPath: "/foo%20%2Fbar", + expectedPath: "/foo /bar", + expectedRaw: "/foo%20%2Fbar", + expectedStatus: http.StatusOK, }, { - desc: "path with traversal and encoded slash", - rawPath: "/a/..%2Fb/", - expectedPath: "/a/../b/", - expectedRaw: "/a/..%2Fb/", - expectedRoutingPath: "/a/..%2Fb/", - expectedStatus: http.StatusOK, + desc: "path with traversal and encoded slash", + rawPath: "/a/..%2Fb/", + expectedPath: "/a/../b/", + expectedRaw: "/a/..%2Fb/", + expectedStatus: http.StatusOK, }, } @@ -706,7 +643,6 @@ func TestPathOperations(t *testing.T) { assert.Equal(t, test.expectedStatus, res.StatusCode) assert.Equal(t, test.expectedPath, res.Header.Get("Path")) assert.Equal(t, test.expectedRaw, res.Header.Get("RawPath")) - assert.Equal(t, test.expectedRoutingPath, res.Header.Get("RoutingPath")) }) } }