Compare commits

...

26 Commits

Author SHA1 Message Date
Ludovic Fernandez
f9e9e11035 Prepare release v1.7.19 2019-10-28 14:58:04 +01:00
Daniel Tomcej
772c9ca4d5 Allow Default Certificate to work on macOS 10.15 2019-10-25 17:08:05 +02:00
Ismail Alidzhikov
208d0fa471 Update DaemonSet apiVersion 2019-10-20 23:50:04 +02:00
mikezhang
9f72b6d1d5 Add functions to support precise float compute of weight (#5663) 2019-10-16 17:29:21 +02:00
Ingo Gottwald
29ef007917 Backport Go 1.13 integration test fixes 2019-10-07 10:46:04 +02:00
Eliel Goncalves
590a0d67bb Fix Location response header http to https when SSL 2019-10-07 10:16:05 +02:00
mpl
74ad83f05a Prepare release v1.7.18 2019-09-26 15:46:05 +02:00
Ludovic Fernandez
d707c8ba93 Prepare release v1.7.17 2019-09-23 19:48:04 +02:00
Nicholas Wiersma
640eb62ca1 Avoid closing stdout when the accesslog handler is closed
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
Co-authored-by: jlevesy <julien.levesy@containo.us>
2019-09-23 14:50:06 +02:00
mpl
216710864e Actually send header and code during WriteHeader, if needed
Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
2019-09-20 18:42:03 +02:00
Brad Jones
226f20b626 Add note clarifying client certificate header 2019-09-19 09:06:03 +02:00
Ludovic Fernandez
151be83bce Update docs links. 2019-09-18 15:38:04 +02:00
Ludovic Fernandez
d1a8c7fa78 Update Traefik image version. 2019-09-17 14:12:04 +02:00
Ludovic Fernandez
254dc38c3d Prepare release v1.7.16 2019-09-13 15:04:04 +02:00
mpl
24d084d7e6 implement Flusher and Hijacker for codeCatcher 2019-09-13 14:32:03 +02:00
Ludovic Fernandez
df5f530058 Prepare release v1.7.15 2019-09-12 18:10:05 +02:00
mpl
753d173965 error pages: do not buffer response when it's not an error 2019-09-12 16:20:05 +02:00
Daniel Tomcej
ffd1f122de Add TLS minversion constraint 2019-09-12 14:48:05 +02:00
Piotr Majkrzak
f98b57fdf4 Fix wrong handling of insecure tls auth forward ingress annotation 2019-09-12 11:44:05 +02:00
Damien Duportal
2d37f08864 Improve Access Logs Documentation page 2019-09-11 18:14:03 +02:00
Nicholas Wiersma
a7dbcc282c Consider default cert domain in certificate store
Co-authored-by: Nicolas Mengin <nmengin.pro@gmail.com>
2019-09-11 17:46:04 +02:00
David Dymko
f4f62e7fb3 Update Acme doc - Vultr Wildcard & Root 2019-09-09 09:26:04 +02:00
mpl
4cae8bcb10 Finish kubernetes throttling refactoring 2019-08-31 05:10:04 -07:00
Ben Weissmann
bee370ec6b Throttle Kubernetes config refresh 2019-08-30 03:16:04 -07:00
pitan
f1d016b893 Typo in basic auth usersFile label consul-catalog 2019-08-21 01:36:03 -07:00
Erik Wegner
4defbbe848 Kubernetes support for Auth.HeaderField 2019-08-21 01:30:05 -07:00
47 changed files with 2919 additions and 181 deletions

View File

@@ -13,6 +13,7 @@ env:
- VERSION: $TRAVIS_TAG
- CODENAME: maroilles
- N_MAKE_JOBS: 2
- DOCS_VERIFY_SKIP: true
script:
- echo "Skipping tests... (Tests are executed on SemaphoreCI)"

View File

@@ -1,5 +1,57 @@
# Change Log
## [v1.7.19](https://github.com/containous/traefik/tree/v1.7.19) (2019-10-25)
[All Commits](https://github.com/containous/traefik/compare/v1.7.18...v1.7.19)
**Bug fixes:**
- **[k8s,k8s/ingress]** Add functions to support precise float compute of weight ([#5663](https://github.com/containous/traefik/pull/5663) by [rmrfself](https://github.com/rmrfself))
- **[middleware]** Fix Location response header http to https when SSL ([#5574](https://github.com/containous/traefik/pull/5574) by [elielgoncalves](https://github.com/elielgoncalves))
- **[tls]** Allow Default Certificate to work on macOS 10.15 ([#5662](https://github.com/containous/traefik/pull/5662) by [dtomcej](https://github.com/dtomcej))
**Documentation:**
- **[k8s]** Update DaemonSet apiVersion ([#5682](https://github.com/containous/traefik/pull/5682) by [ialidzhikov](https://github.com/ialidzhikov))
## [v1.7.18](https://github.com/containous/traefik/tree/v1.7.18) (2019-09-23)
[All Commits](https://github.com/containous/traefik/compare/v1.7.17...v1.7.18)
**Bug fixes:**
- **[go,security]** This version is compiled with [Go 1.12.10](https://groups.google.com/d/msg/golang-announce/cszieYyuL9Q/g4Z7pKaqAgAJ), which fixes a vulnerability in previous versions. See the [CVE](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16276) about it for more details.
## [v1.7.17](https://github.com/containous/traefik/tree/v1.7.17) (2019-09-23)
[All Commits](https://github.com/containous/traefik/compare/v1.7.16...v1.7.17)
**Bug fixes:**
- **[logs,middleware]** Avoid closing stdout when the accesslog handler is closed ([#5459](https://github.com/containous/traefik/pull/5459) by [nrwiersma](https://github.com/nrwiersma))
- **[middleware]** Actually send header and code during WriteHeader, if needed ([#5404](https://github.com/containous/traefik/pull/5404) by [mpl](https://github.com/mpl))
**Documentation:**
- **[k8s]** Add note clarifying client certificate header ([#5362](https://github.com/containous/traefik/pull/5362) by [bradjones1](https://github.com/bradjones1))
- **[webui]** Update docs links. ([#5412](https://github.com/containous/traefik/pull/5412) by [ldez](https://github.com/ldez))
- Update Traefik image version. ([#5399](https://github.com/containous/traefik/pull/5399) by [ldez](https://github.com/ldez))
## [v1.7.16](https://github.com/containous/traefik/tree/v1.7.16) (2019-09-13)
[All Commits](https://github.com/containous/traefik/compare/v1.7.15...v1.7.16)
**Bug fixes:**
- **[middleware,websocket]** implement Flusher and Hijacker for codeCatcher ([#5376](https://github.com/containous/traefik/pull/5376) by [mpl](https://github.com/mpl))
## [v1.7.15](https://github.com/containous/traefik/tree/v1.7.15) (2019-09-12)
[All Commits](https://github.com/containous/traefik/compare/v1.7.14...v1.7.15)
**Bug fixes:**
- **[authentication,k8s/ingress]** Kubernetes support for Auth.HeaderField ([#5235](https://github.com/containous/traefik/pull/5235) by [ErikWegner](https://github.com/ErikWegner))
- **[k8s,k8s/ingress]** Finish kubernetes throttling refactoring ([#5269](https://github.com/containous/traefik/pull/5269) by [mpl](https://github.com/mpl))
- **[k8s]** Throttle Kubernetes config refresh ([#4716](https://github.com/containous/traefik/pull/4716) by [benweissmann](https://github.com/benweissmann))
- **[k8s]** Fix wrong handling of insecure tls auth forward ingress annotation ([#5319](https://github.com/containous/traefik/pull/5319) by [majkrzak](https://github.com/majkrzak))
- **[middleware]** error pages: do not buffer response when it&#39;s not an error ([#5285](https://github.com/containous/traefik/pull/5285) by [mpl](https://github.com/mpl))
- **[tls]** Consider default cert domain in certificate store ([#5353](https://github.com/containous/traefik/pull/5353) by [nrwiersma](https://github.com/nrwiersma))
- **[tls]** Add TLS minversion constraint ([#5356](https://github.com/containous/traefik/pull/5356) by [dtomcej](https://github.com/dtomcej))
**Documentation:**
- **[acme]** Update Acme doc - Vultr Wildcard &amp; Root ([#5320](https://github.com/containous/traefik/pull/5320) by [ddymko](https://github.com/ddymko))
- **[consulcatalog]** Typo in basic auth usersFile label consul-catalog ([#5230](https://github.com/containous/traefik/pull/5230) by [pitan](https://github.com/pitan))
- **[logs]** Improve Access Logs Documentation page ([#5238](https://github.com/containous/traefik/pull/5238) by [dduportal](https://github.com/dduportal))
## [v1.7.14](https://github.com/containous/traefik/tree/v1.7.14) (2019-08-14)
[All Commits](https://github.com/containous/traefik/compare/v1.7.13...v1.7.14)

View File

@@ -158,7 +158,7 @@ Integration tests must be run from the `integration/` directory and require the
## Documentation
The [documentation site](https://docs.traefik.io/) is built with [mkdocs](https://mkdocs.org/)
The [documentation site](https://docs.traefik.io/v1.7/) is built with [mkdocs](https://mkdocs.org/)
### Building Documentation

12
Gopkg.lock generated
View File

@@ -1770,6 +1770,13 @@
pruneopts = "NUT"
revision = "1d7be4effb13d2d908342d349d71a284a7542693"
[[projects]]
digest = "1:9ca27be3cfd8a452f9814926a5842c6917289da8c21174ee0f9c79d84850b2ed"
name = "github.com/shopspring/decimal"
packages = ["."]
pruneopts = "NUT"
revision = "f1972eb1d1f519e2e5f4b51f2dea765e8c93a130"
[[projects]]
digest = "1:01252cd79aac70f16cac02a72a1067dd136e0ad6d5b597d0129cf74c739fd8d1"
name = "github.com/sirupsen/logrus"
@@ -1909,11 +1916,11 @@
[[projects]]
branch = "v1"
digest = "1:60888cead16f066c948c078258b27f2885dce91cb6aadacf545b62a1ae1d08cb"
digest = "1:819d4566276aed820b412b7e72683edfe99f53d2ac54e5b13eda197b523a369b"
name = "github.com/unrolled/secure"
packages = ["."]
pruneopts = "NUT"
revision = "a1cf62cc2159fff407728f118c41aece76c397fa"
revision = "232c938a6a69cfd83e26e2bfe100a20486d3a9a0"
[[projects]]
digest = "1:e84e99d5f369afaa9a5c41f55b57fa03047ecd3bac2a65861607882693ceea81"
@@ -2612,6 +2619,7 @@
"github.com/rancher/go-rancher-metadata/metadata",
"github.com/rancher/go-rancher/v2",
"github.com/ryanuber/go-glob",
"github.com/shopspring/decimal",
"github.com/sirupsen/logrus",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/mock",

View File

@@ -264,3 +264,11 @@
[[constraint]]
name = "github.com/google/uuid"
version = "0.2.0"
[[constraint]]
name = "github.com/shopspring/decimal"
revision = "f1972eb1d1f519e2e5f4b51f2dea765e8c93a130"
[[override]]
name = "contrib.go.opencensus.io/exporter/ocagent"
version = "0.4.12"

View File

@@ -4,7 +4,7 @@
</p>
[![Build Status SemaphoreCI](https://semaphoreci.com/api/v1/containous/traefik/branches/master/shields_badge.svg)](https://semaphoreci.com/containous/traefik)
[![Docs](https://img.shields.io/badge/docs-current-brightgreen.svg)](https://docs.traefik.io)
[![Docs](https://img.shields.io/badge/docs-current-brightgreen.svg)](https://docs.traefik.io/v1.7)
[![Go Report Card](https://goreportcard.com/badge/containous/traefik)](http://goreportcard.com/report/containous/traefik)
[![](https://images.microbadger.com/badges/image/traefik.svg)](https://microbadger.com/images/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md)
@@ -70,22 +70,22 @@ _(But if you'd rather configure some of your routes manually, Traefik supports t
## Supported Backends
- [Docker](https://docs.traefik.io/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/configuration/backends/docker#docker-swarm-mode)
- [Kubernetes](https://docs.traefik.io/configuration/backends/kubernetes)
- [Mesos](https://docs.traefik.io/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/configuration/backends/marathon)
- [Rancher](https://docs.traefik.io/configuration/backends/rancher) (API, Metadata)
- [Azure Service Fabric](https://docs.traefik.io/configuration/backends/servicefabric)
- [Consul Catalog](https://docs.traefik.io/configuration/backends/consulcatalog)
- [Consul](https://docs.traefik.io/configuration/backends/consul) / [Etcd](https://docs.traefik.io/configuration/backends/etcd) / [Zookeeper](https://docs.traefik.io/configuration/backends/zookeeper) / [BoltDB](https://docs.traefik.io/configuration/backends/boltdb)
- [Eureka](https://docs.traefik.io/configuration/backends/eureka)
- [Amazon ECS](https://docs.traefik.io/configuration/backends/ecs)
- [Amazon DynamoDB](https://docs.traefik.io/configuration/backends/dynamodb)
- [File](https://docs.traefik.io/configuration/backends/file)
- [Rest](https://docs.traefik.io/configuration/backends/rest)
- [Docker](https://docs.traefik.io/v1.7/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/v1.7/configuration/backends/docker#docker-swarm-mode)
- [Kubernetes](https://docs.traefik.io/v1.7/configuration/backends/kubernetes)
- [Mesos](https://docs.traefik.io/v1.7/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/v1.7/configuration/backends/marathon)
- [Rancher](https://docs.traefik.io/v1.7/configuration/backends/rancher) (API, Metadata)
- [Azure Service Fabric](https://docs.traefik.io/v1.7/configuration/backends/servicefabric)
- [Consul Catalog](https://docs.traefik.io/v1.7/configuration/backends/consulcatalog)
- [Consul](https://docs.traefik.io/v1.7/configuration/backends/consul) / [Etcd](https://docs.traefik.io/v1.7/configuration/backends/etcd) / [Zookeeper](https://docs.traefik.io/v1.7/configuration/backends/zookeeper) / [BoltDB](https://docs.traefik.io/v1.7/configuration/backends/boltdb)
- [Eureka](https://docs.traefik.io/v1.7/configuration/backends/eureka)
- [Amazon ECS](https://docs.traefik.io/v1.7/configuration/backends/ecs)
- [Amazon DynamoDB](https://docs.traefik.io/v1.7/configuration/backends/dynamodb)
- [File](https://docs.traefik.io/v1.7/configuration/backends/file)
- [Rest](https://docs.traefik.io/v1.7/configuration/backends/rest)
## Quickstart
To get your hands on Traefik, you can use the [5-Minute Quickstart](http://docs.traefik.io/#the-traefik-quickstart-using-docker) in our documentation (you will need Docker).
To get your hands on Traefik, you can use the [5-Minute Quickstart](http://docs.traefik.io/v1.7/#the-traefik-quickstart-using-docker) in our documentation (you will need Docker).
Alternatively, if you don't want to install anything on your computer, you can try Traefik online in this great [Katacoda tutorial](https://www.katacoda.com/courses/traefik/deploy-load-balancer) that shows how to load balance requests between multiple Docker containers.
@@ -100,7 +100,7 @@ You can access the simple HTML frontend of Traefik.
## Documentation
You can find the complete documentation at [https://docs.traefik.io](https://docs.traefik.io).
You can find the complete documentation at [https://docs.traefik.io/v1.7](https://docs.traefik.io/v1.7).
A collection of contributions around Traefik can be found at [https://awesome.traefik.io](https://awesome.traefik.io).
## Support

View File

@@ -1365,7 +1365,9 @@ var _templatesKubernetesTmpl = []byte(`[backends]
{{if $frontend.Auth }}
[frontends."{{ $frontendName }}".auth]
headerField = "X-WebAuth-User"
{{if $frontend.Auth.HeaderField }}
headerField = "{{ $frontend.Auth.HeaderField }}"
{{end}}
{{if $frontend.Auth.Basic }}
[frontends."{{ $frontendName }}".auth.basic]

View File

@@ -352,14 +352,14 @@ func stats(globalConfiguration *configuration.GlobalConfiguration) {
Stats collection is enabled.
Many thanks for contributing to Traefik's improvement by allowing us to receive anonymous information from your configuration.
Help us improve Traefik by leaving this feature on :)
More details on: https://docs.traefik.io/basics/#collected-data
More details on: https://docs.traefik.io/v1.7/basics/#collected-data
`)
collect(globalConfiguration)
} else {
log.Info(`
Stats collection is disabled.
Help us improve Traefik by turning this feature on :)
More details on: https://docs.traefik.io/basics/#collected-data
More details on: https://docs.traefik.io/v1.7/basics/#collected-data
`)
}
}

View File

@@ -212,6 +212,12 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
}
}
// Thanks to SSLv3 being enabled by mistake in golang 1.12,
// If no minVersion is set, apply TLS1.0 as the minimum.
if entryPoint.TLS != nil && len(entryPoint.TLS.MinVersion) == 0 {
entryPoint.TLS.MinVersion = "VersionTLS10"
}
if entryPoint.TLS != nil && entryPoint.TLS.DefaultCertificate == nil && len(entryPoint.TLS.Certificates) > 0 {
log.Infof("No tls.defaultCertificate given for %s: using the first item in tls.certificates as a fallback.", entryPointName)
entryPoint.TLS.DefaultCertificate = &entryPoint.TLS.Certificates[0]

View File

@@ -12,6 +12,7 @@ import (
"github.com/containous/traefik/provider"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/provider/file"
"github.com/containous/traefik/tls"
"github.com/stretchr/testify/assert"
)
@@ -269,3 +270,69 @@ func TestInitACMEProvider(t *testing.T) {
})
}
}
func TestSetEffectiveConfigurationTLSMinVersion(t *testing.T) {
testCases := []struct {
desc string
provided EntryPoint
expected EntryPoint
}{
{
desc: "Entrypoint with no TLS",
provided: EntryPoint{
Address: ":80",
},
expected: EntryPoint{
Address: ":80",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
},
},
{
desc: "Entrypoint with TLS Specifying MinVersion",
provided: EntryPoint{
Address: ":443",
TLS: &tls.TLS{
MinVersion: "VersionTLS12",
},
},
expected: EntryPoint{
Address: ":443",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
TLS: &tls.TLS{
MinVersion: "VersionTLS12",
},
},
},
{
desc: "Entrypoint with TLS without Specifying MinVersion",
provided: EntryPoint{
Address: ":443",
TLS: &tls.TLS{},
},
expected: EntryPoint{
Address: ":443",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
TLS: &tls.TLS{
MinVersion: "VersionTLS10",
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gc := &GlobalConfiguration{
EntryPoints: map[string]*EntryPoint{
"foo": &test.provided,
},
}
gc.SetEffectiveConfiguration(defaultConfigFile)
assert.Equal(t, &test.expected, gc.EntryPoints["foo"])
})
}
}

View File

@@ -1,6 +1,6 @@
[Unit]
Description=Traefik
Documentation=https://docs.traefik.io
Documentation=https://docs.traefik.io/v1.7
#After=network-online.target
#AssertFileIsExecutable=/usr/bin/traefik
#AssertPathExists=/etc/traefik/traefik.toml

View File

@@ -336,7 +336,7 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet |
| [Versio](https://www.versio.nl/domeinnamen) | `versio` | `VERSIO_USERNAME`, `VERSIO_PASSWORD` | YES |
| [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | YES |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | YES |
| [Zone.ee](https://www.zone.ee) | `zoneee` | `ZONEEE_API_USER`, `ZONEEE_API_KEY` | YES |
- (1): more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/)

View File

@@ -130,10 +130,10 @@ Additional settings can be defined using Consul Catalog tags.
| `<prefix>.frontend.auth.basic=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). |
| `<prefix>.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `<prefix>.frontend.auth.basic.users=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash`. |
| `<prefix>.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `<prefix>.frontend.auth.digest.users=EXPR` | Sets digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. |
| `<prefix>.frontend.auth.digest.usersfile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.digest.usersFile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. |
| `<prefix>.frontend.auth.forward.authResponseHeaders=EXPR` | Sets the forward authentication authResponseHeaders in CSV format: `X-Auth-User,X-Auth-Header` |
| `<prefix>.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. |

View File

@@ -73,6 +73,14 @@ See also [Kubernetes user guide](/user-guide/kubernetes).
#
# enablePassTLSCert = true
# Throttle how frequently we refresh our configuration from Ingresses when there
# are frequent changes.
#
# Optional
# Default: 0 (no throttling)
#
# throttleDuration = 10s
# Override default configuration template.
#
# Optional
@@ -210,10 +218,14 @@ infos:
serialnumber: true
```
If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value.
If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value.
If at least one flag of the `infos` part is set, it will add a `X-Forwarded-Tls-Client-Cert-Infos` header that contains an escaped string composed of the client certificate data selected by the infos flags.
This infos part is composed like the following example (not escaped):
```Subject="C=FR,ST=SomeState,L=Lyon,O=Cheese,CN=*.cheese.org",NB=1531900816,NA=1563436816,SAN=*.cheese.org,*.cheese.net,cheese.in,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2```
```
Subject="C=FR,ST=SomeState,L=Lyon,O=Cheese,CN=*.cheese.org",NB=1531900816,NA=1563436816,SAN=*.cheese.org,*.cheese.net,cheese.in,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2
```
Note these options work only with certificates issued by CAs included in the applicable [EntryPoint ClientCA section](/configuration/entrypoints/#tls-mutual-authentication); certificates from other CAs are not parsed or passed through as-is.
<4> `traefik.ingress.kubernetes.io/rate-limit` example:
@@ -231,7 +243,7 @@ rateset:
```
<5> `traefik.ingress.kubernetes.io/rule-type`
Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`.
Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`.
<6> `traefik.ingress.kubernetes.io/service-weights`:
Service weights enable to split traffic across multiple backing services in a fine-grained manner.

View File

@@ -97,7 +97,7 @@ In compose file the entrypoint syntax is different. Notice how quotes are used:
```yaml
traefik:
image: traefik
image: traefik:v1.7
command:
- --defaultentrypoints=powpow
- "--entryPoints=Name:powpow Address::42 Compress:true"
@@ -105,7 +105,7 @@ traefik:
or
```yaml
traefik:
image: traefik
image: traefik:v1.7
command: --defaultentrypoints=powpow --entryPoints='Name:powpow Address::42 Compress:true'
```

View File

@@ -105,10 +105,10 @@ logLevel = "ERROR"
## Access Logs
Access logs are written when `[accessLog]` is defined.
By default it will write to stdout and produce logs in the textual [Common Log Format (CLF)](#clf-common-log-format), extended with additional fields.
Access logs are written when the entry `[accessLog]` is defined (or the command line flag `--accesslog`).
By default it writes to stdout and produces logs in the textual [Common Log Format (CLF)](#clf-common-log-format), extended with additional fields.
To enable access logs using the default settings just add the `[accessLog]` entry:
To enable access logs using the default settings, add the `[accessLog]` entry in your `traefik.toml` configuration file:
```toml
[accessLog]
@@ -175,21 +175,41 @@ format = "json" # Default: "common"
minDuration = "10ms"
```
To customize logs format, you must switch to the JSON format:
### CLF - Common Log Format
By default, Traefik use the CLF (`common`) as access log format.
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
```
### Customize Fields
You can customize the fields written in the access logs.
The list of available fields is found below: [List of All Available Fields](#list-of-all-available-fields).
Each field has a "mode" which defines if it is written or not in the access log lines.
The possible values for the mode are:
* `keep`: the field and its value are written on the access log line. This is the default behavior.
* `drop`: the field is not written at all on the access log.
To customize the fields, you must:
* Switch to the JSON format (mandatory)
* Define the "default mode" for all fields (default is `keep`)
* OR Define the fields which does not follow the default mode
```toml
[accessLog]
filePath = "/path/to/access.log"
format = "json" # Default: "common"
[accessLog.filters]
# statusCodes keep only access logs with status codes in the specified range
#
# Optional
# Default: []
#
statusCodes = ["200", "300-302"]
# Access Log Format
#
# Optional
# Default: "common"
#
# Accepted values "common", "json"
#
format = "json"
[accessLog.fields]
@@ -206,6 +226,43 @@ format = "json" # Default: "common"
[accessLog.fields.names]
"ClientUsername" = "drop"
# ...
```
### Customize Headers
Access logs prints the headers of each request, as fields of the access log line.
You can customize which and how the headers are printed, likewise the other fields (see ["Customize Fields" section](#customize-fields)).
Each header has a "mode" which defines how it is written in the access log lines.
The possible values for the mode are:
* `keep`: the header and its value are written on the access log line. This is the default behavior.
* `drop`: the header is not written at all on the access log.
* `redacted`: the header is written, but its value is redacted to avoid leaking sensitive information.
To customize the headers, you must:
* Switch to the JSON format (mandatory)
* Define the "default mode" for all headers (default is `keep`)
* OR Define the headers which does not follow the default mode
!!! important
The headers are written with the prefix `request_` in the access log.
This prefix must not be included when specifying a header in the TOML configuration.
* Do: `"User-Agent" = "drop"`
* Don't: `"redacted_User-Agent" = "drop"`
```toml
[accessLog]
# Access Log Format
#
# Optional
# Default: "common"
#
# Accepted values "common", "json"
#
format = "json"
[accessLog.fields.headers]
# defaultMode
@@ -224,7 +281,7 @@ format = "json" # Default: "common"
# ...
```
### List of all available fields
### List of All Available Fields
| Field | Description |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -259,6 +316,8 @@ format = "json" # Default: "common"
| `Overhead` | The processing time overhead caused by Traefik. |
| `RetryAttempts` | The amount of attempts the request was retried. |
### Depreciation Notice
Deprecated way (before 1.4):
!!! danger "DEPRECATED"
@@ -272,14 +331,6 @@ Deprecated way (before 1.4):
accessLogsFile = "log/access.log"
```
### CLF - Common Log Format
By default, Traefik use the CLF (`common`) as access log format.
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
```
## Log Rotation
Traefik will close and reopen its log files, assuming they're configured, on receipt of a USR1 signal.

View File

@@ -77,7 +77,7 @@ version: '3'
services:
reverse-proxy:
image: traefik # The official Traefik docker image
image: traefik:v1.7 # The official Traefik docker image
command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports:
- "80:80" # The HTTP port

View File

@@ -91,7 +91,7 @@ To watch docker events, add `--docker.watch`.
version: "3"
services:
traefik:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command:
- "--api"
- "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https"
@@ -156,7 +156,7 @@ The initializer in a docker-compose file will be:
```yaml
traefik_init:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command:
- "storeconfig"
- "--api"
@@ -177,7 +177,7 @@ And now, the Traefik part will only have the Consul configuration.
```yaml
traefik:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
depends_on:
- traefik_init
- consul
@@ -200,7 +200,7 @@ The new configuration will be stored in Consul, and you need to restart the Trae
version: "3.4"
services:
traefik_init:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command:
- "storeconfig"
- "--api"
@@ -229,7 +229,7 @@ services:
depends_on:
- consul
traefik:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
depends_on:
- traefik_init
- consul

View File

@@ -50,7 +50,7 @@ version: '2'
services:
traefik:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
restart: always
ports:
- 80:80

View File

@@ -118,7 +118,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http
@@ -180,7 +180,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http

View File

@@ -139,7 +139,7 @@ Here is the [docker-compose file](https://docs.docker.com/compose/compose-file/)
```yaml
traefik:
image: traefik:<stable version from https://hub.docker.com/_/traefik>
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command: --consul --consul.endpoint=127.0.0.1:8500
ports:
- "80:80"

View File

@@ -1,5 +1,5 @@
traefik:
image: traefik
image: traefik:v1.7
command: --api --rancher --rancher.domain=rancher.localhost --rancher.endpoint=http://example.com --rancher.accesskey=XXXXXXX --rancher.secretkey=YYYYYY --logLevel=DEBUG
ports:
- "80:80"

View File

@@ -1,5 +1,5 @@
traefik:
image: traefik
image: traefik:v1.7
command: -c /dev/null --api --docker --docker.domain=docker.localhost --logLevel=DEBUG
ports:
- "80:80"

View File

@@ -26,7 +26,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http

View File

@@ -6,13 +6,17 @@ metadata:
namespace: kube-system
---
kind: DaemonSet
apiVersion: extensions/v1beta1
apiVersion: apps/v1
metadata:
name: traefik-ingress-controller
namespace: kube-system
labels:
k8s-app: traefik-ingress-lb
spec:
selector:
matchLabels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
template:
metadata:
labels:
@@ -22,7 +26,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http

View File

@@ -13,7 +13,7 @@ version: '3'
services:
reverse-proxy:
image: traefik # The official Traefik docker image
image: traefik:v1.7 # The official Traefik docker image
command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports:
- "80:80" # The HTTP port
@@ -101,7 +101,7 @@ IP: 172.27.0.4
### 4 — Enjoy Traefik's Magic
Now that you have a basic understanding of how Traefik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/) and let Traefik work for you!
Whatever your infrastructure is, there is probably [an available Traefik backend](https://docs.traefik.io/#supported-backends) that will do the job.
Now that you have a basic understanding of how Traefik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/v1.7/) and let Traefik work for you!
Whatever your infrastructure is, there is probably [an available Traefik backend](https://docs.traefik.io/v1.7/#supported-backends) that will do the job.
Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Traefik's let's encrypt integration](https://docs.traefik.io/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/).
Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Traefik's let's encrypt integration](https://docs.traefik.io/v1.7/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](https://docs.traefik.io/v1.7/user-guide/docker-and-lets-encrypt/).

View File

@@ -3,7 +3,7 @@ version: '3'
services:
# The reverse proxy service (Traefik)
reverse-proxy:
image: traefik # The official Traefik docker image
image: traefik:v1.7 # The official Traefik docker image
command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports:
- "80:80" # The HTTP port

View File

@@ -341,24 +341,32 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAs(c *check.
err = try.GetRequest("http://127.0.0.1:8080/api/providers", 500*time.Millisecond, try.BodyContains("Host:snitest.org"))
c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil)
c.Assert(err, checker.IsNil)
req.Host = "snitest.com"
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: "snitest.com",
Certificates: []tls.Certificate{},
}
client := http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 1 * time.Second,
}
// Connection without client certificate should fail
_, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server"))
_, err = client.Do(req)
c.Assert(err, checker.NotNil)
// Connect with client signed by ca1
cert, err := tls.LoadX509KeyPair("fixtures/https/clientca/client1.crt", "fixtures/https/clientca/client1.key")
c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key"))
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server"))
conn.Close()
_, err = client.Do(req)
c.Assert(err, checker.IsNil)
// Connect with client signed by ca2
tlsConfig = &tls.Config{
@@ -370,10 +378,13 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAs(c *check.
c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key"))
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
conn, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server"))
client = http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 1 * time.Second,
}
conn.Close()
_, err = client.Do(req)
c.Assert(err, checker.IsNil)
// Connect with client signed by ca3 should fail
tlsConfig = &tls.Config{
@@ -385,8 +396,13 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAs(c *check.
c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key"))
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
_, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server"))
client = http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 1 * time.Second,
}
_, err = client.Do(req)
c.Assert(err, checker.NotNil)
}
// TestWithClientCertificateAuthentication
@@ -402,24 +418,32 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAsMultipleFi
err = try.GetRequest("http://127.0.0.1:8080/api/providers", 1000*time.Millisecond, try.BodyContains("Host:snitest.org"))
c.Assert(err, checker.IsNil)
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil)
c.Assert(err, checker.IsNil)
req.Host = "snitest.com"
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: "snitest.com",
Certificates: []tls.Certificate{},
}
client := http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 1 * time.Second,
}
// Connection without client certificate should fail
_, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server"))
_, err = client.Do(req)
c.Assert(err, checker.NotNil)
// Connect with client signed by ca1
cert, err := tls.LoadX509KeyPair("fixtures/https/clientca/client1.crt", "fixtures/https/clientca/client1.key")
c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key"))
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server"))
conn.Close()
_, err = client.Do(req)
c.Assert(err, checker.IsNil)
// Connect with client signed by ca2
tlsConfig = &tls.Config{
@@ -431,9 +455,13 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAsMultipleFi
c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key"))
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
conn, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server"))
conn.Close()
client = http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 1 * time.Second,
}
_, err = client.Do(req)
c.Assert(err, checker.IsNil)
// Connect with client signed by ca3 should fail
tlsConfig = &tls.Config{
@@ -445,8 +473,13 @@ func (s *HTTPSSuite) TestWithClientCertificateAuthenticationMultipeCAsMultipleFi
c.Assert(err, checker.IsNil, check.Commentf("unable to load client certificate and key"))
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
_, err = tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.NotNil, check.Commentf("should not be allowed to connect to server"))
client = http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 1 * time.Second,
}
_, err = client.Do(req)
c.Assert(err, checker.NotNil)
}
func (s *HTTPSSuite) TestWithRootCAsContentForHTTPSOnBackend(c *check.C) {

View File

@@ -25,11 +25,6 @@ var host = flag.Bool("host", false, "run host integration tests")
var showLog = flag.Bool("tlog", false, "always show Traefik logs")
func Test(t *testing.T) {
check.TestingT(t)
}
func init() {
flag.Parse()
if !*integration {
log.Info("Integration tests disabled.")
return
@@ -70,6 +65,8 @@ func init() {
check.Suite(&ProxyProtocolSuite{})
check.Suite(&Etcd3Suite{})
}
check.TestingT(t)
}
var traefikBinary = "../dist/traefik"

View File

@@ -3,6 +3,7 @@ package accesslog
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
@@ -32,6 +33,19 @@ const (
JSONFormat = "json"
)
type noopCloser struct {
*os.File
}
func (n noopCloser) Write(p []byte) (int, error) {
return n.File.Write(p)
}
func (n noopCloser) Close() error {
// noop
return nil
}
type logHandlerParams struct {
logDataTable *LogData
crr *captureRequestReader
@@ -42,7 +56,7 @@ type logHandlerParams struct {
type LogHandler struct {
config *types.AccessLog
logger *logrus.Logger
file *os.File
file io.WriteCloser
mu sync.Mutex
httpCodeRanges types.HTTPCodeRanges
logHandlerChan chan logHandlerParams
@@ -51,7 +65,7 @@ type LogHandler struct {
// NewLogHandler creates a new LogHandler
func NewLogHandler(config *types.AccessLog) (*LogHandler, error) {
file := os.Stdout
var file io.WriteCloser = noopCloser{os.Stdout}
if len(config.FilePath) > 0 {
f, err := openAccessLogFile(config.FilePath)
if err != nil {
@@ -205,14 +219,15 @@ func (l *LogHandler) Close() error {
// Rotate closes and reopens the log file to allow for rotation
// by an external source.
func (l *LogHandler) Rotate() error {
var err error
if l.file != nil {
defer func(f *os.File) {
f.Close()
}(l.file)
if l.config.FilePath == "" {
return nil
}
if l.file != nil {
defer func(f io.Closer) { _ = f.Close() }(l.file)
}
var err error
l.file, err = os.OpenFile(l.config.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
if err != nil {
return err

View File

@@ -19,7 +19,10 @@ import (
)
// Compile time validation that the response recorder implements http interfaces correctly.
var _ middlewares.Stateful = &responseRecorderWithCloseNotify{}
var (
_ middlewares.Stateful = &responseRecorderWithCloseNotify{}
_ middlewares.Stateful = &codeCatcherWithCloseNotify{}
)
// Handler is a middleware that provides the custom error pages
type Handler struct {
@@ -74,25 +77,29 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
return
}
recorder := newResponseRecorder(w)
next.ServeHTTP(recorder, req)
catcher := newCodeCatcher(w, h.httpCodeRanges)
next.ServeHTTP(catcher, req)
if !catcher.isFilteredCode() {
return
}
// check the recorder code against the configured http status code ranges
code := catcher.getCode()
for _, block := range h.httpCodeRanges {
if recorder.GetCode() >= block[0] && recorder.GetCode() <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", recorder.GetCode())
if code >= block[0] && code <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", code)
var query string
if len(h.backendQuery) > 0 {
query = "/" + strings.TrimPrefix(h.backendQuery, "/")
query = strings.Replace(query, "{status}", strconv.Itoa(recorder.GetCode()), -1)
query = strings.Replace(query, "{status}", strconv.Itoa(code), -1)
}
pageReq, err := newRequest(h.backendURL + query)
if err != nil {
log.Error(err)
w.WriteHeader(recorder.GetCode())
fmt.Fprint(w, http.StatusText(recorder.GetCode()))
w.WriteHeader(code)
fmt.Fprint(w, http.StatusText(code))
return
}
@@ -102,16 +109,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
h.backendHandler.ServeHTTP(recorderErrorPage, pageReq.WithContext(req.Context()))
utils.CopyHeaders(w.Header(), recorderErrorPage.Header())
w.WriteHeader(recorder.GetCode())
w.WriteHeader(code)
w.Write(recorderErrorPage.GetBody().Bytes())
return
}
}
// did not catch a configured status code so proceed with the request
utils.CopyHeaders(w.Header(), recorder.Header())
w.WriteHeader(recorder.GetCode())
w.Write(recorder.GetBody().Bytes())
}
func newRequest(baseURL string) (*http.Request, error) {
@@ -129,6 +131,133 @@ func newRequest(baseURL string) (*http.Request, error) {
return req, nil
}
type responseInterceptor interface {
http.ResponseWriter
http.Flusher
getCode() int
isFilteredCode() bool
}
// codeCatcher is a response writer that detects as soon as possible whether the
// response is a code within the ranges of codes it watches for. If it is, it
// simply drops the data from the response. Otherwise, it forwards it directly to
// the original client (its responseWriter) without any buffering.
type codeCatcher struct {
headerMap http.Header
code int
httpCodeRanges types.HTTPCodeRanges
firstWrite bool
caughtFilteredCode bool
responseWriter http.ResponseWriter
headersSent bool
err error
}
type codeCatcherWithCloseNotify struct {
*codeCatcher
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone away.
func (cc *codeCatcherWithCloseNotify) CloseNotify() <-chan bool {
return cc.responseWriter.(http.CloseNotifier).CloseNotify()
}
func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) responseInterceptor {
catcher := &codeCatcher{
headerMap: make(http.Header),
code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200.
responseWriter: rw,
httpCodeRanges: httpCodeRanges,
firstWrite: true,
}
if _, ok := rw.(http.CloseNotifier); ok {
return &codeCatcherWithCloseNotify{catcher}
}
return catcher
}
func (cc *codeCatcher) Header() http.Header {
if cc.headerMap == nil {
cc.headerMap = make(http.Header)
}
return cc.headerMap
}
func (cc *codeCatcher) getCode() int {
return cc.code
}
// isFilteredCode returns whether the codeCatcher received a response code among the ones it is watching,
// and for which the response should be deferred to the error handler.
func (cc *codeCatcher) isFilteredCode() bool {
return cc.caughtFilteredCode
}
func (cc *codeCatcher) Write(buf []byte) (int, error) {
if !cc.firstWrite {
if cc.caughtFilteredCode {
// We don't care about the contents of the response,
// since we want to serve the ones from the error page,
// so we just drop them.
return len(buf), nil
}
return cc.responseWriter.Write(buf)
}
cc.firstWrite = false
// If WriteHeader was already called from the caller, this is a NOOP.
// Otherwise, cc.code is actually a 200 here.
cc.WriteHeader(cc.code)
if cc.caughtFilteredCode {
return len(buf), nil
}
return cc.responseWriter.Write(buf)
}
func (cc *codeCatcher) WriteHeader(code int) {
if cc.headersSent || cc.caughtFilteredCode {
return
}
cc.code = code
for _, block := range cc.httpCodeRanges {
if cc.code >= block[0] && cc.code <= block[1] {
cc.caughtFilteredCode = true
break
}
}
// it will be up to the other response recorder to send the headers,
// so it is out of our hands now.
if cc.caughtFilteredCode {
return
}
utils.CopyHeaders(cc.responseWriter.Header(), cc.Header())
cc.responseWriter.WriteHeader(cc.code)
cc.headersSent = true
}
// Hijack hijacks the connection
func (cc *codeCatcher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cc.responseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", cc.responseWriter)
}
// Flush sends any buffered data to the client.
func (cc *codeCatcher) Flush() {
// If WriteHeader was already called from the caller, this is a NOOP.
// Otherwise, cc.code is actually a 200 here.
cc.WriteHeader(cc.code)
if flusher, ok := cc.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
type responseRecorder interface {
http.ResponseWriter
http.Flusher

View File

@@ -34,6 +34,30 @@ func TestHandler(t *testing.T) {
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusOK))
},
},
{
desc: "no error, but not a 200",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
backendCode: http.StatusPartialContent,
backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "My error page.")
}),
validate: func(t *testing.T, recorder *httptest.ResponseRecorder) {
assert.Equal(t, http.StatusPartialContent, recorder.Code, "HTTP status")
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusPartialContent))
},
},
{
desc: "a 304, so no Write called",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
backendCode: http.StatusNotModified,
backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "whatever, should not be called")
}),
validate: func(t *testing.T, recorder *httptest.ResponseRecorder) {
assert.Equal(t, http.StatusNotModified, recorder.Code, "HTTP status")
assert.Contains(t, recorder.Body.String(), "")
},
},
{
desc: "in the range",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
@@ -108,6 +132,9 @@ func TestHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.backendCode)
if test.backendCode == http.StatusNotModified {
return
}
fmt.Fprintln(w, http.StatusText(test.backendCode))
})

View File

@@ -16,6 +16,7 @@ import (
"time"
"github.com/cenk/backoff"
"github.com/containous/flaeg"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
@@ -68,6 +69,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for" export:"true"`
IngressEndpoint *IngressEndpoint `description:"Kubernetes Ingress Endpoint"`
ThrottleDuration flaeg.Duration `description:"Ingress refresh throttle duration"`
lastConfiguration safe.Safe
}
@@ -137,16 +139,29 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
return nil
}
}
throttleDuration := time.Duration(p.ThrottleDuration)
throttledChan := throttleEvents(throttleDuration, stop, eventsChan)
if throttledChan != nil {
eventsChan = throttledChan
}
for {
select {
case <-stop:
return nil
case event := <-eventsChan:
// Note that event is the *first* event that came in during this
// throttling interval -- if we're hitting our throttle, we may have
// dropped events. This is fine, because we don't treat different
// event types differently. But if we do in the future, we'll need to
// track more information about the dropped events.
log.Debugf("Received Kubernetes event kind %T", event)
templateObjects, err := p.loadIngresses(k8sClient)
if err != nil {
return err
}
if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) {
log.Debugf("Skipping Kubernetes event kind %T", event)
} else {
@@ -156,6 +171,11 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
Configuration: p.loadConfig(*templateObjects),
}
}
// If we're throttling, we sleep here for the throttle duration to
// enforce that we don't refresh faster than our throttle. time.Sleep
// returns immediately if p.ThrottleDuration is 0 (no throttle).
time.Sleep(throttleDuration)
}
}
}
@@ -599,6 +619,39 @@ func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, tem
return nil
}
func throttleEvents(throttleDuration time.Duration, stop chan bool, eventsChan <-chan interface{}) chan interface{} {
if throttleDuration == 0 {
return nil
}
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling)
eventsChanBuffered := make(chan interface{}, 1)
// Run a goroutine that reads events from eventChan and does a
// non-blocking write to pendingEvent. This guarantees that writing to
// eventChan will never block, and that pendingEvent will have
// something in it if there's been an event since we read from that channel.
go func() {
for {
select {
case <-stop:
return
case nextEvent := <-eventsChan:
select {
case eventsChanBuffered <- nextEvent:
default:
// We already have an event in eventsChanBuffered, so we'll
// do a refresh as soon as our throttle allows us to. It's fine
// to drop the event and keep whatever's in the buffer -- we
// don't do different things for different events
log.Debugf("Dropping event kind %T due to throttling", nextEvent)
}
}
}
}()
return eventsChanBuffered
}
func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) {
if len(pa.Path) == 0 {
return "", nil
@@ -940,12 +993,12 @@ func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*type
}
authSecretName := getStringValue(i.Annotations, annotationKubernetesAuthForwardTLSSecret, "")
if len(authSecretName) > 0 {
authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %s", err)
}
authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %s", err)
}
if authSecretCert != "" || authSecretKey != "" {
forwardAuth.TLS = &types.ClientTLS{
Cert: authSecretCert,
Key: authSecretKey,
@@ -953,10 +1006,20 @@ func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*type
}
}
if forwardAuth.TLS == nil && label.Has(i.Annotations, getAnnotationName(i.Annotations, annotationKubernetesAuthForwardTLSInsecure)) {
forwardAuth.TLS = &types.ClientTLS{
InsecureSkipVerify: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTLSInsecure, false),
}
}
return forwardAuth, nil
}
func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) {
if len(secretName) == 0 {
return "", "", nil
}
secret, exists, err := k8sClient.GetSecret(namespace, secretName)
if err != nil {
return "", "", fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err)

View File

@@ -3,6 +3,8 @@ package kubernetes
import (
"strconv"
"strings"
"github.com/shopspring/decimal"
)
const defaultPercentageValuePrecision = 3
@@ -43,5 +45,6 @@ func newPercentageValueFromString(rawValue string) (percentageValue, error) {
// newPercentageValueFromFloat64 reads percentage value from float64
func newPercentageValueFromFloat64(f float64) percentageValue {
return percentageValue(f * (1000 * 100))
value := decimal.NewFromFloat(f).Mul(decimal.NewFromFloat(1000 * 100))
return percentageValue(value.IntPart())
}

View File

@@ -74,46 +74,54 @@ func TestNewPercentageValueFromString(t *testing.T) {
}{
{
value: "1%",
expectError: false,
expectedString: "1.000%",
expectedFloat64: 0.01,
},
{
value: "0.5",
expectError: false,
expectedString: "0.500%",
expectedFloat64: 0.005,
},
{
value: "99%",
expectError: false,
expectedString: "99.000%",
expectedFloat64: 0.99,
},
{
value: "99.9%",
expectError: false,
expectedString: "99.900%",
expectedFloat64: 0.999,
},
{
value: "-99.9%",
expectError: false,
expectedString: "-99.900%",
expectedFloat64: -0.999,
},
{
value: "-99.99999%",
expectError: false,
expectedString: "-99.999%",
expectedFloat64: -0.99999,
},
{
value: "0%",
expectError: false,
expectedString: "0.000%",
expectedFloat64: 0,
},
{
value: "2.3%",
expectedString: "2.300%",
expectedFloat64: 0.023,
},
{
value: "5.1%",
expectedString: "5.100%",
expectedFloat64: 0.051,
},
{
value: "83.85%",
expectedString: "83.850%",
expectedFloat64: 0.83850,
},
{
value: "%",
expectError: true,

View File

@@ -59,7 +59,9 @@
{{if $frontend.Auth }}
[frontends."{{ $frontendName }}".auth]
headerField = "X-WebAuth-User"
{{if $frontend.Auth.HeaderField }}
headerField = "{{ $frontend.Auth.HeaderField }}"
{{end}}
{{if $frontend.Auth.Basic }}
[frontends."{{ $frontendName }}".auth.basic]

View File

@@ -2,6 +2,7 @@ package tls
import (
"crypto/tls"
"crypto/x509"
"net"
"sort"
"strings"
@@ -47,6 +48,11 @@ func (c CertificateStore) GetAllDomains() []string {
allCerts = append(allCerts, domains)
}
}
// Get Default certificate
if c.DefaultCertificate != nil {
allCerts = append(allCerts, getCertificateDomains(c.DefaultCertificate)...)
}
return allCerts
}
@@ -115,6 +121,27 @@ func (c CertificateStore) ResetCache() {
}
}
func getCertificateDomains(cert *tls.Certificate) []string {
if cert == nil {
return nil
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil
}
var names []string
if len(x509Cert.Subject.CommonName) > 0 {
names = append(names, x509Cert.Subject.CommonName)
}
for _, san := range x509Cert.DNSNames {
names = append(names, san)
}
return names
}
// MatchDomain return true if a domain match the cert domain
func MatchDomain(domain string, certDomain string) bool {
if domain == certDomain {

View File

@@ -13,6 +13,90 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetAllDomains(t *testing.T) {
testCases := []struct {
desc string
staticCert string
dynamicCert string
defaultCert string
expectedDomains []string
}{
{
desc: "Empty Store, returns no domains",
staticCert: "",
dynamicCert: "",
defaultCert: "",
expectedDomains: nil,
},
{
desc: "Static cert domains",
staticCert: "snitest.com",
dynamicCert: "",
defaultCert: "",
expectedDomains: []string{"snitest.com"},
},
{
desc: "Dynamic cert domains",
staticCert: "",
dynamicCert: "snitest.com",
defaultCert: "",
expectedDomains: []string{"snitest.com"},
},
{
desc: "Default cert domains",
staticCert: "",
dynamicCert: "",
defaultCert: "snitest.com",
expectedDomains: []string{"snitest.com"},
},
{
desc: "All domains",
staticCert: "www.snitest.com",
dynamicCert: "*.snitest.com",
defaultCert: "snitest.com",
expectedDomains: []string{"www.snitest.com", "*.snitest.com", "snitest.com"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
staticMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert, false)
require.NoError(t, err)
staticMap[strings.ToLower(test.staticCert)] = cert
}
dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, false)
require.NoError(t, err)
dynamicMap[strings.ToLower(test.dynamicCert)] = cert
}
var defaultCert *tls.Certificate
if test.defaultCert != "" {
cert, err := loadTestCert(test.defaultCert, false)
require.NoError(t, err)
defaultCert = cert
}
store := &CertificateStore{
DynamicCerts: safe.New(dynamicMap),
StaticCerts: safe.New(staticMap),
DefaultCertificate: defaultCert,
CertCache: cache.New(1*time.Hour, 10*time.Minute),
}
actual := store.GetAllDomains()
assert.Equal(t, test.expectedDomains, actual)
})
}
}
func TestGetBestCertificate(t *testing.T) {
testCases := []struct {
desc string
@@ -116,15 +200,15 @@ func TestGetBestCertificate(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
staticMap := map[string]*tls.Certificate{}
dynamicMap := map[string]*tls.Certificate{}
staticMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert, test.uppercase)
require.NoError(t, err)
staticMap[strings.ToLower(test.staticCert)] = cert
}
dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, test.uppercase)
require.NoError(t, err)

View File

@@ -86,6 +86,7 @@ func derCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]by
NotAfter: expiration,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageDataEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{domain},
}

45
vendor/github.com/shopspring/decimal/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,45 @@
The MIT License (MIT)
Copyright (c) 2015 Spring, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
- Based on https://github.com/oguzbilgic/fpd, which has the following license:
"""
The MIT License (MIT)
Copyright (c) 2013 Oguz Bilgic
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

414
vendor/github.com/shopspring/decimal/decimal-go.go generated vendored Normal file
View File

@@ -0,0 +1,414 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Multiprecision decimal numbers.
// For floating-point formatting only; not general purpose.
// Only operations are assign and (binary) left/right shift.
// Can do binary floating point in multiprecision decimal precisely
// because 2 divides 10; cannot do decimal floating point
// in multiprecision binary precisely.
package decimal
type decimal struct {
d [800]byte // digits, big-endian representation
nd int // number of digits used
dp int // decimal point
neg bool // negative flag
trunc bool // discarded nonzero digits beyond d[:nd]
}
func (a *decimal) String() string {
n := 10 + a.nd
if a.dp > 0 {
n += a.dp
}
if a.dp < 0 {
n += -a.dp
}
buf := make([]byte, n)
w := 0
switch {
case a.nd == 0:
return "0"
case a.dp <= 0:
// zeros fill space between decimal point and digits
buf[w] = '0'
w++
buf[w] = '.'
w++
w += digitZero(buf[w : w+-a.dp])
w += copy(buf[w:], a.d[0:a.nd])
case a.dp < a.nd:
// decimal point in middle of digits
w += copy(buf[w:], a.d[0:a.dp])
buf[w] = '.'
w++
w += copy(buf[w:], a.d[a.dp:a.nd])
default:
// zeros fill space between digits and decimal point
w += copy(buf[w:], a.d[0:a.nd])
w += digitZero(buf[w : w+a.dp-a.nd])
}
return string(buf[0:w])
}
func digitZero(dst []byte) int {
for i := range dst {
dst[i] = '0'
}
return len(dst)
}
// trim trailing zeros from number.
// (They are meaningless; the decimal point is tracked
// independent of the number of digits.)
func trim(a *decimal) {
for a.nd > 0 && a.d[a.nd-1] == '0' {
a.nd--
}
if a.nd == 0 {
a.dp = 0
}
}
// Assign v to a.
func (a *decimal) Assign(v uint64) {
var buf [24]byte
// Write reversed decimal in buf.
n := 0
for v > 0 {
v1 := v / 10
v -= 10 * v1
buf[n] = byte(v + '0')
n++
v = v1
}
// Reverse again to produce forward decimal in a.d.
a.nd = 0
for n--; n >= 0; n-- {
a.d[a.nd] = buf[n]
a.nd++
}
a.dp = a.nd
trim(a)
}
// Maximum shift that we can do in one pass without overflow.
// A uint has 32 or 64 bits, and we have to be able to accommodate 9<<k.
const uintSize = 32 << (^uint(0) >> 63)
const maxShift = uintSize - 4
// Binary shift right (/ 2) by k bits. k <= maxShift to avoid overflow.
func rightShift(a *decimal, k uint) {
r := 0 // read pointer
w := 0 // write pointer
// Pick up enough leading digits to cover first shift.
var n uint
for ; n>>k == 0; r++ {
if r >= a.nd {
if n == 0 {
// a == 0; shouldn't get here, but handle anyway.
a.nd = 0
return
}
for n>>k == 0 {
n = n * 10
r++
}
break
}
c := uint(a.d[r])
n = n*10 + c - '0'
}
a.dp -= r - 1
var mask uint = (1 << k) - 1
// Pick up a digit, put down a digit.
for ; r < a.nd; r++ {
c := uint(a.d[r])
dig := n >> k
n &= mask
a.d[w] = byte(dig + '0')
w++
n = n*10 + c - '0'
}
// Put down extra digits.
for n > 0 {
dig := n >> k
n &= mask
if w < len(a.d) {
a.d[w] = byte(dig + '0')
w++
} else if dig > 0 {
a.trunc = true
}
n = n * 10
}
a.nd = w
trim(a)
}
// Cheat sheet for left shift: table indexed by shift count giving
// number of new digits that will be introduced by that shift.
//
// For example, leftcheats[4] = {2, "625"}. That means that
// if we are shifting by 4 (multiplying by 16), it will add 2 digits
// when the string prefix is "625" through "999", and one fewer digit
// if the string prefix is "000" through "624".
//
// Credit for this trick goes to Ken.
type leftCheat struct {
delta int // number of new digits
cutoff string // minus one digit if original < a.
}
var leftcheats = []leftCheat{
// Leading digits of 1/2^i = 5^i.
// 5^23 is not an exact 64-bit floating point number,
// so have to use bc for the math.
// Go up to 60 to be large enough for 32bit and 64bit platforms.
/*
seq 60 | sed 's/^/5^/' | bc |
awk 'BEGIN{ print "\t{ 0, \"\" }," }
{
log2 = log(2)/log(10)
printf("\t{ %d, \"%s\" },\t// * %d\n",
int(log2*NR+1), $0, 2**NR)
}'
*/
{0, ""},
{1, "5"}, // * 2
{1, "25"}, // * 4
{1, "125"}, // * 8
{2, "625"}, // * 16
{2, "3125"}, // * 32
{2, "15625"}, // * 64
{3, "78125"}, // * 128
{3, "390625"}, // * 256
{3, "1953125"}, // * 512
{4, "9765625"}, // * 1024
{4, "48828125"}, // * 2048
{4, "244140625"}, // * 4096
{4, "1220703125"}, // * 8192
{5, "6103515625"}, // * 16384
{5, "30517578125"}, // * 32768
{5, "152587890625"}, // * 65536
{6, "762939453125"}, // * 131072
{6, "3814697265625"}, // * 262144
{6, "19073486328125"}, // * 524288
{7, "95367431640625"}, // * 1048576
{7, "476837158203125"}, // * 2097152
{7, "2384185791015625"}, // * 4194304
{7, "11920928955078125"}, // * 8388608
{8, "59604644775390625"}, // * 16777216
{8, "298023223876953125"}, // * 33554432
{8, "1490116119384765625"}, // * 67108864
{9, "7450580596923828125"}, // * 134217728
{9, "37252902984619140625"}, // * 268435456
{9, "186264514923095703125"}, // * 536870912
{10, "931322574615478515625"}, // * 1073741824
{10, "4656612873077392578125"}, // * 2147483648
{10, "23283064365386962890625"}, // * 4294967296
{10, "116415321826934814453125"}, // * 8589934592
{11, "582076609134674072265625"}, // * 17179869184
{11, "2910383045673370361328125"}, // * 34359738368
{11, "14551915228366851806640625"}, // * 68719476736
{12, "72759576141834259033203125"}, // * 137438953472
{12, "363797880709171295166015625"}, // * 274877906944
{12, "1818989403545856475830078125"}, // * 549755813888
{13, "9094947017729282379150390625"}, // * 1099511627776
{13, "45474735088646411895751953125"}, // * 2199023255552
{13, "227373675443232059478759765625"}, // * 4398046511104
{13, "1136868377216160297393798828125"}, // * 8796093022208
{14, "5684341886080801486968994140625"}, // * 17592186044416
{14, "28421709430404007434844970703125"}, // * 35184372088832
{14, "142108547152020037174224853515625"}, // * 70368744177664
{15, "710542735760100185871124267578125"}, // * 140737488355328
{15, "3552713678800500929355621337890625"}, // * 281474976710656
{15, "17763568394002504646778106689453125"}, // * 562949953421312
{16, "88817841970012523233890533447265625"}, // * 1125899906842624
{16, "444089209850062616169452667236328125"}, // * 2251799813685248
{16, "2220446049250313080847263336181640625"}, // * 4503599627370496
{16, "11102230246251565404236316680908203125"}, // * 9007199254740992
{17, "55511151231257827021181583404541015625"}, // * 18014398509481984
{17, "277555756156289135105907917022705078125"}, // * 36028797018963968
{17, "1387778780781445675529539585113525390625"}, // * 72057594037927936
{18, "6938893903907228377647697925567626953125"}, // * 144115188075855872
{18, "34694469519536141888238489627838134765625"}, // * 288230376151711744
{18, "173472347597680709441192448139190673828125"}, // * 576460752303423488
{19, "867361737988403547205962240695953369140625"}, // * 1152921504606846976
}
// Is the leading prefix of b lexicographically less than s?
func prefixIsLessThan(b []byte, s string) bool {
for i := 0; i < len(s); i++ {
if i >= len(b) {
return true
}
if b[i] != s[i] {
return b[i] < s[i]
}
}
return false
}
// Binary shift left (* 2) by k bits. k <= maxShift to avoid overflow.
func leftShift(a *decimal, k uint) {
delta := leftcheats[k].delta
if prefixIsLessThan(a.d[0:a.nd], leftcheats[k].cutoff) {
delta--
}
r := a.nd // read index
w := a.nd + delta // write index
// Pick up a digit, put down a digit.
var n uint
for r--; r >= 0; r-- {
n += (uint(a.d[r]) - '0') << k
quo := n / 10
rem := n - 10*quo
w--
if w < len(a.d) {
a.d[w] = byte(rem + '0')
} else if rem != 0 {
a.trunc = true
}
n = quo
}
// Put down extra digits.
for n > 0 {
quo := n / 10
rem := n - 10*quo
w--
if w < len(a.d) {
a.d[w] = byte(rem + '0')
} else if rem != 0 {
a.trunc = true
}
n = quo
}
a.nd += delta
if a.nd >= len(a.d) {
a.nd = len(a.d)
}
a.dp += delta
trim(a)
}
// Binary shift left (k > 0) or right (k < 0).
func (a *decimal) Shift(k int) {
switch {
case a.nd == 0:
// nothing to do: a == 0
case k > 0:
for k > maxShift {
leftShift(a, maxShift)
k -= maxShift
}
leftShift(a, uint(k))
case k < 0:
for k < -maxShift {
rightShift(a, maxShift)
k += maxShift
}
rightShift(a, uint(-k))
}
}
// If we chop a at nd digits, should we round up?
func shouldRoundUp(a *decimal, nd int) bool {
if nd < 0 || nd >= a.nd {
return false
}
if a.d[nd] == '5' && nd+1 == a.nd { // exactly halfway - round to even
// if we truncated, a little higher than what's recorded - always round up
if a.trunc {
return true
}
return nd > 0 && (a.d[nd-1]-'0')%2 != 0
}
// not halfway - digit tells all
return a.d[nd] >= '5'
}
// Round a to nd digits (or fewer).
// If nd is zero, it means we're rounding
// just to the left of the digits, as in
// 0.09 -> 0.1.
func (a *decimal) Round(nd int) {
if nd < 0 || nd >= a.nd {
return
}
if shouldRoundUp(a, nd) {
a.RoundUp(nd)
} else {
a.RoundDown(nd)
}
}
// Round a down to nd digits (or fewer).
func (a *decimal) RoundDown(nd int) {
if nd < 0 || nd >= a.nd {
return
}
a.nd = nd
trim(a)
}
// Round a up to nd digits (or fewer).
func (a *decimal) RoundUp(nd int) {
if nd < 0 || nd >= a.nd {
return
}
// round up
for i := nd - 1; i >= 0; i-- {
c := a.d[i]
if c < '9' { // can stop after this digit
a.d[i]++
a.nd = i + 1
return
}
}
// Number is all 9s.
// Change to single 1 with adjusted decimal point.
a.d[0] = '1'
a.nd = 1
a.dp++
}
// Extract integer part, rounded appropriately.
// No guarantees about overflow.
func (a *decimal) RoundedInteger() uint64 {
if a.dp > 20 {
return 0xFFFFFFFFFFFFFFFF
}
var i int
n := uint64(0)
for i = 0; i < a.dp && i < a.nd; i++ {
n = n*10 + uint64(a.d[i]-'0')
}
for ; i < a.dp; i++ {
n *= 10
}
if shouldRoundUp(a, a.dp) {
n++
}
return n
}

1438
vendor/github.com/shopspring/decimal/decimal.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

47
vendor/github.com/shopspring/decimal/decomposer.go generated vendored Normal file
View File

@@ -0,0 +1,47 @@
package decimal
import (
"fmt"
"math/big"
)
// Decompose returns the internal decimal state into parts.
// If the provided buf has sufficient capacity, buf may be returned as the coefficient with
// the value set and length set as appropriate.
func (d Decimal) Decompose(buf []byte) (form byte, negative bool, coefficient []byte, exponent int32) {
negative = d.value.Sign() < 0
exponent = d.exp
coefficient = d.value.Bytes()
return
}
const (
decomposeFinite = 0
decomposeInfinite = 1
decomposeNaN = 2
)
// Compose sets the internal decimal value from parts. If the value cannot be
// represented then an error should be returned.
func (d *Decimal) Compose(form byte, negative bool, coefficient []byte, exponent int32) error {
switch form {
default:
return fmt.Errorf("unknown form: %v", form)
case decomposeFinite:
// Set rest of finite form below.
case decomposeInfinite:
return fmt.Errorf("Infinite form not supported")
case decomposeNaN:
return fmt.Errorf("NaN form not supported")
}
// Finite form.
if d.value == nil {
d.value = &big.Int{}
}
d.value.SetBytes(coefficient)
if negative && d.value.Sign() >= 0 {
d.value.Neg(d.value)
}
d.exp = exponent
return nil
}

118
vendor/github.com/shopspring/decimal/rounding.go generated vendored Normal file
View File

@@ -0,0 +1,118 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Multiprecision decimal numbers.
// For floating-point formatting only; not general purpose.
// Only operations are assign and (binary) left/right shift.
// Can do binary floating point in multiprecision decimal precisely
// because 2 divides 10; cannot do decimal floating point
// in multiprecision binary precisely.
package decimal
type floatInfo struct {
mantbits uint
expbits uint
bias int
}
var float32info = floatInfo{23, 8, -127}
var float64info = floatInfo{52, 11, -1023}
// roundShortest rounds d (= mant * 2^exp) to the shortest number of digits
// that will let the original floating point value be precisely reconstructed.
func roundShortest(d *decimal, mant uint64, exp int, flt *floatInfo) {
// If mantissa is zero, the number is zero; stop now.
if mant == 0 {
d.nd = 0
return
}
// Compute upper and lower such that any decimal number
// between upper and lower (possibly inclusive)
// will round to the original floating point number.
// We may see at once that the number is already shortest.
//
// Suppose d is not denormal, so that 2^exp <= d < 10^dp.
// The closest shorter number is at least 10^(dp-nd) away.
// The lower/upper bounds computed below are at distance
// at most 2^(exp-mantbits).
//
// So the number is already shortest if 10^(dp-nd) > 2^(exp-mantbits),
// or equivalently log2(10)*(dp-nd) > exp-mantbits.
// It is true if 332/100*(dp-nd) >= exp-mantbits (log2(10) > 3.32).
minexp := flt.bias + 1 // minimum possible exponent
if exp > minexp && 332*(d.dp-d.nd) >= 100*(exp-int(flt.mantbits)) {
// The number is already shortest.
return
}
// d = mant << (exp - mantbits)
// Next highest floating point number is mant+1 << exp-mantbits.
// Our upper bound is halfway between, mant*2+1 << exp-mantbits-1.
upper := new(decimal)
upper.Assign(mant*2 + 1)
upper.Shift(exp - int(flt.mantbits) - 1)
// d = mant << (exp - mantbits)
// Next lowest floating point number is mant-1 << exp-mantbits,
// unless mant-1 drops the significant bit and exp is not the minimum exp,
// in which case the next lowest is mant*2-1 << exp-mantbits-1.
// Either way, call it mantlo << explo-mantbits.
// Our lower bound is halfway between, mantlo*2+1 << explo-mantbits-1.
var mantlo uint64
var explo int
if mant > 1<<flt.mantbits || exp == minexp {
mantlo = mant - 1
explo = exp
} else {
mantlo = mant*2 - 1
explo = exp - 1
}
lower := new(decimal)
lower.Assign(mantlo*2 + 1)
lower.Shift(explo - int(flt.mantbits) - 1)
// The upper and lower bounds are possible outputs only if
// the original mantissa is even, so that IEEE round-to-even
// would round to the original mantissa and not the neighbors.
inclusive := mant%2 == 0
// Now we can figure out the minimum number of digits required.
// Walk along until d has distinguished itself from upper and lower.
for i := 0; i < d.nd; i++ {
l := byte('0') // lower digit
if i < lower.nd {
l = lower.d[i]
}
m := d.d[i] // middle digit
u := byte('0') // upper digit
if i < upper.nd {
u = upper.d[i]
}
// Okay to round down (truncate) if lower has a different digit
// or if lower is inclusive and is exactly the result of rounding
// down (i.e., and we have reached the final digit of lower).
okdown := l != m || inclusive && i+1 == lower.nd
// Okay to round up if upper has a different digit and either upper
// is inclusive or upper is bigger than the result of rounding up.
okup := m != u && (inclusive || m+1 < u || i+1 < upper.nd)
// If it's okay to do either, then round to the nearest one.
// If it's okay to do only one, do it.
switch {
case okdown && okup:
d.Round(i + 1)
return
case okdown:
d.RoundDown(i + 1)
return
case okup:
d.RoundUp(i + 1)
return
}
}
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
)
@@ -11,7 +12,7 @@ type secureCtxKey string
const (
stsHeader = "Strict-Transport-Security"
stsSubdomainString = "; includeSubdomains"
stsSubdomainString = "; includeSubDomains"
stsPreloadString = "; preload"
frameOptionsHeader = "X-Frame-Options"
frameOptionsValue = "DENY"
@@ -20,8 +21,11 @@ const (
xssProtectionHeader = "X-XSS-Protection"
xssProtectionValue = "1; mode=block"
cspHeader = "Content-Security-Policy"
cspReportOnlyHeader = "Content-Security-Policy-Report-Only"
hpkpHeader = "Public-Key-Pins"
referrerPolicyHeader = "Referrer-Policy"
featurePolicyHeader = "Feature-Policy"
expectCTHeader = "Expect-CT"
ctxSecureHeaderKey = secureCtxKey("SecureResponseHeader")
cspNonceSize = 16
@@ -61,6 +65,8 @@ type Options struct {
STSPreload bool
// ContentSecurityPolicy allows the Content-Security-Policy header value to be set with a custom value. Default is "".
ContentSecurityPolicy string
// ContentSecurityPolicyReportOnly allows the Content-Security-Policy-Report-Only header value to be set with a custom value. Default is "".
ContentSecurityPolicyReportOnly string
// CustomBrowserXssValue allows the X-XSS-Protection header value to be set with a custom value. This overrides the BrowserXssFilter option. Default is "".
CustomBrowserXssValue string // nolint: golint
// Passing a template string will replace `$NONCE` with a dynamic nonce value of 16 bytes for each request which can be later retrieved using the Nonce function.
@@ -71,10 +77,15 @@ type Options struct {
PublicKey string
// ReferrerPolicy allows sites to control when browsers will pass the Referer header to other sites. Default is "".
ReferrerPolicy string
// FeaturePolicy allows to selectively enable and disable use of various browser features and APIs. Default is "".
FeaturePolicy string
// SSLHost is the host name that is used to redirect http requests to https. Default is "", which indicates to use the same host.
SSLHost string
// AllowedHosts is a list of fully qualified domain names that are allowed. Default is empty list, which allows any and all host names.
AllowedHosts []string
// AllowedHostsAreRegex determines, if the provided slice contains valid regular expressions. If this flag is set to true, every request's
// host will be checked against these expressions. Default is false for backwards compatibility.
AllowedHostsAreRegex bool
// HostsProxyHeaders is a set of header keys that may hold a proxied hostname value for the request.
HostsProxyHeaders []string
// SSLHostFunc is a function pointer, the return value of the function is the host name that has same functionality as `SSHost`. Default is nil.
@@ -84,6 +95,8 @@ type Options struct {
SSLProxyHeaders map[string]string
// STSSeconds is the max-age of the Strict-Transport-Security header. Default is 0, which would NOT include the header.
STSSeconds int64
// ExpectCTHeader allows the Expect-CT header value to be set with a custom value. Default is "".
ExpectCTHeader string
}
// Secure is a middleware that helps setup a few basic security features. A single secure.Options struct can be
@@ -94,6 +107,10 @@ type Secure struct {
// badHostHandler is the handler used when an incorrect host is passed in.
badHostHandler http.Handler
// cRegexAllowedHosts saves the compiled regular expressions of the AllowedHosts
// option for subsequent use in processRequest
cRegexAllowedHosts []*regexp.Regexp
}
// New constructs a new Secure instance with the supplied options.
@@ -106,13 +123,27 @@ func New(options ...Options) *Secure {
}
o.ContentSecurityPolicy = strings.Replace(o.ContentSecurityPolicy, "$NONCE", "'nonce-%[1]s'", -1)
o.ContentSecurityPolicyReportOnly = strings.Replace(o.ContentSecurityPolicyReportOnly, "$NONCE", "'nonce-%[1]s'", -1)
o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s")
o.nonceEnabled = strings.Contains(o.ContentSecurityPolicy, "%[1]s") || strings.Contains(o.ContentSecurityPolicyReportOnly, "%[1]s")
return &Secure{
s := &Secure{
opt: o,
badHostHandler: http.HandlerFunc(defaultBadHostHandler),
}
if s.opt.AllowedHostsAreRegex {
// Test for invalid regular expressions in AllowedHosts
for _, allowedHost := range o.AllowedHosts {
regex, err := regexp.Compile(fmt.Sprintf("^%s$", allowedHost))
if err != nil {
panic(fmt.Sprintf("Error parsing AllowedHost: %s", err))
}
s.cRegexAllowedHosts = append(s.cRegexAllowedHosts, regex)
}
}
return s
}
// SetBadHostHandler sets the handler to call when secure rejects the host name.
@@ -123,13 +154,10 @@ func (s *Secure) SetBadHostHandler(handler http.Handler) {
// Handler implements the http.HandlerFunc for integration with the standard net/http lib.
func (s *Secure) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := s.Process(w, r)
responseHeader, r, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
// If there was an error, do not continue.
if err != nil {
@@ -144,13 +172,9 @@ func (s *Secure) Handler(h http.Handler) http.Handler {
// Note that this is for requests only and will not write any headers.
func (s *Secure) HandlerForRequestOnly(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
responseHeader, err := s.processRequest(w, r)
responseHeader, r, err := s.processRequest(w, r)
// If there was an error, do not continue.
if err != nil {
@@ -167,13 +191,10 @@ func (s *Secure) HandlerForRequestOnly(h http.Handler) http.Handler {
// HandlerFuncWithNext is a special implementation for Negroni, but could be used elsewhere.
func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := s.Process(w, r)
responseHeader, r, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
// If there was an error, do not call next.
if err == nil && next != nil {
@@ -184,13 +205,9 @@ func (s *Secure) HandlerFuncWithNext(w http.ResponseWriter, r *http.Request, nex
// HandlerFuncWithNextForRequestOnly is a special implementation for Negroni, but could be used elsewhere.
// Note that this is for requests only and will not write any headers.
func (s *Secure) HandlerFuncWithNextForRequestOnly(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
responseHeader, err := s.processRequest(w, r)
responseHeader, r, err := s.processRequest(w, r)
// If there was an error, do not call next.
if err == nil && next != nil {
@@ -202,21 +219,44 @@ func (s *Secure) HandlerFuncWithNextForRequestOnly(w http.ResponseWriter, r *htt
}
}
// Process runs the actual checks and writes the headers in the ResponseWriter.
func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {
responseHeader, err := s.processRequest(w, r)
if responseHeader != nil {
for key, values := range responseHeader {
for _, value := range values {
w.Header().Add(key, value)
}
// addResponseHeaders Adds the headers from 'responseHeader' to the response.
func addResponseHeaders(responseHeader http.Header, w http.ResponseWriter) {
for key, values := range responseHeader {
for _, value := range values {
w.Header().Set(key, value)
}
}
}
// Process runs the actual checks and writes the headers in the ResponseWriter.
func (s *Secure) Process(w http.ResponseWriter, r *http.Request) error {
responseHeader, _, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
return err
}
// ProcessAndReturnNonce runs the actual checks and writes the headers in the ResponseWriter.
// In addition, the generated nonce for the request is returned as well as the error value.
func (s *Secure) ProcessAndReturnNonce(w http.ResponseWriter, r *http.Request) (string, error) {
responseHeader, newR, err := s.processRequest(w, r)
addResponseHeaders(responseHeader, w)
return CSPNonce(newR.Context()), err
}
// ProcessNoModifyRequest runs the actual checks but does not write the headers in the ResponseWriter.
func (s *Secure) ProcessNoModifyRequest(w http.ResponseWriter, r *http.Request) (http.Header, *http.Request, error) {
return s.processRequest(w, r)
}
// processRequest runs the actual checks on the request and returns an error if the middleware chain should stop.
func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.Header, error) {
func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.Header, *http.Request, error) {
// Setup nonce if required.
if s.opt.nonceEnabled {
r = withCSPNonce(r, cspRandNonce())
}
// Resolve the host for the request, using proxy headers if present.
host := r.Host
for _, header := range s.opt.HostsProxyHeaders {
@@ -229,16 +269,25 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
// Allowed hosts check.
if len(s.opt.AllowedHosts) > 0 && !s.opt.IsDevelopment {
isGoodHost := false
for _, allowedHost := range s.opt.AllowedHosts {
if strings.EqualFold(allowedHost, host) {
isGoodHost = true
break
if s.opt.AllowedHostsAreRegex {
for _, allowedHost := range s.cRegexAllowedHosts {
if match := allowedHost.MatchString(host); match {
isGoodHost = true
break
}
}
} else {
for _, allowedHost := range s.opt.AllowedHosts {
if strings.EqualFold(allowedHost, host) {
isGoodHost = true
break
}
}
}
if !isGoodHost {
s.badHostHandler.ServeHTTP(w, r)
return nil, fmt.Errorf("bad host name: %s", host)
return nil, nil, fmt.Errorf("bad host name: %s", host)
}
}
@@ -265,11 +314,11 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
}
http.Redirect(w, r, url.String(), status)
return nil, fmt.Errorf("redirecting to HTTPS")
return nil, nil, fmt.Errorf("redirecting to HTTPS")
}
if s.opt.SSLForceHost {
var SSLHost = host;
var SSLHost = host
if s.opt.SSLHostFunc != nil {
if h := (*s.opt.SSLHostFunc)(host); len(h) > 0 {
SSLHost = h
@@ -288,7 +337,7 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
}
http.Redirect(w, r, url.String(), status)
return nil, fmt.Errorf("redirecting to HTTPS")
return nil, nil, fmt.Errorf("redirecting to HTTPS")
}
}
@@ -343,12 +392,31 @@ func (s *Secure) processRequest(w http.ResponseWriter, r *http.Request) (http.He
}
}
// Content Security Policy Report Only header.
if len(s.opt.ContentSecurityPolicyReportOnly) > 0 {
if s.opt.nonceEnabled {
responseHeader.Set(cspReportOnlyHeader, fmt.Sprintf(s.opt.ContentSecurityPolicyReportOnly, CSPNonce(r.Context())))
} else {
responseHeader.Set(cspReportOnlyHeader, s.opt.ContentSecurityPolicyReportOnly)
}
}
// Referrer Policy header.
if len(s.opt.ReferrerPolicy) > 0 {
responseHeader.Set(referrerPolicyHeader, s.opt.ReferrerPolicy)
}
return responseHeader, nil
// Feature Policy header.
if len(s.opt.FeaturePolicy) > 0 {
responseHeader.Set(featurePolicyHeader, s.opt.FeaturePolicy)
}
// Expect-CT header.
if len(s.opt.ExpectCTHeader) > 0 {
responseHeader.Set(expectCTHeader, s.opt.ExpectCTHeader)
}
return responseHeader, r, nil
}
// isSSL determine if we are on HTTPS.
@@ -369,6 +437,13 @@ func (s *Secure) isSSL(r *http.Request) bool {
// Used by http.ReverseProxy.
func (s *Secure) ModifyResponseHeaders(res *http.Response) error {
if res != nil && res.Request != nil {
// Fix Location response header http to https when SSL is enabled.
location := res.Header.Get("Location")
if s.isSSL(res.Request) && strings.Contains(location, "http:") {
location = strings.Replace(location, "http:", "https:", 1)
res.Header.Set("Location", location)
}
responseHeader := res.Request.Context().Value(ctxSecureHeaderKey)
if responseHeader != nil {
for header, values := range responseHeader.(http.Header) {

1
webui/.gitignore vendored
View File

@@ -8,6 +8,7 @@
# dependencies
/node_modules
/.quasar
# IDEs and editors
/.idea

View File

@@ -25,7 +25,7 @@
<a class="navbar-item" [href]="releaseLink" target="_blank">
{{ version }} / {{ codename }}
</a>
<a class="navbar-item" href="https://docs.traefik.io" target="_blank">
<a class="navbar-item" href="https://docs.traefik.io/v1.7" target="_blank">
Documentation
</a>
</div>