diff --git a/CHANGELOG.md b/CHANGELOG.md index c62480e59..0dcc6c8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Change Log +## [v1.7.0-rc3](https://github.com/containous/traefik/tree/v1.7.0-rc3) (2018-08-01) +[All Commits](https://github.com/containous/traefik/compare/v1.7.0-rc2...v1.7.0-rc3) + +**Enhancements:** +- **[consul,etcd,tls]** Improve TLS integration tests ([#3679](https://github.com/containous/traefik/pull/3679) by [mmatur](https://github.com/mmatur)) +- **[k8s]** Add possibility to set a protocol ([#3648](https://github.com/containous/traefik/pull/3648) by [SantoDE](https://github.com/SantoDE)) + +**Bug fixes:** +- **[acme]** Fix acme account deletion without provider change ([#3664](https://github.com/containous/traefik/pull/3664) by [zyclonite](https://github.com/zyclonite)) +- **[acme]** Update lego ([#3659](https://github.com/containous/traefik/pull/3659) by [mmatur](https://github.com/mmatur)) +- **[acme]** Fix ACME certificate for wildcard and root domains ([#3675](https://github.com/containous/traefik/pull/3675) by [nmengin](https://github.com/nmengin)) +- **[api]** Remove TLS in API ([#3665](https://github.com/containous/traefik/pull/3665) by [mmatur](https://github.com/mmatur)) +- **[docker]** Uses both binded HostIP and HostPort when useBindPortIP=true ([#3638](https://github.com/containous/traefik/pull/3638) by [geraldcroes](https://github.com/geraldcroes)) +- **[k8s]** Fix Rewrite-target regex ([#3699](https://github.com/containous/traefik/pull/3699) by [dtomcej](https://github.com/dtomcej)) +- **[middleware]** Correct Entrypoint Redirect with Stripped or Added Path ([#3631](https://github.com/containous/traefik/pull/3631) by [dtomcej](https://github.com/dtomcej)) +- **[tracing]** Added default configuration for DataDog APM Tracer ([#3655](https://github.com/containous/traefik/pull/3655) by [aantono](https://github.com/aantono)) +- **[tracing]** Added support for Trace name truncation for traces ([#3689](https://github.com/containous/traefik/pull/3689) by [aantono](https://github.com/aantono)) +- **[websocket]** Handle shutdown of Hijacked connections ([#3636](https://github.com/containous/traefik/pull/3636) by [Juliens](https://github.com/Juliens)) +- H2C: Remove buggy line in init to make verbose switch working ([#3701](https://github.com/containous/traefik/pull/3701) by [dduportal](https://github.com/dduportal)) +- Updating oxy dependency ([#3700](https://github.com/containous/traefik/pull/3700) by [crholm](https://github.com/crholm)) + +**Documentation:** +- **[acme]** Update Namecheap status ([#3604](https://github.com/containous/traefik/pull/3604) by [stoinov](https://github.com/stoinov)) +- **[acme]** Fix some DNS provider link ([#3639](https://github.com/containous/traefik/pull/3639) by [ldez](https://github.com/ldez)) +- **[docker]** Fix style in examples/quickstart ([#3705](https://github.com/containous/traefik/pull/3705) by [korigod](https://github.com/korigod)) +- **[k8s]** Add traefik prefix to k8s annotations ([#3682](https://github.com/containous/traefik/pull/3682) by [zifeo](https://github.com/zifeo)) +- **[middleware,tracing]** Fix missing tracing backend in documentation ([#3706](https://github.com/containous/traefik/pull/3706) by [mmatur](https://github.com/mmatur)) +- Replace unrendered emoji ([#3690](https://github.com/containous/traefik/pull/3690) by [korigod](https://github.com/korigod)) + ## [v1.7.0-rc2](https://github.com/containous/traefik/tree/v1.7.0-rc2) (2018-07-17) [All Commits](https://github.com/containous/traefik/compare/v1.7.0-rc1...v1.7.0-rc2) diff --git a/Gopkg.lock b/Gopkg.lock index 6ac7c2792..cf6d2a03c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1279,7 +1279,7 @@ "roundrobin", "utils" ] - revision = "a3ed5f65204f4ffccbb56d58cec466cdb7ab730b" + revision = "fb889e801a26e7e18ef36322ac72a07157f8cc1f" [[projects]] name = "github.com/vulcand/predicate" diff --git a/acme/acme.go b/acme/acme.go index 85599bc3e..492439d83 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -9,6 +9,7 @@ import ( fmtlog "log" "net" "net/http" + "net/url" "reflect" "strings" "time" @@ -183,7 +184,8 @@ func (a *ACME) leadershipListener(elected bool) error { account := object.(*Account) account.Init() // Reset Account values if caServer changed, thus registration URI can be updated - if account != nil && account.Registration != nil && !strings.HasPrefix(account.Registration.URI, a.CAServer) { + if account != nil && account.Registration != nil && !isAccountMatchingCaServer(account.Registration.URI, a.CAServer) { + log.Info("Account URI does not match the current CAServer. The account will be reset") account.reset() } @@ -230,6 +232,20 @@ func (a *ACME) leadershipListener(elected bool) error { return nil } +func isAccountMatchingCaServer(accountURI string, serverURI string) bool { + aru, err := url.Parse(accountURI) + if err != nil { + log.Infof("Unable to parse account.Registration URL : %v", err) + return false + } + cau, err := url.Parse(serverURI) + if err != nil { + log.Infof("Unable to parse CAServer URL : %v", err) + return false + } + return cau.Hostname() == aru.Hostname() +} + func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domain := types.CanonicalDomain(clientHello.ServerName) account := a.store.Get().(*Account) diff --git a/cmd/configuration.go b/cmd/configuration.go index 1e6e9d64f..3e9d4aeea 100644 --- a/cmd/configuration.go +++ b/cmd/configuration.go @@ -9,6 +9,7 @@ import ( "github.com/containous/traefik/configuration" "github.com/containous/traefik/middlewares/accesslog" "github.com/containous/traefik/middlewares/tracing" + "github.com/containous/traefik/middlewares/tracing/datadog" "github.com/containous/traefik/middlewares/tracing/jaeger" "github.com/containous/traefik/middlewares/tracing/zipkin" "github.com/containous/traefik/ping" @@ -190,8 +191,9 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { // default Tracing defaultTracing := tracing.Tracing{ - Backend: "jaeger", - ServiceName: "traefik", + Backend: "jaeger", + ServiceName: "traefik", + SpanNameLimit: 0, Jaeger: &jaeger.Config{ SamplingServerURL: "http://localhost:5778/sampling", SamplingType: "const", @@ -206,6 +208,11 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { ID128Bit: true, Debug: false, }, + DataDog: &datadog.Config{ + LocalAgentHostPort: "localhost:8126", + GlobalTag: "", + Debug: false, + }, } // default LifeCycle diff --git a/configuration/configuration.go b/configuration/configuration.go index f6b29d3ae..48aaecab6 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -11,6 +11,7 @@ import ( "github.com/containous/traefik/api" "github.com/containous/traefik/log" "github.com/containous/traefik/middlewares/tracing" + "github.com/containous/traefik/middlewares/tracing/datadog" "github.com/containous/traefik/middlewares/tracing/jaeger" "github.com/containous/traefik/middlewares/tracing/zipkin" "github.com/containous/traefik/ping" @@ -197,6 +198,10 @@ func (gc *GlobalConfiguration) initTracing() { log.Warn("Zipkin configuration will be ignored") gc.Tracing.Zipkin = nil } + if gc.Tracing.DataDog != nil { + log.Warn("DataDog configuration will be ignored") + gc.Tracing.DataDog = nil + } case zipkin.Name: if gc.Tracing.Zipkin == nil { gc.Tracing.Zipkin = &zipkin.Config{ @@ -210,6 +215,26 @@ func (gc *GlobalConfiguration) initTracing() { log.Warn("Jaeger configuration will be ignored") gc.Tracing.Jaeger = nil } + if gc.Tracing.DataDog != nil { + log.Warn("DataDog configuration will be ignored") + gc.Tracing.DataDog = nil + } + case datadog.Name: + if gc.Tracing.DataDog == nil { + gc.Tracing.DataDog = &datadog.Config{ + LocalAgentHostPort: "localhost:8126", + GlobalTag: "", + Debug: false, + } + } + if gc.Tracing.Zipkin != nil { + log.Warn("Zipkin configuration will be ignored") + gc.Tracing.Zipkin = nil + } + if gc.Tracing.Jaeger != nil { + log.Warn("Jaeger configuration will be ignored") + gc.Tracing.Jaeger = nil + } default: log.Warnf("Unknown tracer %q", gc.Tracing.Backend) return diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 4518f190d..789d8a88b 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -159,7 +159,8 @@ The following general annotations are applicable on the Ingress object: | `traefik.ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"` | A comma-separated list of IP ranges permitted for access (6). | | `ingress.kubernetes.io/whitelist-x-forwarded-for: "true"` | Use `X-Forwarded-For` header as valid source of IP for the white list. | | `traefik.ingress.kubernetes.io/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) | -| `traefik.ingress.kubernetes.io/service-weights: ` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) | +| `traefik.ingress.kubernetes.io/service-weights: ` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) +| `ingress.kubernetes.io/protocol: ` | Set the protocol Traefik will use to communicate with pods. <1> `traefik.ingress.kubernetes.io/error-pages` example: @@ -253,7 +254,7 @@ The following annotations are applicable on the Service object associated with a | `traefik.ingress.kubernetes.io/affinity: "true"` | Enable backend sticky sessions. | | `traefik.ingress.kubernetes.io/circuit-breaker-expression: ` | Set the circuit breaker expression for the backend. | | `traefik.ingress.kubernetes.io/load-balancer-method: drr` | Override the default `wrr` load balancer algorithm. | -| `traefik.ingress.kubernetes.io/max-conn-amount: 10` | Set a maximum number of connections to the backend.
Must be used in conjunction with the below label to take effect. | +| `traefik.ingress.kubernetes.io/max-conn-amount: "10"` | Set a maximum number of connections to the backend.
Must be used in conjunction with the below label to take effect. | | `traefik.ingress.kubernetes.io/max-conn-extractor-func: client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by.
Must be used in conjunction with the above label to take effect. | | `traefik.ingress.kubernetes.io/session-cookie-name: ` | Manually set the cookie name for sticky sessions. | diff --git a/docs/configuration/tracing.md b/docs/configuration/tracing.md index 030c61c0f..db523f072 100644 --- a/docs/configuration/tracing.md +++ b/docs/configuration/tracing.md @@ -4,7 +4,7 @@ Tracing system allows developers to visualize call flows in there infrastructure We use [OpenTracing](http://opentracing.io). It is an open standard designed for distributed tracing. -Træfik supports two backends: Jaeger and Zipkin. +Træfik supports three tracing backends: Jaeger, Zipkin and DataDog. ## Jaeger @@ -22,6 +22,13 @@ Træfik supports two backends: Jaeger and Zipkin. # Default: "traefik" # serviceName = "traefik" + + # Span name limit allows for name truncation in case of very long Frontend/Backend names + # This can prevent certain tracing providers to drop traces that exceed their length limits + # + # Default: 0 - no truncation will occur + # + spanNameLimit = 0 [tracing.jaeger] # Sampling Server URL is the address of jaeger-agent's HTTP sampling server @@ -84,6 +91,13 @@ Træfik supports two backends: Jaeger and Zipkin. # Default: "traefik" # serviceName = "traefik" + + # Span name limit allows for name truncation in case of very long Frontend/Backend names + # This can prevent certain tracing providers to drop traces that exceed their length limits + # + # Default: 0 - no truncation will occur + # + spanNameLimit = 150 [tracing.zipkin] # Zipking HTTP endpoint used to send data @@ -127,6 +141,13 @@ Træfik supports two backends: Jaeger and Zipkin. # Default: "traefik" # serviceName = "traefik" + + # Span name limit allows for name truncation in case of very long Frontend/Backend names + # This can prevent certain tracing providers to drop traces that exceed their length limits + # + # Default: 0 - no truncation will occur + # + spanNameLimit = 100 [tracing.datadog] # Local Agent Host Port instructs reporter to send spans to datadog-tracing-agent at this address diff --git a/docs/index.md b/docs/index.md index e466c3f6d..833c85f33 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,7 +44,7 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t - Keeps access logs (JSON, CLF) - Fast - Exposes a Rest API -- Packaged as a single binary file (made with :heart: with go) and available as a [tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image +- Packaged as a single binary file (made with ❤️ with go) and available as a [tiny](https://microbadger.com/images/traefik) [official](https://hub.docker.com/r/_/traefik/) docker image ## Supported Providers diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index d6f0eb42f..c194af91e 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -453,8 +453,8 @@ kubectl create secret generic mysecret --from-file auth --namespace=monitoring C. Attach the following annotations to the Ingress object: -- `ingress.kubernetes.io/auth-type: "basic"` -- `ingress.kubernetes.io/auth-secret: "mysecret"` +- `traefik.ingress.kubernetes.io/auth-type: "basic"` +- `traefik.ingress.kubernetes.io/auth-secret: "mysecret"` They specify basic authentication and reference the Secret `mysecret` containing the credentials. @@ -468,8 +468,8 @@ metadata: namespace: monitoring annotations: kubernetes.io/ingress.class: traefik - ingress.kubernetes.io/auth-type: "basic" - ingress.kubernetes.io/auth-secret: "mysecret" + traefik.ingress.kubernetes.io/auth-type: "basic" + traefik.ingress.kubernetes.io/auth-secret: "mysecret" spec: rules: - host: dashboard.prometheus.example.com diff --git a/examples/quickstart/docker-compose.yml b/examples/quickstart/docker-compose.yml index f31f5d408..bd1c8a202 100644 --- a/examples/quickstart/docker-compose.yml +++ b/examples/quickstart/docker-compose.yml @@ -1,18 +1,18 @@ version: '3' services: - #The reverse proxy service (Træfik) + # The reverse proxy service (Træfik) reverse-proxy: - image: traefik #The official Traefik docker image - command: --api --docker #Enables the web UI and tells Træfik to listen to docker + image: traefik # The official Traefik docker image + command: --api --docker # Enables the web UI and tells Træfik to listen to docker ports: - - "80:80" #The HTTP port - - "8080:8080" #The Web UI (enabled by --api) + - "80:80" # The HTTP port + - "8080:8080" # The Web UI (enabled by --api) volumes: - - /var/run/docker.sock:/var/run/docker.sock #So that Traefik can listen to the Docker events + - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events - #A container that exposes a simple API + # A container that exposes a simple API whoami: - image: emilevauge/whoami #A container that exposes an API to show it's IP address + image: emilevauge/whoami # A container that exposes an API to show it's IP address labels: - - "traefik.frontend.rule=Host:whoami.docker.localhost" \ No newline at end of file + - "traefik.frontend.rule=Host:whoami.docker.localhost" diff --git a/h2c/h2c.go b/h2c/h2c.go index 70e144d2d..c5961620a 100644 --- a/h2c/h2c.go +++ b/h2c/h2c.go @@ -38,7 +38,6 @@ func init() { if strings.Contains(e, "http2debug=1") || strings.Contains(e, "http2debug=2") { http2VerboseLogs = true } - http2VerboseLogs = true } // Server implements net.Handler and enables h2c. Users who want h2c just need diff --git a/integration/acme_test.go b/integration/acme_test.go index 6f10f81e8..906f48cf2 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -79,7 +79,7 @@ func setupPebbleRootCA() (*http.Transport, error) { } func (s *AcmeSuite) SetUpSuite(c *check.C) { - s.createComposeProject(c, "peddle") + s.createComposeProject(c, "pebble") s.composeProject.Start(c) s.fakeDNSServer = startFakeDNSServer() @@ -91,7 +91,7 @@ func (s *AcmeSuite) SetUpSuite(c *check.C) { c.Fatal(err) } - // wait for peddle + // wait for pebble req := testhelpers.MustNewRequest(http.MethodGet, s.getAcmeURL(), nil) client := &http.Client{ diff --git a/integration/consul_test.go b/integration/consul_test.go index a6c1c192f..39a321db6 100644 --- a/integration/consul_test.go +++ b/integration/consul_test.go @@ -585,21 +585,14 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // wait for traefik - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, try.BodyContains("MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7hG")) - 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) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName)) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we configure the second keypair in consul and the request for host "snitest.org" will use the second keypair for key, value := range tlsconfigure2 { @@ -614,18 +607,12 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // waiting for traefik to pull configuration - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) - 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: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName)) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.org") } diff --git a/integration/etcd3_test.go b/integration/etcd3_test.go index 00dc755ed..ad877469b 100644 --- a/integration/etcd3_test.go +++ b/integration/etcd3_test.go @@ -532,21 +532,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - // wait for Træfik - err = try.GetRequest("http://127.0.0.1:8081/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) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName)) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair @@ -562,20 +555,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // waiting for Træfik to pull configuration - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) - 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: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName)) 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) { @@ -646,21 +633,14 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) { 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) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName)) 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 { @@ -668,18 +648,12 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) { 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) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn("TRAEFIK DEFAULT CERT")) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "TRAEFIK DEFAULT CERT") } diff --git a/integration/etcd_test.go b/integration/etcd_test.go index 6e6133f86..80dcf0b40 100644 --- a/integration/etcd_test.go +++ b/integration/etcd_test.go @@ -548,21 +548,14 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) { c.Assert(err, checker.IsNil) defer cmd.Process.Kill() - // wait for Træfik - err = try.GetRequest("http://127.0.0.1:8081/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) + + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName)) c.Assert(err, checker.IsNil) - cn := resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.com") // now we configure the second keypair in etcd and the request for host "snitest.org" will use the second keypair @@ -578,18 +571,12 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) { }) c.Assert(err, checker.IsNil) - // waiting for Træfik to pull configuration - err = try.GetRequest("http://127.0.0.1:8081/api/providers", 30*time.Second, try.BodyContains("MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r")) - 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: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName)) c.Assert(err, checker.IsNil) - cn = resp.TLS.PeerCertificates[0].Subject.CommonName - c.Assert(cn, checker.Equals, "snitest.org") } diff --git a/integration/fixtures/file/dir/simple2.toml b/integration/fixtures/file/dir/simple2.toml index e02f63550..dcbcffc57 100644 --- a/integration/fixtures/file/dir/simple2.toml +++ b/integration/fixtures/file/dir/simple2.toml @@ -2,7 +2,7 @@ [backends] [backends.backend2] [backends.backend2.servers.server1] - url = "http://172.17.0.2:80" + url = "http://172.17.0.123:80" weight = 1 [frontends] diff --git a/integration/fixtures/https/https_redirect.toml b/integration/fixtures/https/https_redirect.toml new file mode 100644 index 000000000..498d14f89 --- /dev/null +++ b/integration/fixtures/https/https_redirect.toml @@ -0,0 +1,36 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":8888" + [entryPoints.http.redirect] + entryPoint = "https" + [entryPoints.https] + address = ":8443" + [entryPoints.https.tls] + +[api] + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + url = "http://127.0.0.1:80" + weight = 1 + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Host: example.com; PathPrefixStrip: /api" + [frontends.frontend2] + backend = "backend1" + [frontends.frontend2.routes.test_1] + rule = "Host: test.com; AddPrefix: /foo" + [frontends.frontend3] + backend = "backend1" + [frontends.frontend3.routes.test_1] + rule = "Host: foo.com; PathPrefixStripRegex: /{id:[a-z]+}" diff --git a/integration/https_test.go b/integration/https_test.go index 8729549af..5db8a54ab 100644 --- a/integration/https_test.go +++ b/integration/https_test.go @@ -3,7 +3,6 @@ package integration import ( "bytes" "crypto/tls" - "fmt" "net" "net/http" "net/http/httptest" @@ -66,7 +65,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { defer cmd.Process.Kill() // wait for Traefik - err = try.GetRequest("http://127.0.0.1:8080/api/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.org")) + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1*time.Second, try.BodyContains("Host:snitest.org")) c.Assert(err, checker.IsNil) backend1 := startTestServer("9010", http.StatusNoContent) @@ -92,27 +91,23 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) { }, } - client := &http.Client{Transport: tr1} req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - req.Host = "snitest.com" - req.Header.Set("Host", "snitest.com") + 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) - // Expected a 204 (from backend1) - c.Assert(resp.StatusCode, checker.Equals, http.StatusNoContent) - client = &http.Client{Transport: tr2} + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNoContent)) + c.Assert(err, checker.IsNil) + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - req.Host = "snitest.org" - req.Header.Set("Host", "snitest.org") + req.Host = tr2.TLSClientConfig.ServerName + req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - resp, err = client.Do(req) + + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusResetContent)) c.Assert(err, checker.IsNil) - // Expected a 205 (from backend2) - c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) } // TestWithSNIStrictNotMatchedRequest involves a client sending a SNI hostname of @@ -561,28 +556,25 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) { err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent)) c.Assert(err, checker.IsNil) - client := &http.Client{Transport: tr1} req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) 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) - // snitest.org certificate must be used yet - c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, check.Equals, tr1.TLSClientConfig.ServerName) - // Expected a 204 (from backend1) - c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) - client = &http.Client{Transport: tr2} + // snitest.org certificate must be used yet && Expected a 204 (from backend1) + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusResetContent)) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) + c.Assert(err, checker.IsNil) req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) - resp, err = client.Do(req) + req.Header.Set("Accept", "*/*") + + // snitest.com certificate does not exist, default certificate has to be used && Expected a 205 (from backend2) + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("TRAEFIK DEFAULT CERT"), try.StatusCodeIs(http.StatusNoContent)) c.Assert(err, checker.IsNil) - // snitest.com certificate does not exist, default certificate has to be used - c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Not(check.Equals), tr2.TLSClientConfig.ServerName) - // Expected a 205 (from backend2) - c.Assert(resp.StatusCode, checker.Equals, http.StatusNoContent) } // TestWithSNIDynamicConfigRouteWithChange involves a client sending HTTPS requests with @@ -633,57 +625,26 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) { err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent)) c.Assert(err, checker.IsNil) + // Change certificates configuration file content + modifyCertificateConfFileContent(c, tr1.TLSClientConfig.ServerName, dynamicConfFileName, "https") + req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) - client := &http.Client{Transport: tr1} + c.Assert(err, checker.IsNil) req.Host = tr1.TLSClientConfig.ServerName req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Accept", "*/*") - // Change certificates configuration file content - modifyCertificateConfFileContent(c, tr1.TLSClientConfig.ServerName, dynamicConfFileName, "https") - 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 != tr1.TLSClientConfig.ServerName { - return fmt.Errorf("domain %s found in place of %s", cn, tr1.TLSClientConfig.ServerName) - } - - return nil - }) + err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNotFound)) + c.Assert(err, checker.IsNil) + + req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) - client = &http.Client{Transport: tr2} req.Host = tr2.TLSClientConfig.ServerName req.Header.Set("Host", tr2.TLSClientConfig.ServerName) + req.Header.Set("Accept", "*/*") - 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 - }) + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("TRAEFIK DEFAULT CERT"), try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) } // TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion involves a client sending HTTPS requests with @@ -725,53 +686,19 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c c.Assert(err, checker.IsNil) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) - client := &http.Client{Transport: tr2} + c.Assert(err, checker.IsNil) 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 - }) + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusResetContent)) c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent) + // Change certificates configuration file content modifyCertificateConfFileContent(c, "", 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 instead of the default one", tr2.TLSClientConfig.ServerName) - } - - return nil - }) + err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("TRAEFIK DEFAULT CERT"), try.StatusCodeIs(http.StatusNotFound)) c.Assert(err, checker.IsNil) - c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound) } // modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. @@ -804,3 +731,134 @@ func modifyCertificateConfFileContent(c *check.C, certFileName, confFileName, en c.Assert(err, checker.IsNil) } } + +func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C) { + cmd, display := s.traefikCmd(withConfigFile("fixtures/https/https_redirect.toml")) + defer display(c) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + // wait for Traefik + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 500*time.Millisecond, try.BodyContains("Host: example.com")) + c.Assert(err, checker.IsNil) + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + testCases := []struct { + desc string + host string + sourceURL string + expectedURL string + }{ + { + desc: "Stripped URL redirect", + host: "example.com", + sourceURL: "http://127.0.0.1:8888/api", + expectedURL: "https://example.com:8443/api", + }, + { + desc: "Stripped URL with trailing slash redirect", + host: "example.com", + sourceURL: "http://127.0.0.1:8888/api/", + expectedURL: "https://example.com:8443/api/", + }, + { + desc: "Stripped URL with double trailing slash redirect", + host: "example.com", + sourceURL: "http://127.0.0.1:8888/api//", + expectedURL: "https://example.com:8443/api//", + }, + { + desc: "Stripped URL with path redirect", + host: "example.com", + sourceURL: "http://127.0.0.1:8888/api/bacon", + expectedURL: "https://example.com:8443/api/bacon", + }, + { + desc: "Stripped URL with path and trailing slash redirect", + host: "example.com", + sourceURL: "http://127.0.0.1:8888/api/bacon/", + expectedURL: "https://example.com:8443/api/bacon/", + }, + { + desc: "Stripped URL with path and double trailing slash redirect", + host: "example.com", + sourceURL: "http://127.0.0.1:8888/api/bacon//", + expectedURL: "https://example.com:8443/api/bacon//", + }, + { + desc: "Root Path with redirect", + host: "test.com", + sourceURL: "http://127.0.0.1:8888/", + expectedURL: "https://test.com:8443/", + }, + { + desc: "Root Path with double trailing slash redirect", + host: "test.com", + sourceURL: "http://127.0.0.1:8888//", + expectedURL: "https://test.com:8443//", + }, + { + desc: "AddPrefix with redirect", + host: "test.com", + sourceURL: "http://127.0.0.1:8888/wtf", + expectedURL: "https://test.com:8443/wtf", + }, + { + desc: "AddPrefix with trailing slash redirect", + host: "test.com", + sourceURL: "http://127.0.0.1:8888/wtf/", + expectedURL: "https://test.com:8443/wtf/", + }, + { + desc: "AddPrefix with matching path segment redirect", + host: "test.com", + sourceURL: "http://127.0.0.1:8888/wtf/foo", + expectedURL: "https://test.com:8443/wtf/foo", + }, + { + desc: "Stripped URL Regex redirect", + host: "foo.com", + sourceURL: "http://127.0.0.1:8888/api", + expectedURL: "https://foo.com:8443/api", + }, + { + desc: "Stripped URL Regex with trailing slash redirect", + host: "foo.com", + sourceURL: "http://127.0.0.1:8888/api/", + expectedURL: "https://foo.com:8443/api/", + }, + { + desc: "Stripped URL Regex with path redirect", + host: "foo.com", + sourceURL: "http://127.0.0.1:8888/api/bacon", + expectedURL: "https://foo.com:8443/api/bacon", + }, + { + desc: "Stripped URL Regex with path and trailing slash redirect", + host: "foo.com", + sourceURL: "http://127.0.0.1:8888/api/bacon/", + expectedURL: "https://foo.com:8443/api/bacon/", + }, + } + + for _, test := range testCases { + test := test + + req, err := http.NewRequest("GET", test.sourceURL, nil) + c.Assert(err, checker.IsNil) + req.Host = test.host + + resp, err := client.Do(req) + c.Assert(err, checker.IsNil) + defer resp.Body.Close() + + location := resp.Header.Get("Location") + c.Assert(location, checker.Equals, test.expectedURL) + } +} diff --git a/integration/resources/compose/peddle.yml b/integration/resources/compose/pebble.yml similarity index 73% rename from integration/resources/compose/peddle.yml rename to integration/resources/compose/pebble.yml index f1b053f34..2828c66ef 100644 --- a/integration/resources/compose/peddle.yml +++ b/integration/resources/compose/pebble.yml @@ -1,6 +1,6 @@ pebble: - image: ldez/pebble - command: --dnsserver ${DOCKER_HOST_IP}:5053 + image: letsencrypt/pebble:2018-07-27 + command: pebble --dnsserver ${DOCKER_HOST_IP}:5053 ports: - 14000:14000 environment: diff --git a/integration/try/condition.go b/integration/try/condition.go index 999b6256e..537859d53 100644 --- a/integration/try/condition.go +++ b/integration/try/condition.go @@ -88,6 +88,31 @@ func HasBody() ResponseCondition { } } +// HasCn returns a retry condition function. +// The condition returns an error if the cn is not correct. +func HasCn(cn string) ResponseCondition { + return func(res *http.Response) error { + if res.TLS == nil { + return errors.New("response doesn't have TLS") + } + + if len(res.TLS.PeerCertificates) == 0 { + return errors.New("response TLS doesn't have peer certificates") + } + + if res.TLS.PeerCertificates[0] == nil { + return errors.New("first peer certificate is nil") + } + + commonName := res.TLS.PeerCertificates[0].Subject.CommonName + if cn != commonName { + return fmt.Errorf("common name don't match: %s != %s", cn, commonName) + } + + return nil + } +} + // StatusCodeIs returns a retry condition function. // The condition returns an error if the given response's status code is not the // given HTTP status code. diff --git a/integration/try/try.go b/integration/try/try.go index f201cd0d8..0f75cbdaf 100644 --- a/integration/try/try.go +++ b/integration/try/try.go @@ -31,7 +31,7 @@ func Sleep(d time.Duration) { // response body needs to be closed or not. Callers are expected to close on // their own if the function returns a nil error. func Response(req *http.Request, timeout time.Duration) (*http.Response, error) { - return doTryRequest(req, timeout) + return doTryRequest(req, timeout, nil) } // ResponseUntilStatusCode is like Request, but returns the response for further @@ -40,7 +40,7 @@ func Response(req *http.Request, timeout time.Duration) (*http.Response, error) // response body needs to be closed or not. Callers are expected to close on // their own if the function returns a nil error. func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCode int) (*http.Response, error) { - return doTryRequest(req, timeout, StatusCodeIs(statusCode)) + return doTryRequest(req, timeout, nil, StatusCodeIs(statusCode)) } // GetRequest is like Do, but runs a request against the given URL and applies @@ -48,7 +48,7 @@ func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCod // ResponseCondition may be nil, in which case only the request against the URL must // succeed. func GetRequest(url string, timeout time.Duration, conditions ...ResponseCondition) error { - resp, err := doTryGet(url, timeout, conditions...) + resp, err := doTryGet(url, timeout, nil, conditions...) if resp != nil && resp.Body != nil { defer resp.Body.Close() @@ -62,7 +62,21 @@ func GetRequest(url string, timeout time.Duration, conditions ...ResponseConditi // ResponseCondition may be nil, in which case only the request against the URL must // succeed. func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCondition) error { - resp, err := doTryRequest(req, timeout, conditions...) + resp, err := doTryRequest(req, timeout, nil, conditions...) + + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + + return err +} + +// RequestWithTransport is like Do, but runs a request against the given URL and applies +// the condition on the response. +// ResponseCondition may be nil, in which case only the request against the URL must +// succeed. +func RequestWithTransport(req *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) error { + resp, err := doTryRequest(req, timeout, transport, conditions...) if resp != nil && resp.Body != nil { defer resp.Body.Close() @@ -112,24 +126,27 @@ func Do(timeout time.Duration, operation DoCondition) error { } } -func doTryGet(url string, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) { +func doTryGet(url string, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } - return doTryRequest(req, timeout, conditions...) + return doTryRequest(req, timeout, transport, conditions...) } -func doTryRequest(request *http.Request, timeout time.Duration, conditions ...ResponseCondition) (*http.Response, error) { - return doRequest(Do, timeout, request, conditions...) +func doTryRequest(request *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) { + return doRequest(Do, timeout, request, transport, conditions...) } -func doRequest(action timedAction, timeout time.Duration, request *http.Request, conditions ...ResponseCondition) (*http.Response, error) { +func doRequest(action timedAction, timeout time.Duration, request *http.Request, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) { var resp *http.Response return resp, action(timeout, func() error { var err error client := http.DefaultClient + if transport != nil { + client.Transport = transport + } resp, err = client.Do(request) if err != nil { diff --git a/middlewares/addPrefix.go b/middlewares/addPrefix.go index 306903ae2..19f142fe3 100644 --- a/middlewares/addPrefix.go +++ b/middlewares/addPrefix.go @@ -1,6 +1,7 @@ package middlewares import ( + "context" "net/http" ) @@ -10,12 +11,21 @@ type AddPrefix struct { Prefix string } +type key string + +const ( + // AddPrefixKey is the key within the request context used to + // store the added prefix + AddPrefixKey key = "AddPrefix" +) + func (s *AddPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.URL.Path = s.Prefix + r.URL.Path if r.URL.RawPath != "" { r.URL.RawPath = s.Prefix + r.URL.RawPath } r.RequestURI = r.URL.RequestURI() + r = r.WithContext(context.WithValue(r.Context(), AddPrefixKey, s.Prefix)) s.Handler.ServeHTTP(w, r) } diff --git a/middlewares/redirect/redirect.go b/middlewares/redirect/redirect.go index c1ec263b3..1b1dfe7dd 100644 --- a/middlewares/redirect/redirect.go +++ b/middlewares/redirect/redirect.go @@ -11,6 +11,7 @@ import ( "text/template" "github.com/containous/traefik/configuration" + "github.com/containous/traefik/middlewares" "github.com/urfave/negroni" "github.com/vulcand/oxy/utils" ) @@ -85,6 +86,30 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http return } + if stripPrefix, stripPrefixOk := req.Context().Value(middlewares.StripPrefixKey).(string); stripPrefixOk { + if len(stripPrefix) > 0 { + tempPath := parsedURL.Path + parsedURL.Path = stripPrefix + if len(tempPath) > 0 && tempPath != "/" { + parsedURL.Path = stripPrefix + tempPath + } + + if trailingSlash, trailingSlashOk := req.Context().Value(middlewares.StripPrefixSlashKey).(bool); trailingSlashOk { + if trailingSlash { + if !strings.HasSuffix(parsedURL.Path, "/") { + parsedURL.Path = fmt.Sprintf("%s/", parsedURL.Path) + } + } + } + } + } + + if addPrefix, addPrefixOk := req.Context().Value(middlewares.AddPrefixKey).(string); addPrefixOk { + if len(addPrefix) > 0 { + parsedURL.Path = strings.Replace(parsedURL.Path, addPrefix, "", 1) + } + } + if newURL != oldURL { handler := &moveHandler{location: parsedURL, permanent: h.permanent} handler.ServeHTTP(rw, req) diff --git a/middlewares/stripPrefix.go b/middlewares/stripPrefix.go index e156950a1..f5295d94c 100644 --- a/middlewares/stripPrefix.go +++ b/middlewares/stripPrefix.go @@ -1,12 +1,21 @@ package middlewares import ( + "context" "net/http" "strings" ) -// ForwardedPrefixHeader is the default header to set prefix -const ForwardedPrefixHeader = "X-Forwarded-Prefix" +const ( + // StripPrefixKey is the key within the request context used to + // store the stripped prefix + StripPrefixKey key = "StripPrefix" + // StripPrefixSlashKey is the key within the request context used to + // store the stripped slash + StripPrefixSlashKey key = "StripPrefixSlash" + // ForwardedPrefixHeader is the default header to set prefix + ForwardedPrefixHeader = "X-Forwarded-Prefix" +) // StripPrefix is a middleware used to strip prefix from an URL request type StripPrefix struct { @@ -17,18 +26,21 @@ type StripPrefix struct { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { for _, prefix := range s.Prefixes { if strings.HasPrefix(r.URL.Path, prefix) { + trailingSlash := r.URL.Path == prefix+"/" r.URL.Path = stripPrefix(r.URL.Path, prefix) if r.URL.RawPath != "" { r.URL.RawPath = stripPrefix(r.URL.RawPath, prefix) } - s.serveRequest(w, r, strings.TrimSpace(prefix)) + s.serveRequest(w, r, strings.TrimSpace(prefix), trailingSlash) return } } http.NotFound(w, r) } -func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string) { +func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, trailingSlash bool) { + r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) + r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, prefix)) r.Header.Add(ForwardedPrefixHeader, prefix) r.RequestURI = r.URL.RequestURI() s.Handler.ServeHTTP(w, r) diff --git a/middlewares/stripPrefixRegex.go b/middlewares/stripPrefixRegex.go index bcd66d912..d86733dd6 100644 --- a/middlewares/stripPrefixRegex.go +++ b/middlewares/stripPrefixRegex.go @@ -1,6 +1,7 @@ package middlewares import ( + "context" "net/http" "github.com/containous/mux" @@ -39,10 +40,13 @@ func (s *StripPrefixRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + trailingSlash := r.URL.Path == prefix.Path+"/" r.URL.Path = r.URL.Path[len(prefix.Path):] if r.URL.RawPath != "" { r.URL.RawPath = r.URL.RawPath[len(prefix.Path):] } + r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) + r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, prefix.Path)) r.Header.Add(ForwardedPrefixHeader, prefix.Path) r.RequestURI = r.URL.RequestURI() s.Handler.ServeHTTP(w, r) diff --git a/middlewares/tracing/entrypoint.go b/middlewares/tracing/entrypoint.go index eb2f6aca6..a35bcea92 100644 --- a/middlewares/tracing/entrypoint.go +++ b/middlewares/tracing/entrypoint.go @@ -22,12 +22,10 @@ func (t *Tracing) NewEntryPoint(name string) negroni.Handler { } func (e *entryPointMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - opNameFunc := func(r *http.Request) string { - return fmt.Sprintf("Entrypoint %s %s", e.entryPoint, r.Host) - } + opNameFunc := generateEntryPointSpanName ctx, _ := e.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) - span := e.StartSpan(opNameFunc(r), ext.RPCServerOption(ctx)) + span := e.StartSpan(opNameFunc(r, e.entryPoint, e.SpanNameLimit), ext.RPCServerOption(ctx)) ext.Component.Set(span, e.ServiceName) LogRequest(span, r) ext.SpanKindRPCServer.Set(span) @@ -40,3 +38,20 @@ func (e *entryPointMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, LogResponseCode(span, recorder.Status()) span.Finish() } + +// generateEntryPointSpanName will return a Span name of an appropriate lenth based on the 'spanLimit' argument. If needed, it will be truncated, but will not be less than 24 characters. +func generateEntryPointSpanName(r *http.Request, entryPoint string, spanLimit int) string { + name := fmt.Sprintf("Entrypoint %s %s", entryPoint, r.Host) + + if spanLimit > 0 && len(name) > spanLimit { + if spanLimit < EntryPointMaxLengthNumber { + log.Warnf("SpanNameLimit is set to be less than required static number of characters, defaulting to %d + 3", EntryPointMaxLengthNumber) + spanLimit = EntryPointMaxLengthNumber + 3 + } + hash := computeHash(name) + limit := (spanLimit - EntryPointMaxLengthNumber) / 2 + name = fmt.Sprintf("Entrypoint %s %s %s", truncateString(entryPoint, limit), truncateString(r.Host, limit), hash) + } + + return name +} diff --git a/middlewares/tracing/entrypoint_test.go b/middlewares/tracing/entrypoint_test.go new file mode 100644 index 000000000..865bcfc09 --- /dev/null +++ b/middlewares/tracing/entrypoint_test.go @@ -0,0 +1,70 @@ +package tracing + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/opentracing/opentracing-go/ext" + "github.com/stretchr/testify/assert" +) + +func TestEntryPointMiddlewareServeHTTP(t *testing.T) { + expectedTags := map[string]interface{}{ + "span.kind": ext.SpanKindRPCServerEnum, + "http.method": "GET", + "component": "", + "http.url": "http://www.test.com", + "http.host": "www.test.com", + } + + testCases := []struct { + desc string + entryPoint string + tracing *Tracing + expectedTags map[string]interface{} + expectedName string + }{ + { + desc: "no truncation test", + entryPoint: "test", + tracing: &Tracing{ + SpanNameLimit: 0, + tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}}, + }, + expectedTags: expectedTags, + expectedName: "Entrypoint test www.test.com", + }, { + desc: "basic test", + entryPoint: "test", + tracing: &Tracing{ + SpanNameLimit: 25, + tracer: &MockTracer{Span: &MockSpan{Tags: make(map[string]interface{})}}, + }, + expectedTags: expectedTags, + expectedName: "Entrypoint te... ww... 39b97e58", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + e := &entryPointMiddleware{ + entryPoint: test.entryPoint, + Tracing: test.tracing, + } + + next := func(http.ResponseWriter, *http.Request) { + span := test.tracing.tracer.(*MockTracer).Span + + actual := span.Tags + assert.Equal(t, test.expectedTags, actual) + assert.Equal(t, test.expectedName, span.OpName) + } + + e.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "http://www.test.com", nil), next) + }) + } +} diff --git a/middlewares/tracing/forwarder.go b/middlewares/tracing/forwarder.go index aa80486c5..fd4f243bf 100644 --- a/middlewares/tracing/forwarder.go +++ b/middlewares/tracing/forwarder.go @@ -23,7 +23,7 @@ func (t *Tracing) NewForwarderMiddleware(frontend, backend string) negroni.Handl Tracing: t, frontend: frontend, backend: backend, - opName: fmt.Sprintf("forward %s/%s", frontend, backend), + opName: generateForwardSpanName(frontend, backend, t.SpanNameLimit), } } @@ -44,3 +44,20 @@ func (f *forwarderMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, LogResponseCode(span, recorder.Status()) } + +// generateForwardSpanName will return a Span name of an appropriate lenth based on the 'spanLimit' argument. If needed, it will be truncated, but will not be less than 21 characters +func generateForwardSpanName(frontend, backend string, spanLimit int) string { + name := fmt.Sprintf("forward %s/%s", frontend, backend) + + if spanLimit > 0 && len(name) > spanLimit { + if spanLimit < ForwardMaxLengthNumber { + log.Warnf("SpanNameLimit is set to be less than required static number of characters, defaulting to %d + 3", ForwardMaxLengthNumber) + spanLimit = ForwardMaxLengthNumber + 3 + } + hash := computeHash(name) + limit := (spanLimit - ForwardMaxLengthNumber) / 2 + name = fmt.Sprintf("forward %s/%s/%s", truncateString(frontend, limit), truncateString(backend, limit), hash) + } + + return name +} diff --git a/middlewares/tracing/forwarder_test.go b/middlewares/tracing/forwarder_test.go new file mode 100644 index 000000000..00c90c293 --- /dev/null +++ b/middlewares/tracing/forwarder_test.go @@ -0,0 +1,93 @@ +package tracing + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTracingNewForwarderMiddleware(t *testing.T) { + testCases := []struct { + desc string + tracer *Tracing + frontend string + backend string + expected *forwarderMiddleware + }{ + { + desc: "Simple Forward Tracer without truncation and hashing", + tracer: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service.domain.tld", + backend: "some-service.domain.tld", + expected: &forwarderMiddleware{ + Tracing: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service.domain.tld", + backend: "some-service.domain.tld", + opName: "forward some-service.domain.tld/some-service.domain.tld", + }, + }, { + desc: "Simple Forward Tracer with truncation and hashing", + tracer: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service-100.slug.namespace.environment.domain.tld", + backend: "some-service-100.slug.namespace.environment.domain.tld", + expected: &forwarderMiddleware{ + Tracing: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service-100.slug.namespace.environment.domain.tld", + backend: "some-service-100.slug.namespace.environment.domain.tld", + opName: "forward some-service-100.slug.namespace.enviro.../some-service-100.slug.namespace.enviro.../bc4a0d48", + }, + }, + { + desc: "Exactly 101 chars", + tracer: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service1.namespace.environment.domain.tld", + backend: "some-service1.namespace.environment.domain.tld", + expected: &forwarderMiddleware{ + Tracing: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service1.namespace.environment.domain.tld", + backend: "some-service1.namespace.environment.domain.tld", + opName: "forward some-service1.namespace.environment.domain.tld/some-service1.namespace.environment.domain.tld", + }, + }, + { + desc: "More than 101 chars", + tracer: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service1.frontend.namespace.environment.domain.tld", + backend: "some-service1.backend.namespace.environment.domain.tld", + expected: &forwarderMiddleware{ + Tracing: &Tracing{ + SpanNameLimit: 101, + }, + frontend: "some-service1.frontend.namespace.environment.domain.tld", + backend: "some-service1.backend.namespace.environment.domain.tld", + opName: "forward some-service1.frontend.namespace.envir.../some-service1.backend.namespace.enviro.../fa49dd23", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := test.tracer.NewForwarderMiddleware(test.frontend, test.backend) + + assert.Equal(t, test.expected, actual) + assert.True(t, len(test.expected.opName) <= test.tracer.SpanNameLimit) + }) + } +} diff --git a/middlewares/tracing/tracing.go b/middlewares/tracing/tracing.go index ac7f5b7a9..a18c81806 100644 --- a/middlewares/tracing/tracing.go +++ b/middlewares/tracing/tracing.go @@ -1,6 +1,7 @@ package tracing import ( + "crypto/sha256" "fmt" "io" "net/http" @@ -13,13 +14,23 @@ import ( "github.com/opentracing/opentracing-go/ext" ) +// ForwardMaxLengthNumber defines the number of static characters in the Forwarding Span Trace name : 8 chars for 'forward ' + 8 chars for hash + 2 chars for '_'. +const ForwardMaxLengthNumber = 18 + +// EntryPointMaxLengthNumber defines the number of static characters in the Entrypoint Span Trace name : 11 chars for 'Entrypoint ' + 8 chars for hash + 2 chars for '_'. +const EntryPointMaxLengthNumber = 21 + +// TraceNameHashLength defines the number of characters to use from the head of the generated hash. +const TraceNameHashLength = 8 + // Tracing middleware type Tracing struct { - Backend string `description:"Selects the tracking backend ('jaeger','zipkin', 'datadog')." export:"true"` - ServiceName string `description:"Set the name for this service" export:"true"` - Jaeger *jaeger.Config `description:"Settings for jaeger"` - Zipkin *zipkin.Config `description:"Settings for zipkin"` - DataDog *datadog.Config `description:"Settings for DataDog"` + Backend string `description:"Selects the tracking backend ('jaeger','zipkin', 'datadog')." export:"true"` + ServiceName string `description:"Set the name for this service" export:"true"` + SpanNameLimit int `description:"Set the maximum character limit for Span names (default 0 = no limit)" export:"true"` + Jaeger *jaeger.Config `description:"Settings for jaeger"` + Zipkin *zipkin.Config `description:"Settings for zipkin"` + DataDog *datadog.Config `description:"Settings for DataDog"` tracer opentracing.Tracer closer io.Closer @@ -147,16 +158,40 @@ func SetError(r *http.Request) { } } -// SetErrorAndDebugLog flags the span associated with this request as in error and create a debug log +// SetErrorAndDebugLog flags the span associated with this request as in error and create a debug log. func SetErrorAndDebugLog(r *http.Request, format string, args ...interface{}) { SetError(r) log.Debugf(format, args...) LogEventf(r, format, args...) } -// SetErrorAndWarnLog flags the span associated with this request as in error and create a debug log +// SetErrorAndWarnLog flags the span associated with this request as in error and create a debug log. func SetErrorAndWarnLog(r *http.Request, format string, args ...interface{}) { SetError(r) log.Warnf(format, args...) LogEventf(r, format, args...) } + +// truncateString reduces the length of the 'str' argument to 'num' - 3 and adds a '...' suffix to the tail. +func truncateString(str string, num int) string { + text := str + if len(str) > num { + if num > 3 { + num -= 3 + } + text = str[0:num] + "..." + } + return text +} + +// computeHash returns the first TraceNameHashLength character of the sha256 hash for 'name' argument. +func computeHash(name string) string { + data := []byte(name) + hash := sha256.New() + if _, err := hash.Write(data); err != nil { + // Impossible case + log.Errorf("Fail to create Span name hash for %s: %v", name, err) + } + + return fmt.Sprintf("%x", hash.Sum(nil))[:TraceNameHashLength] +} diff --git a/middlewares/tracing/tracing_test.go b/middlewares/tracing/tracing_test.go new file mode 100644 index 000000000..d4a631312 --- /dev/null +++ b/middlewares/tracing/tracing_test.go @@ -0,0 +1,133 @@ +package tracing + +import ( + "testing" + + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/log" + "github.com/stretchr/testify/assert" +) + +type MockTracer struct { + Span *MockSpan +} + +type MockSpan struct { + OpName string + Tags map[string]interface{} +} + +type MockSpanContext struct { +} + +// MockSpanContext: +func (n MockSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {} + +// MockSpan: +func (n MockSpan) Context() opentracing.SpanContext { return MockSpanContext{} } +func (n MockSpan) SetBaggageItem(key, val string) opentracing.Span { + return MockSpan{Tags: make(map[string]interface{})} +} +func (n MockSpan) BaggageItem(key string) string { return "" } +func (n MockSpan) SetTag(key string, value interface{}) opentracing.Span { + n.Tags[key] = value + return n +} +func (n MockSpan) LogFields(fields ...log.Field) {} +func (n MockSpan) LogKV(keyVals ...interface{}) {} +func (n MockSpan) Finish() {} +func (n MockSpan) FinishWithOptions(opts opentracing.FinishOptions) {} +func (n MockSpan) SetOperationName(operationName string) opentracing.Span { return n } +func (n MockSpan) Tracer() opentracing.Tracer { return MockTracer{} } +func (n MockSpan) LogEvent(event string) {} +func (n MockSpan) LogEventWithPayload(event string, payload interface{}) {} +func (n MockSpan) Log(data opentracing.LogData) {} +func (n MockSpan) Reset() { + n.Tags = make(map[string]interface{}) +} + +// StartSpan belongs to the Tracer interface. +func (n MockTracer) StartSpan(operationName string, opts ...opentracing.StartSpanOption) opentracing.Span { + n.Span.OpName = operationName + return n.Span +} + +// Inject belongs to the Tracer interface. +func (n MockTracer) Inject(sp opentracing.SpanContext, format interface{}, carrier interface{}) error { + return nil +} + +// Extract belongs to the Tracer interface. +func (n MockTracer) Extract(format interface{}, carrier interface{}) (opentracing.SpanContext, error) { + return nil, opentracing.ErrSpanContextNotFound +} + +func TestTruncateString(t *testing.T) { + testCases := []struct { + desc string + text string + limit int + expected string + }{ + { + desc: "short text less than limit 10", + text: "short", + limit: 10, + expected: "short", + }, + { + desc: "basic truncate with limit 10", + text: "some very long pice of text", + limit: 10, + expected: "some ve...", + }, + { + desc: "truncate long FQDN to 39 chars", + text: "some-service-100.slug.namespace.environment.domain.tld", + limit: 39, + expected: "some-service-100.slug.namespace.envi...", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := truncateString(test.text, test.limit) + + assert.Equal(t, test.expected, actual) + assert.True(t, len(actual) <= test.limit) + }) + } +} + +func TestComputeHash(t *testing.T) { + testCases := []struct { + desc string + text string + expected string + }{ + { + desc: "hashing", + text: "some very long pice of text", + expected: "0258ea1c", + }, + { + desc: "short text less than limit 10", + text: "short", + expected: "f9b0078b", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := computeHash(test.text) + + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/provider/acme/provider.go b/provider/acme/provider.go index f06ffa0bd..830bafa25 100644 --- a/provider/acme/provider.go +++ b/provider/acme/provider.go @@ -6,12 +6,14 @@ import ( "fmt" "io/ioutil" fmtlog "log" + "net/url" "reflect" "strings" "sync" "time" "github.com/BurntSushi/ty/fun" + "github.com/cenk/backoff" "github.com/containous/flaeg/parse" "github.com/containous/traefik/log" "github.com/containous/traefik/rules" @@ -73,6 +75,8 @@ type Certificate struct { type DNSChallenge struct { Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."` DelayBeforeCheck parse.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."` + preCheckTimeout time.Duration + preCheckInterval time.Duration } // HTTPChallenge contains HTTP challenge Configuration @@ -130,7 +134,8 @@ func (p *Provider) Init(_ types.Constraints) error { } // Reset Account if caServer changed, thus registration URI can be updated - if p.account != nil && p.account.Registration != nil && !strings.HasPrefix(p.account.Registration.URI, p.CAServer) { + if p.account != nil && p.account.Registration != nil && !isAccountMatchingCaServer(p.account.Registration.URI, p.CAServer) { + log.Info("Account URI does not match the current CAServer. The account will be reset") p.account = nil } @@ -142,6 +147,20 @@ func (p *Provider) Init(_ types.Constraints) error { return nil } +func isAccountMatchingCaServer(accountURI string, serverURI string) bool { + aru, err := url.Parse(accountURI) + if err != nil { + log.Infof("Unable to parse account.Registration URL : %v", err) + return false + } + cau, err := url.Parse(serverURI) + if err != nil { + log.Infof("Unable to parse CAServer URL : %v", err) + return false + } + return cau.Hostname() == aru.Hostname() +} + // Provide allows the file provider to provide configurations to traefik // using the given Configuration channel. func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { @@ -246,6 +265,16 @@ func (p *Provider) getClient() (*acme.Client, error) { if err != nil { return nil, err } + + // Same default values than LEGO + p.DNSChallenge.preCheckTimeout = 60 * time.Second + p.DNSChallenge.preCheckInterval = 2 * time.Second + + // Set the precheck timeout into the DNSChallenge provider + if challengeProviderTimeout, ok := provider.(acme.ChallengeProviderTimeout); ok { + p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval = challengeProviderTimeout.Timeout() + } + } else if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 { log.Debug("Using HTTP Challenge provider.") @@ -345,13 +374,20 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return nil, fmt.Errorf("cannot get ACME client %v", err) } + var certificate *acme.CertificateResource bundle := true - - certificate, err := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple) - if err != nil { - return nil, fmt.Errorf("cannot obtain certificates: %+v", err) + if p.useCertificateWithRetry(uncheckedDomains) { + certificate, err = obtainCertificateWithRetry(domains, client, p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval, bundle) + } else { + certificate, err = client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple) } + if err != nil { + return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %v", uncheckedDomains, err) + } + if certificate == nil { + return nil, fmt.Errorf("domains %v do not generate a certificate", uncheckedDomains) + } if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 { return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, certificate) } @@ -368,6 +404,60 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati return certificate, nil } +func (p *Provider) useCertificateWithRetry(domains []string) bool { + // Check if we can use the retry mechanism only if we use the DNS Challenge and if is there are at least 2 domains to check + if p.DNSChallenge != nil && len(domains) > 1 { + rootDomain := "" + for _, searchWildcardDomain := range domains { + // Search a wildcard domain if not already found + if len(rootDomain) == 0 && strings.HasPrefix(searchWildcardDomain, "*.") { + rootDomain = strings.TrimPrefix(searchWildcardDomain, "*.") + if len(rootDomain) > 0 { + // Look for a root domain which matches the wildcard domain + for _, searchRootDomain := range domains { + if rootDomain == searchRootDomain { + // If the domains list contains a wildcard domain and its root domain, we can use the retry mechanism to obtain the certificate + return true + } + } + } + // There is only one wildcard domain in the slice, if its root domain has not been found, the retry mechanism does not have to be used + return false + } + } + } + + return false +} + +func obtainCertificateWithRetry(domains []string, client *acme.Client, timeout, interval time.Duration, bundle bool) (*acme.CertificateResource, error) { + var certificate *acme.CertificateResource + var err error + + operation := func() error { + certificate, err = client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple) + return err + } + + notify := func(err error, time time.Duration) { + log.Errorf("Error obtaining certificate retrying in %s", time) + } + + // Define a retry backOff to let LEGO tries twice to obtain a certificate for both wildcard and root domain + ebo := backoff.NewExponentialBackOff() + ebo.MaxElapsedTime = 2 * timeout + ebo.MaxInterval = interval + rbo := backoff.WithMaxRetries(ebo, 2) + + err = backoff.RetryNotify(safe.OperationWithRecover(operation), rbo, notify) + if err != nil { + log.Errorf("Error obtaining certificate: %v", err) + return nil, err + } + + return certificate, nil +} + func dnsOverrideDelay(delay parse.Duration) error { if delay == 0 { return nil diff --git a/provider/acme/provider_test.go b/provider/acme/provider_test.go index 047f4058f..abbe34a0f 100644 --- a/provider/acme/provider_test.go +++ b/provider/acme/provider_test.go @@ -429,3 +429,136 @@ func TestDeleteUnnecessaryDomains(t *testing.T) { }) } } + +func TestIsAccountMatchingCaServer(t *testing.T) { + testCases := []struct { + desc string + accountURI string + serverURI string + expected bool + }{ + { + desc: "acme staging with matching account", + accountURI: "https://acme-staging-v02.api.letsencrypt.org/acme/acct/1234567", + serverURI: "https://acme-staging-v02.api.letsencrypt.org/acme/directory", + expected: true, + }, + { + desc: "acme production with matching account", + accountURI: "https://acme-v02.api.letsencrypt.org/acme/acct/1234567", + serverURI: "https://acme-v02.api.letsencrypt.org/acme/directory", + expected: true, + }, + { + desc: "http only acme with matching account", + accountURI: "http://acme.api.letsencrypt.org/acme/acct/1234567", + serverURI: "http://acme.api.letsencrypt.org/acme/directory", + expected: true, + }, + { + desc: "different subdomains for account and server", + accountURI: "https://test1.example.org/acme/acct/1234567", + serverURI: "https://test2.example.org/acme/directory", + expected: false, + }, + { + desc: "different domains for account and server", + accountURI: "https://test.example1.org/acme/acct/1234567", + serverURI: "https://test.example2.org/acme/directory", + expected: false, + }, + { + desc: "different tld for account and server", + accountURI: "https://test.example.com/acme/acct/1234567", + serverURI: "https://test.example.org/acme/directory", + expected: false, + }, + { + desc: "malformed account url", + accountURI: "//|\\/test.example.com/acme/acct/1234567", + serverURI: "https://test.example.com/acme/directory", + expected: false, + }, + { + desc: "malformed server url", + accountURI: "https://test.example.com/acme/acct/1234567", + serverURI: "//|\\/test.example.com/acme/directory", + expected: false, + }, + { + desc: "malformed server and account url", + accountURI: "//|\\/test.example.com/acme/acct/1234567", + serverURI: "//|\\/test.example.com/acme/directory", + expected: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := isAccountMatchingCaServer(test.accountURI, test.serverURI) + + assert.Equal(t, test.expected, result) + }) + } +} + +func TestUseBackOffToObtainCertificate(t *testing.T) { + testCases := []struct { + desc string + domains []string + dnsChallenge *DNSChallenge + expectedResponse bool + }{ + { + desc: "only one single domain", + domains: []string{"acme.wtf"}, + dnsChallenge: &DNSChallenge{}, + expectedResponse: false, + }, + { + desc: "only one wildcard domain", + domains: []string{"*.acme.wtf"}, + dnsChallenge: &DNSChallenge{}, + expectedResponse: false, + }, + { + desc: "wildcard domain with no root domain", + domains: []string{"*.acme.wtf", "foo.acme.wtf", "bar.acme.wtf", "foo.bar"}, + dnsChallenge: &DNSChallenge{}, + expectedResponse: false, + }, + { + desc: "wildcard and root domain", + domains: []string{"*.acme.wtf", "foo.acme.wtf", "bar.acme.wtf", "acme.wtf"}, + dnsChallenge: &DNSChallenge{}, + expectedResponse: true, + }, + { + desc: "wildcard and root domain but no DNS challenge", + domains: []string{"*.acme.wtf", "acme.wtf"}, + dnsChallenge: nil, + expectedResponse: false, + }, + { + desc: "two wildcard domains (must never happen)", + domains: []string{"*.acme.wtf", "*.bar.foo"}, + dnsChallenge: nil, + expectedResponse: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}} + + actualResponse := acmeProvider.useCertificateWithRetry(test.domains) + assert.Equal(t, test.expectedResponse, actualResponse, "unexpected response to use backOff") + }) + } +} diff --git a/provider/kubernetes/annotations.go b/provider/kubernetes/annotations.go index 4b716e395..e85211b2f 100644 --- a/provider/kubernetes/annotations.go +++ b/provider/kubernetes/annotations.go @@ -63,6 +63,7 @@ const ( annotationKubernetesPublicKey = "ingress.kubernetes.io/public-key" annotationKubernetesReferrerPolicy = "ingress.kubernetes.io/referrer-policy" annotationKubernetesIsDevelopment = "ingress.kubernetes.io/is-development" + annotationKubernetesProtocol = "ingress.kubernetes.io/protocol" ) // TODO [breaking] remove label support diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 9ed458e94..926424b9f 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -43,6 +43,8 @@ const ( traefikDefaultIngressClass = "traefik" defaultBackendName = "global-default-backend" defaultFrontendName = "global-default-frontend" + allowedProtocolHTTPS = "https" + allowedProtocolH2C = "h2c" ) // IngressEndpoint holds the endpoint information for the Kubernetes provider @@ -312,6 +314,16 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) protocol = "https" } + protocol = getStringValue(i.Annotations, annotationKubernetesProtocol, protocol) + switch protocol { + case allowedProtocolHTTPS: + case allowedProtocolH2C: + case label.DefaultProtocol: + default: + log.Errorf("Invalid protocol %s/%s specified for Ingress %s - skipping", annotationKubernetesProtocol, i.Namespace, i.Name) + continue + } + if service.Spec.Type == "ExternalName" { url := protocol + "://" + service.Spec.ExternalName if port.Port != 443 && port.Port != 80 { @@ -535,7 +547,7 @@ func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.I if ruleType == ruleTypeReplacePath { return "", fmt.Errorf("rewrite-target must not be used together with annotation %q", annotationKubernetesRuleType) } - rewriteTargetRule := fmt.Sprintf("ReplacePathRegex: ^%s/(.*) %s/$1", pa.Path, strings.TrimRight(rewriteTarget, "/")) + rewriteTargetRule := fmt.Sprintf("ReplacePathRegex: ^%s(.*) %s$1", pa.Path, strings.TrimRight(rewriteTarget, "/")) rules = append(rules, rewriteTargetRule) } diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index e370dc411..e205473f8 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1222,6 +1222,41 @@ rateset: iPaths(onePath(iPath("/customheaders"), iBackend("service1", intstr.FromInt(80))))), ), ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesProtocol, "h2c"), + iRules( + iRule( + iHost("protocol"), + iPaths(onePath(iPath("/valid"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesProtocol, "foobar"), + iRules( + iRule( + iHost("protocol"), + iPaths(onePath(iPath("/notvalid"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesProtocol, "http"), + iRules( + iRule( + iHost("protocol"), + iPaths(onePath(iPath("/missmatch"), iBackend("serviceHTTPS", intstr.FromInt(443))))), + ), + ), + buildIngress( + iNamespace("testing"), + iRules( + iRule( + iHost("protocol"), + iPaths(onePath(iPath("/noAnnotation"), iBackend("serviceHTTPS", intstr.FromInt(443))))), + ), + ), } services := []*corev1.Service{ @@ -1243,6 +1278,16 @@ rateset: clusterIP("10.0.0.2"), sPorts(sPort(802, ""))), ), + buildService( + sName("serviceHTTPS"), + sNamespace("testing"), + sUID("2"), + sSpec( + clusterIP("10.0.0.3"), + sType("ExternalName"), + sExternalName("example.com"), + sPorts(sPort(443, "https"))), + ), } secrets := []*corev1.Secret{ @@ -1350,6 +1395,28 @@ rateset: servers(), lbMethod("wrr"), ), + backend("protocol/valid", + servers( + server("h2c://example.com", weight(1)), + server("h2c://example.com", weight(1))), + lbMethod("wrr"), + ), + backend("protocol/notvalid", + servers(), + lbMethod("wrr"), + ), + backend("protocol/missmatch", + servers( + server("http://example.com", weight(1)), + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + backend("protocol/noAnnotation", + servers( + server("https://example.com", weight(1)), + server("https://example.com", weight(1))), + lbMethod("wrr"), + ), ), frontends( frontend("foo/bar", @@ -1408,7 +1475,7 @@ rateset: frontend("rewrite/api", passHostHeader(), routes( - route("/api", "PathPrefix:/api;ReplacePathRegex: ^/api/(.*) /$1"), + route("/api", "PathPrefix:/api;ReplacePathRegex: ^/api(.*) $1"), route("rewrite", "Host:rewrite")), ), frontend("error-pages/errorpages", @@ -1481,6 +1548,34 @@ rateset: route("root", "Host:root"), ), ), + frontend("protocol/valid", + passHostHeader(), + routes( + route("/valid", "PathPrefix:/valid"), + route("protocol", "Host:protocol"), + ), + ), + frontend("protocol/notvalid", + passHostHeader(), + routes( + route("/notvalid", "PathPrefix:/notvalid"), + route("protocol", "Host:protocol"), + ), + ), + frontend("protocol/missmatch", + passHostHeader(), + routes( + route("/missmatch", "PathPrefix:/missmatch"), + route("protocol", "Host:protocol"), + ), + ), + frontend("protocol/noAnnotation", + passHostHeader(), + routes( + route("/noAnnotation", "PathPrefix:/noAnnotation"), + route("protocol", "Host:protocol"), + ), + ), ), ) diff --git a/types/types.go b/types/types.go index 4adb82b6f..1d6630cbb 100644 --- a/types/types.go +++ b/types/types.go @@ -253,7 +253,7 @@ type Configurations map[string]*Configuration type Configuration struct { Backends map[string]*Backend `json:"backends,omitempty"` Frontends map[string]*Frontend `json:"frontends,omitempty"` - TLS []*traefiktls.Configuration `json:"tls,omitempty"` + TLS []*traefiktls.Configuration `json:"-"` } // ConfigMessage hold configuration information exchanged between parts of traefik. diff --git a/vendor/github.com/vulcand/oxy/forward/fwd.go b/vendor/github.com/vulcand/oxy/forward/fwd.go index 337d5eff5..cd057f59c 100644 --- a/vendor/github.com/vulcand/oxy/forward/fwd.go +++ b/vendor/github.com/vulcand/oxy/forward/fwd.go @@ -395,6 +395,15 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request, errClient := make(chan error, 1) errBackend := make(chan error, 1) replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + + src.SetPingHandler(func(data string) error { + return dst.WriteMessage(websocket.PingMessage, []byte(data)) + }) + + src.SetPongHandler(func(data string) error { + return dst.WriteMessage(websocket.PongMessage, []byte(data)) + }) + for { msgType, msg, err := src.ReadMessage()