package server import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "reflect" "testing" "time" "github.com/containous/flaeg" "github.com/containous/mux" "github.com/containous/traefik/configuration" "github.com/containous/traefik/healthcheck" "github.com/containous/traefik/metrics" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/rules" "github.com/containous/traefik/testhelpers" "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/unrolled/secure" "github.com/urfave/negroni" "github.com/vulcand/oxy/roundrobin" ) // LocalhostCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // generated from src/crypto/tls: // go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h var ( localhostCert = tls.FileOrContent(`-----BEGIN CERTIFICATE----- MIICEzCCAXygAwIBAgIQMIMChMLGrR+QvmQvpwAU6zANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB iQKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9SjY1bIw4 iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZBl2+XsDul rKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQABo2gwZjAO BgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUw AwEB/zAuBgNVHREEJzAlggtleGFtcGxlLmNvbYcEfwAAAYcQAAAAAAAAAAAAAAAA AAAAATANBgkqhkiG9w0BAQsFAAOBgQCEcetwO59EWk7WiJsG4x8SY+UIAA+flUI9 tyC4lNhbcF2Idq9greZwbYCqTTTr2XiRNSMLCOjKyI7ukPoPjo16ocHj+P3vZGfs h1fIw3cSS2OolhloGw/XM6RWPWtPAlGykKLciQrBru5NAPvCMsb/I1DAceTiotQM fblo6RBxUQ== -----END CERTIFICATE-----`) // LocalhostKey is the private key for localhostCert. localhostKey = tls.FileOrContent(`-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDuLnQAI3mDgey3VBzWnB2L39JUU4txjeVE6myuDqkM/uGlfjb9 SjY1bIw4iA5sBBZzHi3z0h1YV8QPuxEbi4nW91IJm2gsvvZhIrCHS3l6afab4pZB l2+XsDulrKBxKKtD1rGxlG4LjncdabFn9gvLZad2bSysqz/qTAUStTvqJQIDAQAB AoGAGRzwwir7XvBOAy5tM/uV6e+Zf6anZzus1s1Y1ClbjbE6HXbnWWF/wbZGOpet 3Zm4vD6MXc7jpTLryzTQIvVdfQbRc6+MUVeLKwZatTXtdZrhu+Jk7hx0nTPy8Jcb uJqFk541aEw+mMogY/xEcfbWd6IOkp+4xqjlFLBEDytgbIECQQDvH/E6nk+hgN4H qzzVtxxr397vWrjrIgPbJpQvBsafG7b0dA4AFjwVbFLmQcj2PprIMmPcQrooz8vp jy4SHEg1AkEA/v13/5M47K9vCxmb8QeD/asydfsgS5TeuNi8DoUBEmiSJwma7FXY fFUtxuvL7XvjwjN5B30pNEbc6Iuyt7y4MQJBAIt21su4b3sjXNueLKH85Q+phy2U fQtuUE9txblTu14q3N7gHRZB4ZMhFYyDy8CKrN2cPg/Fvyt0Xlp/DoCzjA0CQQDU y2ptGsuSmgUtWj3NM9xuwYPm+Z/F84K6+ARYiZ6PYj013sovGKUFfYAqVXVlxtIX qyUBnu3X9ps8ZfjLZO7BAkEAlT4R5Yl6cGhaJQYZHOde3JEMhNRcVFMO8dJDaFeo f9Oeos0UUothgiDktdQHxdNEwLjQf7lJJBzV+5OtwswCWA== -----END RSA PRIVATE KEY-----`) ) type testLoadBalancer struct{} func (lb *testLoadBalancer) RemoveServer(u *url.URL) error { return nil } func (lb *testLoadBalancer) UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error { return nil } func (lb *testLoadBalancer) Servers() []*url.URL { return []*url.URL{} } func TestPrepareServerTimeouts(t *testing.T) { tests := []struct { desc string globalConfig configuration.GlobalConfiguration wantIdleTimeout time.Duration wantReadTimeout time.Duration wantWriteTimeout time.Duration }{ { desc: "full configuration", globalConfig: configuration.GlobalConfiguration{ RespondingTimeouts: &configuration.RespondingTimeouts{ IdleTimeout: flaeg.Duration(10 * time.Second), ReadTimeout: flaeg.Duration(12 * time.Second), WriteTimeout: flaeg.Duration(14 * time.Second), }, }, wantIdleTimeout: time.Duration(10 * time.Second), wantReadTimeout: time.Duration(12 * time.Second), wantWriteTimeout: time.Duration(14 * time.Second), }, { desc: "using defaults", globalConfig: configuration.GlobalConfiguration{}, wantIdleTimeout: time.Duration(180 * time.Second), wantReadTimeout: time.Duration(0 * time.Second), wantWriteTimeout: time.Duration(0 * time.Second), }, { desc: "deprecated IdleTimeout configured", globalConfig: configuration.GlobalConfiguration{ IdleTimeout: flaeg.Duration(45 * time.Second), }, wantIdleTimeout: time.Duration(45 * time.Second), wantReadTimeout: time.Duration(0 * time.Second), wantWriteTimeout: time.Duration(0 * time.Second), }, { desc: "deprecated and new IdleTimeout configured", globalConfig: configuration.GlobalConfiguration{ IdleTimeout: flaeg.Duration(45 * time.Second), RespondingTimeouts: &configuration.RespondingTimeouts{ IdleTimeout: flaeg.Duration(80 * time.Second), }, }, wantIdleTimeout: time.Duration(45 * time.Second), wantReadTimeout: time.Duration(0 * time.Second), wantWriteTimeout: time.Duration(0 * time.Second), }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() entryPointName := "http" entryPoint := &configuration.EntryPoint{ Address: "localhost:0", ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, } router := middlewares.NewHandlerSwitcher(mux.NewRouter()) srv := NewServer(test.globalConfig, nil) httpServer, _, err := srv.prepareServer(entryPointName, entryPoint, router, nil, nil) if err != nil { t.Fatalf("Unexpected error when preparing srv: %s", err) } if httpServer.IdleTimeout != test.wantIdleTimeout { t.Errorf("Got %s as IdleTimeout, want %s", httpServer.IdleTimeout, test.wantIdleTimeout) } if httpServer.ReadTimeout != test.wantReadTimeout { t.Errorf("Got %s as ReadTimeout, want %s", httpServer.ReadTimeout, test.wantReadTimeout) } if httpServer.WriteTimeout != test.wantWriteTimeout { t.Errorf("Got %s as WriteTimeout, want %s", httpServer.WriteTimeout, test.wantWriteTimeout) } }) } } func TestListenProvidersSkipsEmptyConfigs(t *testing.T) { server, stop, invokeStopChan := setupListenProvider(10 * time.Millisecond) defer invokeStopChan() go func() { for { select { case <-stop: return case <-server.configurationValidatedChan: t.Error("An empty configuration was published but it should not") } } }() server.configurationChan <- types.ConfigMessage{ProviderName: "kubernetes"} // give some time so that the configuration can be processed time.Sleep(100 * time.Millisecond) } func TestListenProvidersSkipsSameConfigurationForProvider(t *testing.T) { server, stop, invokeStopChan := setupListenProvider(10 * time.Millisecond) defer invokeStopChan() publishedConfigCount := 0 go func() { for { select { case <-stop: return case config := <-server.configurationValidatedChan: // set the current configuration // this is usually done in the processing part of the published configuration // so we have to emulate the behaviour here currentConfigurations := server.currentConfigurations.Get().(types.Configurations) currentConfigurations[config.ProviderName] = config.Configuration server.currentConfigurations.Set(currentConfigurations) publishedConfigCount++ if publishedConfigCount > 1 { t.Error("Same configuration should not be published multiple times") } } } }() config := buildDynamicConfig( withFrontend("frontend", buildFrontend()), withBackend("backend", buildBackend()), ) // provide a configuration server.configurationChan <- types.ConfigMessage{ProviderName: "kubernetes", Configuration: config} // give some time so that the configuration can be processed time.Sleep(20 * time.Millisecond) // provide the same configuration a second time server.configurationChan <- types.ConfigMessage{ProviderName: "kubernetes", Configuration: config} // give some time so that the configuration can be processed time.Sleep(100 * time.Millisecond) } func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) { server, stop, invokeStopChan := setupListenProvider(10 * time.Millisecond) defer invokeStopChan() publishedProviderConfigCount := map[string]int{} publishedConfigCount := 0 consumePublishedConfigsDone := make(chan bool) go func() { for { select { case <-stop: return case newConfig := <-server.configurationValidatedChan: publishedProviderConfigCount[newConfig.ProviderName]++ publishedConfigCount++ if publishedConfigCount == 2 { consumePublishedConfigsDone <- true return } } } }() config := buildDynamicConfig( withFrontend("frontend", buildFrontend()), withBackend("backend", buildBackend()), ) server.configurationChan <- types.ConfigMessage{ProviderName: "kubernetes", Configuration: config} server.configurationChan <- types.ConfigMessage{ProviderName: "marathon", Configuration: config} select { case <-consumePublishedConfigsDone: if val := publishedProviderConfigCount["kubernetes"]; val != 1 { t.Errorf("Got %d configuration publication(s) for provider %q, want 1", val, "kubernetes") } if val := publishedProviderConfigCount["marathon"]; val != 1 { t.Errorf("Got %d configuration publication(s) for provider %q, want 1", val, "marathon") } case <-time.After(100 * time.Millisecond): t.Errorf("Published configurations were not consumed in time") } } // setupListenProvider configures the Server and starts listenProviders func setupListenProvider(throttleDuration time.Duration) (server *Server, stop chan bool, invokeStopChan func()) { stop = make(chan bool) invokeStopChan = func() { stop <- true } globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "http": &configuration.EntryPoint{}, }, ProvidersThrottleDuration: flaeg.Duration(throttleDuration), } server = NewServer(globalConfig, nil) go server.listenProviders(stop) return server, stop, invokeStopChan } func TestThrottleProviderConfigReload(t *testing.T) { throttleDuration := 30 * time.Millisecond publishConfig := make(chan types.ConfigMessage) providerConfig := make(chan types.ConfigMessage) stop := make(chan bool) defer func() { stop <- true }() globalConfig := configuration.GlobalConfiguration{} server := NewServer(globalConfig, nil) go server.throttleProviderConfigReload(throttleDuration, publishConfig, providerConfig, stop) publishedConfigCount := 0 stopConsumeConfigs := make(chan bool) go func() { for { select { case <-stop: return case <-stopConsumeConfigs: return case <-publishConfig: publishedConfigCount++ } } }() // publish 5 new configs, one new config each 10 milliseconds for i := 0; i < 5; i++ { providerConfig <- types.ConfigMessage{} time.Sleep(10 * time.Millisecond) } // after 50 milliseconds 5 new configs were published // with a throttle duration of 30 milliseconds this means, we should have received 2 new configs wantPublishedConfigCount := 2 if publishedConfigCount != wantPublishedConfigCount { t.Errorf("%d times configs were published, want %d times", publishedConfigCount, wantPublishedConfigCount) } stopConsumeConfigs <- true select { case <-publishConfig: // There should be exactly one more message that we receive after ~60 milliseconds since the start of the test. select { case <-publishConfig: t.Error("extra config publication found") case <-time.After(100 * time.Millisecond): return } case <-time.After(100 * time.Millisecond): t.Error("Last config was not published in time") } } func TestServerMultipleFrontendRules(t *testing.T) { cases := []struct { expression string requestURL string expectedURL string }{ { expression: "Host:foo.bar", requestURL: "http://foo.bar", expectedURL: "http://foo.bar", }, { expression: "PathPrefix:/management;ReplacePath:/health", requestURL: "http://foo.bar/management", expectedURL: "http://foo.bar/health", }, { expression: "Host:foo.bar;AddPrefix:/blah", requestURL: "http://foo.bar/baz", expectedURL: "http://foo.bar/blah/baz", }, { expression: "PathPrefixStripRegex:/one/{two}/{three:[0-9]+}", requestURL: "http://foo.bar/one/some/12345/four", expectedURL: "http://foo.bar/four", }, { expression: "PathPrefixStripRegex:/one/{two}/{three:[0-9]+};AddPrefix:/zero", requestURL: "http://foo.bar/one/some/12345/four", expectedURL: "http://foo.bar/zero/four", }, { expression: "AddPrefix:/blah;ReplacePath:/baz", requestURL: "http://foo.bar/hello", expectedURL: "http://foo.bar/baz", }, { expression: "PathPrefixStrip:/management;ReplacePath:/health", requestURL: "http://foo.bar/management", expectedURL: "http://foo.bar/health", }, } for _, test := range cases { test := test t.Run(test.expression, func(t *testing.T) { t.Parallel() router := mux.NewRouter() route := router.NewRoute() serverRoute := &types.ServerRoute{Route: route} rules := &rules.Rules{Route: serverRoute} expression := test.expression routeResult, err := rules.Parse(expression) if err != nil { t.Fatalf("Error while building route for %s: %+v", expression, err) } request := testhelpers.MustNewRequest(http.MethodGet, test.requestURL, nil) routeMatch := routeResult.Match(request, &mux.RouteMatch{Route: routeResult}) if !routeMatch { t.Fatalf("Rule %s doesn't match", expression) } server := new(Server) server.wireFrontendBackend(serverRoute, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.String() != test.expectedURL { t.Fatalf("got URL %s, expected %s", r.URL.String(), test.expectedURL) } })) serverRoute.Route.GetHandler().ServeHTTP(nil, request) }) } } func TestServerLoadConfigHealthCheckOptions(t *testing.T) { healthChecks := []*types.HealthCheck{ nil, { Path: "/path", }, } for _, lbMethod := range []string{"Wrr", "Drr"} { for _, healthCheck := range healthChecks { t.Run(fmt.Sprintf("%s/hc=%t", lbMethod, healthCheck != nil), func(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "http": &configuration.EntryPoint{ ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, }, HealthCheck: &configuration.HealthCheckConfig{Interval: flaeg.Duration(5 * time.Second)}, } dynamicConfigs := types.Configurations{ "config": &types.Configuration{ Frontends: map[string]*types.Frontend{ "frontend": { EntryPoints: []string{"http"}, Backend: "backend", }, }, Backends: map[string]*types.Backend{ "backend": { Servers: map[string]types.Server{ "server": { URL: "http://localhost", }, }, LoadBalancer: &types.LoadBalancer{ Method: lbMethod, }, HealthCheck: healthCheck, }, }, TLS: []*tls.Configuration{ { Certificate: &tls.Certificate{ CertFile: localhostCert, KeyFile: localhostKey, }, EntryPoints: []string{"http"}, }, }, }, } srv := NewServer(globalConfig, nil) if _, err := srv.loadConfig(dynamicConfigs, globalConfig); err != nil { t.Fatalf("got error: %s", err) } wantNumHealthCheckBackends := 0 if healthCheck != nil { wantNumHealthCheckBackends = 1 } gotNumHealthCheckBackends := len(healthcheck.GetHealthCheck(testhelpers.NewCollectingHealthCheckMetrics()).Backends) if gotNumHealthCheckBackends != wantNumHealthCheckBackends { t.Errorf("got %d health check backends, want %d", gotNumHealthCheckBackends, wantNumHealthCheckBackends) } }) } } } func TestServerParseHealthCheckOptions(t *testing.T) { lb := &testLoadBalancer{} globalInterval := 15 * time.Second tests := []struct { desc string hc *types.HealthCheck wantOpts *healthcheck.Options }{ { desc: "nil health check", hc: nil, wantOpts: nil, }, { desc: "empty path", hc: &types.HealthCheck{ Path: "", }, wantOpts: nil, }, { desc: "unparseable interval", hc: &types.HealthCheck{ Path: "/path", Interval: "unparseable", }, wantOpts: &healthcheck.Options{ Path: "/path", Interval: globalInterval, LB: lb, }, }, { desc: "sub-zero interval", hc: &types.HealthCheck{ Path: "/path", Interval: "-42s", }, wantOpts: &healthcheck.Options{ Path: "/path", Interval: globalInterval, LB: lb, }, }, { desc: "parseable interval", hc: &types.HealthCheck{ Path: "/path", Interval: "5m", }, wantOpts: &healthcheck.Options{ Path: "/path", Interval: 5 * time.Minute, LB: lb, }, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() gotOpts := parseHealthCheckOptions(lb, "backend", test.hc, &configuration.HealthCheckConfig{Interval: flaeg.Duration(globalInterval)}) if !reflect.DeepEqual(gotOpts, test.wantOpts) { t.Errorf("got health check options %+v, want %+v", gotOpts, test.wantOpts) } }) } } func TestBuildIPWhiteLister(t *testing.T) { testCases := []struct { desc string whitelistSourceRange []string whiteList *types.WhiteList middlewareConfigured bool errMessage string }{ { desc: "no whitelists configured", whitelistSourceRange: nil, middlewareConfigured: false, errMessage: "", }, { desc: "whitelists configured (deprecated)", whitelistSourceRange: []string{ "1.2.3.4/24", "fe80::/16", }, middlewareConfigured: true, errMessage: "", }, { desc: "invalid whitelists configured (deprecated)", whitelistSourceRange: []string{ "foo", }, middlewareConfigured: false, errMessage: "parsing CIDR whitelist [foo]: parsing CIDR white list : invalid CIDR address: foo", }, { desc: "whitelists configured", whiteList: &types.WhiteList{ SourceRange: []string{ "1.2.3.4/24", "fe80::/16", }, UseXForwardedFor: false, }, middlewareConfigured: true, errMessage: "", }, { desc: "invalid whitelists configured (deprecated)", whiteList: &types.WhiteList{ SourceRange: []string{ "foo", }, UseXForwardedFor: false, }, middlewareConfigured: false, errMessage: "parsing CIDR whitelist [foo]: parsing CIDR white list : invalid CIDR address: foo", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() middleware, err := buildIPWhiteLister(test.whiteList, test.whitelistSourceRange) if test.errMessage != "" { require.EqualError(t, err, test.errMessage) } else { assert.NoError(t, err) if test.middlewareConfigured { require.NotNil(t, middleware, "not expected middleware to be configured") } else { require.Nil(t, middleware, "expected middleware to be configured") } } }) } } func TestServerLoadConfigEmptyBasicAuth(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, }, } dynamicConfigs := types.Configurations{ "config": &types.Configuration{ Frontends: map[string]*types.Frontend{ "frontend": { EntryPoints: []string{"http"}, Backend: "backend", BasicAuth: []string{""}, }, }, Backends: map[string]*types.Backend{ "backend": { Servers: map[string]types.Server{ "server": { URL: "http://localhost", }, }, LoadBalancer: &types.LoadBalancer{ Method: "Wrr", }, }, }, }, } srv := NewServer(globalConfig, nil) if _, err := srv.loadConfig(dynamicConfigs, globalConfig); err != nil { t.Fatalf("got error: %s", err) } } func TestServerLoadCertificateWithDefaultEntryPoint(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "https": &configuration.EntryPoint{TLS: &tls.TLS{}}, "http": &configuration.EntryPoint{}, }, DefaultEntryPoints: []string{"http", "https"}, } dynamicConfigs := types.Configurations{ "config": &types.Configuration{ TLS: []*tls.Configuration{ { Certificate: &tls.Certificate{ CertFile: localhostCert, KeyFile: localhostKey, }, }, }, }, } srv := NewServer(globalConfig, nil) if mapEntryPoints, err := srv.loadConfig(dynamicConfigs, globalConfig); err != nil { t.Fatalf("got error: %s", err) } else if mapEntryPoints["https"].certs.Get() == nil { t.Fatal("got error: https entryPoint must have TLS certificates.") } } func TestConfigureBackends(t *testing.T) { validMethod := "Drr" defaultMethod := "wrr" tests := []struct { desc string lb *types.LoadBalancer wantMethod string wantStickiness *types.Stickiness }{ { desc: "valid load balancer method with sticky enabled", lb: &types.LoadBalancer{ Method: validMethod, Stickiness: &types.Stickiness{}, }, wantMethod: validMethod, wantStickiness: &types.Stickiness{}, }, { desc: "valid load balancer method with sticky disabled", lb: &types.LoadBalancer{ Method: validMethod, Stickiness: nil, }, wantMethod: validMethod, }, { desc: "invalid load balancer method with sticky enabled", lb: &types.LoadBalancer{ Method: "Invalid", Stickiness: &types.Stickiness{}, }, wantMethod: defaultMethod, wantStickiness: &types.Stickiness{}, }, { desc: "invalid load balancer method with sticky disabled", lb: &types.LoadBalancer{ Method: "Invalid", Stickiness: nil, }, wantMethod: defaultMethod, }, { desc: "missing load balancer", lb: nil, wantMethod: defaultMethod, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() backend := &types.Backend{ LoadBalancer: test.lb, } configureBackends(map[string]*types.Backend{ "backend": backend, }) wantLB := types.LoadBalancer{ Method: test.wantMethod, Stickiness: test.wantStickiness, } if !reflect.DeepEqual(*backend.LoadBalancer, wantLB) { t.Errorf("got backend load-balancer\n%v\nwant\n%v\n", spew.Sdump(backend.LoadBalancer), spew.Sdump(wantLB)) } }) } } func TestServerEntryPointWhitelistConfig(t *testing.T) { tests := []struct { desc string entrypoint *configuration.EntryPoint wantMiddleware bool }{ { desc: "no whitelist middleware if no config on entrypoint", entrypoint: &configuration.EntryPoint{ Address: ":0", ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, wantMiddleware: false, }, { desc: "whitelist middleware should be added if configured on entrypoint", entrypoint: &configuration.EntryPoint{ Address: ":0", WhitelistSourceRange: []string{ "127.0.0.1/32", }, ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, wantMiddleware: true, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() srv := Server{ globalConfiguration: configuration.GlobalConfiguration{ EntryPoints: map[string]*configuration.EntryPoint{ "test": test.entrypoint, }, }, metricsRegistry: metrics.NewVoidRegistry(), } srv.serverEntryPoints = srv.buildEntryPoints(srv.globalConfiguration) srvEntryPoint := srv.setupServerEntryPoint("test", srv.serverEntryPoints["test"]) handler := srvEntryPoint.httpServer.Handler.(*mux.Router).NotFoundHandler.(*negroni.Negroni) found := false for _, handler := range handler.Handlers() { if reflect.TypeOf(handler) == reflect.TypeOf((*middlewares.IPWhiteLister)(nil)) { found = true } } if found && !test.wantMiddleware { t.Errorf("ip whitelist middleware was installed even though it should not") } if !found && test.wantMiddleware { t.Errorf("ip whitelist middleware was not installed even though it should have") } }) } } func TestServerResponseEmptyBackend(t *testing.T) { const requestPath = "/path" const routeRule = "Path:" + requestPath testCases := []struct { desc string dynamicConfig func(testServerURL string) *types.Configuration wantStatusCode int }{ { desc: "Ok", dynamicConfig: func(testServerURL string) *types.Configuration { return buildDynamicConfig( withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))), withBackend("backend", buildBackend(withServer("testServer", testServerURL))), ) }, wantStatusCode: http.StatusOK, }, { desc: "No Frontend", dynamicConfig: func(testServerURL string) *types.Configuration { return buildDynamicConfig() }, wantStatusCode: http.StatusNotFound, }, { desc: "Empty Backend LB-Drr", dynamicConfig: func(testServerURL string) *types.Configuration { return buildDynamicConfig( withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))), withBackend("backend", buildBackend(withLoadBalancer("Drr", false))), ) }, wantStatusCode: http.StatusServiceUnavailable, }, { desc: "Empty Backend LB-Drr Sticky", dynamicConfig: func(testServerURL string) *types.Configuration { return buildDynamicConfig( withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))), withBackend("backend", buildBackend(withLoadBalancer("Drr", true))), ) }, wantStatusCode: http.StatusServiceUnavailable, }, { desc: "Empty Backend LB-Wrr", dynamicConfig: func(testServerURL string) *types.Configuration { return buildDynamicConfig( withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))), withBackend("backend", buildBackend(withLoadBalancer("Wrr", false))), ) }, wantStatusCode: http.StatusServiceUnavailable, }, { desc: "Empty Backend LB-Wrr Sticky", dynamicConfig: func(testServerURL string) *types.Configuration { return buildDynamicConfig( withFrontend("frontend", buildFrontend(withRoute(requestPath, routeRule))), withBackend("backend", buildBackend(withLoadBalancer("Wrr", true))), ) }, wantStatusCode: http.StatusServiceUnavailable, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() testServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) })) defer testServer.Close() globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, }, } dynamicConfigs := types.Configurations{"config": test.dynamicConfig(testServer.URL)} srv := NewServer(globalConfig, nil) entryPoints, err := srv.loadConfig(dynamicConfigs, globalConfig) if err != nil { t.Fatalf("error loading config: %s", err) } responseRecorder := &httptest.ResponseRecorder{} request := httptest.NewRequest(http.MethodGet, testServer.URL+requestPath, nil) entryPoints["http"].httpRouter.ServeHTTP(responseRecorder, request) if responseRecorder.Result().StatusCode != test.wantStatusCode { t.Errorf("got status code %d, want %d", responseRecorder.Result().StatusCode, test.wantStatusCode) } }) } } func TestBuildRedirectHandler(t *testing.T) { srv := Server{ globalConfiguration: configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "http": &configuration.EntryPoint{Address: ":80"}, "https": &configuration.EntryPoint{Address: ":443", TLS: &tls.TLS{}}, }, }, } testCases := []struct { desc string srcEntryPointName string url string entryPoint *configuration.EntryPoint redirect *types.Redirect expectedURL string }{ { desc: "redirect regex", srcEntryPointName: "http", url: "http://foo.com", redirect: &types.Redirect{ Regex: `^(?:http?:\/\/)(foo)(\.com)$`, Replacement: "https://$1{{\"bar\"}}$2", }, entryPoint: &configuration.EntryPoint{ Address: ":80", Redirect: &types.Redirect{ Regex: `^(?:http?:\/\/)(foo)(\.com)$`, Replacement: "https://$1{{\"bar\"}}$2", }, }, expectedURL: "https://foobar.com", }, { desc: "redirect entry point", srcEntryPointName: "http", url: "http://foo:80", redirect: &types.Redirect{ EntryPoint: "https", }, entryPoint: &configuration.EntryPoint{ Address: ":80", Redirect: &types.Redirect{ EntryPoint: "https", }, }, expectedURL: "https://foo:443", }, { desc: "redirect entry point with regex (ignored)", srcEntryPointName: "http", url: "http://foo.com:80", redirect: &types.Redirect{ EntryPoint: "https", Regex: `^(?:http?:\/\/)(foo)(\.com)$`, Replacement: "https://$1{{\"bar\"}}$2", }, entryPoint: &configuration.EntryPoint{ Address: ":80", Redirect: &types.Redirect{ EntryPoint: "https", Regex: `^(?:http?:\/\/)(foo)(\.com)$`, Replacement: "https://$1{{\"bar\"}}$2", }, }, expectedURL: "https://foo.com:443", }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() rewrite, err := srv.buildRedirectHandler(test.srcEntryPointName, test.redirect) require.NoError(t, err) req := testhelpers.MustNewRequest(http.MethodGet, test.url, nil) recorder := httptest.NewRecorder() rewrite.ServeHTTP(recorder, req, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Location", "fail") })) location, err := recorder.Result().Location() require.NoError(t, err) assert.Equal(t, test.expectedURL, location.String()) }) } } type mockContext struct { headers http.Header } func (c mockContext) Deadline() (deadline time.Time, ok bool) { return deadline, ok } func (c mockContext) Done() <-chan struct{} { ch := make(chan struct{}) close(ch) return ch } func (c mockContext) Err() error { return context.DeadlineExceeded } func (c mockContext) Value(key interface{}) interface{} { return c.headers } func TestNewServerWithResponseModifiers(t *testing.T) { cases := []struct { desc string headerMiddleware *middlewares.HeaderStruct secureMiddleware *secure.Secure ctx context.Context expected map[string]string }{ { desc: "header and secure nil", headerMiddleware: nil, secureMiddleware: nil, ctx: mockContext{}, expected: map[string]string{ "X-Default": "powpow", "Referrer-Policy": "same-origin", }, }, { desc: "header middleware not nil", headerMiddleware: middlewares.NewHeaderFromStruct(&types.Headers{ CustomResponseHeaders: map[string]string{ "X-Default": "powpow", }, }), secureMiddleware: nil, ctx: mockContext{}, expected: map[string]string{ "X-Default": "powpow", "Referrer-Policy": "same-origin", }, }, { desc: "secure middleware not nil", headerMiddleware: nil, secureMiddleware: middlewares.NewSecure(&types.Headers{ ReferrerPolicy: "no-referrer", }), ctx: mockContext{ headers: http.Header{"Referrer-Policy": []string{"no-referrer"}}, }, expected: map[string]string{ "X-Default": "powpow", "Referrer-Policy": "no-referrer", }, }, { desc: "header and secure middleware not nil", headerMiddleware: middlewares.NewHeaderFromStruct(&types.Headers{ CustomResponseHeaders: map[string]string{ "Referrer-Policy": "powpow", }, }), secureMiddleware: middlewares.NewSecure(&types.Headers{ ReferrerPolicy: "no-referrer", }), ctx: mockContext{ headers: http.Header{"Referrer-Policy": []string{"no-referrer"}}, }, expected: map[string]string{ "X-Default": "powpow", "Referrer-Policy": "powpow", }, }, } for _, test := range cases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() headers := make(http.Header) headers.Add("X-Default", "powpow") headers.Add("Referrer-Policy", "same-origin") req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1", nil) res := &http.Response{ Request: req.WithContext(test.ctx), Header: headers, } responseModifier := buildModifyResponse(test.secureMiddleware, test.headerMiddleware) err := responseModifier(res) assert.NoError(t, err) assert.Equal(t, len(test.expected), len(res.Header)) for k, v := range test.expected { assert.Equal(t, v, res.Header.Get(k)) } }) } } func buildDynamicConfig(dynamicConfigBuilders ...func(*types.Configuration)) *types.Configuration { config := &types.Configuration{ Frontends: make(map[string]*types.Frontend), Backends: make(map[string]*types.Backend), } for _, build := range dynamicConfigBuilders { build(config) } return config } func withFrontend(frontendName string, frontend *types.Frontend) func(*types.Configuration) { return func(config *types.Configuration) { config.Frontends[frontendName] = frontend } } func withBackend(backendName string, backend *types.Backend) func(*types.Configuration) { return func(config *types.Configuration) { config.Backends[backendName] = backend } } func buildFrontend(frontendBuilders ...func(*types.Frontend)) *types.Frontend { fe := &types.Frontend{ EntryPoints: []string{"http"}, Backend: "backend", Routes: make(map[string]types.Route), } for _, build := range frontendBuilders { build(fe) } return fe } func withRoute(routeName, rule string) func(*types.Frontend) { return func(fe *types.Frontend) { fe.Routes[routeName] = types.Route{Rule: rule} } } func buildBackend(backendBuilders ...func(*types.Backend)) *types.Backend { be := &types.Backend{ Servers: make(map[string]types.Server), LoadBalancer: &types.LoadBalancer{Method: "Wrr"}, } for _, build := range backendBuilders { build(be) } return be } func withServer(name, url string) func(backend *types.Backend) { return func(be *types.Backend) { be.Servers[name] = types.Server{URL: url} } } func withLoadBalancer(method string, sticky bool) func(*types.Backend) { return func(be *types.Backend) { if sticky { be.LoadBalancer = &types.LoadBalancer{Method: method, Stickiness: &types.Stickiness{CookieName: "test"}} } else { be.LoadBalancer = &types.LoadBalancer{Method: method} } } }