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") -___ - +

+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 @@