Use routing path in v3 matchers
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
parent
de1802d849
commit
859f4e8868
9 changed files with 338 additions and 209 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue