add let's encrypt support

Signed-off-by: Emile Vauge <emile@vauge.com>
This commit is contained in:
Emile Vauge 2016-02-25 18:30:13 +01:00
parent 087b68e14d
commit 6e484e5c2d
No known key found for this signature in database
GPG key ID: D808B4C167352E59
18 changed files with 556 additions and 172 deletions

View file

@ -37,14 +37,14 @@ traefik*
The idea behind `glide` is the following : 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`. (`go get …`) the dependencies in the `GOPATH`.
- if you need another dependency, import and use it in - if you need another dependency, import and use it in
the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in
`vendor` and add it to your `glide.yaml`. `vendor` and add it to your `glide.yaml`.
```bash ```bash
$ glide up --quick $ glide install
# generate # generate
$ go generate $ go generate
# Simple go build # Simple go build

2
.gitignore vendored
View file

@ -1,6 +1,7 @@
/dist /dist
gen.go gen.go
.idea .idea
.intellij
log log
*.iml *.iml
traefik traefik
@ -8,3 +9,4 @@ traefik.toml
*.test *.test
vendor/ vendor/
static/ static/
.vscode/

11
.pre-commit-config.yaml Normal file
View file

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

View file

@ -84,7 +84,7 @@ generate-webui: build-webui
fi fi
lint: lint:
$(foreach file,$(SRCS),golint $(file) || exit;) script/validate-golint
fmt: fmt:
gofmt -s -l -w $(SRCS) gofmt -s -l -w $(SRCS)

337
acme.go Normal file
View file

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

1
cmd.go
View file

@ -175,6 +175,7 @@ func init() {
viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost")) viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost"))
viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second)) viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second))
viper.SetDefault("logLevel", "ERROR") viper.SetDefault("logLevel", "ERROR")
viper.SetDefault("MaxIdleConnsPerHost", 200)
} }
func run() { func run() {

View file

@ -12,6 +12,7 @@ import (
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"github.com/spf13/viper" "github.com/spf13/viper"
"sync"
) )
// GlobalConfiguration holds global configuration (with providers, etc.). // GlobalConfiguration holds global configuration (with providers, etc.).
@ -22,6 +23,7 @@ type GlobalConfiguration struct {
TraefikLogsFile string TraefikLogsFile string
LogLevel string LogLevel string
EntryPoints EntryPoints EntryPoints EntryPoints
ACME *ACME
DefaultEntryPoints DefaultEntryPoints DefaultEntryPoints DefaultEntryPoints
ProvidersThrottleDuration time.Duration ProvidersThrottleDuration time.Duration
MaxIdleConnsPerHost int MaxIdleConnsPerHost int
@ -140,6 +142,23 @@ type TLS struct {
Certificates Certificates 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 // Certificates defines traefik certificates type
type Certificates []Certificate type Certificates []Certificate
@ -244,6 +263,7 @@ func LoadConfiguration() *GlobalConfiguration {
viper.Set("boltdb", arguments.Boltdb) viper.Set("boltdb", arguments.Boltdb)
} }
if err := unmarshal(&configuration); err != nil { if err := unmarshal(&configuration); err != nil {
fmtlog.Fatalf("Error reading file: %s", err) fmtlog.Fatalf("Error reading file: %s", err)
} }

View file

@ -177,46 +177,6 @@ Use "traefik [command] --help" for more information about a command.
# Global configuration # 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. # Timeout in seconds.
# Duration to give active requests a chance to finish during hot-reloads # 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 # 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"
``` ```

24
glide.lock generated
View file

@ -1,5 +1,5 @@
hash: 2a18c9cab231b5e108c666641c2436da3d9a1a0d9d1c586948af94271a47b317 hash: 6f5b6e92b805fed0bb6a5bfe411b5ca501bc04accebeb739cec039e6499271e2
updated: 2016-03-15T23:01:22.853471291+01:00 updated: 2016-03-16T13:22:21.850972237+01:00
imports: imports:
- name: github.com/alecthomas/template - name: github.com/alecthomas/template
version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
@ -165,14 +165,12 @@ imports:
version: 44874009257d4d47ba9806f1b7f72a32a015e4d8 version: 44874009257d4d47ba9806f1b7f72a32a015e4d8
- name: github.com/mailgun/manners - name: github.com/mailgun/manners
version: fada45142db3f93097ca917da107aa3fad0ffcb5 version: fada45142db3f93097ca917da107aa3fad0ffcb5
- name: github.com/mailgun/oxy
version: 8aaf36279137ac04ace3792a4f86098631b27d5a
subpackages:
- cbreaker
- name: github.com/mailgun/predicate - name: github.com/mailgun/predicate
version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
- name: github.com/mailgun/timetools - name: github.com/mailgun/timetools
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0 version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
- name: github.com/miekg/dns
version: b9171237b0642de1d8e8004f16869970e065f46b
- name: github.com/mitchellh/mapstructure - name: github.com/mitchellh/mapstructure
version: d2dd0262208475919e1a362f675cfc0e7c10e905 version: d2dd0262208475919e1a362f675cfc0e7c10e905
- name: github.com/opencontainers/runc - name: github.com/opencontainers/runc
@ -203,6 +201,11 @@ imports:
version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7 version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7
- name: github.com/spf13/viper - name: github.com/spf13/viper
version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
- name: github.com/square/go-jose
version: 70a7e670bd0d4bb35902d31f3a75a6689843abed
subpackages:
- cipher
- json
- name: github.com/stretchr/objx - name: github.com/stretchr/objx
version: cbeaeb16a013161a98496fad62933b1d21786672 version: cbeaeb16a013161a98496fad62933b1d21786672
- name: github.com/stretchr/testify - name: github.com/stretchr/testify
@ -233,10 +236,19 @@ imports:
- router - router
- name: github.com/wendal/errors - name: github.com/wendal/errors
version: f66c77a7882b399795a8987ebf87ef64a427417e 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 - name: golang.org/x/net
version: d9558e5c97f85372afee28cf2b6059d7d3818919 version: d9558e5c97f85372afee28cf2b6059d7d3818919
subpackages: subpackages:
- context - context
- publicsuffix
- name: golang.org/x/sys - name: golang.org/x/sys
version: eb2c74142fd19a79b3f237334c7384d5167b1b46 version: eb2c74142fd19a79b3f237334c7384d5167b1b46
subpackages: subpackages:

View file

@ -164,4 +164,4 @@ import:
- package: github.com/google/go-querystring/query - package: github.com/google/go-querystring/query
- package: github.com/vulcand/vulcand/plugin/rewrite - package: github.com/vulcand/vulcand/plugin/rewrite
- package: github.com/stretchr/testify/mock - package: github.com/stretchr/testify/mock
- package: github.com/xenolf/lego

View file

@ -34,7 +34,8 @@ type DockerTLS struct {
// Provide allows the provider to provide configurations to traefik // Provide allows the provider to provide configurations to traefik
// using the given configuration channel. // using the given configuration channel.
func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) error { func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) error {
go func() {
operation := func() error {
var dockerClient *docker.Client var dockerClient *docker.Client
var err error var err error
@ -57,12 +58,15 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) er
return err return err
} }
log.Debug("Docker connection established") log.Debug("Docker connection established")
configuration := provider.loadDockerConfig(listContainers(dockerClient))
configurationChan <- types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
if provider.Watch { if provider.Watch {
dockerEvents := make(chan *docker.APIEvents) dockerEvents := make(chan *docker.APIEvents)
dockerClient.AddEventListener(dockerEvents) dockerClient.AddEventListener(dockerEvents)
log.Debug("Docker listening") log.Debug("Docker listening")
go func() {
operation := func() error {
for { for {
event := <-dockerEvents event := <-dockerEvents
if event == nil { if event == nil {
@ -81,6 +85,8 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) er
} }
} }
} }
return nil
}
notify := func(err error, time time.Duration) { notify := func(err error, time time.Duration) {
log.Errorf("Docker connection error %+v, retrying in %s", err, time) log.Errorf("Docker connection error %+v, retrying in %s", err, time)
} }
@ -89,13 +95,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) er
log.Fatalf("Cannot connect to docker server %+v", err) log.Fatalf("Cannot connect to docker server %+v", err)
} }
}() }()
}
configuration := provider.loadDockerConfig(listContainers(dockerClient))
configurationChan <- types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
return nil return nil
} }

View file

@ -17,4 +17,4 @@ if [ -z "$DATE" ]; then
fi fi
# Build binaries # 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 .

View file

@ -32,5 +32,5 @@ fi
rm -f dist/traefik_* rm -f dist/traefik_*
# Build binaries # 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}}" -output="dist/traefik_{{.OS}}-{{.Arch}}"

View file

@ -34,7 +34,7 @@ var oxyLogger = &OxyLogger{}
// Server is the reverse-proxy/load-balancer engine // Server is the reverse-proxy/load-balancer engine
type Server struct { type Server struct {
serverEntryPoints map[string]serverEntryPoint serverEntryPoints serverEntryPoints
configurationChan chan types.ConfigMessage configurationChan chan types.ConfigMessage
configurationValidatedChan chan types.ConfigMessage configurationValidatedChan chan types.ConfigMessage
signals chan os.Signal signals chan os.Signal
@ -46,6 +46,8 @@ type Server struct {
loggerMiddleware *middlewares.Logger loggerMiddleware *middlewares.Logger
} }
type serverEntryPoints map[string]*serverEntryPoint
type serverEntryPoint struct { type serverEntryPoint struct {
httpServer *manners.GracefulServer httpServer *manners.GracefulServer
httpRouter *middlewares.HandlerSwitcher httpRouter *middlewares.HandlerSwitcher
@ -55,7 +57,7 @@ type serverEntryPoint struct {
func NewServer(globalConfiguration GlobalConfiguration) *Server { func NewServer(globalConfiguration GlobalConfiguration) *Server {
server := new(Server) server := new(Server)
server.serverEntryPoints = make(map[string]serverEntryPoint) server.serverEntryPoints = make(map[string]*serverEntryPoint)
server.configurationChan = make(chan types.ConfigMessage, 10) server.configurationChan = make(chan types.ConfigMessage, 10)
server.configurationValidatedChan = make(chan types.ConfigMessage, 10) server.configurationValidatedChan = make(chan types.ConfigMessage, 10)
server.signals = make(chan os.Signal, 1) 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. // Start starts the server and blocks until server is shutted down.
func (server *Server) Start() { func (server *Server) Start() {
server.startHTTPServers()
go server.listenProviders() go server.listenProviders()
go server.listenConfigurations() go server.listenConfigurations()
server.configureProviders() server.configureProviders()
@ -96,6 +99,19 @@ func (server *Server) Close() {
server.loggerMiddleware.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() { func (server *Server) listenProviders() {
lastReceivedConfiguration := time.Unix(0, 0) lastReceivedConfiguration := time.Unix(0, 0)
lastConfigs := make(map[string]*types.ConfigMessage) lastConfigs := make(map[string]*types.ConfigMessage)
@ -141,22 +157,8 @@ func (server *Server) listenConfigurations() {
if err == nil { if err == nil {
server.serverLock.Lock() server.serverLock.Lock()
for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints { for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints {
currentServerEntryPoint := server.serverEntryPoints[newServerEntryPointName] server.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler())
if currentServerEntryPoint.httpServer == nil { log.Infof("Server configurartion reloaded on %s", server.serverEntryPoints[newServerEntryPointName].httpServer.Addr)
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.currentConfigurations = newConfigurations server.currentConfigurations = newConfigurations
server.serverLock.Unlock() server.serverLock.Unlock()
@ -222,19 +224,21 @@ func (server *Server) listenSignals() {
} }
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI // 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 { if tlsOption == nil {
return nil, 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 { if len(tlsOption.Certificates) == 0 {
return nil, nil return nil, nil
} }
config := &tls.Config{} config := &tls.Config{}
if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"}
}
var err error var err error
config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates)) config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates))
for i, v := range 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) { 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 { if srv.TLSConfig != nil {
err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig) if err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig); err != nil {
if err != nil {
log.Fatal("Error creating server: ", err) log.Fatal("Error creating server: ", err)
} }
} else { } else {
err := srv.ListenAndServe() if err := srv.ListenAndServe(); err != nil {
if err != nil {
log.Fatal("Error creating server: ", err) log.Fatal("Error creating server: ", err)
} }
} }
log.Info("Server stopped") log.Info("Server stopped")
} }
func (server *Server) prepareServer(router http.Handler, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) { func (server *Server) prepareServer(router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
log.Info("Preparing server") log.Infof("Preparing server %+v", entryPoint)
// middlewares // middlewares
var negroni = negroni.New() var negroni = negroni.New()
for _, middleware := range middlewares { for _, middleware := range middlewares {
negroni.Use(middleware) negroni.Use(middleware)
} }
negroni.UseHandler(router) negroni.UseHandler(router)
tlsConfig, err := server.createTLSConfig(entryPoint.TLS) tlsConfig, err := server.createTLSConfig(entryPoint.TLS, router)
if err != nil { if err != nil {
log.Fatalf("Error creating TLS config %s", err) log.Fatalf("Error creating TLS config %s", err)
return nil, err return nil, err
@ -299,11 +301,11 @@ func (server *Server) prepareServer(router http.Handler, entryPoint *EntryPoint,
return gracefulServer, nil return gracefulServer, nil
} }
func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]serverEntryPoint { func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]*serverEntryPoint {
serverEntryPoints := make(map[string]serverEntryPoint) serverEntryPoints := make(map[string]*serverEntryPoint)
for entryPointName := range globalConfiguration.EntryPoints { for entryPointName := range globalConfiguration.EntryPoints {
router := server.buildDefaultHTTPRouter() router := server.buildDefaultHTTPRouter()
serverEntryPoints[entryPointName] = serverEntryPoint{ serverEntryPoints[entryPointName] = &serverEntryPoint{
httpRouter: middlewares.NewHandlerSwitcher(router), 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 // LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic
// provider configurations. // 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) serverEntryPoints := server.buildEntryPoints(globalConfiguration)
redirectHandlers := make(map[string]http.Handler) redirectHandlers := make(map[string]http.Handler)

View file

@ -2,46 +2,6 @@
# Global configuration # 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. # Timeout in seconds.
# Duration to give active requests a chance to finish during hot-reloads # Duration to give active requests a chance to finish during hot-reloads
# #
@ -87,6 +47,45 @@
# #
# MaxIdleConnsPerHost = 200 # 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 # Web configuration backend

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -2,9 +2,10 @@
<html ng-app="traefik"> <html ng-app="traefik">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>/ˈTræfɪk/</title> <title>Træfɪk</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<link rel="icon" type="image/png" href="traefik.icon.png" />
<!-- Place favicon.ico and apple-touch-icon.png in the root directory --> <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css({.tmp/serve,src}) styles/vendor.css --> <!-- build:css({.tmp/serve,src}) styles/vendor.css -->
@ -29,7 +30,7 @@
<nav class="navbar navbar-default"> <nav class="navbar navbar-default">
<div class="container-fluid"> <div class="container-fluid">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand traefik-text" ui-sref="provider">/ˈTr<span class="traefik-blue">æ</span>fɪk/</a> <a class="navbar-brand traefik-text" ui-sref="provider"><img src="traefik.icon.png"/></a>
</div> </div>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">

BIN
webui/src/traefik.icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB