From 8cf9385938ec7510b815737c3b5beacbdd2bf485 Mon Sep 17 00:00:00 2001 From: Tom Moulard Date: Tue, 6 Dec 2022 10:40:06 +0100 Subject: [PATCH] Rework Host and HostRegexp matchers Co-authored-by: Simon Delicata --- docs/content/routing/routers/index.md | 88 ++++++++++++++------------- pkg/muxer/http/matcher.go | 27 +++----- pkg/muxer/http/matcher_test.go | 31 +++++++++- 3 files changed, 80 insertions(+), 66 deletions(-) diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 14acc4448..51328317b 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -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, 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`)) ``` @@ -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. !!! 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)$`) ``` @@ -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. +These matchers will match the request's host in lowercase. + !!! example "Examples" 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)$`) ``` @@ -321,7 +323,7 @@ The `Method` matchers allows to match requests sent with the given method. !!! example "Example" Match `OPTIONS` requests: - + ```yaml Method(`OPTIONS`) ``` @@ -337,14 +339,14 @@ 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`) ``` @@ -376,7 +378,7 @@ The `Query` and `QueryRegexp` matchers allow to match requests based on query pa !!! example "Examples" Match requests with a `mobile` query parameter set to `true`, such as in `/search?mobile=true`: - + ```yaml 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`: - + ```yaml QueryRegexp(`mobile`, `^(true|yes)$`) ``` Match requests with a `mobile` query parameter set to any value (including the empty value): - + ```yaml QueryRegexp(`mobile`, `^.*$`) ``` @@ -414,15 +416,15 @@ It only matches the request client IP and does not use the `X-Forwarded-For` hea !!! 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" @@ -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 connection appears to have hanged. - 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 - whatsoever (neither TCP nor HTTP), and there is at least one + 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 + whatsoever (neither TCP nor HTTP), and there is at least one non-TLS TCP router that leads to the server in question. ??? 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, 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`)) ``` @@ -1019,23 +1021,23 @@ These matchers do not support non-ASCII characters, use punycode encoded values !!! example "Examples" Match all connections: - + ```yaml tab="HostSNI" HostSNI(`*`) ``` - + ```yaml tab="HostSNIRegexp" HostSNIRegexp(`^.*$`) ``` Match TCP connections sent to `example.com`: - + ```yaml HostSNI(`example.com`) ``` Match TCP connections openned on any subdomain of `example.com`: - + ```yaml HostSNIRegexp(`^.+\.example\.com$`) ``` @@ -1047,17 +1049,17 @@ The `ClientIP` matcher allows matching connections opened by a client with the g !!! example "Examples" 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`) ``` @@ -1078,14 +1080,14 @@ protocol, and Traefik returns an error if this is attempted. !!! example "Example" Match connections using the ALPN protocol `h2`: - + ```yaml ALPN(`h2`) ``` ### 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. 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, 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, - 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 + 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 under [EntryPoints](../entrypoints/#udp-options). ### EntryPoints diff --git a/pkg/muxer/http/matcher.go b/pkg/muxer/http/matcher.go index ceafbd6b4..d30e6df61 100644 --- a/pkg/muxer/http/matcher.go +++ b/pkg/muxer/http/matcher.go @@ -75,25 +75,6 @@ func host(route *mux.Route, hosts ...string) error { 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 } @@ -101,6 +82,11 @@ func host(route *mux.Route, hosts ...string) error { return true } + flatH := requestdecorator.GetCNAMEFlatten(req.Context()) + if len(flatH) > 0 { + return strings.EqualFold(flatH, host) + } + // Check for match on trailing period on host if last := len(host) - 1; last >= 0 && 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 { - return re.MatchString(req.Host) + return re.MatchString(requestdecorator.GetCanonizedHost(req.Context())) || + re.MatchString(requestdecorator.GetCNAMEFlatten(req.Context())) }) return nil diff --git a/pkg/muxer/http/matcher_test.go b/pkg/muxer/http/matcher_test.go index 231d34f91..2d31d8dc8 100644 --- a/pkg/muxer/http/matcher_test.go +++ b/pkg/muxer/http/matcher_test.go @@ -198,6 +198,7 @@ func TestHostMatcher(t *testing.T) { rule: "Host(`example.com`)", expected: map[string]int{ "https://example.com": http.StatusOK, + "https://example.com:8080": http.StatusOK, "https://example.com/path": http.StatusOK, "https://example.org": http.StatusNotFound, "https://example.org/path": http.StatusNotFound, @@ -227,6 +228,16 @@ func TestHostMatcher(t *testing.T) { "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", rule: "Host(`xn--9t9h.com`)", @@ -258,7 +269,7 @@ func TestHostMatcher(t *testing.T) { require.NoError(t, err) - // RequestDecorator is necessary for the host rule + // RequestDecorator is necessary for the Host matcher reqHost := requestdecorator.New(nil) results := make(map[string]int) @@ -312,11 +323,23 @@ func TestHostRegexpMatcher(t *testing.T) { rule: "HostRegexp(`^[a-zA-Z-]+\\.com$`)", expected: map[string]int{ "https://example.com": http.StatusOK, + "https://example.com:8080": http.StatusOK, "https://example.com/path": http.StatusOK, "https://example.org": 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", rule: "HostRegexp(`{domain:[a-zA-Z-]+\\.com}`)", @@ -343,16 +366,18 @@ func TestHostRegexpMatcher(t *testing.T) { require.Error(t, err) return } - require.NoError(t, err) + // RequestDecorator is necessary for the HostRegexp matcher + 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) - muxer.ServeHTTP(w, req) + reqHost.ServeHTTP(w, req, muxer.ServeHTTP) results[calledURL] = w.Code } assert.Equal(t, test.expected, results)