Merge branch 'v1.7' into master

This commit is contained in:
Fernandez Ludovic 2018-08-28 15:50:00 +02:00
commit 4055654e9b
63 changed files with 2687 additions and 852 deletions

View file

@ -1,5 +1,23 @@
# Change Log # Change Log
## [v1.6.6](https://github.com/containous/traefik/tree/v1.6.6) (2018-08-20)
[All Commits](https://github.com/containous/traefik/compare/v1.6.5...v1.6.6)
**Bug fixes:**
- **[acme]** Avoid duplicated ACME resolution ([#3751](https://github.com/containous/traefik/pull/3751) by [nmengin](https://github.com/nmengin))
- **[api]** Remove TLS in API ([#3788](https://github.com/containous/traefik/pull/3788) by [Juliens](https://github.com/Juliens))
- **[cluster]** Remove unusable `--cluster` flag ([#3616](https://github.com/containous/traefik/pull/3616) by [dtomcej](https://github.com/dtomcej))
- **[ecs]** Fix bad condition in ECS provider ([#3609](https://github.com/containous/traefik/pull/3609) by [mmatur](https://github.com/mmatur))
- Set keepalive on TCP socket so idleTimeout works ([#3740](https://github.com/containous/traefik/pull/3740) by [ajardan](https://github.com/ajardan))
**Documentation:**
- A tiny rewording on the documentation API's page ([#3794](https://github.com/containous/traefik/pull/3794) by [dduportal](https://github.com/dduportal))
- Adding warnings and solution about the configuration exposure ([#3790](https://github.com/containous/traefik/pull/3790) by [dduportal](https://github.com/dduportal))
- Fix path to the debug pprof API ([#3608](https://github.com/containous/traefik/pull/3608) by [multani](https://github.com/multani))
**Misc:**
- **[oxy,websocket]** Update oxy dependency ([#3777](https://github.com/containous/traefik/pull/3777) by [Juliens](https://github.com/Juliens))
## [v1.7.0-rc3](https://github.com/containous/traefik/tree/v1.7.0-rc3) (2018-08-01) ## [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) [All Commits](https://github.com/containous/traefik/compare/v1.7.0-rc2...v1.7.0-rc3)

5
Gopkg.lock generated
View file

@ -695,7 +695,7 @@
branch = "master" branch = "master"
name = "github.com/gorilla/websocket" name = "github.com/gorilla/websocket"
packages = ["."] packages = ["."]
revision = "eb925808374e5ca90c83401a40d711dc08c0c0f6" revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
[[projects]] [[projects]]
name = "github.com/gravitational/trace" name = "github.com/gravitational/trace"
@ -776,6 +776,7 @@
revision = "9b66602d496a139e4722bdde32f0f1ac1c12d4a8" revision = "9b66602d496a139e4722bdde32f0f1ac1c12d4a8"
[[projects]] [[projects]]
branch = "master"
name = "github.com/jjcollinge/servicefabric" name = "github.com/jjcollinge/servicefabric"
packages = ["."] packages = ["."]
revision = "8eebe170fa1ba25d3dfb928b3f86a7313b13b9fe" revision = "8eebe170fa1ba25d3dfb928b3f86a7313b13b9fe"
@ -1262,7 +1263,7 @@
"roundrobin", "roundrobin",
"utils" "utils"
] ]
revision = "fb889e801a26e7e18ef36322ac72a07157f8cc1f" revision = "f6bbeac6d5c4c06f88ba07ed42983ff36a5b407e"
[[projects]] [[projects]]
name = "github.com/vulcand/predicate" name = "github.com/vulcand/predicate"

View file

@ -9,7 +9,7 @@
[![](https://images.microbadger.com/badges/image/traefik.svg)](https://microbadger.com/images/traefik) [![](https://images.microbadger.com/badges/image/traefik.svg)](https://microbadger.com/images/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md)
[![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io) [![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io)
[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) [![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik)
Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.
@ -164,12 +164,10 @@ Each version is supported until the next one is released (e.g. 1.1.x will be sup
We use [Semantic Versioning](http://semver.org/) We use [Semantic Versioning](http://semver.org/)
## Plumbing ## Mailing lists
- [Oxy](https://github.com/vulcand/oxy): an awesome proxy library made by Mailgun folks - General announcements, new releases: mail at news+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/news)
- [Gorilla mux](https://github.com/gorilla/mux): famous request router - Security announcements: mail at security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security).
- [Negroni](https://github.com/urfave/negroni): web middlewares made simple
- [Lego](https://github.com/xenolf/lego): the best [Let's Encrypt](https://letsencrypt.org) library in go
## Credits ## Credits

View file

@ -12,6 +12,7 @@ import (
"net/url" "net/url"
"reflect" "reflect"
"strings" "strings"
"sync"
"time" "time"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
@ -64,6 +65,8 @@ type ACME struct {
jobs *channels.InfiniteChannel jobs *channels.InfiniteChannel
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"` TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
dynamicCerts *safe.Safe dynamicCerts *safe.Safe
resolvingDomains map[string]struct{}
resolvingDomainsMutex sync.RWMutex
} }
func (a *ACME) init() error { func (a *ACME) init() error {
@ -76,6 +79,10 @@ func (a *ACME) init() error {
} }
a.jobs = channels.NewInfiniteChannel() a.jobs = channels.NewInfiniteChannel()
// Init the currently resolved domain map
a.resolvingDomains = make(map[string]struct{})
return nil return nil
} }
@ -202,6 +209,9 @@ func (a *ACME) leadershipListener(elected bool) error {
} }
needRegister = true needRegister = true
} else if len(account.KeyType) == 0 {
// Set the KeyType if not already defined in the account
account.KeyType = acmeprovider.GetKeyType(a.KeyType)
} }
a.client, err = a.buildACMEClient(account) a.client, err = a.buildACMEClient(account)
@ -534,6 +544,10 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
if len(uncheckedDomains) == 0 { if len(uncheckedDomains) == 0 {
return return
} }
a.addResolvingDomains(uncheckedDomains)
defer a.removeResolvingDomains(uncheckedDomains)
certificate, err := a.getDomainsCertificates(uncheckedDomains) certificate, err := a.getDomainsCertificates(uncheckedDomains)
if err != nil { if err != nil {
log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err) log.Errorf("Error getting ACME certificates %+v : %v", uncheckedDomains, err)
@ -565,6 +579,24 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
} }
} }
func (a *ACME) addResolvingDomains(resolvingDomains []string) {
a.resolvingDomainsMutex.Lock()
defer a.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
a.resolvingDomains[domain] = struct{}{}
}
}
func (a *ACME) removeResolvingDomains(resolvingDomains []string) {
a.resolvingDomainsMutex.Lock()
defer a.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
delete(a.resolvingDomains, domain)
}
}
// Get provided certificate which check a domains list (Main and SANs) // Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates // from static and dynamic provided certificates
func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate { func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate {
@ -600,6 +632,9 @@ func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Ce
// Get provided certificate which check a domains list (Main and SANs) // Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates // from static and dynamic provided certificates
func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string { func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string {
a.resolvingDomainsMutex.RLock()
defer a.resolvingDomainsMutex.RUnlock()
log.Debugf("Looking for provided certificate to validate %s...", domains) log.Debugf("Looking for provided certificate to validate %s...", domains)
allCerts := make(map[string]*tls.Certificate) allCerts := make(map[string]*tls.Certificate)
@ -622,6 +657,13 @@ func (a *ACME) getUncheckedDomains(domains []string, account *Account) []string
} }
} }
// Get currently resolved domains
for domain := range a.resolvingDomains {
if _, ok := allCerts[domain]; !ok {
allCerts[domain] = &tls.Certificate{}
}
}
// Get Configuration Domains // Get Configuration Domains
for i := 0; i < len(a.Domains); i++ { for i := 0; i < len(a.Domains); i++ {
allCerts[a.Domains[i].Main] = &tls.Certificate{} allCerts[a.Domains[i].Main] = &tls.Certificate{}

View file

@ -331,9 +331,12 @@ func TestAcme_getUncheckedCertificates(t *testing.T) {
mm["*.containo.us"] = &tls.Certificate{} mm["*.containo.us"] = &tls.Certificate{}
mm["traefik.acme.io"] = &tls.Certificate{} mm["traefik.acme.io"] = &tls.Certificate{}
a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}} dm := make(map[string]struct{})
dm["*.traefik.wtf"] = struct{}{}
domains := []string{"traefik.containo.us", "trae.containo.us"} a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}, resolvingDomains: dm}
domains := []string{"traefik.containo.us", "trae.containo.us", "foo.traefik.wtf"}
uncheckedDomains := a.getUncheckedDomains(domains, nil) uncheckedDomains := a.getUncheckedDomains(domains, nil)
assert.Empty(t, uncheckedDomains) assert.Empty(t, uncheckedDomains)
domains = []string{"traefik.acme.io", "trae.acme.io"} domains = []string{"traefik.acme.io", "trae.acme.io"}
@ -351,6 +354,9 @@ func TestAcme_getUncheckedCertificates(t *testing.T) {
account := Account{DomainsCertificate: domainsCertificates} account := Account{DomainsCertificate: domainsCertificates}
uncheckedDomains = a.getUncheckedDomains(domains, &account) uncheckedDomains = a.getUncheckedDomains(domains, &account)
assert.Empty(t, uncheckedDomains) assert.Empty(t, uncheckedDomains)
domains = []string{"traefik.containo.us", "trae.containo.us", "traefik.wtf"}
uncheckedDomains = a.getUncheckedDomains(domains, nil)
assert.Len(t, uncheckedDomains, 1)
} }
func TestAcme_getProvidedCertificate(t *testing.T) { func TestAcme_getProvidedCertificate(t *testing.T) {

View file

@ -144,8 +144,8 @@ var _templatesConsul_catalogTmpl = []byte(`[backends]
[frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls] [frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
@ -389,8 +389,8 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}}
[frontends."frontend-{{ $frontendName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
@ -549,13 +549,13 @@ var _templatesEcsTmpl = []byte(`[backends]
{{range $serviceName, $instances := .Services }} {{range $serviceName, $instances := .Services }}
{{ $firstInstance := index $instances 0 }} {{ $firstInstance := index $instances 0 }}
{{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} {{ $circuitBreaker := getCircuitBreaker $firstInstance.SegmentLabels }}
{{if $circuitBreaker }} {{if $circuitBreaker }}
[backends."backend-{{ $serviceName }}".circuitBreaker] [backends."backend-{{ $serviceName }}".circuitBreaker]
expression = "{{ $circuitBreaker.Expression }}" expression = "{{ $circuitBreaker.Expression }}"
{{end}} {{end}}
{{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} {{ $loadBalancer := getLoadBalancer $firstInstance.SegmentLabels }}
{{if $loadBalancer }} {{if $loadBalancer }}
[backends."backend-{{ $serviceName }}".loadBalancer] [backends."backend-{{ $serviceName }}".loadBalancer]
method = "{{ $loadBalancer.Method }}" method = "{{ $loadBalancer.Method }}"
@ -565,14 +565,14 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{end}} {{end}}
{{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} {{ $maxConn := getMaxConn $firstInstance.SegmentLabels }}
{{if $maxConn }} {{if $maxConn }}
[backends."backend-{{ $serviceName }}".maxConn] [backends."backend-{{ $serviceName }}".maxConn]
extractorFunc = "{{ $maxConn.ExtractorFunc }}" extractorFunc = "{{ $maxConn.ExtractorFunc }}"
amount = {{ $maxConn.Amount }} amount = {{ $maxConn.Amount }}
{{end}} {{end}}
{{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} {{ $healthCheck := getHealthCheck $firstInstance.SegmentLabels }}
{{if $healthCheck }} {{if $healthCheck }}
[backends."backend-{{ $serviceName }}".healthCheck] [backends."backend-{{ $serviceName }}".healthCheck]
scheme = "{{ $healthCheck.Scheme }}" scheme = "{{ $healthCheck.Scheme }}"
@ -588,7 +588,7 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{end}} {{end}}
{{ $buffering := getBuffering $firstInstance.TraefikLabels }} {{ $buffering := getBuffering $firstInstance.SegmentLabels }}
{{if $buffering }} {{if $buffering }}
[backends."backend-{{ $serviceName }}".buffering] [backends."backend-{{ $serviceName }}".buffering]
maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }}
@ -610,38 +610,40 @@ var _templatesEcsTmpl = []byte(`[backends]
{{range $serviceName, $instances := .Services }} {{range $serviceName, $instances := .Services }}
{{range $instance := filterFrontends $instances }} {{range $instance := filterFrontends $instances }}
[frontends."frontend-{{ $serviceName }}"] {{ $frontendName := getFrontendName $instance }}
backend = "backend-{{ $serviceName }}"
priority = {{ getPriority $instance.TraefikLabels }}
passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }}
passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }}
entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} [frontends."frontend-{{ $frontendName }}"]
backend = "backend-{{ $serviceName }}"
priority = {{ getPriority $instance.SegmentLabels }}
passHostHeader = {{ getPassHostHeader $instance.SegmentLabels }}
passTLSCert = {{ getPassTLSCert $instance.SegmentLabels }}
entryPoints = [{{range getEntryPoints $instance.SegmentLabels }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $auth := getAuth $instance.TraefikLabels }} {{ $auth := getAuth $instance.SegmentLabels }}
{{if $auth }} {{if $auth }}
[frontends."frontend-{{ $serviceName }}".auth] [frontends."frontend-{{ $frontendName }}".auth]
headerField = "{{ $auth.HeaderField }}" headerField = "{{ $auth.HeaderField }}"
{{if $auth.Forward }} {{if $auth.Forward }}
[frontends."frontend-{{ $serviceName }}".auth.forward] [frontends."frontend-{{ $frontendName }}".auth.forward]
address = "{{ $auth.Forward.Address }}" address = "{{ $auth.Forward.Address }}"
trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }} trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }}
{{if $auth.Forward.TLS }} {{if $auth.Forward.TLS }}
[frontends."frontend-{{ $serviceName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
{{if $auth.Basic }} {{if $auth.Basic }}
[frontends."frontend-{{ $serviceName }}".auth.basic] [frontends."frontend-{{ $frontendName }}".auth.basic]
removeHeader = {{ $auth.Basic.RemoveHeader }} removeHeader = {{ $auth.Basic.RemoveHeader }}
{{if $auth.Basic.Users }} {{if $auth.Basic.Users }}
users = [{{range $auth.Basic.Users }} users = [{{range $auth.Basic.Users }}
@ -652,7 +654,7 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{if $auth.Digest }} {{if $auth.Digest }}
[frontends."frontend-{{ $serviceName }}".auth.digest] [frontends."frontend-{{ $frontendName }}".auth.digest]
removeHeader = {{ $auth.Digest.RemoveHeader }} removeHeader = {{ $auth.Digest.RemoveHeader }}
{{if $auth.Digest.Users }} {{if $auth.Digest.Users }}
users = [{{range $auth.Digest.Users }} users = [{{range $auth.Digest.Users }}
@ -663,14 +665,14 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{end}} {{end}}
{{ $whitelist := getWhiteList $instance.TraefikLabels }} {{ $whitelist := getWhiteList $instance.SegmentLabels }}
{{if $whitelist }} {{if $whitelist }}
[frontends."frontend-{{ $serviceName }}".whiteList] [frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }} sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if $whitelist.IPStrategy }} {{if $whitelist.IPStrategy }}
[frontends."frontend-{{ $serviceName }}".whiteList.IPStrategy] [frontends."frontend-{{ $frontendName }}".whiteList.IPStrategy]
depth = {{ $whitelist.IPStrategy.Depth }} depth = {{ $whitelist.IPStrategy.Depth }}
excludedIPs = [{{range $whitelist.IPStrategy.ExcludedIPs }} excludedIPs = [{{range $whitelist.IPStrategy.ExcludedIPs }}
"{{.}}", "{{.}}",
@ -678,20 +680,20 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{end}} {{end}}
{{ $redirect := getRedirect $instance.TraefikLabels }} {{ $redirect := getRedirect $instance.SegmentLabels }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $serviceName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]
entryPoint = "{{ $redirect.EntryPoint }}" entryPoint = "{{ $redirect.EntryPoint }}"
regex = "{{ $redirect.Regex }}" regex = "{{ $redirect.Regex }}"
replacement = "{{ $redirect.Replacement }}" replacement = "{{ $redirect.Replacement }}"
permanent = {{ $redirect.Permanent }} permanent = {{ $redirect.Permanent }}
{{end}} {{end}}
{{ $errorPages := getErrorPages $instance.TraefikLabels }} {{ $errorPages := getErrorPages $instance.SegmentLabels }}
{{if $errorPages }} {{if $errorPages }}
[frontends."frontend-{{ $serviceName }}".errors] [frontends."frontend-{{ $frontendName }}".errors]
{{range $pageName, $page := $errorPages }} {{range $pageName, $page := $errorPages }}
[frontends."frontend-{{ $serviceName }}".errors."{{ $pageName }}"] [frontends."frontend-{{ $frontendName }}".errors."{{ $pageName }}"]
status = [{{range $page.Status }} status = [{{range $page.Status }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
@ -700,22 +702,22 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{end}} {{end}}
{{ $rateLimit := getRateLimit $instance.TraefikLabels }} {{ $rateLimit := getRateLimit $instance.SegmentLabels }}
{{if $rateLimit }} {{if $rateLimit }}
[frontends."frontend-{{ $serviceName }}".rateLimit] [frontends."frontend-{{ $frontendName }}".rateLimit]
extractorFunc = "{{ $rateLimit.ExtractorFunc }}" extractorFunc = "{{ $rateLimit.ExtractorFunc }}"
[frontends."frontend-{{ $serviceName }}".rateLimit.rateSet] [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet]
{{ range $limitName, $limit := $rateLimit.RateSet }} {{ range $limitName, $limit := $rateLimit.RateSet }}
[frontends."frontend-{{ $serviceName }}".rateLimit.rateSet."{{ $limitName }}"] [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"]
period = "{{ $limit.Period }}" period = "{{ $limit.Period }}"
average = {{ $limit.Average }} average = {{ $limit.Average }}
burst = {{ $limit.Burst }} burst = {{ $limit.Burst }}
{{end}} {{end}}
{{end}} {{end}}
{{ $headers := getHeaders $instance.TraefikLabels }} {{ $headers := getHeaders $instance.SegmentLabels }}
{{if $headers }} {{if $headers }}
[frontends."frontend-{{ $serviceName }}".headers] [frontends."frontend-{{ $frontendName }}".headers]
SSLRedirect = {{ $headers.SSLRedirect }} SSLRedirect = {{ $headers.SSLRedirect }}
SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }}
SSLHost = "{{ $headers.SSLHost }}" SSLHost = "{{ $headers.SSLHost }}"
@ -747,28 +749,28 @@ var _templatesEcsTmpl = []byte(`[backends]
{{end}} {{end}}
{{if $headers.CustomRequestHeaders }} {{if $headers.CustomRequestHeaders }}
[frontends."frontend-{{ $serviceName }}".headers.customRequestHeaders] [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders]
{{range $k, $v := $headers.CustomRequestHeaders }} {{range $k, $v := $headers.CustomRequestHeaders }}
{{$k}} = "{{$v}}" {{$k}} = "{{$v}}"
{{end}} {{end}}
{{end}} {{end}}
{{if $headers.CustomResponseHeaders }} {{if $headers.CustomResponseHeaders }}
[frontends."frontend-{{ $serviceName }}".headers.customResponseHeaders] [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders]
{{range $k, $v := $headers.CustomResponseHeaders }} {{range $k, $v := $headers.CustomResponseHeaders }}
{{$k}} = "{{$v}}" {{$k}} = "{{$v}}"
{{end}} {{end}}
{{end}} {{end}}
{{if $headers.SSLProxyHeaders }} {{if $headers.SSLProxyHeaders }}
[frontends."frontend-{{ $serviceName }}".headers.SSLProxyHeaders] [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders]
{{range $k, $v := $headers.SSLProxyHeaders }} {{range $k, $v := $headers.SSLProxyHeaders }}
{{$k}} = "{{$v}}" {{$k}} = "{{$v}}"
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}
[frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"]
rule = "{{ getFrontendRule $instance }}" rule = "{{ getFrontendRule $instance }}"
{{end}} {{end}}
@ -912,8 +914,8 @@ var _templatesKubernetesTmpl = []byte(`[backends]
trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }}
{{if $frontend.Auth.Forward.TLS }} {{if $frontend.Auth.Forward.TLS }}
[frontends."{{ $frontendName }}".auth.forward.tls] [frontends."{{ $frontendName }}".auth.forward.tls]
cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" cert = """{{ $frontend.Auth.Forward.TLS.Cert }}"""
key = "{{ $frontend.Auth.Forward.TLS.Key }}" key = """{{ $frontend.Auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
@ -1137,8 +1139,8 @@ var _templatesKvTmpl = []byte(`[backends]
[frontends."{{ $frontendName }}".auth.forward.tls] [frontends."{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
@ -1399,8 +1401,8 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }}
[frontends."{{ $frontendName }}".auth.forward.tls] [frontends."{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
@ -1646,8 +1648,8 @@ var _templatesMesosTmpl = []byte(`[backends]
[frontends."frontend-{{ $frontendName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
@ -1915,8 +1917,8 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }}
[frontends."frontend-{{ $frontendName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -78,7 +78,7 @@ func (d *Datastore) watchChanges() error {
stopCh := make(chan struct{}) stopCh := make(chan struct{})
kvCh, err := d.kv.Watch(d.lockKey, stopCh, nil) kvCh, err := d.kv.Watch(d.lockKey, stopCh, nil)
if err != nil { if err != nil {
return err return fmt.Errorf("error while watching key %s: %v", d.lockKey, err)
} }
safe.Go(func() { safe.Go(func() {
ctx, cancel := context.WithCancel(d.ctx) ctx, cancel := context.WithCancel(d.ctx)

View file

@ -102,29 +102,23 @@ entryPoint = "https"
# #
# KeyType = "RSA4096" # KeyType = "RSA4096"
# Domains list. # Use a TLS-ALPN-01 ACME challenge.
# Only domains defined here can generate wildcard certificates.
#
# [[acme.domains]]
# main = "local1.com"
# sans = ["test1.local1.com", "test2.local1.com"]
# [[acme.domains]]
# main = "local2.com"
# [[acme.domains]]
# main = "*.local3.com"
# sans = ["local3.com", "test1.test1.local3.com"]
# Use a HTTP-01 ACME challenge.
# #
# Optional (but recommended) # Optional (but recommended)
# #
[acme.httpChallenge] [acme.tlsChallenge]
# Use a HTTP-01 ACME challenge.
#
# Optional
#
# [acme.httpChallenge]
# EntryPoint to use for the HTTP-01 challenges. # EntryPoint to use for the HTTP-01 challenges.
# #
# Required # Required
# #
entryPoint = "http" # entryPoint = "http"
# Use a DNS-01 ACME challenge rather than HTTP-01 challenge. # Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
# Note: mandatory for wildcard certificate generation. # Note: mandatory for wildcard certificate generation.
@ -147,6 +141,18 @@ entryPoint = "https"
# Default: 0 # Default: 0
# #
# delayBeforeCheck = 0 # delayBeforeCheck = 0
# Domains list.
# Only domains defined here can generate wildcard certificates.
#
# [[acme.domains]]
# main = "local1.com"
# sans = ["test1.local1.com", "test2.local1.com"]
# [[acme.domains]]
# main = "local2.com"
# [[acme.domains]]
# main = "*.local3.com"
# sans = ["local3.com", "test1.test1.local3.com"]
``` ```
### `caServer` ### `caServer`
@ -164,7 +170,7 @@ caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
### ACME Challenge ### ACME Challenge
#### TLS Challenge #### `tlsChallenge`
Use the `TLS-ALPN-01` challenge to generate and renew ACME certificates by provisioning a TLS certificate. Use the `TLS-ALPN-01` challenge to generate and renew ACME certificates by provisioning a TLS certificate.
@ -246,7 +252,7 @@ Useful if internal networks block external DNS queries.
Here is a list of supported `provider`s, that can automate the DNS verification, along with the required environment variables and their [wildcard & root domain support](/configuration/acme/#wildcard-domains) for each. Do not hesitate to complete it. Here is a list of supported `provider`s, that can automate the DNS verification, along with the required environment variables and their [wildcard & root domain support](/configuration/acme/#wildcard-domains) for each. Do not hesitate to complete it.
| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support | | Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support |
|--------------------------------------------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------| |--------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet | | [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet |
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet | | [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP` | Not tested yet |
| [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet | | [Blue Cat](https://www.bluecatnetworks.com/) | `bluecat` | `BLUECAT_SERVER_URL`, `BLUECAT_USER_NAME`, `BLUECAT_PASSWORD`, `BLUECAT_CONFIG_NAME`, `BLUECAT_DNS_VIEW` | Not tested yet |
@ -278,12 +284,11 @@ Here is a list of supported `provider`s, that can automate the DNS verification,
| [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet | | [PowerDNS](https://www.powerdns.com) | `pdns` | `PDNS_API_KEY`, `PDNS_API_URL` | Not tested yet |
| [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet | | [Rackspace](https://www.rackspace.com/cloud/dns) | `rackspace` | `RACKSPACE_USER`, `RACKSPACE_API_KEY` | Not tested yet |
| [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet | | [RFC2136](https://tools.ietf.org/html/rfc2136) | `rfc2136` | `RFC2136_TSIG_KEY`, `RFC2136_TSIG_SECRET`, `RFC2136_TSIG_ALGORITHM`, `RFC2136_NAMESERVER` | Not tested yet |
| [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_HOSTED_ZONE_ID` or a configured user/instance IAM profile. | YES | | [Route 53](https://aws.amazon.com/route53/) | `route53` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `[AWS_REGION]`, `[AWS_HOSTED_ZONE_ID]` or a configured user/instance IAM profile. | YES |
| [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet | | [Sakura Cloud](https://cloud.sakura.ad.jp/) | `sakuracloud` | `SAKURACLOUD_ACCESS_TOKEN`, `SAKURACLOUD_ACCESS_TOKEN_SECRET` | Not tested yet |
| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet | | [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet | | [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet |
### `domains` ### `domains`
You can provide SANs (alternative domains) to each main domain. You can provide SANs (alternative domains) to each main domain.

View file

@ -4,6 +4,9 @@
```toml ```toml
# API definition # API definition
# Warning: Enabling API will expose Træfik's configuration.
# It is not recommended in production,
# unless secured by authentication and authorizations
[api] [api]
# Name of the related entry point # Name of the related entry point
# #
@ -12,7 +15,7 @@
# #
entryPoint = "traefik" entryPoint = "traefik"
# Enabled Dashboard # Enable Dashboard
# #
# Optional # Optional
# Default: true # Default: true
@ -38,6 +41,22 @@ For more customization, see [entry points](/configuration/entrypoints/) document
![Web UI Health](/img/traefik-health.png) ![Web UI Health](/img/traefik-health.png)
## Security
Enabling the API will expose all configuration elements,
including sensitive data.
It is not recommended in production,
unless secured by authentication and authorizations.
A good sane default (but not exhaustive) set of recommendations
would be to apply the following protection mechanism:
* _At application level:_ enabling HTTP [Basic Authentication](#authentication)
* _At transport level:_ NOT exposing publicly the API's port,
keeping it restricted over internal networks
(restricted networks as in https://en.wikipedia.org/wiki/Principle_of_least_privilege).
## API ## API
| Path | Method | Description | | Path | Method | Description |

View file

@ -152,6 +152,17 @@ Additional settings can be defined using Consul Catalog tags.
| `<prefix>.frontend.whiteList.ipStrategy.depth=5` | See [whitelist](/configuration/entrypoints/#white-listing) | | `<prefix>.frontend.whiteList.ipStrategy.depth=5` | See [whitelist](/configuration/entrypoints/#white-listing) |
| `<prefix>.frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1` | See [whitelist](/configuration/entrypoints/#white-listing) | | `<prefix>.frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1` | See [whitelist](/configuration/entrypoints/#white-listing) |
### Multiple frontends for a single service
If you need to support multiple frontends for a service, for example when having multiple `rules` that can't be combined, specify them as follows:
```
<prefix>.frontends.A.rule=Host:A:PathPrefix:/A
<prefix>.frontends.B.rule=Host:B:PathPrefix:/
```
`A` and `B` here are just arbitrary names, they can be anything. You can use any setting that applies to `<prefix>.frontend` from the table above.
### Custom Headers ### Custom Headers
!!! note !!! note

View file

@ -19,7 +19,7 @@ Træfik can be configured to use Docker as a provider.
# #
endpoint = "unix:///var/run/docker.sock" endpoint = "unix:///var/run/docker.sock"
# Default domain used. # Default base domain used for the frontend rules.
# Can be overridden by setting the "traefik.domain" label on a container. # Can be overridden by setting the "traefik.domain" label on a container.
# #
# Required # Required
@ -110,7 +110,7 @@ To enable constraints see [provider-specific constraints section](/configuration
# #
endpoint = "tcp://127.0.0.1:2375" endpoint = "tcp://127.0.0.1:2375"
# Default domain used. # Default base domain used for the frontend rules.
# Can be overridden by setting the "traefik.domain" label on a services. # Can be overridden by setting the "traefik.domain" label on a services.
# #
# Optional # Optional
@ -210,7 +210,7 @@ Labels can be used on containers to override default behavior.
| Label | Description | | Label | Description |
|-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `traefik.docker.network` | Overrides the default docker network to use for connections to the container. [1] | | `traefik.docker.network` | Overrides the default docker network to use for connections to the container. [1] |
| `traefik.domain` | Sets the default domain for the frontend rules. | | `traefik.domain` | Sets the default base domain for the frontend rules. For more information, check the [Container Labels section's of the user guide "Let's Encrypt & Docker"](/user-guide/docker-and-lets-encrypt/#container-labels) |
| `traefik.enable=false` | Disables this container in Træfik. | | `traefik.enable=false` | Disables this container in Træfik. |
| `traefik.port=80` | Registers this port. Useful when the container exposes multiples ports. | | `traefik.port=80` | Registers this port. Useful when the container exposes multiples ports. |
| `traefik.protocol=https` | Overrides the default `http` protocol | | `traefik.protocol=https` | Overrides the default `http` protocol |
@ -237,10 +237,10 @@ Labels can be used on containers to override default behavior.
| `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2] (DEPRECATED). | | `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2] (DEPRECATED). |
| `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2]. | | `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` [2]. |
| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | | `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. |
| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `traefik.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. |
| `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. |
| `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). |
@ -330,10 +330,10 @@ Segment labels override the default behavior.
| `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` |
| `traefik.<segment_name>.frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | | `traefik.<segment_name>.frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` |
| `traefik.<segment_name>.frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | | `traefik.<segment_name>.frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` |
| `traefik.<segment_name>.frontend.auth.basic.usersfile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersfile` | | `traefik.<segment_name>.frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` |
| `traefik.<segment_name>.frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | | `traefik.<segment_name>.frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` |
| `traefik.<segment_name>.frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | | `traefik.<segment_name>.frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` |
| `traefik.<segment_name>.frontend.auth.digest.usersfile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersfile` | | `traefik.<segment_name>.frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` |
| `traefik.<segment_name>.frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` | | `traefik.<segment_name>.frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` |
| `traefik.<segment_name>.frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | | `traefik.<segment_name>.frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` |
| `traefik.<segment_name>.frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | | `traefik.<segment_name>.frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` |

View file

@ -223,3 +223,88 @@ Labels can be used on task containers to override default behavior:
| `traefik.frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. | | `traefik.frontend.headers.STSSeconds=315360000` | Sets the max-age of the STS header. |
| `traefik.frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. | | `traefik.frontend.headers.STSIncludeSubdomains=true` | Adds the `IncludeSubdomains` section of the STS header. |
| `traefik.frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. | | `traefik.frontend.headers.STSPreload=true` | Adds the preload flag to the STS header. |
### Containers with Multiple Ports (segment labels)
Segment labels are used to define routes to an application exposing multiple ports.
A segment is a group of labels that apply to a port exposed by an application.
You can define as many segments as ports exposed in an application.
Segment labels override the default behavior.
| Label | Description |
|------------------------------------------------------------------------------|----------------------------------------------------------------|
| `traefik.<segment_name>.backend=BACKEND` | Same as `traefik.backend` |
| `traefik.<segment_name>.domain=DOMAIN` | Same as `traefik.domain` |
| `traefik.<segment_name>.port=PORT` | Same as `traefik.port` |
| `traefik.<segment_name>.protocol=http` | Same as `traefik.protocol` |
| `traefik.<segment_name>.weight=10` | Same as `traefik.weight` |
| `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` |
| `traefik.<segment_name>.frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` |
| `traefik.<segment_name>.frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` |
| `traefik.<segment_name>.frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` |
| `traefik.<segment_name>.frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` |
| `traefik.<segment_name>.frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` |
| `traefik.<segment_name>.frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` |
| `traefik.<segment_name>.frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` |
| `traefik.<segment_name>.frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` |
| `traefik.<segment_name>.frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` |
| `traefik.<segment_name>.frontend.auth.forward.tls.cert=/path/server.pem` | Same as `traefik.frontend.auth.forward.tls.cert` |
| `traefik.<segment_name>.frontend.auth.forward.tls.insecureSkipVerify=true` | Same as `traefik.frontend.auth.forward.tls.insecureSkipVerify` |
| `traefik.<segment_name>.frontend.auth.forward.tls.key=/path/server.key` | Same as `traefik.frontend.auth.forward.tls.key` |
| `traefik.<segment_name>.frontend.auth.forward.trustForwardHeader=true` | Same as `traefik.frontend.auth.forward.trustForwardHeader` |
| `traefik.<segment_name>.frontend.auth.headerField=X-WebAuth-User` | Same as `traefik.frontend.auth.headerField` |
| `traefik.<segment_name>.frontend.auth.removeHeader=true` | Same as `traefik.frontend.auth.removeHeader` |
| `traefik.<segment_name>.frontend.entryPoints=https` | Same as `traefik.frontend.entryPoints` |
| `traefik.<segment_name>.frontend.errors.<name>.backend=NAME` | Same as `traefik.frontend.errors.<name>.backend` |
| `traefik.<segment_name>.frontend.errors.<name>.query=PATH` | Same as `traefik.frontend.errors.<name>.query` |
| `traefik.<segment_name>.frontend.errors.<name>.status=RANGE` | Same as `traefik.frontend.errors.<name>.status` |
| `traefik.<segment_name>.frontend.passHostHeader=true` | Same as `traefik.frontend.passHostHeader` |
| `traefik.<segment_name>.frontend.passTLSCert=true` | Same as `traefik.frontend.passTLSCert` |
| `traefik.<segment_name>.frontend.priority=10` | Same as `traefik.frontend.priority` |
| `traefik.<segment_name>.frontend.rateLimit.extractorFunc=EXP` | Same as `traefik.frontend.rateLimit.extractorFunc` |
| `traefik.<segment_name>.frontend.rateLimit.rateSet.<name>.period=6` | Same as `traefik.frontend.rateLimit.rateSet.<name>.period` |
| `traefik.<segment_name>.frontend.rateLimit.rateSet.<name>.average=6` | Same as `traefik.frontend.rateLimit.rateSet.<name>.average` |
| `traefik.<segment_name>.frontend.rateLimit.rateSet.<name>.burst=6` | Same as `traefik.frontend.rateLimit.rateSet.<name>.burst` |
| `traefik.<segment_name>.frontend.redirect.entryPoint=https` | Same as `traefik.frontend.redirect.entryPoint` |
| `traefik.<segment_name>.frontend.redirect.regex=^http://localhost/(.*)` | Same as `traefik.frontend.redirect.regex` |
| `traefik.<segment_name>.frontend.redirect.replacement=http://mydomain/$1` | Same as `traefik.frontend.redirect.replacement` |
| `traefik.<segment_name>.frontend.redirect.permanent=true` | Same as `traefik.frontend.redirect.permanent` |
| `traefik.<segment_name>.frontend.rule=EXP` | Same as `traefik.frontend.rule` |
| `traefik.<segment_name>.frontend.whiteList.sourceRange=RANGE` | Same as `traefik.frontend.whiteList.sourceRange` |
| `traefik.<segment_name>.frontend.whiteList.useXForwardedFor=true` | Same as `traefik.frontend.whiteList.useXForwardedFor` |
| `traefik.<segment_name>.frontend.whiteList.ipStrategy=true` | Same as `traefik.frontend.whiteList.ipStrategy` |
| `traefik.<segment_name>.frontend.whiteList.ipStrategy.depth=5` | Same as `traefik.frontend.whiteList.ipStrategy.depth` |
| `traefik.<segment_name>.frontend.whiteList.ipStrategy.excludedIPs=127.0.0.1` | Same as `traefik.frontend.whiteList.ipStrategy.excludedIPs` |
#### Custom Headers
| Label | Description |
|----------------------------------------------------------------------|----------------------------------------------------------|
| `traefik.<segment_name>.frontend.headers.customRequestHeaders=EXPR ` | Same as `traefik.frontend.headers.customRequestHeaders` |
| `traefik.<segment_name>.frontend.headers.customResponseHeaders=EXPR` | Same as `traefik.frontend.headers.customResponseHeaders` |
#### Security Headers
| Label | Description |
|-------------------------------------------------------------------------|--------------------------------------------------------------|
| `traefik.<segment_name>.frontend.headers.allowedHosts=EXPR` | Same as `traefik.frontend.headers.allowedHosts` |
| `traefik.<segment_name>.frontend.headers.browserXSSFilter=true` | Same as `traefik.frontend.headers.browserXSSFilter` |
| `traefik.<segment_name>.frontend.headers.contentSecurityPolicy=VALUE` | Same as `traefik.frontend.headers.contentSecurityPolicy` |
| `traefik.<segment_name>.frontend.headers.contentTypeNosniff=true` | Same as `traefik.frontend.headers.contentTypeNosniff` |
| `traefik.<segment_name>.frontend.headers.customBrowserXSSValue=VALUE` | Same as `traefik.frontend.headers.customBrowserXSSValue` |
| `traefik.<segment_name>.frontend.headers.customFrameOptionsValue=VALUE` | Same as `traefik.frontend.headers.customFrameOptionsValue` |
| `traefik.<segment_name>.frontend.headers.forceSTSHeader=false` | Same as `traefik.frontend.headers.forceSTSHeader` |
| `traefik.<segment_name>.frontend.headers.frameDeny=false` | Same as `traefik.frontend.headers.frameDeny` |
| `traefik.<segment_name>.frontend.headers.hostsProxyHeaders=EXPR` | Same as `traefik.frontend.headers.hostsProxyHeaders` |
| `traefik.<segment_name>.frontend.headers.isDevelopment=false` | Same as `traefik.frontend.headers.isDevelopment` |
| `traefik.<segment_name>.frontend.headers.publicKey=VALUE` | Same as `traefik.frontend.headers.publicKey` |
| `traefik.<segment_name>.frontend.headers.referrerPolicy=VALUE` | Same as `traefik.frontend.headers.referrerPolicy` |
| `traefik.<segment_name>.frontend.headers.SSLRedirect=true` | Same as `traefik.frontend.headers.SSLRedirect` |
| `traefik.<segment_name>.frontend.headers.SSLTemporaryRedirect=true` | Same as `traefik.frontend.headers.SSLTemporaryRedirect` |
| `traefik.<segment_name>.frontend.headers.SSLHost=HOST` | Same as `traefik.frontend.headers.SSLHost` |
| `traefik.<segment_name>.frontend.headers.SSLForceHost=true` | Same as `traefik.frontend.headers.SSLForceHost` |
| `traefik.<segment_name>.frontend.headers.SSLProxyHeaders=EXPR` | Same as `traefik.frontend.headers.SSLProxyHeaders=EXPR` |
| `traefik.<segment_name>.frontend.headers.STSSeconds=315360000` | Same as `traefik.frontend.headers.STSSeconds=315360000` |
| `traefik.<segment_name>.frontend.headers.STSIncludeSubdomains=true` | Same as `traefik.frontend.headers.STSIncludeSubdomains=true` |
| `traefik.<segment_name>.frontend.headers.STSPreload=true` | Same as `traefik.frontend.headers.STSPreload=true` |

View file

@ -127,7 +127,13 @@ This will give more flexibility in cloud/dynamic environments.
Traefik automatically requests endpoint information based on the service provided in the ingress spec. Traefik automatically requests endpoint information based on the service provided in the ingress spec.
Although traefik will connect directly to the endpoints (pods), it still checks the service port to see if TLS communication is required. Although traefik will connect directly to the endpoints (pods), it still checks the service port to see if TLS communication is required.
If the service port defined in the ingress spec is 443, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically.
There are 2 ways to configure Traefik to use https to communicate with backend pods:
1. If the service port defined in the ingress spec is 443 (note that you can still use `targetPort` to use a different port on your pod).
2. If the service port defined in the ingress spec has a name that starts with `https` (such as `https-api`, `https-web` or just `https`).
If either of those configuration options exist, then the backend communication protocol is assumed to be TLS, and will connect via TLS automatically.
!!! note !!! note
Please note that by enabling TLS communication between traefik and your pods, you will have to have trusted certificates that have the proper trust chain and IP subject name. Please note that by enabling TLS communication between traefik and your pods, you will have to have trusted certificates that have the proper trust chain and IP subject name.

View file

@ -166,10 +166,10 @@ Labels can be used on task containers to override default behavior:
| `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). | | `traefik.frontend.auth.basic=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). |
| `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` . | | `traefik.frontend.auth.basic.users=EXPR` | Sets the basic authentication to this frontend in CSV format: `User:Hash,User:Hash` . |
| `traefik.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets the basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. | | `traefik.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. | | `traefik.frontend.auth.digest.users=EXPR` | Sets the digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. |
| `traefik.frontend.auth.digest.usersfile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. | | `traefik.frontend.auth.digest.usersFile=/path/.htdigest` | Sets the digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `traefik.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. | | `traefik.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. |
| `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. | | `traefik.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. |
| `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). | | `traefik.frontend.auth.forward.tls.caOptional=true` | Checks the certificates if present but do not force to be signed by a specified Certificate Authority (CA). |
@ -249,10 +249,10 @@ Segment labels override the default behavior.
| `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` | | `traefik.<segment_name>.frontend.auth.basic=EXPR` | Same as `traefik.frontend.auth.basic` |
| `traefik.<segment_name>.frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` | | `traefik.<segment_name>.frontend.auth.basic.removeHeader=true` | Same as `traefik.frontend.auth.basic.removeHeader` |
| `traefik.<segment_name>.frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` | | `traefik.<segment_name>.frontend.auth.basic.users=EXPR` | Same as `traefik.frontend.auth.basic.users` |
| `traefik.<segment_name>.frontend.auth.basic.usersfile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersfile` | | `traefik.<segment_name>.frontend.auth.basic.usersFile=/path/.htpasswd` | Same as `traefik.frontend.auth.basic.usersFile` |
| `traefik.<segment_name>.frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` | | `traefik.<segment_name>.frontend.auth.digest.removeHeader=true` | Same as `traefik.frontend.auth.digest.removeHeader` |
| `traefik.<segment_name>.frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` | | `traefik.<segment_name>.frontend.auth.digest.users=EXPR` | Same as `traefik.frontend.auth.digest.users` |
| `traefik.<segment_name>.frontend.auth.digest.usersfile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersfile` | | `traefik.<segment_name>.frontend.auth.digest.usersFile=/path/.htdigest` | Same as `traefik.frontend.auth.digest.usersFile` |
| `traefik.<segment_name>.frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` | | `traefik.<segment_name>.frontend.auth.forward.address=https://example.com` | Same as `traefik.frontend.auth.forward.address` |
| `traefik.<segment_name>.frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` | | `traefik.<segment_name>.frontend.auth.forward.tls.ca=/path/ca.pem` | Same as `traefik.frontend.auth.forward.tls.ca` |
| `traefik.<segment_name>.frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` | | `traefik.<segment_name>.frontend.auth.forward.tls.caOptional=true` | Same as `traefik.frontend.auth.forward.tls.caOptional` |

View file

@ -7,7 +7,7 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/containous/traefik)](https://goreportcard.com/report/github.com/containous/traefik) [![Go Report Card](https://goreportcard.com/badge/github.com/containous/traefik)](https://goreportcard.com/report/github.com/containous/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md)
[![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io) [![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io)
[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy) [![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik)
Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. Træfik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.
@ -86,6 +86,10 @@ services:
- /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
``` ```
!!! warning
Enabling the Web UI with the `--api` flag might exposes configuration elements. You can read more about this on the [API/Dashboard's Security section](/configuration/api#security).
**That's it. Now you can launch Træfik!** **That's it. Now you can launch Træfik!**
Start your `reverse-proxy` with the following command: Start your `reverse-proxy` with the following command:
@ -199,3 +203,19 @@ Using the tiny Docker image:
```shell ```shell
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik
``` ```
## Security
### Security Advisories
We strongly advise you to join our mailing list to be aware of the latest announcements from our security team. You can subscribe sending a mail to security+subscribe@traefik.io or on [the online viewer](https://groups.google.com/a/traefik.io/forum/#!forum/security).
### CVE
Reported vulnerabilities can be found on
[cve.mitre.org](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=traefik).
### Report a Vulnerability
We want to keep Træfik safe for everyone.
If you've discovered a security vulnerability in Træfik, we appreciate your help in disclosing it to us in a responsible manner, using [this form](https://security.traefik.io).

View file

@ -1,4 +1,4 @@
# Docker & Traefik # Let's Encrypt & Docker
In this use case, we want to use Træfik as a _layer-7_ load balancer with SSL termination for a set of micro-services used to run a web application. In this use case, we want to use Træfik as a _layer-7_ load balancer with SSL termination for a set of micro-services used to run a web application.
@ -8,7 +8,7 @@ In addition, we want to use Let's Encrypt to automatically generate and renew SS
## Setting Up ## Setting Up
In order for this to work, you'll need a server with a public IP address, with Docker installed on it. In order for this to work, you'll need a server with a public IP address, with Docker and docker-compose installed on it.
In this example, we're using the fictitious domain _my-awesome-app.org_. In this example, we're using the fictitious domain _my-awesome-app.org_.

View file

@ -50,7 +50,7 @@ start_boulder() {
# Script usage # Script usage
show_usage() { show_usage() {
echo echo
echo "USAGE : manage_acme_docker_environment.sh [--start|--stop|--restart]" echo "USAGE : manage_acme_docker_environment.sh [--dev|--start|--stop|--restart]"
echo echo
} }

View file

@ -676,3 +676,49 @@ func (s *ConsulCatalogSuite) TestMaintenanceMode(c *check.C) {
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
} }
func (s *ConsulCatalogSuite) TestMultipleFrontendRule(c *check.C) {
cmd, display := s.traefikCmd(
withConfigFile("fixtures/consul_catalog/simple.toml"),
"--consulCatalog",
"--consulCatalog.endpoint="+s.consulIP+":8500",
"--consulCatalog.domain=consul.localhost")
defer display(c)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
// Wait for Traefik to turn ready.
err = try.GetRequest("http://127.0.0.1:8000/", 2*time.Second, try.StatusCodeIs(http.StatusNotFound))
c.Assert(err, checker.IsNil)
whoami := s.composeProject.Container(c, "whoami1")
err = s.registerService("test", whoami.NetworkSettings.IPAddress, 80,
[]string{
"traefik.frontends.service1.rule=Host:whoami1.consul.localhost",
"traefik.frontends.service2.rule=Host:whoami2.consul.localhost",
})
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "test.consul.localhost"
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "whoami1.consul.localhost"
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "whoami2.consul.localhost"
err = try.Request(req, 10*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody())
c.Assert(err, checker.IsNil)
}

View file

@ -22,6 +22,7 @@ defaultEntryPoints = ["http", "https"]
weight = 1 weight = 1
[frontends] [frontends]
[frontends.frontend1] [frontends.frontend1]
backend = "backend1" backend = "backend1"
[frontends.frontend1.routes.test_1] [frontends.frontend1.routes.test_1]
@ -29,8 +30,41 @@ defaultEntryPoints = ["http", "https"]
[frontends.frontend2] [frontends.frontend2]
backend = "backend1" backend = "backend1"
[frontends.frontend2.routes.test_1] [frontends.frontend2.routes.test_1]
rule = "Host: test.com; AddPrefix: /foo" rule = "Host: example2.com; PathPrefixStrip: /api/"
[frontends.frontend3] [frontends.frontend3]
backend = "backend1" backend = "backend1"
[frontends.frontend3.routes.test_1] [frontends.frontend3.routes.test_1]
rule = "Host: test.com; AddPrefix: /foo"
[frontends.frontend4]
backend = "backend1"
[frontends.frontend4.routes.test_1]
rule = "Host: test2.com; AddPrefix: /foo/"
[frontends.frontend5]
backend = "backend1"
[frontends.frontend5.routes.test_1]
rule = "Host: foo.com; PathPrefixStripRegex: /{id:[a-z]+}" rule = "Host: foo.com; PathPrefixStripRegex: /{id:[a-z]+}"
[frontends.frontend6]
backend = "backend1"
[frontends.frontend6.routes.test_1]
rule = "Host: foo2.com; PathPrefixStripRegex: /{id:[a-z]+}/"
[frontends.frontend7]
backend = "backend1"
[frontends.frontend7.routes.test_1]
rule = "Host: bar.com; ReplacePathRegex: /api /"
[frontends.frontend8]
backend = "backend1"
[frontends.frontend8.routes.test_1]
rule = "Host: bar2.com; ReplacePathRegex: /api/ /"
[frontends.frontend9]
backend = "backend1"
[frontends.frontend9.routes.test_1]
rule = "Host: pow.com; ReplacePath: /api"
[frontends.frontend10]
backend = "backend1"
[frontends.frontend10.routes.test_1]
rule = "Host: pow2.com; ReplacePath: /api/"

View file

@ -3,6 +3,7 @@ package integration
import ( import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"fmt"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -743,7 +744,7 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(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: example.com")) err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains("Host: example.com"))
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
client := &http.Client{ client := &http.Client{
@ -754,114 +755,81 @@ func (s *HTTPSSuite) TestEntrypointHttpsRedirectAndPathModification(c *check.C)
testCases := []struct { testCases := []struct {
desc string desc string
host string hosts []string
sourceURL string path string
expectedURL string
}{ }{
{ {
desc: "Stripped URL redirect", desc: "Stripped URL redirect",
host: "example.com", hosts: []string{"example.com", "foo.com", "bar.com"},
sourceURL: "http://127.0.0.1:8888/api", path: "/api",
expectedURL: "https://example.com:8443/api",
}, },
{ {
desc: "Stripped URL with trailing slash redirect", desc: "Stripped URL with trailing slash redirect",
host: "example.com", hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"},
sourceURL: "http://127.0.0.1:8888/api/", path: "/api/",
expectedURL: "https://example.com:8443/api/",
}, },
{ {
desc: "Stripped URL with double trailing slash redirect", desc: "Stripped URL with double trailing slash redirect",
host: "example.com", hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"},
sourceURL: "http://127.0.0.1:8888/api//", path: "/api//",
expectedURL: "https://example.com:8443/api//",
}, },
{ {
desc: "Stripped URL with path redirect", desc: "Stripped URL with path redirect",
host: "example.com", hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"},
sourceURL: "http://127.0.0.1:8888/api/bacon", path: "/api/bacon",
expectedURL: "https://example.com:8443/api/bacon",
}, },
{ {
desc: "Stripped URL with path and trailing slash redirect", desc: "Stripped URL with path and trailing slash redirect",
host: "example.com", hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"},
sourceURL: "http://127.0.0.1:8888/api/bacon/", path: "/api/bacon/",
expectedURL: "https://example.com:8443/api/bacon/",
}, },
{ {
desc: "Stripped URL with path and double trailing slash redirect", desc: "Stripped URL with path and double trailing slash redirect",
host: "example.com", hosts: []string{"example.com", "example2.com", "foo.com", "foo2.com", "bar.com", "bar2.com"},
sourceURL: "http://127.0.0.1:8888/api/bacon//", path: "/api/bacon//",
expectedURL: "https://example.com:8443/api/bacon//",
}, },
{ {
desc: "Root Path with redirect", desc: "Root Path with redirect",
host: "test.com", hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"},
sourceURL: "http://127.0.0.1:8888/", path: "/",
expectedURL: "https://test.com:8443/",
}, },
{ {
desc: "Root Path with double trailing slash redirect", desc: "Root Path with double trailing slash redirect",
host: "test.com", hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"},
sourceURL: "http://127.0.0.1:8888//", path: "//",
expectedURL: "https://test.com:8443//",
}, },
{ {
desc: "AddPrefix with redirect", desc: "Path modify with redirect",
host: "test.com", hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"},
sourceURL: "http://127.0.0.1:8888/wtf", path: "/wtf",
expectedURL: "https://test.com:8443/wtf",
}, },
{ {
desc: "AddPrefix with trailing slash redirect", desc: "Path modify with trailing slash redirect",
host: "test.com", hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"},
sourceURL: "http://127.0.0.1:8888/wtf/", path: "/wtf/",
expectedURL: "https://test.com:8443/wtf/",
}, },
{ {
desc: "AddPrefix with matching path segment redirect", desc: "Path modify with matching path segment redirect",
host: "test.com", hosts: []string{"test.com", "test2.com", "pow.com", "pow2.com"},
sourceURL: "http://127.0.0.1:8888/wtf/foo", path: "/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 { for _, test := range testCases {
test := test sourceURL := fmt.Sprintf("http://127.0.0.1:8888%s", test.path)
for _, host := range test.hosts {
req, err := http.NewRequest("GET", test.sourceURL, nil) req, err := http.NewRequest("GET", sourceURL, nil)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
req.Host = test.host req.Host = host
resp, err := client.Do(req) resp, err := client.Do(req)
c.Assert(err, checker.IsNil) c.Assert(err, checker.IsNil)
defer resp.Body.Close() defer resp.Body.Close()
location := resp.Header.Get("Location") location := resp.Header.Get("Location")
c.Assert(location, checker.Equals, test.expectedURL) expected := fmt.Sprintf("https://%s:8443%s", host, test.path)
c.Assert(location, checker.Equals, expected)
}
} }
} }

View file

@ -88,19 +88,7 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
if stripPrefix, stripPrefixOk := req.Context().Value(middlewares.StripPrefixKey).(string); stripPrefixOk { if stripPrefix, stripPrefixOk := req.Context().Value(middlewares.StripPrefixKey).(string); stripPrefixOk {
if len(stripPrefix) > 0 { if len(stripPrefix) > 0 {
tempPath := parsedURL.Path
parsedURL.Path = stripPrefix 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)
}
}
}
} }
} }
@ -110,6 +98,12 @@ func (h *handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http
} }
} }
if replacePath, replacePathOk := req.Context().Value(middlewares.ReplacePathKey).(string); replacePathOk {
if len(replacePath) > 0 {
parsedURL.Path = replacePath
}
}
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,11 +1,17 @@
package middlewares package middlewares
import ( import (
"context"
"net/http" "net/http"
) )
const (
// ReplacePathKey is the key within the request context used to
// store the replaced path
ReplacePathKey key = "ReplacePath"
// ReplacedPathHeader is the default header to set the old path to // ReplacedPathHeader is the default header to set the old path to
const ReplacedPathHeader = "X-Replaced-Path" ReplacedPathHeader = "X-Replaced-Path"
)
// ReplacePath is a middleware used to replace the path of a URL request // ReplacePath is a middleware used to replace the path of a URL request
type ReplacePath struct { type ReplacePath struct {
@ -14,6 +20,7 @@ type ReplacePath struct {
} }
func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), ReplacePathKey, r.URL.Path))
r.Header.Add(ReplacedPathHeader, r.URL.Path) r.Header.Add(ReplacedPathHeader, r.URL.Path)
r.URL.Path = s.Path r.URL.Path = s.Path
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()

View file

@ -1,6 +1,7 @@
package middlewares package middlewares
import ( import (
"context"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
@ -30,6 +31,7 @@ func NewReplacePathRegexHandler(regex string, replacement string, handler http.H
func (s *ReplacePathRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *ReplacePathRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.Regexp != nil && len(s.Replacement) > 0 && s.Regexp.MatchString(r.URL.Path) { if s.Regexp != nil && len(s.Replacement) > 0 && s.Regexp.MatchString(r.URL.Path) {
r = r.WithContext(context.WithValue(r.Context(), ReplacePathKey, r.URL.Path))
r.Header.Add(ReplacedPathHeader, r.URL.Path) r.Header.Add(ReplacedPathHeader, r.URL.Path)
r.URL.Path = s.Regexp.ReplaceAllString(r.URL.Path, s.Replacement) r.URL.Path = s.Regexp.ReplaceAllString(r.URL.Path, s.Replacement)
r.RequestURI = r.URL.RequestURI() r.RequestURI = r.URL.RequestURI()

View file

@ -10,9 +10,6 @@ const (
// StripPrefixKey is the key within the request context used to // StripPrefixKey is the key within the request context used to
// store the stripped prefix // store the stripped prefix
StripPrefixKey key = "StripPrefix" 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
ForwardedPrefixHeader = "X-Forwarded-Prefix" ForwardedPrefixHeader = "X-Forwarded-Prefix"
) )
@ -26,21 +23,20 @@ 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+"/" rawReqPath := r.URL.Path
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), trailingSlash) s.serveRequest(w, r, strings.TrimSpace(prefix), rawReqPath)
return return
} }
} }
http.NotFound(w, r) http.NotFound(w, r)
} }
func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, trailingSlash bool) { func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string, rawReqPath string) {
r = r.WithContext(context.WithValue(r.Context(), StripPrefixSlashKey, trailingSlash)) r = r.WithContext(context.WithValue(r.Context(), StripPrefixKey, rawReqPath))
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

@ -39,16 +39,14 @@ func (s *StripPrefixRegex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Error("Error in stripPrefix middleware", err) log.Error("Error in stripPrefix middleware", err)
return return
} }
rawReqPath := r.URL.Path
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, rawReqPath))
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 = ensureLeadingSlash(r.URL.RequestURI())
s.Handler.ServeHTTP(w, r) s.Handler.ServeHTTP(w, r)
return return
} }

View file

@ -16,14 +16,11 @@ theme:
include_sidebar: true include_sidebar: true
favicon: img/traefik.icon.png favicon: img/traefik.icon.png
logo: img/traefik.logo.png logo: img/traefik.logo.png
palette:
primary: 'blue'
accent: 'light blue'
feature:
tabs: false
palette: palette:
primary: 'cyan' primary: 'cyan'
accent: 'cyan' accent: 'cyan'
feature:
tabs: false
i18n: i18n:
prev: 'Previous' prev: 'Previous'
next: 'Next' next: 'Next'
@ -45,7 +42,7 @@ google_analytics:
# - type: 'slack' # - type: 'slack'
# link: 'https://slack.traefik.io' # link: 'https://slack.traefik.io'
# - type: 'twitter' # - type: 'twitter'
# link: 'https://twitter.com/traefikproxy' # link: 'https://twitter.com/traefik'
extra_css: extra_css:
- theme/styles/extra.css - theme/styles/extra.css

View file

@ -62,6 +62,8 @@ type Provider struct {
clientMutex sync.Mutex clientMutex sync.Mutex
configFromListenerChan chan types.Configuration configFromListenerChan chan types.Configuration
pool *safe.Pool pool *safe.Pool
resolvingDomains map[string]struct{}
resolvingDomainsMutex sync.RWMutex
} }
// Certificate is a struct which contains all data needed from an ACME certificate // Certificate is a struct which contains all data needed from an ACME certificate
@ -144,6 +146,9 @@ func (p *Provider) Init(_ types.Constraints) error {
return fmt.Errorf("unable to get ACME certificates : %v", err) return fmt.Errorf("unable to get ACME certificates : %v", err)
} }
// Init the currently resolved domain map
p.resolvingDomains = make(map[string]struct{})
return nil return nil
} }
@ -309,6 +314,12 @@ func (p *Provider) initAccount() (*Account, error) {
return nil, err return nil, err
} }
} }
// Set the KeyType if not already defined in the account
if len(p.account.KeyType) == 0 {
p.account.KeyType = GetKeyType(p.KeyType)
}
return p.account, nil return p.account, nil
} }
@ -367,6 +378,9 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
return nil, nil return nil, nil
} }
p.addResolvingDomains(uncheckedDomains)
defer p.removeResolvingDomains(uncheckedDomains)
log.Debugf("Loading ACME certificates %+v...", uncheckedDomains) log.Debugf("Loading ACME certificates %+v...", uncheckedDomains)
client, err := p.getClient() client, err := p.getClient()
@ -404,6 +418,24 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
return certificate, nil return certificate, nil
} }
func (p *Provider) removeResolvingDomains(resolvingDomains []string) {
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
delete(p.resolvingDomains, domain)
}
}
func (p *Provider) addResolvingDomains(resolvingDomains []string) {
p.resolvingDomainsMutex.Lock()
defer p.resolvingDomainsMutex.Unlock()
for _, domain := range resolvingDomains {
p.resolvingDomains[domain] = struct{}{}
}
}
func (p *Provider) useCertificateWithRetry(domains []string) bool { 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 // 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 { if p.DNSChallenge != nil && len(domains) > 1 {
@ -630,6 +662,9 @@ func (p *Provider) renewCertificates() {
// Get provided certificate which check a domains list (Main and SANs) // Get provided certificate which check a domains list (Main and SANs)
// from static and dynamic provided certificates // from static and dynamic provided certificates
func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string { func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurationDomains bool) []string {
p.resolvingDomainsMutex.RLock()
defer p.resolvingDomainsMutex.RUnlock()
log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck) log.Debugf("Looking for provided certificate(s) to validate %q...", domainsToCheck)
allDomains := p.certificateStore.GetAllDomains() allDomains := p.certificateStore.GetAllDomains()
@ -639,6 +674,11 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati
allDomains = append(allDomains, strings.Join(certificate.Domain.ToStrArray(), ",")) allDomains = append(allDomains, strings.Join(certificate.Domain.ToStrArray(), ","))
} }
// Get currently resolved domains
for domain := range p.resolvingDomains {
allDomains = append(allDomains, domain)
}
// Get Configuration Domains // Get Configuration Domains
if checkConfigurationDomains { if checkConfigurationDomains {
for i := 0; i < len(p.Domains); i++ { for i := 0; i < len(p.Domains); i++ {
@ -658,7 +698,7 @@ func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) [
} }
if len(uncheckedDomains) == 0 { if len(uncheckedDomains) == 0 {
log.Debugf("No ACME certificate to generate for domains %q.", domainsToCheck) log.Debugf("No ACME certificate generation required for domains %q.", domainsToCheck)
} else { } else {
log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ",")) log.Debugf("Domains %q need ACME certificates generation for domains %q.", domainsToCheck, strings.Join(uncheckedDomains, ","))
} }

View file

@ -8,6 +8,7 @@ import (
traefiktls "github.com/containous/traefik/tls" traefiktls "github.com/containous/traefik/tls"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/xenolf/lego/acme"
) )
func TestGetUncheckedCertificates(t *testing.T) { func TestGetUncheckedCertificates(t *testing.T) {
@ -27,6 +28,7 @@ func TestGetUncheckedCertificates(t *testing.T) {
desc string desc string
dynamicCerts *safe.Safe dynamicCerts *safe.Safe
staticCerts *safe.Safe staticCerts *safe.Safe
resolvingDomains map[string]struct{}
acmeCertificates []*Certificate acmeCertificates []*Certificate
domains []string domains []string
expectedDomains []string expectedDomains []string
@ -139,6 +141,40 @@ func TestGetUncheckedCertificates(t *testing.T) {
}, },
expectedDomains: []string{"traefik.wtf"}, expectedDomains: []string{"traefik.wtf"},
}, },
{
desc: "all domains already managed by ACME",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
resolvingDomains: map[string]struct{}{
"traefik.wtf": {},
"foo.traefik.wtf": {},
},
expectedDomains: []string{},
},
{
desc: "one domain already managed by ACME",
domains: []string{"traefik.wtf", "foo.traefik.wtf"},
resolvingDomains: map[string]struct{}{
"traefik.wtf": {},
},
expectedDomains: []string{"foo.traefik.wtf"},
},
{
desc: "wildcard domain already managed by ACME checks the domains",
domains: []string{"bar.traefik.wtf", "foo.traefik.wtf"},
resolvingDomains: map[string]struct{}{
"*.traefik.wtf": {},
},
expectedDomains: []string{},
},
{
desc: "wildcard domain already managed by ACME checks domains and another domain checks one other domain, one domain still unchecked",
domains: []string{"traefik.wtf", "bar.traefik.wtf", "foo.traefik.wtf", "acme.wtf"},
resolvingDomains: map[string]struct{}{
"*.traefik.wtf": {},
"traefik.wtf": {},
},
expectedDomains: []string{"acme.wtf"},
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -146,12 +182,17 @@ func TestGetUncheckedCertificates(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
if test.resolvingDomains == nil {
test.resolvingDomains = make(map[string]struct{})
}
acmeProvider := Provider{ acmeProvider := Provider{
certificateStore: &traefiktls.CertificateStore{ certificateStore: &traefiktls.CertificateStore{
DynamicCerts: test.dynamicCerts, DynamicCerts: test.dynamicCerts,
StaticCerts: test.staticCerts, StaticCerts: test.staticCerts,
}, },
certificates: test.acmeCertificates, certificates: test.acmeCertificates,
resolvingDomains: test.resolvingDomains,
} }
domains := acmeProvider.getUncheckedDomains(test.domains, false) domains := acmeProvider.getUncheckedDomains(test.domains, false)
@ -562,3 +603,82 @@ func TestUseBackOffToObtainCertificate(t *testing.T) {
}) })
} }
} }
func TestInitAccount(t *testing.T) {
testCases := []struct {
desc string
account *Account
email string
keyType string
expectedAccount *Account
}{
{
desc: "Existing account with all information",
account: &Account{
Email: "foo@foo.net",
KeyType: acme.EC256,
},
expectedAccount: &Account{
Email: "foo@foo.net",
KeyType: acme.EC256,
},
},
{
desc: "Account nil",
email: "foo@foo.net",
keyType: "EC256",
expectedAccount: &Account{
Email: "foo@foo.net",
KeyType: acme.EC256,
},
},
{
desc: "Existing account with no email",
account: &Account{
KeyType: acme.RSA4096,
},
email: "foo@foo.net",
keyType: "EC256",
expectedAccount: &Account{
Email: "foo@foo.net",
KeyType: acme.EC256,
},
},
{
desc: "Existing account with no key type",
account: &Account{
Email: "foo@foo.net",
},
email: "bar@foo.net",
keyType: "EC256",
expectedAccount: &Account{
Email: "foo@foo.net",
KeyType: acme.EC256,
},
},
{
desc: "Existing account and provider with no key type",
account: &Account{
Email: "foo@foo.net",
},
email: "bar@foo.net",
expectedAccount: &Account{
Email: "foo@foo.net",
KeyType: acme.RSA4096,
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
acmeProvider := Provider{account: test.account, Configuration: &Configuration{Email: test.email, KeyType: test.keyType}}
actualAccount, err := acmeProvider.initAccount()
assert.Nil(t, err, "Init account in error")
assert.Equal(t, test.expectedAccount.Email, actualAccount.Email, "unexpected email account")
assert.Equal(t, test.expectedAccount.KeyType, actualAccount.KeyType, "unexpected keyType account")
})
}
}

View file

@ -55,7 +55,7 @@ func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configurat
var services []*serviceUpdate var services []*serviceUpdate
for _, info := range catalog { for _, info := range catalog {
if len(info.Nodes) > 0 { if len(info.Nodes) > 0 {
services = append(services, info.Service) services = append(services, p.generateFrontends(info.Service)...)
allNodes = append(allNodes, info.Nodes...) allNodes = append(allNodes, info.Nodes...)
} }
} }
@ -135,6 +135,9 @@ func (p *Provider) setupFrontEndRuleTemplate() {
// Specific functions // Specific functions
func getServiceBackendName(service *serviceUpdate) string { func getServiceBackendName(service *serviceUpdate) string {
if service.ParentServiceName != "" {
return strings.ToLower(service.ParentServiceName)
}
return strings.ToLower(service.ServiceName) return strings.ToLower(service.ServiceName)
} }

View file

@ -120,6 +120,80 @@ func TestProviderBuildConfiguration(t *testing.T) {
}, },
}, },
}, },
{
desc: "Should build config which contains three frontends and one backend",
nodes: []catalogUpdate{
{
Service: &serviceUpdate{
ServiceName: "test",
Attributes: []string{
"random.foo=bar",
label.Prefix + "frontend.rule=Host:A",
label.Prefix + "frontends.test1.rule=Host:B",
label.Prefix + "frontends.test2.rule=Host:C",
},
},
Nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "test",
Address: "127.0.0.1",
Port: 80,
Tags: []string{
"random.foo=bar",
},
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-test": {
Backend: "backend-test",
PassHostHeader: true,
Routes: map[string]types.Route{
"route-host-test": {
Rule: "Host:A",
},
},
EntryPoints: []string{},
},
"frontend-test-test1": {
Backend: "backend-test",
PassHostHeader: true,
Routes: map[string]types.Route{
"route-host-test-test1": {
Rule: "Host:B",
},
},
EntryPoints: []string{},
},
"frontend-test-test2": {
Backend: "backend-test",
PassHostHeader: true,
Routes: map[string]types.Route{
"route-host-test-test2": {
Rule: "Host:C",
},
},
EntryPoints: []string{},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"test-0-O0Tnh-SwzY69M6SurTKP3wNKkzI": {
URL: "http://127.0.0.1:80",
Weight: 1,
},
},
},
},
},
{ {
desc: "Should build config with a basic auth with a backward compatibility", desc: "Should build config with a basic auth with a backward compatibility",
nodes: []catalogUpdate{ nodes: []catalogUpdate{

View file

@ -51,10 +51,16 @@ type Service struct {
type serviceUpdate struct { type serviceUpdate struct {
ServiceName string ServiceName string
ParentServiceName string
Attributes []string Attributes []string
TraefikLabels map[string]string TraefikLabels map[string]string
} }
type frontendSegment struct {
Name string
Labels map[string]string
}
type catalogUpdate struct { type catalogUpdate struct {
Service *serviceUpdate Service *serviceUpdate
Nodes []*api.ServiceEntry Nodes []*api.ServiceEntry
@ -560,3 +566,52 @@ func (p *Provider) getConstraintTags(tags []string) []string {
return values return values
} }
func (p *Provider) generateFrontends(service *serviceUpdate) []*serviceUpdate {
frontends := make([]*serviceUpdate, 0)
// to support <prefix>.frontend.xxx
frontends = append(frontends, &serviceUpdate{
ServiceName: service.ServiceName,
ParentServiceName: service.ServiceName,
Attributes: service.Attributes,
TraefikLabels: service.TraefikLabels,
})
// loop over children of <prefix>.frontends.*
for _, frontend := range getSegments(p.Prefix+".frontends", p.Prefix, service.TraefikLabels) {
frontends = append(frontends, &serviceUpdate{
ServiceName: service.ServiceName + "-" + frontend.Name,
ParentServiceName: service.ServiceName,
Attributes: service.Attributes,
TraefikLabels: frontend.Labels,
})
}
return frontends
}
func getSegments(path string, prefix string, tree map[string]string) []*frontendSegment {
segments := make([]*frontendSegment, 0)
// find segment names
segmentNames := make(map[string]bool)
for key := range tree {
if strings.HasPrefix(key, path+".") {
segmentNames[strings.SplitN(strings.TrimPrefix(key, path+"."), ".", 2)[0]] = true
}
}
// get labels for each segment found
for segment := range segmentNames {
labels := make(map[string]string)
for key, value := range tree {
if strings.HasPrefix(key, path+"."+segment) {
labels[prefix+".frontend"+strings.TrimPrefix(key, path+"."+segment)] = value
}
}
segments = append(segments, &frontendSegment{
Name: segment,
Labels: labels,
})
}
return segments
}

View file

@ -0,0 +1,80 @@
package ecs
import (
"github.com/aws/aws-sdk-go/service/ecs"
)
func instance(ops ...func(*ecsInstance)) ecsInstance {
e := &ecsInstance{
containerDefinition: &ecs.ContainerDefinition{},
}
for _, op := range ops {
op(e)
}
return *e
}
func name(name string) func(*ecsInstance) {
return func(e *ecsInstance) {
e.Name = name
}
}
func ID(ID string) func(*ecsInstance) {
return func(e *ecsInstance) {
e.ID = ID
}
}
func iMachine(opts ...func(*machine)) func(*ecsInstance) {
return func(e *ecsInstance) {
e.machine = &machine{}
for _, opt := range opts {
opt(e.machine)
}
}
}
func mState(state string) func(*machine) {
return func(m *machine) {
m.state = state
}
}
func mPrivateIP(ip string) func(*machine) {
return func(m *machine) {
m.privateIP = ip
}
}
func mPorts(opts ...func(*portMapping)) func(*machine) {
return func(m *machine) {
for _, opt := range opts {
p := &portMapping{}
opt(p)
m.ports = append(m.ports, *p)
}
}
}
func mPort(containerPort int32, hostPort int32) func(*portMapping) {
return func(pm *portMapping) {
pm.containerPort = int64(containerPort)
pm.hostPort = int64(hostPort)
}
}
func labels(labels map[string]string) func(*ecsInstance) {
return func(c *ecsInstance) {
c.TraefikLabels = labels
}
}
func dockerLabels(labels map[string]*string) func(*ecsInstance) {
return func(c *ecsInstance) {
c.containerDefinition.DockerLabels = labels
}
}

View file

@ -1,6 +1,8 @@
package ecs package ecs
import ( import (
"crypto/md5"
"encoding/hex"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@ -17,18 +19,6 @@ import (
// buildConfiguration fills the config template with the given instances // buildConfiguration fills the config template with the given instances
func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configuration, error) { func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configuration, error) {
services := make(map[string][]ecsInstance)
for _, instance := range instances {
backendName := getBackendName(instance)
if p.filterInstance(instance) {
if serviceInstances, ok := services[backendName]; ok {
services[backendName] = append(serviceInstances, instance)
} else {
services[backendName] = []ecsInstance{instance}
}
}
}
var ecsFuncMap = template.FuncMap{ var ecsFuncMap = template.FuncMap{
// Backend functions // Backend functions
"getHost": getHost, "getHost": getHost,
@ -43,6 +33,7 @@ func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configura
// Frontend functions // Frontend functions
"filterFrontends": filterFrontends, "filterFrontends": filterFrontends,
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"getFrontendName": p.getFrontendName,
"getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), "getPassHostHeader": label.GetFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader),
"getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPassTLSCert": label.GetFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
"getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority), "getPriority": label.GetFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriority),
@ -56,6 +47,25 @@ func (p *Provider) buildConfiguration(instances []ecsInstance) (*types.Configura
"getWhiteList": label.GetWhiteList, "getWhiteList": label.GetWhiteList,
} }
services := make(map[string][]ecsInstance)
for _, instance := range instances {
segmentProperties := label.ExtractTraefikLabels(instance.TraefikLabels)
for segmentName, labels := range segmentProperties {
instance.SegmentLabels = labels
instance.SegmentName = segmentName
backendName := getBackendName(instance)
if p.filterInstance(instance) {
if serviceInstances, ok := services[backendName]; ok {
services[backendName] = append(serviceInstances, instance)
} else {
services[backendName] = []ecsInstance{instance}
}
}
}
}
return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct {
Services map[string][]ecsInstance Services map[string][]ecsInstance
}{ }{
@ -101,25 +111,61 @@ func (p *Provider) filterInstance(i ecsInstance) bool {
} }
func getBackendName(i ecsInstance) string { func getBackendName(i ecsInstance) string {
if value := label.GetStringValue(i.TraefikLabels, label.TraefikBackend, ""); len(value) > 0 { if len(i.SegmentName) > 0 {
return value return getSegmentBackendName(i)
} }
return i.Name
return getDefaultBackendName(i)
}
func getSegmentBackendName(i ecsInstance) string {
if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) > 0 {
return provider.Normalize(i.Name + "-" + value)
}
return provider.Normalize(i.Name + "-" + i.SegmentName)
}
func getDefaultBackendName(i ecsInstance) string {
if value := label.GetStringValue(i.SegmentLabels, label.TraefikBackend, ""); len(value) != 0 {
return provider.Normalize(value)
}
return provider.Normalize(i.Name)
} }
func (p *Provider) getFrontendRule(i ecsInstance) string { func (p *Provider) getFrontendRule(i ecsInstance) string {
domain := label.GetStringValue(i.TraefikLabels, label.TraefikDomain, p.Domain) if value := label.GetStringValue(i.SegmentLabels, label.TraefikFrontendRule, ""); len(value) != 0 {
return value
}
domain := label.GetStringValue(i.SegmentLabels, label.TraefikDomain, p.Domain)
defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + domain defaultRule := "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + domain
return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule) return label.GetStringValue(i.TraefikLabels, label.TraefikFrontendRule, defaultRule)
} }
func (p *Provider) getFrontendName(instance ecsInstance) string {
name := getBackendName(instance)
if len(instance.SegmentName) > 0 {
name = instance.SegmentName + "-" + name
}
return provider.Normalize(name)
}
func getHost(i ecsInstance) string { func getHost(i ecsInstance) string {
return i.machine.privateIP return i.machine.privateIP
} }
func getPort(i ecsInstance) string { func getPort(i ecsInstance) string {
if value := label.GetStringValue(i.TraefikLabels, label.TraefikPort, ""); len(value) > 0 { value := label.GetStringValue(i.SegmentLabels, label.TraefikPort, "")
if len(value) == 0 {
value = label.GetStringValue(i.TraefikLabels, label.TraefikPort, "")
}
if len(value) > 0 {
port, err := strconv.ParseInt(value, 10, 64) port, err := strconv.ParseInt(value, 10, 64)
if err == nil { if err == nil {
for _, mapping := range i.machine.ports { for _, mapping := range i.machine.ports {
@ -138,6 +184,10 @@ func filterFrontends(instances []ecsInstance) []ecsInstance {
return fun.Filter(func(i ecsInstance) bool { return fun.Filter(func(i ecsInstance) bool {
backendName := getBackendName(i) backendName := getBackendName(i)
if len(i.SegmentName) > 0 {
backendName = backendName + "-" + i.SegmentName
}
_, found := byName[backendName] _, found := byName[backendName]
if !found { if !found {
byName[backendName] = struct{}{} byName[backendName] = struct{}{}
@ -154,14 +204,21 @@ func getServers(instances []ecsInstance) map[string]types.Server {
servers = make(map[string]types.Server) servers = make(map[string]types.Server)
} }
protocol := label.GetStringValue(instance.TraefikLabels, label.TraefikProtocol, label.DefaultProtocol) protocol := label.GetStringValue(instance.SegmentLabels, label.TraefikProtocol, label.DefaultProtocol)
host := getHost(instance) host := getHost(instance)
port := getPort(instance) port := getPort(instance)
serverName := provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID)) serverURL := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port))
serverName := getServerName(instance, serverURL)
if _, exist := servers[serverName]; exist {
log.Debugf("Skipping server %q with the same URL.", serverName)
continue
}
servers[serverName] = types.Server{ servers[serverName] = types.Server{
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(host, port)), URL: serverURL,
Weight: label.GetIntValue(instance.TraefikLabels, label.TraefikWeight, label.DefaultWeight), Weight: label.GetIntValue(instance.SegmentLabels, label.TraefikWeight, label.DefaultWeight),
} }
} }
@ -171,3 +228,18 @@ func getServers(instances []ecsInstance) map[string]types.Server {
func isEnabled(i ecsInstance, exposedByDefault bool) bool { func isEnabled(i ecsInstance, exposedByDefault bool) bool {
return label.GetBoolValue(i.TraefikLabels, label.TraefikEnable, exposedByDefault) return label.GetBoolValue(i.TraefikLabels, label.TraefikEnable, exposedByDefault)
} }
func getServerName(instance ecsInstance, url string) string {
hash := md5.New()
_, err := hash.Write([]byte(url))
if err != nil {
// Impossible case
log.Errorf("Fail to hash server URL %q", url)
}
if len(instance.SegmentName) > 0 {
return provider.Normalize(fmt.Sprintf("server-%s-%s-%s", instance.Name, instance.ID, hex.EncodeToString(hash.Sum(nil))))
}
return provider.Normalize(fmt.Sprintf("server-%s-%s", instance.Name, instance.ID))
}

View file

@ -0,0 +1,852 @@
package ecs
import (
"testing"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/provider/label"
"github.com/containous/traefik/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSegmentBuildConfiguration(t *testing.T) {
testCases := []struct {
desc string
instanceInfo []ecsInstance
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
desc: "when no container",
instanceInfo: []ecsInstance{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
desc: "simple configuration",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth basic",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Basic: &types.Basic{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth basic backward compatibility",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth digest",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Digest: &types.Digest{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "auth forward",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.frontend.entryPoints": "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
PassHostHeader: true,
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Forward: &types.Forward{
Address: "auth.server",
TrustForwardHeader: true,
TLS: &types.ClientTLS{
CA: "ca.crt",
CAOptional: true,
Cert: "server.crt",
Key: "server.key",
InsecureSkipVerify: true,
},
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-863563a2e23c95502862016417ee95ea": {
URL: "http://127.0.0.1:2503",
Weight: label.DefaultWeight,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "when all labels are set",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("foo"),
labels(map[string]string{
label.Prefix + "sauternes." + label.SuffixPort: "666",
label.Prefix + "sauternes." + label.SuffixProtocol: "https",
label.Prefix + "sauternes." + label.SuffixWeight: "12",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicRemoveHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasicUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestRemoveHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsers: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendAuthDigestUsersFile: ".htpasswd",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardAddress: "auth.server",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTrustForwardHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCa: "ca.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCaOptional: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSCert: "server.crt",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSKey: "server.key",
label.Prefix + "sauternes." + label.SuffixFrontendAuthForwardTLSInsecureSkipVerify: "true",
label.Prefix + "sauternes." + label.SuffixFrontendAuthHeaderField: "X-WebAuth-User",
label.Prefix + "sauternes." + label.SuffixFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
label.Prefix + "sauternes." + label.SuffixFrontendEntryPoints: "http,https",
label.Prefix + "sauternes." + label.SuffixFrontendPassHostHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPassTLSCert: "true",
label.Prefix + "sauternes." + label.SuffixFrontendPriority: "666",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectEntryPoint: "https",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListSourceRange: "10.10.10.10",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyExcludedIPS: "10.10.10.10,10.10.10.11",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListIPStrategyDepth: "5",
label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersAllowedHosts: "foo,bar,bor",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersHostsProxyHeaders: "foo,bar,bor",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLHost: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomFrameOptionsValue: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentSecurityPolicy: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersPublicKey: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersReferrerPolicy: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersCustomBrowserXSSValue: "foo",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSSeconds: "666",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLForceHost: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLRedirect: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSSLTemporaryRedirect: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSIncludeSubdomains: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersSTSPreload: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersForceSTSHeader: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersFrameDeny: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersContentTypeNosniff: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersBrowserXSSFilter: "true",
label.Prefix + "sauternes." + label.SuffixFrontendHeadersIsDevelopment: "true",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar",
label.Prefix + "sauternes." + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query",
label.Prefix + "sauternes." + label.SuffixFrontendRateLimitExtractorFunc: "client.ip",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6",
label.Prefix + "sauternes." + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 666),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-foo-sauternes": {
Backend: "backend-foo-sauternes",
EntryPoints: []string{
"http",
"https",
},
PassHostHeader: true,
PassTLSCert: true,
Priority: 666,
Auth: &types.Auth{
HeaderField: "X-WebAuth-User",
Basic: &types.Basic{
RemoveHeader: true,
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
UsersFile: ".htpasswd",
},
},
WhiteList: &types.WhiteList{
SourceRange: []string{"10.10.10.10"},
IPStrategy: &types.IPStrategy{
Depth: 5,
ExcludedIPs: []string{"10.10.10.10", "10.10.10.11"},
},
},
Headers: &types.Headers{
CustomRequestHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
CustomResponseHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
AllowedHosts: []string{
"foo",
"bar",
"bor",
},
HostsProxyHeaders: []string{
"foo",
"bar",
"bor",
},
SSLRedirect: true,
SSLTemporaryRedirect: true,
SSLForceHost: true,
SSLHost: "foo",
SSLProxyHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
},
STSSeconds: 666,
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,
FrameDeny: true,
CustomFrameOptionsValue: "foo",
ContentTypeNosniff: true,
BrowserXSSFilter: true,
CustomBrowserXSSValue: "foo",
ContentSecurityPolicy: "foo",
PublicKey: "foo",
ReferrerPolicy: "foo",
IsDevelopment: true,
},
Errors: map[string]*types.ErrorPage{
"foo": {
Status: []string{"404"},
Query: "foo_query",
Backend: "backend-foobar",
},
"bar": {
Status: []string{"500", "600"},
Query: "bar_query",
Backend: "backend-foobar",
},
},
RateLimit: &types.RateLimit{
ExtractorFunc: "client.ip",
RateSet: map[string]*types.Rate{
"foo": {
Period: parse.Duration(6 * time.Second),
Average: 12,
Burst: 18,
},
"bar": {
Period: parse.Duration(3 * time.Second),
Average: 6,
Burst: 9,
},
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
Regex: "",
Replacement: "",
Permanent: true,
},
Routes: map[string]types.Route{
"route-frontend-sauternes-foo-sauternes": {
Rule: "Host:foo.ecs.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-foo-sauternes": {
Servers: map[string]types.Server{
"server-foo-123456789abc-7f6444e0dff3330c8b0ad2bbbd383b0f": {
URL: "https://127.0.0.1:666",
Weight: 12,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several containers",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.sauternes.port": "2503",
"traefik.sauternes.protocol": "https",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.passHostHeader": "false",
"traefik.sauternes.frontend.rule": "Path:/mypath",
"traefik.sauternes.frontend.priority": "5000",
"traefik.sauternes.frontend.entryPoints": "http,https,ws",
"traefik.sauternes.frontend.auth.basic": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
"traefik.sauternes.frontend.redirect.entryPoint": "https",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
instance(
ID("abc987654321"),
name("test2"),
labels(map[string]string{
"traefik.anothersauternes.port": "8079",
"traefik.anothersauternes.weight": "33",
"traefik.anothersauternes.frontend.rule": "Path:/anotherpath",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.2"),
mPorts(
mPort(80, 8079),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: false,
Priority: 5000,
EntryPoints: []string{"http", "https", "ws"},
Auth: &types.Auth{
Basic: &types.Basic{
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
},
},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/mypath",
},
},
},
"frontend-anothersauternes-test2-anothersauternes": {
Backend: "backend-test2-anothersauternes",
PassHostHeader: true,
EntryPoints: []string{},
Routes: map[string]types.Route{
"route-frontend-anothersauternes-test2-anothersauternes": {
Rule: "Path:/anotherpath",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
"backend-test2-anothersauternes": {
Servers: map[string]types.Server{
"server-test2-abc987654321-045e3e4aa5a744a325c099b803700a93": {
URL: "http://127.0.0.2:8079",
Weight: 33,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and same port",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.port": "2503",
"traefik.protocol": "https",
"traefik.weight": "80",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and different port (wrong behavior)",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.protocol": "https",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.port": "2503",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.port": "2504",
"traefik.arbois.weight": "90",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
mPort(80, 2504),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
"server-test1-123456789abc-315a41140f1bd825b066e39686c18482": {
URL: "https://127.0.0.1:2504",
Weight: 90,
},
},
CircuitBreaker: nil,
},
},
},
{
desc: "several segments with the same backend name and different port binding",
instanceInfo: []ecsInstance{
instance(
ID("123456789abc"),
name("test1"),
labels(map[string]string{
"traefik.protocol": "https",
"traefik.frontend.entryPoints": "http,https",
"traefik.frontend.redirect.entryPoint": "https",
"traefik.sauternes.port": "2503",
"traefik.sauternes.weight": "80",
"traefik.sauternes.backend": "foobar",
"traefik.sauternes.frontend.rule": "Path:/sauternes",
"traefik.sauternes.frontend.priority": "5000",
"traefik.arbois.port": "8080",
"traefik.arbois.weight": "90",
"traefik.arbois.backend": "foobar",
"traefik.arbois.frontend.rule": "Path:/arbois",
"traefik.arbois.frontend.priority": "3000",
}),
iMachine(
mState(ec2.InstanceStateNameRunning),
mPrivateIP("127.0.0.1"),
mPorts(
mPort(80, 2503),
mPort(8080, 2504),
),
),
),
},
expectedFrontends: map[string]*types.Frontend{
"frontend-sauternes-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 5000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-sauternes-test1-foobar": {
Rule: "Path:/sauternes",
},
},
},
"frontend-arbois-test1-foobar": {
Backend: "backend-test1-foobar",
PassHostHeader: true,
Priority: 3000,
EntryPoints: []string{"http", "https"},
Redirect: &types.Redirect{
EntryPoint: "https",
},
Routes: map[string]types.Route{
"route-frontend-arbois-test1-foobar": {
Rule: "Path:/arbois",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test1-foobar": {
Servers: map[string]types.Server{
"server-test1-123456789abc-79533a101142718f0fdf84c42593c41e": {
URL: "https://127.0.0.1:2503",
Weight: 80,
},
"server-test1-123456789abc-315a41140f1bd825b066e39686c18482": {
URL: "https://127.0.0.1:2504",
Weight: 90,
},
},
CircuitBreaker: nil,
},
},
},
}
provider := &Provider{
Domain: "ecs.localhost",
ExposedByDefault: true,
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actualConfig, err := provider.buildConfiguration(test.instanceInfo)
assert.NoError(t, err)
require.NotNil(t, actualConfig, "actualConfig")
assert.EqualValues(t, test.expectedBackends, actualConfig.Backends)
assert.EqualValues(t, test.expectedFrontends, actualConfig.Frontends)
})
}
}

View file

@ -23,18 +23,18 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "config parsed successfully", desc: "config parsed successfully",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "instance", name("instance"),
ID: "1", ID("1"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{}),
DockerLabels: map[string]*string{}, iMachine(
}, mState(ec2.InstanceStateNameRunning),
machine: &machine{ mPrivateIP("10.0.0.1"),
state: ec2.InstanceStateNameRunning, mPorts(
privateIP: "10.0.0.1", mPort(0, 1337),
ports: []portMapping{{hostPort: 1337}}, ),
}, ),
}, ),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -63,20 +63,21 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "config parsed successfully with health check labels", desc: "config parsed successfully with health check labels",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "instance", name("instance"),
ID: "1", ID("1"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikBackendHealthCheckPath: aws.String("/health"), label.TraefikBackendHealthCheckPath: aws.String("/health"),
label.TraefikBackendHealthCheckInterval: aws.String("1s"), label.TraefikBackendHealthCheckInterval: aws.String("1s"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -109,22 +110,23 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "config parsed successfully with basic auth labels", desc: "config parsed successfully with basic auth labels",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "instance", name("instance"),
ID: "1", ID("1"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), label.TraefikFrontendAuthBasicUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"),
label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"), label.TraefikFrontendAuthBasicUsersFile: aws.String(".htpasswd"),
label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"), label.TraefikFrontendAuthBasicRemoveHeader: aws.String("true"),
label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -162,19 +164,20 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "config parsed successfully with basic auth (backward compatibility) labels", desc: "config parsed successfully with basic auth (backward compatibility) labels",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "instance", name("instance"),
ID: "1", ID("1"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), label.TraefikFrontendAuthBasic: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -209,22 +212,23 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "config parsed successfully with digest auth labels", desc: "config parsed successfully with digest auth labels",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "instance", name("instance"),
ID: "1", ID("1"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"), label.TraefikFrontendAuthDigestRemoveHeader: aws.String("true"),
label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), label.TraefikFrontendAuthDigestUsers: aws.String("test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"),
label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"), label.TraefikFrontendAuthDigestUsersFile: aws.String(".htpasswd"),
label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -262,11 +266,10 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "config parsed successfully with forward auth labels", desc: "config parsed successfully with forward auth labels",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "instance", name("instance"),
ID: "1", ID("1"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"), label.TraefikFrontendAuthForwardAddress: aws.String("auth.server"),
label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"), label.TraefikFrontendAuthForwardTrustForwardHeader: aws.String("true"),
label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"), label.TraefikFrontendAuthForwardTLSCa: aws.String("ca.crt"),
@ -274,13 +277,15 @@ func TestBuildConfiguration(t *testing.T) {
label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"), label.TraefikFrontendAuthForwardTLSCert: aws.String("server.crt"),
label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"), label.TraefikFrontendAuthForwardTLSKey: aws.String("server.key"),
label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"), label.TraefikFrontendAuthForwardTLSInsecureSkipVerify: aws.String("true"), label.TraefikFrontendAuthHeaderField: aws.String("X-WebAuth-User"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -323,11 +328,10 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "when all labels are set", desc: "when all labels are set",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "testing-instance", name("testing-instance"),
ID: "6", ID("6"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikPort: aws.String("666"), label.TraefikPort: aws.String("666"),
label.TraefikProtocol: aws.String("https"), label.TraefikProtocol: aws.String("https"),
label.TraefikWeight: aws.String("12"), label.TraefikWeight: aws.String("12"),
@ -418,13 +422,15 @@ func TestBuildConfiguration(t *testing.T) {
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"),
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"),
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{
@ -585,11 +591,10 @@ func TestBuildConfiguration(t *testing.T) {
{ {
desc: "Containers with same backend name", desc: "Containers with same backend name",
instances: []ecsInstance{ instances: []ecsInstance{
{ instance(
Name: "testing-instance-v1", name("testing-instance-v1"),
ID: "6", ID("6"),
containerDefinition: &ecs.ContainerDefinition{ dockerLabels(map[string]*string{
DockerLabels: map[string]*string{
label.TraefikPort: aws.String("666"), label.TraefikPort: aws.String("666"),
label.TraefikProtocol: aws.String("https"), label.TraefikProtocol: aws.String("https"),
label.TraefikWeight: aws.String("12"), label.TraefikWeight: aws.String("12"),
@ -663,18 +668,19 @@ func TestBuildConfiguration(t *testing.T) {
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"),
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"),
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.0.0.1", mPrivateIP("10.0.0.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
{ ),
Name: "testing-instance-v2", ),
ID: "6", instance(
containerDefinition: &ecs.ContainerDefinition{ name("testing-instance-v2"),
DockerLabels: map[string]*string{ ID("6"),
dockerLabels(map[string]*string{
label.TraefikPort: aws.String("555"), label.TraefikPort: aws.String("555"),
label.TraefikProtocol: aws.String("https"), label.TraefikProtocol: aws.String("https"),
label.TraefikWeight: aws.String("15"), label.TraefikWeight: aws.String("15"),
@ -748,13 +754,15 @@ func TestBuildConfiguration(t *testing.T) {
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: aws.String("3"),
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: aws.String("6"),
label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"), label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: aws.String("9"),
}}, }),
machine: &machine{ iMachine(
state: ec2.InstanceStateNameRunning, mState(ec2.InstanceStateNameRunning),
privateIP: "10.2.2.1", mPrivateIP("10.2.2.1"),
ports: []portMapping{{hostPort: 1337}}, mPorts(
}, mPort(0, 1337),
}, ),
),
),
}, },
expected: &types.Configuration{ expected: &types.Configuration{
Backends: map[string]*types.Backend{ Backends: map[string]*types.Backend{

View file

@ -45,6 +45,8 @@ type ecsInstance struct {
containerDefinition *ecs.ContainerDefinition containerDefinition *ecs.ContainerDefinition
machine *machine machine *machine
TraefikLabels map[string]string TraefikLabels map[string]string
SegmentLabels map[string]string
SegmentName string
} }
type portMapping struct { type portMapping struct {

View file

@ -122,7 +122,6 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
pool.Go(func(stop chan bool) { pool.Go(func(stop chan bool) {
operation := func() error { operation := func() error {
for {
stopWatch := make(chan struct{}, 1) stopWatch := make(chan struct{}, 1)
defer close(stopWatch) defer close(stopWatch)
eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch)
@ -142,12 +141,10 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
return nil return nil
case event := <-eventsChan: case event := <-eventsChan:
log.Debugf("Received Kubernetes event kind %T", event) log.Debugf("Received Kubernetes event kind %T", event)
templateObjects, err := p.loadIngresses(k8sClient) templateObjects, err := p.loadIngresses(k8sClient)
if err != nil { if err != nil {
return err return err
} }
if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) {
log.Debugf("Skipping Kubernetes event kind %T", event) log.Debugf("Skipping Kubernetes event kind %T", event)
} else { } else {
@ -160,7 +157,6 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
} }
} }
} }
}
notify := func(err error, time time.Duration) { notify := func(err error, time time.Duration) {
log.Errorf("Provider connection error: %s; retrying in %s", err, time) log.Errorf("Provider connection error: %s; retrying in %s", err, time)
@ -224,7 +220,12 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
} }
for _, pa := range r.HTTP.Paths { for _, pa := range r.HTTP.Paths {
priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0)
baseName := r.Host + pa.Path baseName := r.Host + pa.Path
if priority > 0 {
baseName = strconv.Itoa(priority) + "-" + baseName
}
if _, exists := templateObjects.Backends[baseName]; !exists { if _, exists := templateObjects.Backends[baseName]; !exists {
templateObjects.Backends[baseName] = &types.Backend{ templateObjects.Backends[baseName] = &types.Backend{
Servers: make(map[string]types.Server), Servers: make(map[string]types.Server),
@ -250,7 +251,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders)
passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert)
priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0)
entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints)
templateObjects.Frontends[baseName] = &types.Frontend{ templateObjects.Frontends[baseName] = &types.Frontend{
@ -883,7 +883,19 @@ func getFrontendRedirect(i *extensionsv1beta1.Ingress, baseName, path string) *t
} }
redirectRegex := getStringValue(i.Annotations, annotationKubernetesRedirectRegex, "") redirectRegex := getStringValue(i.Annotations, annotationKubernetesRedirectRegex, "")
_, err := strconv.Unquote(`"` + redirectRegex + `"`)
if err != nil {
log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid regex: %s", i.Namespace, i.Name, redirectRegex)
return nil
}
redirectReplacement := getStringValue(i.Annotations, annotationKubernetesRedirectReplacement, "") redirectReplacement := getStringValue(i.Annotations, annotationKubernetesRedirectReplacement, "")
_, err = strconv.Unquote(`"` + redirectReplacement + `"`)
if err != nil {
log.Debugf("Skipping Redirect on Ingress %s/%s due to invalid replacement: %q", i.Namespace, i.Name, redirectRegex)
return nil
}
if len(redirectRegex) > 0 && len(redirectReplacement) > 0 { if len(redirectRegex) > 0 && len(redirectReplacement) > 0 {
return &types.Redirect{ return &types.Redirect{
Regex: redirectRegex, Regex: redirectRegex,

View file

@ -740,6 +740,34 @@ func TestGetPassTLSCert(t *testing.T) {
assert.Equal(t, expected, actual) assert.Equal(t, expected, actual)
} }
func TestInvalidRedirectAnnotation(t *testing.T) {
ingresses := []*extensionsv1beta1.Ingress{
buildIngress(iNamespace("awesome"),
iAnnotation(annotationKubernetesRedirectRegex, `bad\.regex`),
iAnnotation(annotationKubernetesRedirectReplacement, "test"),
iRules(iRule(
iHost("foo"),
iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))),
),
),
buildIngress(iNamespace("awesome"),
iAnnotation(annotationKubernetesRedirectRegex, `test`),
iAnnotation(annotationKubernetesRedirectReplacement, `bad\.replacement`),
iRules(iRule(
iHost("foo"),
iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))),
),
),
}
for _, ingress := range ingresses {
actual := getFrontendRedirect(ingress, "test", "/")
var expected *types.Redirect
assert.Equal(t, expected, actual)
}
}
func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) {
ingresses := []*extensionsv1beta1.Ingress{ ingresses := []*extensionsv1beta1.Ingress{
buildIngress(iNamespace("awesome"), buildIngress(iNamespace("awesome"),
@ -1847,13 +1875,13 @@ func TestPriorityHeaderValue(t *testing.T) {
expected := buildConfiguration( expected := buildConfiguration(
backends( backends(
backend("foo/bar", backend("1337-foo/bar",
servers(server("http://example.com", weight(1))), servers(server("http://example.com", weight(1))),
lbMethod("wrr"), lbMethod("wrr"),
), ),
), ),
frontends( frontends(
frontend("foo/bar", frontend("1337-foo/bar",
passHostHeader(), passHostHeader(),
priority(1337), priority(1337),
routes( routes(

View file

@ -62,3 +62,13 @@ for OS in ${OS_PLATFORM_ARG[@]}; do
done done
done done
done done
# Build ppc64le binaries
OS_PLATFORM_ARG=(linux)
OS_ARCH_ARG=(ppc64le)
for OS in ${OS_PLATFORM_ARG[@]}; do
for ARCH in ${OS_ARCH_ARG[@]}; do
echo "Building binary for ${OS}/${ARCH}..."
GOARCH=${ARCH} GOOS=${OS} CGO_ENABLED=0 ${GO_BUILD_CMD} "${GO_BUILD_OPT}" -o "dist/traefik_${OS}-${ARCH}" ./cmd/traefik/
done
done

View file

@ -163,6 +163,22 @@ func (s serverEntryPoint) Shutdown(ctx context.Context) {
wg.Wait() wg.Wait()
} }
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
// connections.
type tcpKeepAliveListener struct {
*net.TCPListener
}
func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
tc, err := ln.AcceptTCP()
if err != nil {
return nil, err
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
// NewServer returns an initialized Server. // NewServer returns an initialized Server.
func NewServer(globalConfiguration configuration.GlobalConfiguration, provider provider.Provider, entrypoints map[string]EntryPoint) *Server { func NewServer(globalConfiguration configuration.GlobalConfiguration, provider provider.Provider, entrypoints map[string]EntryPoint) *Server {
server := &Server{} server := &Server{}
@ -549,6 +565,8 @@ func (s *Server) prepareServer(entryPointName string, entryPoint *configuration.
return nil, nil, fmt.Errorf("error opening listener: %v", err) return nil, nil, fmt.Errorf("error opening listener: %v", err)
} }
listener = tcpKeepAliveListener{listener.(*net.TCPListener)}
if entryPoint.ProxyProtocol != nil { if entryPoint.ProxyProtocol != nil {
listener, err = buildProxyProtocolListener(entryPoint, listener) listener, err = buildProxyProtocolListener(entryPoint, listener)
if err != nil { if err != nil {

View file

@ -88,8 +88,8 @@
[frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls] [frontends."frontend-{{ $service.ServiceName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -88,8 +88,8 @@
[frontends."frontend-{{ $frontendName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -2,13 +2,13 @@
{{range $serviceName, $instances := .Services }} {{range $serviceName, $instances := .Services }}
{{ $firstInstance := index $instances 0 }} {{ $firstInstance := index $instances 0 }}
{{ $circuitBreaker := getCircuitBreaker $firstInstance.TraefikLabels }} {{ $circuitBreaker := getCircuitBreaker $firstInstance.SegmentLabels }}
{{if $circuitBreaker }} {{if $circuitBreaker }}
[backends."backend-{{ $serviceName }}".circuitBreaker] [backends."backend-{{ $serviceName }}".circuitBreaker]
expression = "{{ $circuitBreaker.Expression }}" expression = "{{ $circuitBreaker.Expression }}"
{{end}} {{end}}
{{ $loadBalancer := getLoadBalancer $firstInstance.TraefikLabels }} {{ $loadBalancer := getLoadBalancer $firstInstance.SegmentLabels }}
{{if $loadBalancer }} {{if $loadBalancer }}
[backends."backend-{{ $serviceName }}".loadBalancer] [backends."backend-{{ $serviceName }}".loadBalancer]
method = "{{ $loadBalancer.Method }}" method = "{{ $loadBalancer.Method }}"
@ -18,14 +18,14 @@
{{end}} {{end}}
{{end}} {{end}}
{{ $maxConn := getMaxConn $firstInstance.TraefikLabels }} {{ $maxConn := getMaxConn $firstInstance.SegmentLabels }}
{{if $maxConn }} {{if $maxConn }}
[backends."backend-{{ $serviceName }}".maxConn] [backends."backend-{{ $serviceName }}".maxConn]
extractorFunc = "{{ $maxConn.ExtractorFunc }}" extractorFunc = "{{ $maxConn.ExtractorFunc }}"
amount = {{ $maxConn.Amount }} amount = {{ $maxConn.Amount }}
{{end}} {{end}}
{{ $healthCheck := getHealthCheck $firstInstance.TraefikLabels }} {{ $healthCheck := getHealthCheck $firstInstance.SegmentLabels }}
{{if $healthCheck }} {{if $healthCheck }}
[backends."backend-{{ $serviceName }}".healthCheck] [backends."backend-{{ $serviceName }}".healthCheck]
scheme = "{{ $healthCheck.Scheme }}" scheme = "{{ $healthCheck.Scheme }}"
@ -41,7 +41,7 @@
{{end}} {{end}}
{{end}} {{end}}
{{ $buffering := getBuffering $firstInstance.TraefikLabels }} {{ $buffering := getBuffering $firstInstance.SegmentLabels }}
{{if $buffering }} {{if $buffering }}
[backends."backend-{{ $serviceName }}".buffering] [backends."backend-{{ $serviceName }}".buffering]
maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }} maxRequestBodyBytes = {{ $buffering.MaxRequestBodyBytes }}
@ -63,38 +63,40 @@
{{range $serviceName, $instances := .Services }} {{range $serviceName, $instances := .Services }}
{{range $instance := filterFrontends $instances }} {{range $instance := filterFrontends $instances }}
[frontends."frontend-{{ $serviceName }}"] {{ $frontendName := getFrontendName $instance }}
backend = "backend-{{ $serviceName }}"
priority = {{ getPriority $instance.TraefikLabels }}
passHostHeader = {{ getPassHostHeader $instance.TraefikLabels }}
passTLSCert = {{ getPassTLSCert $instance.TraefikLabels }}
entryPoints = [{{range getEntryPoints $instance.TraefikLabels }} [frontends."frontend-{{ $frontendName }}"]
backend = "backend-{{ $serviceName }}"
priority = {{ getPriority $instance.SegmentLabels }}
passHostHeader = {{ getPassHostHeader $instance.SegmentLabels }}
passTLSCert = {{ getPassTLSCert $instance.SegmentLabels }}
entryPoints = [{{range getEntryPoints $instance.SegmentLabels }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $auth := getAuth $instance.TraefikLabels }} {{ $auth := getAuth $instance.SegmentLabels }}
{{if $auth }} {{if $auth }}
[frontends."frontend-{{ $serviceName }}".auth] [frontends."frontend-{{ $frontendName }}".auth]
headerField = "{{ $auth.HeaderField }}" headerField = "{{ $auth.HeaderField }}"
{{if $auth.Forward }} {{if $auth.Forward }}
[frontends."frontend-{{ $serviceName }}".auth.forward] [frontends."frontend-{{ $frontendName }}".auth.forward]
address = "{{ $auth.Forward.Address }}" address = "{{ $auth.Forward.Address }}"
trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }} trustForwardHeader = {{ $auth.Forward.TrustForwardHeader }}
{{if $auth.Forward.TLS }} {{if $auth.Forward.TLS }}
[frontends."frontend-{{ $serviceName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}
{{if $auth.Basic }} {{if $auth.Basic }}
[frontends."frontend-{{ $serviceName }}".auth.basic] [frontends."frontend-{{ $frontendName }}".auth.basic]
removeHeader = {{ $auth.Basic.RemoveHeader }} removeHeader = {{ $auth.Basic.RemoveHeader }}
{{if $auth.Basic.Users }} {{if $auth.Basic.Users }}
users = [{{range $auth.Basic.Users }} users = [{{range $auth.Basic.Users }}
@ -105,7 +107,7 @@
{{end}} {{end}}
{{if $auth.Digest }} {{if $auth.Digest }}
[frontends."frontend-{{ $serviceName }}".auth.digest] [frontends."frontend-{{ $frontendName }}".auth.digest]
removeHeader = {{ $auth.Digest.RemoveHeader }} removeHeader = {{ $auth.Digest.RemoveHeader }}
{{if $auth.Digest.Users }} {{if $auth.Digest.Users }}
users = [{{range $auth.Digest.Users }} users = [{{range $auth.Digest.Users }}
@ -116,14 +118,14 @@
{{end}} {{end}}
{{end}} {{end}}
{{ $whitelist := getWhiteList $instance.TraefikLabels }} {{ $whitelist := getWhiteList $instance.SegmentLabels }}
{{if $whitelist }} {{if $whitelist }}
[frontends."frontend-{{ $serviceName }}".whiteList] [frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }} sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{if $whitelist.IPStrategy }} {{if $whitelist.IPStrategy }}
[frontends."frontend-{{ $serviceName }}".whiteList.IPStrategy] [frontends."frontend-{{ $frontendName }}".whiteList.IPStrategy]
depth = {{ $whitelist.IPStrategy.Depth }} depth = {{ $whitelist.IPStrategy.Depth }}
excludedIPs = [{{range $whitelist.IPStrategy.ExcludedIPs }} excludedIPs = [{{range $whitelist.IPStrategy.ExcludedIPs }}
"{{.}}", "{{.}}",
@ -131,20 +133,20 @@
{{end}} {{end}}
{{end}} {{end}}
{{ $redirect := getRedirect $instance.TraefikLabels }} {{ $redirect := getRedirect $instance.SegmentLabels }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $serviceName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]
entryPoint = "{{ $redirect.EntryPoint }}" entryPoint = "{{ $redirect.EntryPoint }}"
regex = "{{ $redirect.Regex }}" regex = "{{ $redirect.Regex }}"
replacement = "{{ $redirect.Replacement }}" replacement = "{{ $redirect.Replacement }}"
permanent = {{ $redirect.Permanent }} permanent = {{ $redirect.Permanent }}
{{end}} {{end}}
{{ $errorPages := getErrorPages $instance.TraefikLabels }} {{ $errorPages := getErrorPages $instance.SegmentLabels }}
{{if $errorPages }} {{if $errorPages }}
[frontends."frontend-{{ $serviceName }}".errors] [frontends."frontend-{{ $frontendName }}".errors]
{{range $pageName, $page := $errorPages }} {{range $pageName, $page := $errorPages }}
[frontends."frontend-{{ $serviceName }}".errors."{{ $pageName }}"] [frontends."frontend-{{ $frontendName }}".errors."{{ $pageName }}"]
status = [{{range $page.Status }} status = [{{range $page.Status }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
@ -153,22 +155,22 @@
{{end}} {{end}}
{{end}} {{end}}
{{ $rateLimit := getRateLimit $instance.TraefikLabels }} {{ $rateLimit := getRateLimit $instance.SegmentLabels }}
{{if $rateLimit }} {{if $rateLimit }}
[frontends."frontend-{{ $serviceName }}".rateLimit] [frontends."frontend-{{ $frontendName }}".rateLimit]
extractorFunc = "{{ $rateLimit.ExtractorFunc }}" extractorFunc = "{{ $rateLimit.ExtractorFunc }}"
[frontends."frontend-{{ $serviceName }}".rateLimit.rateSet] [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet]
{{ range $limitName, $limit := $rateLimit.RateSet }} {{ range $limitName, $limit := $rateLimit.RateSet }}
[frontends."frontend-{{ $serviceName }}".rateLimit.rateSet."{{ $limitName }}"] [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet."{{ $limitName }}"]
period = "{{ $limit.Period }}" period = "{{ $limit.Period }}"
average = {{ $limit.Average }} average = {{ $limit.Average }}
burst = {{ $limit.Burst }} burst = {{ $limit.Burst }}
{{end}} {{end}}
{{end}} {{end}}
{{ $headers := getHeaders $instance.TraefikLabels }} {{ $headers := getHeaders $instance.SegmentLabels }}
{{if $headers }} {{if $headers }}
[frontends."frontend-{{ $serviceName }}".headers] [frontends."frontend-{{ $frontendName }}".headers]
SSLRedirect = {{ $headers.SSLRedirect }} SSLRedirect = {{ $headers.SSLRedirect }}
SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }}
SSLHost = "{{ $headers.SSLHost }}" SSLHost = "{{ $headers.SSLHost }}"
@ -200,28 +202,28 @@
{{end}} {{end}}
{{if $headers.CustomRequestHeaders }} {{if $headers.CustomRequestHeaders }}
[frontends."frontend-{{ $serviceName }}".headers.customRequestHeaders] [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders]
{{range $k, $v := $headers.CustomRequestHeaders }} {{range $k, $v := $headers.CustomRequestHeaders }}
{{$k}} = "{{$v}}" {{$k}} = "{{$v}}"
{{end}} {{end}}
{{end}} {{end}}
{{if $headers.CustomResponseHeaders }} {{if $headers.CustomResponseHeaders }}
[frontends."frontend-{{ $serviceName }}".headers.customResponseHeaders] [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders]
{{range $k, $v := $headers.CustomResponseHeaders }} {{range $k, $v := $headers.CustomResponseHeaders }}
{{$k}} = "{{$v}}" {{$k}} = "{{$v}}"
{{end}} {{end}}
{{end}} {{end}}
{{if $headers.SSLProxyHeaders }} {{if $headers.SSLProxyHeaders }}
[frontends."frontend-{{ $serviceName }}".headers.SSLProxyHeaders] [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders]
{{range $k, $v := $headers.SSLProxyHeaders }} {{range $k, $v := $headers.SSLProxyHeaders }}
{{$k}} = "{{$v}}" {{$k}} = "{{$v}}"
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}
[frontends."frontend-{{ $serviceName }}".routes."route-frontend-{{ $serviceName }}"] [frontends."frontend-{{ $frontendName }}".routes."route-frontend-{{ $frontendName }}"]
rule = "{{ getFrontendRule $instance }}" rule = "{{ getFrontendRule $instance }}"
{{end}} {{end}}

View file

@ -80,8 +80,8 @@
trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }} trustForwardHeader = {{ $frontend.Auth.Forward.TrustForwardHeader }}
{{if $frontend.Auth.Forward.TLS }} {{if $frontend.Auth.Forward.TLS }}
[frontends."{{ $frontendName }}".auth.forward.tls] [frontends."{{ $frontendName }}".auth.forward.tls]
cert = "{{ $frontend.Auth.Forward.TLS.Cert }}" cert = """{{ $frontend.Auth.Forward.TLS.Cert }}"""
key = "{{ $frontend.Auth.Forward.TLS.Key }}" key = """{{ $frontend.Auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $frontend.Auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -87,8 +87,8 @@
[frontends."{{ $frontendName }}".auth.forward.tls] [frontends."{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -90,8 +90,8 @@
[frontends."{{ $frontendName }}".auth.forward.tls] [frontends."{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -90,8 +90,8 @@
[frontends."frontend-{{ $frontendName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -88,8 +88,8 @@
[frontends."frontend-{{ $frontendName }}".auth.forward.tls] [frontends."frontend-{{ $frontendName }}".auth.forward.tls]
ca = "{{ $auth.Forward.TLS.CA }}" ca = "{{ $auth.Forward.TLS.CA }}"
caOptional = {{ $auth.Forward.TLS.CAOptional }} caOptional = {{ $auth.Forward.TLS.CAOptional }}
cert = "{{ $auth.Forward.TLS.Cert }}" cert = """{{ $auth.Forward.TLS.Cert }}"""
key = "{{ $auth.Forward.TLS.Key }}" key = """{{ $auth.Forward.TLS.Key }}"""
insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }} insecureSkipVerify = {{ $auth.Forward.TLS.InsecureSkipVerify }}
{{end}} {{end}}
{{end}} {{end}}

View file

@ -6,12 +6,14 @@ package websocket
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httptrace"
"net/url" "net/url"
"strings" "strings"
"time" "time"
@ -51,6 +53,10 @@ type Dialer struct {
// NetDial is nil, net.Dial is used. // NetDial is nil, net.Dial is used.
NetDial func(network, addr string) (net.Conn, error) NetDial func(network, addr string) (net.Conn, error)
// NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, net.DialContext is used.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given // Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the // Request. If the function returns a non-nil error, the
// request is aborted with the provided error. // request is aborted with the provided error.
@ -69,6 +75,17 @@ type Dialer struct {
// do not limit the size of the messages that can be sent or received. // do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the client's requested subprotocols. // Subprotocols specifies the client's requested subprotocols.
Subprotocols []string Subprotocols []string
@ -84,6 +101,11 @@ type Dialer struct {
Jar http.CookieJar Jar http.CookieJar
} }
// Dial creates a new client connection by calling DialContext with a background context.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
return d.DialContext(context.Background(), urlStr, requestHeader)
}
var errMalformedURL = errors.New("malformed ws or wss URL") var errMalformedURL = errors.New("malformed ws or wss URL")
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) { func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
@ -111,19 +133,20 @@ var DefaultDialer = &Dialer{
} }
// nilDialer is dialer to use when receiver is nil. // nilDialer is dialer to use when receiver is nil.
var nilDialer Dialer = *DefaultDialer var nilDialer = *DefaultDialer
// Dial creates a new client connection. Use requestHeader to specify the // DialContext creates a new client connection. Use requestHeader to specify the
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie). // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
// Use the response.Header to get the selected subprotocol // Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie). // (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
// //
// The context will be used in the request and in the Dialer
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a // If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication, // non-nil *http.Response so that callers can handle redirects, authentication,
// etcetera. The response body may not contain the entire response and does not // etcetera. The response body may not contain the entire response and does not
// need to be closed by the application. // need to be closed by the application.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) { func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
if d == nil { if d == nil {
d = &nilDialer d = &nilDialer
} }
@ -161,6 +184,7 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
Header: make(http.Header), Header: make(http.Header),
Host: u.Host, Host: u.Host,
} }
req = req.WithContext(ctx)
// Set the cookies present in the cookie jar of the dialer // Set the cookies present in the cookie jar of the dialer
if d.Jar != nil { if d.Jar != nil {
@ -201,23 +225,33 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
} }
if d.EnableCompression { if d.EnableCompression {
req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
} }
var deadline time.Time
if d.HandshakeTimeout != 0 { if d.HandshakeTimeout != 0 {
deadline = time.Now().Add(d.HandshakeTimeout) var cancel func()
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
defer cancel()
} }
// Get network dial function. // Get network dial function.
netDial := d.NetDial var netDial func(network, add string) (net.Conn, error)
if netDial == nil {
netDialer := &net.Dialer{Deadline: deadline} if d.NetDialContext != nil {
netDial = netDialer.Dial netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
} else {
netDialer := &net.Dialer{}
netDial = func(network, addr string) (net.Conn, error) {
return netDialer.DialContext(ctx, network, addr)
}
} }
// If needed, wrap the dial function to set the connection deadline. // If needed, wrap the dial function to set the connection deadline.
if !deadline.Equal(time.Time{}) { if deadline, ok := ctx.Deadline(); ok {
forwardDial := netDial forwardDial := netDial
netDial = func(network, addr string) (net.Conn, error) { netDial = func(network, addr string) (net.Conn, error) {
c, err := forwardDial(network, addr) c, err := forwardDial(network, addr)
@ -249,7 +283,17 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
} }
hostPort, hostNoPort := hostPortNoPort(u) hostPort, hostNoPort := hostPortNoPort(u)
trace := httptrace.ContextClientTrace(ctx)
if trace != nil && trace.GetConn != nil {
trace.GetConn(hostPort)
}
netConn, err := netDial("tcp", hostPort) netConn, err := netDial("tcp", hostPort)
if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{
Conn: netConn,
})
}
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -267,22 +311,31 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
} }
tlsConn := tls.Client(netConn, cfg) tlsConn := tls.Client(netConn, cfg)
netConn = tlsConn netConn = tlsConn
if err := tlsConn.Handshake(); err != nil {
return nil, nil, err var err error
if trace != nil {
err = doHandshakeWithTrace(trace, tlsConn, cfg)
} else {
err = doHandshake(tlsConn, cfg)
} }
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
} }
}
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize) conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
if err := req.Write(netConn); err != nil { if err := req.Write(netConn); err != nil {
return nil, nil, err return nil, nil, err
} }
if trace != nil && trace.GotFirstResponseByte != nil {
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
trace.GotFirstResponseByte()
}
}
resp, err := http.ReadResponse(conn.br, req) resp, err := http.ReadResponse(conn.br, req)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -328,3 +381,15 @@ func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Re
netConn = nil // to avoid close in defer. netConn = nil // to avoid close in defer.
return conn, resp, nil return conn, resp, nil
} }
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.Handshake(); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View file

@ -223,6 +223,20 @@ func isValidReceivedCloseCode(code int) bool {
return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999) return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
} }
// BufferPool represents a pool of buffers. The *sync.Pool type satisfies this
// interface. The type of the value stored in a pool is not specified.
type BufferPool interface {
// Get gets a value from the pool or returns nil if the pool is empty.
Get() interface{}
// Put adds a value to the pool.
Put(interface{})
}
// writePoolData is the type added to the write buffer pool. This wrapper is
// used to prevent applications from peeking at and depending on the values
// added to the pool.
type writePoolData struct{ buf []byte }
// The Conn type represents a WebSocket connection. // The Conn type represents a WebSocket connection.
type Conn struct { type Conn struct {
conn net.Conn conn net.Conn
@ -232,6 +246,8 @@ type Conn struct {
// Write fields // Write fields
mu chan bool // used as mutex to protect write to conn mu chan bool // used as mutex to protect write to conn
writeBuf []byte // frame is constructed in this buffer. writeBuf []byte // frame is constructed in this buffer.
writePool BufferPool
writeBufSize int
writeDeadline time.Time writeDeadline time.Time
writer io.WriteCloser // the current writer returned to the application writer io.WriteCloser // the current writer returned to the application
isWriting bool // for best-effort concurrent write detection isWriting bool // for best-effort concurrent write detection
@ -263,64 +279,29 @@ type Conn struct {
newDecompressionReader func(io.Reader) io.ReadCloser newDecompressionReader func(io.Reader) io.ReadCloser
} }
func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn { func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, writeBufferPool BufferPool, br *bufio.Reader, writeBuf []byte) *Conn {
return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
}
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
mu := make(chan bool, 1)
mu <- true
var br *bufio.Reader
if readBufferSize == 0 && brw != nil && brw.Reader != nil {
// Reuse the supplied bufio.Reader if the buffer has a useful size.
// This code assumes that peek on a reader returns
// bufio.Reader.buf[:0].
brw.Reader.Reset(conn)
if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
br = brw.Reader
}
}
if br == nil { if br == nil {
if readBufferSize == 0 { if readBufferSize == 0 {
readBufferSize = defaultReadBufferSize readBufferSize = defaultReadBufferSize
} } else if readBufferSize < maxControlFramePayloadSize {
if readBufferSize < maxControlFramePayloadSize { // must be large enough for control frame
readBufferSize = maxControlFramePayloadSize readBufferSize = maxControlFramePayloadSize
} }
br = bufio.NewReaderSize(conn, readBufferSize) br = bufio.NewReaderSize(conn, readBufferSize)
} }
var writeBuf []byte if writeBufferSize <= 0 {
if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
// Use the bufio.Writer's buffer if the buffer has a useful size. This
// code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
brw.Writer.Reset(&wh)
brw.Writer.WriteByte(0)
brw.Flush()
if cap(wh.p) >= maxFrameHeaderSize+256 {
writeBuf = wh.p[:cap(wh.p)]
}
}
if writeBuf == nil {
if writeBufferSize == 0 {
writeBufferSize = defaultWriteBufferSize writeBufferSize = defaultWriteBufferSize
} }
writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize) writeBufferSize += maxFrameHeaderSize
if writeBuf == nil && writeBufferPool == nil {
writeBuf = make([]byte, writeBufferSize)
} }
mu := make(chan bool, 1)
mu <- true
c := &Conn{ c := &Conn{
isServer: isServer, isServer: isServer,
br: br, br: br,
@ -328,6 +309,8 @@ func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize in
mu: mu, mu: mu,
readFinal: true, readFinal: true,
writeBuf: writeBuf, writeBuf: writeBuf,
writePool: writeBufferPool,
writeBufSize: writeBufferSize,
enableWriteCompression: true, enableWriteCompression: true,
compressionLevel: defaultCompressionLevel, compressionLevel: defaultCompressionLevel,
} }
@ -370,6 +353,15 @@ func (c *Conn) writeFatal(err error) error {
return err return err
} }
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}
func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error { func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error {
<-c.mu <-c.mu
defer func() { c.mu <- true }() defer func() { c.mu <- true }()
@ -475,9 +467,21 @@ func (c *Conn) prepWrite(messageType int) error {
c.writeErrMu.Lock() c.writeErrMu.Lock()
err := c.writeErr err := c.writeErr
c.writeErrMu.Unlock() c.writeErrMu.Unlock()
if err != nil {
return err return err
} }
if c.writeBuf == nil {
wpd, ok := c.writePool.Get().(writePoolData)
if ok {
c.writeBuf = wpd.buf
} else {
c.writeBuf = make([]byte, c.writeBufSize)
}
}
return nil
}
// NextWriter returns a writer for the next message to send. The writer's Close // NextWriter returns a writer for the next message to send. The writer's Close
// method flushes the complete message to the network. // method flushes the complete message to the network.
// //
@ -601,6 +605,10 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
if final { if final {
c.writer = nil c.writer = nil
if c.writePool != nil {
c.writePool.Put(writePoolData{buf: c.writeBuf})
c.writeBuf = nil
}
return nil return nil
} }

View file

@ -1,18 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
c.br.Discard(len(p))
return p, err
}

View file

@ -1,21 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.5
package websocket
import "io"
func (c *Conn) read(n int) ([]byte, error) {
p, err := c.br.Peek(n)
if err == io.EOF {
err = errUnexpectedEOF
}
if len(p) > 0 {
// advance over the bytes just read
io.ReadFull(c.br, p)
}
return p, err
}

View file

@ -19,7 +19,6 @@ import (
type PreparedMessage struct { type PreparedMessage struct {
messageType int messageType int
data []byte data []byte
err error
mu sync.Mutex mu sync.Mutex
frames map[prepareKey]*preparedFrame frames map[prepareKey]*preparedFrame
} }

View file

@ -14,7 +14,7 @@ import (
"strings" "strings"
) )
type netDialerFunc func(netowrk, addr string) (net.Conn, error) type netDialerFunc func(network, addr string) (net.Conn, error)
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
return fn(network, addr) return fn(network, addr)

View file

@ -7,7 +7,7 @@ package websocket
import ( import (
"bufio" "bufio"
"errors" "errors"
"net" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -33,10 +33,23 @@ type Upgrader struct {
// or received. // or received.
ReadBufferSize, WriteBufferSize int ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the server's supported protocols in order of // Subprotocols specifies the server's supported protocols in order of
// preference. If this field is set, then the Upgrade method negotiates a // preference. If this field is not nil, then the Upgrade method negotiates a
// subprotocol by selecting the first match in this list with a protocol // subprotocol by selecting the first match in this list with a protocol
// requested by the client. // requested by the client. If there's no match, then no protocol is
// negotiated (the Sec-Websocket-Protocol header is not included in the
// handshake response).
Subprotocols []string Subprotocols []string
// Error specifies the function for generating HTTP error responses. If Error // Error specifies the function for generating HTTP error responses. If Error
@ -103,7 +116,7 @@ func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header
// //
// The responseHeader is included in the response to the client's upgrade // The responseHeader is included in the response to the client's upgrade
// request. Use the responseHeader to specify cookies (Set-Cookie) and the // request. Use the responseHeader to specify cookies (Set-Cookie) and the
// application negotiated subprotocol (Sec-Websocket-Protocol). // application negotiated subprotocol (Sec-WebSocket-Protocol).
// //
// If the upgrade fails, then Upgrade replies to the client with an HTTP error // If the upgrade fails, then Upgrade replies to the client with an HTTP error
// response. // response.
@ -127,7 +140,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok { if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported") return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
} }
checkOrigin := u.CheckOrigin checkOrigin := u.CheckOrigin
@ -140,7 +153,7 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
challengeKey := r.Header.Get("Sec-Websocket-Key") challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" { if challengeKey == "" {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-WebSocket-Key' header is missing or blank")
} }
subprotocol := u.selectSubprotocol(r, responseHeader) subprotocol := u.selectSubprotocol(r, responseHeader)
@ -157,17 +170,12 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
} }
var (
netConn net.Conn
err error
)
h, ok := w.(http.Hijacker) h, ok := w.(http.Hijacker)
if !ok { if !ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker") return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
} }
var brw *bufio.ReadWriter var brw *bufio.ReadWriter
netConn, brw, err = h.Hijack() netConn, brw, err := h.Hijack()
if err != nil { if err != nil {
return u.returnError(w, r, http.StatusInternalServerError, err.Error()) return u.returnError(w, r, http.StatusInternalServerError, err.Error())
} }
@ -177,7 +185,21 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
return nil, errors.New("websocket: client sent data before handshake is complete") return nil, errors.New("websocket: client sent data before handshake is complete")
} }
c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw) var br *bufio.Reader
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
// Reuse hijacked buffered reader as connection reader.
br = brw.Reader
}
buf := bufioWriterBuffer(netConn, brw.Writer)
var writeBuf []byte
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
// Reuse hijacked write buffer as connection buffer.
writeBuf = buf
}
c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
c.subprotocol = subprotocol c.subprotocol = subprotocol
if compress { if compress {
@ -185,17 +207,23 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
c.newDecompressionReader = decompressNoContextTakeover c.newDecompressionReader = decompressNoContextTakeover
} }
p := c.writeBuf[:0] // Use larger of hijacked buffer and connection write buffer for header.
p := buf
if len(c.writeBuf) > len(p) {
p = c.writeBuf
}
p = p[:0]
p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...) p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
p = append(p, computeAcceptKey(challengeKey)...) p = append(p, computeAcceptKey(challengeKey)...)
p = append(p, "\r\n"...) p = append(p, "\r\n"...)
if c.subprotocol != "" { if c.subprotocol != "" {
p = append(p, "Sec-Websocket-Protocol: "...) p = append(p, "Sec-WebSocket-Protocol: "...)
p = append(p, c.subprotocol...) p = append(p, c.subprotocol...)
p = append(p, "\r\n"...) p = append(p, "\r\n"...)
} }
if compress { if compress {
p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...) p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
} }
for k, vs := range responseHeader { for k, vs := range responseHeader {
if k == "Sec-Websocket-Protocol" { if k == "Sec-Websocket-Protocol" {
@ -296,3 +324,40 @@ func IsWebSocketUpgrade(r *http.Request) bool {
return tokenListContainsValue(r.Header, "Connection", "upgrade") && return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
tokenListContainsValue(r.Header, "Upgrade", "websocket") tokenListContainsValue(r.Header, "Upgrade", "websocket")
} }
// bufioReaderSize size returns the size of a bufio.Reader.
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int {
// This code assumes that peek on a reset reader returns
// bufio.Reader.buf[:0].
// TODO: Use bufio.Reader.Size() after Go 1.10
br.Reset(originalReader)
if p, err := br.Peek(0); err == nil {
return cap(p)
}
return 0
}
// writeHook is an io.Writer that records the last slice passed to it vio
// io.Writer.Write.
type writeHook struct {
p []byte
}
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
// This code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
bw.Reset(&wh)
bw.WriteByte(0)
bw.Flush()
bw.Reset(originalWriter)
return wh.p[:cap(wh.p)]
}

19
vendor/github.com/gorilla/websocket/trace.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
// +build go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
if trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err := doHandshake(tlsConn, cfg)
if trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
}
return err
}

12
vendor/github.com/gorilla/websocket/trace_17.go generated vendored Normal file
View file

@ -0,0 +1,12 @@
// +build !go1.8
package websocket
import (
"crypto/tls"
"net/http/httptrace"
)
func doHandshakeWithTrace(trace *httptrace.ClientTrace, tlsConn *tls.Conn, cfg *tls.Config) error {
return doHandshake(tlsConn, cfg)
}

View file

@ -178,7 +178,7 @@ headers:
return false return false
} }
// parseExtensiosn parses WebSocket extensions from a header. // parseExtensions parses WebSocket extensions from a header.
func parseExtensions(header http.Header) []map[string]string { func parseExtensions(header http.Header) []map[string]string {
// From RFC 6455: // From RFC 6455:
// //

View file

@ -4,9 +4,11 @@
package forward package forward
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -257,6 +259,8 @@ func New(setters ...optSetter) (*Forwarder, error) {
errorHandler: f.errHandler, errorHandler: f.errHandler,
} }
f.postConfig()
return f, nil return f, nil
} }
@ -309,11 +313,6 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
outReq.URL.RawQuery = u.RawQuery outReq.URL.RawQuery = u.RawQuery
outReq.RequestURI = "" // Outgoing request should not have RequestURI outReq.RequestURI = "" // Outgoing request should not have RequestURI
// Do not pass client Host header unless optsetter PassHostHeader is set.
if !f.passHost {
outReq.Host = target.Host
}
outReq.Proto = "HTTP/1.1" outReq.Proto = "HTTP/1.1"
outReq.ProtoMajor = 1 outReq.ProtoMajor = 1
outReq.ProtoMinor = 1 outReq.ProtoMinor = 1
@ -321,6 +320,11 @@ func (f *httpForwarder) modifyRequest(outReq *http.Request, target *url.URL) {
if f.rewriter != nil { if f.rewriter != nil {
f.rewriter.Rewrite(outReq) f.rewriter.Rewrite(outReq)
} }
// Do not pass client Host header unless optsetter PassHostHeader is set.
if !f.passHost {
outReq.Host = target.Host
}
} }
// serveHTTP forwards websocket traffic // serveHTTP forwards websocket traffic
@ -340,7 +344,7 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
// WebSocket is only in http/1.1 // WebSocket is only in http/1.1
dialer.TLSClientConfig.NextProtos = []string{"http/1.1"} dialer.TLSClientConfig.NextProtos = []string{"http/1.1"}
} }
targetConn, resp, err := dialer.Dial(outReq.URL.String(), outReq.Header) targetConn, resp, err := dialer.DialContext(outReq.Context(), outReq.URL.String(), outReq.Header)
if err != nil { if err != nil {
if resp == nil { if resp == nil {
ctx.errHandler.ServeHTTP(w, req, err) ctx.errHandler.ServeHTTP(w, req, err)
@ -396,16 +400,28 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
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) {
forward := func(messageType int, reader io.Reader) error {
writer, err := dst.NextWriter(messageType)
if err != nil {
return err
}
_, err = io.Copy(writer, reader)
if err != nil {
return err
}
return writer.Close()
}
src.SetPingHandler(func(data string) error { src.SetPingHandler(func(data string) error {
return dst.WriteMessage(websocket.PingMessage, []byte(data)) return forward(websocket.PingMessage, bytes.NewReader([]byte(data)))
}) })
src.SetPongHandler(func(data string) error { src.SetPongHandler(func(data string) error {
return dst.WriteMessage(websocket.PongMessage, []byte(data)) return forward(websocket.PongMessage, bytes.NewReader([]byte(data)))
}) })
for { for {
msgType, msg, err := src.ReadMessage() msgType, reader, err := src.NextReader()
if err != nil { if err != nil {
m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err))
@ -423,11 +439,11 @@ func (f *httpForwarder) serveWebSocket(w http.ResponseWriter, req *http.Request,
} }
errc <- err errc <- err
if m != nil { if m != nil {
dst.WriteMessage(websocket.CloseMessage, m) forward(websocket.CloseMessage, bytes.NewReader([]byte(m)))
} }
break break
} }
err = dst.WriteMessage(msgType, msg) err = forward(msgType, reader)
if err != nil { if err != nil {
errc <- err errc <- err
break break

5
vendor/github.com/vulcand/oxy/forward/post_config.go generated vendored Normal file
View file

@ -0,0 +1,5 @@
// +build go1.11
package forward
func (f *Forwarder) postConfig() {}

View file

@ -0,0 +1,42 @@
// +build !go1.11
package forward
import (
"context"
"net/http"
)
type key string
const (
teHeader key = "TeHeader"
)
type TeTrailerRoundTripper struct {
http.RoundTripper
}
func (t *TeTrailerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
teHeader := req.Context().Value(teHeader)
if teHeader != nil {
req.Header.Set("Te", teHeader.(string))
}
return t.RoundTripper.RoundTrip(req)
}
type TeTrailerRewriter struct {
ReqRewriter
}
func (t *TeTrailerRewriter) Rewrite(req *http.Request) {
if req.Header.Get("Te") == "trailers" {
*req = *req.WithContext(context.WithValue(req.Context(), teHeader, req.Header.Get("Te")))
}
t.ReqRewriter.Rewrite(req)
}
func (f *Forwarder) postConfig() {
f.roundTripper = &TeTrailerRoundTripper{RoundTripper: f.roundTripper}
f.rewriter = &TeTrailerRewriter{ReqRewriter: f.rewriter}
}

View file

@ -69,12 +69,6 @@ func (rw *HeaderRewriter) Rewrite(req *http.Request) {
if rw.Hostname != "" { if rw.Hostname != "" {
req.Header.Set(XForwardedServer, rw.Hostname) req.Header.Set(XForwardedServer, rw.Hostname)
} }
if !IsWebsocketRequest(req) {
// Remove hop-by-hop headers to the backend. Especially important is "Connection" because we want a persistent
// connection, regardless of what the client sent to us.
utils.RemoveHeaders(req.Header, HopHeaders...)
}
} }
func forwardedPort(req *http.Request) string { func forwardedPort(req *http.Request) string {

View file

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"context"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -8,6 +9,12 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// StatusClientClosedRequest non-standard HTTP status code for client disconnection
const StatusClientClosedRequest = 499
// StatusClientClosedRequestText non-standard HTTP status for client disconnection
const StatusClientClosedRequestText = "Client Closed Request"
// ErrorHandler error handler // ErrorHandler error handler
type ErrorHandler interface { type ErrorHandler interface {
ServeHTTP(w http.ResponseWriter, req *http.Request, err error) ServeHTTP(w http.ResponseWriter, req *http.Request, err error)
@ -21,6 +28,7 @@ type StdHandler struct{}
func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) { func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err error) {
statusCode := http.StatusInternalServerError statusCode := http.StatusInternalServerError
if e, ok := err.(net.Error); ok { if e, ok := err.(net.Error); ok {
if e.Timeout() { if e.Timeout() {
statusCode = http.StatusGatewayTimeout statusCode = http.StatusGatewayTimeout
@ -29,10 +37,20 @@ func (e *StdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request, err err
} }
} else if err == io.EOF { } else if err == io.EOF {
statusCode = http.StatusBadGateway statusCode = http.StatusBadGateway
} else if err == context.Canceled {
statusCode = StatusClientClosedRequest
} }
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
w.Write([]byte(http.StatusText(statusCode))) w.Write([]byte(statusText(statusCode)))
log.Debugf("'%d %s' caused by: %v", statusCode, http.StatusText(statusCode), err) log.Debugf("'%d %s' caused by: %v", statusCode, statusText(statusCode), err)
}
func statusText(statusCode int) string {
if statusCode == StatusClientClosedRequest {
return StatusClientClosedRequestText
}
return http.StatusText(statusCode)
} }
// ErrorHandlerFunc error handler function type // ErrorHandlerFunc error handler function type