diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index ade7bba6f..3b31e6bd0 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -37,14 +37,14 @@ traefik*
The idea behind `glide` is the following :
-- when checkout(ing) a project, **run `glide up --quick`** to install
+- when checkout(ing) a project, **run `glide install`** to install
(`go get …`) the dependencies in the `GOPATH`.
- if you need another dependency, import and use it in
the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in
`vendor` and add it to your `glide.yaml`.
```bash
-$ glide up --quick
+$ glide install
# generate
$ go generate
# Simple go build
diff --git a/.gitignore b/.gitignore
index 03aa43689..cac197c4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,12 @@
/dist
gen.go
.idea
+.intellij
log
*.iml
traefik
traefik.toml
*.test
vendor/
-static/
\ No newline at end of file
+static/
+.vscode/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..438c30722
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,10 @@
+- repo: git://github.com/pre-commit/pre-commit-hooks
+ sha: 44e1753f98b0da305332abe26856c3e621c5c439
+ hooks:
+ - id: detect-private-key
+- repo: git://github.com/containous/pre-commit-hooks
+ sha: 35e641b5107671e94102b0ce909648559e568d61
+ hooks:
+ - id: goFmt
+ - id: goLint
+ - id: goErrcheck
diff --git a/Makefile b/Makefile
index 8d0079ef2..39fed86de 100644
--- a/Makefile
+++ b/Makefile
@@ -41,8 +41,8 @@ test-unit: build
test-integration: build
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-integration
-validate: build
- $(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint
+validate: build
+ $(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint
validate-gofmt: build
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt
@@ -84,7 +84,7 @@ generate-webui: build-webui
fi
lint:
- $(foreach file,$(SRCS),golint $(file) || exit;)
+ script/validate-golint
fmt:
gofmt -s -l -w $(SRCS)
diff --git a/README.md b/README.md
index 6b826fdef..829aa61e5 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[![Build Status](https://travis-ci.org/containous/traefik.svg?branch=master)](https://travis-ci.org/containous/traefik)
-[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](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://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com)
[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy)
@@ -18,8 +18,7 @@ It supports several backends ([Docker :whale:](https://www.docker.com/), [Mesos/
- [It's fast](docs/index.md#benchmarks)
- No dependency hell, single binary made with go
-- Simple json Rest API
-- Simple TOML file configuration
+- Rest API
- Multiple backends supported: Docker, Mesos/Marathon, Consul, Etcd, and more to come
- Watchers for backends, can listen change in backends to apply a new configuration automatically
- Hot-reloading of configuration. No need to restart the process
@@ -29,10 +28,11 @@ It supports several backends ([Docker :whale:](https://www.docker.com/), [Mesos/
- Rest Metrics
- Tiny docker image included [![Image Layers](https://badge.imagelayers.io/containous/traefik:latest.svg)](https://imagelayers.io/?images=containous/traefik:latest)
- SSL backends support
-- SSL frontend support
+- SSL frontend support (with SNI)
- Clean AngularJS Web UI
- Websocket support
- HTTP/2 support
+- [Let's Encrypt](https://letsencrypt.org) support (Automatic HTTPS)
## Demo
@@ -53,6 +53,7 @@ You can access to a simple HTML frontend of Træfik.
- [Gorilla mux](https://github.com/gorilla/mux): famous request router
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
- [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers
+- [Lego](https://github.com/xenolf/lego): the best [Let's Encrypt](https://letsencrypt.org) library in go
## Quick start
diff --git a/acme/acme.go b/acme/acme.go
new file mode 100644
index 000000000..c3498d074
--- /dev/null
+++ b/acme/acme.go
@@ -0,0 +1,405 @@
+package acme
+
+import (
+ "crypto"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
+ "errors"
+ "fmt"
+ log "github.com/Sirupsen/logrus"
+ "github.com/xenolf/lego/acme"
+ "io/ioutil"
+ fmtlog "log"
+ "os"
+ "reflect"
+ "sync"
+ "time"
+)
+
+// Account is used to store lets encrypt registration info
+type Account struct {
+ Email string
+ Registration *acme.RegistrationResource
+ PrivateKey []byte
+ DomainsCertificate DomainsCertificates
+}
+
+// GetEmail returns email
+func (a Account) GetEmail() string {
+ return a.Email
+}
+
+// GetRegistration returns lets encrypt registration resource
+func (a Account) GetRegistration() *acme.RegistrationResource {
+ return a.Registration
+}
+
+// GetPrivateKey returns private key
+func (a Account) GetPrivateKey() crypto.PrivateKey {
+ if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
+ return privateKey
+ }
+ log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey)
+ return nil
+}
+
+// Certificate is used to store certificate info
+type Certificate struct {
+ Domain string
+ CertURL string
+ CertStableURL string
+ PrivateKey []byte
+ Certificate []byte
+}
+
+// DomainsCertificates stores a certificate for multiple domains
+type DomainsCertificates struct {
+ Certs []*DomainsCertificate
+ lock *sync.RWMutex
+}
+
+func (dc *DomainsCertificates) init() error {
+ if dc.lock == nil {
+ dc.lock = &sync.RWMutex{}
+ }
+ dc.lock.Lock()
+ defer dc.lock.Unlock()
+ for _, domainsCertificate := range dc.Certs {
+ tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
+ if err != nil {
+ return err
+ }
+ domainsCertificate.tlsCert = &tlsCert
+ }
+ return nil
+}
+
+func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error {
+ dc.lock.Lock()
+ defer dc.lock.Unlock()
+
+ for _, domainsCertificate := range dc.Certs {
+ if reflect.DeepEqual(domain, domainsCertificate.Domains) {
+ domainsCertificate.Certificate = acmeCert
+ tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
+ if err != nil {
+ return err
+ }
+ domainsCertificate.tlsCert = &tlsCert
+ return nil
+ }
+ }
+ return errors.New("Certificate to renew not found for domain " + domain.Main)
+}
+
+func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) {
+ dc.lock.Lock()
+ defer dc.lock.Unlock()
+
+ tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
+ if err != nil {
+ return nil, err
+ }
+ cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert}
+ dc.Certs = append(dc.Certs, &cert)
+ return &cert, nil
+}
+
+func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) {
+ dc.lock.RLock()
+ defer dc.lock.RUnlock()
+ for _, domainsCertificate := range dc.Certs {
+ domains := []string{}
+ domains = append(domains, domainsCertificate.Domains.Main)
+ domains = append(domains, domainsCertificate.Domains.SANs...)
+ for _, domain := range domains {
+ if domain == domainToFind {
+ return domainsCertificate, true
+ }
+ }
+ }
+ return nil, false
+}
+
+func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) {
+ dc.lock.RLock()
+ defer dc.lock.RUnlock()
+ for _, domainsCertificate := range dc.Certs {
+ if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) {
+ return domainsCertificate, true
+ }
+ }
+ return nil, false
+}
+
+// DomainsCertificate contains a certificate for multiple domains
+type DomainsCertificate struct {
+ Domains Domain
+ Certificate *Certificate
+ tlsCert *tls.Certificate
+}
+
+// ACME allows to connect to lets encrypt and retrieve certs
+type ACME struct {
+ Email string
+ Domains []Domain
+ StorageFile string
+ OnDemand bool
+ CAServer string
+ EntryPoint string
+ storageLock sync.RWMutex
+}
+
+// Domain holds a domain name with SANs
+type Domain struct {
+ Main string
+ SANs []string
+}
+
+// CreateConfig creates a tls.config from using ACME configuration
+func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error {
+ acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
+
+ if len(a.StorageFile) == 0 {
+ return errors.New("Empty StorageFile, please provide a filenmae for certs storage")
+ }
+
+ log.Debugf("Generating default certificate...")
+ if len(tlsConfig.Certificates) == 0 {
+ // no certificates in TLS config, so we add a default one
+ cert, err := generateDefaultCertificate()
+ if err != nil {
+ return err
+ }
+ tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
+ }
+ var account *Account
+ var needRegister bool
+
+ // if certificates in storage, load them
+ if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 {
+ log.Infof("Loading ACME certificates...")
+ // load account
+ account, err = a.loadAccount(a)
+ if err != nil {
+ return err
+ }
+ } else {
+ log.Infof("Generating ACME Account...")
+ // Create a user. New accounts need an email and private key to start
+ privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
+ if err != nil {
+ return err
+ }
+ account = &Account{
+ Email: a.Email,
+ PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
+ }
+ account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}}
+ needRegister = true
+ }
+
+ client, err := a.buildACMEClient(account)
+ if err != nil {
+ return err
+ }
+ client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
+ wrapperChallengeProvider := newWrapperChallengeProvider()
+ client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider)
+
+ if needRegister {
+ // New users will need to register; be sure to save it
+ reg, err := client.Register()
+ if err != nil {
+ return err
+ }
+ account.Registration = reg
+ }
+
+ // The client has a URL to the current Let's Encrypt Subscriber
+ // Agreement. The user will need to agree to it.
+ err = client.AgreeToTOS()
+ if err != nil {
+ return err
+ }
+
+ go a.retrieveCertificates(client, account)
+
+ tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok {
+ return challengeCert, nil
+ }
+ if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
+ return domainCert.tlsCert, nil
+ }
+ if a.OnDemand {
+ if CheckOnDemandDomain != nil && !CheckOnDemandDomain(clientHello.ServerName) {
+ return nil, nil
+ }
+ return a.loadCertificateOnDemand(client, account, clientHello)
+ }
+ return nil, nil
+ }
+
+ ticker := time.NewTicker(24 * time.Hour)
+ go func() {
+ for {
+ select {
+ case <-ticker.C:
+
+ if err := a.renewCertificates(client, account); err != nil {
+ log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
+ }
+ }
+ }
+
+ }()
+ return nil
+}
+
+func (a *ACME) retrieveCertificates(client *acme.Client, account *Account) {
+ log.Infof("Retrieving ACME certificates...")
+ for _, domain := range a.Domains {
+ // check if cert isn't already loaded
+ if _, exists := account.DomainsCertificate.exists(domain); !exists {
+ domains := []string{}
+ domains = append(domains, domain.Main)
+ domains = append(domains, domain.SANs...)
+ certificateResource, err := a.getDomainsCertificates(client, domains)
+ if err != nil {
+ log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
+ continue
+ }
+ _, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
+ if err != nil {
+ log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error())
+ continue
+ }
+ if err = a.saveAccount(account); err != nil {
+ log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
+ continue
+ }
+ }
+ }
+ log.Infof("Retrieved ACME certificates")
+}
+
+func (a *ACME) renewCertificates(client *acme.Client, account *Account) error {
+ for _, certificateResource := range account.DomainsCertificate.Certs {
+ // <= 7 days left, renew certificate
+ if certificateResource.tlsCert.Leaf.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) {
+ log.Debugf("Renewing certificate %+v", certificateResource.Domains)
+ renewedCert, err := client.RenewCertificate(acme.CertificateResource{
+ Domain: certificateResource.Certificate.Domain,
+ CertURL: certificateResource.Certificate.CertURL,
+ CertStableURL: certificateResource.Certificate.CertStableURL,
+ PrivateKey: certificateResource.Certificate.PrivateKey,
+ Certificate: certificateResource.Certificate.Certificate,
+ }, false)
+ if err != nil {
+ return err
+ }
+ log.Debugf("Renewed certificate %+v", certificateResource.Domains)
+ renewedACMECert := &Certificate{
+ Domain: renewedCert.Domain,
+ CertURL: renewedCert.CertURL,
+ CertStableURL: renewedCert.CertStableURL,
+ PrivateKey: renewedCert.PrivateKey,
+ Certificate: renewedCert.Certificate,
+ }
+ err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
+ if err != nil {
+ return err
+ }
+ if err = a.saveAccount(account); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (a *ACME) buildACMEClient(Account *Account) (*acme.Client, error) {
+ caServer := "https://acme-v01.api.letsencrypt.org/directory"
+ if len(a.CAServer) > 0 {
+ caServer = a.CAServer
+ }
+ client, err := acme.NewClient(caServer, Account, acme.RSA4096)
+ if err != nil {
+ return nil, err
+ }
+
+ return client, nil
+}
+
+func (a *ACME) loadCertificateOnDemand(client *acme.Client, Account *Account, clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
+ if certificateResource, ok := Account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
+ return certificateResource.tlsCert, nil
+ }
+ Certificate, err := a.getDomainsCertificates(client, []string{clientHello.ServerName})
+ if err != nil {
+ return nil, err
+ }
+ log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName)
+ cert, err := Account.DomainsCertificate.addCertificateForDomains(Certificate, Domain{Main: clientHello.ServerName})
+ if err != nil {
+ return nil, err
+ }
+ if err = a.saveAccount(Account); err != nil {
+ return nil, err
+ }
+ return cert.tlsCert, nil
+}
+
+func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) {
+ a.storageLock.RLock()
+ defer a.storageLock.RUnlock()
+ Account := Account{
+ DomainsCertificate: DomainsCertificates{},
+ }
+ file, err := ioutil.ReadFile(acmeConfig.StorageFile)
+ if err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(file, &Account); err != nil {
+ return nil, err
+ }
+ err = Account.DomainsCertificate.init()
+ if err != nil {
+ return nil, err
+ }
+ log.Infof("Loaded ACME config from storage %s", acmeConfig.StorageFile)
+ return &Account, nil
+}
+
+func (a *ACME) saveAccount(Account *Account) error {
+ a.storageLock.Lock()
+ defer a.storageLock.Unlock()
+ // write account to file
+ data, err := json.MarshalIndent(Account, "", " ")
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(a.StorageFile, data, 0644)
+}
+
+func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) {
+ log.Debugf("Loading ACME certificates %s...", domains)
+ bundle := false
+ certificate, failures := client.ObtainCertificate(domains, bundle, nil)
+ if len(failures) > 0 {
+ log.Error(failures)
+ return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)
+ }
+ log.Debugf("Loaded ACME certificates %s", domains)
+ return &Certificate{
+ Domain: certificate.Domain,
+ CertURL: certificate.CertURL,
+ CertStableURL: certificate.CertStableURL,
+ PrivateKey: certificate.PrivateKey,
+ Certificate: certificate.Certificate,
+ }, nil
+}
diff --git a/acme/challengeProvider.go b/acme/challengeProvider.go
new file mode 100644
index 000000000..8bdda2673
--- /dev/null
+++ b/acme/challengeProvider.go
@@ -0,0 +1,56 @@
+package acme
+
+import (
+ "crypto/tls"
+ "sync"
+
+ "crypto/x509"
+ "github.com/xenolf/lego/acme"
+)
+
+type wrapperChallengeProvider struct {
+ challengeCerts map[string]*tls.Certificate
+ lock sync.RWMutex
+}
+
+func newWrapperChallengeProvider() *wrapperChallengeProvider {
+ return &wrapperChallengeProvider{
+ challengeCerts: map[string]*tls.Certificate{},
+ }
+}
+
+func (c *wrapperChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
+ c.lock.RLock()
+ defer c.lock.RUnlock()
+ if cert, ok := c.challengeCerts[domain]; ok {
+ return cert, true
+ }
+ return nil, false
+}
+
+func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error {
+ cert, err := acme.TLSSNI01ChallengeCert(keyAuth)
+ if err != nil {
+ return err
+ }
+ cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
+ if err != nil {
+ return err
+ }
+
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ for i := range cert.Leaf.DNSNames {
+ c.challengeCerts[cert.Leaf.DNSNames[i]] = &cert
+ }
+
+ return nil
+
+}
+
+func (c *wrapperChallengeProvider) CleanUp(domain, token, keyAuth string) error {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ delete(c.challengeCerts, domain)
+ return nil
+}
diff --git a/acme/crypto.go b/acme/crypto.go
new file mode 100644
index 000000000..6fa544b70
--- /dev/null
+++ b/acme/crypto.go
@@ -0,0 +1,78 @@
+package acme
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/hex"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "time"
+)
+
+func generateDefaultCertificate() (*tls.Certificate, error) {
+ rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, err
+ }
+ rsaPrivPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)})
+
+ randomBytes := make([]byte, 100)
+ _, err = rand.Read(randomBytes)
+ if err != nil {
+ return nil, err
+ }
+ zBytes := sha256.Sum256(randomBytes)
+ z := hex.EncodeToString(zBytes[:sha256.Size])
+ domain := fmt.Sprintf("%s.%s.traefik.default", z[:32], z[32:])
+ tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
+ if err != nil {
+ return nil, err
+ }
+
+ certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ return &certificate, nil
+}
+func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
+ derBytes, err := generateDerCert(privKey, time.Time{}, domain)
+ if err != nil {
+ return nil, err
+ }
+
+ return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
+}
+
+func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ return nil, err
+ }
+
+ if expiration.IsZero() {
+ expiration = time.Now().Add(365)
+ }
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ CommonName: "TRAEFIK DEFAULT CERT",
+ },
+ NotBefore: time.Now(),
+ NotAfter: expiration,
+
+ KeyUsage: x509.KeyUsageKeyEncipherment,
+ BasicConstraintsValid: true,
+ DNSNames: []string{domain},
+ }
+
+ return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
+}
diff --git a/build.Dockerfile b/build.Dockerfile
index 5711a8778..ad2145376 100644
--- a/build.Dockerfile
+++ b/build.Dockerfile
@@ -1,11 +1,11 @@
FROM golang:1.6.0-alpine
-RUN apk update && apk add git bash gcc
-
-RUN go get github.com/Masterminds/glide
-RUN go get github.com/mitchellh/gox
-RUN go get github.com/jteeuwen/go-bindata/...
-RUN go get github.com/golang/lint/golint
+RUN apk update && apk add git bash gcc musl-dev \
+&& go get github.com/Masterminds/glide \
+&& go get github.com/mitchellh/gox \
+&& go get github.com/jteeuwen/go-bindata/... \
+&& go get github.com/golang/lint/golint \
+&& go get github.com/kisielk/errcheck
# Which docker version to test on
ENV DOCKER_VERSION 1.10.1
diff --git a/cmd.go b/cmd.go
index e0639197c..3cbab053a 100644
--- a/cmd.go
+++ b/cmd.go
@@ -166,15 +166,15 @@ func init() {
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Endpoint, "boltdb.endpoint", "127.0.0.1:4001", "Boltdb server endpoint")
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store")
- viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
- viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
- //viper.BindPFlag("defaultEntryPoints", traefikCmd.PersistentFlags().Lookup("defaultEntryPoints"))
- viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
+ _ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
+ _ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
+ _ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
// TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105
- viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
- viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost"))
+ _ = viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
+ _ = viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost"))
viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second))
viper.SetDefault("logLevel", "ERROR")
+ viper.SetDefault("MaxIdleConnsPerHost", 200)
}
func run() {
@@ -196,7 +196,11 @@ func run() {
if len(globalConfiguration.TraefikLogsFile) > 0 {
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
- defer fi.Close()
+ defer func() {
+ if err := fi.Close(); err != nil {
+ log.Error("Error closinf file", err)
+ }
+ }()
if err != nil {
log.Fatal("Error opening file", err)
} else {
diff --git a/configuration.go b/configuration.go
index c7e97a934..597c3d725 100644
--- a/configuration.go
+++ b/configuration.go
@@ -8,6 +8,7 @@ import (
"strings"
"time"
+ "github.com/containous/traefik/acme"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/types"
"github.com/mitchellh/mapstructure"
@@ -22,6 +23,7 @@ type GlobalConfiguration struct {
TraefikLogsFile string
LogLevel string
EntryPoints EntryPoints
+ ACME *acme.ACME
DefaultEntryPoints DefaultEntryPoints
ProvidersThrottleDuration time.Duration
MaxIdleConnsPerHost int
@@ -92,7 +94,9 @@ func (ep *EntryPoints) Set(value string) error {
var tls *TLS
if len(result["TLS"]) > 0 {
certs := Certificates{}
- certs.Set(result["TLS"])
+ if err := certs.Set(result["TLS"]); err != nil {
+ return err
+ }
tls = &TLS{
Certificates: certs,
}
@@ -244,6 +248,7 @@ func LoadConfiguration() *GlobalConfiguration {
viper.Set("boltdb", arguments.Boltdb)
}
if err := unmarshal(&configuration); err != nil {
+
fmtlog.Fatalf("Error reading file: %s", err)
}
diff --git a/docs/index.md b/docs/index.md
index f219f7018..2a6ce76b7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
-![Træfɪk](http://traefik.github.io/traefik.logo.svg "Træfɪk")
-___
-
+
+
+
# Documentation
@@ -54,15 +54,20 @@ Various methods of load-balancing is supported:
- `drr`: Dynamic Round Robin: increases weights on servers that perform better than others. It also rolls back to original weights if the servers have changed.
A circuit breaker can also be applied to a backend, preventing high loads on failing servers.
+Initial state is Standby. CB observes the statistics and does not modify the request.
+In case if condition matches, CB enters Tripped state, where it responds with predefines code or redirects to another frontend.
+Once Tripped timer expires, CB enters Recovering state and resets all stats.
+In case if the condition does not match and recovery timer expries, CB enters Standby state.
+
It can be configured using:
- Methods: `LatencyAtQuantileMS`, `NetworkErrorRatio`, `ResponseCodeRatio`
- Operators: `AND`, `OR`, `EQ`, `NEQ`, `LT`, `LE`, `GT`, `GE`
For example:
-- `NetworkErrorRatio() > 0.5`
-- `LatencyAtQuantileMS(50.0) > 50`
-- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`
+- `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window for a frontend
+- `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds.
+- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in range [500-600) to [0-600)
## Launch configuration
@@ -177,46 +182,6 @@ Use "traefik [command] --help" for more information about a command.
# Global configuration
################################################################
-# Entrypoints definition
-#
-# Optional
-# Default:
-# [entryPoints]
-# [entryPoints.http]
-# address = ":80"
-#
-# To redirect an http entrypoint to an https entrypoint (with SNI support):
-# [entryPoints]
-# [entryPoints.http]
-# address = ":80"
-# [entryPoints.http.redirect]
-# entryPoint = "https"
-# [entryPoints.https]
-# address = ":443"
-# [entryPoints.https.tls]
-# [[entryPoints.https.tls.certificates]]
-# CertFile = "integration/fixtures/https/snitest.com.cert"
-# KeyFile = "integration/fixtures/https/snitest.com.key"
-# [[entryPoints.https.tls.certificates]]
-# CertFile = "integration/fixtures/https/snitest.org.cert"
-# KeyFile = "integration/fixtures/https/snitest.org.key"
-#
-# To redirect an entrypoint rewriting the URL:
-# [entryPoints]
-# [entryPoints.http]
-# address = ":80"
-# [entryPoints.http.redirect]
-# regex = "^http://localhost/(.*)"
-# replacement = "http://mydomain/$1"
-
-# Entrypoints to be used by frontends that do not specify any entrypoint.
-# Each frontend can specify its own entrypoints.
-#
-# Optional
-# Default: ["http"]
-#
-# defaultEntryPoints = ["http", "https"]
-
# Timeout in seconds.
# Duration to give active requests a chance to finish during hot-reloads
#
@@ -262,6 +227,203 @@ Use "traefik [command] --help" for more information about a command.
#
# MaxIdleConnsPerHost = 200
+# Entrypoints to be used by frontends that do not specify any entrypoint.
+# Each frontend can specify its own entrypoints.
+#
+# Optional
+# Default: ["http"]
+#
+# defaultEntryPoints = ["http", "https"]
+
+# Enable ACME (Let's Encrypt): automatic SSL
+#
+# Optional
+#
+# [acme]
+
+# Email address used for registration
+#
+# Required
+#
+# email = "test@traefik.io"
+
+# File used for certificates storage.
+# WARNING, if you use Traefik in Docker, don't forget to mount this file as a volume.
+#
+# Required
+#
+# storageFile = "acme.json"
+
+# Entrypoint to proxy acme challenge to.
+# WARNING, must point to an entrypoint on port 443
+#
+# Required
+#
+# entryPoint = "https"
+
+# Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate.
+# WARNING, TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks.
+# WARNING, Take note that Let's Encrypt have rate limiting: https://community.letsencrypt.org/t/quick-start-guide/1631
+#
+# Optional
+#
+# onDemand = true
+
+# CA server to use
+# Uncomment the line to run on the staging let's encrypt server
+# Leave comment to go to prod
+#
+# Optional
+#
+# caServer = "https://acme-staging.api.letsencrypt.org/directory"
+
+# Domains list
+# You can provide SANs (alternative domains) to each main domain
+# WARNING, Take note that Let's Encrypt have rate limiting: https://community.letsencrypt.org/t/quick-start-guide/1631
+# Each domain & SANs will lead to a certificate request.
+#
+# [[acme.domains]]
+# main = "local1.com"
+# sans = ["test1.local1.com", "test2.local1.com"]
+# [[acme.domains]]
+# main = "local2.com"
+# sans = ["test1.local2.com", "test2x.local2.com"]
+# [[acme.domains]]
+# main = "local3.com"
+# [[acme.domains]]
+# main = "local4.com"
+
+
+# Entrypoints definition
+#
+# Optional
+# Default:
+# [entryPoints]
+# [entryPoints.http]
+# address = ":80"
+#
+# To redirect an http entrypoint to an https entrypoint (with SNI support):
+# [entryPoints]
+# [entryPoints.http]
+# address = ":80"
+# [entryPoints.http.redirect]
+# entryPoint = "https"
+# [entryPoints.https]
+# address = ":443"
+# [entryPoints.https.tls]
+# [[entryPoints.https.tls.certificates]]
+# CertFile = "integration/fixtures/https/snitest.com.cert"
+# KeyFile = "integration/fixtures/https/snitest.com.key"
+# [[entryPoints.https.tls.certificates]]
+# CertFile = "integration/fixtures/https/snitest.org.cert"
+# KeyFile = "integration/fixtures/https/snitest.org.key"
+#
+# To redirect an entrypoint rewriting the URL:
+# [entryPoints]
+# [entryPoints.http]
+# address = ":80"
+# [entryPoints.http.redirect]
+# regex = "^http://localhost/(.*)"
+# replacement = "http://mydomain/$1"
+```
+
+### Samples
+
+#### HTTP only
+
+```
+defaultEntryPoints = ["http"]
+[entryPoints]
+ [entryPoints.http]
+ address = ":80"
+```
+
+### HTTP + HTTPS (with SNI)
+
+```
+defaultEntryPoints = ["http", "https"]
+[entryPoints]
+ [entryPoints.http]
+ address = ":80"
+ [entryPoints.https]
+ address = ":443"
+ [entryPoints.https.tls]
+ [[entryPoints.https.tls.certificates]]
+ CertFile = "integration/fixtures/https/snitest.com.cert"
+ KeyFile = "integration/fixtures/https/snitest.com.key"
+ [[entryPoints.https.tls.certificates]]
+ CertFile = "integration/fixtures/https/snitest.org.cert"
+ KeyFile = "integration/fixtures/https/snitest.org.key"
+```
+
+### HTTP redirect on HTTPS
+
+```
+defaultEntryPoints = ["http", "https"]
+[entryPoints]
+ [entryPoints.http]
+ address = ":80"
+ [entryPoints.http.redirect]
+ entryPoint = "https"
+ [entryPoints.https]
+ address = ":443"
+ [entryPoints.https.tls]
+ [[entryPoints.https.tls.certificates]]
+ certFile = "tests/traefik.crt"
+ keyFile = "tests/traefik.key"
+```
+
+### Let's Encrypt support
+
+```
+[entryPoints]
+ [entryPoints.https]
+ address = ":443"
+ [entryPoints.https.tls]
+ # certs used as default certs
+ [[entryPoints.https.tls.certificates]]
+ certFile = "tests/traefik.crt"
+ keyFile = "tests/traefik.key"
+[acme]
+email = "test@traefik.io"
+storageFile = "acme.json"
+onDemand = true
+caServer = "http://172.18.0.1:4000/directory"
+entryPoint = "https"
+
+[[acme.domains]]
+ main = "local1.com"
+ sans = ["test1.local1.com", "test2.local1.com"]
+[[acme.domains]]
+ main = "local2.com"
+ sans = ["test1.local2.com", "test2x.local2.com"]
+[[acme.domains]]
+ main = "local3.com"
+[[acme.domains]]
+ main = "local4.com"
+```
+
+### Override entrypoints in frontends
+
+```
+[frontends]
+ [frontends.frontend1]
+ backend = "backend2"
+ [frontends.frontend1.routes.test_1]
+ rule = "Host"
+ value = "test.localhost"
+ [frontends.frontend2]
+ backend = "backend1"
+ passHostHeader = true
+ entrypoints = ["https"] # overrides defaultEntryPoints
+ [frontends.frontend2.routes.test_1]
+ rule = "Host"
+ value = "{subdomain:[a-z]+}.localhost"
+ [frontends.frontend3]
+ entrypoints = ["http", "https"] # overrides defaultEntryPoints
+ backend = "backend2"
+ rule = "Path"
+ value = "/test"
```
diff --git a/glide.lock b/glide.lock
index a74d445ea..42fb5ed30 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,5 +1,5 @@
-hash: 2a18c9cab231b5e108c666641c2436da3d9a1a0d9d1c586948af94271a47b317
-updated: 2016-03-15T23:01:22.853471291+01:00
+hash: 6f5b6e92b805fed0bb6a5bfe411b5ca501bc04accebeb739cec039e6499271e2
+updated: 2016-03-16T13:22:21.850972237+01:00
imports:
- name: github.com/alecthomas/template
version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
@@ -165,14 +165,12 @@ imports:
version: 44874009257d4d47ba9806f1b7f72a32a015e4d8
- name: github.com/mailgun/manners
version: fada45142db3f93097ca917da107aa3fad0ffcb5
-- name: github.com/mailgun/oxy
- version: 8aaf36279137ac04ace3792a4f86098631b27d5a
- subpackages:
- - cbreaker
- name: github.com/mailgun/predicate
version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
- name: github.com/mailgun/timetools
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
+- name: github.com/miekg/dns
+ version: b9171237b0642de1d8e8004f16869970e065f46b
- name: github.com/mitchellh/mapstructure
version: d2dd0262208475919e1a362f675cfc0e7c10e905
- name: github.com/opencontainers/runc
@@ -203,6 +201,11 @@ imports:
version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7
- name: github.com/spf13/viper
version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
+- name: github.com/square/go-jose
+ version: 70a7e670bd0d4bb35902d31f3a75a6689843abed
+ subpackages:
+ - cipher
+ - json
- name: github.com/stretchr/objx
version: cbeaeb16a013161a98496fad62933b1d21786672
- name: github.com/stretchr/testify
@@ -233,10 +236,19 @@ imports:
- router
- name: github.com/wendal/errors
version: f66c77a7882b399795a8987ebf87ef64a427417e
+- name: github.com/xenolf/lego
+ version: 118d9d5ec92bc243ea054742a03afae813ac1314
+ subpackages:
+ - acme
+- name: golang.org/x/crypto
+ version: 6025851c7c2bf210daf74d22300c699b16541847
+ subpackages:
+ - ocsp
- name: golang.org/x/net
version: d9558e5c97f85372afee28cf2b6059d7d3818919
subpackages:
- context
+ - publicsuffix
- name: golang.org/x/sys
version: eb2c74142fd19a79b3f237334c7384d5167b1b46
subpackages:
diff --git a/glide.yaml b/glide.yaml
index caa66c296..682baac75 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -164,4 +164,4 @@ import:
- package: github.com/google/go-querystring/query
- package: github.com/vulcand/vulcand/plugin/rewrite
- package: github.com/stretchr/testify/mock
-
+ - package: github.com/xenolf/lego
diff --git a/integration/basic_test.go b/integration/basic_test.go
index 90dd2fd25..d9a9178a8 100644
--- a/integration/basic_test.go
+++ b/integration/basic_test.go
@@ -46,10 +46,9 @@ func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) {
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1:8000/")
- // Expected no response as we did not configure anything
- c.Assert(resp, checker.IsNil)
- c.Assert(err, checker.NotNil)
- c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
+ // Expected a 404 as we did not configure anything
+ c.Assert(err, checker.IsNil)
+ c.Assert(resp.StatusCode, checker.Equals, 404)
}
func (s *SimpleSuite) TestWithWebConfig(c *check.C) {
diff --git a/integration/consul_test.go b/integration/consul_test.go
index c672cfde2..a59247adb 100644
--- a/integration/consul_test.go
+++ b/integration/consul_test.go
@@ -5,7 +5,6 @@ import (
"os/exec"
"time"
- "fmt"
checker "github.com/vdemeester/shakers"
check "gopkg.in/check.v1"
)
@@ -20,8 +19,7 @@ func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) {
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1:8000/")
- // Expected no response as we did not configure anything
- c.Assert(resp, checker.IsNil)
- c.Assert(err, checker.NotNil)
- c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
+ // Expected a 404 as we did not configure anything
+ c.Assert(err, checker.IsNil)
+ c.Assert(resp.StatusCode, checker.Equals, 404)
}
diff --git a/integration/etcd_test.go b/integration/etcd_test.go
index 377bbc437..8352cc511 100644
--- a/integration/etcd_test.go
+++ b/integration/etcd_test.go
@@ -5,7 +5,6 @@ import (
"os/exec"
"time"
- "fmt"
checker "github.com/vdemeester/shakers"
check "gopkg.in/check.v1"
)
@@ -20,8 +19,7 @@ func (s *EtcdSuite) TestSimpleConfiguration(c *check.C) {
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1:8000/")
- // Expected no response as we did not configure anything
- c.Assert(resp, checker.IsNil)
- c.Assert(err, checker.NotNil)
- c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
+ // Expected a 404 as we did not configure anything
+ c.Assert(err, checker.IsNil)
+ c.Assert(resp.StatusCode, checker.Equals, 404)
}
diff --git a/integration/marathon_test.go b/integration/marathon_test.go
index ea45381f4..40a42ffd6 100644
--- a/integration/marathon_test.go
+++ b/integration/marathon_test.go
@@ -1,7 +1,6 @@
package main
import (
- "fmt"
"net/http"
"os/exec"
"time"
@@ -20,8 +19,7 @@ func (s *MarathonSuite) TestSimpleConfiguration(c *check.C) {
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1:8000/")
- // Expected no response as we did not configure anything
- c.Assert(resp, checker.IsNil)
- c.Assert(err, checker.NotNil)
- c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
+ // Expected a 404 as we did not configure anything
+ c.Assert(err, checker.IsNil)
+ c.Assert(resp.StatusCode, checker.Equals, 404)
}
diff --git a/provider/docker.go b/provider/docker.go
index 28e260ec0..58939554f 100644
--- a/provider/docker.go
+++ b/provider/docker.go
@@ -34,35 +34,39 @@ type DockerTLS struct {
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) error {
+ go func() {
+ operation := func() error {
+ var dockerClient *docker.Client
+ var err error
- var dockerClient *docker.Client
- var err error
-
- if provider.TLS != nil {
- dockerClient, err = docker.NewTLSClient(provider.Endpoint,
- provider.TLS.Cert, provider.TLS.Key, provider.TLS.CA)
- if err == nil {
- dockerClient.TLSConfig.InsecureSkipVerify = provider.TLS.InsecureSkipVerify
- }
- } else {
- dockerClient, err = docker.NewClient(provider.Endpoint)
- }
- if err != nil {
- log.Errorf("Failed to create a client for docker, error: %s", err)
- return err
- }
- err = dockerClient.Ping()
- if err != nil {
- log.Errorf("Docker connection error %+v", err)
- return err
- }
- log.Debug("Docker connection established")
- if provider.Watch {
- dockerEvents := make(chan *docker.APIEvents)
- dockerClient.AddEventListener(dockerEvents)
- log.Debug("Docker listening")
- go func() {
- operation := func() error {
+ if provider.TLS != nil {
+ dockerClient, err = docker.NewTLSClient(provider.Endpoint,
+ provider.TLS.Cert, provider.TLS.Key, provider.TLS.CA)
+ if err == nil {
+ dockerClient.TLSConfig.InsecureSkipVerify = provider.TLS.InsecureSkipVerify
+ }
+ } else {
+ dockerClient, err = docker.NewClient(provider.Endpoint)
+ }
+ if err != nil {
+ log.Errorf("Failed to create a client for docker, error: %s", err)
+ return err
+ }
+ err = dockerClient.Ping()
+ if err != nil {
+ log.Errorf("Docker connection error %+v", err)
+ return err
+ }
+ log.Debug("Docker connection established")
+ configuration := provider.loadDockerConfig(listContainers(dockerClient))
+ configurationChan <- types.ConfigMessage{
+ ProviderName: "docker",
+ Configuration: configuration,
+ }
+ if provider.Watch {
+ dockerEvents := make(chan *docker.APIEvents)
+ dockerClient.AddEventListener(dockerEvents)
+ log.Debug("Docker listening")
for {
event := <-dockerEvents
if event == nil {
@@ -81,21 +85,17 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) er
}
}
}
- notify := func(err error, time time.Duration) {
- log.Errorf("Docker connection error %+v, retrying in %s", err, time)
- }
- err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
- if err != nil {
- log.Fatalf("Cannot connect to docker server %+v", err)
- }
- }()
- }
+ return nil
+ }
+ notify := func(err error, time time.Duration) {
+ log.Errorf("Docker connection error %+v, retrying in %s", err, time)
+ }
+ err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
+ if err != nil {
+ log.Fatalf("Cannot connect to docker server %+v", err)
+ }
+ }()
- configuration := provider.loadDockerConfig(listContainers(dockerClient))
- configurationChan <- types.ConfigMessage{
- ProviderName: "docker",
- Configuration: configuration,
- }
return nil
}
diff --git a/script/binary b/script/binary
index 2806238d4..506535f5f 100755
--- a/script/binary
+++ b/script/binary
@@ -17,4 +17,4 @@ if [ -z "$DATE" ]; then
fi
# Build binaries
-CGO_ENABLED=0 go build -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik .
+CGO_ENABLED=0 GOGC=off go build -v -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik .
diff --git a/script/crossbinary b/script/crossbinary
index 3da66a209..d154e2056 100755
--- a/script/crossbinary
+++ b/script/crossbinary
@@ -32,5 +32,5 @@ fi
rm -f dist/traefik_*
# Build binaries
-gox -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \
+GOGC=off gox -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \
-output="dist/traefik_{{.OS}}-{{.Arch}}"
diff --git a/script/validate-errcheck b/script/validate-errcheck
new file mode 100755
index 000000000..cfdad38f2
--- /dev/null
+++ b/script/validate-errcheck
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+source "$(dirname "$BASH_SOURCE")/.validate"
+
+IFS=$'\n'
+files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) )
+unset IFS
+
+errors=()
+failedErrcheck=$(errcheck .)
+if [ "$failedErrcheck" ]; then
+ errors+=( "$failedErrcheck" )
+fi
+
+if [ ${#errors[@]} -eq 0 ]; then
+ echo 'Congratulations! All Go source files have been errchecked.'
+else
+ {
+ echo "Errors from errcheck:"
+ for err in "${errors[@]}"; do
+ echo "$err"
+ done
+ echo
+ echo 'Please fix the above errors. You can test via "errcheck" and commit the result.'
+ echo
+ } >&2
+ false
+fi
diff --git a/server.go b/server.go
index a85eccbde..47398cfdb 100644
--- a/server.go
+++ b/server.go
@@ -34,7 +34,7 @@ var oxyLogger = &OxyLogger{}
// Server is the reverse-proxy/load-balancer engine
type Server struct {
- serverEntryPoints map[string]serverEntryPoint
+ serverEntryPoints serverEntryPoints
configurationChan chan types.ConfigMessage
configurationValidatedChan chan types.ConfigMessage
signals chan os.Signal
@@ -46,6 +46,8 @@ type Server struct {
loggerMiddleware *middlewares.Logger
}
+type serverEntryPoints map[string]*serverEntryPoint
+
type serverEntryPoint struct {
httpServer *manners.GracefulServer
httpRouter *middlewares.HandlerSwitcher
@@ -55,7 +57,7 @@ type serverEntryPoint struct {
func NewServer(globalConfiguration GlobalConfiguration) *Server {
server := new(Server)
- server.serverEntryPoints = make(map[string]serverEntryPoint)
+ server.serverEntryPoints = make(map[string]*serverEntryPoint)
server.configurationChan = make(chan types.ConfigMessage, 10)
server.configurationValidatedChan = make(chan types.ConfigMessage, 10)
server.signals = make(chan os.Signal, 1)
@@ -71,6 +73,7 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server {
// Start starts the server and blocks until server is shutted down.
func (server *Server) Start() {
+ server.startHTTPServers()
go server.listenProviders()
go server.listenConfigurations()
server.configureProviders()
@@ -96,6 +99,19 @@ func (server *Server) Close() {
server.loggerMiddleware.Close()
}
+func (server *Server) startHTTPServers() {
+ server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
+ for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
+ newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, server.loggerMiddleware, metrics)
+ if err != nil {
+ log.Fatal("Error preparing server: ", err)
+ }
+ serverEntryPoint := server.serverEntryPoints[newServerEntryPointName]
+ serverEntryPoint.httpServer = newsrv
+ go server.startServer(serverEntryPoint.httpServer, server.globalConfiguration)
+ }
+}
+
func (server *Server) listenProviders() {
lastReceivedConfiguration := time.Unix(0, 0)
lastConfigs := make(map[string]*types.ConfigMessage)
@@ -141,22 +157,8 @@ func (server *Server) listenConfigurations() {
if err == nil {
server.serverLock.Lock()
for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints {
- currentServerEntryPoint := server.serverEntryPoints[newServerEntryPointName]
- if currentServerEntryPoint.httpServer == nil {
- newsrv, err := server.prepareServer(newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, server.loggerMiddleware, metrics)
- if err != nil {
- log.Fatal("Error preparing server: ", err)
- }
- go server.startServer(newsrv, server.globalConfiguration)
- currentServerEntryPoint.httpServer = newsrv
- currentServerEntryPoint.httpRouter = newServerEntryPoint.httpRouter
- server.serverEntryPoints[newServerEntryPointName] = currentServerEntryPoint
- log.Infof("Created new Handler: %p", newServerEntryPoint.httpRouter.GetHandler())
- } else {
- handlerSwitcher := currentServerEntryPoint.httpRouter
- handlerSwitcher.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler())
- log.Infof("Created new Handler: %p", newServerEntryPoint.httpRouter.GetHandler())
- }
+ server.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler())
+ log.Infof("Server configurartion reloaded on %s", server.serverEntryPoints[newServerEntryPointName].httpServer.Addr)
}
server.currentConfigurations = newConfigurations
server.serverLock.Unlock()
@@ -222,26 +224,41 @@ func (server *Server) listenSignals() {
}
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI
-func (server *Server) createTLSConfig(tlsOption *TLS) (*tls.Config, error) {
+func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) {
if tlsOption == nil {
return nil, nil
}
- if len(tlsOption.Certificates) == 0 {
- return nil, nil
- }
config := &tls.Config{}
- if config.NextProtos == nil {
- config.NextProtos = []string{"http/1.1"}
- }
-
- var err error
- config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates))
- for i, v := range tlsOption.Certificates {
- config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
+ config.Certificates = []tls.Certificate{}
+ for _, v := range tlsOption.Certificates {
+ cert, err := tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
if err != nil {
return nil, err
}
+ config.Certificates = append(config.Certificates, cert)
+ }
+
+ if server.globalConfiguration.ACME != nil {
+ if _, ok := server.serverEntryPoints[server.globalConfiguration.ACME.EntryPoint]; ok {
+ if entryPointName == server.globalConfiguration.ACME.EntryPoint {
+ checkOnDemandDomain := func(domain string) bool {
+ if router.GetHandler().Match(&http.Request{URL: &url.URL{}, Host: domain}, &mux.RouteMatch{}) {
+ return true
+ }
+ return false
+ }
+ err := server.globalConfiguration.ACME.CreateConfig(config, checkOnDemandDomain)
+ if err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + " for ACME configuration")
+ }
+ }
+ if len(config.Certificates) == 0 {
+ return nil, errors.New("No certificates found for TLS entrypoint " + entryPointName)
}
// BuildNameToCertificate parses the CommonName and SubjectAlternateName fields
// in each certificate and populates the config.NameToCertificate map.
@@ -250,30 +267,28 @@ func (server *Server) createTLSConfig(tlsOption *TLS) (*tls.Config, error) {
}
func (server *Server) startServer(srv *manners.GracefulServer, globalConfiguration GlobalConfiguration) {
- log.Info("Starting server on ", srv.Addr)
+ log.Infof("Starting server on %s", srv.Addr)
if srv.TLSConfig != nil {
- err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig)
- if err != nil {
+ if err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig); err != nil {
log.Fatal("Error creating server: ", err)
}
} else {
- err := srv.ListenAndServe()
- if err != nil {
+ if err := srv.ListenAndServe(); err != nil {
log.Fatal("Error creating server: ", err)
}
}
log.Info("Server stopped")
}
-func (server *Server) prepareServer(router http.Handler, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
- log.Info("Preparing server")
+func (server *Server) prepareServer(entryPointName string, router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
+ log.Infof("Preparing server %s %+v", entryPointName, entryPoint)
// middlewares
var negroni = negroni.New()
for _, middleware := range middlewares {
negroni.Use(middleware)
}
negroni.UseHandler(router)
- tlsConfig, err := server.createTLSConfig(entryPoint.TLS)
+ tlsConfig, err := server.createTLSConfig(entryPointName, entryPoint.TLS, router)
if err != nil {
log.Fatalf("Error creating TLS config %s", err)
return nil, err
@@ -299,11 +314,11 @@ func (server *Server) prepareServer(router http.Handler, entryPoint *EntryPoint,
return gracefulServer, nil
}
-func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]serverEntryPoint {
- serverEntryPoints := make(map[string]serverEntryPoint)
+func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]*serverEntryPoint {
+ serverEntryPoints := make(map[string]*serverEntryPoint)
for entryPointName := range globalConfiguration.EntryPoints {
router := server.buildDefaultHTTPRouter()
- serverEntryPoints[entryPointName] = serverEntryPoint{
+ serverEntryPoints[entryPointName] = &serverEntryPoint{
httpRouter: middlewares.NewHandlerSwitcher(router),
}
}
@@ -312,7 +327,7 @@ func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration)
// LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic
// provider configurations.
-func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (map[string]serverEntryPoint, error) {
+func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (map[string]*serverEntryPoint, error) {
serverEntryPoints := server.buildEntryPoints(globalConfiguration)
redirectHandlers := make(map[string]http.Handler)
@@ -328,6 +343,10 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = globalConfiguration.DefaultEntryPoints
}
+ if len(frontend.EntryPoints) == 0 {
+ log.Errorf("No entrypoint defined for frontend %s, defaultEntryPoints:%s. Skipping it", frontendName, globalConfiguration.DefaultEntryPoints)
+ continue
+ }
for _, entryPointName := range frontend.EntryPoints {
log.Debugf("Wiring frontend %s to entryPoint %s", frontendName, entryPointName)
if _, ok := serverEntryPoints[entryPointName]; !ok {
@@ -375,7 +394,9 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
return nil, err
}
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
- rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight))
+ if err := rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight)); err != nil {
+ return nil, err
+ }
}
case types.Wrr:
log.Debugf("Creating load-balancer wrr")
@@ -386,7 +407,9 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
return nil, err
}
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
- rr.UpsertServer(url, roundrobin.Weight(server.Weight))
+ if err := rr.UpsertServer(url, roundrobin.Weight(server.Weight)); err != nil {
+ return nil, err
+ }
}
}
var negroni = negroni.New()
diff --git a/traefik.sample.toml b/traefik.sample.toml
index efa330b6a..e0f05dfff 100644
--- a/traefik.sample.toml
+++ b/traefik.sample.toml
@@ -2,46 +2,6 @@
# Global configuration
################################################################
-# Entrypoints definition
-#
-# Optional
-# Default:
-# [entryPoints]
-# [entryPoints.http]
-# address = ":80"
-#
-# To redirect an http entrypoint to an https entrypoint (with SNI support):
-# [entryPoints]
-# [entryPoints.http]
-# address = ":80"
-# [entryPoints.http.redirect]
-# entryPoint = "https"
-# [entryPoints.https]
-# address = ":443"
-# [entryPoints.https.tls]
-# [[entryPoints.https.tls.certificates]]
-# CertFile = "integration/fixtures/https/snitest.com.cert"
-# KeyFile = "integration/fixtures/https/snitest.com.key"
-# [[entryPoints.https.tls.certificates]]
-# CertFile = "integration/fixtures/https/snitest.org.cert"
-# KeyFile = "integration/fixtures/https/snitest.org.key"
-#
-# To redirect an entrypoint rewriting the URL:
-# [entryPoints]
-# [entryPoints.http]
-# address = ":80"
-# [entryPoints.http.redirect]
-# regex = "^http://localhost/(.*)"
-# replacement = "http://mydomain/$1"
-
-# Entrypoints to be used by frontends that do not specify any entrypoint.
-# Each frontend can specify its own entrypoints.
-#
-# Optional
-# Default: ["http"]
-#
-# defaultEntryPoints = ["http", "https"]
-
# Timeout in seconds.
# Duration to give active requests a chance to finish during hot-reloads
#
@@ -87,6 +47,102 @@
#
# MaxIdleConnsPerHost = 200
+# Entrypoints to be used by frontends that do not specify any entrypoint.
+# Each frontend can specify its own entrypoints.
+#
+# Optional
+# Default: ["http"]
+#
+# defaultEntryPoints = ["http", "https"]
+
+# Enable ACME (Let's Encrypt): automatic SSL
+#
+# Optional
+#
+# [acme]
+
+# Email address used for registration
+#
+# Required
+#
+# email = "test@traefik.io"
+
+# File used for certificates storage.
+# WARNING, if you use Traefik in Docker, don't forget to mount this file as a volume.
+#
+# Required
+#
+# storageFile = "acme.json"
+
+# Entrypoint to proxy acme challenge to.
+# WARNING, must point to an entrypoint on port 443
+#
+# Required
+#
+# entryPoint = "https"
+
+# Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate.
+# WARNING, TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks.
+# WARNING, Take note that Let's Encrypt have rate limiting: https://community.letsencrypt.org/t/quick-start-guide/1631
+#
+# Optional
+#
+# onDemand = true
+
+# CA server to use
+# Uncomment the line to run on the staging let's encrypt server
+# Leave comment to go to prod
+#
+# Optional
+#
+# caServer = "https://acme-staging.api.letsencrypt.org/directory"
+
+# Domains list
+# You can provide SANs (alternative domains) to each main domain
+#
+# [[acme.domains]]
+# main = "local1.com"
+# sans = ["test1.local1.com", "test2.local1.com"]
+# [[acme.domains]]
+# main = "local2.com"
+# sans = ["test1.local2.com", "test2x.local2.com"]
+# [[acme.domains]]
+# main = "local3.com"
+# [[acme.domains]]
+# main = "local4.com"
+
+
+# Entrypoints definition
+#
+# Optional
+# Default:
+# [entryPoints]
+# [entryPoints.http]
+# address = ":80"
+#
+# To redirect an http entrypoint to an https entrypoint (with SNI support):
+# [entryPoints]
+# [entryPoints.http]
+# address = ":80"
+# [entryPoints.http.redirect]
+# entryPoint = "https"
+# [entryPoints.https]
+# address = ":443"
+# [entryPoints.https.tls]
+# [[entryPoints.https.tls.certificates]]
+# CertFile = "integration/fixtures/https/snitest.com.cert"
+# KeyFile = "integration/fixtures/https/snitest.com.key"
+# [[entryPoints.https.tls.certificates]]
+# CertFile = "integration/fixtures/https/snitest.org.cert"
+# KeyFile = "integration/fixtures/https/snitest.org.key"
+#
+# To redirect an entrypoint rewriting the URL:
+# [entryPoints]
+# [entryPoints.http]
+# address = ":80"
+# [entryPoints.http.redirect]
+# regex = "^http://localhost/(.*)"
+# replacement = "http://mydomain/$1"
################################################################
# Web configuration backend
diff --git a/webui/src/favicon.ico b/webui/src/favicon.ico
deleted file mode 100644
index e29818b9a..000000000
Binary files a/webui/src/favicon.ico and /dev/null differ
diff --git a/webui/src/index.html b/webui/src/index.html
index 5fad79c84..fbb832c71 100644
--- a/webui/src/index.html
+++ b/webui/src/index.html
@@ -2,9 +2,10 @@
- /ˈTræfɪk/
+ Træfɪk
+
@@ -29,7 +30,7 @@