From 1e44e339ad0b65fe6ee63d2a8ccc719463cf7649 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Thu, 21 Dec 2017 14:16:03 +0100 Subject: [PATCH 01/12] Allow deleting dynamically all TLS certificates from an entryPoint --- .../fixtures/https/dynamic_https_sni.toml | 3 + integration/https_test.go | 98 ++++++++++++++++++- server/server.go | 6 +- 3 files changed, 101 insertions(+), 6 deletions(-) diff --git a/integration/fixtures/https/dynamic_https_sni.toml b/integration/fixtures/https/dynamic_https_sni.toml index 19471cc8a..3c5b84da4 100644 --- a/integration/fixtures/https/dynamic_https_sni.toml +++ b/integration/fixtures/https/dynamic_https_sni.toml @@ -6,6 +6,9 @@ defaultEntryPoints = ["https"] [entryPoints.https] address = ":4443" [entryPoints.https.tls] + [entryPoints.https02] + address = ":8443" + [entryPoints.https02.tls] [web] address = ":8080" diff --git a/integration/https_test.go b/integration/https_test.go index d657eca14..ab576b973 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -353,7 +353,7 @@ func startTestServer(port string, statusCode int) (ts *httptest.Server) { return ts } -// TestWithSNIConfigRoute involves a client sending HTTPS requests with +// TestWithSNIDynamicConfigRouteWithNoChange involves a client sending HTTPS requests with // SNI hostnames of "snitest.org" and "snitest.com". The test verifies // that traefik routes the requests to the expected backends thanks to given certificate if possible // otherwise thanks to the default one. @@ -424,7 +424,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { c.Assert(resp.StatusCode, checker.Equals, http.StatusNoContent) } -// TestWithSNIConfigRoute involves a client sending HTTPS requests with +// TestWithSNIDynamicConfigRouteWithChange involves a client sending HTTPS requests with // SNI hostnames of "snitest.org" and "snitest.com". The test verifies // that traefik updates its configuration when the HTTPS configuration is modified and // it routes the requests to the expected backends thanks to given certificate if possible @@ -479,7 +479,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { req.Header.Set("Accept", "*/*") // Change certificates configuration file content - modifyCertificateConfFileContent(c, tr1.TLSClientConfig.ServerName, dynamicConfFileName) + modifyCertificateConfFileContent(c, tr1.TLSClientConfig.ServerName, dynamicConfFileName, "https") var resp *http.Response err = try.Do(30*time.Second, func() error { resp, err = client.Do(req) @@ -525,8 +525,96 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) } +// TestWithSNIDynamicConfigRouteWithChangeForEmptyTlsConfiguration involves a client sending HTTPS requests with +// SNI hostnames of "snitest.org" and "snitest.com". The test verifies +// that traefik updates its configuration when the HTTPS configuration is modified, even if it totally deleted, and +// it routes the requests to the expected backends thanks to given certificate if possible +// otherwise thanks to the default one. +func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c *check.C) { + dynamicConfFileName := s.adaptFile(c, "fixtures/https/dynamic_https.toml", struct{}{}) + defer os.Remove(dynamicConfFileName) + confFileName := s.adaptFile(c, "fixtures/https/dynamic_https_sni.toml", struct { + DynamicConfFileName string + }{ + DynamicConfFileName: dynamicConfFileName, + }) + defer os.Remove(confFileName) + cmd, display := s.traefikCmd(withConfigFile(confFileName)) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + tr2 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.org", + }, + } + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:"+tr2.TLSClientConfig.ServerName)) + c.Assert(err, checker.IsNil) + + backend2 := startTestServer("9020", http.StatusResetContent) + + defer backend2.Close() + + err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent)) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + client := &http.Client{Transport: tr2} + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + + var resp *http.Response + err = try.Do(30*time.Second, func() error { + resp, err = client.Do(req) + + // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\ + req.Close = true + + if err != nil { + return err + } + + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + if cn != tr2.TLSClientConfig.ServerName { + return fmt.Errorf("domain %s found in place of %s", cn, tr2.TLSClientConfig.ServerName) + } + + return nil + }) + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) + // Change certificates configuration file content + modifyCertificateConfFileContent(c, "snitest.com", dynamicConfFileName, "https02") + + err = try.Do(60*time.Second, func() error { + resp, err = client.Do(req) + + // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\ + req.Close = true + + if err != nil { + return err + } + + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + if cn == tr2.TLSClientConfig.ServerName { + return fmt.Errorf("domain %s found in place of default one", tr2.TLSClientConfig.ServerName) + } + + return nil + }) + c.Assert(err, checker.IsNil) + c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) +} + // modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. -func modifyCertificateConfFileContent(c *check.C, certFileName, confFileName string) { +func modifyCertificateConfFileContent(c *check.C, certFileName, confFileName, entryPoint string) { tlsConf := types.Configuration{ TLSConfiguration: []*traefikTls.Configuration{ { @@ -534,7 +622,7 @@ func modifyCertificateConfFileContent(c *check.C, certFileName, confFileName str CertFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".cert"), KeyFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".key"), }, - EntryPoints: []string{"https"}, + EntryPoints: []string{entryPoint}, }, }, } diff --git a/server/server.go b/server/server.go index b9ab28535..be79edf96 100644 --- a/server/server.go +++ b/server/server.go @@ -439,7 +439,11 @@ func (s *Server) loadConfiguration(configMsg types.ConfigMessage) { if err == nil { for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints { s.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler()) - if &newServerEntryPoint.certs != nil { + if s.globalConfiguration.EntryPoints[newServerEntryPointName].TLS == nil { + if newServerEntryPoint.certs.Get() != nil { + log.Debugf("Certificates not added to non-TLS entryPoint %s.", newServerEntryPointName) + } + } else { s.serverEntryPoints[newServerEntryPointName].certs.Set(newServerEntryPoint.certs.Get()) } log.Infof("Server configuration reloaded on %s", s.serverEntryPoints[newServerEntryPointName].httpServer.Addr) From e8333883df2b2d3ca4ab13410980dc100600cbff Mon Sep 17 00:00:00 2001 From: lishaoxiong Date: Fri, 22 Dec 2017 01:02:04 +0800 Subject: [PATCH 02/12] Add tests for TLS dynamic configuration in ETCD3 --- integration/etcd3_test.go | 107 +++++++++++++++++++++++++++++++++++ integration/try/condition.go | 19 +++++++ 2 files changed, 126 insertions(+) diff --git a/integration/etcd3_test.go b/integration/etcd3_test.go index 48645ac4a..df03bab12 100644 --- a/integration/etcd3_test.go +++ b/integration/etcd3_test.go @@ -573,6 +573,113 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) { req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") resp, err = client.Do(req) + c.Assert(err, checker.IsNil) cn = resp.TLS.PeerCertificates[0].Subject.CommonName c.Assert(cn, checker.Equals, "snitest.org") } + +func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) { + // start Træfik + cmd, display := s.traefikCmd( + withConfigFile("fixtures/etcd/simple_https.toml"), + "--etcd", + "--etcd.endpoint="+ipEtcd+":4001", + "--etcd.useAPIV3=true") + defer display(c) + + // prepare to config + snitestComCert, err := ioutil.ReadFile("fixtures/https/snitest.com.cert") + c.Assert(err, checker.IsNil) + snitestComKey, err := ioutil.ReadFile("fixtures/https/snitest.com.key") + c.Assert(err, checker.IsNil) + + backend1 := map[string]string{ + "/traefik/backends/backend1/circuitbreaker/expression": "NetworkErrorRatio() > 0.5", + "/traefik/backends/backend1/servers/server1/url": "http://" + ipWhoami01 + ":80", + "/traefik/backends/backend1/servers/server1/weight": "1", + "/traefik/backends/backend1/servers/server2/url": "http://" + ipWhoami02 + ":80", + "/traefik/backends/backend1/servers/server2/weight": "1", + } + + frontend1 := map[string]string{ + "/traefik/frontends/frontend1/backend": "backend1", + "/traefik/frontends/frontend1/entrypoints": "https", + "/traefik/frontends/frontend1/priority": "1", + "/traefik/frontends/frontend1/routes/test_1/rule": "Host:snitest.com", + } + + tlsconfigure1 := map[string]string{ + "/traefik/tlsconfiguration/snitestcom/entrypoints": "https", + "/traefik/tlsconfiguration/snitestcom/certificate/keyfile": string(snitestComKey), + "/traefik/tlsconfiguration/snitestcom/certificate/certfile": string(snitestComCert), + } + + // config backends,frontends and first tls keypair + for key, value := range backend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range frontend1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + for key, value := range tlsconfigure1 { + err := s.kv.Put(key, []byte(value), nil) + c.Assert(err, checker.IsNil) + } + + tr1 := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: "snitest.com", + }, + } + + // wait for etcd + err = try.Do(60*time.Second, func() error { + _, err := s.kv.Get("/traefik/tlsconfiguration/snitestcom/certificate/keyfile", nil) + return err + }) + c.Assert(err, checker.IsNil) + + err = cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Træfik + err = try.GetRequest(traefikWebEtcdURL+"api/providers", 60*time.Second, try.BodyContains(string("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h"))) + c.Assert(err, checker.IsNil) + + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client := &http.Client{Transport: tr1} + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + var resp *http.Response + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + cn := resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "snitest.com") + + // now we delete the tls cert/key pairs,so the endpoint show use default cert/key pair + for key := range tlsconfigure1 { + err := s.kv.Delete(key) + c.Assert(err, checker.IsNil) + } + + // waiting for Træfik to pull configuration + err = try.GetRequest(traefikWebEtcdURL+"api/providers", 30*time.Second, try.BodyNotContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7h")) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) + client = &http.Client{Transport: tr1} + req.Host = tr1.TLSClientConfig.ServerName + req.Header.Set("Host", tr1.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") + resp, err = client.Do(req) + c.Assert(err, checker.IsNil) + cn = resp.TLS.PeerCertificates[0].Subject.CommonName + c.Assert(cn, checker.Equals, "TRAEFIK DEFAULT CERT") +} diff --git a/integration/try/condition.go b/integration/try/condition.go index bff63b0cb..93010631f 100644 --- a/integration/try/condition.go +++ b/integration/try/condition.go @@ -34,6 +34,25 @@ func BodyContains(values ...string) ResponseCondition { } } +// BodyNotContains returns a retry condition function. +// The condition returns an error if the request body contain one of the given +// strings. +func BodyNotContains(values ...string) ResponseCondition { + return func(res *http.Response) error { + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %s", err) + } + + for _, value := range values { + if strings.Contains(string(body), value) { + return fmt.Errorf("find '%s' in body '%s'", value, string(body)) + } + } + return nil + } +} + // BodyContainsOr returns a retry condition function. // The condition returns an error if the request body does not contain one of the given // strings. From 9a7821b8fac6cb9e130774a99721648d93f26c84 Mon Sep 17 00:00:00 2001 From: NicoMen Date: Thu, 21 Dec 2017 21:24:03 +0100 Subject: [PATCH 03/12] Send empty configuration from file provider --- integration/https_test.go | 41 +++++++++++++++++++++------------------ provider/file/file.go | 10 ++++++---- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/integration/https_test.go b/integration/https_test.go index ab576b973..2582dfcda 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -590,7 +590,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c c.Assert(err, checker.IsNil) c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) // Change certificates configuration file content - modifyCertificateConfFileContent(c, "snitest.com", dynamicConfFileName, "https02") + modifyCertificateConfFileContent(c, "", dynamicConfFileName, "https02") err = try.Do(60*time.Second, func() error { resp, err = client.Do(req) @@ -615,28 +615,31 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c // modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. func modifyCertificateConfFileContent(c *check.C, certFileName, confFileName, entryPoint string) { - tlsConf := types.Configuration{ - TLSConfiguration: []*traefikTls.Configuration{ - { - Certificate: &traefikTls.Certificate{ - CertFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".cert"), - KeyFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".key"), - }, - EntryPoints: []string{entryPoint}, - }, - }, - } - var confBuffer bytes.Buffer - e := toml.NewEncoder(&confBuffer) - err := e.Encode(tlsConf) - c.Assert(err, checker.IsNil) - f, err := os.OpenFile("./"+confFileName, os.O_WRONLY, os.ModeExclusive) c.Assert(err, checker.IsNil) defer func() { f.Close() }() f.Truncate(0) - _, err = f.Write(confBuffer.Bytes()) - c.Assert(err, checker.IsNil) + // If certificate file is not provided, just truncate the configuration file + if len(certFileName) > 0 { + tlsConf := types.Configuration{ + TLSConfiguration: []*traefikTls.Configuration{ + { + Certificate: &traefikTls.Certificate{ + CertFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".cert"), + KeyFile: traefikTls.FileOrContent("fixtures/https/" + certFileName + ".key"), + }, + EntryPoints: []string{entryPoint}, + }, + }, + } + var confBuffer bytes.Buffer + e := toml.NewEncoder(&confBuffer) + err := e.Encode(tlsConf) + c.Assert(err, checker.IsNil) + + _, err = f.Write(confBuffer.Bytes()) + c.Assert(err, checker.IsNil) + } } diff --git a/provider/file/file.go b/provider/file/file.go index 918c7cf4d..06e25bb2c 100644 --- a/provider/file/file.go +++ b/provider/file/file.go @@ -126,7 +126,10 @@ func sendConfigToChannel(configurationChan chan<- types.ConfigMessage, configura } func loadFileConfig(filename string) (*types.Configuration, error) { - configuration := new(types.Configuration) + configuration := &types.Configuration{ + Frontends: make(map[string]*types.Frontend), + Backends: make(map[string]*types.Backend), + } if _, err := toml.DecodeFile(filename, configuration); err != nil { return nil, fmt.Errorf("error reading configuration file: %s", err) } @@ -142,9 +145,8 @@ func loadFileConfigFromDirectory(directory string, configuration *types.Configur if configuration == nil { configuration = &types.Configuration{ - Frontends: make(map[string]*types.Frontend), - Backends: make(map[string]*types.Backend), - TLSConfiguration: make([]*tls.Configuration, 0), + Frontends: make(map[string]*types.Frontend), + Backends: make(map[string]*types.Backend), } } From 75533b2beb9db76f811057975ab8f36e39785804 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 2 Jan 2018 09:44:02 +0100 Subject: [PATCH 04/12] Use prefix for sticky and stickiness tags. --- provider/consul/consul_catalog.go | 6 +- types/common_label.go | 131 +++++++++++++++--------------- 2 files changed, 70 insertions(+), 67 deletions(-) diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index b87d63e90..97000ed67 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -430,7 +430,7 @@ func (p *CatalogProvider) getBasicAuth(tags []string) []string { } func (p *CatalogProvider) getSticky(tags []string) string { - stickyTag := p.getTag(types.LabelBackendLoadbalancerSticky, tags, "") + stickyTag := p.getAttribute(types.SuffixBackendLoadBalancerSticky, tags, "") if len(stickyTag) > 0 { log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) } else { @@ -440,12 +440,12 @@ func (p *CatalogProvider) getSticky(tags []string) string { } func (p *CatalogProvider) hasStickinessLabel(tags []string) bool { - stickinessTag := p.getTag(types.LabelBackendLoadbalancerStickiness, tags, "") + stickinessTag := p.getAttribute(types.SuffixBackendLoadBalancerStickiness, tags, "") return len(stickinessTag) > 0 && strings.EqualFold(strings.TrimSpace(stickinessTag), "true") } func (p *CatalogProvider) getStickinessCookieName(tags []string) string { - return p.getTag(types.LabelBackendLoadbalancerStickinessCookieName, tags, "") + return p.getAttribute(types.SuffixBackendLoadBalancerStickinessCookieName, tags, "") } func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue string) string { diff --git a/types/common_label.go b/types/common_label.go index f0bc3ffd5..dbe1e017e 100644 --- a/types/common_label.go +++ b/types/common_label.go @@ -4,70 +4,73 @@ import "strings" // Traefik labels const ( - LabelPrefix = "traefik." - SuffixPort = "port" - SuffixProtocol = "protocol" - SuffixWeight = "weight" - SuffixFrontendAuthBasic = "frontend.auth.basic" - SuffixFrontendBackend = "frontend.backend" - SuffixFrontendEntryPoints = "frontend.entryPoints" - SuffixFrontendPassHostHeader = "frontend.passHostHeader" - SuffixFrontendPriority = "frontend.priority" - SuffixFrontendRedirectEntryPoint = "frontend.redirect.entryPoint" - SuffixFrontendRedirectRegex = "frontend.redirect.regex" - SuffixFrontendRedirectReplacement = "frontend.redirect.replacement" - SuffixFrontendRule = "frontend.rule" - LabelDomain = LabelPrefix + "domain" - LabelEnable = LabelPrefix + "enable" - LabelPort = LabelPrefix + SuffixPort - LabelPortIndex = LabelPrefix + "portIndex" - LabelProtocol = LabelPrefix + SuffixProtocol - LabelTags = LabelPrefix + "tags" - LabelWeight = LabelPrefix + SuffixWeight - LabelFrontendAuthBasic = LabelPrefix + SuffixFrontendAuthBasic - LabelFrontendEntryPoints = LabelPrefix + SuffixFrontendEntryPoints - LabelFrontendPassHostHeader = LabelPrefix + SuffixFrontendPassHostHeader - LabelFrontendPassTLSCert = LabelPrefix + "frontend.passTLSCert" - LabelFrontendPriority = LabelPrefix + SuffixFrontendPriority - LabelFrontendRule = LabelPrefix + SuffixFrontendRule - LabelFrontendRuleType = LabelPrefix + "frontend.rule.type" - LabelFrontendRedirectEntryPoint = LabelPrefix + SuffixFrontendRedirectEntryPoint - LabelFrontendRedirectRegex = LabelPrefix + SuffixFrontendRedirectRegex - LabelFrontendRedirectReplacement = LabelPrefix + SuffixFrontendRedirectReplacement - LabelTraefikFrontendValue = LabelPrefix + "frontend.value" - LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange" - LabelFrontendRequestHeaders = LabelPrefix + "frontend.headers.customRequestHeaders" - LabelFrontendResponseHeaders = LabelPrefix + "frontend.headers.customResponseHeaders" - LabelFrontendAllowedHosts = LabelPrefix + "frontend.headers.allowedHosts" - LabelFrontendHostsProxyHeaders = LabelPrefix + "frontend.headers.hostsProxyHeaders" - LabelFrontendSSLRedirect = LabelPrefix + "frontend.headers.SSLRedirect" - LabelFrontendSSLTemporaryRedirect = LabelPrefix + "frontend.headers.SSLTemporaryRedirect" - LabelFrontendSSLHost = LabelPrefix + "frontend.headers.SSLHost" - LabelFrontendSSLProxyHeaders = LabelPrefix + "frontend.headers.SSLProxyHeaders" - LabelFrontendSTSSeconds = LabelPrefix + "frontend.headers.STSSeconds" - LabelFrontendSTSIncludeSubdomains = LabelPrefix + "frontend.headers.STSIncludeSubdomains" - LabelFrontendSTSPreload = LabelPrefix + "frontend.headers.STSPreload" - LabelFrontendForceSTSHeader = LabelPrefix + "frontend.headers.forceSTSHeader" - LabelFrontendFrameDeny = LabelPrefix + "frontend.headers.frameDeny" - LabelFrontendCustomFrameOptionsValue = LabelPrefix + "frontend.headers.customFrameOptionsValue" - LabelFrontendContentTypeNosniff = LabelPrefix + "frontend.headers.contentTypeNosniff" - LabelFrontendBrowserXSSFilter = LabelPrefix + "frontend.headers.browserXSSFilter" - LabelFrontendContentSecurityPolicy = LabelPrefix + "frontend.headers.contentSecurityPolicy" - LabelFrontendPublicKey = LabelPrefix + "frontend.headers.publicKey" - LabelFrontendReferrerPolicy = LabelPrefix + "frontend.headers.referrerPolicy" - LabelFrontendIsDevelopment = LabelPrefix + "frontend.headers.isDevelopment" - LabelBackend = LabelPrefix + "backend" - LabelBackendID = LabelPrefix + "backend.id" - LabelTraefikBackendCircuitbreaker = LabelPrefix + "backend.circuitbreaker" - LabelBackendCircuitbreakerExpression = LabelPrefix + "backend.circuitbreaker.expression" - LabelBackendHealthcheckPath = LabelPrefix + "backend.healthcheck.path" - LabelBackendHealthcheckInterval = LabelPrefix + "backend.healthcheck.interval" - LabelBackendLoadbalancerMethod = LabelPrefix + "backend.loadbalancer.method" - LabelBackendLoadbalancerSticky = LabelPrefix + "backend.loadbalancer.sticky" - LabelBackendLoadbalancerStickiness = LabelPrefix + "backend.loadbalancer.stickiness" - LabelBackendLoadbalancerStickinessCookieName = LabelPrefix + "backend.loadbalancer.stickiness.cookieName" - LabelBackendMaxconnAmount = LabelPrefix + "backend.maxconn.amount" - LabelBackendMaxconnExtractorfunc = LabelPrefix + "backend.maxconn.extractorfunc" + LabelPrefix = "traefik." + SuffixPort = "port" + SuffixProtocol = "protocol" + SuffixWeight = "weight" + SuffixFrontendAuthBasic = "frontend.auth.basic" + SuffixFrontendBackend = "frontend.backend" + SuffixFrontendEntryPoints = "frontend.entryPoints" + SuffixFrontendPassHostHeader = "frontend.passHostHeader" + SuffixFrontendPriority = "frontend.priority" + SuffixFrontendRedirectEntryPoint = "frontend.redirect.entryPoint" + SuffixFrontendRedirectRegex = "frontend.redirect.regex" + SuffixFrontendRedirectReplacement = "frontend.redirect.replacement" + SuffixFrontendRule = "frontend.rule" + SuffixBackendLoadBalancerSticky = "backend.loadbalancer.sticky" + SuffixBackendLoadBalancerStickiness = "backend.loadbalancer.stickiness" + SuffixBackendLoadBalancerStickinessCookieName = "backend.loadbalancer.stickiness.cookieName" + LabelDomain = LabelPrefix + "domain" + LabelEnable = LabelPrefix + "enable" + LabelPort = LabelPrefix + SuffixPort + LabelPortIndex = LabelPrefix + "portIndex" + LabelProtocol = LabelPrefix + SuffixProtocol + LabelTags = LabelPrefix + "tags" + LabelWeight = LabelPrefix + SuffixWeight + LabelFrontendAuthBasic = LabelPrefix + SuffixFrontendAuthBasic + LabelFrontendEntryPoints = LabelPrefix + SuffixFrontendEntryPoints + LabelFrontendPassHostHeader = LabelPrefix + SuffixFrontendPassHostHeader + LabelFrontendPassTLSCert = LabelPrefix + "frontend.passTLSCert" + LabelFrontendPriority = LabelPrefix + SuffixFrontendPriority + LabelFrontendRule = LabelPrefix + SuffixFrontendRule + LabelFrontendRuleType = LabelPrefix + "frontend.rule.type" + LabelFrontendRedirectEntryPoint = LabelPrefix + SuffixFrontendRedirectEntryPoint + LabelFrontendRedirectRegex = LabelPrefix + SuffixFrontendRedirectRegex + LabelFrontendRedirectReplacement = LabelPrefix + SuffixFrontendRedirectReplacement + LabelTraefikFrontendValue = LabelPrefix + "frontend.value" + LabelTraefikFrontendWhitelistSourceRange = LabelPrefix + "frontend.whitelistSourceRange" + LabelFrontendRequestHeaders = LabelPrefix + "frontend.headers.customRequestHeaders" + LabelFrontendResponseHeaders = LabelPrefix + "frontend.headers.customResponseHeaders" + LabelFrontendAllowedHosts = LabelPrefix + "frontend.headers.allowedHosts" + LabelFrontendHostsProxyHeaders = LabelPrefix + "frontend.headers.hostsProxyHeaders" + LabelFrontendSSLRedirect = LabelPrefix + "frontend.headers.SSLRedirect" + LabelFrontendSSLTemporaryRedirect = LabelPrefix + "frontend.headers.SSLTemporaryRedirect" + LabelFrontendSSLHost = LabelPrefix + "frontend.headers.SSLHost" + LabelFrontendSSLProxyHeaders = LabelPrefix + "frontend.headers.SSLProxyHeaders" + LabelFrontendSTSSeconds = LabelPrefix + "frontend.headers.STSSeconds" + LabelFrontendSTSIncludeSubdomains = LabelPrefix + "frontend.headers.STSIncludeSubdomains" + LabelFrontendSTSPreload = LabelPrefix + "frontend.headers.STSPreload" + LabelFrontendForceSTSHeader = LabelPrefix + "frontend.headers.forceSTSHeader" + LabelFrontendFrameDeny = LabelPrefix + "frontend.headers.frameDeny" + LabelFrontendCustomFrameOptionsValue = LabelPrefix + "frontend.headers.customFrameOptionsValue" + LabelFrontendContentTypeNosniff = LabelPrefix + "frontend.headers.contentTypeNosniff" + LabelFrontendBrowserXSSFilter = LabelPrefix + "frontend.headers.browserXSSFilter" + LabelFrontendContentSecurityPolicy = LabelPrefix + "frontend.headers.contentSecurityPolicy" + LabelFrontendPublicKey = LabelPrefix + "frontend.headers.publicKey" + LabelFrontendReferrerPolicy = LabelPrefix + "frontend.headers.referrerPolicy" + LabelFrontendIsDevelopment = LabelPrefix + "frontend.headers.isDevelopment" + LabelBackend = LabelPrefix + "backend" + LabelBackendID = LabelPrefix + "backend.id" + LabelTraefikBackendCircuitbreaker = LabelPrefix + "backend.circuitbreaker" + LabelBackendCircuitbreakerExpression = LabelPrefix + "backend.circuitbreaker.expression" + LabelBackendHealthcheckPath = LabelPrefix + "backend.healthcheck.path" + LabelBackendHealthcheckInterval = LabelPrefix + "backend.healthcheck.interval" + LabelBackendLoadbalancerMethod = LabelPrefix + "backend.loadbalancer.method" + LabelBackendLoadbalancerSticky = LabelPrefix + SuffixBackendLoadBalancerSticky + LabelBackendLoadbalancerStickiness = LabelPrefix + SuffixBackendLoadBalancerStickiness + LabelBackendLoadbalancerStickinessCookieName = LabelPrefix + SuffixBackendLoadBalancerStickinessCookieName + LabelBackendMaxconnAmount = LabelPrefix + "backend.maxconn.amount" + LabelBackendMaxconnExtractorfunc = LabelPrefix + "backend.maxconn.extractorfunc" ) //ServiceLabel converts a key value of Label*, given a serviceName, into a pattern .. From bfd142b13b69988eecd47861c4f79b7368b26bd6 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 2 Jan 2018 10:10:04 +0100 Subject: [PATCH 05/12] Fix custom headers template --- autogen/gentemplates/gen.go | 77 ++++++++++++++------------ middlewares/headers.go | 12 ++-- middlewares/headers_test.go | 10 ++-- middlewares/secure.go | 6 +- provider/docker/docker.go | 13 +++++ provider/kubernetes/kubernetes.go | 4 +- provider/kubernetes/kubernetes_test.go | 24 ++++++++ server/server.go | 10 ++-- templates/docker.tmpl | 25 +++++---- templates/kubernetes.tmpl | 52 +++++++++-------- types/types.go | 14 ++--- 11 files changed, 151 insertions(+), 96 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index cbe9b72a0..e4b479f9a 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -219,6 +219,7 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} replacement = "{{getRedirectReplacement $container}}" {{end}} + {{ if hasHeaders $container}} [frontends."frontend-{{$frontend}}".headers] {{if hasSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}} @@ -265,6 +266,16 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{if hasIsDevelopmentHeaders $container}} IsDevelopment = {{getIsDevelopmentHeaders $container}} {{end}} + {{if hasAllowedHostsHeaders $container}} + AllowedHosts = [{{range getAllowedHostsHeaders $container}} + "{{.}}", + {{end}}] + {{end}} + {{if hasHostsProxyHeaders $container}} + HostsProxyHeaders = [{{range getHostsProxyHeaders $container}} + "{{.}}", + {{end}}] + {{end}} {{if hasRequestHeaders $container}} [frontends."frontend-{{$frontend}}".headers.customrequestheaders] {{range $k, $v := getRequestHeaders $container}} @@ -277,24 +288,14 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}} {{$k}} = "{{$v}}" {{end}} {{end}} - {{if hasAllowedHostsHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.AllowedHosts] - {{range getAllowedHostsHeaders $container}} - "{{.}}" - {{end}} - {{end}} - {{if hasHostsProxyHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.HostsProxyHeaders] - {{range getHostsProxyHeaders $container}} - "{{.}}" - {{end}} - {{end}} {{if hasSSLProxyHeaders $container}} [frontends."frontend-{{$frontend}}".headers.SSLProxyHeaders] {{range $k, $v := getSSLProxyHeaders $container}} {{$k}} = "{{$v}}" {{end}} {{end}} + {{end}} + [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] rule = "{{getFrontendRule $container}}" {{end}} @@ -445,6 +446,7 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend replacement = "{{$frontend.RedirectReplacement}}" {{end}} + {{ if $frontend.Headers }} [frontends."{{$frontendName}}".headers] SSLRedirect = {{$frontend.Headers.SSLRedirect}} SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}} @@ -461,40 +463,45 @@ var _templatesKubernetesTmpl = []byte(`[backends]{{range $backendName, $backend PublicKey = "{{$frontend.Headers.PublicKey}}" ReferrerPolicy = "{{$frontend.Headers.ReferrerPolicy}}" IsDevelopment = {{$frontend.Headers.IsDevelopment}} -{{if $frontend.Headers.CustomRequestHeaders}} - [frontends."{{$frontendName}}".headers.customrequestheaders] + + {{if $frontend.Headers.AllowedHosts}} + AllowedHosts = [{{range $frontend.Headers.AllowedHosts}} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Headers.HostsProxyHeaders}} + HostsProxyHeaders = [{{range $frontend.Headers.HostsProxyHeaders}} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Headers.CustomRequestHeaders}} + [frontends."{{$frontendName}}".headers.customrequestheaders] {{range $k, $v := $frontend.Headers.CustomRequestHeaders}} {{$k}} = "{{$v}}" {{end}} -{{end}} -{{if $frontend.Headers.CustomResponseHeaders}} - [frontends."{{$frontendName}}".headers.customresponseheaders] + {{end}} + + {{if $frontend.Headers.CustomResponseHeaders}} + [frontends."{{$frontendName}}".headers.customresponseheaders] {{range $k, $v := $frontend.Headers.CustomResponseHeaders}} {{$k}} = "{{$v}}" {{end}} -{{end}} -{{if $frontend.Headers.AllowedHosts}} - [frontends."{{$frontendName}}".headers.AllowedHosts] - {{range $frontend.Headers.AllowedHosts}} - "{{.}}" - {{end}} -{{end}} -{{if $frontend.Headers.HostsProxyHeaders}} - [frontends."{{$frontendName}}".headers.HostsProxyHeaders] - {{range $frontend.Headers.HostsProxyHeaders}} - "{{.}}" - {{end}} -{{end}} -{{if $frontend.Headers.SSLProxyHeaders}} - [frontends."{{$frontendName}}".headers.SSLProxyHeaders] + {{end}} + + {{if $frontend.Headers.SSLProxyHeaders}} + [frontends."{{$frontendName}}".headers.SSLProxyHeaders] {{range $k, $v := $frontend.Headers.SSLProxyHeaders}} {{$k}} = "{{$v}}" {{end}} + {{end}} {{end}} - {{range $routeName, $route := $frontend.Routes}} - [frontends."{{$frontendName}}".routes."{{$routeName}}"] + + {{range $routeName, $route := $frontend.Routes}} + [frontends."{{$frontendName}}".routes."{{$routeName}}"] rule = "{{$route.Rule}}" - {{end}} + {{end}} {{end}} `) diff --git a/middlewares/headers.go b/middlewares/headers.go index 6ddef1ca2..0bc2cf997 100644 --- a/middlewares/headers.go +++ b/middlewares/headers.go @@ -24,14 +24,16 @@ type HeaderStruct struct { } // NewHeaderFromStruct constructs a new header instance from supplied frontend header struct. -func NewHeaderFromStruct(headers types.Headers) *HeaderStruct { - o := HeaderOptions{ - CustomRequestHeaders: headers.CustomRequestHeaders, - CustomResponseHeaders: headers.CustomResponseHeaders, +func NewHeaderFromStruct(headers *types.Headers) *HeaderStruct { + if headers == nil || !headers.HasCustomHeadersDefined() { + return nil } return &HeaderStruct{ - opt: o, + opt: HeaderOptions{ + CustomRequestHeaders: headers.CustomRequestHeaders, + CustomResponseHeaders: headers.CustomResponseHeaders, + }, } } diff --git a/middlewares/headers_test.go b/middlewares/headers_test.go index 011859837..7520353f7 100644 --- a/middlewares/headers_test.go +++ b/middlewares/headers_test.go @@ -17,16 +17,14 @@ var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // newHeader constructs a new header instance with supplied options. func newHeader(options ...HeaderOptions) *HeaderStruct { - var o HeaderOptions + var opt HeaderOptions if len(options) == 0 { - o = HeaderOptions{} + opt = HeaderOptions{} } else { - o = options[0] + opt = options[0] } - return &HeaderStruct{ - opt: o, - } + return &HeaderStruct{opt: opt} } func TestNoConfig(t *testing.T) { diff --git a/middlewares/secure.go b/middlewares/secure.go index ba40c1886..f012143ba 100644 --- a/middlewares/secure.go +++ b/middlewares/secure.go @@ -6,7 +6,11 @@ import ( ) // NewSecure constructs a new Secure instance with supplied options. -func NewSecure(headers types.Headers) *secure.Secure { +func NewSecure(headers *types.Headers) *secure.Secure { + if headers == nil || !headers.HasSecureHeadersDefined() { + return nil + } + opt := secure.Options{ AllowedHosts: headers.AllowedHosts, HostsProxyHeaders: headers.HostsProxyHeaders, diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 9300c3a9c..bee27d225 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -290,6 +290,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con "getServiceBackend": getServiceBackend, "getWhitelistSourceRange": getFuncSliceStringLabel(types.LabelTraefikFrontendWhitelistSourceRange), + "hasHeaders": hasHeaders, "hasRequestHeaders": hasLabel(types.LabelFrontendRequestHeaders), "getRequestHeaders": getFuncMapLabel(types.LabelFrontendRequestHeaders), "hasResponseHeaders": hasLabel(types.LabelFrontendResponseHeaders), @@ -895,3 +896,15 @@ func hasRedirect(container dockerData) bool { return hasLabel(types.LabelFrontendRedirectEntryPoint)(container) || hasLabel(types.LabelFrontendRedirectReplacement)(container) && hasLabel(types.LabelFrontendRedirectRegex)(container) } + +// TODO will be rewrite when merge on master +func hasHeaders(container dockerData) bool { + // LabelPrefix + "frontend.headers. + + for key := range container.Labels { + if strings.HasPrefix(key, types.LabelPrefix+"frontend.headers.") { + return true + } + } + return false +} diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index cd577ce74..a1367977d 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -208,7 +208,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) priority := getPriority(i) - headers := types.Headers{ + headers := &types.Headers{ CustomRequestHeaders: getMapAnnotation(i, annotationKubernetesCustomRequestHeaders), CustomResponseHeaders: getMapAnnotation(i, annotationKubernetesCustomResponseHeaders), AllowedHosts: getSliceAnnotation(i, annotationKubernetesAllowedHosts), @@ -364,7 +364,7 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config } func getSTSSeconds(i *v1beta1.Ingress) int64 { - value, err := strconv.ParseInt(i.ObjectMeta.Annotations[annotationKubernetesHSTSMaxAge], 10, 64) + value, err := strconv.ParseInt(i.Annotations[annotationKubernetesHSTSMaxAge], 10, 64) if err == nil && value > 0 { return value } diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 2aeebe4c0..cb206fb17 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -280,6 +280,7 @@ func TestLoadIngresses(t *testing.T) { "foo/bar": { Backend: "foo/bar", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -292,6 +293,7 @@ func TestLoadIngresses(t *testing.T) { "foo/namedthing": { Backend: "foo/namedthing", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/namedthing": { Rule: "PathPrefix:/namedthing", @@ -304,6 +306,7 @@ func TestLoadIngresses(t *testing.T) { "bar": { Backend: "bar", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "bar": { Rule: "Host:bar", @@ -409,6 +412,7 @@ func TestRuleType(t *testing.T) { expected := map[string]*types.Frontend{ "host/path": { Backend: "host/path", + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/path": { Rule: fmt.Sprintf("%s:/path", test.frontendRuleType), @@ -494,6 +498,7 @@ func TestGetPassHostHeader(t *testing.T) { Frontends: map[string]*types.Frontend{ "foo/bar": { Backend: "foo/bar", + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -580,6 +585,7 @@ func TestGetPassTLSCert(t *testing.T) { Backend: "foo/bar", PassTLSCert: true, PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -682,6 +688,7 @@ func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { "foo": { Backend: "foo", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "foo": { Rule: "Host:foo", @@ -762,6 +769,7 @@ func TestHostlessIngress(t *testing.T) { Frontends: map[string]*types.Frontend{ "/bar": { Backend: "/bar", + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -980,6 +988,7 @@ func TestServiceAnnotations(t *testing.T) { "foo/bar": { Backend: "foo/bar", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -992,6 +1001,7 @@ func TestServiceAnnotations(t *testing.T) { "bar": { Backend: "bar", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "bar": { Rule: "Host:bar", @@ -1445,6 +1455,7 @@ func TestIngressAnnotations(t *testing.T) { "foo/bar": { Backend: "foo/bar", PassHostHeader: false, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -1457,6 +1468,7 @@ func TestIngressAnnotations(t *testing.T) { "other/stuff": { Backend: "other/stuff", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/stuff": { Rule: "PathPrefix:/stuff", @@ -1469,6 +1481,7 @@ func TestIngressAnnotations(t *testing.T) { "other/": { Backend: "other/", PassHostHeader: true, + Headers: &types.Headers{}, EntryPoints: []string{"http", "https"}, Routes: map[string]types.Route{ "/": { @@ -1483,6 +1496,7 @@ func TestIngressAnnotations(t *testing.T) { Backend: "other/sslstuff", PassHostHeader: true, PassTLSCert: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/sslstuff": { Rule: "PathPrefix:/sslstuff", @@ -1495,6 +1509,7 @@ func TestIngressAnnotations(t *testing.T) { "basic/auth": { Backend: "basic/auth", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/auth": { Rule: "PathPrefix:/auth", @@ -1508,6 +1523,7 @@ func TestIngressAnnotations(t *testing.T) { "redirect/https": { Backend: "redirect/https", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/https": { Rule: "PathPrefix:/https", @@ -1524,6 +1540,7 @@ func TestIngressAnnotations(t *testing.T) { "test/whitelist-source-range": { Backend: "test/whitelist-source-range", PassHostHeader: true, + Headers: &types.Headers{}, WhitelistSourceRange: []string{ "1.1.1.1/24", "1234:abcd::42/32", @@ -1540,6 +1557,7 @@ func TestIngressAnnotations(t *testing.T) { "rewrite/api": { Backend: "rewrite/api", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/api": { Rule: "PathPrefix:/api;ReplacePath:/", @@ -1641,6 +1659,7 @@ func TestPriorityHeaderValue(t *testing.T) { Backend: "foo/bar", PassHostHeader: true, Priority: 1337, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -1742,6 +1761,7 @@ func TestInvalidPassTLSCertValue(t *testing.T) { Backend: "foo/bar", PassTLSCert: false, PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -1842,6 +1862,7 @@ func TestInvalidPassHostHeaderValue(t *testing.T) { "foo/bar": { Backend: "foo/bar", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "/bar": { Rule: "PathPrefix:/bar", @@ -2143,6 +2164,7 @@ func TestMissingResources(t *testing.T) { "fully_working": { Backend: "fully_working", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "fully_working": { Rule: "Host:fully_working", @@ -2152,6 +2174,7 @@ func TestMissingResources(t *testing.T) { "missing_endpoints": { Backend: "missing_endpoints", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "missing_endpoints": { Rule: "Host:missing_endpoints", @@ -2161,6 +2184,7 @@ func TestMissingResources(t *testing.T) { "missing_endpoint_subsets": { Backend: "missing_endpoint_subsets", PassHostHeader: true, + Headers: &types.Headers{}, Routes: map[string]types.Route{ "missing_endpoint_subsets": { Rule: "Host:missing_endpoint_subsets", diff --git a/server/server.go b/server/server.go index be79edf96..4265305ee 100644 --- a/server/server.go +++ b/server/server.go @@ -980,10 +980,9 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura continue frontend } - var headerMiddleware *middlewares.HeaderStruct + headerMiddleware := middlewares.NewHeaderFromStruct(frontend.Headers) var responseModifier func(res *http.Response) error - if frontend.Headers.HasCustomHeadersDefined() { - headerMiddleware = middlewares.NewHeaderFromStruct(frontend.Headers) + if headerMiddleware != nil { responseModifier = headerMiddleware.ModifyResponseHeaders } @@ -1166,8 +1165,9 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura log.Debugf("Adding header middleware for frontend %s", frontendName) n.Use(headerMiddleware) } - if frontend.Headers.HasSecureHeadersDefined() { - secureMiddleware := middlewares.NewSecure(frontend.Headers) + + secureMiddleware := middlewares.NewSecure(frontend.Headers) + if secureMiddleware != nil { log.Debugf("Adding secure middleware for frontend %s", frontendName) n.UseFunc(secureMiddleware.HandlerFuncWithNext) } diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 6d1db7a02..e4c21bdc0 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -94,6 +94,7 @@ replacement = "{{getRedirectReplacement $container}}" {{end}} + {{ if hasHeaders $container}} [frontends."frontend-{{$frontend}}".headers] {{if hasSSLRedirectHeaders $container}} SSLRedirect = {{getSSLRedirectHeaders $container}} @@ -140,6 +141,16 @@ {{if hasIsDevelopmentHeaders $container}} IsDevelopment = {{getIsDevelopmentHeaders $container}} {{end}} + {{if hasAllowedHostsHeaders $container}} + AllowedHosts = [{{range getAllowedHostsHeaders $container}} + "{{.}}", + {{end}}] + {{end}} + {{if hasHostsProxyHeaders $container}} + HostsProxyHeaders = [{{range getHostsProxyHeaders $container}} + "{{.}}", + {{end}}] + {{end}} {{if hasRequestHeaders $container}} [frontends."frontend-{{$frontend}}".headers.customrequestheaders] {{range $k, $v := getRequestHeaders $container}} @@ -152,24 +163,14 @@ {{$k}} = "{{$v}}" {{end}} {{end}} - {{if hasAllowedHostsHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.AllowedHosts] - {{range getAllowedHostsHeaders $container}} - "{{.}}" - {{end}} - {{end}} - {{if hasHostsProxyHeaders $container}} - [frontends."frontend-{{$frontend}}".headers.HostsProxyHeaders] - {{range getHostsProxyHeaders $container}} - "{{.}}" - {{end}} - {{end}} {{if hasSSLProxyHeaders $container}} [frontends."frontend-{{$frontend}}".headers.SSLProxyHeaders] {{range $k, $v := getSSLProxyHeaders $container}} {{$k}} = "{{$v}}" {{end}} {{end}} + {{end}} + [frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"] rule = "{{getFrontendRule $container}}" {{end}} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 741088953..2509a8fbc 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -42,6 +42,7 @@ replacement = "{{$frontend.RedirectReplacement}}" {{end}} + {{ if $frontend.Headers }} [frontends."{{$frontendName}}".headers] SSLRedirect = {{$frontend.Headers.SSLRedirect}} SSLTemporaryRedirect = {{$frontend.Headers.SSLTemporaryRedirect}} @@ -58,38 +59,43 @@ PublicKey = "{{$frontend.Headers.PublicKey}}" ReferrerPolicy = "{{$frontend.Headers.ReferrerPolicy}}" IsDevelopment = {{$frontend.Headers.IsDevelopment}} -{{if $frontend.Headers.CustomRequestHeaders}} - [frontends."{{$frontendName}}".headers.customrequestheaders] + + {{if $frontend.Headers.AllowedHosts}} + AllowedHosts = [{{range $frontend.Headers.AllowedHosts}} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Headers.HostsProxyHeaders}} + HostsProxyHeaders = [{{range $frontend.Headers.HostsProxyHeaders}} + "{{.}}", + {{end}}] + {{end}} + + {{if $frontend.Headers.CustomRequestHeaders}} + [frontends."{{$frontendName}}".headers.customrequestheaders] {{range $k, $v := $frontend.Headers.CustomRequestHeaders}} {{$k}} = "{{$v}}" {{end}} -{{end}} -{{if $frontend.Headers.CustomResponseHeaders}} - [frontends."{{$frontendName}}".headers.customresponseheaders] + {{end}} + + {{if $frontend.Headers.CustomResponseHeaders}} + [frontends."{{$frontendName}}".headers.customresponseheaders] {{range $k, $v := $frontend.Headers.CustomResponseHeaders}} {{$k}} = "{{$v}}" {{end}} -{{end}} -{{if $frontend.Headers.AllowedHosts}} - [frontends."{{$frontendName}}".headers.AllowedHosts] - {{range $frontend.Headers.AllowedHosts}} - "{{.}}" - {{end}} -{{end}} -{{if $frontend.Headers.HostsProxyHeaders}} - [frontends."{{$frontendName}}".headers.HostsProxyHeaders] - {{range $frontend.Headers.HostsProxyHeaders}} - "{{.}}" - {{end}} -{{end}} -{{if $frontend.Headers.SSLProxyHeaders}} - [frontends."{{$frontendName}}".headers.SSLProxyHeaders] + {{end}} + + {{if $frontend.Headers.SSLProxyHeaders}} + [frontends."{{$frontendName}}".headers.SSLProxyHeaders] {{range $k, $v := $frontend.Headers.SSLProxyHeaders}} {{$k}} = "{{$v}}" {{end}} + {{end}} {{end}} - {{range $routeName, $route := $frontend.Routes}} - [frontends."{{$frontendName}}".routes."{{$routeName}}"] + + {{range $routeName, $route := $frontend.Routes}} + [frontends."{{$frontendName}}".routes."{{$routeName}}"] rule = "{{$route.Rule}}" - {{end}} + {{end}} {{end}} diff --git a/types/types.go b/types/types.go index 0625b2271..2f337e65d 100644 --- a/types/types.go +++ b/types/types.go @@ -113,14 +113,14 @@ type Headers struct { } // HasCustomHeadersDefined checks to see if any of the custom header elements have been set -func (h Headers) HasCustomHeadersDefined() bool { - return len(h.CustomResponseHeaders) != 0 || - len(h.CustomRequestHeaders) != 0 +func (h *Headers) HasCustomHeadersDefined() bool { + return h != nil && (len(h.CustomResponseHeaders) != 0 || + len(h.CustomRequestHeaders) != 0) } // HasSecureHeadersDefined checks to see if any of the secure header elements have been set -func (h Headers) HasSecureHeadersDefined() bool { - return len(h.AllowedHosts) != 0 || +func (h *Headers) HasSecureHeadersDefined() bool { + return h != nil && (len(h.AllowedHosts) != 0 || len(h.HostsProxyHeaders) != 0 || h.SSLRedirect || h.SSLTemporaryRedirect || @@ -137,7 +137,7 @@ func (h Headers) HasSecureHeadersDefined() bool { h.ContentSecurityPolicy != "" || h.PublicKey != "" || h.ReferrerPolicy != "" || - h.IsDevelopment + h.IsDevelopment) } // Frontend holds frontend configuration. @@ -150,7 +150,7 @@ type Frontend struct { Priority int `json:"priority"` BasicAuth []string `json:"basicAuth"` WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"` - Headers Headers `json:"headers,omitempty"` + Headers *Headers `json:"headers,omitempty"` Errors map[string]ErrorPage `json:"errors,omitempty"` RateLimit *RateLimit `json:"ratelimit,omitempty"` Redirect *Redirect `json:"redirect,omitempty"` From 718fc7a79d7d8f0cf21f4e2991105c7011fa8428 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 2 Jan 2018 10:14:03 +0100 Subject: [PATCH 06/12] Fix bug report command --- cmd/traefik/bug.go | 8 ++++---- cmd/traefik/bug_test.go | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/cmd/traefik/bug.go b/cmd/traefik/bug.go index 27d99d389..2c7551853 100644 --- a/cmd/traefik/bug.go +++ b/cmd/traefik/bug.go @@ -84,7 +84,7 @@ Add more configuration information here. ) // newBugCmd builds a new Bug command -func newBugCmd(traefikConfiguration interface{}, traefikPointersConfiguration interface{}) *flaeg.Command { +func newBugCmd(traefikConfiguration *TraefikConfiguration, traefikPointersConfiguration *TraefikConfiguration) *flaeg.Command { //version Command init return &flaeg.Command{ @@ -99,7 +99,7 @@ func newBugCmd(traefikConfiguration interface{}, traefikPointersConfiguration in } } -func runBugCmd(traefikConfiguration interface{}) func() error { +func runBugCmd(traefikConfiguration *TraefikConfiguration) func() error { return func() error { body, err := createBugReport(traefikConfiguration) @@ -113,7 +113,7 @@ func runBugCmd(traefikConfiguration interface{}) func() error { } } -func createBugReport(traefikConfiguration interface{}) (string, error) { +func createBugReport(traefikConfiguration *TraefikConfiguration) (string, error) { var version bytes.Buffer if err := getVersionPrint(&version); err != nil { return "", err @@ -124,7 +124,7 @@ func createBugReport(traefikConfiguration interface{}) (string, error) { return "", err } - config, err := anonymize.Do(&traefikConfiguration, true) + config, err := anonymize.Do(traefikConfiguration, true) if err != nil { return "", err } diff --git a/cmd/traefik/bug_test.go b/cmd/traefik/bug_test.go index efe191bcc..60c068283 100644 --- a/cmd/traefik/bug_test.go +++ b/cmd/traefik/bug_test.go @@ -6,16 +6,27 @@ import ( "github.com/containous/traefik/cmd/traefik/anonymize" "github.com/containous/traefik/configuration" "github.com/containous/traefik/provider/file" + "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" ) func Test_createBugReport(t *testing.T) { - traefikConfiguration := TraefikConfiguration{ + traefikConfiguration := &TraefikConfiguration{ ConfigFile: "FOO", GlobalConfiguration: configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ "goo": &configuration.EntryPoint{ Address: "hoo.bar", + Auth: &types.Auth{ + Basic: &types.Basic{ + UsersFile: "foo Basic UsersFile", + Users: types.Users{"foo Basic Users 1", "foo Basic Users 2", "foo Basic Users 3"}, + }, + Digest: &types.Digest{ + UsersFile: "foo Digest UsersFile", + Users: types.Users{"foo Digest Users 1", "foo Digest Users 2", "foo Digest Users 3"}, + }, + }, }, }, File: &file.Provider{ @@ -27,6 +38,11 @@ func Test_createBugReport(t *testing.T) { report, err := createBugReport(traefikConfiguration) assert.NoError(t, err, report) + + // exported anonymous configuration + assert.NotContains(t, "web Basic Users ", report) + assert.NotContains(t, "foo Digest Users ", report) + assert.NotContains(t, "hoo.bar", report) } func Test_anonymize_traefikConfiguration(t *testing.T) { From 7d23d3c0a46c94165b25ae1485621e37e7bb2b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20P=C4=99drys?= Date: Tue, 2 Jan 2018 10:38:03 +0100 Subject: [PATCH 07/12] Typo in docker.endpoint TCP port. --- docs/user-guide/swarm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user-guide/swarm.md b/docs/user-guide/swarm.md index d63cd7ae2..3d9c7df8c 100644 --- a/docs/user-guide/swarm.md +++ b/docs/user-guide/swarm.md @@ -86,7 +86,7 @@ docker $(docker-machine config mhs-demo0) run \ -c /dev/null \ --docker \ --docker.domain=traefik \ - --docker.endpoint=tcp://$(docker-machine ip mhs-demo0):3376 \ + --docker.endpoint=tcp://$(docker-machine ip mhs-demo0):2376 \ --docker.tls \ --docker.tls.ca=/ssl/ca.pem \ --docker.tls.cert=/ssl/server.pem \ @@ -105,7 +105,7 @@ Let's explain this command: | `-v /var/lib/boot2docker/:/ssl` | mount the ssl keys generated by docker-machine | | `-c /dev/null` | empty config file | | `--docker` | enable docker backend | -| `--docker.endpoint=tcp://172.18.0.1:3376` | connect to the swarm master using the docker_gwbridge network | +| `--docker.endpoint=tcp://172.18.0.1:2376` | connect to the swarm master using the docker_gwbridge network | | `--docker.tls` | enable TLS using the docker-machine keys | | `--web` | activate the webUI on port 8080 | From e8e8b41eed79d06e250a996ef69924c50e256a99 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Jan 2018 10:52:03 +0100 Subject: [PATCH 08/12] Normalize serviceName added to the service backend names --- provider/consul/consul_catalog_test.go | 16 ++++++++-------- provider/docker/docker.go | 4 ++-- provider/docker/service_test.go | 10 ++++++++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index 3e6899f80..06d3a8495 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -12,14 +12,6 @@ import ( ) func TestConsulCatalogGetFrontendRule(t *testing.T) { - provider := &CatalogProvider{ - Domain: "localhost", - Prefix: "traefik", - FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", - frontEndRuleTemplate: template.New("consul catalog frontend rule"), - } - provider.setupFrontEndTemplate() - testCases := []struct { desc string service serviceUpdate @@ -71,6 +63,14 @@ func TestConsulCatalogGetFrontendRule(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() + provider := &CatalogProvider{ + Domain: "localhost", + Prefix: "traefik", + FrontEndRule: "Host:{{.ServiceName}}.{{.Domain}}", + frontEndRuleTemplate: template.New("consul catalog frontend rule"), + } + provider.setupFrontEndTemplate() + actual := provider.getFrontendRule(test.service) assert.Equal(t, test.expected, actual) }) diff --git a/provider/docker/docker.go b/provider/docker/docker.go index 34a60a2f3..fb6a86aee 100644 --- a/provider/docker/docker.go +++ b/provider/docker/docker.go @@ -433,9 +433,9 @@ func (p *Provider) getServicePriority(container dockerData, serviceName string) // Extract backend from labels for a given service and a given docker container func (p *Provider) getServiceBackend(container dockerData, serviceName string) string { if value, ok := getContainerServiceLabel(container, serviceName, "frontend.backend"); ok { - return container.ServiceName + "-" + value + return provider.Normalize(container.ServiceName + "-" + value) } - return strings.TrimPrefix(container.ServiceName, "/") + "-" + p.getBackend(container) + "-" + provider.Normalize(serviceName) + return provider.Normalize(container.ServiceName + "-" + p.getBackend(container) + "-" + serviceName) } // Extract rule from labels for a given service and a given docker container diff --git a/provider/docker/service_test.go b/provider/docker/service_test.go index 08febf216..c4659b01b 100644 --- a/provider/docker/service_test.go +++ b/provider/docker/service_test.go @@ -173,12 +173,22 @@ func TestDockerGetServiceBackend(t *testing.T) { container: containerJSON(name("foo")), expected: "foo-foo-myservice", }, + { + container: containerJSON(name("foo.bar")), + expected: "foo-bar-foo-bar-myservice", + }, { container: containerJSON(labels(map[string]string{ types.LabelBackend: "another-backend", })), expected: "fake-another-backend-myservice", }, + { + container: containerJSON(labels(map[string]string{ + types.LabelBackend: "another.backend", + })), + expected: "fake-another-backend-myservice", + }, { container: containerJSON(labels(map[string]string{ "traefik.myservice.frontend.backend": "custom-backend", From 4819974a1c5dfa8e28156db38ce3fe9f0eb5cf33 Mon Sep 17 00:00:00 2001 From: Timo Reimann Date: Tue, 2 Jan 2018 11:08:02 +0100 Subject: [PATCH 09/12] Improve Marathon service label documentation. --- docs/configuration/backends/marathon.md | 13 ++++++++----- docs/user-guide/marathon.md | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/configuration/backends/marathon.md b/docs/configuration/backends/marathon.md index c4592e10c..13982dbc7 100644 --- a/docs/configuration/backends/marathon.md +++ b/docs/configuration/backends/marathon.md @@ -150,12 +150,15 @@ domain = "marathon.localhost" To enable constraints see [backend-specific constraints section](/configuration/commons/#backend-specific). - ## Labels: overriding default behaviour -### On Containers +Marathon labels may be used to dynamically change the routing and forwarding behaviour. -Labels can be used on containers to override default behaviour: +They may be specified on one of two levels: Application or service. + +### Application Level + +The following labels can be defined on Marathon applications. They adjust the behaviour for the entire application. | Label | Description | |-----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -180,9 +183,9 @@ Labels can be used on containers to override default behaviour: | `traefik.frontend.entryPoints=http,https` | assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | | `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash`. | -### On Services +### Service Level -If several ports need to be exposed from a container, the services labels can be used: +For applications that expose multiple ports, specific labels can be used to extract one frontend/backend configuration pair per port. Each such pair is called a _service_. The (freely choosable) name of the service is an integral part of the service label name. | Label | Description | |--------------------------------------------------------|------------------------------------------------------------------------------------------------------| diff --git a/docs/user-guide/marathon.md b/docs/user-guide/marathon.md index 8c13ebfab..5df0ee2da 100644 --- a/docs/user-guide/marathon.md +++ b/docs/user-guide/marathon.md @@ -28,6 +28,24 @@ Following is the order by which Traefik tries to identify the port (the first on 1. The port from the application's `portDefinitions` field (possibly indexed through the `traefik.portIndex` label, otherwise the first one). 1. The port from the application's `ipAddressPerTask` field (possibly indexed through the `traefik.portIndex` label, otherwise the first one). +## Applications with multiple ports + +Some Marathon applications may expose multiple ports. Traefik supports creating one so-called _service_ per port using [specific labels](/configuration/backends/marathon#service-level). + +For instance, assume that a Marathon application exposes a web API on port 80 and an admin interface on port 8080. It would then be possible to make each service available by specifying the following Marathon labels: + +``` +traefik.web.port=80 +``` + +``` +traefik.admin.port=8080 +``` + +(Note that the service names `web` and `admin` can be chosen arbitrarily.) + +Technically, Traefik will create one pair of frontend and backend configurations for each service. + ## Achieving high availability ### Scenarios From 0ee6973e2f5fe02e0238f81d6f52a671e730cb24 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 2 Jan 2018 11:28:02 +0100 Subject: [PATCH 10/12] Upgrade docs dependencies and adapt configuration --- LICENSE.md | 2 +- docs/theme/partials/footer.html | 4 ++-- mkdocs.yml | 37 +++++++++++++++------------------ requirements.txt | 4 ++-- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index a67e37b43..ed3503b6e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016-2017 Containous SAS +Copyright (c) 2016-2018 Containous SAS Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/theme/partials/footer.html b/docs/theme/partials/footer.html index 98cb1e652..e3b586dbe 100644 --- a/docs/theme/partials/footer.html +++ b/docs/theme/partials/footer.html @@ -20,7 +20,7 @@ IN THE SOFTWARE. --> -{% import "partials/language.html" as lang %} +{% import "partials/language.html" as lang with context %}