Added ReplacePathRegex middleware
This commit is contained in:
parent
e8633d17e8
commit
5042c5bf40
5 changed files with 138 additions and 0 deletions
|
@ -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.
|
- `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.
|
- `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
|
#### Matchers
|
||||||
|
|
||||||
|
|
38
middlewares/replace_path_regex.go
Normal file
38
middlewares/replace_path_regex.go
Normal file
|
@ -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)
|
||||||
|
}
|
80
middlewares/replace_path_regex_test.go
Normal file
80
middlewares/replace_path_regex_test.go
Normal file
|
@ -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.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -92,6 +92,13 @@ func (r *Rules) replacePath(paths ...string) *mux.Route {
|
||||||
return r.route.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 {
|
func (r *Rules) addPrefix(paths ...string) *mux.Route {
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
r.route.addPrefix = path
|
r.route.addPrefix = path
|
||||||
|
@ -155,6 +162,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f
|
||||||
"HeadersRegexp": r.headersRegexp,
|
"HeadersRegexp": r.headersRegexp,
|
||||||
"AddPrefix": r.addPrefix,
|
"AddPrefix": r.addPrefix,
|
||||||
"ReplacePath": r.replacePath,
|
"ReplacePath": r.replacePath,
|
||||||
|
"ReplacePathRegex": r.replacePathRegex,
|
||||||
"Query": r.query,
|
"Query": r.query,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -81,6 +82,7 @@ type serverRoute struct {
|
||||||
stripPrefixesRegex []string
|
stripPrefixesRegex []string
|
||||||
addPrefix string
|
addPrefix string
|
||||||
replacePath string
|
replacePath string
|
||||||
|
replacePathRegex string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer returns an initialized Server.
|
// 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)
|
// 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)
|
// -- Adding Path Prefix should happen after all *Strip Matcher+Modifiers ran, but before Replace (in case it's configured)
|
||||||
if len(serverRoute.addPrefix) > 0 {
|
if len(serverRoute.addPrefix) > 0 {
|
||||||
|
|
Loading…
Reference in a new issue