From 5042c5bf406889dcb9e4492ed95bb9e5ebaed5a4 Mon Sep 17 00:00:00 2001 From: Tiscs Sun Date: Mon, 30 Oct 2017 19:54:03 +0800 Subject: [PATCH] Added ReplacePathRegex middleware --- docs/basics.md | 1 + middlewares/replace_path_regex.go | 38 ++++++++++++ middlewares/replace_path_regex_test.go | 80 ++++++++++++++++++++++++++ server/rules.go | 8 +++ server/server.go | 11 ++++ 5 files changed, 138 insertions(+) create mode 100644 middlewares/replace_path_regex.go create mode 100644 middlewares/replace_path_regex_test.go diff --git a/docs/basics.md b/docs/basics.md index 024c8a144..70b32370e 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -86,6 +86,7 @@ Following is the list of existing modifier rules: - `AddPrefix: /products`: Add path prefix to the existing request path prior to forwarding the request to the backend. - `ReplacePath: /serverless-path`: Replaces the path and adds the old path to the `X-Replaced-Path` header. Useful for mapping to AWS Lambda or Google Cloud Functions. +- `ReplacePathRegex: ^/api/v2/(.*) /api/$1`: Replaces the path with a regular expression and adds the old path to the `X-Replaced-Path` header. Separate the regular expression and the replacement by a space. #### Matchers diff --git a/middlewares/replace_path_regex.go b/middlewares/replace_path_regex.go new file mode 100644 index 000000000..4d97c0de5 --- /dev/null +++ b/middlewares/replace_path_regex.go @@ -0,0 +1,38 @@ +package middlewares + +import ( + "net/http" + "regexp" + "strings" + + "github.com/containous/traefik/log" +) + +// ReplacePathRegex is a middleware used to replace the path of a URL request with a regular expression +type ReplacePathRegex struct { + Handler http.Handler + Regexp *regexp.Regexp + Replacement string +} + +// NewReplacePathRegexHandler returns a new ReplacePathRegex +func NewReplacePathRegexHandler(regex string, replacement string, handler http.Handler) http.Handler { + exp, err := regexp.Compile(strings.TrimSpace(regex)) + if err != nil { + log.Errorf("Error compiling regular expression %s: %s", regex, err) + } + return &ReplacePathRegex{ + Regexp: exp, + Replacement: strings.TrimSpace(replacement), + Handler: handler, + } +} + +func (s *ReplacePathRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if s.Regexp != nil && len(s.Replacement) > 0 && s.Regexp.MatchString(r.URL.Path) { + r.Header.Add(ReplacedPathHeader, r.URL.Path) + r.URL.Path = s.Regexp.ReplaceAllString(r.URL.Path, s.Replacement) + r.RequestURI = r.URL.RequestURI() + } + s.Handler.ServeHTTP(w, r) +} diff --git a/middlewares/replace_path_regex_test.go b/middlewares/replace_path_regex_test.go new file mode 100644 index 000000000..606deedfb --- /dev/null +++ b/middlewares/replace_path_regex_test.go @@ -0,0 +1,80 @@ +package middlewares + +import ( + "net/http" + "testing" + + "github.com/containous/traefik/testhelpers" + "github.com/stretchr/testify/assert" +) + +func TestReplacePathRegex(t *testing.T) { + testCases := []struct { + desc string + path string + replacement string + regex string + expectedPath string + expectedHeader string + }{ + { + desc: "simple regex", + path: "/whoami/and/whoami", + replacement: "/who-am-i/$1", + regex: `^/whoami/(.*)`, + expectedPath: "/who-am-i/and/whoami", + expectedHeader: "/whoami/and/whoami", + }, + { + desc: "simple replace (no regex)", + path: "/whoami/and/whoami", + replacement: "/who-am-i", + regex: `/whoami`, + expectedPath: "/who-am-i/and/who-am-i", + expectedHeader: "/whoami/and/whoami", + }, + { + desc: "multiple replacement", + path: "/downloads/src/source.go", + replacement: "/downloads/$1-$2", + regex: `^(?i)/downloads/([^/]+)/([^/]+)$`, + expectedPath: "/downloads/src-source.go", + expectedHeader: "/downloads/src/source.go", + }, + { + desc: "invalid regular expression", + path: "/invalid/regexp/test", + replacement: "/valid/regexp/$1", + regex: `^(?err)/invalid/regexp/([^/]+)$`, + expectedPath: "/invalid/regexp/test", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var actualPath, actualHeader, requestURI string + handler := NewReplacePathRegexHandler( + test.regex, + test.replacement, + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + actualPath = r.URL.Path + actualHeader = r.Header.Get(ReplacedPathHeader) + requestURI = r.RequestURI + }), + ) + + req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+test.path, nil) + + handler.ServeHTTP(nil, req) + + assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.") + assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader) + if test.expectedHeader != "" { + assert.Equal(t, actualPath, requestURI, "Unexpected request URI.") + } + }) + } +} diff --git a/server/rules.go b/server/rules.go index 18444ad74..661b80224 100644 --- a/server/rules.go +++ b/server/rules.go @@ -92,6 +92,13 @@ func (r *Rules) replacePath(paths ...string) *mux.Route { return r.route.route } +func (r *Rules) replacePathRegex(paths ...string) *mux.Route { + for _, path := range paths { + r.route.replacePathRegex = path + } + return r.route.route +} + func (r *Rules) addPrefix(paths ...string) *mux.Route { for _, path := range paths { r.route.addPrefix = path @@ -155,6 +162,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f "HeadersRegexp": r.headersRegexp, "AddPrefix": r.addPrefix, "ReplacePath": r.replacePath, + "ReplacePathRegex": r.replacePathRegex, "Query": r.query, } diff --git a/server/server.go b/server/server.go index 74ff984fd..e19609bbf 100644 --- a/server/server.go +++ b/server/server.go @@ -16,6 +16,7 @@ import ( "reflect" "regexp" "sort" + "strings" "sync" "time" @@ -81,6 +82,7 @@ type serverRoute struct { stripPrefixesRegex []string addPrefix string replacePath string + replacePathRegex string } // NewServer returns an initialized Server. @@ -1065,6 +1067,15 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http } } + if len(serverRoute.replacePathRegex) > 0 { + sp := strings.Split(serverRoute.replacePathRegex, " ") + if len(sp) == 2 { + handler = middlewares.NewReplacePathRegexHandler(sp[0], sp[1], handler) + } else { + log.Warnf("Invalid syntax for ReplacePathRegex: %s. Separate the regular expression and the replacement by a space.", serverRoute.replacePathRegex) + } + } + // add prefix - This needs to always be right before ReplacePath on the chain (second in order in this function) // -- Adding Path Prefix should happen after all *Strip Matcher+Modifiers ran, but before Replace (in case it's configured) if len(serverRoute.addPrefix) > 0 {