1
0
mirror of https://github.com/containous/traefik.git synced 2025-09-30 17:44:25 +03:00

Compare commits

..

6 Commits

Author SHA1 Message Date
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
24 changed files with 178 additions and 81 deletions

View File

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

View File

@@ -1,5 +1,17 @@
# Change Log # Change Log
## [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) ## [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) [All Commits](https://github.com/containous/traefik/compare/v1.7.15...v1.7.16)

View File

@@ -158,7 +158,7 @@ Integration tests must be run from the `integration/` directory and require the
## Documentation ## 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 ### Building Documentation

View File

@@ -4,7 +4,7 @@
</p> </p>
[![Build Status SemaphoreCI](https://semaphoreci.com/api/v1/containous/traefik/branches/master/shields_badge.svg)](https://semaphoreci.com/containous/traefik) [![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) [![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) [![](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) [![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 ## Supported Backends
- [Docker](https://docs.traefik.io/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/configuration/backends/docker#docker-swarm-mode) - [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/configuration/backends/kubernetes) - [Kubernetes](https://docs.traefik.io/v1.7/configuration/backends/kubernetes)
- [Mesos](https://docs.traefik.io/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/configuration/backends/marathon) - [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/configuration/backends/rancher) (API, Metadata) - [Rancher](https://docs.traefik.io/v1.7/configuration/backends/rancher) (API, Metadata)
- [Azure Service Fabric](https://docs.traefik.io/configuration/backends/servicefabric) - [Azure Service Fabric](https://docs.traefik.io/v1.7/configuration/backends/servicefabric)
- [Consul Catalog](https://docs.traefik.io/configuration/backends/consulcatalog) - [Consul Catalog](https://docs.traefik.io/v1.7/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) - [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/configuration/backends/eureka) - [Eureka](https://docs.traefik.io/v1.7/configuration/backends/eureka)
- [Amazon ECS](https://docs.traefik.io/configuration/backends/ecs) - [Amazon ECS](https://docs.traefik.io/v1.7/configuration/backends/ecs)
- [Amazon DynamoDB](https://docs.traefik.io/configuration/backends/dynamodb) - [Amazon DynamoDB](https://docs.traefik.io/v1.7/configuration/backends/dynamodb)
- [File](https://docs.traefik.io/configuration/backends/file) - [File](https://docs.traefik.io/v1.7/configuration/backends/file)
- [Rest](https://docs.traefik.io/configuration/backends/rest) - [Rest](https://docs.traefik.io/v1.7/configuration/backends/rest)
## Quickstart ## 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. 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 ## 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). A collection of contributions around Traefik can be found at [https://awesome.traefik.io](https://awesome.traefik.io).
## Support ## Support

View File

@@ -352,14 +352,14 @@ func stats(globalConfiguration *configuration.GlobalConfiguration) {
Stats collection is enabled. Stats collection is enabled.
Many thanks for contributing to Traefik's improvement by allowing us to receive anonymous information from your configuration. 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 :) 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) collect(globalConfiguration)
} else { } else {
log.Info(` log.Info(`
Stats collection is disabled. Stats collection is disabled.
Help us improve Traefik by turning this feature on :) 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

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

View File

@@ -221,7 +221,11 @@ infos:
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. 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): 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: <4> `traefik.ingress.kubernetes.io/rate-limit` example:

View File

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

View File

@@ -77,7 +77,7 @@ version: '3'
services: services:
reverse-proxy: 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 command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports: ports:
- "80:80" # The HTTP port - "80:80" # The HTTP port

View File

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

View File

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

View File

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

View File

@@ -139,7 +139,7 @@ Here is the [docker-compose file](https://docs.docker.com/compose/compose-file/)
```yaml ```yaml
traefik: 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 command: --consul --consul.endpoint=127.0.0.1:8500
ports: ports:
- "80:80" - "80:80"

View File

@@ -1,5 +1,5 @@
traefik: 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 command: --api --rancher --rancher.domain=rancher.localhost --rancher.endpoint=http://example.com --rancher.accesskey=XXXXXXX --rancher.secretkey=YYYYYY --logLevel=DEBUG
ports: ports:
- "80:80" - "80:80"

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ version: '3'
services: services:
reverse-proxy: 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 command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports: ports:
- "80:80" # The HTTP port - "80:80" # The HTTP port
@@ -101,7 +101,7 @@ IP: 172.27.0.4
### 4 — Enjoy Traefik's Magic ### 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! 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/#supported-backends) that will do the job. 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: services:
# The reverse proxy service (Traefik) # The reverse proxy service (Traefik)
reverse-proxy: 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 command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports: ports:
- "80:80" # The HTTP port - "80:80" # The HTTP port

View File

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

View File

@@ -19,7 +19,10 @@ import (
) )
// Compile time validation that the response recorder implements http interfaces correctly. // 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 // Handler is a middleware that provides the custom error pages
type Handler struct { type Handler struct {
@@ -76,26 +79,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
catcher := newCodeCatcher(w, h.httpCodeRanges) catcher := newCodeCatcher(w, h.httpCodeRanges)
next.ServeHTTP(catcher, req) next.ServeHTTP(catcher, req)
if !catcher.isError { if !catcher.isFilteredCode() {
return return
} }
// check the recorder code against the configured http status code ranges // check the recorder code against the configured http status code ranges
code := catcher.getCode()
for _, block := range h.httpCodeRanges { for _, block := range h.httpCodeRanges {
if catcher.code >= block[0] && catcher.code <= block[1] { if code >= block[0] && code <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", catcher.code) log.Errorf("Caught HTTP Status Code %d, returning error page", code)
var query string var query string
if len(h.backendQuery) > 0 { if len(h.backendQuery) > 0 {
query = "/" + strings.TrimPrefix(h.backendQuery, "/") query = "/" + strings.TrimPrefix(h.backendQuery, "/")
query = strings.Replace(query, "{status}", strconv.Itoa(catcher.code), -1) query = strings.Replace(query, "{status}", strconv.Itoa(code), -1)
} }
pageReq, err := newRequest(h.backendURL + query) pageReq, err := newRequest(h.backendURL + query)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
w.WriteHeader(catcher.code) w.WriteHeader(code)
fmt.Fprint(w, http.StatusText(catcher.code)) fmt.Fprint(w, http.StatusText(code))
return return
} }
@@ -105,7 +109,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
h.backendHandler.ServeHTTP(recorderErrorPage, pageReq.WithContext(req.Context())) h.backendHandler.ServeHTTP(recorderErrorPage, pageReq.WithContext(req.Context()))
utils.CopyHeaders(w.Header(), recorderErrorPage.Header()) utils.CopyHeaders(w.Header(), recorderErrorPage.Header())
w.WriteHeader(catcher.code) w.WriteHeader(code)
w.Write(recorderErrorPage.GetBody().Bytes()) w.Write(recorderErrorPage.GetBody().Bytes())
return return
} }
@@ -127,21 +131,39 @@ func newRequest(baseURL string) (*http.Request, error) {
return req, nil 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 // 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 // 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 // simply drops the data from the response. Otherwise, it forwards it directly to
// the original client (its responseWriter) without any buffering. // the original client (its responseWriter) without any buffering.
type codeCatcher struct { type codeCatcher struct {
headerMap http.Header headerMap http.Header
code int code int
httpCodeRanges types.HTTPCodeRanges httpCodeRanges types.HTTPCodeRanges
firstWrite bool firstWrite bool
isError bool caughtFilteredCode bool
responseWriter http.ResponseWriter responseWriter http.ResponseWriter
err error headersSent bool
err error
} }
func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) *codeCatcher { 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{ catcher := &codeCatcher{
headerMap: make(http.Header), headerMap: make(http.Header),
code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200. code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200.
@@ -149,6 +171,9 @@ func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges)
httpCodeRanges: httpCodeRanges, httpCodeRanges: httpCodeRanges,
firstWrite: true, firstWrite: true,
} }
if _, ok := rw.(http.CloseNotifier); ok {
return &codeCatcherWithCloseNotify{catcher}
}
return catcher return catcher
} }
@@ -160,12 +185,19 @@ func (cc *codeCatcher) Header() http.Header {
return cc.headerMap 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) { func (cc *codeCatcher) Write(buf []byte) (int, error) {
if cc.err != nil {
return 0, cc.err
}
if !cc.firstWrite { if !cc.firstWrite {
if cc.isError { if cc.caughtFilteredCode {
// We don't care about the contents of the response, // We don't care about the contents of the response,
// since we want to serve the ones from the error page, // since we want to serve the ones from the error page,
// so we just drop them. // so we just drop them.
@@ -173,25 +205,38 @@ func (cc *codeCatcher) Write(buf []byte) (int, error) {
} }
return cc.responseWriter.Write(buf) return cc.responseWriter.Write(buf)
} }
for _, block := range cc.httpCodeRanges {
if cc.code >= block[0] && cc.code <= block[1] {
cc.isError = true
break
}
}
cc.firstWrite = false cc.firstWrite = false
if !cc.isError {
utils.CopyHeaders(cc.responseWriter.Header(), cc.Header()) // If WriteHeader was already called from the caller, this is a NOOP.
cc.responseWriter.WriteHeader(cc.code) // Otherwise, cc.code is actually a 200 here.
} else { cc.WriteHeader(cc.code)
if cc.caughtFilteredCode {
return len(buf), nil return len(buf), nil
} }
return cc.responseWriter.Write(buf) return cc.responseWriter.Write(buf)
} }
func (cc *codeCatcher) WriteHeader(code int) { func (cc *codeCatcher) WriteHeader(code int) {
if cc.headersSent || cc.caughtFilteredCode {
return
}
cc.code = code 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 // Hijack hijacks the connection
@@ -204,6 +249,10 @@ func (cc *codeCatcher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// Flush sends any buffered data to the client. // Flush sends any buffered data to the client.
func (cc *codeCatcher) Flush() { 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 { if flusher, ok := cc.responseWriter.(http.Flusher); ok {
flusher.Flush() flusher.Flush()
} }

View File

@@ -46,6 +46,18 @@ func TestHandler(t *testing.T) {
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusPartialContent)) 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", desc: "in the range",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}}, errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
@@ -120,6 +132,9 @@ func TestHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.backendCode) w.WriteHeader(test.backendCode)
if test.backendCode == http.StatusNotModified {
return
}
fmt.Fprintln(w, http.StatusText(test.backendCode)) fmt.Fprintln(w, http.StatusText(test.backendCode))
}) })

1
webui/.gitignore vendored
View File

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

View File

@@ -25,7 +25,7 @@
<a class="navbar-item" [href]="releaseLink" target="_blank"> <a class="navbar-item" [href]="releaseLink" target="_blank">
{{ version }} / {{ codename }} {{ version }} / {{ codename }}
</a> </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 Documentation
</a> </a>
</div> </div>