diff --git a/docs/basics.md b/docs/basics.md index f1636332f..02e0ce6eb 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -104,9 +104,11 @@ Following is the list of existing matcher rules along with examples: - `HostRegexp: traefik.io, {subdomain:[a-z]+}.traefik.io`: Match request host. It accepts a sequence of literal and regular expression hosts. - `Method: GET, POST, PUT`: Match request HTTP method. It accepts a sequence of HTTP methods. - `Path: /products/, /articles/{category}/{id:[0-9]+}`: Match exact request path. It accepts a sequence of literal and regular expression paths. -- `PathStrip: /products/, /articles/{category}/{id:[0-9]+}`: Match exact path and strip off the path prior to forwarding the request to the backend. It accepts a sequence of literal and regular expression paths. +- `PathStrip: /products/`: Match exact path and strip off the path prior to forwarding the request to the backend. It accepts a sequence of literal paths. +- `PathStripRegex: /articles/{category}/{id:[0-9]+}`: Match exact path and strip off the path prior to forwarding the request to the backend. It accepts a sequence of literal and regular expression paths. - `PathPrefix: /products/, /articles/{category}/{id:[0-9]+}`: Match request prefix path. It accepts a sequence of literal and regular expression prefix paths. -- `PathPrefixStrip: /products/, /articles/{category}/{id:[0-9]+}`: Match request prefix path and strip off the path prefix prior to forwarding the request to the backend. It accepts a sequence of literal and regular expression prefix paths. Starting with Traefik 1.3, the stripped prefix path will be available in the `X-Forwarded-Prefix` header. +- `PathPrefixStrip: /products/`: Match request prefix path and strip off the path prefix prior to forwarding the request to the backend. It accepts a sequence of literal prefix paths. Starting with Traefik 1.3, the stripped prefix path will be available in the `X-Forwarded-Prefix` header. +- `PathPrefixStripRegex: /articles/{category}/{id:[0-9]+}`: Match request prefix path and strip off the path prefix prior to forwarding the request to the backend. It accepts a sequence of literal and regular expression prefix paths. Starting with Traefik 1.3, the stripped prefix path will be available in the `X-Forwarded-Prefix` header. In order to use regular expressions with Host and Path matchers, you must declare an arbitrarily named variable followed by the colon-separated regular expression, all enclosed in curly braces. Any pattern supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used. Example: `/posts/{id:[0-9]+}`. diff --git a/middlewares/stripPrefixRegex.go b/middlewares/stripPrefixRegex.go new file mode 100644 index 000000000..f2a85834e --- /dev/null +++ b/middlewares/stripPrefixRegex.go @@ -0,0 +1,54 @@ +package middlewares + +import ( + "net/http" + + "github.com/containous/mux" + "github.com/containous/traefik/log" +) + +// StripPrefixRegex is a middleware used to strip prefix from an URL request +type StripPrefixRegex struct { + Handler http.Handler + router *mux.Router +} + +// NewStripPrefixRegex builds a new StripPrefixRegex given a handler and prefixes +func NewStripPrefixRegex(handler http.Handler, prefixes []string) *StripPrefixRegex { + stripPrefix := StripPrefixRegex{Handler: handler, router: mux.NewRouter()} + + for _, prefix := range prefixes { + stripPrefix.router.PathPrefix(prefix) + } + + return &stripPrefix +} + +func (s *StripPrefixRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var match mux.RouteMatch + if s.router.Match(r, &match) { + params := make([]string, 0, len(match.Vars)*2) + for key, val := range match.Vars { + params = append(params, key) + params = append(params, val) + } + + prefix, err := match.Route.URL(params...) + if err != nil || len(prefix.Path) > len(r.URL.Path) { + log.Error("Error in stripPrefix middleware", err) + return + } + + r.URL.Path = r.URL.Path[len(prefix.Path):] + r.Header[forwardedPrefixHeader] = []string{prefix.Path} + r.RequestURI = r.URL.RequestURI() + s.Handler.ServeHTTP(w, r) + return + } + http.NotFound(w, r) +} + +// SetHandler sets handler +func (s *StripPrefixRegex) SetHandler(Handler http.Handler) { + s.Handler = Handler +} diff --git a/middlewares/stripPrefixRegex_test.go b/middlewares/stripPrefixRegex_test.go new file mode 100644 index 000000000..cb1bc1eb1 --- /dev/null +++ b/middlewares/stripPrefixRegex_test.go @@ -0,0 +1,55 @@ +package middlewares + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func TestStripPrefixRegex(t *testing.T) { + + handlerPath := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, r.URL.Path) + }) + + handler := NewStripPrefixRegex(handlerPath, []string{"/a/api/", "/b/{regex}/", "/c/{category}/{id:[0-9]+}/"}) + server := httptest.NewServer(handler) + defer server.Close() + + tests := []struct { + expectedCode int + expectedResponse string + url string + }{ + {url: "/a/test", expectedCode: 404, expectedResponse: "404 page not found\n"}, + {url: "/a/api/test", expectedCode: 200, expectedResponse: "test"}, + + {url: "/b/api/", expectedCode: 200, expectedResponse: ""}, + {url: "/b/api/test1", expectedCode: 200, expectedResponse: "test1"}, + {url: "/b/api2/test2", expectedCode: 200, expectedResponse: "test2"}, + + {url: "/c/api/123/", expectedCode: 200, expectedResponse: ""}, + {url: "/c/api/123/test3", expectedCode: 200, expectedResponse: "test3"}, + {url: "/c/api/abc/test4", expectedCode: 404, expectedResponse: "404 page not found\n"}, + } + + for _, test := range tests { + resp, err := http.Get(server.URL + test.url) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != test.expectedCode { + t.Fatalf("Received non-%d response: %d\n", test.expectedCode, resp.StatusCode) + } + response, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if test.expectedResponse != string(response) { + t.Errorf("Expected '%s' : '%s'\n", test.expectedResponse, response) + } + } + +} diff --git a/server/rules.go b/server/rules.go index a0760bfc9..128c91ae7 100644 --- a/server/rules.go +++ b/server/rules.go @@ -75,6 +75,16 @@ func (r *Rules) pathStrip(paths ...string) *mux.Route { return r.route.route } +func (r *Rules) pathStripRegex(paths ...string) *mux.Route { + sort.Sort(bySize(paths)) + r.route.stripPrefixesRegex = paths + router := r.route.route.Subrouter() + for _, path := range paths { + router.Path(strings.TrimSpace(path)) + } + return r.route.route +} + func (r *Rules) replacePath(paths ...string) *mux.Route { for _, path := range paths { r.route.replacePath = path @@ -99,6 +109,16 @@ func (r *Rules) pathPrefixStrip(paths ...string) *mux.Route { return r.route.route } +func (r *Rules) pathPrefixStripRegex(paths ...string) *mux.Route { + sort.Sort(bySize(paths)) + r.route.stripPrefixesRegex = paths + router := r.route.route.Subrouter() + for _, path := range paths { + router.PathPrefix(strings.TrimSpace(path)) + } + return r.route.route +} + func (r *Rules) methods(methods ...string) *mux.Route { return r.route.route.Methods(methods...) } @@ -113,17 +133,19 @@ func (r *Rules) headersRegexp(headers ...string) *mux.Route { func (r *Rules) parseRules(expression string, onRule func(functionName string, function interface{}, arguments []string) error) error { functions := map[string]interface{}{ - "Host": r.host, - "HostRegexp": r.hostRegexp, - "Path": r.path, - "PathStrip": r.pathStrip, - "PathPrefix": r.pathPrefix, - "PathPrefixStrip": r.pathPrefixStrip, - "Method": r.methods, - "Headers": r.headers, - "HeadersRegexp": r.headersRegexp, - "AddPrefix": r.addPrefix, - "ReplacePath": r.replacePath, + "Host": r.host, + "HostRegexp": r.hostRegexp, + "Path": r.path, + "PathStrip": r.pathStrip, + "PathStripRegex": r.pathStripRegex, + "PathPrefix": r.pathPrefix, + "PathPrefixStrip": r.pathPrefixStrip, + "PathPrefixStripRegex": r.pathPrefixStripRegex, + "Method": r.methods, + "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 9409acad2..65ac45c45 100644 --- a/server/server.go +++ b/server/server.go @@ -62,10 +62,11 @@ type serverEntryPoint struct { } type serverRoute struct { - route *mux.Route - stripPrefixes []string - addPrefix string - replacePath string + route *mux.Route + stripPrefixes []string + stripPrefixesRegex []string + addPrefix string + replacePath string } // NewServer returns an initialized Server. @@ -807,6 +808,11 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http } } + // strip prefix with regex + if len(serverRoute.stripPrefixesRegex) > 0 { + handler = middlewares.NewStripPrefixRegex(handler, serverRoute.stripPrefixesRegex) + } + // path replace if len(serverRoute.replacePath) > 0 { handler = &middlewares.ReplacePath{