Merge branch 'v1.7' into master

This commit is contained in:
Fernandez Ludovic 2018-08-02 17:28:44 +02:00
commit dad0e75121
38 changed files with 1171 additions and 235 deletions

View file

@ -1,5 +1,34 @@
# Change Log # 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) ## [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) [All Commits](https://github.com/containous/traefik/compare/v1.7.0-rc1...v1.7.0-rc2)

2
Gopkg.lock generated
View file

@ -1279,7 +1279,7 @@
"roundrobin", "roundrobin",
"utils" "utils"
] ]
revision = "a3ed5f65204f4ffccbb56d58cec466cdb7ab730b" revision = "fb889e801a26e7e18ef36322ac72a07157f8cc1f"
[[projects]] [[projects]]
name = "github.com/vulcand/predicate" name = "github.com/vulcand/predicate"

View file

@ -9,6 +9,7 @@ import (
fmtlog "log" fmtlog "log"
"net" "net"
"net/http" "net/http"
"net/url"
"reflect" "reflect"
"strings" "strings"
"time" "time"
@ -183,7 +184,8 @@ func (a *ACME) leadershipListener(elected bool) error {
account := object.(*Account) account := object.(*Account)
account.Init() account.Init()
// Reset Account values if caServer changed, thus registration URI can be updated // 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() account.reset()
} }
@ -230,6 +232,20 @@ func (a *ACME) leadershipListener(elected bool) error {
return nil 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) { func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
domain := types.CanonicalDomain(clientHello.ServerName) domain := types.CanonicalDomain(clientHello.ServerName)
account := a.store.Get().(*Account) account := a.store.Get().(*Account)

View file

@ -9,6 +9,7 @@ import (
"github.com/containous/traefik/configuration" "github.com/containous/traefik/configuration"
"github.com/containous/traefik/middlewares/accesslog" "github.com/containous/traefik/middlewares/accesslog"
"github.com/containous/traefik/middlewares/tracing" "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/jaeger"
"github.com/containous/traefik/middlewares/tracing/zipkin" "github.com/containous/traefik/middlewares/tracing/zipkin"
"github.com/containous/traefik/ping" "github.com/containous/traefik/ping"
@ -192,6 +193,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultTracing := tracing.Tracing{ defaultTracing := tracing.Tracing{
Backend: "jaeger", Backend: "jaeger",
ServiceName: "traefik", ServiceName: "traefik",
SpanNameLimit: 0,
Jaeger: &jaeger.Config{ Jaeger: &jaeger.Config{
SamplingServerURL: "http://localhost:5778/sampling", SamplingServerURL: "http://localhost:5778/sampling",
SamplingType: "const", SamplingType: "const",
@ -206,6 +208,11 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
ID128Bit: true, ID128Bit: true,
Debug: false, Debug: false,
}, },
DataDog: &datadog.Config{
LocalAgentHostPort: "localhost:8126",
GlobalTag: "",
Debug: false,
},
} }
// default LifeCycle // default LifeCycle

View file

@ -11,6 +11,7 @@ import (
"github.com/containous/traefik/api" "github.com/containous/traefik/api"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/middlewares/tracing" "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/jaeger"
"github.com/containous/traefik/middlewares/tracing/zipkin" "github.com/containous/traefik/middlewares/tracing/zipkin"
"github.com/containous/traefik/ping" "github.com/containous/traefik/ping"
@ -197,6 +198,10 @@ func (gc *GlobalConfiguration) initTracing() {
log.Warn("Zipkin configuration will be ignored") log.Warn("Zipkin configuration will be ignored")
gc.Tracing.Zipkin = nil gc.Tracing.Zipkin = nil
} }
if gc.Tracing.DataDog != nil {
log.Warn("DataDog configuration will be ignored")
gc.Tracing.DataDog = nil
}
case zipkin.Name: case zipkin.Name:
if gc.Tracing.Zipkin == nil { if gc.Tracing.Zipkin == nil {
gc.Tracing.Zipkin = &zipkin.Config{ gc.Tracing.Zipkin = &zipkin.Config{
@ -210,6 +215,26 @@ func (gc *GlobalConfiguration) initTracing() {
log.Warn("Jaeger configuration will be ignored") log.Warn("Jaeger configuration will be ignored")
gc.Tracing.Jaeger = nil 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: default:
log.Warnf("Unknown tracer %q", gc.Tracing.Backend) log.Warnf("Unknown tracer %q", gc.Tracing.Backend)
return return

View file

@ -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). | | `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. | | `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/app-root: "/index.html"` | Redirects all requests for `/` to the defined path. (4) |
| `traefik.ingress.kubernetes.io/service-weights: <YML>` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5) | | `traefik.ingress.kubernetes.io/service-weights: <YML>` | Set ingress backend weights specified as percentage or decimal numbers in YAML. (5)
| `ingress.kubernetes.io/protocol: <NAME>` | Set the protocol Traefik will use to communicate with pods.
<1> `traefik.ingress.kubernetes.io/error-pages` example: <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/affinity: "true"` | Enable backend sticky sessions. |
| `traefik.ingress.kubernetes.io/circuit-breaker-expression: <expression>` | Set the circuit breaker expression for the backend. | | `traefik.ingress.kubernetes.io/circuit-breaker-expression: <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/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.<br>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.<br>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.<br>Must be used in conjunction with the above 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.<br>Must be used in conjunction with the above label to take effect. |
| `traefik.ingress.kubernetes.io/session-cookie-name: <NAME>` | Manually set the cookie name for sticky sessions. | | `traefik.ingress.kubernetes.io/session-cookie-name: <NAME>` | Manually set the cookie name for sticky sessions. |

View file

@ -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. 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 ## Jaeger
@ -23,6 +23,13 @@ Træfik supports two backends: Jaeger and Zipkin.
# #
serviceName = "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] [tracing.jaeger]
# Sampling Server URL is the address of jaeger-agent's HTTP sampling server # Sampling Server URL is the address of jaeger-agent's HTTP sampling server
# #
@ -85,6 +92,13 @@ Træfik supports two backends: Jaeger and Zipkin.
# #
serviceName = "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] [tracing.zipkin]
# Zipking HTTP endpoint used to send data # Zipking HTTP endpoint used to send data
# #
@ -128,6 +142,13 @@ Træfik supports two backends: Jaeger and Zipkin.
# #
serviceName = "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] [tracing.datadog]
# Local Agent Host Port instructs reporter to send spans to datadog-tracing-agent at this address # Local Agent Host Port instructs reporter to send spans to datadog-tracing-agent at this address
# #

View file

@ -44,7 +44,7 @@ _(But if you'd rather configure some of your routes manually, Træfik supports t
- Keeps access logs (JSON, CLF) - Keeps access logs (JSON, CLF)
- Fast - Fast
- Exposes a Rest API - 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 ## Supported Providers

View file

@ -453,8 +453,8 @@ kubectl create secret generic mysecret --from-file auth --namespace=monitoring
C. Attach the following annotations to the Ingress object: C. Attach the following annotations to the Ingress object:
- `ingress.kubernetes.io/auth-type: "basic"` - `traefik.ingress.kubernetes.io/auth-type: "basic"`
- `ingress.kubernetes.io/auth-secret: "mysecret"` - `traefik.ingress.kubernetes.io/auth-secret: "mysecret"`
They specify basic authentication and reference the Secret `mysecret` containing the credentials. They specify basic authentication and reference the Secret `mysecret` containing the credentials.
@ -468,8 +468,8 @@ metadata:
namespace: monitoring namespace: monitoring
annotations: annotations:
kubernetes.io/ingress.class: traefik kubernetes.io/ingress.class: traefik
ingress.kubernetes.io/auth-type: "basic" traefik.ingress.kubernetes.io/auth-type: "basic"
ingress.kubernetes.io/auth-secret: "mysecret" traefik.ingress.kubernetes.io/auth-secret: "mysecret"
spec: spec:
rules: rules:
- host: dashboard.prometheus.example.com - host: dashboard.prometheus.example.com

View file

@ -38,7 +38,6 @@ func init() {
if strings.Contains(e, "http2debug=1") || strings.Contains(e, "http2debug=2") { if strings.Contains(e, "http2debug=1") || strings.Contains(e, "http2debug=2") {
http2VerboseLogs = true http2VerboseLogs = true
} }
http2VerboseLogs = true
} }
// Server implements net.Handler and enables h2c. Users who want h2c just need // Server implements net.Handler and enables h2c. Users who want h2c just need

View file

@ -79,7 +79,7 @@ func setupPebbleRootCA() (*http.Transport, error) {
} }
func (s *AcmeSuite) SetUpSuite(c *check.C) { func (s *AcmeSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "peddle") s.createComposeProject(c, "pebble")
s.composeProject.Start(c) s.composeProject.Start(c)
s.fakeDNSServer = startFakeDNSServer() s.fakeDNSServer = startFakeDNSServer()
@ -91,7 +91,7 @@ func (s *AcmeSuite) SetUpSuite(c *check.C) {
c.Fatal(err) c.Fatal(err)
} }
// wait for peddle // wait for pebble
req := testhelpers.MustNewRequest(http.MethodGet, s.getAcmeURL(), nil) req := testhelpers.MustNewRequest(http.MethodGet, s.getAcmeURL(), nil)
client := &http.Client{ client := &http.Client{

View file

@ -585,21 +585,14 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) {
}) })
c.Assert(err, checker.IsNil) 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) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) 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 // 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 { for key, value := range tlsconfigure2 {
@ -614,18 +607,12 @@ func (s *ConsulSuite) TestSNIDynamicTlsConfig(c *check.C) {
}) })
c.Assert(err, checker.IsNil) 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) req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.org")
} }

View file

@ -532,21 +532,14 @@ func (s *Etcd3Suite) TestSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() 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) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) 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 // 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) 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) req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) 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) { func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) {
@ -646,21 +633,14 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() 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) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) 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 // now we delete the tls cert/key pairs,so the endpoint show use default cert/key pair
for key := range tlsconfigure1 { for key := range tlsconfigure1 {
@ -668,18 +648,12 @@ func (s *Etcd3Suite) TestDeleteSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil) 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) req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "TRAEFIK DEFAULT CERT")
} }

View file

@ -548,21 +548,14 @@ func (s *EtcdSuite) TestSNIDynamicTlsConfig(c *check.C) {
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer cmd.Process.Kill() 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) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req.Host = tr1.TLSClientConfig.ServerName req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) 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 // 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) 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) req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) c.Assert(err, checker.IsNil)
cn = resp.TLS.PeerCertificates[0].Subject.CommonName
c.Assert(cn, checker.Equals, "snitest.org")
} }

View file

@ -2,7 +2,7 @@
[backends] [backends]
[backends.backend2] [backends.backend2]
[backends.backend2.servers.server1] [backends.backend2.servers.server1]
url = "http://172.17.0.2:80" url = "http://172.17.0.123:80"
weight = 1 weight = 1
[frontends] [frontends]

View file

@ -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]+}"

View file

@ -3,7 +3,6 @@ package integration
import ( import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"fmt"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -66,7 +65,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
defer cmd.Process.Kill() defer cmd.Process.Kill()
// wait for Traefik // 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) c.Assert(err, checker.IsNil)
backend1 := startTestServer("9010", http.StatusNoContent) 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) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
req.Host = "snitest.com" req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", "snitest.com") req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
req.Host = "snitest.org" req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", "snitest.org") req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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) 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 // 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)) err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client := &http.Client{Transport: tr1}
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
req.Host = tr1.TLSClientConfig.ServerName req.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") 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.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("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) 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 // 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)) err = try.GetRequest(backend2.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusResetContent))
c.Assert(err, checker.IsNil) 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) 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.Host = tr1.TLSClientConfig.ServerName
req.Header.Set("Host", tr1.TLSClientConfig.ServerName) req.Header.Set("Host", tr1.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
// Change certificates configuration file content err = try.RequestWithTransport(req, 30*time.Second, tr1, try.HasCn(tr1.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusNotFound))
modifyCertificateConfFileContent(c, tr1.TLSClientConfig.ServerName, dynamicConfFileName, "https") c.Assert(err, checker.IsNil)
var resp *http.Response
err = try.Do(30*time.Second, func() error { req, err = http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil)
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
})
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound)
client = &http.Client{Transport: tr2}
req.Host = tr2.TLSClientConfig.ServerName req.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*")
err = try.Do(60*time.Second, func() error { err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("TRAEFIK DEFAULT CERT"), try.StatusCodeIs(http.StatusNotFound))
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(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound)
} }
// TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion involves a client sending HTTPS requests with // TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion involves a client sending HTTPS requests with
@ -725,53 +686,19 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443/", nil) 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.Host = tr2.TLSClientConfig.ServerName
req.Header.Set("Host", tr2.TLSClientConfig.ServerName) req.Header.Set("Host", tr2.TLSClientConfig.ServerName)
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
var resp *http.Response err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn(tr2.TLSClientConfig.ServerName), try.StatusCodeIs(http.StatusResetContent))
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(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, http.StatusResetContent)
// Change certificates configuration file content // Change certificates configuration file content
modifyCertificateConfFileContent(c, "", dynamicConfFileName, "https02") modifyCertificateConfFileContent(c, "", dynamicConfFileName, "https02")
err = try.Do(60*time.Second, func() error { err = try.RequestWithTransport(req, 30*time.Second, tr2, try.HasCn("TRAEFIK DEFAULT CERT"), try.StatusCodeIs(http.StatusNotFound))
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
})
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound)
} }
// modifyCertificateConfFileContent replaces the content of a HTTPS configuration file. // 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) 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)
}
}

View file

@ -1,6 +1,6 @@
pebble: pebble:
image: ldez/pebble image: letsencrypt/pebble:2018-07-27
command: --dnsserver ${DOCKER_HOST_IP}:5053 command: pebble --dnsserver ${DOCKER_HOST_IP}:5053
ports: ports:
- 14000:14000 - 14000:14000
environment: environment:

View file

@ -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. // StatusCodeIs returns a retry condition function.
// The condition returns an error if the given response's status code is not the // The condition returns an error if the given response's status code is not the
// given HTTP status code. // given HTTP status code.

View file

@ -31,7 +31,7 @@ func Sleep(d time.Duration) {
// response body needs to be closed or not. Callers are expected to close on // response body needs to be closed or not. Callers are expected to close on
// their own if the function returns a nil error. // their own if the function returns a nil error.
func Response(req *http.Request, timeout time.Duration) (*http.Response, 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 // 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 // response body needs to be closed or not. Callers are expected to close on
// their own if the function returns a nil error. // their own if the function returns a nil error.
func ResponseUntilStatusCode(req *http.Request, timeout time.Duration, statusCode int) (*http.Response, 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 // 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 // ResponseCondition may be nil, in which case only the request against the URL must
// succeed. // succeed.
func GetRequest(url string, timeout time.Duration, conditions ...ResponseCondition) error { 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 { if resp != nil && resp.Body != nil {
defer resp.Body.Close() 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 // ResponseCondition may be nil, in which case only the request against the URL must
// succeed. // succeed.
func Request(req *http.Request, timeout time.Duration, conditions ...ResponseCondition) error { 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 { if resp != nil && resp.Body != nil {
defer resp.Body.Close() 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) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, err 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) { func doTryRequest(request *http.Request, timeout time.Duration, transport *http.Transport, conditions ...ResponseCondition) (*http.Response, error) {
return doRequest(Do, timeout, request, conditions...) 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 var resp *http.Response
return resp, action(timeout, func() error { return resp, action(timeout, func() error {
var err error var err error
client := http.DefaultClient client := http.DefaultClient
if transport != nil {
client.Transport = transport
}
resp, err = client.Do(request) resp, err = client.Do(request)
if err != nil { if err != nil {

View file

@ -1,6 +1,7 @@
package middlewares package middlewares
import ( import (
"context"
"net/http" "net/http"
) )
@ -10,12 +11,21 @@ type AddPrefix struct {
Prefix string 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) { func (s *AddPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.URL.Path = s.Prefix + r.URL.Path r.URL.Path = s.Prefix + r.URL.Path
if r.URL.RawPath != "" { if r.URL.RawPath != "" {
r.URL.RawPath = s.Prefix + r.URL.RawPath r.URL.RawPath = s.Prefix + r.URL.RawPath
} }
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()
r = r.WithContext(context.WithValue(r.Context(), AddPrefixKey, s.Prefix))
s.Handler.ServeHTTP(w, r) s.Handler.ServeHTTP(w, r)
} }

View file

@ -11,6 +11,7 @@ import (
"text/template" "text/template"
"github.com/containous/traefik/configuration" "github.com/containous/traefik/configuration"
"github.com/containous/traefik/middlewares"
"github.com/urfave/negroni" "github.com/urfave/negroni"
"github.com/vulcand/oxy/utils" "github.com/vulcand/oxy/utils"
) )
@ -85,6 +86,30 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
return 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 { if newURL != oldURL {
handler := &moveHandler{location: parsedURL, permanent: h.permanent} handler := &moveHandler{location: parsedURL, permanent: h.permanent}
handler.ServeHTTP(rw, req) handler.ServeHTTP(rw, req)

View file

@ -1,12 +1,21 @@
package middlewares package middlewares
import ( import (
"context"
"net/http" "net/http"
"strings" "strings"
) )
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 is the default header to set prefix
const ForwardedPrefixHeader = "X-Forwarded-Prefix" ForwardedPrefixHeader = "X-Forwarded-Prefix"
)
// StripPrefix is a middleware used to strip prefix from an URL request // StripPrefix is a middleware used to strip prefix from an URL request
type StripPrefix struct { type StripPrefix struct {
@ -17,18 +26,21 @@ type StripPrefix struct {
func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, prefix := range s.Prefixes { for _, prefix := range s.Prefixes {
if strings.HasPrefix(r.URL.Path, prefix) { if strings.HasPrefix(r.URL.Path, prefix) {
trailingSlash := r.URL.Path == prefix+"/"
r.URL.Path = stripPrefix(r.URL.Path, prefix) r.URL.Path = stripPrefix(r.URL.Path, prefix)
if r.URL.RawPath != "" { if r.URL.RawPath != "" {
r.URL.RawPath = stripPrefix(r.URL.RawPath, prefix) r.URL.RawPath = stripPrefix(r.URL.RawPath, prefix)
} }
s.serveRequest(w, r, strings.TrimSpace(prefix)) s.serveRequest(w, r, strings.TrimSpace(prefix), trailingSlash)
return return
} }
} }
http.NotFound(w, r) 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.Header.Add(ForwardedPrefixHeader, prefix)
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r) s.Handler.ServeHTTP(w, r)

View file

@ -1,6 +1,7 @@
package middlewares package middlewares
import ( import (
"context"
"net/http" "net/http"
"github.com/containous/mux" "github.com/containous/mux"
@ -39,10 +40,13 @@ func (s *StripPrefixRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
trailingSlash := r.URL.Path == prefix.Path+"/"
r.URL.Path = r.URL.Path[len(prefix.Path):] r.URL.Path = r.URL.Path[len(prefix.Path):]
if r.URL.RawPath != "" { if r.URL.RawPath != "" {
r.URL.RawPath = r.URL.RawPath[len(prefix.Path):] 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.Header.Add(ForwardedPrefixHeader, prefix.Path)
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r) s.Handler.ServeHTTP(w, r)

View file

@ -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) { func (e *entryPointMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
opNameFunc := func(r *http.Request) string { opNameFunc := generateEntryPointSpanName
return fmt.Sprintf("Entrypoint %s %s", e.entryPoint, r.Host)
}
ctx, _ := e.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) 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) ext.Component.Set(span, e.ServiceName)
LogRequest(span, r) LogRequest(span, r)
ext.SpanKindRPCServer.Set(span) ext.SpanKindRPCServer.Set(span)
@ -40,3 +38,20 @@ func (e *entryPointMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request,
LogResponseCode(span, recorder.Status()) LogResponseCode(span, recorder.Status())
span.Finish() 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
}

View file

@ -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)
})
}
}

View file

@ -23,7 +23,7 @@ func (t *Tracing) NewForwarderMiddleware(frontend, backend string) negroni.Handl
Tracing: t, Tracing: t,
frontend: frontend, frontend: frontend,
backend: backend, 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()) 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
}

View file

@ -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)
})
}
}

View file

@ -1,6 +1,7 @@
package tracing package tracing
import ( import (
"crypto/sha256"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -13,10 +14,20 @@ import (
"github.com/opentracing/opentracing-go/ext" "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 // Tracing middleware
type Tracing struct { type Tracing struct {
Backend string `description:"Selects the tracking backend ('jaeger','zipkin', 'datadog')." export:"true"` Backend string `description:"Selects the tracking backend ('jaeger','zipkin', 'datadog')." export:"true"`
ServiceName string `description:"Set the name for this service" 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"` Jaeger *jaeger.Config `description:"Settings for jaeger"`
Zipkin *zipkin.Config `description:"Settings for zipkin"` Zipkin *zipkin.Config `description:"Settings for zipkin"`
DataDog *datadog.Config `description:"Settings for DataDog"` DataDog *datadog.Config `description:"Settings for DataDog"`
@ -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{}) { func SetErrorAndDebugLog(r *http.Request, format string, args ...interface{}) {
SetError(r) SetError(r)
log.Debugf(format, args...) log.Debugf(format, args...)
LogEventf(r, 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{}) { func SetErrorAndWarnLog(r *http.Request, format string, args ...interface{}) {
SetError(r) SetError(r)
log.Warnf(format, args...) log.Warnf(format, args...)
LogEventf(r, 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]
}

View file

@ -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)
})
}
}

View file

@ -6,12 +6,14 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
fmtlog "log" fmtlog "log"
"net/url"
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
"github.com/cenk/backoff"
"github.com/containous/flaeg/parse" "github.com/containous/flaeg/parse"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
"github.com/containous/traefik/rules" "github.com/containous/traefik/rules"
@ -73,6 +75,8 @@ type Certificate struct {
type DNSChallenge struct { type DNSChallenge struct {
Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."` 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."` 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 // 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 // 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 p.account = nil
} }
@ -142,6 +147,20 @@ func (p *Provider) Init(_ types.Constraints) error {
return nil 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 // Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel. // using the given Configuration channel.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { 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 { if err != nil {
return nil, err 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 { } else if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
log.Debug("Using HTTP Challenge provider.") 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) return nil, fmt.Errorf("cannot get ACME client %v", err)
} }
var certificate *acme.CertificateResource
bundle := true bundle := true
if p.useCertificateWithRetry(uncheckedDomains) {
certificate, err := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple) certificate, err = obtainCertificateWithRetry(domains, client, p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval, bundle)
if err != nil { } else {
return nil, fmt.Errorf("cannot obtain certificates: %+v", err) 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 { if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 {
return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, certificate) 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 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 { func dnsOverrideDelay(delay parse.Duration) error {
if delay == 0 { if delay == 0 {
return nil return nil

View file

@ -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")
})
}
}

View file

@ -63,6 +63,7 @@ const (
annotationKubernetesPublicKey = "ingress.kubernetes.io/public-key" annotationKubernetesPublicKey = "ingress.kubernetes.io/public-key"
annotationKubernetesReferrerPolicy = "ingress.kubernetes.io/referrer-policy" annotationKubernetesReferrerPolicy = "ingress.kubernetes.io/referrer-policy"
annotationKubernetesIsDevelopment = "ingress.kubernetes.io/is-development" annotationKubernetesIsDevelopment = "ingress.kubernetes.io/is-development"
annotationKubernetesProtocol = "ingress.kubernetes.io/protocol"
) )
// TODO [breaking] remove label support // TODO [breaking] remove label support

View file

@ -43,6 +43,8 @@ const (
traefikDefaultIngressClass = "traefik" traefikDefaultIngressClass = "traefik"
defaultBackendName = "global-default-backend" defaultBackendName = "global-default-backend"
defaultFrontendName = "global-default-frontend" defaultFrontendName = "global-default-frontend"
allowedProtocolHTTPS = "https"
allowedProtocolH2C = "h2c"
) )
// IngressEndpoint holds the endpoint information for the Kubernetes provider // 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 = "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" { if service.Spec.Type == "ExternalName" {
url := protocol + "://" + service.Spec.ExternalName url := protocol + "://" + service.Spec.ExternalName
if port.Port != 443 && port.Port != 80 { if port.Port != 443 && port.Port != 80 {
@ -535,7 +547,7 @@ func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.I
if ruleType == ruleTypeReplacePath { if ruleType == ruleTypeReplacePath {
return "", fmt.Errorf("rewrite-target must not be used together with annotation %q", annotationKubernetesRuleType) 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) rules = append(rules, rewriteTargetRule)
} }

View file

@ -1222,6 +1222,41 @@ rateset:
iPaths(onePath(iPath("/customheaders"), iBackend("service1", intstr.FromInt(80))))), 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{ services := []*corev1.Service{
@ -1243,6 +1278,16 @@ rateset:
clusterIP("10.0.0.2"), clusterIP("10.0.0.2"),
sPorts(sPort(802, ""))), 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{ secrets := []*corev1.Secret{
@ -1350,6 +1395,28 @@ rateset:
servers(), servers(),
lbMethod("wrr"), 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( frontends(
frontend("foo/bar", frontend("foo/bar",
@ -1408,7 +1475,7 @@ rateset:
frontend("rewrite/api", frontend("rewrite/api",
passHostHeader(), passHostHeader(),
routes( routes(
route("/api", "PathPrefix:/api;ReplacePathRegex: ^/api/(.*) /$1"), route("/api", "PathPrefix:/api;ReplacePathRegex: ^/api(.*) $1"),
route("rewrite", "Host:rewrite")), route("rewrite", "Host:rewrite")),
), ),
frontend("error-pages/errorpages", frontend("error-pages/errorpages",
@ -1481,6 +1548,34 @@ rateset:
route("root", "Host:root"), 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"),
),
),
), ),
) )

View file

@ -253,7 +253,7 @@ type Configurations map[string]*Configuration
type Configuration struct { type Configuration struct {
Backends map[string]*Backend `json:"backends,omitempty"` Backends map[string]*Backend `json:"backends,omitempty"`
Frontends map[string]*Frontend `json:"frontends,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. // ConfigMessage hold configuration information exchanged between parts of traefik.

View file

@ -395,6 +395,15 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
errClient := make(chan error, 1) errClient := make(chan error, 1)
errBackend := make(chan error, 1) errBackend := make(chan error, 1)
replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { 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 { for {
msgType, msg, err := src.ReadMessage() msgType, msg, err := src.ReadMessage()