Added ReplacePathRegex middleware

This commit is contained in:
Tiscs Sun 2017-10-30 19:54:03 +08:00 committed by Traefiker
parent e8633d17e8
commit 5042c5bf40
5 changed files with 138 additions and 0 deletions

View file

@ -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

View 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)
}

View 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.")
}
})
}
}

View file

@ -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,
}

View file

@ -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 {