Rework Host and HostRegexp matchers

Co-authored-by: Simon Delicata <simon.delicata@traefik.io>
This commit is contained in:
Tom Moulard 2022-12-06 10:40:06 +01:00 committed by GitHub
parent 519ed8bde5
commit 8cf9385938
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 66 deletions

View file

@ -288,6 +288,8 @@ 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`:

View file

@ -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

View file

@ -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)