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..8178c66f6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +- repo: git://github.com/pre-commit/pre-commit-hooks + sha: 97b88d9610bcc03982ddac33caba98bb2b751f5f + hooks: + - id: detect-private-key + +- repo: git://github.com/containous/pre-commit-hooks + sha: HEAD + hooks: + - id: goFmt + - id: goLint + \ No newline at end of file diff --git a/Makefile b/Makefile index 8d0079ef2..588d40123 100644 --- a/Makefile +++ b/Makefile @@ -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/acme.go b/acme.go new file mode 100644 index 000000000..d33fced92 --- /dev/null +++ b/acme.go @@ -0,0 +1,337 @@ +/* +Copyright +*/ +package main + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/containous/traefik/middlewares" + "github.com/gorilla/mux" + "github.com/xenolf/lego/acme" + "io/ioutil" + fmtlog "log" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "time" +) + +// ACMEAccount is used to store lets encrypt registration info +type ACMEAccount struct { + Email string + Registration *acme.RegistrationResource + PrivateKey []byte + CertificatesMap DomainsCertificates +} + +// DomainsCertificates stores a certificate for multiple domains +type DomainsCertificates []DomainsCertificate + +func (dc DomainsCertificates) getCertificateForDomain(domainToFind string) (*AcmeCertificate, bool) { + for _, domainsCertificate := range dc { + for _, domain := range domainsCertificate.Domains { + if domain == domainToFind { + return domainsCertificate.Certificate, true + } + } + } + return nil, false +} + +// DomainsCertificate contains a certificate for multiple domains +type DomainsCertificate struct { + Domains []string + Certificate *AcmeCertificate +} + +// GetEmail returns email +func (a ACMEAccount) GetEmail() string { + return a.Email +} + +// GetRegistration returns lets encrypt registration resource +func (a ACMEAccount) GetRegistration() *acme.RegistrationResource { + return a.Registration +} + +// GetPrivateKey returns private key +func (a ACMEAccount) 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 +} + +// AcmeCertificate is used to store certificate info +type AcmeCertificate struct { + Domain string + CertURL string + CertStableURL string + PrivateKey []byte + Certificate []byte +} + +func (a *ACME) createACMEConfig(router *middlewares.HandlerSwitcher, proxyRouter *middlewares.HandlerSwitcher) (*tls.Config, error) { + acme.Logger = fmtlog.New(ioutil.Discard, "", 0) + + if len(a.StorageFile) == 0 { + return nil, errors.New("Empty StorageFile, please provide a filenmae for certs storage") + } + + // if certificates in storage, load them + if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 { + // load account + acmeAccount, err := a.loadACMEAccount(a) + if err != nil { + return nil, err + } + + // build client + client, err := a.buildACMEClient(acmeAccount) + if err != nil { + return nil, err + } + config := &tls.Config{} + config.Certificates = []tls.Certificate{} + for _, certificateResource := range acmeAccount.CertificatesMap { + cert, err := tls.X509KeyPair(certificateResource.Certificate.Certificate, certificateResource.Certificate.PrivateKey) + if err != nil { + return nil, err + } + leaf, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return nil, err + } + // <= 30 days left, renew certificate + if leaf.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) { + 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 nil, err + } + log.Debugf("Renewed certificate %s", renewedCert.Domain) + certificateResource.Certificate = &AcmeCertificate{ + Domain: renewedCert.Domain, + CertURL: renewedCert.CertURL, + CertStableURL: renewedCert.CertStableURL, + PrivateKey: renewedCert.PrivateKey, + Certificate: renewedCert.Certificate, + } + if err = a.saveACMEAccount(acmeAccount); err != nil { + return nil, err + } + cert, err = tls.X509KeyPair(renewedCert.Certificate, renewedCert.PrivateKey) + if err != nil { + return nil, err + } + } + config.Certificates = append(config.Certificates, cert) + } + config.BuildNameToCertificate() + if a.OnDemand { + config.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if !router.GetHandler().Match(&http.Request{URL: &url.URL{}, Host: clientHello.ServerName}, &mux.RouteMatch{}) { + return nil, nil + } + return a.loadCertificateOnDemand(client, acmeAccount, clientHello, proxyRouter) + } + } + return config, nil + } + log.Infof("Loading ACME certificates...") + + // Create a user. New accounts need an email and private key to start + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, err + } + acmeAccount := &ACMEAccount{ + Email: a.Email, + PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), + } + + client, err := a.buildACMEClient(acmeAccount) + if err != nil { + return nil, err + } + + //client.SetTLSAddress(acmeConfig.TLSAddress) + // New users will need to register; be sure to save it + reg, err := client.Register() + if err != nil { + return nil, err + } + acmeAccount.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 nil, err + } + + config := &tls.Config{} + config.Certificates = []tls.Certificate{} + acmeAccount.CertificatesMap = []DomainsCertificate{} + + for _, domain := range a.Domains { + domains := append([]string{domain.Main}, domain.SANs...) + certificateResource, err := a.getDomainsCertificates(client, domains, proxyRouter) + if err != nil { + return nil, err + } + cert, err := tls.X509KeyPair(certificateResource.Certificate, certificateResource.PrivateKey) + if err != nil { + return nil, err + } + config.Certificates = append(config.Certificates, cert) + acmeAccount.CertificatesMap = append(acmeAccount.CertificatesMap, DomainsCertificate{Domains: domains, Certificate: certificateResource}) + } + // BuildNameToCertificate parses the CommonName and SubjectAlternateName fields + // in each certificate and populates the config.NameToCertificate map. + config.BuildNameToCertificate() + if a.OnDemand { + config.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if !router.GetHandler().Match(&http.Request{URL: &url.URL{}, Host: clientHello.ServerName}, &mux.RouteMatch{}) { + return nil, nil + } + return a.loadCertificateOnDemand(client, acmeAccount, clientHello, proxyRouter) + } + } + if err = a.saveACMEAccount(acmeAccount); err != nil { + return nil, err + } + return config, nil +} + +func (a *ACME) buildACMEClient(acmeAccount *ACMEAccount) (*acme.Client, error) { + + // A client facilitates communication with the CA server. This CA URL is + // configured for a local dev instance of Boulder running in Docker in a VM. + caServer := "https://acme-v01.api.letsencrypt.org/directory" + if len(a.CAServer) > 0 { + caServer = a.CAServer + } + client, err := acme.NewClient(caServer, acmeAccount, acme.RSA4096) + if err != nil { + return nil, err + } + + return client, nil +} + +// Ask the kernel for a free open port that is ready to use +func (a *ACME) getFreePort() (string, error) { + addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0") + if err != nil { + return "", err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return "", err + } + defer l.Close() + return l.Addr().String(), nil +} + +func (a *ACME) loadCertificateOnDemand(client *acme.Client, acmeAccount *ACMEAccount, clientHello *tls.ClientHelloInfo, proxyRouter *middlewares.HandlerSwitcher) (*tls.Certificate, error) { + if certificateResource, ok := acmeAccount.CertificatesMap.getCertificateForDomain(clientHello.ServerName); ok { + cert, err := tls.X509KeyPair(certificateResource.Certificate, certificateResource.PrivateKey) + if err != nil { + return nil, err + } + return &cert, nil + } + certificateResource, err := a.getDomainsCertificates(client, []string{clientHello.ServerName}, proxyRouter) + if err != nil { + return nil, err + } + log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName) + acmeAccount.CertificatesMap = append(acmeAccount.CertificatesMap, DomainsCertificate{Domains: []string{clientHello.ServerName}, Certificate: certificateResource}) + if err = a.saveACMEAccount(acmeAccount); err != nil { + return nil, err + } + cert, err := tls.X509KeyPair(certificateResource.Certificate, certificateResource.PrivateKey) + if err != nil { + return nil, err + } + return &cert, nil +} + +func (a *ACME) loadACMEAccount(acmeConfig *ACME) (*ACMEAccount, error) { + a.storageLock.Lock() + defer a.storageLock.Unlock() + acmeAccount := ACMEAccount{ + CertificatesMap: DomainsCertificates{}, + } + file, err := ioutil.ReadFile(acmeConfig.StorageFile) + if err != nil { + return nil, err + } + if err := json.Unmarshal(file, &acmeAccount); err != nil { + return nil, err + } + log.Infof("Loaded ACME config from storage %s", acmeConfig.StorageFile) + return &acmeAccount, nil +} + +func (a *ACME) saveACMEAccount(acmeAccount *ACMEAccount) error { + a.storageLock.Lock() + defer a.storageLock.Unlock() + // write account to file + data, err := json.MarshalIndent(acmeAccount, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(a.StorageFile, data, 0644) +} + +func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string, proxyRouter *middlewares.HandlerSwitcher) (*AcmeCertificate, error) { + var proxyRoute *mux.Route + proxyRoute = proxyRouter.GetHandler().Get("9141156b44763db2a504b8c63cf6f81c") + if proxyRoute == nil { + proxyRoute = proxyRouter.GetHandler().NewRoute().PathPrefix("/.well-known/acme-challenge/").Name("9141156b44763db2a504b8c63cf6f81c") + } + url, err := url.Parse("http://127.0.0.1:5002") + if err != nil { + return nil, err + } + reverseProxy := httputil.NewSingleHostReverseProxy(url) + proxyRoute.Handler(reverseProxy) + defer proxyRoute.Handler(http.NotFoundHandler()) + // The acme library takes care of completing the challenges to obtain the certificate(s). + // Of course, the hostnames must resolve to this machine or it will fail. + log.Debugf("Loading ACME certificates %s", domains) + bundle := false + client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) + client.SetHTTPAddress("127.0.0.1:5002") + certificate, failures := client.ObtainCertificate(domains, bundle, nil) + if len(failures) > 0 { + log.Error(failures) + return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures) + } + return &AcmeCertificate{ + Domain: certificate.Domain, + CertURL: certificate.CertURL, + CertStableURL: certificate.CertStableURL, + PrivateKey: certificate.PrivateKey, + Certificate: certificate.Certificate, + }, nil +} diff --git a/cmd.go b/cmd.go index e0639197c..f42303512 100644 --- a/cmd.go +++ b/cmd.go @@ -175,6 +175,7 @@ func init() { 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() { diff --git a/configuration.go b/configuration.go index c7e97a934..329cb3c25 100644 --- a/configuration.go +++ b/configuration.go @@ -12,6 +12,7 @@ import ( "github.com/containous/traefik/types" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + "sync" ) // GlobalConfiguration holds global configuration (with providers, etc.). @@ -22,6 +23,7 @@ type GlobalConfiguration struct { TraefikLogsFile string LogLevel string EntryPoints EntryPoints + ACME *ACME DefaultEntryPoints DefaultEntryPoints ProvidersThrottleDuration time.Duration MaxIdleConnsPerHost int @@ -140,6 +142,23 @@ type TLS struct { Certificates Certificates } +// 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.Mutex +} + +// Domain holds a domain name with SANs +type Domain struct { + Main string + SANs []string +} + // Certificates defines traefik certificates type type Certificates []Certificate @@ -244,6 +263,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..8e13b1643 100644 --- a/docs/index.md +++ b/docs/index.md @@ -177,46 +177,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 +222,45 @@ 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"] + +# 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" ``` 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/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/server.go b/server.go index a85eccbde..1e5f2b796 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(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,19 +224,21 @@ 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(tlsOption *TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) { if tlsOption == nil { return nil, nil } + if server.globalConfiguration.ACME != nil { + if acmeEntrypoint, ok := server.serverEntryPoints[server.globalConfiguration.ACME.EntryPoint]; ok { + return server.globalConfiguration.ACME.createACMEConfig(router, acmeEntrypoint.httpRouter) + } + return nil, errors.New("Unknown entrypoint " + server.globalConfiguration.ACME.EntryPoint + "for ACME configuration") + } 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 { @@ -250,30 +254,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(router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) { + log.Infof("Preparing server %+v", 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(entryPoint.TLS, router) if err != nil { log.Fatalf("Error creating TLS config %s", err) return nil, err @@ -299,11 +301,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 +314,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) diff --git a/traefik.sample.toml b/traefik.sample.toml index efa330b6a..27f6e822a 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,45 @@ # # 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"] + +# 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 @@