Update routing syntax
Co-authored-by: Tom Moulard <tom.moulard@traefik.io>
This commit is contained in:
parent
b93141992e
commit
4d86668af3
27 changed files with 2484 additions and 2085 deletions
|
@ -30,3 +30,18 @@ In v3, the reported status code for gRPC requests is now the value of the `Grpc-
|
|||
- `sslRedirect`, `sslTemporaryRedirect`, `sslHost`, `sslForceHost` and `featurePolicy` options of the Headers middleware have been removed.
|
||||
- The `forceSlash` option of the StripPrefix middleware has been removed.
|
||||
- the `preferServerCipherSuites` option has been removed.
|
||||
|
||||
## Matchers
|
||||
|
||||
In v3, the `Headers` and `HeadersRegexp` matchers have been renamed to `Header` and `HeaderRegexp` respectively.
|
||||
|
||||
`QueryRegexp` has been introduced to match query values using a regular expression.
|
||||
|
||||
`HeaderRegexp`, `HostRegexp`, `PathRegexp`, `QueryRegexp`, and `HostSNIRegexp` matchers now uses the [Go regexp syntax](https://golang.org/pkg/regexp/syntax/).
|
||||
|
||||
All matchers now take a single value (except `Headers`, `HeaderRegexp`, `Query`, and `QueryRegexp` which take two)
|
||||
and should be explicitly combined using logical operators to mimic previous behavior.
|
||||
|
||||
`Query` can take a single value to match is the query value that has no value (e.g. `/search?mobile`).
|
||||
|
||||
`HostHeader` has been removed, use `Host` instead.
|
||||
|
|
|
@ -93,12 +93,12 @@ rule = "Host(`traefik.example.com`)"
|
|||
|
||||
```bash tab="Path Prefix Rule"
|
||||
# The dashboard can be accessed on http://example.com/dashboard/ or http://traefik.example.com/dashboard/
|
||||
rule = "PathPrefix(`/api`, `/dashboard`)"
|
||||
rule = "PathPrefix(`/api`) || PathPrefix(`/dashboard`)"
|
||||
```
|
||||
|
||||
```bash tab="Combination of Rules"
|
||||
# The dashboard can be accessed on http://traefik.example.com/dashboard/
|
||||
rule = "Host(`traefik.example.com`) && PathPrefix(`/api`, `/dashboard`)"
|
||||
rule = "Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
||||
```
|
||||
|
||||
??? example "Dashboard Dynamic Configuration Examples"
|
||||
|
|
|
@ -214,78 +214,224 @@ If you want to limit the router scope to a set of entry points, set the `entryPo
|
|||
Rules are a set of matchers configured with values, that determine if a particular request matches specific criteria.
|
||||
If the rule is verified, the router becomes active, calls middlewares, and then forwards the request to the service.
|
||||
|
||||
??? tip "Backticks or Quotes?"
|
||||
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
|
||||
|
||||
Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
|
||||
|
||||
!!! example "Host is example.com"
|
||||
|
||||
```toml
|
||||
rule = "Host(`example.com`)"
|
||||
```
|
||||
|
||||
!!! example "Host is example.com OR Host is example.org AND path is /traefik"
|
||||
|
||||
```toml
|
||||
rule = "Host(`example.com`) || (Host(`example.org`) && Path(`/traefik`))"
|
||||
```
|
||||
|
||||
The table below lists all the available matchers:
|
||||
|
||||
| Rule | Description |
|
||||
|------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
|
||||
| ```Headers(`key`, `value`)``` | Check if there is a key `key`defined in the headers, with the value `value` |
|
||||
| ```HeadersRegexp(`key`, `regexp`)``` | Check if there is a key `key`defined in the headers, with a value that matches the regular expression `regexp` |
|
||||
| ```Host(`example.com`, ...)``` | Check if the request domain (host header value) targets one of the given `domains`. |
|
||||
| ```HostHeader(`example.com`, ...)``` | Same as `Host`, only exists for historical reasons. |
|
||||
| ```HostRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Match the request domain. See "Regexp Syntax" below. |
|
||||
| ```Method(`GET`, ...)``` | Check if the request method is one of the given `methods` (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`) |
|
||||
| ```Path(`/path`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`, ...)``` | Match exact request path. See "Regexp Syntax" below. |
|
||||
| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)``` | Match request prefix path. See "Regexp Syntax" below. |
|
||||
| ```Query(`foo=bar`, `bar=baz`)``` | Match Query String parameters. It accepts a sequence of key=value pairs. |
|
||||
| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Match if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. |
|
||||
| Rule | Description |
|
||||
|-----------------------------------------------------------------|:-------------------------------------------------------------------------------|
|
||||
| [```Header(`key`, `value`)```](#header-and-headerregexp) | Matches requests containing a header named `key` set to `value`. |
|
||||
| [```HeaderRegexp(`key`, `regexp`)```](#header-and-headerregexp) | Matches requests containing a header named `key` matching `regexp`. |
|
||||
| [```Host(`domain`)```](#host-and-hostregexp) | Matches requests host set to `domain`. |
|
||||
| [```HostRegexp(`regexp`)```](#host-and-hostregexp) | Matches requests host matching `regexp`. |
|
||||
| [```Method(`method`)```](#method) | Matches requests method set to `method`. |
|
||||
| [```Path(`path`)```](#path-pathprefix-and-pathregexp) | Matches requests path set to `path`. |
|
||||
| [```PathPrefix(`prefix`)```](#path-pathprefix-and-pathregexp) | Matches requests path prefix set to `prefix`. |
|
||||
| [```PathRegexp(`regexp`)```](#path-pathprefix-and-pathregexp) | Matches request path using `regexp`. |
|
||||
| [```Query(`key`, `value`)```](#query-and-queryregexp) | Matches requests query parameters named `key` set to `value`. |
|
||||
| [```QueryRegexp(`key`, `regexp`)```](#query-and-queryregexp) | Matches requests query parameters named `key` matching `regexp`. |
|
||||
| [```ClientIP(`ip`)```](#clientip) | Matches requests client IP using `ip`. It accepts IPv4, IPv6 and CIDR formats. |
|
||||
|
||||
!!! important "Non-ASCII Domain Names"
|
||||
!!! tip "Backticks or Quotes?"
|
||||
|
||||
Non-ASCII characters are not supported in `Host` and `HostRegexp` expressions, and by doing so the associated router will be invalid.
|
||||
For the `Host` expression, domain names containing non-ASCII characters must be provided as punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)).
|
||||
As well, when using the `HostRegexp` expressions, in order to match domain names containing non-ASCII characters, the regular expression should match a punycode encoded domain name.
|
||||
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
|
||||
|
||||
Single quotes `'` are not accepted since the values are [Go's String Literals](https://golang.org/ref/spec#String_literals).
|
||||
|
||||
!!! important "Regexp Syntax"
|
||||
|
||||
`HostRegexp`, `PathPrefix`, and `Path` accept an expression with zero or more groups enclosed by curly braces, which are called named regexps.
|
||||
Named regexps, of the form `{name:regexp}`, are the only expressions considered for regexp matching.
|
||||
The regexp name (`name` in the above example) is an arbitrary value, that exists only for historical reasons.
|
||||
Matchers that accept a regexp as their value use a [Go](https://golang.org/pkg/regexp/) flavored syntax.
|
||||
|
||||
Any `regexp` supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used.
|
||||
For example, here is a case insensitive path matcher syntax: ```Path(`/{path:(?i:Products)}`)```.
|
||||
|
||||
!!! info "Combining Matchers Using Operators and Parenthesis"
|
||||
!!! info "Expressing Complex Rules Using Operators and Parenthesis"
|
||||
|
||||
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
||||
as well as parentheses.
|
||||
|
||||
One can invert a matcher by using the NOT (`!`) operator.
|
||||
|
||||
The following rule matches requests where:
|
||||
|
||||
- either host is `example.com` OR,
|
||||
- host is `example.org` AND path is NOT `/traefik`
|
||||
|
||||
```yaml
|
||||
Host(`example.com`) || (Host(`example.org`) && !Path(`/traefik`))
|
||||
```
|
||||
|
||||
!!! info "Inverting a matcher"
|
||||
#### Header and HeaderRegexp
|
||||
|
||||
One can invert a matcher by using the `!` operator.
|
||||
The `Header` and `HeaderRegexp` matchers allow to match requests that contain specific header.
|
||||
|
||||
!!! important "Rule, Middleware, and Services"
|
||||
!!! example "Examples"
|
||||
|
||||
Match requests with a `Content-Type` header set to `application/yaml`:
|
||||
|
||||
```yaml
|
||||
Header(`Content-Type`, `application/yaml`)
|
||||
```
|
||||
|
||||
Match requests with a `Content-Type` header set to either `application/json` or `application/yaml`:
|
||||
|
||||
```yaml
|
||||
HeaderRegexp(`Content-Type`, `^application/(json|yaml)$`)
|
||||
```
|
||||
|
||||
To match headers [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
|
||||
|
||||
```yaml
|
||||
HeaderRegexp(`Content-Type`, `(?i)^application/(json|yaml)$`)
|
||||
```
|
||||
|
||||
The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.
|
||||
#### Host and HostRegexp
|
||||
|
||||
!!! info "Path Vs PathPrefix"
|
||||
The `Host` and `HostRegexp` matchers allow to match requests that are targeted to a given host.
|
||||
|
||||
Use `Path` if your service listens on the exact path only. For instance, ```Path(`/products`)``` would match `/products` but not `/products/shoes`.
|
||||
These matchers do not support non-ASCII characters, use punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)) to match such domains.
|
||||
|
||||
Use a `*Prefix*` matcher if your service listens on a particular base path but also serves requests on sub-paths.
|
||||
For instance, ```PathPrefix(`/products`)``` would match `/products` and `/products/shoes`,
|
||||
as well as `/productsforsale`, and `/productsforsale/shoes`.
|
||||
Since the path is forwarded as-is, your service is expected to listen on `/products`.
|
||||
If no Host is set in the request URL (e.g., it's an IP address), these matchers will look at the `Host` header.
|
||||
|
||||
!!! info "ClientIP matcher"
|
||||
!!! example "Examples"
|
||||
|
||||
The `ClientIP` matcher will only match the request client IP and does not use the `X-Forwarded-For` header for matching.
|
||||
Match requests with `Host` set to `example.com`:
|
||||
|
||||
```yaml
|
||||
Host(`example.com`)
|
||||
```
|
||||
|
||||
Match requests sent to any subdomain of `example.com`:
|
||||
|
||||
```yaml
|
||||
HostRegexp(`^.+\.example\.com$`)
|
||||
```
|
||||
|
||||
Match requests with `Host` set to either `example.com` or `example.org`:
|
||||
|
||||
```yaml
|
||||
HostRegexp(`^example\.(com|org)$`)
|
||||
```
|
||||
|
||||
To match domains [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
|
||||
|
||||
```yaml
|
||||
HostRegexp(`(?i)^example\.(com|org)$`)
|
||||
```
|
||||
|
||||
#### Method
|
||||
|
||||
The `Method` matchers allows to match requests sent with the given method.
|
||||
|
||||
!!! example "Example"
|
||||
|
||||
Match `OPTIONS` requests:
|
||||
|
||||
```yaml
|
||||
Method(`OPTIONS`)
|
||||
```
|
||||
|
||||
#### Path, PathPrefix, and PathRegexp
|
||||
|
||||
These matchers allow matching requests based on their URL path.
|
||||
|
||||
For exact matches, use `Path` and its prefixed alternative `PathPrefix`, for regexp matches, use `PathRegexp`.
|
||||
|
||||
Path are always starting with a `/`, except for `PathRegexp`.
|
||||
|
||||
!!! example "Examples"
|
||||
|
||||
Match `/products` but neither `/products/shoes` nor `/products/`:
|
||||
|
||||
```yaml
|
||||
Path(`/products`)
|
||||
```
|
||||
|
||||
Match `/products` as well as everything under `/products`,
|
||||
such as `/products/shoes`, `/products/` but also `/products-for-sale`:
|
||||
|
||||
```yaml
|
||||
PathPrefix(`/products`)
|
||||
```
|
||||
|
||||
Match both `/products/shoes` and `/products/socks` with and ID like `/products/shoes/57`:
|
||||
|
||||
```yaml
|
||||
PathRegexp(`^/products/(shoes|socks)/[0-9]+$`)
|
||||
```
|
||||
|
||||
Match requests with a path ending in either `.jpeg`, `.jpg` or `.png`:
|
||||
|
||||
```yaml
|
||||
PathRegexp(`\.(jpeg|jpg|png)$`)
|
||||
```
|
||||
|
||||
Match `/products` as well as everything under `/products`,
|
||||
such as `/products/shoes`, `/products/` but also `/products-for-sale`,
|
||||
[case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity):
|
||||
|
||||
```yaml
|
||||
HostRegexp(`(?i)^/products`)
|
||||
```
|
||||
|
||||
#### Query and QueryRegexp
|
||||
|
||||
The `Query` and `QueryRegexp` matchers allow to match requests based on query parameters.
|
||||
|
||||
!!! example "Examples"
|
||||
|
||||
Match requests with a `mobile` query parameter set to `true`, such as in `/search?mobile=true`:
|
||||
|
||||
```yaml
|
||||
Query(`mobile`, `true`)
|
||||
```
|
||||
|
||||
To match requests with a query parameter `mobile` that has no value, such as in `/search?mobile`, use:
|
||||
|
||||
```yaml
|
||||
Query(`mobile`)
|
||||
```
|
||||
|
||||
Match requests with a `mobile` query parameter set to either `true` or `yes`:
|
||||
|
||||
```yaml
|
||||
QueryRegexp(`mobile`, `^(true|yes)$`)
|
||||
```
|
||||
|
||||
Match requests with a `mobile` query parameter set to any value (including the empty value):
|
||||
|
||||
```yaml
|
||||
QueryRegexp(`mobile`, `^.*$`)
|
||||
```
|
||||
|
||||
To match query parameters [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
|
||||
|
||||
```yaml
|
||||
QueryRegexp(`mobile`, `(?i)^(true|yes)$`)
|
||||
```
|
||||
|
||||
#### ClientIP
|
||||
|
||||
The `ClientIP` matcher allows matching requests sent from the given client IP.
|
||||
|
||||
It only matches the request client IP and does not use the `X-Forwarded-For` header for matching.
|
||||
|
||||
!!! example "Examples"
|
||||
|
||||
Match requests coming from a given IP:
|
||||
|
||||
```yaml tab="IPv4"
|
||||
ClientIP(`10.76.105.11`)
|
||||
```
|
||||
|
||||
```yaml tab="IPv6"
|
||||
ClientIP(`::1`)
|
||||
```
|
||||
|
||||
Match requests coming from a given subnet:
|
||||
|
||||
```yaml tab="IPv4"
|
||||
ClientIP(`192.168.1.0/24`)
|
||||
```
|
||||
|
||||
```yaml tab="IPv6"
|
||||
ClientIP(`fe80::/10`)
|
||||
```
|
||||
|
||||
### Priority
|
||||
|
||||
|
@ -300,7 +446,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
|
|||
http:
|
||||
routers:
|
||||
Router-1:
|
||||
rule: "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
|
||||
rule: "HostRegexp(`[a-z]+\.traefik\.com`)"
|
||||
# ...
|
||||
Router-2:
|
||||
rule: "Host(`foobar.traefik.com`)"
|
||||
|
@ -311,7 +457,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
|
|||
## Dynamic configuration
|
||||
[http.routers]
|
||||
[http.routers.Router-1]
|
||||
rule = "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
|
||||
rule = "HostRegexp(`[a-z]+\\.traefik\\.com`)"
|
||||
# ...
|
||||
[http.routers.Router-2]
|
||||
rule = "Host(`foobar.traefik.com`)"
|
||||
|
@ -320,10 +466,10 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
|
|||
|
||||
In this case, all requests with host `foobar.traefik.com` will be routed through `Router-1` instead of `Router-2`.
|
||||
|
||||
| Name | Rule | Priority |
|
||||
|----------|----------------------------------------------------|----------|
|
||||
| Router-1 | ```HostRegexp(`{subdomain:[a-z]+}.traefik.com`)``` | 44 |
|
||||
| Router-2 | ```Host(`foobar.traefik.com`)``` | 26 |
|
||||
| Name | Rule | Priority |
|
||||
|----------|------------------------------------------|----------|
|
||||
| Router-1 | ```HostRegexp(`[a-z]+\.traefik\.com`)``` | 44 |
|
||||
| Router-2 | ```Host(`foobar.traefik.com`)``` | 26 |
|
||||
|
||||
The previous table shows that `Router-1` has a higher priority than `Router-2`.
|
||||
|
||||
|
@ -336,7 +482,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
|
|||
http:
|
||||
routers:
|
||||
Router-1:
|
||||
rule: "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
|
||||
rule: "HostRegexp(`[a-z]+\\.traefik\\.com`)"
|
||||
entryPoints:
|
||||
- "web"
|
||||
service: service-1
|
||||
|
@ -353,7 +499,7 @@ A value of `0` for the priority is ignored: `priority = 0` means that the defaul
|
|||
## Dynamic configuration
|
||||
[http.routers]
|
||||
[http.routers.Router-1]
|
||||
rule = "HostRegexp(`{subdomain:[a-z]+}.traefik.com`)"
|
||||
rule = "HostRegexp(`[a-z]+\\.traefik\\.com`)"
|
||||
entryPoints = ["web"]
|
||||
service = "service-1"
|
||||
priority = 1
|
||||
|
@ -818,48 +964,49 @@ If you want to limit the router scope to a set of entry points, set the entry po
|
|||
|
||||
### Rule
|
||||
|
||||
Rules are a set of matchers configured with values, that determine if a particular request matches specific criteria.
|
||||
Rules are a set of matchers configured with values, that determine if a particular connection matches specific criteria.
|
||||
If the rule is verified, the router becomes active, calls middlewares, and then forwards the request to the service.
|
||||
|
||||
??? tip "Backticks or Quotes?"
|
||||
|
||||
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
|
||||
|
||||
Single quotes `'` are not accepted since the values are [Golang's String Literals](https://golang.org/ref/spec#String_literals).
|
||||
|
||||
!!! example "HostSNI is example.com"
|
||||
|
||||
```toml
|
||||
rule = "HostSNI(`example.com`)"
|
||||
```
|
||||
|
||||
!!! example "HostSNI is example.com OR HostSNI is example.org AND ClientIP is 0.0.0.0"
|
||||
|
||||
```toml
|
||||
rule = "HostSNI(`example.com`) || (HostSNI(`example.org`) && ClientIP(`0.0.0.0`))"
|
||||
```
|
||||
|
||||
The table below lists all the available matchers:
|
||||
|
||||
| Rule | Description |
|
||||
|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| ```HostSNI(`domain-1`, ...)``` | Checks if the Server Name Indication corresponds to the given `domains`. |
|
||||
| ```HostSNIRegexp(`example.com`, `{subdomain:[a-z]+}.example.com`, ...)``` | Checks if the Server Name Indication matches the given regular expressions. See "Regexp Syntax" below. |
|
||||
| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Checks if the connection client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. |
|
||||
| ```ALPN(`mqtt`, `h2c`)``` | Checks if any of the connection ALPN protocols is one of the given protocols. |
|
||||
| Rule | Description |
|
||||
|-------------------------------------------------------------|:-------------------------------------------------------------------------------------------------|
|
||||
| [```HostSNI(`domain`)```](#hostsni-and-hostsniregexp) | Checks if the connection's Server Name Indication is equal to `domain`. |
|
||||
| [```HostSNIRegexp(`regexp`)```](#hostsni-and-hostsniregexp) | Checks if the connection's Server Name Indication matches `regexp`. |
|
||||
| [```ClientIP(`ip`)```](#clientip_1) | Checks if the connection's client IP correspond to `ip`. It accepts IPv4, IPv6 and CIDR formats. |
|
||||
| [```ALPN(`protocol`)```](#alpn) | Checks if the connection's ALPN protocol equals `protocol`. |
|
||||
|
||||
!!! important "Non-ASCII Domain Names"
|
||||
!!! tip "Backticks or Quotes?"
|
||||
|
||||
Non-ASCII characters are not supported in the `HostSNI` and `HostSNIRegexp` expressions, and so using them would invalidate the associated TCP router.
|
||||
Domain names containing non-ASCII characters must be provided as punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)).
|
||||
To set the value of a rule, use [backticks](https://en.wiktionary.org/wiki/backtick) ``` ` ``` or escaped double-quotes `\"`.
|
||||
|
||||
Single quotes `'` are not accepted since the values are [Go's String Literals](https://golang.org/ref/spec#String_literals).
|
||||
|
||||
!!! important "Regexp Syntax"
|
||||
|
||||
`HostSNIRegexp` accepts an expression with zero or more groups enclosed by curly braces, which are called named regexps.
|
||||
Named regexps, of the form `{name:regexp}`, are the only expressions considered for regexp matching.
|
||||
The regexp name (`name` in the above example) is an arbitrary value, that exists only for historical reasons.
|
||||
Matchers that accept a regexp as their value use a [Go](https://golang.org/pkg/regexp/) flavored syntax.
|
||||
|
||||
Any `regexp` supported by [Go's regexp package](https://golang.org/pkg/regexp/) may be used.
|
||||
!!! info "Expressing Complex Rules Using Operators and Parenthesis"
|
||||
|
||||
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
||||
as well as parentheses.
|
||||
|
||||
One can invert a matcher by using the NOT (`!`) operator.
|
||||
|
||||
The following rule matches connections where:
|
||||
|
||||
- either Server Name Indication is `example.com` OR,
|
||||
- Server Name Indication is `example.org` AND ALPN protocol is NOT `h2`
|
||||
|
||||
```yaml
|
||||
HostSNI(`example.com`) || (HostSNI(`example.org`) && !ALPN(`h2`))
|
||||
```
|
||||
|
||||
#### HostSNI and HostSNIRegexp
|
||||
|
||||
`HostSNI` and `HostSNIRegexp` matchers allow to match connections targeted to a given domain.
|
||||
|
||||
These matchers do not support non-ASCII characters, use punycode encoded values ([rfc 3492](https://tools.ietf.org/html/rfc3492)) to match such domains.
|
||||
|
||||
!!! important "HostSNI & TLS"
|
||||
|
||||
|
@ -869,25 +1016,72 @@ The table below lists all the available matchers:
|
|||
when one wants a non-TLS router that matches all (non-TLS) requests,
|
||||
one should use the specific ```HostSNI(`*`)``` syntax.
|
||||
|
||||
!!! info "Combining Matchers Using Operators and Parenthesis"
|
||||
!!! example "Examples"
|
||||
|
||||
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
||||
as well as parentheses.
|
||||
Match all connections:
|
||||
|
||||
```yaml tab="HostSNI"
|
||||
HostSNI(`*`)
|
||||
```
|
||||
|
||||
```yaml tab="HostSNIRegexp"
|
||||
HostSNIRegexp(`^.*$`)
|
||||
```
|
||||
|
||||
!!! info "Inverting a matcher"
|
||||
Match TCP connections sent to `example.com`:
|
||||
|
||||
```yaml
|
||||
HostSNI(`example.com`)
|
||||
```
|
||||
|
||||
One can invert a matcher by using the `!` operator.
|
||||
Match TCP connections openned on any subdomain of `example.com`:
|
||||
|
||||
```yaml
|
||||
HostSNIRegexp(`^.+\.example\.com$`)
|
||||
```
|
||||
|
||||
!!! important "Rule, Middleware, and Services"
|
||||
#### ClientIP
|
||||
|
||||
The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.
|
||||
The `ClientIP` matcher allows matching connections opened by a client with the given IP.
|
||||
|
||||
!!! important "ALPN ACME-TLS/1"
|
||||
!!! example "Examples"
|
||||
|
||||
It would be a security issue to let a user-defined router catch the response to
|
||||
an ACME TLS challenge previously initiated by Traefik.
|
||||
For this reason, the `ALPN` matcher is not allowed to match the `ACME-TLS/1`
|
||||
protocol, and Traefik returns an error if this is attempted.
|
||||
Match connections opened by a given IP:
|
||||
|
||||
```yaml tab="IPv4"
|
||||
ClientIP(`10.76.105.11`)
|
||||
```
|
||||
|
||||
```yaml tab="IPv6"
|
||||
ClientIP(`::1`)
|
||||
```
|
||||
|
||||
Match connections coming from a given subnet:
|
||||
|
||||
```yaml tab="IPv4"
|
||||
ClientIP(`192.168.1.0/24`)
|
||||
```
|
||||
|
||||
```yaml tab="IPv6"
|
||||
ClientIP(`fe80::/10`)
|
||||
```
|
||||
|
||||
#### ALPN
|
||||
|
||||
The `ALPN` matcher allows matching connections the given protocol.
|
||||
|
||||
It would be a security issue to let a user-defined router catch the response to
|
||||
an ACME TLS challenge previously initiated by Traefik.
|
||||
For this reason, the `ALPN` matcher is not allowed to match the `ACME-TLS/1`
|
||||
protocol, and Traefik returns an error if this is attempted.
|
||||
|
||||
!!! example "Example"
|
||||
|
||||
Match connections using the ALPN protocol `h2`:
|
||||
|
||||
```yaml
|
||||
ALPN(`h2`)
|
||||
```
|
||||
|
||||
### Priority
|
||||
|
||||
|
|
7
go.mod
7
go.mod
|
@ -77,11 +77,12 @@ require (
|
|||
github.com/vulcand/predicate v1.2.0
|
||||
go.elastic.co/apm v1.13.1
|
||||
go.elastic.co/apm/module/apmot v1.13.1
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
|
||||
golang.org/x/exp v0.0.0-20221114191408-850992195362
|
||||
golang.org/x/mod v0.6.0
|
||||
golang.org/x/net v0.1.0
|
||||
golang.org/x/text v0.4.0
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
|
||||
golang.org/x/tools v0.1.12
|
||||
golang.org/x/tools v0.2.0
|
||||
google.golang.org/grpc v1.46.0
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.43.1
|
||||
gopkg.in/fsnotify.v1 v1.4.7
|
||||
|
@ -330,7 +331,7 @@ require (
|
|||
go.uber.org/zap v1.18.1 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
|
|
15
go.sum
15
go.sum
|
@ -2028,8 +2028,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -2044,7 +2044,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
|
|||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
|
||||
golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925 h1:5XVKs2rlCg8EFyRcvO8/XFwYxh1oKJO1Q3X5vttIf9c=
|
||||
golang.org/x/exp v0.0.0-20221114191408-850992195362 h1:NoHlPRbyl1VFI6FjwHtPQCN7wAMXI6cKcqrmXhOOfBQ=
|
||||
golang.org/x/exp v0.0.0-20221114191408-850992195362/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
|
@ -2074,8 +2075,8 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM
|
|||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -2417,8 +2418,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -20,12 +20,12 @@
|
|||
|
||||
[http.routers]
|
||||
[http.routers.router1]
|
||||
rule = "HostRegexp(`{subdomain:[a-z1-9-]+}.snitest.com`)"
|
||||
rule = "HostRegexp(`[a-z1-9-]+\\.snitest\\.com`)"
|
||||
service = "service1"
|
||||
[http.routers.router1.tls]
|
||||
|
||||
[http.routers.router2]
|
||||
rule = "HostRegexp(`{subdomain:[a-z1-9-]+}.www.snitest.com`)"
|
||||
rule = "HostRegexp(`[a-z1-9-]+\\.www\\.snitest\\.com`)"
|
||||
service = "service1"
|
||||
[http.routers.router2.tls]
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
[log]
|
||||
level = "DEBUG"
|
||||
noColor = true
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.webHost]
|
||||
|
@ -30,12 +31,12 @@
|
|||
[http.routers.router2]
|
||||
entryPoints = ["webHostRegexp"]
|
||||
service = "service1"
|
||||
rule = "!HostRegexp(`test.localhost`)"
|
||||
rule = "!HostRegexp(`test\\.localhost`)"
|
||||
|
||||
[http.routers.router3]
|
||||
entryPoints = ["webQuery"]
|
||||
service = "service1"
|
||||
rule = "!Query(`foo=`)"
|
||||
rule = "!QueryRegexp(`foo`, `.*`)"
|
||||
|
||||
|
||||
[http.services]
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
[http.routers]
|
||||
[http.routers.router1]
|
||||
service = "service1"
|
||||
rule = "Path(`/echo`,`/ws`)"
|
||||
rule = "Path(`/echo`) || Path(`/ws`)"
|
||||
[http.routers.router1.tls]
|
||||
|
||||
[http.services]
|
||||
|
|
|
@ -1092,7 +1092,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) {
|
|||
defer s.killCmd(cmd)
|
||||
|
||||
// wait for Traefik
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("HostRegexp(`{subdomain:[a-z1-9-]+}.www.snitest.com`)"))
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("HostRegexp(`[a-z1-9-]+\\\\.www\\\\.snitest\\\\.com`)"))
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
|
|
|
@ -1337,7 +1337,7 @@ func (s *SimpleSuite) TestMuxer(c *check.C) {
|
|||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "!Query with semicolon, no match",
|
||||
desc: "!Query with semicolon and empty query param value, no match",
|
||||
request: "GET /?foo=; HTTP/1.1\r\nHost: other.localhost\r\n\r\n",
|
||||
target: "127.0.0.1:8002",
|
||||
expected: http.StatusNotFound,
|
||||
|
@ -1367,9 +1367,7 @@ func (s *SimpleSuite) TestMuxer(c *check.C) {
|
|||
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
if resp.StatusCode != test.expected {
|
||||
c.Errorf("%s failed with %d instead of %d", test.desc, resp.StatusCode, test.expected)
|
||||
}
|
||||
c.Assert(resp.StatusCode, checker.Equals, test.expected, check.Commentf(test.desc))
|
||||
|
||||
if test.body != "" {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
|
|
253
pkg/muxer/http/matcher.go
Normal file
253
pkg/muxer/http/matcher.go
Normal file
|
@ -0,0 +1,253 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v2/pkg/ip"
|
||||
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var httpFuncs = map[string]func(*mux.Route, ...string) error{
|
||||
"ClientIP": expectNParameters(clientIP, 1),
|
||||
"Method": expectNParameters(method, 1),
|
||||
"Host": expectNParameters(host, 1),
|
||||
"HostRegexp": expectNParameters(hostRegexp, 1),
|
||||
"Path": expectNParameters(path, 1),
|
||||
"PathRegexp": expectNParameters(pathRegexp, 1),
|
||||
"PathPrefix": expectNParameters(pathPrefix, 1),
|
||||
"Header": expectNParameters(header, 2),
|
||||
"HeaderRegexp": expectNParameters(headerRegexp, 2),
|
||||
"Query": expectNParameters(query, 1, 2),
|
||||
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
|
||||
}
|
||||
|
||||
func expectNParameters(fn func(*mux.Route, ...string) error, n ...int) func(*mux.Route, ...string) error {
|
||||
return func(route *mux.Route, s ...string) error {
|
||||
if !slices.Contains(n, len(s)) {
|
||||
return fmt.Errorf("unexpected number of parameters; got %d, expected one of %v", len(s), n)
|
||||
}
|
||||
|
||||
return fn(route, s...)
|
||||
}
|
||||
}
|
||||
|
||||
func clientIP(route *mux.Route, clientIP ...string) error {
|
||||
checker, err := ip.NewChecker(clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
|
||||
}
|
||||
|
||||
strategy := ip.RemoteAddrStrategy{}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
ok, err := checker.Contains(strategy.GetIP(req))
|
||||
if err != nil {
|
||||
log.Ctx(req.Context()).Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
|
||||
return false
|
||||
}
|
||||
|
||||
return ok
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func method(route *mux.Route, methods ...string) error {
|
||||
return route.Methods(methods...).GetError()
|
||||
}
|
||||
|
||||
func host(route *mux.Route, hosts ...string) error {
|
||||
host := hosts[0]
|
||||
|
||||
if !IsASCII(host) {
|
||||
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", host)
|
||||
}
|
||||
|
||||
host = strings.ToLower(host)
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
||||
if len(reqHost) == 0 {
|
||||
// If the request is an HTTP/1.0 request, then a Host may not be defined.
|
||||
if req.ProtoAtLeast(1, 1) {
|
||||
log.Ctx(req.Context()).Warn().Str("host", req.Host).Msg("Could not retrieve CanonizedHost, rejecting")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
|
||||
if len(flatH) > 0 {
|
||||
if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) {
|
||||
return true
|
||||
}
|
||||
|
||||
log.Ctx(req.Context()).Debug().
|
||||
Str("host", reqHost).
|
||||
Str("flattenHost", flatH).
|
||||
Str("matcher", host).
|
||||
Msg("CNAMEFlattening: resolved Host does not match")
|
||||
return false
|
||||
}
|
||||
|
||||
if reqHost == host {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for match on trailing period on host
|
||||
if last := len(host) - 1; last >= 0 && host[last] == '.' {
|
||||
h := host[:last]
|
||||
if reqHost == h {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for match on trailing period on request
|
||||
if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
|
||||
h := reqHost[:last]
|
||||
if h == host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostRegexp(route *mux.Route, hosts ...string) error {
|
||||
host := hosts[0]
|
||||
|
||||
if !IsASCII(host) {
|
||||
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling HostRegexp matcher: %w", err)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
return re.MatchString(req.Host)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func path(route *mux.Route, paths ...string) error {
|
||||
path := paths[0]
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return fmt.Errorf("path %q does not start with a '/'", path)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
return req.URL.Path == path
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathRegexp(route *mux.Route, paths ...string) error {
|
||||
path := paths[0]
|
||||
|
||||
re, err := regexp.Compile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling PathPrefix matcher: %w", err)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
return re.MatchString(req.URL.Path)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathPrefix(route *mux.Route, paths ...string) error {
|
||||
path := paths[0]
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return fmt.Errorf("path %q does not start with a '/'", path)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
return strings.HasPrefix(req.URL.Path, path)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func header(route *mux.Route, headers ...string) error {
|
||||
return route.Headers(headers...).GetError()
|
||||
}
|
||||
|
||||
func headerRegexp(route *mux.Route, headers ...string) error {
|
||||
return route.HeadersRegexp(headers...).GetError()
|
||||
}
|
||||
|
||||
func query(route *mux.Route, queries ...string) error {
|
||||
key := queries[0]
|
||||
|
||||
var value string
|
||||
if len(queries) == 2 {
|
||||
value = queries[1]
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
values, ok := req.URL.Query()[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return slices.Contains(values, value)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func queryRegexp(route *mux.Route, queries ...string) error {
|
||||
if len(queries) == 1 {
|
||||
return query(route, queries...)
|
||||
}
|
||||
|
||||
key, value := queries[0], queries[1]
|
||||
|
||||
re, err := regexp.Compile(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling QueryRegexp matcher: %w", err)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
values, ok := req.URL.Query()[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
idx := slices.IndexFunc(values, func(value string) bool {
|
||||
return re.MatchString(value)
|
||||
})
|
||||
|
||||
return idx >= 0
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsASCII checks if the given string contains only ASCII characters.
|
||||
func IsASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
974
pkg/muxer/http/matcher_test.go
Normal file
974
pkg/muxer/http/matcher_test.go
Normal file
|
@ -0,0 +1,974 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
||||
)
|
||||
|
||||
func TestClientIPMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid ClientIP matcher",
|
||||
rule: "ClientIP(`1`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid ClientIP matcher (no parameter)",
|
||||
rule: "ClientIP()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid ClientIP matcher (empty parameter)",
|
||||
rule: "ClientIP(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid ClientIP matcher (too many parameters)",
|
||||
rule: "ClientIP(`127.0.0.1`, `192.168.1.0/24`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid ClientIP matcher",
|
||||
rule: "ClientIP(`127.0.0.1`)",
|
||||
expected: map[string]int{
|
||||
"127.0.0.1": http.StatusOK,
|
||||
"192.168.1.1": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid ClientIP matcher but invalid remote address",
|
||||
rule: "ClientIP(`127.0.0.1`)",
|
||||
expected: map[string]int{
|
||||
"1": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid ClientIP matcher using CIDR",
|
||||
rule: "ClientIP(`192.168.1.0/24`)",
|
||||
expected: map[string]int{
|
||||
"192.168.1.1": http.StatusOK,
|
||||
"192.168.1.100": http.StatusOK,
|
||||
"192.168.2.1": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for remoteAddr := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
|
||||
req.RemoteAddr = remoteAddr
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[remoteAddr] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMethodMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid Method matcher (no parameter)",
|
||||
rule: "Method()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Method matcher (empty parameter)",
|
||||
rule: "Method(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Method matcher (too many parameters)",
|
||||
rule: "Method(`GET`, `POST`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid Method matcher",
|
||||
rule: "Method(`GET`)",
|
||||
expected: map[string]int{
|
||||
http.MethodGet: http.StatusOK,
|
||||
http.MethodPost: http.StatusMethodNotAllowed,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid Method matcher (lower case)",
|
||||
rule: "Method(`get`)",
|
||||
expected: map[string]int{
|
||||
http.MethodGet: http.StatusOK,
|
||||
http.MethodPost: http.StatusMethodNotAllowed,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for method := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(method, "https://example.com", http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[method] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid Host matcher (no parameter)",
|
||||
rule: "Host()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Host matcher (empty parameter)",
|
||||
rule: "Host(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Host matcher (non-ASCII)",
|
||||
rule: "Host(`🦭.com`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Host matcher (too many parameters)",
|
||||
rule: "Host(`example.com`, `example.org`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid Host matcher",
|
||||
rule: "Host(`example.com`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusOK,
|
||||
"https://example.com/path": http.StatusOK,
|
||||
"https://example.org": http.StatusNotFound,
|
||||
"https://example.org/path": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid Host matcher - matcher ending with a dot",
|
||||
rule: "Host(`example.com.`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusOK,
|
||||
"https://example.com/path": http.StatusOK,
|
||||
"https://example.org": http.StatusNotFound,
|
||||
"https://example.org/path": http.StatusNotFound,
|
||||
"https://example.com.": http.StatusOK,
|
||||
"https://example.com./path": http.StatusOK,
|
||||
"https://example.org.": http.StatusNotFound,
|
||||
"https://example.org./path": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid Host matcher - URL ending with a dot",
|
||||
rule: "Host(`example.com`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com.": http.StatusOK,
|
||||
"https://example.com./path": http.StatusOK,
|
||||
"https://example.org.": http.StatusNotFound,
|
||||
"https://example.org./path": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid Host matcher - puny-coded emoji",
|
||||
rule: "Host(`xn--9t9h.com`)",
|
||||
expected: map[string]int{
|
||||
"https://xn--9t9h.com": http.StatusOK,
|
||||
"https://xn--9t9h.com/path": http.StatusOK,
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com/path": http.StatusNotFound,
|
||||
// The request's sender must use puny-code.
|
||||
"https://🦭.com": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// RequestDecorator is necessary for the host rule
|
||||
reqHost := requestdecorator.New(nil)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostRegexpMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid HostRegexp matcher (no parameter)",
|
||||
rule: "HostRegexp()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HostRegexp matcher (empty parameter)",
|
||||
rule: "HostRegexp(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HostRegexp matcher (non-ASCII)",
|
||||
rule: "HostRegexp(`🦭.com`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HostRegexp matcher (invalid regexp)",
|
||||
rule: "HostRegexp(`(example.com`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HostRegexp matcher (too many parameters)",
|
||||
rule: "HostRegexp(`example.com`, `example.org`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid HostRegexp matcher",
|
||||
rule: "HostRegexp(`^[a-zA-Z-]+\\.com$`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusOK,
|
||||
"https://example.com/path": http.StatusOK,
|
||||
"https://example.org": http.StatusNotFound,
|
||||
"https://example.org/path": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid HostRegexp matcher with Traefik v2 syntax",
|
||||
rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com/path": http.StatusNotFound,
|
||||
"https://example.org": http.StatusNotFound,
|
||||
"https://example.org/path": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid Path matcher (no parameter)",
|
||||
rule: "Path()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Path matcher (empty parameter)",
|
||||
rule: "Path(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Path matcher (no leading /)",
|
||||
rule: "Path(`css`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Path matcher (too many parameters)",
|
||||
rule: "Path(`/css`, `/js`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid Path matcher",
|
||||
rule: "Path(`/css`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com/html": http.StatusNotFound,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/css/": http.StatusNotFound,
|
||||
"https://example.com/css/main.css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathRegexpMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid PathRegexp matcher (no parameter)",
|
||||
rule: "PathRegexp()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid PathRegexp matcher (empty parameter)",
|
||||
rule: "PathRegexp(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid PathRegexp matcher (invalid regexp)",
|
||||
rule: "PathRegexp(`/(css`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid PathRegexp matcher (too many parameters)",
|
||||
rule: "PathRegexp(`/css`, `/js`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid PathRegexp matcher",
|
||||
rule: "PathRegexp(`^/(css|js)`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com/html": http.StatusNotFound,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.com/CSS": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/css/": http.StatusOK,
|
||||
"https://example.com/css/main.css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusOK,
|
||||
"https://example.com/js/": http.StatusOK,
|
||||
"https://example.com/js/main.js": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid PathRegexp matcher with Traefik v2 syntax",
|
||||
rule: `PathRegexp("/{path:(css|js)}")`,
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com/html": http.StatusNotFound,
|
||||
"https://example.org/css": http.StatusNotFound,
|
||||
"https://example.com/{path:css}": http.StatusOK,
|
||||
"https://example.com/{path:css}/": http.StatusOK,
|
||||
"https://example.com/%7Bpath:css%7D": http.StatusOK,
|
||||
"https://example.com/%7Bpath:css%7D/": http.StatusOK,
|
||||
"https://example.com/{path:js}": http.StatusOK,
|
||||
"https://example.com/{path:js}/": http.StatusOK,
|
||||
"https://example.com/%7Bpath:js%7D": http.StatusOK,
|
||||
"https://example.com/%7Bpath:js%7D/": http.StatusOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathPrefixMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid PathPrefix matcher (no parameter)",
|
||||
rule: "PathPrefix()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid PathPrefix matcher (empty parameter)",
|
||||
rule: "PathPrefix(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid PathPrefix matcher (no leading /)",
|
||||
rule: "PathPrefix(`css`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid PathPrefix matcher (too many parameters)",
|
||||
rule: "PathPrefix(`/css`, `/js`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid PathPrefix matcher",
|
||||
rule: `PathPrefix("/css")`,
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com/html": http.StatusNotFound,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/css/": http.StatusOK,
|
||||
"https://example.com/css/main.css": http.StatusOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[*http.Header]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid Header matcher (no parameter)",
|
||||
rule: "Header()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Header matcher (missing value parameter)",
|
||||
rule: "Header(`X-Forwarded-Host`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Header matcher (missing value parameter)",
|
||||
rule: "Header(`X-Forwarded-Host`, ``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Header matcher (missing key parameter)",
|
||||
rule: "Header(``, `example.com`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Header matcher (too many parameters)",
|
||||
rule: "Header(`X-Forwarded-Host`, `example.com`, `example.org`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid Header matcher",
|
||||
rule: "Header(`X-Forwarded-Proto`, `https`)",
|
||||
expected: map[*http.Header]int{
|
||||
{"X-Forwarded-Proto": []string{"https"}}: http.StatusOK,
|
||||
{"x-forwarded-proto": []string{"https"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"http", "https"}}: http.StatusOK,
|
||||
{"X-Forwarded-Proto": []string{"https", "http"}}: http.StatusOK,
|
||||
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for headers := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
|
||||
req.Header = *headers
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
assert.Equal(t, test.expected[headers], w.Code, headers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderRegexpMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[*http.Header]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid HeaderRegexp matcher (no parameter)",
|
||||
rule: "HeaderRegexp()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HeaderRegexp matcher (missing value parameter)",
|
||||
rule: "HeaderRegexp(`X-Forwarded-Host`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HeaderRegexp matcher (missing value parameter)",
|
||||
rule: "HeaderRegexp(`X-Forwarded-Host`, ``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HeaderRegexp matcher (missing key parameter)",
|
||||
rule: "HeaderRegexp(``, `example.com`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HeaderRegexp matcher (invalid regexp)",
|
||||
rule: "HeaderRegexp(`X-Forwarded-Host`,`(example.com`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid HeaderRegexp matcher (too many parameters)",
|
||||
rule: "HeaderRegexp(`X-Forwarded-Host`, `example.com`, `example.org`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid HeaderRegexp matcher",
|
||||
rule: "HeaderRegexp(`X-Forwarded-Proto`, `^https?$`)",
|
||||
expected: map[*http.Header]int{
|
||||
{"X-Forwarded-Proto": []string{"http"}}: http.StatusOK,
|
||||
{"x-forwarded-proto": []string{"http"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"https"}}: http.StatusOK,
|
||||
{"X-Forwarded-Proto": []string{"HTTPS"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"ws", "https"}}: http.StatusOK,
|
||||
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid HeaderRegexp matcher with Traefik v2 syntax",
|
||||
rule: "HeaderRegexp(`X-Forwarded-Proto`, `http{secure:s?}`)",
|
||||
expected: map[*http.Header]int{
|
||||
{"X-Forwarded-Proto": []string{"http"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"https"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"http{secure:}"}}: http.StatusOK,
|
||||
{"X-Forwarded-Proto": []string{"HTTP{secure:}"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"http{secure:s}"}}: http.StatusOK,
|
||||
{"X-Forwarded-Proto": []string{"http{secure:S}"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"HTTPS"}}: http.StatusNotFound,
|
||||
{"X-Forwarded-Proto": []string{"ws", "http{secure:s}"}}: http.StatusOK,
|
||||
{"X-Forwarded-Host": []string{"example.com"}}: http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for headers := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://example.com", http.NoBody)
|
||||
req.Header = *headers
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
assert.Equal(t, test.expected[headers], w.Code, *headers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid Query matcher (no parameter)",
|
||||
rule: "Query()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Query matcher (empty key, one parameter)",
|
||||
rule: "Query(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Query matcher (empty key)",
|
||||
rule: "Query(``, `traefik`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Query matcher (empty value)",
|
||||
rule: "Query(`q`, ``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid Query matcher (too many parameters)",
|
||||
rule: "Query(`q`, `traefik`, `proxy`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid Query matcher",
|
||||
rule: "Query(`q`, `traefik`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com?q=traefik": http.StatusOK,
|
||||
"https://example.com?rel=ddg&q=traefik": http.StatusOK,
|
||||
"https://example.com?q=traefik&q=proxy": http.StatusOK,
|
||||
"https://example.com?q=awesome&q=traefik": http.StatusOK,
|
||||
"https://example.com?q=nginx": http.StatusNotFound,
|
||||
"https://example.com?rel=ddg": http.StatusNotFound,
|
||||
"https://example.com?q=TRAEFIK": http.StatusNotFound,
|
||||
"https://example.com?Q=traefik": http.StatusNotFound,
|
||||
"https://example.com?rel=traefik": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid Query matcher with empty value",
|
||||
rule: "Query(`mobile`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com?mobile": http.StatusOK,
|
||||
"https://example.com?mobile=true": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRegexpMatcher(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "invalid QueryRegexp matcher (no parameter)",
|
||||
rule: "QueryRegexp()",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid QueryRegexp matcher (empty parameter)",
|
||||
rule: "QueryRegexp(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid QueryRegexp matcher (invalid regexp)",
|
||||
rule: "QueryRegexp(`q`, `(traefik`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid QueryRegexp matcher (too many parameters)",
|
||||
rule: "QueryRegexp(`q`, `traefik`, `proxy`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "valid QueryRegexp matcher",
|
||||
rule: "QueryRegexp(`q`, `^(traefik|nginx)$`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com?q=traefik": http.StatusOK,
|
||||
"https://example.com?rel=ddg&q=traefik": http.StatusOK,
|
||||
"https://example.com?q=traefik&q=proxy": http.StatusOK,
|
||||
"https://example.com?q=awesome&q=traefik": http.StatusOK,
|
||||
"https://example.com?q=TRAEFIK": http.StatusNotFound,
|
||||
"https://example.com?Q=traefik": http.StatusNotFound,
|
||||
"https://example.com?rel=traefik": http.StatusNotFound,
|
||||
"https://example.com?q=nginx": http.StatusOK,
|
||||
"https://example.com?rel=ddg&q=nginx": http.StatusOK,
|
||||
"https://example.com?q=nginx&q=proxy": http.StatusOK,
|
||||
"https://example.com?q=awesome&q=nginx": http.StatusOK,
|
||||
"https://example.com?q=NGINX": http.StatusNotFound,
|
||||
"https://example.com?Q=nginx": http.StatusNotFound,
|
||||
"https://example.com?rel=nginx": http.StatusNotFound,
|
||||
"https://example.com?q=haproxy": http.StatusNotFound,
|
||||
"https://example.com?rel=ddg": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid QueryRegexp matcher",
|
||||
rule: "QueryRegexp(`q`, `^.*$`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com": http.StatusNotFound,
|
||||
"https://example.com?q=traefik": http.StatusOK,
|
||||
"https://example.com?rel=ddg&q=traefik": http.StatusOK,
|
||||
"https://example.com?q=traefik&q=proxy": http.StatusOK,
|
||||
"https://example.com?q=awesome&q=traefik": http.StatusOK,
|
||||
"https://example.com?q=TRAEFIK": http.StatusOK,
|
||||
"https://example.com?Q=traefik": http.StatusNotFound,
|
||||
"https://example.com?rel=traefik": http.StatusNotFound,
|
||||
"https://example.com?q=nginx": http.StatusOK,
|
||||
"https://example.com?rel=ddg&q=nginx": http.StatusOK,
|
||||
"https://example.com?q=nginx&q=proxy": http.StatusOK,
|
||||
"https://example.com?q=awesome&q=nginx": http.StatusOK,
|
||||
"https://example.com?q=NGINX": http.StatusOK,
|
||||
"https://example.com?Q=nginx": http.StatusNotFound,
|
||||
"https://example.com?rel=nginx": http.StatusNotFound,
|
||||
"https://example.com?q=haproxy": http.StatusOK,
|
||||
"https://example.com?rel=ddg": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid QueryRegexp matcher with Traefik v2 syntax",
|
||||
rule: "QueryRegexp(`q`, `{value:(traefik|nginx)}`)",
|
||||
expected: map[string]int{
|
||||
"https://example.com?q=traefik": http.StatusNotFound,
|
||||
"https://example.com?q={value:traefik}": http.StatusOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,32 +3,12 @@ package http
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v2/pkg/ip"
|
||||
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
||||
"github.com/traefik/traefik/v2/pkg/rules"
|
||||
"github.com/vulcand/predicate"
|
||||
)
|
||||
|
||||
const hostMatcher = "Host"
|
||||
|
||||
var httpFuncs = map[string]func(*mux.Route, ...string) error{
|
||||
hostMatcher: host,
|
||||
"HostHeader": host,
|
||||
"HostRegexp": hostRegexp,
|
||||
"ClientIP": clientIP,
|
||||
"Path": path,
|
||||
"PathPrefix": pathPrefix,
|
||||
"Method": methods,
|
||||
"Headers": headers,
|
||||
"HeadersRegexp": headersRegexp,
|
||||
"Query": query,
|
||||
}
|
||||
|
||||
// Muxer handles routing with rules.
|
||||
type Muxer struct {
|
||||
*mux.Router
|
||||
|
@ -80,171 +60,6 @@ func (r *Muxer) AddRoute(rule string, priority int, handler http.Handler) error
|
|||
return nil
|
||||
}
|
||||
|
||||
// ParseDomains extract domains from rule.
|
||||
func ParseDomains(rule string) ([]string, error) {
|
||||
var matchers []string
|
||||
for matcher := range httpFuncs {
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
|
||||
parser, err := rules.NewParser(matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parse, err := parser.Parse(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buildTree, ok := parse.(rules.TreeBuilder)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
||||
}
|
||||
|
||||
return buildTree().ParseMatchers([]string{hostMatcher}), nil
|
||||
}
|
||||
|
||||
func path(route *mux.Route, paths ...string) error {
|
||||
rt := route.Subrouter()
|
||||
|
||||
for _, path := range paths {
|
||||
if err := rt.Path(path).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathPrefix(route *mux.Route, paths ...string) error {
|
||||
rt := route.Subrouter()
|
||||
|
||||
for _, path := range paths {
|
||||
if err := rt.PathPrefix(path).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func host(route *mux.Route, hosts ...string) error {
|
||||
for i, host := range hosts {
|
||||
if !IsASCII(host) {
|
||||
return fmt.Errorf("invalid value %q for \"Host\" matcher, non-ASCII characters are not allowed", host)
|
||||
}
|
||||
|
||||
hosts[i] = strings.ToLower(host)
|
||||
}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
||||
if len(reqHost) == 0 {
|
||||
// If the request is an HTTP/1.0 request, then a Host may not be defined.
|
||||
if req.ProtoAtLeast(1, 1) {
|
||||
log.Ctx(req.Context()).Warn().Msgf("Could not retrieve CanonizedHost, rejecting %s", req.Host)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
|
||||
if len(flatH) > 0 {
|
||||
for _, host := range hosts {
|
||||
if strings.EqualFold(reqHost, host) || strings.EqualFold(flatH, host) {
|
||||
return true
|
||||
}
|
||||
log.Ctx(req.Context()).Debug().Msgf("CNAMEFlattening: request %s which resolved to %s, is not matched to route %s", reqHost, flatH, host)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
if reqHost == host {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for match on trailing period on host
|
||||
if last := len(host) - 1; last >= 0 && host[last] == '.' {
|
||||
h := host[:last]
|
||||
if reqHost == h {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for match on trailing period on request
|
||||
if last := len(reqHost) - 1; last >= 0 && reqHost[last] == '.' {
|
||||
h := reqHost[:last]
|
||||
if h == host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientIP(route *mux.Route, clientIPs ...string) error {
|
||||
checker, err := ip.NewChecker(clientIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
|
||||
}
|
||||
|
||||
strategy := ip.RemoteAddrStrategy{}
|
||||
|
||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||
ok, err := checker.Contains(strategy.GetIP(req))
|
||||
if err != nil {
|
||||
log.Ctx(req.Context()).Warn().Err(err).Msg("\"ClientIP\" matcher: could not match remote address")
|
||||
return false
|
||||
}
|
||||
|
||||
return ok
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostRegexp(route *mux.Route, hosts ...string) error {
|
||||
router := route.Subrouter()
|
||||
for _, host := range hosts {
|
||||
if !IsASCII(host) {
|
||||
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
|
||||
}
|
||||
|
||||
tmpRt := router.Host(host)
|
||||
if tmpRt.GetError() != nil {
|
||||
return tmpRt.GetError()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func methods(route *mux.Route, methods ...string) error {
|
||||
return route.Methods(methods...).GetError()
|
||||
}
|
||||
|
||||
func headers(route *mux.Route, headers ...string) error {
|
||||
return route.Headers(headers...).GetError()
|
||||
}
|
||||
|
||||
func headersRegexp(route *mux.Route, headers ...string) error {
|
||||
return route.HeadersRegexp(headers...).GetError()
|
||||
}
|
||||
|
||||
func query(route *mux.Route, query ...string) error {
|
||||
var queries []string
|
||||
for _, elem := range query {
|
||||
queries = append(queries, strings.SplitN(elem, "=", 2)...)
|
||||
}
|
||||
|
||||
route.Queries(queries...)
|
||||
// Queries can return nil so we can't chain the GetError()
|
||||
return route.GetError()
|
||||
}
|
||||
|
||||
func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
|
||||
switch rule.Matcher {
|
||||
case "and":
|
||||
|
@ -276,20 +91,6 @@ func addRuleOnRouter(router *mux.Router, rule *rules.Tree) error {
|
|||
}
|
||||
}
|
||||
|
||||
func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error {
|
||||
return func(r *mux.Route, v ...string) error {
|
||||
router := mux.NewRouter()
|
||||
err := m(router.NewRoute(), v...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.MatcherFunc(func(req *http.Request, ma *mux.RouteMatch) bool {
|
||||
return !router.Match(req, ma)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
|
||||
switch rule.Matcher {
|
||||
case "and":
|
||||
|
@ -322,13 +123,44 @@ func addRuleOnRoute(route *mux.Route, rule *rules.Tree) error {
|
|||
}
|
||||
}
|
||||
|
||||
// IsASCII checks if the given string contains only ASCII characters.
|
||||
func IsASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error {
|
||||
return func(r *mux.Route, v ...string) error {
|
||||
router := mux.NewRouter()
|
||||
|
||||
err := m(router.NewRoute(), v...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.MatcherFunc(func(req *http.Request, ma *mux.RouteMatch) bool {
|
||||
return !router.Match(req, ma)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ParseDomains extract domains from rule.
|
||||
func ParseDomains(rule string) ([]string, error) {
|
||||
var matchers []string
|
||||
for matcher := range httpFuncs {
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
|
||||
return true
|
||||
parser, err := rules.NewParser(matchers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parse, err := parser.Parse(rule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buildTree, ok := parse.(rules.TreeBuilder)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
||||
}
|
||||
|
||||
return buildTree().ParseMatchers([]string{"Host"}), nil
|
||||
}
|
||||
|
|
|
@ -7,14 +7,13 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
||||
"github.com/traefik/traefik/v2/pkg/testhelpers"
|
||||
)
|
||||
|
||||
func Test_addRoute(t *testing.T) {
|
||||
func TestMuxer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
|
@ -33,607 +32,177 @@ func Test_addRoute(t *testing.T) {
|
|||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Host empty",
|
||||
rule: "Host(``)",
|
||||
desc: "Rule without quote",
|
||||
rule: "Host(example.com)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "PathPrefix empty",
|
||||
rule: "PathPrefix(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "PathPrefix",
|
||||
rule: "PathPrefix(`/foo`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wrong PathPrefix",
|
||||
rule: "PathPrefix(`/bar`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host",
|
||||
rule: "Host(`localhost`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non-ASCII Host",
|
||||
rule: "Host(`locàlhost`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Non-ASCII HostRegexp",
|
||||
rule: "HostRegexp(`locàlhost`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "HostHeader equivalent to Host",
|
||||
rule: "HostHeader(`localhost`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
"http://bar/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host with trailing period in rule",
|
||||
rule: "Host(`localhost.`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host with trailing period in domain",
|
||||
rule: "Host(`localhost`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost./foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host with trailing period in domain and rule",
|
||||
rule: "Host(`localhost.`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost./foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "wrong Host",
|
||||
rule: "Host(`nope`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix",
|
||||
rule: "Host(`localhost`) && PathPrefix(`/foo`)",
|
||||
rule: "Host(`localhost`) && PathPrefix(`/css`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix wrong PathPrefix",
|
||||
rule: "Host(`localhost`) && PathPrefix(`/bar`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix wrong Host",
|
||||
rule: "Host(`nope`) && PathPrefix(`/foo`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix Host OR, first host",
|
||||
rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix Host OR, second host",
|
||||
rule: "Host(`nope`,`localhost`) && PathPrefix(`/foo`)",
|
||||
expected: map[string]int{
|
||||
"http://nope/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Host and PathPrefix Host OR, first host and wrong PathPrefix",
|
||||
rule: "Host(`nope,localhost`) && PathPrefix(`/bar`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HostRegexp with capturing group",
|
||||
rule: "HostRegexp(`{subdomain:(foo\\.)?bar\\.com}`)",
|
||||
expected: map[string]int{
|
||||
"http://foo.bar.com": http.StatusOK,
|
||||
"http://bar.com": http.StatusOK,
|
||||
"http://fooubar.com": http.StatusNotFound,
|
||||
"http://barucom": http.StatusNotFound,
|
||||
"http://barcom": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HostRegexp with non capturing group",
|
||||
rule: "HostRegexp(`{subdomain:(?:foo\\.)?bar\\.com}`)",
|
||||
expected: map[string]int{
|
||||
"http://foo.bar.com": http.StatusOK,
|
||||
"http://bar.com": http.StatusOK,
|
||||
"http://fooubar.com": http.StatusNotFound,
|
||||
"http://barucom": http.StatusNotFound,
|
||||
"http://barcom": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Methods with GET",
|
||||
rule: "Method(`GET`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Methods with GET and POST",
|
||||
rule: "Method(`GET`,`POST`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Methods with POST",
|
||||
rule: "Method(`POST`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusMethodNotAllowed,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Header with matching header",
|
||||
rule: "Headers(`Content-Type`,`application/json`)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Header without matching header",
|
||||
rule: "Headers(`Content-Type`,`application/foo`)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HeaderRegExp with matching header",
|
||||
rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HeaderRegExp without matching header",
|
||||
rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/foo",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "HeaderRegExp with matching second header",
|
||||
rule: "HeadersRegexp(`Content-Type`, `application/(text|json)`)",
|
||||
headers: map[string]string{
|
||||
"Content-Type": "application/text",
|
||||
},
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Query with multiple params",
|
||||
rule: "Query(`foo=bar`, `bar=baz`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo?foo=bar&bar=baz": http.StatusOK,
|
||||
"http://localhost/foo?bar=baz": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Query with multiple equals",
|
||||
rule: "Query(`foo=b=ar`)",
|
||||
expected: map[string]int{
|
||||
"http://localhost/foo?foo=b=ar": http.StatusOK,
|
||||
"http://localhost/foo?foo=bar": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with simple path",
|
||||
rule: `Path("/a")`,
|
||||
expected: map[string]int{
|
||||
"http://plop/a": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: `Rule with a simple host`,
|
||||
rule: `Host("plop")`,
|
||||
expected: map[string]int{
|
||||
"http://plop": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with Path AND Host",
|
||||
rule: `Path("/a") && Host("plop")`,
|
||||
expected: map[string]int{
|
||||
"http://plop/a": http.StatusOK,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
"https://localhost/css": http.StatusOK,
|
||||
"https://localhost/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with Host OR Host",
|
||||
rule: `Host("tchouk") || Host("pouet")`,
|
||||
rule: "Host(`example.com`) || Host(`example.org`)",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"http://pouet/a": http.StatusOK,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.org/js": http.StatusOK,
|
||||
"https://example.eu/html": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with host OR (host AND path)",
|
||||
rule: `Host("tchouk") || (Host("pouet") && Path("/powpow"))`,
|
||||
rule: `Host("example.com") || (Host("example.org") && Path("/css"))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusOK,
|
||||
"http://pouet/powpow": http.StatusOK,
|
||||
"http://pouet/toto": http.StatusNotFound,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.org/js": http.StatusNotFound,
|
||||
"https://example.eu/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with host OR host AND path",
|
||||
rule: `Host("tchouk") || Host("pouet") && Path("/powpow")`,
|
||||
rule: `Host("example.com") || Host("example.org") && Path("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusOK,
|
||||
"http://pouet/powpow": http.StatusOK,
|
||||
"http://pouet/toto": http.StatusNotFound,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.org/js": http.StatusNotFound,
|
||||
"https://example.eu/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with (host OR host) AND path",
|
||||
rule: `(Host("tchouk") || Host("pouet")) && Path("/powpow")`,
|
||||
rule: `(Host("example.com") || Host("example.org")) && Path("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
"http://tchouk/powpow": http.StatusOK,
|
||||
"http://pouet/powpow": http.StatusOK,
|
||||
"http://pouet/toto": http.StatusNotFound,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with multiple host AND path",
|
||||
rule: `(Host("tchouk","pouet")) && Path("/powpow")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
"http://tchouk/powpow": http.StatusOK,
|
||||
"http://pouet/powpow": http.StatusOK,
|
||||
"http://pouet/toto": http.StatusNotFound,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with multiple host AND multiple path",
|
||||
rule: `Host("tchouk","pouet") && Path("/powpow", "/titi")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
"http://tchouk/powpow": http.StatusOK,
|
||||
"http://pouet/powpow": http.StatusOK,
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://pouet/titi": http.StatusOK,
|
||||
"http://pouet/toto": http.StatusNotFound,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.org/js": http.StatusNotFound,
|
||||
"https://example.eu/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with (host AND path) OR (host AND path)",
|
||||
rule: `(Host("tchouk") && Path("/titi")) || ((Host("pouet")) && Path("/powpow"))`,
|
||||
rule: `(Host("example.com") && Path("/js")) || ((Host("example.org")) && Path("/css"))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"http://pouet/powpow": http.StatusOK,
|
||||
"http://pouet/toto": http.StatusNotFound,
|
||||
"http://plopi/a": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusNotFound,
|
||||
"https://example.com/js": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.org/js": http.StatusNotFound,
|
||||
"https://example.eu/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule without quote",
|
||||
rule: `Host(tchouk)`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule case UPPER",
|
||||
rule: `(HOST("tchouk") && PATHPREFIX("/titi"))`,
|
||||
rule: `PATHPREFIX("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule case lower",
|
||||
rule: `(host("tchouk") && pathprefix("/titi"))`,
|
||||
rule: `pathprefix("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule case CamelCase",
|
||||
rule: `(Host("tchouk") && PathPrefix("/titi"))`,
|
||||
rule: `PathPrefix("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule case Title",
|
||||
rule: `(Host("tchouk") && Pathprefix("/titi"))`,
|
||||
rule: `Pathprefix("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule Path with error",
|
||||
rule: `Path("titi")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule PathPrefix with error",
|
||||
rule: `PathPrefix("titi")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule HostRegexp with error",
|
||||
rule: `HostRegexp("{test")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule Headers with error",
|
||||
rule: `Headers("titi")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule HeadersRegexp with error",
|
||||
rule: `HeadersRegexp("titi")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule Query",
|
||||
rule: `Query("titi")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule Query with bad syntax",
|
||||
rule: `Query("titi={test")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule with Path without args",
|
||||
rule: `Host("tchouk") && Path()`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule with an empty path",
|
||||
rule: `Host("tchouk") && Path("")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule with an empty path",
|
||||
rule: `Host("tchouk") && Path("", "/titi")`,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Rule with not",
|
||||
rule: `!Host("tchouk")`,
|
||||
rule: `!Host("example.com")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://test/powpow": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with not on Path",
|
||||
rule: `!Path("/titi")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://tchouk/powpow": http.StatusOK,
|
||||
"https://example.org": http.StatusOK,
|
||||
"https://example.com": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with not on multiple route with or",
|
||||
rule: `!(Host("tchouk") || Host("toto"))`,
|
||||
rule: `!(Host("example.com") || Host("example.org"))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://toto/powpow": http.StatusNotFound,
|
||||
"http://test/powpow": http.StatusOK,
|
||||
"https://example.eu/js": http.StatusOK,
|
||||
"https://example.com/css": http.StatusNotFound,
|
||||
"https://example.org/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with not on multiple route with and",
|
||||
rule: `!(Host("tchouk") && Path("/titi"))`,
|
||||
rule: `!(Host("example.com") && Path("/css"))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"http://test/titi": http.StatusOK,
|
||||
"https://example.com/js": http.StatusOK,
|
||||
"https://example.eu/css": http.StatusOK,
|
||||
"https://example.com/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with not on multiple route with and another not",
|
||||
rule: `!(Host("tchouk") && !Path("/titi"))`,
|
||||
rule: `!(Host("example.com") && !Path("/css"))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://toto/titi": http.StatusOK,
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with not on two rule",
|
||||
rule: `!Host("tchouk") || !Path("/titi")`,
|
||||
rule: `!Host("example.com") || !Path("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"http://test/titi": http.StatusOK,
|
||||
"https://example.com/js": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.com/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule case with double not",
|
||||
rule: `!(!(Host("tchouk") && Pathprefix("/titi")))`,
|
||||
rule: `!(!(Host("example.com") && Pathprefix("/css")))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"http://test/titi": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
"https://example.org/css": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule case with not domain",
|
||||
rule: `!Host("tchouk") && Pathprefix("/titi")`,
|
||||
rule: `!Host("example.com") && Pathprefix("/css")`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"http://toto/powpow": http.StatusNotFound,
|
||||
"http://toto/titi": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.org/js": http.StatusNotFound,
|
||||
"https://example.com/css": http.StatusNotFound,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Rule with multiple host AND multiple path AND not",
|
||||
rule: `!(Host("tchouk","pouet") && Path("/powpow", "/titi"))`,
|
||||
rule: `!(Host("example.com") && Path("/js"))`,
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"http://tchouk/powpow": http.StatusNotFound,
|
||||
"http://pouet/powpow": http.StatusNotFound,
|
||||
"http://tchouk/titi": http.StatusNotFound,
|
||||
"http://pouet/titi": http.StatusNotFound,
|
||||
"http://pouet/toto": http.StatusOK,
|
||||
"http://plopi/a": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "ClientIP empty",
|
||||
rule: "ClientIP(``)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ClientIP",
|
||||
rule: "ClientIP(`invalid`)",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Non matching ClientIP",
|
||||
rule: "ClientIP(`10.10.1.1`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non matching IPv6",
|
||||
rule: "ClientIP(`10::10`)",
|
||||
remoteAddr: "::1",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IP",
|
||||
rule: "ClientIP(`10.0.0.0`)",
|
||||
remoteAddr: "10.0.0.0:8456",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IPv6",
|
||||
rule: "ClientIP(`10::10`)",
|
||||
remoteAddr: "10::10",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IP among several IP",
|
||||
rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non Matching IP with CIDR",
|
||||
rule: "ClientIP(`11.0.0.0/24`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non Matching IPv6 with CIDR",
|
||||
rule: "ClientIP(`11::/16`)",
|
||||
remoteAddr: "10::",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusNotFound,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IP with CIDR",
|
||||
rule: "ClientIP(`10.0.0.0/16`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IPv6 with CIDR",
|
||||
rule: "ClientIP(`10::/16`)",
|
||||
remoteAddr: "10::10",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IP among several CIDR",
|
||||
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IP among non matching CIDR and matching IP",
|
||||
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Matching IP among matching CIDR and non matching IP",
|
||||
rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
|
||||
remoteAddr: "10.0.0.0",
|
||||
expected: map[string]int{
|
||||
"http://tchouk/toto": http.StatusOK,
|
||||
"https://example.com/js": http.StatusNotFound,
|
||||
"https://example.com/html": http.StatusOK,
|
||||
"https://example.org/js": http.StatusOK,
|
||||
"https://example.com/css": http.StatusOK,
|
||||
"https://example.org/css": http.StatusOK,
|
||||
"https://example.org/html": http.StatusOK,
|
||||
"https://example.eu/images": http.StatusOK,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -644,36 +213,37 @@ func Test_addRoute(t *testing.T) {
|
|||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
err = muxer.AddRoute(test.rule, 0, handler)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
// RequestDecorator is necessary for the host rule
|
||||
reqHost := requestdecorator.New(nil)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil)
|
||||
|
||||
// Useful for the ClientIP matcher
|
||||
req.RemoteAddr = test.remoteAddr
|
||||
|
||||
for key, value := range test.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
assert.Equal(t, test.expected, results)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// RequestDecorator is necessary for the host rule
|
||||
reqHost := requestdecorator.New(nil)
|
||||
|
||||
results := make(map[string]int)
|
||||
for calledURL := range test.expected {
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||
|
||||
// Useful for the ClientIP matcher
|
||||
req.RemoteAddr = test.remoteAddr
|
||||
|
||||
for key, value := range test.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
|
||||
results[calledURL] = w.Code
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -813,7 +383,7 @@ func Test_addRoutePriority(t *testing.T) {
|
|||
muxer.SortRoutes()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, test.path, http.NoBody)
|
||||
|
||||
muxer.ServeHTTP(w, req)
|
||||
|
||||
|
@ -822,86 +392,6 @@ func Test_addRoutePriority(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHostRegexp(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
hostExp string
|
||||
urls map[string]bool
|
||||
}{
|
||||
{
|
||||
desc: "capturing group",
|
||||
hostExp: "{subdomain:(foo\\.)?bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://foo.bar.com": true,
|
||||
"http://bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "non capturing group",
|
||||
hostExp: "{subdomain:(?:foo\\.)?bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://foo.bar.com": true,
|
||||
"http://bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "regex insensitive",
|
||||
hostExp: "{dummy:[A-Za-z-]+\\.bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://FOO.bar.com": true,
|
||||
"http://foo.bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "insensitive host",
|
||||
hostExp: "{dummy:[a-z-]+\\.bar\\.com}",
|
||||
urls: map[string]bool{
|
||||
"http://FOO.bar.com": true,
|
||||
"http://foo.bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "insensitive host simple",
|
||||
hostExp: "foo.bar.com",
|
||||
urls: map[string]bool{
|
||||
"http://FOO.bar.com": true,
|
||||
"http://foo.bar.com": true,
|
||||
"http://fooubar.com": false,
|
||||
"http://barucom": false,
|
||||
"http://barcom": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rt := &mux.Route{}
|
||||
err := hostRegexp(rt, test.hostExp)
|
||||
require.NoError(t, err)
|
||||
|
||||
for testURL, match := range test.urls {
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
|
||||
assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}), testURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDomains(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
|
@ -914,21 +404,6 @@ func TestParseDomains(t *testing.T) {
|
|||
expression: "Foobar(`foo.bar`,`test.bar`)",
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
description: "Several host rules",
|
||||
expression: "Host(`foo.bar`,`test.bar`)",
|
||||
domain: []string{"foo.bar", "test.bar"},
|
||||
},
|
||||
{
|
||||
description: "Several host rules upper",
|
||||
expression: "HOST(`foo.bar`,`test.bar`)",
|
||||
domain: []string{"foo.bar", "test.bar"},
|
||||
},
|
||||
{
|
||||
description: "Several host rules lower",
|
||||
expression: "host(`foo.bar`,`test.bar`)",
|
||||
domain: []string{"foo.bar", "test.bar"},
|
||||
},
|
||||
{
|
||||
description: "No host rule",
|
||||
expression: "Path(`/test`)",
|
||||
|
@ -938,6 +413,11 @@ func TestParseDomains(t *testing.T) {
|
|||
expression: "Host(`foo.bar`) && Path(`/test`)",
|
||||
domain: []string{"foo.bar"},
|
||||
},
|
||||
{
|
||||
description: "Host rule to trim and another rule",
|
||||
expression: "Host(`Foo.Bar`) || Host(`bar.buz`) && Path(`/test`)",
|
||||
domain: []string{"foo.bar", "bar.buz"},
|
||||
},
|
||||
{
|
||||
description: "Host rule to trim and another rule",
|
||||
expression: "Host(`Foo.Bar`) && Path(`/test`)",
|
||||
|
@ -967,7 +447,9 @@ func TestParseDomains(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAbsoluteFormURL(t *testing.T) {
|
||||
// TestEmptyHost is a non regression test for
|
||||
// https://github.com/traefik/traefik/pull/9131
|
||||
func TestEmptyHost(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
request string
|
||||
|
@ -975,41 +457,41 @@ func TestAbsoluteFormURL(t *testing.T) {
|
|||
expected int
|
||||
}{
|
||||
{
|
||||
desc: "!HostRegexp with absolute-form URL with empty host with non-matching host header",
|
||||
request: "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
|
||||
rule: "!HostRegexp(`test.localhost`)",
|
||||
expected: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "!Host with absolute-form URL with empty host with non-matching host header",
|
||||
request: "GET http://@/ HTTP/1.1\r\nHost: test.localhost\r\n\r\n",
|
||||
rule: "!Host(`test.localhost`)",
|
||||
expected: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "!HostRegexp with absolute-form URL with matching host header",
|
||||
request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
|
||||
rule: "!HostRegexp(`test.localhost`)",
|
||||
expected: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "!Host with absolute-form URL with matching host header",
|
||||
request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
|
||||
rule: "!Host(`test.localhost`)",
|
||||
expected: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "!HostRegexp with absolute-form URL with non-matching host header",
|
||||
request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
|
||||
rule: "!HostRegexp(`toto.localhost`)",
|
||||
desc: "HostRegexp with absolute-form URL with empty host with non-matching host header",
|
||||
request: "GET http://@/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
rule: "HostRegexp(`example.com`)",
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "!Host with absolute-form URL with non-matching host header",
|
||||
request: "GET http://test.localhost/ HTTP/1.1\r\nHost: toto.localhost\r\n\r\n",
|
||||
rule: "!Host(`toto.localhost`)",
|
||||
desc: "Host with absolute-form URL with empty host with non-matching host header",
|
||||
request: "GET http://@/ HTTP/1.1\r\nHost: example.com\r\n\r\n",
|
||||
rule: "Host(`example.com`)",
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "HostRegexp with absolute-form URL with matching host header",
|
||||
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
|
||||
rule: "HostRegexp(`example.com`)",
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "Host with absolute-form URL with matching host header",
|
||||
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
|
||||
rule: "Host(`example.com`)",
|
||||
expected: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "HostRegexp with absolute-form URL with non-matching host header",
|
||||
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
|
||||
rule: "HostRegexp(`example.org`)",
|
||||
expected: http.StatusNotFound,
|
||||
},
|
||||
{
|
||||
desc: "Host with absolute-form URL with non-matching host header",
|
||||
request: "GET http://example.com/ HTTP/1.1\r\nHost: example.org\r\n\r\n",
|
||||
rule: "Host(`example.org`)",
|
||||
expected: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
|
134
pkg/muxer/tcp/matcher.go
Normal file
134
pkg/muxer/tcp/matcher.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v2/pkg/ip"
|
||||
)
|
||||
|
||||
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
|
||||
"ALPN": expect1Parameter(alpn),
|
||||
"ClientIP": expect1Parameter(clientIP),
|
||||
"HostSNI": expect1Parameter(hostSNI),
|
||||
"HostSNIRegexp": expect1Parameter(hostSNIRegexp),
|
||||
}
|
||||
|
||||
func expect1Parameter(fn func(*matchersTree, ...string) error) func(*matchersTree, ...string) error {
|
||||
return func(route *matchersTree, s ...string) error {
|
||||
if len(s) != 1 {
|
||||
return fmt.Errorf("unexpected number of parameters; got %d, expected 1", len(s))
|
||||
}
|
||||
|
||||
return fn(route, s...)
|
||||
}
|
||||
}
|
||||
|
||||
// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
|
||||
func alpn(tree *matchersTree, protos ...string) error {
|
||||
proto := protos[0]
|
||||
|
||||
if proto == tlsalpn01.ACMETLS1Protocol {
|
||||
return fmt.Errorf("invalid protocol value for ALPN matcher, %q is not allowed", proto)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
for _, alpnProto := range meta.alpnProtos {
|
||||
if alpnProto == proto {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func clientIP(tree *matchersTree, clientIP ...string) error {
|
||||
checker, err := ip.NewChecker(clientIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing IP checker for ClientIP matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
ok, err := checker.Contains(meta.remoteIP)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("ClientIP matcher: could not match remote address")
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
|
||||
|
||||
// hostSNI checks if the SNI Host of the connection match the matcher host.
|
||||
func hostSNI(tree *matchersTree, hosts ...string) error {
|
||||
host := hosts[0]
|
||||
|
||||
if host == "*" {
|
||||
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
|
||||
// it allows matching with an empty serverName.
|
||||
tree.matcher = func(meta ConnData) bool { return true }
|
||||
return nil
|
||||
}
|
||||
|
||||
if !almostFQDN.MatchString(host) {
|
||||
return fmt.Errorf("invalid value for HostSNI matcher, %q is not a valid hostname", host)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
if meta.serverName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if host == meta.serverName {
|
||||
return true
|
||||
}
|
||||
|
||||
// trim trailing period in case of FQDN
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
|
||||
return host == meta.serverName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
|
||||
func hostSNIRegexp(tree *matchersTree, templates ...string) error {
|
||||
template := templates[0]
|
||||
|
||||
if !isASCII(template) {
|
||||
return fmt.Errorf("invalid value for HostSNIRegexp matcher, %q is not a valid hostname", template)
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling HostSNIRegexp matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
return re.MatchString(meta.serverName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isASCII checks if the given string contains only ASCII characters.
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
383
pkg/muxer/tcp/matcher_test.go
Normal file
383
pkg/muxer/tcp/matcher_test.go
Normal file
|
@ -0,0 +1,383 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/tcp"
|
||||
)
|
||||
|
||||
func Test_HostSNICatchAll(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
isCatchAll bool
|
||||
}{
|
||||
{
|
||||
desc: "HostSNI(`example.com`) is not catchAll",
|
||||
rule: "HostSNI(`example.com`)",
|
||||
},
|
||||
{
|
||||
desc: "HostSNI(`*`) is catchAll",
|
||||
rule: "HostSNI(`*`)",
|
||||
isCatchAll: true,
|
||||
},
|
||||
{
|
||||
desc: "HostSNIRegexp(`^.*$`) is not catchAll",
|
||||
rule: "HostSNIRegexp(`.*`)",
|
||||
isCatchAll: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
require.NoError(t, err)
|
||||
|
||||
handler, catchAll := muxer.Match(ConnData{
|
||||
serverName: "example.com",
|
||||
})
|
||||
require.NotNil(t, handler)
|
||||
assert.Equal(t, test.isCatchAll, catchAll)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HostSNI(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
serverName string
|
||||
buildErr bool
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNI matcher (empty host)",
|
||||
rule: "HostSNI(``)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNI matcher (too many parameters)",
|
||||
rule: "HostSNI(`example.com`, `example.org`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNI matcher (globing sub domain)",
|
||||
rule: "HostSNI(`*.com`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNI matcher (non ASCII host)",
|
||||
rule: "HostSNI(`🦭.com`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI matcher - puny-coded emoji",
|
||||
rule: "HostSNI(`xn--9t9h.com`)",
|
||||
serverName: "xn--9t9h.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid HostSNI matcher - puny-coded emoji but emoji in server name",
|
||||
rule: "HostSNI(`xn--9t9h.com`)",
|
||||
serverName: "🦭.com",
|
||||
},
|
||||
{
|
||||
desc: "Matching hosts",
|
||||
rule: "HostSNI(`example.com`)",
|
||||
serverName: "example.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
desc: "No matching hosts",
|
||||
rule: "HostSNI(`example.com`)",
|
||||
serverName: "example.org",
|
||||
},
|
||||
{
|
||||
desc: "Matching globing host `*`",
|
||||
rule: "HostSNI(`*`)",
|
||||
serverName: "example.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching globing host `*` and empty server name",
|
||||
rule: "HostSNI(`*`)",
|
||||
serverName: "",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching host with trailing dot",
|
||||
rule: "HostSNI(`example.com.`)",
|
||||
serverName: "example.com.",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching host with trailing dot but not in server name",
|
||||
rule: "HostSNI(`example.com.`)",
|
||||
serverName: "example.com",
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
desc: "Matching hosts with subdomains",
|
||||
rule: "HostSNI(`foo.example.com`)",
|
||||
serverName: "foo.example.com",
|
||||
match: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
meta := ConnData{
|
||||
serverName: test.serverName,
|
||||
}
|
||||
|
||||
handler, _ := muxer.Match(meta)
|
||||
require.Equal(t, test.match, handler != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_HostSNIRegexp(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]bool
|
||||
buildErr bool
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNIRegexp matcher (empty host)",
|
||||
rule: "HostSNIRegexp(``)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNIRegexp matcher (non ASCII host)",
|
||||
rule: "HostSNIRegexp(`🦭.com`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNIRegexp matcher (invalid regexp)",
|
||||
rule: "HostSNIRegexp(`(example.com`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid HostSNIRegexp matcher (too many parameters)",
|
||||
rule: "HostSNIRegexp(`example.com`, `example.org`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "valid HostSNIRegexp matcher",
|
||||
rule: "HostSNIRegexp(`^example\\.(com|org)$`)",
|
||||
expected: map[string]bool{
|
||||
"example.com": true,
|
||||
"example.com.": false,
|
||||
"EXAMPLE.com": false,
|
||||
"example.org": true,
|
||||
"exampleuorg": false,
|
||||
"": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid HostSNIRegexp matcher with Traefik v2 syntax",
|
||||
rule: "HostSNIRegexp(`example.{tld:(com|org)}`)",
|
||||
expected: map[string]bool{
|
||||
"example.com": false,
|
||||
"example.com.": false,
|
||||
"EXAMPLE.com": false,
|
||||
"example.org": false,
|
||||
"exampleuorg": false,
|
||||
"": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for serverName, match := range test.expected {
|
||||
meta := ConnData{
|
||||
serverName: serverName,
|
||||
}
|
||||
|
||||
handler, _ := muxer.Match(meta)
|
||||
assert.Equal(t, match, handler != nil, serverName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ClientIP(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]bool
|
||||
buildErr bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ClientIP matcher (empty host)",
|
||||
rule: "ClientIP(``)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ClientIP matcher (non ASCII host)",
|
||||
rule: "ClientIP(`🦭/32`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ClientIP matcher (too many parameters)",
|
||||
rule: "ClientIP(`127.0.0.1`, `127.0.0.2`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "valid ClientIP matcher",
|
||||
rule: "ClientIP(`20.20.20.20`)",
|
||||
expected: map[string]bool{
|
||||
"20.20.20.20": true,
|
||||
"10.10.10.10": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "valid ClientIP matcher with CIDR",
|
||||
rule: "ClientIP(`20.20.20.20/24`)",
|
||||
expected: map[string]bool{
|
||||
"20.20.20.20": true,
|
||||
"20.20.20.40": true,
|
||||
"10.10.10.10": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for remoteIP, match := range test.expected {
|
||||
meta := ConnData{
|
||||
remoteIP: remoteIP,
|
||||
}
|
||||
|
||||
handler, _ := muxer.Match(meta)
|
||||
assert.Equal(t, match, handler != nil, remoteIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ALPN(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
rule string
|
||||
expected map[string]bool
|
||||
buildErr bool
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ALPN matcher (TLS proto)",
|
||||
rule: "ALPN(`acme-tls/1`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ALPN matcher (empty parameters)",
|
||||
rule: "ALPN(``)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid ALPN matcher (too many parameters)",
|
||||
rule: "ALPN(`h2`, `mqtt`)",
|
||||
buildErr: true,
|
||||
},
|
||||
{
|
||||
desc: "Valid ALPN matcher",
|
||||
rule: "ALPN(`h2`)",
|
||||
expected: map[string]bool{
|
||||
"h2": true,
|
||||
"mqtt": false,
|
||||
"": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
muxer, err := NewMuxer()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.AddRoute(test.rule, 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {}))
|
||||
if test.buildErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for proto, match := range test.expected {
|
||||
meta := ConnData{
|
||||
alpnProtos: []string{proto},
|
||||
}
|
||||
|
||||
handler, _ := muxer.Match(meta)
|
||||
assert.Equal(t, match, handler != nil, proto)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,31 +1,18 @@
|
|||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v2/pkg/ip"
|
||||
"github.com/traefik/traefik/v2/pkg/rules"
|
||||
"github.com/traefik/traefik/v2/pkg/tcp"
|
||||
"github.com/traefik/traefik/v2/pkg/types"
|
||||
"github.com/vulcand/predicate"
|
||||
)
|
||||
|
||||
var tcpFuncs = map[string]func(*matchersTree, ...string) error{
|
||||
"HostSNI": hostSNI,
|
||||
"HostSNIRegexp": hostSNIRegexp,
|
||||
"ClientIP": clientIP,
|
||||
"ALPN": alpn,
|
||||
}
|
||||
|
||||
// ParseHostSNI extracts the HostSNIs declared in a rule.
|
||||
// This is a first naive implementation used in TCP routing.
|
||||
func ParseHostSNI(rule string) ([]string, error) {
|
||||
|
@ -261,233 +248,7 @@ func (m *matchersTree) match(meta ConnData) bool {
|
|||
return m.left.match(meta) && m.right.match(meta)
|
||||
default:
|
||||
// This should never happen as it should have been detected during parsing.
|
||||
log.Warn().Msgf("Invalid rule operator %s", m.operator)
|
||||
log.Warn().Str("operator", m.operator).Msg("Invalid rule operator")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func clientIP(tree *matchersTree, clientIPs ...string) error {
|
||||
checker, err := ip.NewChecker(clientIPs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
if meta.remoteIP == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
ok, err := checker.Contains(meta.remoteIP)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("\"ClientIP\" matcher: could not match remote address")
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// alpn checks if any of the connection ALPN protocols matches one of the matcher protocols.
|
||||
func alpn(tree *matchersTree, protos ...string) error {
|
||||
if len(protos) == 0 {
|
||||
return errors.New("empty value for \"ALPN\" matcher is not allowed")
|
||||
}
|
||||
|
||||
for _, proto := range protos {
|
||||
if proto == tlsalpn01.ACMETLS1Protocol {
|
||||
return fmt.Errorf("invalid protocol value for \"ALPN\" matcher, %q is not allowed", proto)
|
||||
}
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
for _, proto := range meta.alpnProtos {
|
||||
for _, filter := range protos {
|
||||
if proto == filter {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var almostFQDN = regexp.MustCompile(`^[[:alnum:]\.-]+$`)
|
||||
|
||||
// hostSNI checks if the SNI Host of the connection match the matcher host.
|
||||
func hostSNI(tree *matchersTree, hosts ...string) error {
|
||||
if len(hosts) == 0 {
|
||||
return errors.New("empty value for \"HostSNI\" matcher is not allowed")
|
||||
}
|
||||
|
||||
for i, host := range hosts {
|
||||
// Special case to allow global wildcard
|
||||
if host == "*" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !almostFQDN.MatchString(host) {
|
||||
return fmt.Errorf("invalid value for \"HostSNI\" matcher, %q is not a valid hostname", host)
|
||||
}
|
||||
|
||||
hosts[i] = strings.ToLower(host)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
// Since a HostSNI(`*`) rule has been provided as catchAll for non-TLS TCP,
|
||||
// it allows matching with an empty serverName.
|
||||
// Which is why we make sure to take that case into account before
|
||||
// checking meta.serverName.
|
||||
if hosts[0] == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if meta.serverName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, host := range hosts {
|
||||
if host == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if host == meta.serverName {
|
||||
return true
|
||||
}
|
||||
|
||||
// trim trailing period in case of FQDN
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
if host == meta.serverName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hostSNIRegexp checks if the SNI Host of the connection matches the matcher host regexp.
|
||||
func hostSNIRegexp(tree *matchersTree, templates ...string) error {
|
||||
if len(templates) == 0 {
|
||||
return fmt.Errorf("empty value for \"HostSNIRegexp\" matcher is not allowed")
|
||||
}
|
||||
|
||||
var regexps []*regexp.Regexp
|
||||
|
||||
for _, template := range templates {
|
||||
preparedPattern, err := preparePattern(template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pattern value for \"HostSNIRegexp\" matcher, %q is not a valid pattern: %w", template, err)
|
||||
}
|
||||
|
||||
regexp, err := regexp.Compile(preparedPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regexps = append(regexps, regexp)
|
||||
}
|
||||
|
||||
tree.matcher = func(meta ConnData) bool {
|
||||
for _, regexp := range regexps {
|
||||
if regexp.MatchString(meta.serverName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: expose more of containous/mux fork to get rid of the following copied code (https://github.com/containous/mux/blob/8ffa4f6d063c/regexp.go).
|
||||
|
||||
// preparePattern builds a regexp pattern from the initial user defined expression.
|
||||
// This function reuses the code dedicated to host matching of the newRouteRegexp func from the gorilla/mux library.
|
||||
// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
|
||||
func preparePattern(template string) (string, error) {
|
||||
// Check if it is well-formed.
|
||||
idxs, errBraces := braceIndices(template)
|
||||
if errBraces != nil {
|
||||
return "", errBraces
|
||||
}
|
||||
|
||||
defaultPattern := "[^.]+"
|
||||
pattern := bytes.NewBufferString("")
|
||||
|
||||
// Host SNI matching is case-insensitive
|
||||
fmt.Fprint(pattern, "(?i)")
|
||||
|
||||
pattern.WriteByte('^')
|
||||
var end int
|
||||
var err error
|
||||
for i := 0; i < len(idxs); i += 2 {
|
||||
// Set all values we are interested in.
|
||||
raw := template[end:idxs[i]]
|
||||
end = idxs[i+1]
|
||||
parts := strings.SplitN(template[idxs[i]+1:end-1], ":", 2)
|
||||
name := parts[0]
|
||||
patt := defaultPattern
|
||||
if len(parts) == 2 {
|
||||
patt = parts[1]
|
||||
}
|
||||
// Name or pattern can't be empty.
|
||||
if name == "" || patt == "" {
|
||||
return "", fmt.Errorf("mux: missing name or pattern in %q",
|
||||
template[idxs[i]:end])
|
||||
}
|
||||
// Build the regexp pattern.
|
||||
fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt)
|
||||
|
||||
// Append variable name and compiled pattern.
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Add the remaining.
|
||||
raw := template[end:]
|
||||
pattern.WriteString(regexp.QuoteMeta(raw))
|
||||
pattern.WriteByte('$')
|
||||
|
||||
return pattern.String(), nil
|
||||
}
|
||||
|
||||
// varGroupName builds a capturing group name for the indexed variable.
|
||||
// This function is a copy of varGroupName func from the gorilla/mux library.
|
||||
// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
|
||||
func varGroupName(idx int) string {
|
||||
return "v" + strconv.Itoa(idx)
|
||||
}
|
||||
|
||||
// braceIndices returns the first level curly brace indices from a string.
|
||||
// This function is a copy of braceIndices func from the gorilla/mux library.
|
||||
// https://github.com/containous/mux/tree/8ffa4f6d063c1e2b834a73be6a1515cca3992618.
|
||||
func braceIndices(s string) ([]int, error) {
|
||||
var level, idx int
|
||||
var idxs []int
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '{':
|
||||
if level++; level == 1 {
|
||||
idx = i
|
||||
}
|
||||
case '}':
|
||||
if level--; level == 0 {
|
||||
idxs = append(idxs, idx, i+1)
|
||||
} else if level < 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if level != 0 {
|
||||
return nil, fmt.Errorf("mux: unbalanced braces in %q", s)
|
||||
}
|
||||
return idxs, nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -101,7 +101,7 @@ func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int,
|
|||
cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
|
||||
EntryPoints: []string{ep},
|
||||
Service: "traefik-hub-agent-service",
|
||||
Rule: "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
|
||||
Rule: "Host(`proxy.traefik`) && (PathPrefix(`/config`) || PathPrefix(`/discover-ip`) || PathPrefix(`/state`))",
|
||||
}
|
||||
|
||||
cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -1163,8 +1164,7 @@ func getRouteBindingSelectorNamespace(client Client, gatewayNamespace string, ro
|
|||
}
|
||||
|
||||
func hostRule(hostnames []v1alpha2.Hostname) (string, error) {
|
||||
var hostNames []string
|
||||
var hostRegexNames []string
|
||||
var rules []string
|
||||
|
||||
for _, hostname := range hostnames {
|
||||
host := string(hostname)
|
||||
|
@ -1177,7 +1177,7 @@ func hostRule(hostnames []v1alpha2.Hostname) (string, error) {
|
|||
|
||||
wildcard := strings.Count(host, "*")
|
||||
if wildcard == 0 {
|
||||
hostNames = append(hostNames, host)
|
||||
rules = append(rules, fmt.Sprintf("Host(`%s`)", host))
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -1186,25 +1186,18 @@ func hostRule(hostnames []v1alpha2.Hostname) (string, error) {
|
|||
return "", fmt.Errorf("invalid rule: %q", host)
|
||||
}
|
||||
|
||||
hostRegexNames = append(hostRegexNames, strings.Replace(host, "*.", "{subdomain:[a-zA-Z0-9-]+}.", 1))
|
||||
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1)
|
||||
rules = append(rules, fmt.Sprintf("HostRegexp(`^%s$`)", host))
|
||||
}
|
||||
|
||||
var res string
|
||||
if len(hostNames) > 0 {
|
||||
res = "Host(`" + strings.Join(hostNames, "`, `") + "`)"
|
||||
switch len(rules) {
|
||||
case 0:
|
||||
return "", nil
|
||||
case 1:
|
||||
return rules[0], nil
|
||||
default:
|
||||
return fmt.Sprintf("(%s)", strings.Join(rules, " || ")), nil
|
||||
}
|
||||
|
||||
if len(hostRegexNames) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
hostRegexp := "HostRegexp(`" + strings.Join(hostRegexNames, "`, `") + "`)"
|
||||
|
||||
if len(res) > 0 {
|
||||
return "(" + res + " || " + hostRegexp + ")", nil
|
||||
}
|
||||
|
||||
return hostRegexp, nil
|
||||
}
|
||||
|
||||
func hostSNIRule(hostnames []v1alpha2.Hostname) (string, error) {
|
||||
|
@ -1227,15 +1220,18 @@ func hostSNIRule(hostnames []v1alpha2.Hostname) (string, error) {
|
|||
return "", fmt.Errorf("wildcard hostname is not supported: %q", h)
|
||||
}
|
||||
|
||||
matchers = append(matchers, "`"+h+"`")
|
||||
matchers = append(matchers, fmt.Sprintf("HostSNI(`%s`)", h))
|
||||
uniqHostnames[hostname] = struct{}{}
|
||||
}
|
||||
|
||||
if len(matchers) == 0 {
|
||||
switch len(matchers) {
|
||||
case 0:
|
||||
return "HostSNI(`*`)", nil
|
||||
case 1:
|
||||
return matchers[0], nil
|
||||
default:
|
||||
return fmt.Sprintf("(%s)", strings.Join(matchers, " || ")), nil
|
||||
}
|
||||
|
||||
return "HostSNI(" + strings.Join(matchers, ",") + ")", nil
|
||||
}
|
||||
|
||||
func extractRule(routeRule v1alpha2.HTTPRouteRule, hostRule string) (string, error) {
|
||||
|
|
|
@ -744,15 +744,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
|||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-http-app-1-my-gateway-web-75dd1ad561e42725558a": {
|
||||
"default-http-app-1-my-gateway-web-66e726cd8903b49727ae": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-http-app-1-my-gateway-web-75dd1ad561e42725558a-wrr",
|
||||
Rule: "Host(`foo.com`, `bar.com`) && PathPrefix(`/`)",
|
||||
Service: "default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr",
|
||||
Rule: "(Host(`foo.com`) || Host(`bar.com`)) && PathPrefix(`/`)",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-http-app-1-my-gateway-web-75dd1ad561e42725558a-wrr": {
|
||||
"default-http-app-1-my-gateway-web-66e726cd8903b49727ae-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
|
@ -802,15 +802,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
|||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-http-app-1-my-gateway-web-2dbd7883f5537db39bca": {
|
||||
"default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-http-app-1-my-gateway-web-2dbd7883f5537db39bca-wrr",
|
||||
Rule: "(Host(`foo.com`) || HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar.com`)) && PathPrefix(`/`)",
|
||||
Service: "default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr",
|
||||
Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.com$`)) && PathPrefix(`/`)",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-http-app-1-my-gateway-web-2dbd7883f5537db39bca-wrr": {
|
||||
"default-http-app-1-my-gateway-web-3b78e2feb3295ddd87f0-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
|
@ -860,15 +860,15 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
|||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-http-app-1-my-gateway-web-eb1490f180299bf5ed29": {
|
||||
"default-http-app-1-my-gateway-web-b0521a61fb43068694b4": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-http-app-1-my-gateway-web-eb1490f180299bf5ed29-wrr",
|
||||
Rule: "(Host(`foo.com`) || HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.foo.com`)) && PathPrefix(`/`)",
|
||||
Service: "default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr",
|
||||
Rule: "(Host(`foo.com`) || HostRegexp(`^[a-zA-Z0-9-]+\\.foo\\.com$`)) && PathPrefix(`/`)",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-http-app-1-my-gateway-web-eb1490f180299bf5ed29-wrr": {
|
||||
"default-http-app-1-my-gateway-web-b0521a61fb43068694b4-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
|
@ -3011,10 +3011,10 @@ func TestLoadTLSRoutes(t *testing.T) {
|
|||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{
|
||||
"default-tls-app-1-my-gateway-tls-339184c3296a9c2c39fa": {
|
||||
"default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7": {
|
||||
EntryPoints: []string{"tls"},
|
||||
Service: "default-tls-app-1-my-gateway-tls-339184c3296a9c2c39fa-wrr-0",
|
||||
Rule: "HostSNI(`foo.example.com`,`bar.example.com`)",
|
||||
Service: "default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7-wrr-0",
|
||||
Rule: "(HostSNI(`foo.example.com`) || HostSNI(`bar.example.com`))",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{
|
||||
Passthrough: true,
|
||||
},
|
||||
|
@ -3022,7 +3022,7 @@ func TestLoadTLSRoutes(t *testing.T) {
|
|||
},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{
|
||||
"default-tls-app-1-my-gateway-tls-339184c3296a9c2c39fa-wrr-0": {
|
||||
"default-tls-app-1-my-gateway-tls-dfc5c7506ac1b172c8b7-wrr-0": {
|
||||
Weighted: &dynamic.TCPWeightedRoundRobin{
|
||||
Services: []dynamic.TCPWRRService{
|
||||
{
|
||||
|
@ -4362,7 +4362,7 @@ func Test_hostRule(t *testing.T) {
|
|||
"Bar",
|
||||
"Bir",
|
||||
},
|
||||
expectedRule: "Host(`Foo`, `Bar`, `Bir`)",
|
||||
expectedRule: "(Host(`Foo`) || Host(`Bar`) || Host(`Bir`))",
|
||||
},
|
||||
{
|
||||
desc: "Multiple Hosts with empty one",
|
||||
|
@ -4389,14 +4389,14 @@ func Test_hostRule(t *testing.T) {
|
|||
"bar.foo",
|
||||
"foo.foo",
|
||||
},
|
||||
expectedRule: "(Host(`bar.foo`, `foo.foo`) || HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar.foo`))",
|
||||
expectedRule: "(HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.foo$`) || Host(`bar.foo`) || Host(`foo.foo`))",
|
||||
},
|
||||
{
|
||||
desc: "Host with wildcard",
|
||||
hostnames: []v1alpha2.Hostname{
|
||||
"*.bar.foo",
|
||||
},
|
||||
expectedRule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar.foo`)",
|
||||
expectedRule: "HostRegexp(`^[a-zA-Z0-9-]+\\.bar\\.foo$`)",
|
||||
},
|
||||
{
|
||||
desc: "Alone wildcard",
|
||||
|
@ -4708,7 +4708,7 @@ func Test_hostSNIRule(t *testing.T) {
|
|||
{
|
||||
desc: "Some empty hostnames",
|
||||
hostnames: []v1alpha2.Hostname{"foo", "", "bar"},
|
||||
expectedRule: "HostSNI(`foo`,`bar`)",
|
||||
expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`))",
|
||||
},
|
||||
{
|
||||
desc: "Valid hostname",
|
||||
|
@ -4718,12 +4718,12 @@ func Test_hostSNIRule(t *testing.T) {
|
|||
{
|
||||
desc: "Multiple valid hostnames",
|
||||
hostnames: []v1alpha2.Hostname{"foo", "bar"},
|
||||
expectedRule: "HostSNI(`foo`,`bar`)",
|
||||
expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`))",
|
||||
},
|
||||
{
|
||||
desc: "Multiple overlapping hostnames",
|
||||
hostnames: []v1alpha2.Hostname{"foo", "bar", "foo", "baz"},
|
||||
expectedRule: "HostSNI(`foo`,`bar`,`baz`)",
|
||||
expectedRule: "(HostSNI(`foo`) || HostSNI(`bar`) || HostSNI(`baz`))",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -400,10 +401,11 @@ func (p *Provider) shouldProcessIngress(ingress *networkingv1.Ingress, ingressCl
|
|||
|
||||
func buildHostRule(host string) string {
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
return "HostRegexp(`" + strings.Replace(host, "*.", "{subdomain:[a-zA-Z0-9-]+}.", 1) + "`)"
|
||||
host = strings.Replace(regexp.QuoteMeta(host), `\*\.`, `[a-zA-Z0-9-]+\.`, 1)
|
||||
return fmt.Sprintf("HostRegexp(`^%s$`)", host)
|
||||
}
|
||||
|
||||
return "Host(`" + host + "`)"
|
||||
return fmt.Sprintf("Host(`%s`)", host)
|
||||
}
|
||||
|
||||
func getCertificates(ctx context.Context, ingress *networkingv1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
|
||||
|
|
|
@ -189,8 +189,8 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
|||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"testing-bar-bar-3be6cfd7daba66cf2fdd": {
|
||||
Rule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.bar`) && PathPrefix(`/bar`)",
|
||||
"testing-bar-bar-aba9a7d00e9b06a78e16": {
|
||||
Rule: "HostRegexp(`^[a-zA-Z0-9-]+\\.bar$`) && PathPrefix(`/bar`)",
|
||||
Service: "testing-service1-80",
|
||||
},
|
||||
"testing-bar-bar-636bf36c00fedaab3d44": {
|
||||
|
@ -1104,7 +1104,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
|||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"testing-foobar-com-bar": {
|
||||
Rule: "HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.foobar.com`) && PathPrefix(`/bar`)",
|
||||
Rule: "HostRegexp(`^[a-zA-Z0-9-]+\\.foobar\\.com$`) && PathPrefix(`/bar`)",
|
||||
Service: "testing-service1-80",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"redirect-web-to-websecure"
|
||||
],
|
||||
"service": "noop@internal",
|
||||
"rule": "HostRegexp(`{host:.+}`)"
|
||||
"rule": "HostRegexp(`^.+$`)"
|
||||
}
|
||||
},
|
||||
"middlewares": {
|
||||
|
@ -27,4 +27,4 @@
|
|||
},
|
||||
"tcp": {},
|
||||
"tls": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"redirect-web-to-443"
|
||||
],
|
||||
"service": "noop@internal",
|
||||
"rule": "HostRegexp(`{host:.+}`)"
|
||||
"rule": "HostRegexp(`^.+$`)"
|
||||
}
|
||||
},
|
||||
"middlewares": {
|
||||
|
@ -27,4 +27,4 @@
|
|||
},
|
||||
"tcp": {},
|
||||
"tls": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"redirect-web-to-websecure"
|
||||
],
|
||||
"service": "noop@internal",
|
||||
"rule": "HostRegexp(`{host:.+}`)"
|
||||
"rule": "HostRegexp(`^.+$`)"
|
||||
}
|
||||
},
|
||||
"middlewares": {
|
||||
|
@ -27,4 +27,4 @@
|
|||
},
|
||||
"tcp": {},
|
||||
"tls": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ func (i *Provider) redirection(ctx context.Context, cfg *dynamic.Configuration)
|
|||
mdName := "redirect-" + rtName
|
||||
|
||||
rt := &dynamic.Router{
|
||||
Rule: "HostRegexp(`{host:.+}`)",
|
||||
Rule: "HostRegexp(`^.+$`)",
|
||||
EntryPoints: []string{name},
|
||||
Middlewares: []string{mdName},
|
||||
Service: "noop@internal",
|
||||
|
|
Loading…
Reference in a new issue