diff --git a/docs/basics.md b/docs/basics.md index 95a9479ad..f1636332f 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -84,6 +84,7 @@ Modifier rules only modify the request. They do not have any impact on routing d 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. ### Matchers diff --git a/middlewares/replace_path.go b/middlewares/replace_path.go new file mode 100644 index 000000000..208415a51 --- /dev/null +++ b/middlewares/replace_path.go @@ -0,0 +1,20 @@ +package middlewares + +import ( + "net/http" +) + +// ReplacePath is a middleware used to replace the path of a URL request +type ReplacePath struct { + Handler http.Handler + Path string +} + +// ReplacedPathHeader is the default header to set the old path to +const ReplacedPathHeader = "X-Replaced-Path" + +func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.Header.Add(ReplacedPathHeader, r.URL.Path) + r.URL.Path = s.Path + s.Handler.ServeHTTP(w, r) +} diff --git a/middlewares/replace_path_test.go b/middlewares/replace_path_test.go new file mode 100644 index 000000000..179ebdabb --- /dev/null +++ b/middlewares/replace_path_test.go @@ -0,0 +1,44 @@ +package middlewares_test + +import ( + "net/http" + "testing" + + "github.com/containous/traefik/middlewares" +) + +func TestReplacePath(t *testing.T) { + const replacementPath = "/replacement-path" + + paths := []string{ + "/example", + "/some/really/long/path", + } + + for _, path := range paths { + t.Run(path, func(t *testing.T) { + var newPath, oldPath string + handler := &middlewares.ReplacePath{ + Path: replacementPath, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + newPath = r.URL.Path + oldPath = r.Header.Get("X-Replaced-Path") + }), + } + + req, err := http.NewRequest("GET", "http://localhost"+path, nil) + if err != nil { + t.Error(err) + } + + handler.ServeHTTP(nil, req) + if newPath != replacementPath { + t.Fatalf("new path should be '%s'", replacementPath) + } + + if oldPath != path { + t.Fatalf("old path should be '%s'", path) + } + }) + } +} diff --git a/server/rules.go b/server/rules.go index 3ac0fa927..a0760bfc9 100644 --- a/server/rules.go +++ b/server/rules.go @@ -75,6 +75,13 @@ func (r *Rules) pathStrip(paths ...string) *mux.Route { return r.route.route } +func (r *Rules) replacePath(paths ...string) *mux.Route { + for _, path := range paths { + r.route.replacePath = path + } + return r.route.route +} + func (r *Rules) addPrefix(paths ...string) *mux.Route { for _, path := range paths { r.route.addPrefix = path @@ -116,6 +123,7 @@ func (r *Rules) parseRules(expression string, onRule func(functionName string, f "Headers": r.headers, "HeadersRegexp": r.headersRegexp, "AddPrefix": r.addPrefix, + "ReplacePath": r.replacePath, } if len(expression) == 0 { diff --git a/server/server.go b/server/server.go index 6045cd35f..9409acad2 100644 --- a/server/server.go +++ b/server/server.go @@ -65,6 +65,7 @@ type serverRoute struct { route *mux.Route stripPrefixes []string addPrefix string + replacePath string } // NewServer returns an initialized Server. @@ -806,6 +807,14 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http } } + // path replace + if len(serverRoute.replacePath) > 0 { + handler = &middlewares.ReplacePath{ + Path: serverRoute.replacePath, + Handler: handler, + } + } + serverRoute.route.Handler(handler) }