Rework Host and HostRegexp matchers
Co-authored-by: Simon Delicata <simon.delicata@traefik.io>
This commit is contained in:
parent
519ed8bde5
commit
8cf9385938
|
@ -244,14 +244,14 @@ The table below lists all the available matchers:
|
||||||
|
|
||||||
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
||||||
as well as parentheses.
|
as well as parentheses.
|
||||||
|
|
||||||
One can invert a matcher by using the NOT (`!`) operator.
|
One can invert a matcher by using the NOT (`!`) operator.
|
||||||
|
|
||||||
The following rule matches requests where:
|
The following rule matches requests where:
|
||||||
|
|
||||||
- either host is `example.com` OR,
|
- either host is `example.com` OR,
|
||||||
- host is `example.org` AND path is NOT `/traefik`
|
- host is `example.org` AND path is NOT `/traefik`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Host(`example.com`) || (Host(`example.org`) && !Path(`/traefik`))
|
Host(`example.com`) || (Host(`example.org`) && !Path(`/traefik`))
|
||||||
```
|
```
|
||||||
|
@ -261,21 +261,21 @@ The table below lists all the available matchers:
|
||||||
The `Header` and `HeaderRegexp` matchers allow to match requests that contain specific header.
|
The `Header` and `HeaderRegexp` matchers allow to match requests that contain specific header.
|
||||||
|
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match requests with a `Content-Type` header set to `application/yaml`:
|
Match requests with a `Content-Type` header set to `application/yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Header(`Content-Type`, `application/yaml`)
|
Header(`Content-Type`, `application/yaml`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match requests with a `Content-Type` header set to either `application/json` or `application/yaml`:
|
Match requests with a `Content-Type` header set to either `application/json` or `application/yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HeaderRegexp(`Content-Type`, `^application/(json|yaml)$`)
|
HeaderRegexp(`Content-Type`, `^application/(json|yaml)$`)
|
||||||
```
|
```
|
||||||
|
|
||||||
To match headers [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
|
To match headers [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity), use the `(?i)` option:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HeaderRegexp(`Content-Type`, `(?i)^application/(json|yaml)$`)
|
HeaderRegexp(`Content-Type`, `(?i)^application/(json|yaml)$`)
|
||||||
```
|
```
|
||||||
|
@ -288,22 +288,24 @@ These matchers do not support non-ASCII characters, use punycode encoded values
|
||||||
|
|
||||||
If no Host is set in the request URL (e.g., it's an IP address), these matchers will look at the `Host` header.
|
If no Host is set in the request URL (e.g., it's an IP address), these matchers will look at the `Host` header.
|
||||||
|
|
||||||
|
These matchers will match the request's host in lowercase.
|
||||||
|
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match requests with `Host` set to `example.com`:
|
Match requests with `Host` set to `example.com`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Host(`example.com`)
|
Host(`example.com`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match requests sent to any subdomain of `example.com`:
|
Match requests sent to any subdomain of `example.com`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HostRegexp(`^.+\.example\.com$`)
|
HostRegexp(`^.+\.example\.com$`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match requests with `Host` set to either `example.com` or `example.org`:
|
Match requests with `Host` set to either `example.com` or `example.org`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HostRegexp(`^example\.(com|org)$`)
|
HostRegexp(`^example\.(com|org)$`)
|
||||||
```
|
```
|
||||||
|
@ -321,7 +323,7 @@ The `Method` matchers allows to match requests sent with the given method.
|
||||||
!!! example "Example"
|
!!! example "Example"
|
||||||
|
|
||||||
Match `OPTIONS` requests:
|
Match `OPTIONS` requests:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Method(`OPTIONS`)
|
Method(`OPTIONS`)
|
||||||
```
|
```
|
||||||
|
@ -337,14 +339,14 @@ Path are always starting with a `/`, except for `PathRegexp`.
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match `/products` but neither `/products/shoes` nor `/products/`:
|
Match `/products` but neither `/products/shoes` nor `/products/`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Path(`/products`)
|
Path(`/products`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match `/products` as well as everything under `/products`,
|
Match `/products` as well as everything under `/products`,
|
||||||
such as `/products/shoes`, `/products/` but also `/products-for-sale`:
|
such as `/products/shoes`, `/products/` but also `/products-for-sale`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
PathPrefix(`/products`)
|
PathPrefix(`/products`)
|
||||||
```
|
```
|
||||||
|
@ -376,7 +378,7 @@ The `Query` and `QueryRegexp` matchers allow to match requests based on query pa
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match requests with a `mobile` query parameter set to `true`, such as in `/search?mobile=true`:
|
Match requests with a `mobile` query parameter set to `true`, such as in `/search?mobile=true`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
Query(`mobile`, `true`)
|
Query(`mobile`, `true`)
|
||||||
```
|
```
|
||||||
|
@ -388,13 +390,13 @@ The `Query` and `QueryRegexp` matchers allow to match requests based on query pa
|
||||||
```
|
```
|
||||||
|
|
||||||
Match requests with a `mobile` query parameter set to either `true` or `yes`:
|
Match requests with a `mobile` query parameter set to either `true` or `yes`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
QueryRegexp(`mobile`, `^(true|yes)$`)
|
QueryRegexp(`mobile`, `^(true|yes)$`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match requests with a `mobile` query parameter set to any value (including the empty value):
|
Match requests with a `mobile` query parameter set to any value (including the empty value):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
QueryRegexp(`mobile`, `^.*$`)
|
QueryRegexp(`mobile`, `^.*$`)
|
||||||
```
|
```
|
||||||
|
@ -414,15 +416,15 @@ It only matches the request client IP and does not use the `X-Forwarded-For` hea
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match requests coming from a given IP:
|
Match requests coming from a given IP:
|
||||||
|
|
||||||
```yaml tab="IPv4"
|
```yaml tab="IPv4"
|
||||||
ClientIP(`10.76.105.11`)
|
ClientIP(`10.76.105.11`)
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="IPv6"
|
```yaml tab="IPv6"
|
||||||
ClientIP(`::1`)
|
ClientIP(`::1`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match requests coming from a given subnet:
|
Match requests coming from a given subnet:
|
||||||
|
|
||||||
```yaml tab="IPv4"
|
```yaml tab="IPv4"
|
||||||
|
@ -831,9 +833,9 @@ If you want to limit the router scope to a set of entry points, set the entry po
|
||||||
a situation where both sides are waiting for data and the
|
a situation where both sides are waiting for data and the
|
||||||
connection appears to have hanged.
|
connection appears to have hanged.
|
||||||
|
|
||||||
The only way that Traefik can deal with such a case, is to make
|
The only way that Traefik can deal with such a case, is to make
|
||||||
sure that on the concerned entry point, there is no TLS router
|
sure that on the concerned entry point, there is no TLS router
|
||||||
whatsoever (neither TCP nor HTTP), and there is at least one
|
whatsoever (neither TCP nor HTTP), and there is at least one
|
||||||
non-TLS TCP router that leads to the server in question.
|
non-TLS TCP router that leads to the server in question.
|
||||||
|
|
||||||
??? example "Listens to Every Entry Point"
|
??? example "Listens to Every Entry Point"
|
||||||
|
@ -990,14 +992,14 @@ The table below lists all the available matchers:
|
||||||
|
|
||||||
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
The usual AND (`&&`) and OR (`||`) logical operators can be used, with the expected precedence rules,
|
||||||
as well as parentheses.
|
as well as parentheses.
|
||||||
|
|
||||||
One can invert a matcher by using the NOT (`!`) operator.
|
One can invert a matcher by using the NOT (`!`) operator.
|
||||||
|
|
||||||
The following rule matches connections where:
|
The following rule matches connections where:
|
||||||
|
|
||||||
- either Server Name Indication is `example.com` OR,
|
- either Server Name Indication is `example.com` OR,
|
||||||
- Server Name Indication is `example.org` AND ALPN protocol is NOT `h2`
|
- Server Name Indication is `example.org` AND ALPN protocol is NOT `h2`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HostSNI(`example.com`) || (HostSNI(`example.org`) && !ALPN(`h2`))
|
HostSNI(`example.com`) || (HostSNI(`example.org`) && !ALPN(`h2`))
|
||||||
```
|
```
|
||||||
|
@ -1019,23 +1021,23 @@ These matchers do not support non-ASCII characters, use punycode encoded values
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match all connections:
|
Match all connections:
|
||||||
|
|
||||||
```yaml tab="HostSNI"
|
```yaml tab="HostSNI"
|
||||||
HostSNI(`*`)
|
HostSNI(`*`)
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="HostSNIRegexp"
|
```yaml tab="HostSNIRegexp"
|
||||||
HostSNIRegexp(`^.*$`)
|
HostSNIRegexp(`^.*$`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match TCP connections sent to `example.com`:
|
Match TCP connections sent to `example.com`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HostSNI(`example.com`)
|
HostSNI(`example.com`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match TCP connections openned on any subdomain of `example.com`:
|
Match TCP connections openned on any subdomain of `example.com`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
HostSNIRegexp(`^.+\.example\.com$`)
|
HostSNIRegexp(`^.+\.example\.com$`)
|
||||||
```
|
```
|
||||||
|
@ -1047,17 +1049,17 @@ The `ClientIP` matcher allows matching connections opened by a client with the g
|
||||||
!!! example "Examples"
|
!!! example "Examples"
|
||||||
|
|
||||||
Match connections opened by a given IP:
|
Match connections opened by a given IP:
|
||||||
|
|
||||||
```yaml tab="IPv4"
|
```yaml tab="IPv4"
|
||||||
ClientIP(`10.76.105.11`)
|
ClientIP(`10.76.105.11`)
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml tab="IPv6"
|
```yaml tab="IPv6"
|
||||||
ClientIP(`::1`)
|
ClientIP(`::1`)
|
||||||
```
|
```
|
||||||
|
|
||||||
Match connections coming from a given subnet:
|
Match connections coming from a given subnet:
|
||||||
|
|
||||||
```yaml tab="IPv4"
|
```yaml tab="IPv4"
|
||||||
ClientIP(`192.168.1.0/24`)
|
ClientIP(`192.168.1.0/24`)
|
||||||
```
|
```
|
||||||
|
@ -1078,14 +1080,14 @@ protocol, and Traefik returns an error if this is attempted.
|
||||||
!!! example "Example"
|
!!! example "Example"
|
||||||
|
|
||||||
Match connections using the ALPN protocol `h2`:
|
Match connections using the ALPN protocol `h2`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ALPN(`h2`)
|
ALPN(`h2`)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Priority
|
### Priority
|
||||||
|
|
||||||
To avoid path overlap, routes are sorted, by default, in descending order using rules length.
|
To avoid path overlap, routes are sorted, by default, in descending order using rules length.
|
||||||
The priority is directly equal to the length of the rule, and so the longest length has the highest priority.
|
The priority is directly equal to the length of the rule, and so the longest length has the highest priority.
|
||||||
|
|
||||||
A value of `0` for the priority is ignored: `priority = 0` means that the default rules length sorting is used.
|
A value of `0` for the priority is ignored: `priority = 0` means that the default rules length sorting is used.
|
||||||
|
@ -1415,8 +1417,8 @@ So UDP "routers" at this time are pretty much only load-balancers in one form or
|
||||||
It basically means that some state is kept about an ongoing communication between a client and a backend,
|
It basically means that some state is kept about an ongoing communication between a client and a backend,
|
||||||
notably so that the proxy knows where to forward a response packet from a backend.
|
notably so that the proxy knows where to forward a response packet from a backend.
|
||||||
As expected, a `timeout` is associated to each of these sessions,
|
As expected, a `timeout` is associated to each of these sessions,
|
||||||
so that they get cleaned out if they go through a period of inactivity longer than a given duration.
|
so that they get cleaned out if they go through a period of inactivity longer than a given duration.
|
||||||
Timeout can be configured using the `entryPoints.name.udp.timeout` option as described
|
Timeout can be configured using the `entryPoints.name.udp.timeout` option as described
|
||||||
under [EntryPoints](../entrypoints/#udp-options).
|
under [EntryPoints](../entrypoints/#udp-options).
|
||||||
|
|
||||||
### EntryPoints
|
### EntryPoints
|
||||||
|
|
|
@ -75,25 +75,6 @@ func host(route *mux.Route, hosts ...string) error {
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||||
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
reqHost := requestdecorator.GetCanonizedHost(req.Context())
|
||||||
if len(reqHost) == 0 {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +82,11 @@ func host(route *mux.Route, hosts ...string) error {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flatH := requestdecorator.GetCNAMEFlatten(req.Context())
|
||||||
|
if len(flatH) > 0 {
|
||||||
|
return strings.EqualFold(flatH, host)
|
||||||
|
}
|
||||||
|
|
||||||
// Check for match on trailing period on host
|
// Check for match on trailing period on host
|
||||||
if last := len(host) - 1; last >= 0 && host[last] == '.' {
|
if last := len(host) - 1; last >= 0 && host[last] == '.' {
|
||||||
h := host[:last]
|
h := host[:last]
|
||||||
|
@ -136,7 +122,8 @@ func hostRegexp(route *mux.Route, hosts ...string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||||
return re.MatchString(req.Host)
|
return re.MatchString(requestdecorator.GetCanonizedHost(req.Context())) ||
|
||||||
|
re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context()))
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -198,6 +198,7 @@ func TestHostMatcher(t *testing.T) {
|
||||||
rule: "Host(`example.com`)",
|
rule: "Host(`example.com`)",
|
||||||
expected: map[string]int{
|
expected: map[string]int{
|
||||||
"https://example.com": http.StatusOK,
|
"https://example.com": http.StatusOK,
|
||||||
|
"https://example.com:8080": http.StatusOK,
|
||||||
"https://example.com/path": http.StatusOK,
|
"https://example.com/path": http.StatusOK,
|
||||||
"https://example.org": http.StatusNotFound,
|
"https://example.org": http.StatusNotFound,
|
||||||
"https://example.org/path": http.StatusNotFound,
|
"https://example.org/path": http.StatusNotFound,
|
||||||
|
@ -227,6 +228,16 @@ func TestHostMatcher(t *testing.T) {
|
||||||
"https://example.org./path": http.StatusNotFound,
|
"https://example.org./path": http.StatusNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "valid Host matcher - matcher with UPPER case",
|
||||||
|
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",
|
desc: "valid Host matcher - puny-coded emoji",
|
||||||
rule: "Host(`xn--9t9h.com`)",
|
rule: "Host(`xn--9t9h.com`)",
|
||||||
|
@ -258,7 +269,7 @@ func TestHostMatcher(t *testing.T) {
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// RequestDecorator is necessary for the host rule
|
// RequestDecorator is necessary for the Host matcher
|
||||||
reqHost := requestdecorator.New(nil)
|
reqHost := requestdecorator.New(nil)
|
||||||
|
|
||||||
results := make(map[string]int)
|
results := make(map[string]int)
|
||||||
|
@ -312,11 +323,23 @@ func TestHostRegexpMatcher(t *testing.T) {
|
||||||
rule: "HostRegexp(`^[a-zA-Z-]+\\.com$`)",
|
rule: "HostRegexp(`^[a-zA-Z-]+\\.com$`)",
|
||||||
expected: map[string]int{
|
expected: map[string]int{
|
||||||
"https://example.com": http.StatusOK,
|
"https://example.com": http.StatusOK,
|
||||||
|
"https://example.com:8080": http.StatusOK,
|
||||||
"https://example.com/path": http.StatusOK,
|
"https://example.com/path": http.StatusOK,
|
||||||
"https://example.org": http.StatusNotFound,
|
"https://example.org": http.StatusNotFound,
|
||||||
"https://example.org/path": http.StatusNotFound,
|
"https://example.org/path": http.StatusNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "valid HostRegexp matcher with case sensitive regexp",
|
||||||
|
rule: "HostRegexp(`^[A-Z]+\\.com$`)",
|
||||||
|
expected: map[string]int{
|
||||||
|
"https://example.com": http.StatusNotFound,
|
||||||
|
"https://EXAMPLE.com": http.StatusNotFound,
|
||||||
|
"https://example.com/path": http.StatusNotFound,
|
||||||
|
"https://example.org": http.StatusNotFound,
|
||||||
|
"https://example.org/path": http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "valid HostRegexp matcher with Traefik v2 syntax",
|
desc: "valid HostRegexp matcher with Traefik v2 syntax",
|
||||||
rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)",
|
rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)",
|
||||||
|
@ -343,16 +366,18 @@ func TestHostRegexpMatcher(t *testing.T) {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// RequestDecorator is necessary for the HostRegexp matcher
|
||||||
|
reqHost := requestdecorator.New(nil)
|
||||||
|
|
||||||
results := make(map[string]int)
|
results := make(map[string]int)
|
||||||
for calledURL := range test.expected {
|
for calledURL := range test.expected {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
req := httptest.NewRequest(http.MethodGet, calledURL, http.NoBody)
|
||||||
|
|
||||||
muxer.ServeHTTP(w, req)
|
reqHost.ServeHTTP(w, req, muxer.ServeHTTP)
|
||||||
results[calledURL] = w.Code
|
results[calledURL] = w.Code
|
||||||
}
|
}
|
||||||
assert.Equal(t, test.expected, results)
|
assert.Equal(t, test.expected, results)
|
||||||
|
|
Loading…
Reference in a new issue