New rule syntax

Co-authored-by: jbdoumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
SALLEYRON Julien 2019-01-30 16:24:07 +01:00 committed by Traefiker Bot
parent 7155f0d50d
commit 9ebe3c38b2
89 changed files with 1111 additions and 1357 deletions

View file

@ -6,13 +6,13 @@ import (
"net/http"
"github.com/containous/alice"
"github.com/containous/mux"
"github.com/containous/traefik/config"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares/accesslog"
"github.com/containous/traefik/middlewares/recovery"
"github.com/containous/traefik/middlewares/tracing"
"github.com/containous/traefik/responsemodifiers"
"github.com/containous/traefik/rules"
"github.com/containous/traefik/server/internal"
"github.com/containous/traefik/server/middleware"
"github.com/containous/traefik/server/service"
@ -50,6 +50,7 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string) m
entryPointHandlers := make(map[string]http.Handler)
for entryPointName, routers := range entryPointsRouters {
entryPointName := entryPointName
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
handler, err := m.buildEntryPointHandler(ctx, routers)
@ -110,22 +111,24 @@ func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string) map
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.Router) (http.Handler, error) {
router := mux.NewRouter().
SkipClean(true)
router, err := rules.NewRouter()
if err != nil {
return nil, err
}
for routerName, routerConfig := range configs {
ctx := log.With(ctx, log.Str(log.RouterName, routerName))
logger := log.FromContext(ctx)
ctxRouter := log.With(ctx, log.Str(log.RouterName, routerName))
logger := log.FromContext(ctxRouter)
ctx = internal.AddProviderInContext(ctx, routerName)
ctxRouter = internal.AddProviderInContext(ctxRouter, routerName)
handler, err := m.buildRouterHandler(ctx, routerName)
handler, err := m.buildRouterHandler(ctxRouter, routerName)
if err != nil {
logger.Error(err)
continue
}
err = addRoute(ctx, router, routerConfig.Rule, routerConfig.Priority, handler)
err = router.AddRoute(routerConfig.Rule, routerConfig.Priority, handler)
if err != nil {
logger.Error(err)
continue

View file

@ -40,7 +40,7 @@ func TestRouterManager_Get(t *testing.T) {
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -65,7 +65,7 @@ func TestRouterManager_Get(t *testing.T) {
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -79,7 +79,7 @@ func TestRouterManager_Get(t *testing.T) {
routersConfig: map[string]*config.Router{
"foo": {
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -104,7 +104,7 @@ func TestRouterManager_Get(t *testing.T) {
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:bar.bar",
Rule: "Host(`bar.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -130,7 +130,7 @@ func TestRouterManager_Get(t *testing.T) {
EntryPoints: []string{"web"},
Middlewares: []string{"headers-middle", "auth-middle"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -173,7 +173,7 @@ func TestRouterManager_Get(t *testing.T) {
EntryPoints: []string{"web"},
Middlewares: []string{"auth-middle", "headers-middle"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -215,7 +215,7 @@ func TestRouterManager_Get(t *testing.T) {
"provider-1.foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -240,7 +240,7 @@ func TestRouterManager_Get(t *testing.T) {
"provider-1.foo": {
EntryPoints: []string{"web"},
Service: "provider-2.foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -266,7 +266,7 @@ func TestRouterManager_Get(t *testing.T) {
EntryPoints: []string{"web"},
Middlewares: []string{"provider-2.chain-middle", "headers-middle"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{
@ -339,13 +339,12 @@ func TestAccessLog(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
testCases := []struct {
desc string
routersConfig map[string]*config.Router
serviceConfig map[string]*config.Service
middlewaresConfig map[string]*config.Middleware
entryPoints []string
defaultEntryPoints []string
expected string
desc string
routersConfig map[string]*config.Router
serviceConfig map[string]*config.Service
middlewaresConfig map[string]*config.Middleware
entryPoints []string
expected string
}{
{
desc: "apply routerName in accesslog (first match)",
@ -353,12 +352,12 @@ func TestAccessLog(t *testing.T) {
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:bar.foo",
Rule: "Host(`bar.foo`)",
},
},
serviceConfig: map[string]*config.Service{
@ -383,12 +382,12 @@ func TestAccessLog(t *testing.T) {
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:bar.foo",
Rule: "Host(`bar.foo`)",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
Rule: "Host(`foo.bar`)",
},
},
serviceConfig: map[string]*config.Service{

View file

@ -1,167 +0,0 @@
package router
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/containous/mux"
"github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares/requestdecorator"
)
func addRoute(ctx context.Context, router *mux.Router, rule string, priority int, handler http.Handler) error {
matchers, err := parseRule(rule)
if err != nil {
return err
}
if len(matchers) == 0 {
return fmt.Errorf("invalid rule: %s", rule)
}
if priority == 0 {
priority = len(rule)
}
route := router.NewRoute().Handler(handler).Priority(priority)
for _, matcher := range matchers {
matcher(route)
if route.GetError() != nil {
log.FromContext(ctx).Error(route.GetError())
}
}
return nil
}
func parseRule(rule string) ([]func(*mux.Route), error) {
funcs := map[string]func(*mux.Route, ...string){
"Host": host,
"HostRegexp": hostRegexp,
"Path": path,
"PathPrefix": pathPrefix,
"Method": methods,
"Headers": headers,
"HeadersRegexp": headersRegexp,
"Query": query,
}
splitRule := func(c rune) bool {
return c == ';'
}
parsedRules := strings.FieldsFunc(rule, splitRule)
var matchers []func(*mux.Route)
for _, expression := range parsedRules {
expParts := strings.Split(expression, ":")
if len(expParts) > 1 && len(expParts[1]) > 0 {
if fn, ok := funcs[expParts[0]]; ok {
parseOr := func(c rune) bool {
return c == ','
}
exp := strings.FieldsFunc(strings.Join(expParts[1:], ":"), parseOr)
var trimmedExp []string
for _, value := range exp {
trimmedExp = append(trimmedExp, strings.TrimSpace(value))
}
// FIXME struct for onhostrule ?
matcher := func(rt *mux.Route) {
fn(rt, trimmedExp...)
}
matchers = append(matchers, matcher)
} else {
return nil, fmt.Errorf("invalid matcher: %s", expression)
}
}
}
return matchers, nil
}
func path(route *mux.Route, paths ...string) {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.Path(path)
if tmpRt.GetError() != nil {
log.WithoutContext().WithField("paths", strings.Join(paths, ",")).Error(tmpRt.GetError())
}
}
}
func pathPrefix(route *mux.Route, paths ...string) {
rt := route.Subrouter()
for _, path := range paths {
tmpRt := rt.PathPrefix(path)
if tmpRt.GetError() != nil {
log.WithoutContext().WithField("paths", strings.Join(paths, ",")).Error(tmpRt.GetError())
}
}
}
func host(route *mux.Route, hosts ...string) {
for i, host := range hosts {
hosts[i] = strings.ToLower(host)
}
route.MatcherFunc(func(req *http.Request, route *mux.RouteMatch) bool {
reqHost := requestdecorator.GetCanonizedHost(req.Context())
if len(reqHost) == 0 {
log.FromContext(req.Context()).Warnf("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.FromContext(req.Context()).Debugf("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
}
}
return false
})
}
func hostRegexp(route *mux.Route, hosts ...string) {
router := route.Subrouter()
for _, host := range hosts {
router.Host(host)
}
}
func methods(route *mux.Route, methods ...string) {
route.Methods(methods...)
}
func headers(route *mux.Route, headers ...string) {
route.Headers(headers...)
}
func headersRegexp(route *mux.Route, headers ...string) {
route.HeadersRegexp(headers...)
}
func query(route *mux.Route, query ...string) {
var queries []string
for _, elem := range query {
queries = append(queries, strings.Split(elem, "=")...)
}
route.Queries(queries...)
}

View file

@ -1,449 +0,0 @@
package router
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/mux"
"github.com/containous/traefik/middlewares/requestdecorator"
"github.com/containous/traefik/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_addRoute(t *testing.T) {
testCases := []struct {
desc string
rule string
headers map[string]string
expected map[string]int
expectedError bool
}{
{
desc: "no rule",
expectedError: true,
},
{
desc: "Rule with no matcher",
rule: "rulewithnotmatcher",
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: "wrong Host",
rule: "Host:nope",
expected: map[string]int{
"http://localhost/foo": http.StatusNotFound,
},
},
{
desc: "Host and PathPrefix",
rule: "Host:localhost;PathPrefix:/foo",
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:/bar",
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: "Invalid rule syntax",
rule: "Query:param_one=true, /path2;Path: /path1",
expected: map[string]int{
"http://localhost/foo?bar=baz": 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) {})
router := mux.NewRouter()
router.SkipClean(true)
err := addRoute(context.Background(), router, 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)
for key, value := range test.headers {
req.Header.Set(key, value)
}
reqHost.ServeHTTP(w, req, router.ServeHTTP)
results[calledURL] = w.Code
}
assert.Equal(t, test.expected, results)
}
})
}
}
func Test_addRoutePriority(t *testing.T) {
type Case struct {
xFrom string
rule string
priority int
}
testCases := []struct {
desc string
path string
cases []Case
expected string
}{
{
desc: "Higher priority on second rule",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix:/my",
priority: 10,
},
{
xFrom: "header2",
rule: "PathPrefix:/my",
priority: 20,
},
},
expected: "header2",
},
{
desc: "Higher priority on first rule",
path: "/my",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix:/my",
priority: 20,
},
{
xFrom: "header2",
rule: "PathPrefix:/my",
priority: 10,
},
},
expected: "header1",
},
{
desc: "Higher priority on second rule with different rule",
path: "/mypath",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix:/mypath",
priority: 10,
},
{
xFrom: "header2",
rule: "PathPrefix:/my",
priority: 20,
},
},
expected: "header2",
},
{
desc: "Higher priority on longest rule (longest first)",
path: "/mypath",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix:/mypath",
},
{
xFrom: "header2",
rule: "PathPrefix:/my",
},
},
expected: "header1",
},
{
desc: "Higher priority on longest rule (longest second)",
path: "/mypath",
cases: []Case{
{
xFrom: "header1",
rule: "PathPrefix:/my",
},
{
xFrom: "header2",
rule: "PathPrefix:/mypath",
},
},
expected: "header2",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
router := mux.NewRouter()
for _, route := range test.cases {
route := route
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-From", route.xFrom)
})
err := addRoute(context.Background(), router, route.rule, route.priority, handler)
require.NoError(t, err, route)
}
router.SortRoutes()
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, test.path, nil)
router.ServeHTTP(w, req)
assert.Equal(t, test.expected, w.Header().Get("X-From"))
})
}
}
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{}
hostRegexp(rt, test.hostExp)
for testURL, match := range test.urls {
req := testhelpers.MustNewRequest(http.MethodGet, testURL, nil)
assert.Equal(t, match, rt.Match(req, &mux.RouteMatch{}), testURL)
}
})
}
}