Dynamic Configuration Refactoring

This commit is contained in:
Ludovic Fernandez 2018-11-14 10:18:03 +01:00 committed by Traefiker Bot
parent d3ae88f108
commit a09dfa3ce1
452 changed files with 21023 additions and 9419 deletions

View file

@ -0,0 +1,119 @@
package router
import (
"context"
"github.com/containous/alice"
"github.com/containous/mux"
"github.com/containous/traefik/api"
"github.com/containous/traefik/config/static"
"github.com/containous/traefik/log"
"github.com/containous/traefik/metrics"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
)
// chainBuilder The contract of the middleware builder
type chainBuilder interface {
BuildChain(ctx context.Context, middlewares []string) (*alice.Chain, error)
}
// NewRouteAppenderAggregator Creates a new RouteAppenderAggregator
func NewRouteAppenderAggregator(ctx context.Context, chainBuilder chainBuilder, conf static.Configuration, entryPointName string, currentConfiguration *safe.Safe) *RouteAppenderAggregator {
logger := log.FromContext(ctx)
aggregator := &RouteAppenderAggregator{}
// FIXME add REST
if conf.API != nil && conf.API.EntryPoint == entryPointName {
chain, err := chainBuilder.BuildChain(ctx, conf.API.Middlewares)
if err != nil {
logger.Error(err)
} else {
aggregator.AddAppender(&WithMiddleware{
appender: api.Handler{
EntryPoint: conf.API.EntryPoint,
Dashboard: conf.API.Dashboard,
Statistics: conf.API.Statistics,
DashboardAssets: conf.API.DashboardAssets,
CurrentConfigurations: currentConfiguration,
},
routerMiddlewares: chain,
})
}
}
if conf.Ping != nil && conf.Ping.EntryPoint == entryPointName {
chain, err := chainBuilder.BuildChain(ctx, conf.Ping.Middlewares)
if err != nil {
logger.Error(err)
} else {
aggregator.AddAppender(&WithMiddleware{
appender: conf.Ping,
routerMiddlewares: chain,
})
}
}
if conf.Metrics != nil && conf.Metrics.Prometheus != nil && conf.Metrics.Prometheus.EntryPoint == entryPointName {
chain, err := chainBuilder.BuildChain(ctx, conf.Metrics.Prometheus.Middlewares)
if err != nil {
logger.Error(err)
} else {
aggregator.AddAppender(&WithMiddleware{
appender: metrics.PrometheusHandler{},
routerMiddlewares: chain,
})
}
}
return aggregator
}
// RouteAppenderAggregator RouteAppender that aggregate other RouteAppender
type RouteAppenderAggregator struct {
appenders []types.RouteAppender
}
// Append Adds routes to the router
func (r *RouteAppenderAggregator) Append(systemRouter *mux.Router) {
for _, router := range r.appenders {
router.Append(systemRouter)
}
}
// AddAppender adds a router in the aggregator
func (r *RouteAppenderAggregator) AddAppender(router types.RouteAppender) {
r.appenders = append(r.appenders, router)
}
// WithMiddleware router with internal middleware
type WithMiddleware struct {
appender types.RouteAppender
routerMiddlewares *alice.Chain
}
// Append Adds routes to the router
func (wm *WithMiddleware) Append(systemRouter *mux.Router) {
realRouter := systemRouter.PathPrefix("/").Subrouter()
wm.appender.Append(realRouter)
if err := realRouter.Walk(wrapRoute(wm.routerMiddlewares)); err != nil {
log.WithoutContext().Error(err)
}
}
// wrapRoute with middlewares
func wrapRoute(middlewares *alice.Chain) func(*mux.Route, *mux.Router, []*mux.Route) error {
return func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
handler, err := middlewares.Then(route.GetHandler())
if err != nil {
return err
}
route.Handler(handler)
return nil
}
}

View file

@ -0,0 +1,116 @@
package router
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/alice"
"github.com/containous/mux"
"github.com/containous/traefik/config/static"
"github.com/containous/traefik/ping"
"github.com/stretchr/testify/assert"
)
type ChainBuilderMock struct {
middles map[string]alice.Constructor
}
func (c *ChainBuilderMock) BuildChain(ctx context.Context, middles []string) (*alice.Chain, error) {
chain := alice.New()
for _, mName := range middles {
if constructor, ok := c.middles[mName]; ok {
chain = chain.Append(constructor)
}
}
return &chain, nil
}
func TestNewRouteAppenderAggregator(t *testing.T) {
testCases := []struct {
desc string
staticConf static.Configuration
middles map[string]alice.Constructor
expected map[string]int
}{
{
desc: "API with auth, ping without auth",
staticConf: static.Configuration{
API: &static.API{
EntryPoint: "traefik",
Middlewares: []string{"dumb"},
},
Ping: &ping.Handler{
EntryPoint: "traefik",
},
EntryPoints: &static.EntryPoints{
EntryPointList: map[string]static.EntryPoint{
"traefik": {},
},
},
},
middles: map[string]alice.Constructor{
"dumb": func(_ http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}), nil
},
},
expected: map[string]int{
"/wrong": http.StatusBadGateway,
"/ping": http.StatusOK,
//"/.well-known/acme-challenge/token": http.StatusNotFound, // FIXME
"/api/providers": http.StatusUnauthorized,
},
},
{
desc: "Wrong entrypoint name",
staticConf: static.Configuration{
API: &static.API{
EntryPoint: "no",
},
EntryPoints: &static.EntryPoints{
EntryPointList: map[string]static.EntryPoint{
"traefik": {},
},
},
},
expected: map[string]int{
"/api/providers": http.StatusBadGateway,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
chainBuilder := &ChainBuilderMock{middles: test.middles}
ctx := context.Background()
router := NewRouteAppenderAggregator(ctx, chainBuilder, test.staticConf, "traefik", nil)
internalMuxRouter := mux.NewRouter()
router.Append(internalMuxRouter)
internalMuxRouter.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
})
actual := make(map[string]int)
for calledURL := range test.expected {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, calledURL, nil)
internalMuxRouter.ServeHTTP(recorder, request)
actual[calledURL] = recorder.Code
}
assert.Equal(t, test.expected, actual)
})
}
}

View file

@ -0,0 +1,38 @@
package router
import (
"context"
"github.com/containous/traefik/config/static"
"github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/server/middleware"
"github.com/containous/traefik/types"
)
// NewRouteAppenderFactory Creates a new RouteAppenderFactory
func NewRouteAppenderFactory(staticConfiguration static.Configuration, entryPointName string, acmeProvider *acme.Provider) *RouteAppenderFactory {
return &RouteAppenderFactory{
staticConfiguration: staticConfiguration,
entryPointName: entryPointName,
acmeProvider: acmeProvider,
}
}
// RouteAppenderFactory A factory of RouteAppender
type RouteAppenderFactory struct {
staticConfiguration static.Configuration
entryPointName string
acmeProvider *acme.Provider
}
// NewAppender Creates a new RouteAppender
func (r *RouteAppenderFactory) NewAppender(ctx context.Context, middlewaresBuilder *middleware.Builder, currentConfiguration *safe.Safe) types.RouteAppender {
aggregator := NewRouteAppenderAggregator(ctx, middlewaresBuilder, r.staticConfiguration, r.entryPointName, currentConfiguration)
if r.acmeProvider != nil && r.acmeProvider.HTTPChallenge != nil && r.acmeProvider.HTTPChallenge.EntryPoint == r.entryPointName {
aggregator.AddAppender(r.acmeProvider)
}
return aggregator
}

183
server/router/router.go Normal file
View file

@ -0,0 +1,183 @@
package router
import (
"context"
"fmt"
"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/server/middleware"
"github.com/containous/traefik/server/service"
)
const (
recoveryMiddlewareName = "traefik-internal-recovery"
)
// NewManager Creates a new Manager
func NewManager(routers map[string]*config.Router,
serviceManager *service.Manager, middlewaresBuilder *middleware.Builder, modifierBuilder *responsemodifiers.Builder,
) *Manager {
return &Manager{
routerHandlers: make(map[string]http.Handler),
configs: routers,
serviceManager: serviceManager,
middlewaresBuilder: middlewaresBuilder,
modifierBuilder: modifierBuilder,
}
}
// Manager A route/router manager
type Manager struct {
routerHandlers map[string]http.Handler
configs map[string]*config.Router
serviceManager *service.Manager
middlewaresBuilder *middleware.Builder
modifierBuilder *responsemodifiers.Builder
}
// BuildHandlers Builds handler for all entry points
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, defaultEntryPoints []string) map[string]http.Handler {
entryPointsRouters := m.filteredRouters(rootCtx, entryPoints, defaultEntryPoints)
entryPointHandlers := make(map[string]http.Handler)
for entryPointName, routers := range entryPointsRouters {
ctx := log.With(rootCtx, log.Str(log.EntryPointName, entryPointName))
handler, err := m.buildEntryPointHandler(ctx, routers)
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
entryPointHandlers[entryPointName] = handler
}
m.serviceManager.LaunchHealthCheck()
return entryPointHandlers
}
func contains(entryPoints []string, entryPointName string) bool {
for _, name := range entryPoints {
if name == entryPointName {
return true
}
}
return false
}
func (m *Manager) filteredRouters(ctx context.Context, entryPoints []string, defaultEntryPoints []string) map[string]map[string]*config.Router {
entryPointsRouters := make(map[string]map[string]*config.Router)
for rtName, rt := range m.configs {
eps := rt.EntryPoints
if len(eps) == 0 {
eps = defaultEntryPoints
}
for _, entryPointName := range eps {
if !contains(entryPoints, entryPointName) {
log.FromContext(log.With(ctx, log.Str(log.EntryPointName, entryPointName))).
Errorf("entryPoint %q doesn't exist", entryPointName)
continue
}
if _, ok := entryPointsRouters[entryPointName]; !ok {
entryPointsRouters[entryPointName] = make(map[string]*config.Router)
}
entryPointsRouters[entryPointName][rtName] = rt
}
}
return entryPointsRouters
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string]*config.Router) (http.Handler, error) {
router := mux.NewRouter().
SkipClean(true)
for routerName, routerConfig := range configs {
ctx = log.With(ctx, log.Str(log.RouterName, routerName))
logger := log.FromContext(ctx)
handler, err := m.buildRouterHandler(ctx, routerName)
if err != nil {
logger.Error(err)
continue
}
err = addRoute(ctx, router, routerConfig.Rule, routerConfig.Priority, handler)
if err != nil {
logger.Error(err)
continue
}
}
router.SortRoutes()
chain := alice.New()
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return recovery.New(ctx, next, recoveryMiddlewareName)
})
return chain.Then(router)
}
func (m *Manager) buildRouterHandler(ctx context.Context, routerName string) (http.Handler, error) {
if handler, ok := m.routerHandlers[routerName]; ok {
return handler, nil
}
configRouter, ok := m.configs[routerName]
if !ok {
return nil, fmt.Errorf("no configuration for %s", routerName)
}
handler, err := m.buildHandler(ctx, configRouter, routerName)
if err != nil {
return nil, err
}
handlerWithAccessLog, err := alice.New(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
}).Then(handler)
if err != nil {
log.FromContext(ctx).Error(err)
m.routerHandlers[routerName] = handler
} else {
m.routerHandlers[routerName] = handlerWithAccessLog
}
return m.routerHandlers[routerName], nil
}
func (m *Manager) buildHandler(ctx context.Context, router *config.Router, routerName string) (http.Handler, error) {
rm := m.modifierBuilder.Build(ctx, router.Middlewares)
sHandler, err := m.serviceManager.Build(ctx, router.Service, rm)
if err != nil {
return nil, err
}
mHandler, err := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
if err != nil {
return nil, err
}
alHandler := func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, accesslog.ServiceName, router.Service, accesslog.AddServiceFields), nil
}
tHandler := func(next http.Handler) (http.Handler, error) {
return tracing.NewForwarder(ctx, routerName, router.Service, next), nil
}
return alice.New().Append(alHandler).Extend(*mHandler).Append(tHandler).Then(sHandler)
}

View file

@ -0,0 +1,334 @@
package router
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/config"
"github.com/containous/traefik/middlewares/accesslog"
"github.com/containous/traefik/middlewares/requestdecorator"
"github.com/containous/traefik/responsemodifiers"
"github.com/containous/traefik/server/middleware"
"github.com/containous/traefik/server/service"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRouterManager_Get(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
type ExpectedResult struct {
StatusCode int
RequestHeaders map[string]string
}
testCases := []struct {
desc string
routersConfig map[string]*config.Router
serviceConfig map[string]*config.Service
middlewaresConfig map[string]*config.Middleware
entryPoints []string
defaultEntryPoints []string
expected ExpectedResult
}{
{
desc: "no middleware",
routersConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
entryPoints: []string{"web"},
expected: ExpectedResult{StatusCode: http.StatusOK},
},
{
desc: "no middleware, default entry point",
routersConfig: map[string]*config.Router{
"foo": {
Service: "foo-service",
Rule: "Host:foo.bar",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
entryPoints: []string{"web"},
defaultEntryPoints: []string{"web"},
expected: ExpectedResult{StatusCode: http.StatusOK},
},
{
desc: "no middleware, no matching",
routersConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:bar.bar",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
entryPoints: []string{"web"},
expected: ExpectedResult{StatusCode: http.StatusNotFound},
},
{
desc: "middleware: headers > auth",
routersConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Middlewares: []string{"headers-middle", "auth-middle"},
Service: "foo-service",
Rule: "Host:foo.bar",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
middlewaresConfig: map[string]*config.Middleware{
"auth-middle": {
BasicAuth: &config.BasicAuth{
Users: []string{"toto:titi"},
},
},
"headers-middle": {
Headers: &config.Headers{
CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
},
},
},
entryPoints: []string{"web"},
expected: ExpectedResult{
StatusCode: http.StatusUnauthorized,
RequestHeaders: map[string]string{
"X-Apero": "beer",
},
},
},
{
desc: "middleware: auth > header",
routersConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Middlewares: []string{"auth-middle", "headers-middle"},
Service: "foo-service",
Rule: "Host:foo.bar",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
middlewaresConfig: map[string]*config.Middleware{
"auth-middle": {
BasicAuth: &config.BasicAuth{
Users: []string{"toto:titi"},
},
},
"headers-middle": {
Headers: &config.Headers{
CustomRequestHeaders: map[string]string{"X-Apero": "beer"},
},
},
},
entryPoints: []string{"web"},
expected: ExpectedResult{
StatusCode: http.StatusUnauthorized,
RequestHeaders: map[string]string{
"X-Apero": "",
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig)
routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, test.defaultEntryPoints)
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
reqHost := requestdecorator.New(nil)
reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
assert.Equal(t, test.expected.StatusCode, w.Code)
for key, value := range test.expected.RequestHeaders {
assert.Equal(t, value, req.Header.Get(key))
}
})
}
}
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: "apply routerName in accesslog (first match)",
routersConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:bar.foo",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
entryPoints: []string{"web"},
expected: "foo",
},
{
desc: "apply routerName in accesslog (second match)",
routersConfig: map[string]*config.Router{
"foo": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:bar.foo",
},
"bar": {
EntryPoints: []string{"web"},
Service: "foo-service",
Rule: "Host:foo.bar",
},
},
serviceConfig: map[string]*config.Service{
"foo-service": {
LoadBalancer: &config.LoadBalancerService{
Servers: []config.Server{
{
URL: server.URL,
Weight: 1,
},
},
Method: "wrr",
},
},
},
entryPoints: []string{"web"},
expected: "bar",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
serviceManager := service.NewManager(test.serviceConfig, http.DefaultTransport)
middlewaresBuilder := middleware.NewBuilder(test.middlewaresConfig, serviceManager)
responseModifierFactory := responsemodifiers.NewBuilder(test.middlewaresConfig)
routerManager := NewManager(test.routersConfig, serviceManager, middlewaresBuilder, responseModifierFactory)
handlers := routerManager.BuildHandlers(context.Background(), test.entryPoints, test.defaultEntryPoints)
w := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://foo.bar/", nil)
accesslogger, err := accesslog.NewHandler(&types.AccessLog{
Format: "json",
})
require.NoError(t, err)
reqHost := requestdecorator.New(nil)
accesslogger.ServeHTTP(w, req, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
reqHost.ServeHTTP(w, req, handlers["web"].ServeHTTP)
data := accesslog.GetLogData(req)
require.NotNil(t, data)
assert.Equal(t, test.expected, data.Core[accesslog.RouterName])
}))
})
}
}

163
server/router/rules.go Normal file
View file

@ -0,0 +1,163 @@
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 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...)
}

442
server/router/rules_test.go Normal file
View file

@ -0,0 +1,442 @@
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
}{
{
desc: "no rule",
expected: map[string]int{
"http://localhost/foo": http.StatusOK,
},
},
{
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)
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)
}
})
}
}