From 7afd2dbd20969acc7addc012478d5e6c5caebe55 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 14 Nov 2019 10:32:05 +0100 Subject: [PATCH] fix: stripPrefix middleware with empty resulting path. --- docs/content/middlewares/addprefix.md | 3 +- docs/content/middlewares/stripprefix.md | 82 +++++++ .../dynamic-configuration/docker-labels.yml | 55 ++--- .../reference/dynamic-configuration/file.toml | 27 +- .../reference/dynamic-configuration/file.yaml | 230 +++++++++--------- .../marathon-labels.json | 58 ++--- pkg/config/dynamic/middlewares.go | 8 +- pkg/config/label/label_test.go | 3 + pkg/middlewares/addprefix/add_prefix.go | 26 +- pkg/middlewares/addprefix/add_prefix_test.go | 8 +- pkg/middlewares/stripprefix/strip_prefix.go | 39 ++- .../stripprefix/strip_prefix_test.go | 61 ++++- .../stripprefixregex/strip_prefix_regex.go | 16 +- .../strip_prefix_regex_test.go | 55 ++++- 14 files changed, 426 insertions(+), 245 deletions(-) diff --git a/docs/content/middlewares/addprefix.md b/docs/content/middlewares/addprefix.md index c41cd1d40..61afb60b0 100644 --- a/docs/content/middlewares/addprefix.md +++ b/docs/content/middlewares/addprefix.md @@ -58,4 +58,5 @@ http: ### `prefix` -`prefix` is the string to add before the current path in the requested URL. It should include the leading slash (`/`). +`prefix` is the string to add before the current path in the requested URL. +It should include the leading slash (`/`). diff --git a/docs/content/middlewares/stripprefix.md b/docs/content/middlewares/stripprefix.md index f34ab014c..8a7f7f194 100644 --- a/docs/content/middlewares/stripprefix.md +++ b/docs/content/middlewares/stripprefix.md @@ -85,3 +85,85 @@ If your backend is serving assets (e.g., images or Javascript files), chances ar Continuing on the example, the backend should return `/products/shoes/image.png` (and not `/images.png` which Traefik would likely not be able to associate with the same backend). The `X-Forwarded-Prefix` header can be queried to build such URLs dynamically. + +### `forceSlash` + +_Optional, Default=true_ + +```yaml tab="Docker" +labels: + - "traefik.http.middlewares.example.stripprefix.prefixes=/foobar" + - "traefik.http.middlewares.example.stripprefix.forceslash=false" +``` + +```yaml tab="Kubernetes" +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: example +spec: + stripPrefix: + prefixes: + - "/foobar" + forceSlash: false +``` + +```json tab="Marathon" +"labels": { + "traefik.http.middlewares.example.stripprefix.prefixes": "/foobar", + "traefik.http.middlewares.example.stripprefix.forceslash": "false" +} +``` + +```yaml tab="Rancher" +labels: + - "traefik.http.middlewares.example.stripprefix.prefixes=/foobar" + - "traefik.http.middlewares.example.stripprefix.forceSlash=false" +``` + +```toml tab="File (TOML)" +[http.middlewares] + [http.middlewares.example.stripPrefix] + prefixes = ["/foobar"] + forceSlash = false +``` + +```yaml tab="File (YAML)" +http: + middlewares: + example: + stripPrefix: + prefixes: + - "/foobar" + forceSlash: false +``` + +The `forceSlash` option makes sure that the resulting stripped path is not the empty string, by replacing it with `/` when necessary. + +This option was added to keep the initial (non-intuitive) behavior of this middleware, in order to avoid introducing a breaking change. + +It's recommended to explicitly set `forceSlash` to `false`. + +??? info "Behavior examples" + + - `forceSlash=true` + + | Path | Prefix to strip | Result | + |------------|-----------------|--------| + | `/` | `/` | `/` | + | `/foo` | `/foo` | `/` | + | `/foo/` | `/foo` | `/` | + | `/foo/` | `/foo/` | `/` | + | `/bar` | `/foo` | `/bar` | + | `/foo/bar` | `/foo` | `/bar` | + + - `forceSlash=false` + + | Path | Prefix to strip | Result | + |------------|-----------------|--------| + | `/` | `/` | empty | + | `/foo` | `/foo` | empty | + | `/foo/` | `/foo` | `/` | + | `/foo/` | `/foo/` | empty | + | `/bar` | `/foo` | `/bar` | + | `/foo/bar` | `/foo` | `/bar` | diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 722a4fdf4..19ce7cef5 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -103,6 +103,7 @@ - "traefik.http.middlewares.middleware17.replacepathregex.regex=foobar" - "traefik.http.middlewares.middleware17.replacepathregex.replacement=foobar" - "traefik.http.middlewares.middleware18.retry.attempts=42" +- "traefik.http.middlewares.middleware19.stripprefix.forceslash=true" - "traefik.http.middlewares.middleware19.stripprefix.prefixes=foobar, foobar" - "traefik.http.middlewares.middleware20.stripprefixregex.regex=foobar, foobar" - "traefik.http.routers.router0.entrypoints=foobar, foobar" @@ -129,38 +130,22 @@ - "traefik.http.routers.router1.tls.domains[1].main=foobar" - "traefik.http.routers.router1.tls.domains[1].sans=foobar, foobar" - "traefik.http.routers.router1.tls.options=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.headers.name0=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.headers.name1=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.hostname=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.interval=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.path=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.port=42" -- "traefik.http.services.service0.loadbalancer.healthcheck.scheme=foobar" -- "traefik.http.services.service0.loadbalancer.healthcheck.timeout=foobar" -- "traefik.http.services.service0.loadbalancer.passhostheader=true" -- "traefik.http.services.service0.loadbalancer.responseforwarding.flushinterval=foobar" -- "traefik.http.services.service0.loadbalancer.sticky=true" -- "traefik.http.services.service0.loadbalancer.sticky.cookie.httponly=true" -- "traefik.http.services.service0.loadbalancer.sticky.cookie.name=foobar" -- "traefik.http.services.service0.loadbalancer.sticky.cookie.secure=true" -- "traefik.http.services.service0.loadbalancer.server.port=foobar" -- "traefik.http.services.service0.loadbalancer.server.scheme=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.headers.name0=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.headers.name1=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.hostname=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.interval=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.path=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.port=42" -- "traefik.http.services.service1.loadbalancer.healthcheck.scheme=foobar" -- "traefik.http.services.service1.loadbalancer.healthcheck.timeout=foobar" -- "traefik.http.services.service1.loadbalancer.passhostheader=true" -- "traefik.http.services.service1.loadbalancer.responseforwarding.flushinterval=foobar" -- "traefik.http.services.service1.loadbalancer.sticky=true" -- "traefik.http.services.service1.loadbalancer.sticky.cookie.httponly=true" -- "traefik.http.services.service1.loadbalancer.sticky.cookie.name=foobar" -- "traefik.http.services.service1.loadbalancer.sticky.cookie.secure=true" -- "traefik.http.services.service1.loadbalancer.server.port=foobar" -- "traefik.http.services.service1.loadbalancer.server.scheme=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.headers.name0=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.headers.name1=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.hostname=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.interval=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.path=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.port=42" +- "traefik.http.services.service01.loadbalancer.healthcheck.scheme=foobar" +- "traefik.http.services.service01.loadbalancer.healthcheck.timeout=foobar" +- "traefik.http.services.service01.loadbalancer.passhostheader=true" +- "traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval=foobar" +- "traefik.http.services.service01.loadbalancer.sticky=true" +- "traefik.http.services.service01.loadbalancer.sticky.cookie.httponly=true" +- "traefik.http.services.service01.loadbalancer.sticky.cookie.name=foobar" +- "traefik.http.services.service01.loadbalancer.sticky.cookie.secure=true" +- "traefik.http.services.service01.loadbalancer.server.port=foobar" +- "traefik.http.services.service01.loadbalancer.server.scheme=foobar" - "traefik.tcp.routers.tcprouter0.entrypoints=foobar, foobar" - "traefik.tcp.routers.tcprouter0.rule=foobar" - "traefik.tcp.routers.tcprouter0.service=foobar" @@ -183,7 +168,5 @@ - "traefik.tcp.routers.tcprouter1.tls.domains[1].sans=foobar, foobar" - "traefik.tcp.routers.tcprouter1.tls.options=foobar" - "traefik.tcp.routers.tcprouter1.tls.passthrough=true" -- "traefik.tcp.services.tcpservice0.loadbalancer.server.port=foobar" -- "traefik.tcp.services.tcpservice0.loadbalancer.terminationdelay=100" -- "traefik.tcp.services.tcpservice1.loadbalancer.server.port=foobar" -- "traefik.tcp.services.tcpservice1.loadbalancer.terminationdelay=100" +- "traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay=42" +- "traefik.tcp.services.tcpservice01.loadbalancer.server.port=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 8027a082b..326212b6b 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -245,6 +245,7 @@ [http.middlewares.Middleware19] [http.middlewares.Middleware19.stripPrefix] prefixes = ["foobar", "foobar"] + forceSlash = true [http.middlewares.Middleware20] [http.middlewares.Middleware20.stripPrefixRegex] regex = ["foobar", "foobar"] @@ -284,25 +285,25 @@ main = "foobar" sans = ["foobar", "foobar"] [tcp.services] - [tcp.services.TCPService0] - [tcp.services.TCPService0.loadBalancer] - terminationDelay = 100 + [tcp.services.TCPService01] + [tcp.services.TCPService01.loadBalancer] + terminationDelay = 42 - [[tcp.services.TCPService0.loadBalancer.servers]] + [[tcp.services.TCPService01.loadBalancer.servers]] address = "foobar" - [[tcp.services.TCPService0.loadBalancer.servers]] + [[tcp.services.TCPService01.loadBalancer.servers]] address = "foobar" + [tcp.services.TCPService02] + [tcp.services.TCPService02.weighted] - [tcp.services.TCPService1] - [tcp.services.TCPService1.loadBalancer] - terminationDelay = 100 + [[tcp.services.TCPService02.weighted.services]] + name = "foobar" + weight = 42 - [[tcp.services.TCPService1.loadBalancer.servers]] - address = "foobar" - - [[tcp.services.TCPService1.loadBalancer.servers]] - address = "foobar" + [[tcp.services.TCPService02.weighted.services]] + name = "foobar" + weight = 42 [tls] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index e8ab878b7..fe5d8c252 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -2,11 +2,11 @@ http: routers: Router0: entryPoints: - - foobar - - foobar + - foobar + - foobar middlewares: - - foobar - - foobar + - foobar + - foobar service: foobar rule: foobar priority: 42 @@ -14,21 +14,21 @@ http: options: foobar certResolver: foobar domains: - - main: foobar - sans: - - foobar - - foobar - - main: foobar - sans: - - foobar - - foobar + - main: foobar + sans: + - foobar + - foobar + - main: foobar + sans: + - foobar + - foobar Router1: entryPoints: - - foobar - - foobar + - foobar + - foobar middlewares: - - foobar - - foobar + - foobar + - foobar service: foobar rule: foobar priority: 42 @@ -36,14 +36,14 @@ http: options: foobar certResolver: foobar domains: - - main: foobar - sans: - - foobar - - foobar - - main: foobar - sans: - - foobar - - foobar + - main: foobar + sans: + - foobar + - foobar + - main: foobar + sans: + - foobar + - foobar services: Service01: loadBalancer: @@ -53,8 +53,8 @@ http: secure: true httpOnly: true servers: - - url: foobar - - url: foobar + - url: foobar + - url: foobar healthCheck: scheme: foobar path: foobar @@ -72,17 +72,17 @@ http: mirroring: service: foobar mirrors: - - name: foobar - percent: 42 - - name: foobar - percent: 42 + - name: foobar + percent: 42 + - name: foobar + percent: 42 Service03: weighted: services: - - name: foobar - weight: 42 - - name: foobar - weight: 42 + - name: foobar + weight: 42 + - name: foobar + weight: 42 sticky: cookie: name: foobar @@ -95,8 +95,8 @@ http: Middleware01: basicAuth: users: - - foobar - - foobar + - foobar + - foobar usersFile: foobar realm: foobar removeHeader: true @@ -111,8 +111,8 @@ http: Middleware03: chain: middlewares: - - foobar - - foobar + - foobar + - foobar Middleware04: circuitBreaker: expression: foobar @@ -121,8 +121,8 @@ http: Middleware06: digestAuth: users: - - foobar - - foobar + - foobar + - foobar usersFile: foobar removeHeader: true realm: foobar @@ -130,8 +130,8 @@ http: Middleware07: errors: status: - - foobar - - foobar + - foobar + - foobar service: foobar query: foobar Middleware08: @@ -145,8 +145,8 @@ http: insecureSkipVerify: true trustForwardHeader: true authResponseHeaders: - - foobar - - foobar + - foobar + - foobar Middleware09: headers: customRequestHeaders: @@ -157,23 +157,23 @@ http: name1: foobar accessControlAllowCredentials: true accessControlAllowHeaders: - - foobar - - foobar + - foobar + - foobar accessControlAllowMethods: - - foobar - - foobar + - foobar + - foobar accessControlAllowOrigin: foobar accessControlExposeHeaders: - - foobar - - foobar + - foobar + - foobar accessControlMaxAge: 42 addVaryHeader: true allowedHosts: - - foobar - - foobar + - foobar + - foobar hostsProxyHeaders: - - foobar - - foobar + - foobar + - foobar sslRedirect: true sslTemporaryRedirect: true sslHost: foobar @@ -198,13 +198,13 @@ http: Middleware10: ipWhiteList: sourceRange: - - foobar - - foobar + - foobar + - foobar ipStrategy: depth: 42 excludedIPs: - - foobar - - foobar + - foobar + - foobar Middleware11: inFlightReq: amount: 42 @@ -212,8 +212,8 @@ http: ipstrategy: depth: 42 excludedIPs: - - foobar - - foobar + - foobar + - foobar requestHeaderName: foobar requestHost: true Middleware12: @@ -247,8 +247,8 @@ http: ipstrategy: depth: 42 excludedIPs: - - foobar - - foobar + - foobar + - foobar requestHeaderName: foobar requestHost: true Middleware14: @@ -274,19 +274,20 @@ http: Middleware19: stripPrefix: prefixes: - - foobar - - foobar + - foobar + - foobar + forceSlash: true Middleware20: stripPrefixRegex: regex: - - foobar - - foobar + - foobar + - foobar tcp: routers: TCPRouter0: entryPoints: - - foobar - - foobar + - foobar + - foobar service: foobar rule: foobar tls: @@ -294,18 +295,18 @@ tcp: options: foobar certResolver: foobar domains: - - main: foobar - sans: - - foobar - - foobar - - main: foobar - sans: - - foobar - - foobar + - main: foobar + sans: + - foobar + - foobar + - main: foobar + sans: + - foobar + - foobar TCPRouter1: entryPoints: - - foobar - - foobar + - foobar + - foobar service: foobar rule: foobar tls: @@ -313,60 +314,61 @@ tcp: options: foobar certResolver: foobar domains: - - main: foobar - sans: - - foobar - - foobar - - main: foobar - sans: - - foobar - - foobar + - main: foobar + sans: + - foobar + - foobar + - main: foobar + sans: + - foobar + - foobar services: - TCPService0: + TCPService01: loadBalancer: - terminationDelay: 100 + terminationDelay: 42 servers: - - address: foobar - - address: foobar - TCPService1: - loadBalancer: - terminationDelay: 100 - servers: - - address: foobar - - address: foobar + - address: foobar + - address: foobar + TCPService02: + weighted: + services: + - name: foobar + weight: 42 + - name: foobar + weight: 42 tls: certificates: - - certFile: foobar - keyFile: foobar - stores: - - foobar - - foobar - - certFile: foobar - keyFile: foobar - stores: - - foobar - - foobar + - certFile: foobar + keyFile: foobar + stores: + - foobar + - foobar + - certFile: foobar + keyFile: foobar + stores: + - foobar + - foobar options: Options0: minVersion: foobar cipherSuites: - - foobar - - foobar + - foobar + - foobar clientAuth: caFiles: - - foobar - - foobar + - foobar + - foobar clientAuthType: foobar sniStrict: true Options1: minVersion: foobar cipherSuites: - - foobar - - foobar + - foobar + - foobar clientAuth: caFiles: - - foobar - - foobar + - foobar + - foobar clientAuthType: foobar sniStrict: true stores: diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 695e8c6bd..4294c82e0 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -103,6 +103,7 @@ "traefik.http.middlewares.middleware17.replacepathregex.regex": "foobar", "traefik.http.middlewares.middleware17.replacepathregex.replacement": "foobar", "traefik.http.middlewares.middleware18.retry.attempts": "42", +"traefik.http.middlewares.middleware19.stripprefix.forceslash": "true", "traefik.http.middlewares.middleware19.stripprefix.prefixes": "foobar, foobar", "traefik.http.middlewares.middleware20.stripprefixregex.regex": "foobar, foobar", "traefik.http.routers.router0.entrypoints": "foobar, foobar", @@ -110,7 +111,6 @@ "traefik.http.routers.router0.priority": "42", "traefik.http.routers.router0.rule": "foobar", "traefik.http.routers.router0.service": "foobar", -"traefik.http.routers.router0.tls": "true", "traefik.http.routers.router0.tls.certresolver": "foobar", "traefik.http.routers.router0.tls.domains[0].main": "foobar", "traefik.http.routers.router0.tls.domains[0].sans": "foobar, foobar", @@ -122,49 +122,30 @@ "traefik.http.routers.router1.priority": "42", "traefik.http.routers.router1.rule": "foobar", "traefik.http.routers.router1.service": "foobar", -"traefik.http.routers.router1.tls": "true", "traefik.http.routers.router1.tls.certresolver": "foobar", "traefik.http.routers.router1.tls.domains[0].main": "foobar", "traefik.http.routers.router1.tls.domains[0].sans": "foobar, foobar", "traefik.http.routers.router1.tls.domains[1].main": "foobar", "traefik.http.routers.router1.tls.domains[1].sans": "foobar, foobar", "traefik.http.routers.router1.tls.options": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.headers.name0": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.headers.name1": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.hostname": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.interval": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.path": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.port": "42", -"traefik.http.services.service0.loadbalancer.healthcheck.scheme": "foobar", -"traefik.http.services.service0.loadbalancer.healthcheck.timeout": "foobar", -"traefik.http.services.service0.loadbalancer.passhostheader": "true", -"traefik.http.services.service0.loadbalancer.responseforwarding.flushinterval": "foobar", -"traefik.http.services.service0.loadbalancer.sticky": "true", -"traefik.http.services.service0.loadbalancer.sticky.cookie.httponly": "true", -"traefik.http.services.service0.loadbalancer.sticky.cookie.name": "foobar", -"traefik.http.services.service0.loadbalancer.sticky.cookie.secure": "true", -"traefik.http.services.service0.loadbalancer.server.port": "foobar", -"traefik.http.services.service0.loadbalancer.server.scheme": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.headers.name0": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.headers.name1": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.hostname": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.interval": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.path": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.port": "42", -"traefik.http.services.service1.loadbalancer.healthcheck.scheme": "foobar", -"traefik.http.services.service1.loadbalancer.healthcheck.timeout": "foobar", -"traefik.http.services.service1.loadbalancer.passhostheader": "true", -"traefik.http.services.service1.loadbalancer.responseforwarding.flushinterval": "foobar", -"traefik.http.services.service1.loadbalancer.sticky": "true", -"traefik.http.services.service1.loadbalancer.sticky.cookie.httponly": "true", -"traefik.http.services.service1.loadbalancer.sticky.cookie.name": "foobar", -"traefik.http.services.service1.loadbalancer.sticky.cookie.secure": "true", -"traefik.http.services.service1.loadbalancer.server.port": "foobar", -"traefik.http.services.service1.loadbalancer.server.scheme": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.headers.name0": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.headers.name1": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.hostname": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.interval": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.path": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.port": "42", +"traefik.http.services.service01.loadbalancer.healthcheck.scheme": "foobar", +"traefik.http.services.service01.loadbalancer.healthcheck.timeout": "foobar", +"traefik.http.services.service01.loadbalancer.passhostheader": "true", +"traefik.http.services.service01.loadbalancer.responseforwarding.flushinterval": "foobar", +"traefik.http.services.service01.loadbalancer.sticky.cookie.httponly": "true", +"traefik.http.services.service01.loadbalancer.sticky.cookie.name": "foobar", +"traefik.http.services.service01.loadbalancer.sticky.cookie.secure": "true", +"traefik.http.services.service01.loadbalancer.server.port": "foobar", +"traefik.http.services.service01.loadbalancer.server.scheme": "foobar", "traefik.tcp.routers.tcprouter0.entrypoints": "foobar, foobar", "traefik.tcp.routers.tcprouter0.rule": "foobar", "traefik.tcp.routers.tcprouter0.service": "foobar", -"traefik.tcp.routers.tcprouter0.tls": "true", "traefik.tcp.routers.tcprouter0.tls.certresolver": "foobar", "traefik.tcp.routers.tcprouter0.tls.domains[0].main": "foobar", "traefik.tcp.routers.tcprouter0.tls.domains[0].sans": "foobar, foobar", @@ -175,7 +156,6 @@ "traefik.tcp.routers.tcprouter1.entrypoints": "foobar, foobar", "traefik.tcp.routers.tcprouter1.rule": "foobar", "traefik.tcp.routers.tcprouter1.service": "foobar", -"traefik.tcp.routers.tcprouter1.tls": "true", "traefik.tcp.routers.tcprouter1.tls.certresolver": "foobar", "traefik.tcp.routers.tcprouter1.tls.domains[0].main": "foobar", "traefik.tcp.routers.tcprouter1.tls.domains[0].sans": "foobar, foobar", @@ -183,7 +163,5 @@ "traefik.tcp.routers.tcprouter1.tls.domains[1].sans": "foobar, foobar", "traefik.tcp.routers.tcprouter1.tls.options": "foobar", "traefik.tcp.routers.tcprouter1.tls.passthrough": "true", -"traefik.tcp.services.tcpservice0.loadbalancer.server.port": "foobar", -"traefik.tcp.services.tcpservice0.loadbalancer.terminationDelay": "100", -"traefik.tcp.services.tcpservice1.loadbalancer.server.port": "foobar" -"traefik.tcp.services.tcpservice1.loadbalancer.terminationDelay": "100", +"traefik.tcp.services.tcpservice01.loadbalancer.terminationdelay": "42", +"traefik.tcp.services.tcpservice01.loadbalancer.server.port": "foobar", diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index a627ce621..aa63da133 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -355,7 +355,13 @@ type Retry struct { // StripPrefix holds the StripPrefix configuration. type StripPrefix struct { - Prefixes []string `json:"prefixes,omitempty" toml:"prefixes,omitempty" yaml:"prefixes,omitempty"` + Prefixes []string `json:"prefixes,omitempty" toml:"prefixes,omitempty" yaml:"prefixes,omitempty"` + ForceSlash bool `json:"forceSlash,omitempty" toml:"forceSlash,omitempty" yaml:"forceSlash,omitempty"` // Deprecated +} + +// SetDefaults Default values for a StripPrefix. +func (s *StripPrefix) SetDefaults() { + s.ForceSlash = true } // +k8s:deepcopy-gen=true diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 63ac2f48f..fbfece11e 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -368,6 +368,7 @@ func TestDecodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + ForceSlash: true, }, }, "Middleware18": { @@ -771,6 +772,7 @@ func TestEncodeConfiguration(t *testing.T) { "foobar", "fiibar", }, + ForceSlash: true, }, }, "Middleware18": { @@ -1091,6 +1093,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware15.ReplacePathRegex.Replacement": "foobar", "traefik.HTTP.Middlewares.Middleware16.Retry.Attempts": "42", "traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true", "traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware19.Compress": "true", diff --git a/pkg/middlewares/addprefix/add_prefix.go b/pkg/middlewares/addprefix/add_prefix.go index 655e874a8..934b7e0ae 100644 --- a/pkg/middlewares/addprefix/add_prefix.go +++ b/pkg/middlewares/addprefix/add_prefix.go @@ -41,23 +41,35 @@ func New(ctx context.Context, next http.Handler, config dynamic.AddPrefix, name return result, nil } -func (ap *addPrefix) GetTracingInformation() (string, ext.SpanKindEnum) { - return ap.name, tracing.SpanKindNoneEnum +func (a *addPrefix) GetTracingInformation() (string, ext.SpanKindEnum) { + return a.name, tracing.SpanKindNoneEnum } -func (ap *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), ap.name, typeName)) +func (a *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), a.name, typeName)) oldURLPath := req.URL.Path - req.URL.Path = ap.prefix + req.URL.Path + req.URL.Path = ensureLeadingSlash(a.prefix + req.URL.Path) logger.Debugf("URL.Path is now %s (was %s).", req.URL.Path, oldURLPath) if req.URL.RawPath != "" { oldURLRawPath := req.URL.RawPath - req.URL.RawPath = ap.prefix + req.URL.RawPath + req.URL.RawPath = ensureLeadingSlash(a.prefix + req.URL.RawPath) logger.Debugf("URL.RawPath is now %s (was %s).", req.URL.RawPath, oldURLRawPath) } req.RequestURI = req.URL.RequestURI() - ap.next.ServeHTTP(rw, req) + a.next.ServeHTTP(rw, req) +} + +func ensureLeadingSlash(str string) string { + if str == "" { + return str + } + + if str[0] == '/' { + return str + } + + return "/" + str } diff --git a/pkg/middlewares/addprefix/add_prefix_test.go b/pkg/middlewares/addprefix/add_prefix_test.go index 1d130a083..cca4bd19e 100644 --- a/pkg/middlewares/addprefix/add_prefix_test.go +++ b/pkg/middlewares/addprefix/add_prefix_test.go @@ -7,7 +7,6 @@ import ( "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/testhelpers" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -47,7 +46,6 @@ func TestNewAddPrefix(t *testing.T) { } func TestAddPrefix(t *testing.T) { - logrus.SetLevel(logrus.DebugLevel) testCases := []struct { desc string prefix dynamic.AddPrefix @@ -61,6 +59,12 @@ func TestAddPrefix(t *testing.T) { path: "/b", expectedPath: "/a/b", }, + { + desc: "Works with missing leading slash", + prefix: dynamic.AddPrefix{Prefix: "a"}, + path: "/", + expectedPath: "/a/", + }, { desc: "Works with a raw path", prefix: dynamic.AddPrefix{Prefix: "/a"}, diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go index b8008689a..1a0fd8bbc 100644 --- a/pkg/middlewares/stripprefix/strip_prefix.go +++ b/pkg/middlewares/stripprefix/strip_prefix.go @@ -20,18 +20,20 @@ const ( // stripPrefix is a middleware used to strip prefix from an URL request. type stripPrefix struct { - next http.Handler - prefixes []string - name string + next http.Handler + prefixes []string + forceSlash bool // TODO Must be removed (breaking), the default behavior must be forceSlash=false + name string } // New creates a new strip prefix middleware. func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, name string) (http.Handler, error) { log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)).Debug("Creating middleware") return &stripPrefix{ - prefixes: config.Prefixes, - next: next, - name: name, + prefixes: config.Prefixes, + forceSlash: config.ForceSlash, + next: next, + name: name, }, nil } @@ -42,9 +44,9 @@ func (s *stripPrefix) GetTracingInformation() (string, ext.SpanKindEnum) { func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) { for _, prefix := range s.prefixes { if strings.HasPrefix(req.URL.Path, prefix) { - req.URL.Path = getPrefixStripped(req.URL.Path, prefix) + req.URL.Path = s.getPrefixStripped(req.URL.Path, prefix) if req.URL.RawPath != "" { - req.URL.RawPath = getPrefixStripped(req.URL.RawPath, prefix) + req.URL.RawPath = s.getPrefixStripped(req.URL.RawPath, prefix) } s.serveRequest(rw, req, strings.TrimSpace(prefix)) return @@ -59,10 +61,25 @@ func (s *stripPrefix) serveRequest(rw http.ResponseWriter, req *http.Request, pr s.next.ServeHTTP(rw, req) } -func getPrefixStripped(s, prefix string) string { - return ensureLeadingSlash(strings.TrimPrefix(s, prefix)) +func (s *stripPrefix) getPrefixStripped(urlPath, prefix string) string { + if s.forceSlash { + // Only for compatibility reason with the previous behavior, + // but the previous behavior is wrong. + // This needs to be removed in the next breaking version. + return "/" + strings.TrimPrefix(strings.TrimPrefix(urlPath, prefix), "/") + } + + return ensureLeadingSlash(strings.TrimPrefix(urlPath, prefix)) } func ensureLeadingSlash(str string) string { - return "/" + strings.TrimPrefix(str, "/") + if str == "" { + return str + } + + if str[0] == '/' { + return str + } + + return "/" + str } diff --git a/pkg/middlewares/stripprefix/strip_prefix_test.go b/pkg/middlewares/stripprefix/strip_prefix_test.go index 449720902..13a5aedb2 100644 --- a/pkg/middlewares/stripprefix/strip_prefix_test.go +++ b/pkg/middlewares/stripprefix/strip_prefix_test.go @@ -31,6 +31,17 @@ func TestStripPrefix(t *testing.T) { expectedStatusCode: http.StatusOK, expectedPath: "/noprefixes", }, + { + desc: "wildcard (.*) requests (ForceSlash)", + config: dynamic.StripPrefix{ + Prefixes: []string{"/"}, + ForceSlash: true, + }, + path: "/", + expectedStatusCode: http.StatusOK, + expectedPath: "/", + expectedHeader: "/", + }, { desc: "wildcard (.*) requests", config: dynamic.StripPrefix{ @@ -38,9 +49,20 @@ func TestStripPrefix(t *testing.T) { }, path: "/", expectedStatusCode: http.StatusOK, - expectedPath: "/", + expectedPath: "", expectedHeader: "/", }, + { + desc: "prefix and path matching (ForceSlash)", + config: dynamic.StripPrefix{ + Prefixes: []string{"/stat"}, + ForceSlash: true, + }, + path: "/stat", + expectedStatusCode: http.StatusOK, + expectedPath: "/", + expectedHeader: "/stat", + }, { desc: "prefix and path matching", config: dynamic.StripPrefix{ @@ -48,9 +70,20 @@ func TestStripPrefix(t *testing.T) { }, path: "/stat", expectedStatusCode: http.StatusOK, - expectedPath: "/", + expectedPath: "", expectedHeader: "/stat", }, + { + desc: "path prefix on exactly matching path (ForceSlash)", + config: dynamic.StripPrefix{ + Prefixes: []string{"/stat/"}, + ForceSlash: true, + }, + path: "/stat/", + expectedStatusCode: http.StatusOK, + expectedPath: "/", + expectedHeader: "/stat/", + }, { desc: "path prefix on exactly matching path", config: dynamic.StripPrefix{ @@ -58,7 +91,7 @@ func TestStripPrefix(t *testing.T) { }, path: "/stat/", expectedStatusCode: http.StatusOK, - expectedPath: "/", + expectedPath: "", expectedHeader: "/stat/", }, { @@ -101,6 +134,17 @@ func TestStripPrefix(t *testing.T) { expectedPath: "/us", expectedHeader: "/stat", }, + { + desc: "later prefix matching (ForceSlash)", + config: dynamic.StripPrefix{ + Prefixes: []string{"/mismatch", "/stat"}, + ForceSlash: true, + }, + path: "/stat", + expectedStatusCode: http.StatusOK, + expectedPath: "/", + expectedHeader: "/stat", + }, { desc: "later prefix matching", config: dynamic.StripPrefix{ @@ -108,7 +152,7 @@ func TestStripPrefix(t *testing.T) { }, path: "/stat", expectedStatusCode: http.StatusOK, - expectedPath: "/", + expectedPath: "", expectedHeader: "/stat", }, { @@ -162,12 +206,15 @@ func TestStripPrefix(t *testing.T) { assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.") assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ForwardedPrefixHeader) - expectedURI := test.expectedPath + expectedRequestURI := test.expectedPath if test.expectedRawPath != "" { // go HTTP uses the raw path when existent in the RequestURI - expectedURI = test.expectedRawPath + expectedRequestURI = test.expectedRawPath } - assert.Equal(t, expectedURI, requestURI, "Unexpected request URI.") + if test.expectedPath == "" { + expectedRequestURI = "/" + } + assert.Equal(t, expectedRequestURI, requestURI, "Unexpected request URI.") }) } } diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go index 4024ac4f0..4feda225e 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go @@ -60,12 +60,12 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) req.Header.Add(stripprefix.ForwardedPrefixHeader, prefix) - req.URL.Path = strings.Replace(req.URL.Path, prefix, "", 1) + req.URL.Path = ensureLeadingSlash(strings.Replace(req.URL.Path, prefix, "", 1)) if req.URL.RawPath != "" { - req.URL.RawPath = req.URL.RawPath[len(prefix):] + req.URL.RawPath = ensureLeadingSlash(req.URL.RawPath[len(prefix):]) } - req.RequestURI = ensureLeadingSlash(req.URL.RequestURI()) + req.RequestURI = req.URL.RequestURI() s.next.ServeHTTP(rw, req) return } @@ -75,5 +75,13 @@ func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) } func ensureLeadingSlash(str string) string { - return "/" + strings.TrimPrefix(str, "/") + if str == "" { + return str + } + + if str[0] == '/' { + return str + } + + return "/" + str } diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go index 297fa7192..6e0293083 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex_test.go @@ -31,42 +31,66 @@ func TestStripPrefixRegex(t *testing.T) { expectedPath: "/a/test", }, { - path: "/a/test", + path: "/a/test/", expectedStatusCode: http.StatusOK, - expectedPath: "/a/test", + expectedPath: "/a/test/", + }, + { + path: "/a/api/", + expectedStatusCode: http.StatusOK, + expectedPath: "", + expectedHeader: "/a/api/", }, { path: "/a/api/test", expectedStatusCode: http.StatusOK, - expectedPath: "test", + expectedPath: "/test", + expectedHeader: "/a/api/", + }, + { + path: "/a/api/test/", + expectedStatusCode: http.StatusOK, + expectedPath: "/test/", expectedHeader: "/a/api/", }, { path: "/b/api/", expectedStatusCode: http.StatusOK, + expectedPath: "", expectedHeader: "/b/api/", }, + { + path: "/b/api", + expectedStatusCode: http.StatusOK, + expectedPath: "/b/api", + }, { path: "/b/api/test1", expectedStatusCode: http.StatusOK, - expectedPath: "test1", + expectedPath: "/test1", expectedHeader: "/b/api/", }, { path: "/b/api2/test2", expectedStatusCode: http.StatusOK, - expectedPath: "test2", + expectedPath: "/test2", expectedHeader: "/b/api2/", }, { path: "/c/api/123/", expectedStatusCode: http.StatusOK, + expectedPath: "", expectedHeader: "/c/api/123/", }, + { + path: "/c/api/123", + expectedStatusCode: http.StatusOK, + expectedPath: "/c/api/123", + }, { path: "/c/api/123/test3", expectedStatusCode: http.StatusOK, - expectedPath: "test3", + expectedPath: "/test3", expectedHeader: "/c/api/123/", }, { @@ -77,8 +101,8 @@ func TestStripPrefixRegex(t *testing.T) { { path: "/a/api/a%2Fb", expectedStatusCode: http.StatusOK, - expectedPath: "a/b", - expectedRawPath: "a%2Fb", + expectedPath: "/a/b", + expectedRawPath: "/a%2Fb", expectedHeader: "/a/api/", }, } @@ -88,11 +112,12 @@ func TestStripPrefixRegex(t *testing.T) { t.Run(test.path, func(t *testing.T) { t.Parallel() - var actualPath, actualRawPath, actualHeader string + var actualPath, actualRawPath, actualHeader, requestURI string handlerPath := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { actualPath = r.URL.Path actualRawPath = r.URL.RawPath actualHeader = r.Header.Get(stripprefix.ForwardedPrefixHeader) + requestURI = r.RequestURI }) handler, err := New(context.Background(), handlerPath, testPrefixRegex, "foo-strip-prefix-regex") require.NoError(t, err) @@ -106,6 +131,18 @@ func TestStripPrefixRegex(t *testing.T) { assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.") assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.") assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", stripprefix.ForwardedPrefixHeader) + + if test.expectedPath != test.path { + expectedRequestURI := test.expectedPath + if test.expectedRawPath != "" { + // go HTTP uses the raw path when existent in the RequestURI + expectedRequestURI = test.expectedRawPath + } + if test.expectedPath == "" { + expectedRequestURI = "/" + } + assert.Equal(t, expectedRequestURI, requestURI, "Unexpected request URI.") + } }) } }