mirror of
https://github.com/containous/traefik.git
synced 2025-01-11 05:17:52 +03:00
Merge pull request #224 from containous/add-lets-encrypt-suppport
Add let's encrypt support
This commit is contained in:
commit
6bfc849a24
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@ -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
2
.gitignore
vendored
@ -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/
|
10
.pre-commit-config.yaml
Normal file
10
.pre-commit-config.yaml
Normal file
@ -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
|
2
Makefile
2
Makefile
@ -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)
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/containous/traefik.svg?branch=master)](https://travis-ci.org/containous/traefik)
|
[![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)
|
[![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)
|
[![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)
|
- [It's fast](docs/index.md#benchmarks)
|
||||||
- No dependency hell, single binary made with go
|
- No dependency hell, single binary made with go
|
||||||
- Simple json Rest API
|
- Rest API
|
||||||
- Simple TOML file configuration
|
|
||||||
- Multiple backends supported: Docker, Mesos/Marathon, Consul, Etcd, and more to come
|
- 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
|
- Watchers for backends, can listen change in backends to apply a new configuration automatically
|
||||||
- Hot-reloading of configuration. No need to restart the process
|
- 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
|
- Rest Metrics
|
||||||
- Tiny docker image included [![Image Layers](https://badge.imagelayers.io/containous/traefik:latest.svg)](https://imagelayers.io/?images=containous/traefik:latest)
|
- 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 backends support
|
||||||
- SSL frontend support
|
- SSL frontend support (with SNI)
|
||||||
- Clean AngularJS Web UI
|
- Clean AngularJS Web UI
|
||||||
- Websocket support
|
- Websocket support
|
||||||
- HTTP/2 support
|
- HTTP/2 support
|
||||||
|
- [Let's Encrypt](https://letsencrypt.org) support (Automatic HTTPS)
|
||||||
|
|
||||||
## Demo
|
## 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
|
- [Gorilla mux](https://github.com/gorilla/mux): famous request router
|
||||||
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
|
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
|
||||||
- [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers
|
- [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
|
## Quick start
|
||||||
|
|
||||||
|
405
acme/acme.go
Normal file
405
acme/acme.go
Normal file
@ -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
|
||||||
|
}
|
56
acme/challengeProvider.go
Normal file
56
acme/challengeProvider.go
Normal file
@ -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
|
||||||
|
}
|
78
acme/crypto.go
Normal file
78
acme/crypto.go
Normal file
@ -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)
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
FROM golang:1.6.0-alpine
|
FROM golang:1.6.0-alpine
|
||||||
|
|
||||||
RUN apk update && apk add git bash gcc
|
RUN apk update && apk add git bash gcc musl-dev \
|
||||||
|
&& go get github.com/Masterminds/glide \
|
||||||
RUN go get github.com/Masterminds/glide
|
&& go get github.com/mitchellh/gox \
|
||||||
RUN go get github.com/mitchellh/gox
|
&& go get github.com/jteeuwen/go-bindata/... \
|
||||||
RUN go get github.com/jteeuwen/go-bindata/...
|
&& go get github.com/golang/lint/golint \
|
||||||
RUN go get github.com/golang/lint/golint
|
&& go get github.com/kisielk/errcheck
|
||||||
|
|
||||||
# Which docker version to test on
|
# Which docker version to test on
|
||||||
ENV DOCKER_VERSION 1.10.1
|
ENV DOCKER_VERSION 1.10.1
|
||||||
|
18
cmd.go
18
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.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")
|
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store")
|
||||||
|
|
||||||
viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
|
_ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
|
||||||
viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
|
_ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
|
||||||
//viper.BindPFlag("defaultEntryPoints", traefikCmd.PersistentFlags().Lookup("defaultEntryPoints"))
|
_ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
|
||||||
viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
|
|
||||||
// TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105
|
// TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105
|
||||||
viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
|
_ = viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
|
||||||
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() {
|
||||||
@ -196,7 +196,11 @@ func run() {
|
|||||||
|
|
||||||
if len(globalConfiguration.TraefikLogsFile) > 0 {
|
if len(globalConfiguration.TraefikLogsFile) > 0 {
|
||||||
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
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 {
|
if err != nil {
|
||||||
log.Fatal("Error opening file", err)
|
log.Fatal("Error opening file", err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/acme"
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
@ -22,6 +23,7 @@ type GlobalConfiguration struct {
|
|||||||
TraefikLogsFile string
|
TraefikLogsFile string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
EntryPoints EntryPoints
|
EntryPoints EntryPoints
|
||||||
|
ACME *acme.ACME
|
||||||
DefaultEntryPoints DefaultEntryPoints
|
DefaultEntryPoints DefaultEntryPoints
|
||||||
ProvidersThrottleDuration time.Duration
|
ProvidersThrottleDuration time.Duration
|
||||||
MaxIdleConnsPerHost int
|
MaxIdleConnsPerHost int
|
||||||
@ -92,7 +94,9 @@ func (ep *EntryPoints) Set(value string) error {
|
|||||||
var tls *TLS
|
var tls *TLS
|
||||||
if len(result["TLS"]) > 0 {
|
if len(result["TLS"]) > 0 {
|
||||||
certs := Certificates{}
|
certs := Certificates{}
|
||||||
certs.Set(result["TLS"])
|
if err := certs.Set(result["TLS"]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
tls = &TLS{
|
tls = &TLS{
|
||||||
Certificates: certs,
|
Certificates: certs,
|
||||||
}
|
}
|
||||||
@ -244,6 +248,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
254
docs/index.md
254
docs/index.md
@ -1,6 +1,6 @@
|
|||||||
![Træfɪk](http://traefik.github.io/traefik.logo.svg "Træfɪk")
|
<p align="center">
|
||||||
___
|
<img src="http://traefik.github.io/traefik.logo.svg" alt="Træfɪk" title="Træfɪk" />
|
||||||
|
</p>
|
||||||
|
|
||||||
# <a id="top"></a> Documentation
|
# <a id="top"></a> 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.
|
- `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.
|
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:
|
It can be configured using:
|
||||||
|
|
||||||
- Methods: `LatencyAtQuantileMS`, `NetworkErrorRatio`, `ResponseCodeRatio`
|
- Methods: `LatencyAtQuantileMS`, `NetworkErrorRatio`, `ResponseCodeRatio`
|
||||||
- Operators: `AND`, `OR`, `EQ`, `NEQ`, `LT`, `LE`, `GT`, `GE`
|
- Operators: `AND`, `OR`, `EQ`, `NEQ`, `LT`, `LE`, `GT`, `GE`
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
- `NetworkErrorRatio() > 0.5`
|
- `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window for a frontend
|
||||||
- `LatencyAtQuantileMS(50.0) > 50`
|
- `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds.
|
||||||
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`
|
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in range [500-600) to [0-600)
|
||||||
|
|
||||||
|
|
||||||
## <a id="launch"></a> Launch configuration
|
## <a id="launch"></a> Launch configuration
|
||||||
@ -177,46 +182,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 +227,203 @@ 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"]
|
||||||
|
|
||||||
|
# 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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
24
glide.lock
generated
24
glide.lock
generated
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -46,10 +46,9 @@ func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) {
|
|||||||
// TODO validate : run on 80
|
// TODO validate : run on 80
|
||||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||||
|
|
||||||
// Expected no response as we did not configure anything
|
// Expected a 404 as we did not configure anything
|
||||||
c.Assert(resp, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(err, checker.NotNil)
|
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||||
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SimpleSuite) TestWithWebConfig(c *check.C) {
|
func (s *SimpleSuite) TestWithWebConfig(c *check.C) {
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
|
||||||
checker "github.com/vdemeester/shakers"
|
checker "github.com/vdemeester/shakers"
|
||||||
check "gopkg.in/check.v1"
|
check "gopkg.in/check.v1"
|
||||||
)
|
)
|
||||||
@ -20,8 +19,7 @@ func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) {
|
|||||||
// TODO validate : run on 80
|
// TODO validate : run on 80
|
||||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||||
|
|
||||||
// Expected no response as we did not configure anything
|
// Expected a 404 as we did not configure anything
|
||||||
c.Assert(resp, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(err, checker.NotNil)
|
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||||
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
|
||||||
checker "github.com/vdemeester/shakers"
|
checker "github.com/vdemeester/shakers"
|
||||||
check "gopkg.in/check.v1"
|
check "gopkg.in/check.v1"
|
||||||
)
|
)
|
||||||
@ -20,8 +19,7 @@ func (s *EtcdSuite) TestSimpleConfiguration(c *check.C) {
|
|||||||
// TODO validate : run on 80
|
// TODO validate : run on 80
|
||||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||||
|
|
||||||
// Expected no response as we did not configure anything
|
// Expected a 404 as we did not configure anything
|
||||||
c.Assert(resp, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(err, checker.NotNil)
|
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||||
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
@ -20,8 +19,7 @@ func (s *MarathonSuite) TestSimpleConfiguration(c *check.C) {
|
|||||||
// TODO validate : run on 80
|
// TODO validate : run on 80
|
||||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||||
|
|
||||||
// Expected no response as we did not configure anything
|
// Expected a 404 as we did not configure anything
|
||||||
c.Assert(resp, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
c.Assert(err, checker.NotNil)
|
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||||
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 .
|
||||||
|
@ -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}}"
|
||||||
|
28
script/validate-errcheck
Executable file
28
script/validate-errcheck
Executable file
@ -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
|
111
server.go
111
server.go
@ -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(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() {
|
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,26 +224,41 @@ 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(entryPointName string, tlsOption *TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) {
|
||||||
if tlsOption == nil {
|
if tlsOption == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(tlsOption.Certificates) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &tls.Config{}
|
config := &tls.Config{}
|
||||||
if config.NextProtos == nil {
|
config.Certificates = []tls.Certificate{}
|
||||||
config.NextProtos = []string{"http/1.1"}
|
for _, v := range tlsOption.Certificates {
|
||||||
}
|
cert, err := tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
|
||||||
|
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// BuildNameToCertificate parses the CommonName and SubjectAlternateName fields
|
||||||
// in each certificate and populates the config.NameToCertificate map.
|
// 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) {
|
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(entryPointName string, router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
|
||||||
log.Info("Preparing server")
|
log.Infof("Preparing server %s %+v", entryPointName, 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(entryPointName, 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 +314,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 +327,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)
|
||||||
|
|
||||||
@ -328,6 +343,10 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
|||||||
if len(frontend.EntryPoints) == 0 {
|
if len(frontend.EntryPoints) == 0 {
|
||||||
frontend.EntryPoints = globalConfiguration.DefaultEntryPoints
|
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 {
|
for _, entryPointName := range frontend.EntryPoints {
|
||||||
log.Debugf("Wiring frontend %s to entryPoint %s", frontendName, entryPointName)
|
log.Debugf("Wiring frontend %s to entryPoint %s", frontendName, entryPointName)
|
||||||
if _, ok := serverEntryPoints[entryPointName]; !ok {
|
if _, ok := serverEntryPoints[entryPointName]; !ok {
|
||||||
@ -375,7 +394,9 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
|
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:
|
case types.Wrr:
|
||||||
log.Debugf("Creating load-balancer wrr")
|
log.Debugf("Creating load-balancer wrr")
|
||||||
@ -386,7 +407,9 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
|
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()
|
var negroni = negroni.New()
|
||||||
|
@ -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,102 @@
|
|||||||
#
|
#
|
||||||
# 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"]
|
||||||
|
|
||||||
|
# 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
|
# Web configuration backend
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB |
@ -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
BIN
webui/src/traefik.icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Loading…
Reference in New Issue
Block a user