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
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…
Add table
Add a link
Reference in a new issue