Merge branch v2.3 into master.
This commit is contained in:
commit
7c039ca223
57 changed files with 1102 additions and 117 deletions
|
@ -7,7 +7,7 @@ before:
|
||||||
builds:
|
builds:
|
||||||
- binary: traefik
|
- binary: traefik
|
||||||
|
|
||||||
main: ./cmd/traefik/traefik.go
|
main: ./cmd/traefik/
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
ldflags:
|
ldflags:
|
||||||
|
|
51
CHANGELOG.md
51
CHANGELOG.md
|
@ -1,3 +1,54 @@
|
||||||
|
## [v2.3.0-rc3](https://github.com/containous/traefik/tree/v2.3.0-rc3) (2020-07-28)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc2...v2.3.0-rc3)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[k8s,k8s/ingress]** Support Kubernetes Ingress pathType ([#7087](https://github.com/containous/traefik/pull/7087) by [rtribotte](https://github.com/rtribotte))
|
||||||
|
- **[k8s,k8s/ingress]** Use semantic versioning to enable ingress class support ([#7065](https://github.com/containous/traefik/pull/7065) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
- **[provider]** file parser: skip nil value. ([#7058](https://github.com/containous/traefik/pull/7058) by [ldez](https://github.com/ldez))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **[ecs]** Fix documentation for ECS ([#7107](https://github.com/containous/traefik/pull/7107) by [mmatur](https://github.com/mmatur))
|
||||||
|
- **[k8s]** Add migration documentation for IngressClass ([#7083](https://github.com/containous/traefik/pull/7083) by [kevinpollet](https://github.com/kevinpollet))
|
||||||
|
- **[plugins]** Update availability info ([#7060](https://github.com/containous/traefik/pull/7060) by [PCM2](https://github.com/PCM2))
|
||||||
|
|
||||||
|
**Misc:**
|
||||||
|
- Merge current v2.2 branch into v2.3 ([#7116](https://github.com/containous/traefik/pull/7116) by [ldez](https://github.com/ldez))
|
||||||
|
- Merge current v2.2 branch into v2.3 ([#7086](https://github.com/containous/traefik/pull/7086) by [jbdoumenjou](https://github.com/jbdoumenjou))
|
||||||
|
|
||||||
|
## [v2.2.8](https://github.com/containous/traefik/tree/v2.2.8) (2020-07-28)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.2.7...v2.2.8)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[webui]** fix: clean X-Forwarded-Prefix header for the dashboard. ([#7109](https://github.com/containous/traefik/pull/7109) by [ldez](https://github.com/ldez))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- **[docker]** spelling(docs/content/routing/providers/docker.md) ([#7101](https://github.com/containous/traefik/pull/7101) by [szczot3k](https://github.com/szczot3k))
|
||||||
|
- **[k8s]** doc: add name of used key for kubernetes client auth ([#7068](https://github.com/containous/traefik/pull/7068) by [smueller18](https://github.com/smueller18))
|
||||||
|
|
||||||
|
## [v2.2.7](https://github.com/containous/traefik/tree/v2.2.7) (2020-07-20)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.2.6...v2.2.7)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[server,tls]** fix: drop host port to compare with SNI. ([#7071](https://github.com/containous/traefik/pull/7071) by [ldez](https://github.com/ldez))
|
||||||
|
|
||||||
|
## [v2.2.6](https://github.com/containous/traefik/tree/v2.2.6) (2020-07-17)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.2.5...v2.2.6)
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
- **[logs]** fix: access logs header names filtering is case insensitive ([#6900](https://github.com/containous/traefik/pull/6900) by [mjeanroy](https://github.com/mjeanroy))
|
||||||
|
- **[provider]** Get Entrypoints Port Address without protocol for redirect ([#7047](https://github.com/containous/traefik/pull/7047) by [SantoDE](https://github.com/SantoDE))
|
||||||
|
- **[tls]** Fix domain fronting ([#7064](https://github.com/containous/traefik/pull/7064) by [juliens](https://github.com/juliens))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
- fix: documentation references. ([#7049](https://github.com/containous/traefik/pull/7049) by [ldez](https://github.com/ldez))
|
||||||
|
- Add example for entrypoint on one ip address ([#6483](https://github.com/containous/traefik/pull/6483) by [SimonHeimberg](https://github.com/SimonHeimberg))
|
||||||
|
|
||||||
|
## [v2.3.0-rc2](https://github.com/containous/traefik/tree/v2.3.0-rc2) (2020-07-15)
|
||||||
|
[All Commits](https://github.com/containous/traefik/compare/v2.3.0-rc1...v2.3.0-rc2)
|
||||||
|
|
||||||
|
**Misc:**
|
||||||
|
- fix: goreleaser build commands.
|
||||||
|
|
||||||
## [v2.3.0-rc1](https://github.com/containous/traefik/tree/v2.3.0-rc1) (2020-07-15)
|
## [v2.3.0-rc1](https://github.com/containous/traefik/tree/v2.3.0-rc1) (2020-07-15)
|
||||||
[All Commits](https://github.com/containous/traefik/compare/v2.2.0-rc1...v2.3.0-rc1)
|
[All Commits](https://github.com/containous/traefik/compare/v2.2.0-rc1...v2.3.0-rc1)
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ For complete details, refer to your provider's _Additional configuration_ link.
|
||||||
| [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) |
|
| [Zonomi](https://zonomi.com) | `zonomi` | `ZONOMI_API_KEY` | [Additional configuration](https://go-acme.github.io/lego/dns/zonomi) |
|
||||||
|
|
||||||
[^1]: more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/)
|
[^1]: more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/)
|
||||||
[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application)
|
[^2]: [providing_credentials_to_your_application](https://cloud.google.com/docs/authentication/production)
|
||||||
[^3]: [google/default.go](https://github.com/golang/oauth2/blob/36a7019397c4c86cf59eeab3bc0d188bac444277/google/default.go#L61-L76)
|
[^3]: [google/default.go](https://github.com/golang/oauth2/blob/36a7019397c4c86cf59eeab3bc0d188bac444277/google/default.go#L61-L76)
|
||||||
[^4]: `docker stack` remark: there is no way to support terminal attached to container when deploying with `docker stack`, so you might need to run container with `docker run -it` to generate certificates using `manual` provider.
|
[^4]: `docker stack` remark: there is no way to support terminal attached to container when deploying with `docker stack`, so you might need to run container with `docker run -it` to generate certificates using `manual` provider.
|
||||||
[^5]: The `Global API Key` needs to be used, not the `Origin CA Key`.
|
[^5]: The `Global API Key` needs to be used, not the `Origin CA Key`.
|
||||||
|
|
|
@ -428,6 +428,7 @@ metadata:
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
clientAuth:
|
clientAuth:
|
||||||
|
# the CA certificate is extracted from key `tls.ca` of the given secrets.
|
||||||
secretNames:
|
secretNames:
|
||||||
- secretCA
|
- secretCA
|
||||||
clientAuthType: RequireAndVerifyClientCert
|
clientAuthType: RequireAndVerifyClientCert
|
||||||
|
|
|
@ -320,3 +320,8 @@ Since `v2.2.5` this rule has been removed, and you should not use it anymore.
|
||||||
### File Provider
|
### File Provider
|
||||||
|
|
||||||
The file parser has been changed, since v2.3 the unknown options/fields in a dynamic configuration file are treated as errors.
|
The file parser has been changed, since v2.3 the unknown options/fields in a dynamic configuration file are treated as errors.
|
||||||
|
|
||||||
|
### IngressClass
|
||||||
|
|
||||||
|
In `v2.3`, the support of `IngressClass`, which is available since Kubernetes version `1.18`, has been introduced.
|
||||||
|
In order to be able to use this new resource the [Kubernetes RBAC](../reference/dynamic-configuration/kubernetes-crd.md#rbac) must be updated.
|
||||||
|
|
|
@ -10,6 +10,9 @@ For example, Traefik plugins can add features to modify requests or headers, iss
|
||||||
|
|
||||||
Traefik Pilot can also monitor connected Traefik instances and issue alerts when one is not responding, or when it is subject to security vulnerabilities.
|
Traefik Pilot can also monitor connected Traefik instances and issue alerts when one is not responding, or when it is subject to security vulnerabilities.
|
||||||
|
|
||||||
|
!!! note "Availability"
|
||||||
|
Plugins are available for Traefik v2.3.0-rc1 and later.
|
||||||
|
|
||||||
!!! danger "Experimental Features"
|
!!! danger "Experimental Features"
|
||||||
Plugins can potentially modify the behavior of Traefik in unforeseen ways.
|
Plugins can potentially modify the behavior of Traefik in unforeseen ways.
|
||||||
Exercise caution when adding new plugins to production Traefik instances.
|
Exercise caution when adding new plugins to production Traefik instances.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Using Plugins
|
# Using Plugins
|
||||||
|
|
||||||
Since v2.3, plugins are available to any Traefik instance that is [registered](overview.md#connecting-to-traefik-pilot) with Traefik Pilot.
|
Plugins are available to any instance of Traefik v2.3 or later that is [registered](overview.md#connecting-to-traefik-pilot) with Traefik Pilot.
|
||||||
Plugins are hosted on GitHub, but you can browse plugins to add to your registered Traefik instances from the Traefik Pilot UI.
|
Plugins are hosted on GitHub, but you can browse plugins to add to your registered Traefik instances from the Traefik Pilot UI.
|
||||||
|
|
||||||
!!! danger "Experimental Features"
|
!!! danger "Experimental Features"
|
||||||
|
|
|
@ -79,9 +79,35 @@ providers:
|
||||||
# ...
|
# ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Search for services in all clusters.
|
Search for services in clusters list.
|
||||||
If set to true the configured clusters will be ignored and the clusters will be discovered.
|
|
||||||
If set to false the services will be discovered only in configured clusters.
|
- If set to `true` the configured clusters will be ignored and the clusters will be discovered.
|
||||||
|
- If set to `false` the services will be discovered only in configured clusters.
|
||||||
|
|
||||||
|
### `clusters`
|
||||||
|
|
||||||
|
_Optional, Default=["default"]_
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[providers.ecs]
|
||||||
|
cluster = ["default"]
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (YAML)"
|
||||||
|
providers:
|
||||||
|
ecs:
|
||||||
|
clusters:
|
||||||
|
- default
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
--providers.ecs.clusters=default
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Search for services in clusters list.
|
||||||
|
|
||||||
### `exposedByDefault`
|
### `exposedByDefault`
|
||||||
|
|
||||||
|
|
|
@ -258,14 +258,11 @@ Value of `kubernetes.io/ingress.class` annotation that identifies Ingress object
|
||||||
If the parameter is non-empty, only Ingresses containing an annotation with the same value are processed.
|
If the parameter is non-empty, only Ingresses containing an annotation with the same value are processed.
|
||||||
Otherwise, Ingresses missing the annotation, having an empty value, or with the value `traefik` are processed.
|
Otherwise, Ingresses missing the annotation, having an empty value, or with the value `traefik` are processed.
|
||||||
|
|
||||||
#### ingressClass on Kubernetes 1.18+
|
!!! info "Kubernetes 1.18+"
|
||||||
|
|
||||||
If you cluster is running kubernetes 1.18+,
|
If the Kubernetes cluster version is 1.18+,
|
||||||
you can also leverage the newly Introduced `IngressClass` resource to define which Ingress Objects to handle.
|
the new `IngressClass` resource can be leveraged to identify Ingress objects that should be processed.
|
||||||
In that case, Traefik will look for an `IngressClass` in your cluster with the controller of *traefik.io/ingress-controller* inside the spec.
|
In that case, Traefik will look for an `IngressClass` in the cluster with the controller value equal to *traefik.io/ingress-controller*.
|
||||||
|
|
||||||
!!! note ""
|
|
||||||
Please note, the ingressClass configuration on the provider is not used then anymore.
|
|
||||||
|
|
||||||
Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information.
|
Please see [this article](https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/) for more information.
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ rules:
|
||||||
- extensions
|
- extensions
|
||||||
resources:
|
resources:
|
||||||
- ingresses
|
- ingresses
|
||||||
|
- ingressclasses
|
||||||
verbs:
|
verbs:
|
||||||
- get
|
- get
|
||||||
- list
|
- list
|
||||||
|
|
|
@ -168,7 +168,7 @@ The format is:
|
||||||
|
|
||||||
If both TCP and UDP are wanted for the same port, two entryPoints definitions are needed, such as in the example below.
|
If both TCP and UDP are wanted for the same port, two entryPoints definitions are needed, such as in the example below.
|
||||||
|
|
||||||
??? example "Both TCP and UDP on port 3179"
|
??? example "Both TCP and UDP on Port 3179"
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
## Static configuration
|
## Static configuration
|
||||||
|
@ -194,6 +194,30 @@ If both TCP and UDP are wanted for the same port, two entryPoints definitions ar
|
||||||
--entryPoints.udpep.address=:3179/udp
|
--entryPoints.udpep.address=:3179/udp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? example "Listen on Specific IP Addresses Only"
|
||||||
|
|
||||||
|
```toml tab="File (TOML)"
|
||||||
|
[entryPoints.specificIPv4]
|
||||||
|
address = "192.168.2.7:8888"
|
||||||
|
[entryPoints.specificIPv6]
|
||||||
|
address = "[2001:db8::1]:8888"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="File (yaml)"
|
||||||
|
entryPoints:
|
||||||
|
specificIPv4:
|
||||||
|
address: "192.168.2.7:8888"
|
||||||
|
specificIPv6:
|
||||||
|
address: "[2001:db8::1]:8888"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash tab="CLI"
|
||||||
|
entrypoints.specificIPv4.address=192.168.2.7:8888
|
||||||
|
entrypoints.specificIPv6.address=[2001:db8::1]:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
Full details for how to specify `address` can be found in [net.Listen](https://golang.org/pkg/net/#Listen) (and [net.Dial](https://golang.org/pkg/net/#Dial)) of the doc for go.
|
||||||
|
|
||||||
### Forwarded Headers
|
### Forwarded Headers
|
||||||
|
|
||||||
You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`).
|
You can configure Traefik to trust the forwarded headers information (`X-Forwarded-*`).
|
||||||
|
|
|
@ -535,7 +535,7 @@ You can declare UDP Routers and/or Services using labels.
|
||||||
my-container:
|
my-container:
|
||||||
# ...
|
# ...
|
||||||
labels:
|
labels:
|
||||||
- "traefik.udp.routers.my-router.entrypoint=udp"
|
- "traefik.udp.routers.my-router.entrypoints=udp"
|
||||||
- "traefik.udp.services.my-service.loadbalancer.server.port=4123"
|
- "traefik.udp.services.my-service.loadbalancer.server.port=4123"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -322,9 +322,23 @@ which in turn will create the resulting routers, services, handlers, etc.
|
||||||
traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true"
|
traefik.ingress.kubernetes.io/service.sticky.cookie.httponly: "true"
|
||||||
```
|
```
|
||||||
|
|
||||||
### TLS
|
## Path Types on Kubernetes 1.18+
|
||||||
|
|
||||||
#### Communication Between Traefik and Pods
|
If the Kubernetes cluster version is 1.18+,
|
||||||
|
the new `pathType` property can be leveraged to define the rules matchers:
|
||||||
|
|
||||||
|
- `Exact`: This path type forces the rule matcher to `Path`
|
||||||
|
- `Prefix`: This path type forces the rule matcher to `PathPrefix`
|
||||||
|
|
||||||
|
Please see [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types) for more information.
|
||||||
|
|
||||||
|
!!! warning "Multiple Matches"
|
||||||
|
In the case of multiple matches, Traefik will not ensure the priority of a Path matcher over a PathPrefix matcher,
|
||||||
|
as stated in [this documentation](https://kubernetes.io/docs/concepts/services-networking/ingress/#multiple-matches).
|
||||||
|
|
||||||
|
## TLS
|
||||||
|
|
||||||
|
### Communication Between Traefik and Pods
|
||||||
|
|
||||||
Traefik automatically requests endpoint information based on the service provided in the ingress spec.
|
Traefik automatically requests endpoint information based on the service provided in the ingress spec.
|
||||||
Although Traefik will connect directly to the endpoints (pods),
|
Although Traefik will connect directly to the endpoints (pods),
|
||||||
|
@ -346,7 +360,7 @@ and will connect via TLS automatically.
|
||||||
If this is not an option, you may need to skip TLS certificate verification.
|
If this is not an option, you may need to skip TLS certificate verification.
|
||||||
See the [insecureSkipVerify](../../routing/overview.md#insecureskipverify) setting for more details.
|
See the [insecureSkipVerify](../../routing/overview.md#insecureskipverify) setting for more details.
|
||||||
|
|
||||||
#### Certificates Management
|
### Certificates Management
|
||||||
|
|
||||||
??? example "Using a secret"
|
??? example "Using a secret"
|
||||||
|
|
||||||
|
|
|
@ -472,6 +472,11 @@ It refers to a [TLS Options](../../https/tls.md#tls-options) and will be applied
|
||||||
the TLS option is picked from the mapping mentioned above and based on the server name provided during the TLS handshake,
|
the TLS option is picked from the mapping mentioned above and based on the server name provided during the TLS handshake,
|
||||||
and it all happens before routing actually occurs.
|
and it all happens before routing actually occurs.
|
||||||
|
|
||||||
|
!!! info "Domain Fronting"
|
||||||
|
|
||||||
|
In the case of domain fronting,
|
||||||
|
if the TLS options associated with the Host Header and the SNI are different then Traefik will respond with a status code `421`.
|
||||||
|
|
||||||
??? example "Configuring the TLS options"
|
??? example "Configuring the TLS options"
|
||||||
|
|
||||||
```toml tab="File (TOML)"
|
```toml tab="File (TOML)"
|
||||||
|
|
|
@ -444,7 +444,7 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase acmeTestCase) {
|
||||||
// A real file is needed to have the right mode on acme.json file
|
// A real file is needed to have the right mode on acme.json file
|
||||||
defer os.Remove("/tmp/acme.json")
|
defer os.Remove("/tmp/acme.json")
|
||||||
|
|
||||||
backend := startTestServer("9010", http.StatusOK)
|
backend := startTestServer("9010", http.StatusOK, "")
|
||||||
defer backend.Close()
|
defer backend.Close()
|
||||||
|
|
||||||
for _, sub := range testCase.subCases {
|
for _, sub := range testCase.subCases {
|
||||||
|
|
53
integration/fixtures/https/https_domain_fronting.toml
Normal file
53
integration/fixtures/https/https_domain_fronting.toml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
[global]
|
||||||
|
checkNewVersion = false
|
||||||
|
sendAnonymousUsage = false
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "DEBUG"
|
||||||
|
|
||||||
|
[entryPoints.websecure]
|
||||||
|
address = ":4443"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
insecure = true
|
||||||
|
|
||||||
|
[providers.file]
|
||||||
|
filename = "{{ .SelfFilename }}"
|
||||||
|
|
||||||
|
## dynamic configuration ##
|
||||||
|
|
||||||
|
[http.routers.router1]
|
||||||
|
rule = "Host(`site1.www.snitest.com`)"
|
||||||
|
service = "service1"
|
||||||
|
[http.routers.router1.tls]
|
||||||
|
|
||||||
|
[http.routers.router2]
|
||||||
|
rule = "Host(`site2.www.snitest.com`)"
|
||||||
|
service = "service2"
|
||||||
|
[http.routers.router2.tls]
|
||||||
|
|
||||||
|
[http.routers.router3]
|
||||||
|
rule = "Host(`site3.www.snitest.com`)"
|
||||||
|
service = "service3"
|
||||||
|
[http.routers.router3.tls]
|
||||||
|
options = "mytls"
|
||||||
|
|
||||||
|
[http.services.service1]
|
||||||
|
[[http.services.service1.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:9010"
|
||||||
|
|
||||||
|
[http.services.service2]
|
||||||
|
[[http.services.service2.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:9020"
|
||||||
|
|
||||||
|
[http.services.service3]
|
||||||
|
[[http.services.service3.loadBalancer.servers]]
|
||||||
|
url = "http://127.0.0.1:9030"
|
||||||
|
|
||||||
|
[[tls.certificates]]
|
||||||
|
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
|
||||||
|
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
|
||||||
|
|
||||||
|
[tls.options]
|
||||||
|
[tls.options.mytls]
|
||||||
|
maxVersion = "VersionTLS12"
|
|
@ -35,7 +35,7 @@ func (s *HeadersSuite) TestCorsResponses(c *check.C) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
defer cmd.Process.Kill()
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
backend := startTestServer("9000", http.StatusOK)
|
backend := startTestServer("9000", http.StatusOK, "")
|
||||||
defer backend.Close()
|
defer backend.Close()
|
||||||
|
|
||||||
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
||||||
|
@ -124,7 +124,7 @@ func (s *HeadersSuite) TestSecureHeadersResponses(c *check.C) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
defer cmd.Process.Kill()
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
backend := startTestServer("9000", http.StatusOK)
|
backend := startTestServer("9000", http.StatusOK, "")
|
||||||
defer backend.Close()
|
defer backend.Close()
|
||||||
|
|
||||||
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
||||||
|
@ -173,7 +173,7 @@ func (s *HeadersSuite) TestMultipleSecureHeadersResponses(c *check.C) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
defer cmd.Process.Kill()
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
backend := startTestServer("9000", http.StatusOK)
|
backend := startTestServer("9000", http.StatusOK, "")
|
||||||
defer backend.Close()
|
defer backend.Close()
|
||||||
|
|
||||||
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
err = try.GetRequest(backend.URL, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
|
||||||
|
|
|
@ -20,9 +20,7 @@ import (
|
||||||
math "math"
|
math "math"
|
||||||
|
|
||||||
proto "github.com/golang/protobuf/proto"
|
proto "github.com/golang/protobuf/proto"
|
||||||
)
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
context "context"
|
||||||
|
|
||||||
grpc "google.golang.org/grpc"
|
grpc "google.golang.org/grpc"
|
||||||
|
|
|
@ -73,8 +73,8 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
backend1 := startTestServer("9010", http.StatusNoContent)
|
backend1 := startTestServer("9010", http.StatusNoContent, "")
|
||||||
backend2 := startTestServer("9020", http.StatusResetContent)
|
backend2 := startTestServer("9020", http.StatusResetContent, "")
|
||||||
defer backend1.Close()
|
defer backend1.Close()
|
||||||
defer backend2.Close()
|
defer backend2.Close()
|
||||||
|
|
||||||
|
@ -129,8 +129,8 @@ func (s *HTTPSSuite) TestWithTLSOptions(c *check.C) {
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.org`)"))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
backend1 := startTestServer("9010", http.StatusNoContent)
|
backend1 := startTestServer("9010", http.StatusNoContent, "")
|
||||||
backend2 := startTestServer("9020", http.StatusResetContent)
|
backend2 := startTestServer("9020", http.StatusResetContent, "")
|
||||||
defer backend1.Close()
|
defer backend1.Close()
|
||||||
defer backend2.Close()
|
defer backend2.Close()
|
||||||
|
|
||||||
|
@ -215,8 +215,8 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions(c *check.C) {
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)"))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`snitest.net`)"))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
backend1 := startTestServer("9010", http.StatusNoContent)
|
backend1 := startTestServer("9010", http.StatusNoContent, "")
|
||||||
backend2 := startTestServer("9020", http.StatusResetContent)
|
backend2 := startTestServer("9020", http.StatusResetContent, "")
|
||||||
defer backend1.Close()
|
defer backend1.Close()
|
||||||
defer backend2.Close()
|
defer backend2.Close()
|
||||||
|
|
||||||
|
@ -733,9 +733,12 @@ func (s *HTTPSSuite) TestWithRootCAsFileForHTTPSOnBackend(c *check.C) {
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTestServer(port string, statusCode int) (ts *httptest.Server) {
|
func startTestServer(port string, statusCode int, textContent string) (ts *httptest.Server) {
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
|
if textContent != "" {
|
||||||
|
_, _ = w.Write([]byte(textContent))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:"+port)
|
listener, err := net.Listen("tcp", "127.0.0.1:"+port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -787,8 +790,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithNoChange(c *check.C) {
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)"))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr1.TLSClientConfig.ServerName+"`)"))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
backend1 := startTestServer("9010", http.StatusNoContent)
|
backend1 := startTestServer("9010", http.StatusNoContent, "")
|
||||||
backend2 := startTestServer("9020", http.StatusResetContent)
|
backend2 := startTestServer("9020", http.StatusResetContent, "")
|
||||||
defer backend1.Close()
|
defer backend1.Close()
|
||||||
defer backend2.Close()
|
defer backend2.Close()
|
||||||
|
|
||||||
|
@ -856,8 +859,8 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithChange(c *check.C) {
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)"))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)"))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
backend1 := startTestServer("9010", http.StatusNoContent)
|
backend1 := startTestServer("9010", http.StatusNoContent, "")
|
||||||
backend2 := startTestServer("9020", http.StatusResetContent)
|
backend2 := startTestServer("9020", http.StatusResetContent, "")
|
||||||
defer backend1.Close()
|
defer backend1.Close()
|
||||||
defer backend2.Close()
|
defer backend2.Close()
|
||||||
|
|
||||||
|
@ -919,7 +922,7 @@ func (s *HTTPSSuite) TestWithSNIDynamicConfigRouteWithTlsConfigurationDeletion(c
|
||||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)"))
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`"+tr2.TLSClientConfig.ServerName+"`)"))
|
||||||
c.Assert(err, checker.IsNil)
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
backend2 := startTestServer("9020", http.StatusResetContent)
|
backend2 := startTestServer("9020", http.StatusResetContent, "")
|
||||||
|
|
||||||
defer backend2.Close()
|
defer backend2.Close()
|
||||||
|
|
||||||
|
@ -1111,3 +1114,115 @@ func (s *HTTPSSuite) TestWithSNIDynamicCaseInsensitive(c *check.C) {
|
||||||
proto := conn.ConnectionState().NegotiatedProtocol
|
proto := conn.ConnectionState().NegotiatedProtocol
|
||||||
c.Assert(proto, checker.Equals, "h2")
|
c.Assert(proto, checker.Equals, "h2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWithDomainFronting verify the domain fronting behavior
|
||||||
|
func (s *HTTPSSuite) TestWithDomainFronting(c *check.C) {
|
||||||
|
backend := startTestServer("9010", http.StatusOK, "server1")
|
||||||
|
defer backend.Close()
|
||||||
|
backend2 := startTestServer("9020", http.StatusOK, "server2")
|
||||||
|
defer backend2.Close()
|
||||||
|
backend3 := startTestServer("9030", http.StatusOK, "server3")
|
||||||
|
defer backend3.Close()
|
||||||
|
|
||||||
|
file := s.adaptFile(c, "fixtures/https/https_domain_fronting.toml", struct{}{})
|
||||||
|
defer os.Remove(file)
|
||||||
|
cmd, display := s.traefikCmd(withConfigFile(file))
|
||||||
|
defer display(c)
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
// wait for Traefik
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 500*time.Millisecond, try.BodyContains("Host(`site1.www.snitest.com`)"))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
hostHeader string
|
||||||
|
serverName string
|
||||||
|
expectedContent string
|
||||||
|
expectedStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "SimpleCase",
|
||||||
|
hostHeader: "site1.www.snitest.com",
|
||||||
|
serverName: "site1.www.snitest.com",
|
||||||
|
expectedContent: "server1",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Simple case with port in the Host Header",
|
||||||
|
hostHeader: "site3.www.snitest.com:4443",
|
||||||
|
serverName: "site3.www.snitest.com",
|
||||||
|
expectedContent: "server3",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Spaces after the host header",
|
||||||
|
hostHeader: "site3.www.snitest.com ",
|
||||||
|
serverName: "site3.www.snitest.com",
|
||||||
|
expectedContent: "server3",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Spaces after the servername",
|
||||||
|
hostHeader: "site3.www.snitest.com",
|
||||||
|
serverName: "site3.www.snitest.com ",
|
||||||
|
expectedContent: "server3",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Spaces after the servername and host header",
|
||||||
|
hostHeader: "site3.www.snitest.com ",
|
||||||
|
serverName: "site3.www.snitest.com ",
|
||||||
|
expectedContent: "server3",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Domain Fronting with same tlsOptions should follow header",
|
||||||
|
hostHeader: "site1.www.snitest.com",
|
||||||
|
serverName: "site2.www.snitest.com",
|
||||||
|
expectedContent: "server1",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Domain Fronting with same tlsOptions should follow header (2)",
|
||||||
|
hostHeader: "site2.www.snitest.com",
|
||||||
|
serverName: "site1.www.snitest.com",
|
||||||
|
expectedContent: "server2",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Domain Fronting with different tlsOptions should produce a 421",
|
||||||
|
hostHeader: "site2.www.snitest.com",
|
||||||
|
serverName: "site3.www.snitest.com",
|
||||||
|
expectedContent: "",
|
||||||
|
expectedStatusCode: http.StatusMisdirectedRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Domain Fronting with different tlsOptions should produce a 421 (2)",
|
||||||
|
hostHeader: "site3.www.snitest.com",
|
||||||
|
serverName: "site1.www.snitest.com",
|
||||||
|
expectedContent: "",
|
||||||
|
expectedStatusCode: http.StatusMisdirectedRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Case insensitive",
|
||||||
|
hostHeader: "sIte1.www.snitest.com",
|
||||||
|
serverName: "sitE1.www.snitest.com",
|
||||||
|
expectedContent: "server1",
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:4443", nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
req.Host = test.hostHeader
|
||||||
|
|
||||||
|
err = try.RequestWithTransport(req, 500*time.Millisecond, &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true, ServerName: test.serverName}}, try.StatusCodeIs(test.expectedStatusCode), try.BodyContains(test.expectedContent))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||||
|
@ -23,11 +24,29 @@ func (g DashboardHandler) Append(router *mux.Router) {
|
||||||
// Expose dashboard
|
// Expose dashboard
|
||||||
router.Methods(http.MethodGet).
|
router.Methods(http.MethodGet).
|
||||||
Path("/").
|
Path("/").
|
||||||
HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
http.Redirect(response, request, request.Header.Get("X-Forwarded-Prefix")+"/dashboard/", http.StatusFound)
|
http.Redirect(resp, req, safePrefix(req)+"/dashboard/", http.StatusFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.Methods(http.MethodGet).
|
router.Methods(http.MethodGet).
|
||||||
PathPrefix("/dashboard/").
|
PathPrefix("/dashboard/").
|
||||||
Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets)))
|
Handler(http.StripPrefix("/dashboard/", http.FileServer(g.Assets)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func safePrefix(req *http.Request) string {
|
||||||
|
prefix := req.Header.Get("X-Forwarded-Prefix")
|
||||||
|
if prefix == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parse, err := url.Parse(prefix)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if parse.Host != "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse.Path
|
||||||
|
}
|
||||||
|
|
54
pkg/api/dashboard_test.go
Normal file
54
pkg/api/dashboard_test.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_safePrefix(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
value string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "host",
|
||||||
|
value: "https://example.com",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "host with path",
|
||||||
|
value: "https://example.com/foo/bar?test",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "path",
|
||||||
|
value: "/foo/bar",
|
||||||
|
expected: "/foo/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "path without leading slash",
|
||||||
|
value: "foo/bar",
|
||||||
|
expected: "foo/bar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://localhost", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
req.Header.Set("X-Forwarded-Prefix", test.value)
|
||||||
|
|
||||||
|
prefix := safePrefix(req)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, prefix)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,10 @@ func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) error
|
||||||
sortedKeys := sortKeys(vData, filters)
|
sortedKeys := sortKeys(vData, filters)
|
||||||
|
|
||||||
for _, key := range sortedKeys {
|
for _, key := range sortedKeys {
|
||||||
|
if vData.MapIndex(key).IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
value := reflect.ValueOf(vData.MapIndex(key).Interface())
|
value := reflect.ValueOf(vData.MapIndex(key).Interface())
|
||||||
|
|
||||||
child := &parser.Node{Name: key.String()}
|
child := &parser.Node{Name: key.String()}
|
||||||
|
|
|
@ -524,6 +524,20 @@ func Test_decodeRawToNode(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "nil value",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
|
|
@ -448,9 +448,9 @@ func TestLBStatusUpdater(t *testing.T) {
|
||||||
svInfo := &runtime.ServiceInfo{}
|
svInfo := &runtime.ServiceInfo{}
|
||||||
lbsu := NewLBStatusUpdater(lb, svInfo)
|
lbsu := NewLBStatusUpdater(lb, svInfo)
|
||||||
newServer, err := url.Parse("http://foo.com")
|
newServer, err := url.Parse("http://foo.com")
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
err = lbsu.UpsertServer(newServer, roundrobin.Weight(1))
|
err = lbsu.UpsertServer(newServer, roundrobin.Weight(1))
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, len(lbsu.Servers()), 1)
|
assert.Equal(t, len(lbsu.Servers()), 1)
|
||||||
assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1)
|
assert.Equal(t, len(lbsu.BalancerHandler.(*testLoadBalancer).Options()), 1)
|
||||||
statuses := svInfo.GetAllStatus()
|
statuses := svInfo.GetAllStatus()
|
||||||
|
@ -461,7 +461,7 @@ func TestLBStatusUpdater(t *testing.T) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = lbsu.RemoveServer(newServer)
|
err = lbsu.RemoveServer(newServer)
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, len(lbsu.Servers()), 0)
|
assert.Equal(t, len(lbsu.Servers()), 0)
|
||||||
statuses = svInfo.GetAllStatus()
|
statuses = svInfo.GetAllStatus()
|
||||||
assert.Equal(t, len(statuses), 1)
|
assert.Equal(t, len(statuses), 1)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -100,6 +101,17 @@ func NewHandler(config *types.AccessLog) (*Handler, error) {
|
||||||
Level: logrus.InfoLevel,
|
Level: logrus.InfoLevel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform headers names in config to a canonical form, to be used as is without further transformations.
|
||||||
|
if config.Fields != nil && config.Fields.Headers != nil && len(config.Fields.Headers.Names) > 0 {
|
||||||
|
fields := map[string]string{}
|
||||||
|
|
||||||
|
for h, v := range config.Fields.Headers.Names {
|
||||||
|
fields[textproto.CanonicalMIMEHeaderKey(h)] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Fields.Headers.Names = fields
|
||||||
|
}
|
||||||
|
|
||||||
logHandler := &Handler{
|
logHandler := &Handler{
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|
|
@ -41,11 +41,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLogRotation(t *testing.T) {
|
func TestLogRotation(t *testing.T) {
|
||||||
tempDir, err := ioutil.TempDir("", "traefik_")
|
tempDir := createTempDir(t, "traefik_")
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error setting up temporary directory: %s", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tempDir)
|
|
||||||
|
|
||||||
fileName := filepath.Join(tempDir, "traefik.log")
|
fileName := filepath.Join(tempDir, "traefik.log")
|
||||||
rotatedFileName := fileName + ".rotated"
|
rotatedFileName := fileName + ".rotated"
|
||||||
|
@ -119,9 +115,106 @@ func lineCount(t *testing.T, fileName string) int {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerHeaderFields(t *testing.T) {
|
||||||
|
tmpDir := createTempDir(t, CommonFormat)
|
||||||
|
|
||||||
|
expectedValue := "expectedValue"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
accessLogFields types.AccessLogFields
|
||||||
|
header string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "with default mode",
|
||||||
|
header: "User-Agent",
|
||||||
|
expected: types.AccessLogDrop,
|
||||||
|
accessLogFields: types.AccessLogFields{
|
||||||
|
DefaultMode: types.AccessLogDrop,
|
||||||
|
Headers: &types.FieldHeaders{
|
||||||
|
DefaultMode: types.AccessLogDrop,
|
||||||
|
Names: map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with exact header name",
|
||||||
|
header: "User-Agent",
|
||||||
|
expected: types.AccessLogKeep,
|
||||||
|
accessLogFields: types.AccessLogFields{
|
||||||
|
DefaultMode: types.AccessLogDrop,
|
||||||
|
Headers: &types.FieldHeaders{
|
||||||
|
DefaultMode: types.AccessLogDrop,
|
||||||
|
Names: map[string]string{
|
||||||
|
"User-Agent": types.AccessLogKeep,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with case insensitive match on header name",
|
||||||
|
header: "User-Agent",
|
||||||
|
expected: types.AccessLogKeep,
|
||||||
|
accessLogFields: types.AccessLogFields{
|
||||||
|
DefaultMode: types.AccessLogDrop,
|
||||||
|
Headers: &types.FieldHeaders{
|
||||||
|
DefaultMode: types.AccessLogDrop,
|
||||||
|
Names: map[string]string{
|
||||||
|
"user-agent": types.AccessLogKeep,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
logFile, err := ioutil.TempFile(tmpDir, "*.log")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
config := &types.AccessLog{
|
||||||
|
FilePath: logFile.Name(),
|
||||||
|
Format: CommonFormat,
|
||||||
|
Fields: &test.accessLogFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := NewHandler(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer logger.Close()
|
||||||
|
|
||||||
|
if config.FilePath != "" {
|
||||||
|
_, err = os.Stat(config.FilePath)
|
||||||
|
require.NoError(t, err, fmt.Sprintf("logger should create %s", config.FilePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &http.Request{
|
||||||
|
Header: map[string][]string{},
|
||||||
|
URL: &url.URL{
|
||||||
|
Path: testPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req.Header.Set(test.header, expectedValue)
|
||||||
|
|
||||||
|
logger.ServeHTTP(httptest.NewRecorder(), req, http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
logData, err := ioutil.ReadFile(logFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if test.expected == types.AccessLogDrop {
|
||||||
|
assert.NotContains(t, string(logData), expectedValue)
|
||||||
|
} else {
|
||||||
|
assert.Contains(t, string(logData), expectedValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoggerCLF(t *testing.T) {
|
func TestLoggerCLF(t *testing.T) {
|
||||||
tmpDir := createTempDir(t, CommonFormat)
|
tmpDir := createTempDir(t, CommonFormat)
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
||||||
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat}
|
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat}
|
||||||
|
@ -136,7 +229,6 @@ func TestLoggerCLF(t *testing.T) {
|
||||||
|
|
||||||
func TestAsyncLoggerCLF(t *testing.T) {
|
func TestAsyncLoggerCLF(t *testing.T) {
|
||||||
tmpDir := createTempDir(t, CommonFormat)
|
tmpDir := createTempDir(t, CommonFormat)
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
||||||
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024}
|
config := &types.AccessLog{FilePath: logFilePath, Format: CommonFormat, BufferingSize: 1024}
|
||||||
|
@ -358,7 +450,6 @@ func TestLoggerJSON(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tmpDir := createTempDir(t, JSONFormat)
|
tmpDir := createTempDir(t, JSONFormat)
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
logFilePath := filepath.Join(tmpDir, logFileNameSuffix)
|
||||||
|
|
||||||
|
@ -642,6 +733,8 @@ func createTempDir(t *testing.T, prefix string) string {
|
||||||
tmpDir, err := ioutil.TempDir("", prefix)
|
tmpDir, err := ioutil.TempDir("", prefix)
|
||||||
require.NoError(t, err, "failed to create temp dir")
|
require.NoError(t, err, "failed to create temp dir")
|
||||||
|
|
||||||
|
t.Cleanup(func() { _ = os.RemoveAll(tmpDir) })
|
||||||
|
|
||||||
return tmpDir
|
return tmpDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
networkingv1beta1 "k8s.io/api/networking/v1beta1"
|
||||||
|
@ -55,7 +55,7 @@ type Client interface {
|
||||||
GetSecret(namespace, name string) (*corev1.Secret, bool, error)
|
GetSecret(namespace, name string) (*corev1.Secret, bool, error)
|
||||||
GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error)
|
GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error)
|
||||||
UpdateIngressStatus(ing *networkingv1beta1.Ingress, ip, hostname string) error
|
UpdateIngressStatus(ing *networkingv1beta1.Ingress, ip, hostname string) error
|
||||||
GetServerVersion() (major, minor int, err error)
|
GetServerVersion() (*version.Version, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientWrapper struct {
|
type clientWrapper struct {
|
||||||
|
@ -163,13 +163,13 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the kubernetes cluster is v1.18+, we can use the new IngressClass objects
|
serverVersion, err := c.GetServerVersion()
|
||||||
major, minor, err := c.GetServerVersion()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
log.WithoutContext().Errorf("Failed to get server version: %v", err)
|
||||||
|
return eventCh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if major >= 1 && minor >= 18 {
|
if supportsIngressClass(serverVersion) {
|
||||||
c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod)
|
c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod)
|
||||||
c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler)
|
c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler)
|
||||||
c.clusterFactory.Start(stopCh)
|
c.clusterFactory.Start(stopCh)
|
||||||
|
@ -341,7 +341,7 @@ func (c *clientWrapper) GetIngressClass() (*networkingv1beta1.IngressClass, erro
|
||||||
|
|
||||||
for _, ic := range ingressClasses {
|
for _, ic := range ingressClasses {
|
||||||
if ic.Spec.Controller == traefikDefaultIngressClassController {
|
if ic.Spec.Controller == traefikDefaultIngressClassController {
|
||||||
return ic, err
|
return ic, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,23 +381,13 @@ func (c *clientWrapper) newResourceEventHandler(events chan<- interface{}) cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetServerVersion returns the cluster server version, or an error.
|
// GetServerVersion returns the cluster server version, or an error.
|
||||||
func (c *clientWrapper) GetServerVersion() (major, minor int, err error) {
|
func (c *clientWrapper) GetServerVersion() (*version.Version, error) {
|
||||||
version, err := c.clientset.Discovery().ServerVersion()
|
serverVersion, err := c.clientset.Discovery().ServerVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("could not determine cluster API version: %w", err)
|
return nil, fmt.Errorf("could not retrieve server version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
major, err = strconv.Atoi(version.Major)
|
return version.NewVersion(serverVersion.GitVersion)
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("could not determine cluster major API version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
minor, err = strconv.Atoi(version.Minor)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("could not determine cluster minor API version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return major, minor, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventHandlerFunc will pass the obj on to the events channel or drop it.
|
// eventHandlerFunc will pass the obj on to the events channel or drop it.
|
||||||
|
@ -432,3 +422,11 @@ func (c *clientWrapper) isWatchedNamespace(ns string) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IngressClass objects are supported since Kubernetes v1.18.
|
||||||
|
// See https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class
|
||||||
|
func supportsIngressClass(serverVersion *version.Version) bool {
|
||||||
|
ingressClassVersion := version.Must(version.NewVersion("1.18"))
|
||||||
|
|
||||||
|
return ingressClassVersion.LessThanOrEqual(serverVersion)
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/provider/kubernetes/k8s"
|
"github.com/containous/traefik/v2/pkg/provider/kubernetes/k8s"
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
"k8s.io/api/networking/v1beta1"
|
"k8s.io/api/networking/v1beta1"
|
||||||
|
@ -20,8 +21,7 @@ type clientMock struct {
|
||||||
endpoints []*corev1.Endpoints
|
endpoints []*corev1.Endpoints
|
||||||
ingressClass *networkingv1beta1.IngressClass
|
ingressClass *networkingv1beta1.IngressClass
|
||||||
|
|
||||||
serverMajor int
|
serverVersion *version.Version
|
||||||
serverMinor int
|
|
||||||
|
|
||||||
apiServiceError error
|
apiServiceError error
|
||||||
apiSecretError error
|
apiSecretError error
|
||||||
|
@ -31,11 +31,10 @@ type clientMock struct {
|
||||||
watchChan chan interface{}
|
watchChan chan interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClientMock(major, minor int, paths ...string) clientMock {
|
func newClientMock(serverVersion string, paths ...string) clientMock {
|
||||||
c := clientMock{
|
c := clientMock{}
|
||||||
serverMajor: major,
|
|
||||||
serverMinor: minor,
|
c.serverVersion = version.Must(version.NewVersion(serverVersion))
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
yamlContent, err := ioutil.ReadFile(path)
|
yamlContent, err := ioutil.ReadFile(path)
|
||||||
|
@ -75,8 +74,8 @@ func (c clientMock) GetIngresses() []*v1beta1.Ingress {
|
||||||
return c.ingresses
|
return c.ingresses
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c clientMock) GetServerVersion() (major, minor int, err error) {
|
func (c clientMock) GetServerVersion() (*version.Version, error) {
|
||||||
return c.serverMajor, c.serverMinor, nil
|
return c.serverVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) {
|
func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
kind: Endpoints
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.10.0.1
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
|
@ -0,0 +1,16 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.pathmatcher: Path
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
pathType: ""
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 80
|
|
@ -0,0 +1,10 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
clusterIp: 10.0.0.1
|
|
@ -0,0 +1,11 @@
|
||||||
|
kind: Endpoints
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.10.0.1
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
|
@ -0,0 +1,14 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
pathType: Exact
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 80
|
|
@ -0,0 +1,10 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
clusterIp: 10.0.0.1
|
|
@ -0,0 +1,11 @@
|
||||||
|
kind: Endpoints
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.10.0.1
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
|
@ -0,0 +1,16 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.pathmatcher: Path
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 80
|
|
@ -0,0 +1,10 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
clusterIp: 10.0.0.1
|
|
@ -0,0 +1,11 @@
|
||||||
|
kind: Endpoints
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.10.0.1
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
|
@ -0,0 +1,15 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 80
|
|
@ -0,0 +1,10 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
clusterIp: 10.0.0.1
|
|
@ -0,0 +1,11 @@
|
||||||
|
kind: Endpoints
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.10.0.1
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
|
@ -0,0 +1,15 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.pathmatcher: Path
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 80
|
|
@ -0,0 +1,10 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
clusterIp: 10.0.0.1
|
|
@ -0,0 +1,11 @@
|
||||||
|
kind: Endpoints
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
subsets:
|
||||||
|
- addresses:
|
||||||
|
- ip: 10.10.0.1
|
||||||
|
ports:
|
||||||
|
- port: 8080
|
|
@ -0,0 +1,14 @@
|
||||||
|
kind: Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
metadata:
|
||||||
|
name: ""
|
||||||
|
namespace: testing
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /bar
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
serviceName: service1
|
||||||
|
servicePort: 80
|
|
@ -0,0 +1,10 @@
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: service1
|
||||||
|
namespace: testing
|
||||||
|
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
clusterIp: 10.0.0.1
|
|
@ -183,7 +183,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
TCP: &dynamic.TCPConfiguration{},
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
}
|
}
|
||||||
|
|
||||||
major, minor, err := client.GetServerVersion()
|
serverVersion, err := client.GetServerVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to get server version: %v", err)
|
log.FromContext(ctx).Errorf("Failed to get server version: %v", err)
|
||||||
return conf
|
return conf
|
||||||
|
@ -191,16 +191,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
|
|
||||||
var ingressClass *networkingv1beta1.IngressClass
|
var ingressClass *networkingv1beta1.IngressClass
|
||||||
|
|
||||||
if major >= 1 && minor >= 18 {
|
if supportsIngressClass(serverVersion) {
|
||||||
ic, err := client.GetIngressClass()
|
ic, err := client.GetIngressClass()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.FromContext(ctx).Errorf("Failed to find an ingress class: %v", err)
|
log.FromContext(ctx).Warnf("Failed to find an ingress class: %v", err)
|
||||||
return conf
|
|
||||||
}
|
|
||||||
|
|
||||||
if ic == nil {
|
|
||||||
log.FromContext(ctx).Errorf("No ingress class for the traefik-controller in the cluster")
|
|
||||||
return conf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ingressClass = ic
|
ingressClass = ic
|
||||||
|
@ -337,8 +331,12 @@ func (p *Provider) updateIngressStatus(ing *v1beta1.Ingress, k8sClient Client) e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) shouldProcessIngress(providerIngressClass string, ingress *networkingv1beta1.Ingress, ingressClass *networkingv1beta1.IngressClass) bool {
|
func (p *Provider) shouldProcessIngress(providerIngressClass string, ingress *networkingv1beta1.Ingress, ingressClass *networkingv1beta1.IngressClass) bool {
|
||||||
return ingressClass != nil && ingress.Spec.IngressClassName != nil && ingressClass.ObjectMeta.Name == *ingress.Spec.IngressClassName ||
|
// configuration through the new kubernetes ingressClass
|
||||||
providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] ||
|
if ingress.Spec.IngressClassName != nil {
|
||||||
|
return ingressClass != nil && ingressClass.ObjectMeta.Name == *ingress.Spec.IngressClassName
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerIngressClass == ingress.Annotations[annotationKubernetesIngressClass] ||
|
||||||
len(providerIngressClass) == 0 && ingress.Annotations[annotationKubernetesIngressClass] == traefikDefaultIngressClass
|
len(providerIngressClass) == 0 && ingress.Annotations[annotationKubernetesIngressClass] == traefikDefaultIngressClass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -549,9 +547,14 @@ func loadRouter(rule v1beta1.IngressRule, pa v1beta1.HTTPIngressPath, rtConfig *
|
||||||
|
|
||||||
if len(pa.Path) > 0 {
|
if len(pa.Path) > 0 {
|
||||||
matcher := defaultPathMatcher
|
matcher := defaultPathMatcher
|
||||||
|
|
||||||
|
if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == v1beta1.PathTypeImplementationSpecific {
|
||||||
if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" {
|
if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" {
|
||||||
matcher = rtConfig.Router.PathMatcher
|
matcher = rtConfig.Router.PathMatcher
|
||||||
}
|
}
|
||||||
|
} else if *pa.PathType == v1beta1.PathTypeExact {
|
||||||
|
matcher = "Path"
|
||||||
|
}
|
||||||
|
|
||||||
rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path))
|
rules = append(rules, fmt.Sprintf("%s(`%s`)", matcher, pa.Path))
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
ingressClass string
|
ingressClass string
|
||||||
serverMinor int
|
serverVersion string
|
||||||
expected *dynamic.Configuration
|
expected *dynamic.Configuration
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
@ -926,7 +926,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
desc: "v18 Ingress with ingressClass",
|
desc: "v18 Ingress with ingressClass",
|
||||||
serverMinor: 18,
|
serverVersion: "v1.18",
|
||||||
expected: &dynamic.Configuration{
|
expected: &dynamic.Configuration{
|
||||||
TCP: &dynamic.TCPConfiguration{},
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
HTTP: &dynamic.HTTPConfiguration{
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
@ -952,9 +952,149 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "v18 Ingress with no pathType",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "Path(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v18 Ingress with empty pathType",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "Path(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v18 Ingress with implementationSpecific pathType",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "Path(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v18 Ingress with prefix pathType",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "PathPrefix(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v18 Ingress with exact pathType",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "Path(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "v18 Ingress with missing ingressClass",
|
desc: "v18 Ingress with missing ingressClass",
|
||||||
serverMinor: 18,
|
serverVersion: "v1.18",
|
||||||
expected: &dynamic.Configuration{
|
expected: &dynamic.Configuration{
|
||||||
TCP: &dynamic.TCPConfiguration{},
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
HTTP: &dynamic.HTTPConfiguration{
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
@ -964,10 +1104,39 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "v18 Ingress with ingress annotation",
|
||||||
|
serverVersion: "v1.18",
|
||||||
|
expected: &dynamic.Configuration{
|
||||||
|
TCP: &dynamic.TCPConfiguration{},
|
||||||
|
HTTP: &dynamic.HTTPConfiguration{
|
||||||
|
Middlewares: map[string]*dynamic.Middleware{},
|
||||||
|
Routers: map[string]*dynamic.Router{
|
||||||
|
"testing-bar": {
|
||||||
|
Rule: "PathPrefix(`/bar`)",
|
||||||
|
Service: "testing-service1-80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Services: map[string]*dynamic.Service{
|
||||||
|
"testing-service1-80": {
|
||||||
|
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||||
|
PassHostHeader: Bool(true),
|
||||||
|
Servers: []dynamic.Server{
|
||||||
|
{
|
||||||
|
URL: "http://10.10.0.1:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
test := test
|
test := test
|
||||||
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
@ -993,12 +1162,12 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
paths = append(paths, generateTestFilename("_ingressclass", test.desc))
|
paths = append(paths, generateTestFilename("_ingressclass", test.desc))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverMinor := 17
|
serverVersion := test.serverVersion
|
||||||
if test.serverMinor != 0 {
|
if serverVersion == "" {
|
||||||
serverMinor = test.serverMinor
|
serverVersion = "v1.17"
|
||||||
}
|
}
|
||||||
|
|
||||||
clientMock := newClientMock(1, serverMinor, paths...)
|
clientMock := newClientMock(serverVersion, paths...)
|
||||||
|
|
||||||
p := Provider{IngressClass: test.ingressClass}
|
p := Provider{IngressClass: test.ingressClass}
|
||||||
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
|
||||||
|
@ -1179,7 +1348,7 @@ func TestGetCertificates(t *testing.T) {
|
||||||
if test.errResult != "" {
|
if test.errResult != "" {
|
||||||
assert.EqualError(t, err, test.errResult)
|
assert.EqualError(t, err, test.errResult)
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, test.result, tlsConfigs)
|
assert.Equal(t, test.result, tlsConfigs)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
30
pkg/provider/traefik/fixtures/redirection_with_protocol.json
Normal file
30
pkg/provider/traefik/fixtures/redirection_with_protocol.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"http": {
|
||||||
|
"routers": {
|
||||||
|
"web-to-websecure": {
|
||||||
|
"entryPoints": [
|
||||||
|
"web"
|
||||||
|
],
|
||||||
|
"middlewares": [
|
||||||
|
"redirect-web-to-websecure"
|
||||||
|
],
|
||||||
|
"service": "noop@internal",
|
||||||
|
"rule": "HostRegexp(`{host:.+}`)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"middlewares": {
|
||||||
|
"redirect-web-to-websecure": {
|
||||||
|
"redirectScheme": {
|
||||||
|
"scheme": "https",
|
||||||
|
"port": "443",
|
||||||
|
"permanent": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"noop": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tcp": {},
|
||||||
|
"tls": {}
|
||||||
|
}
|
|
@ -142,7 +142,7 @@ func (i *Provider) getEntryPointPort(name string, def *static.Redirections) (str
|
||||||
return "", fmt.Errorf("'to' entry point field references a non-existing entry point: %s", def.EntryPoint.To)
|
return "", fmt.Errorf("'to' entry point field references a non-existing entry point: %s", def.EntryPoint.To)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, port, err := net.SplitHostPort(dst.Address)
|
_, port, err := net.SplitHostPort(dst.GetAddress())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid entry point %q address %q: %w",
|
return "", fmt.Errorf("invalid entry point %q address %q: %w",
|
||||||
name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err)
|
name, i.staticCfg.EntryPoints[def.EntryPoint.To].Address, err)
|
||||||
|
|
|
@ -232,6 +232,28 @@ func Test_createConfiguration(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "redirection_with_protocol.json",
|
||||||
|
staticCfg: static.Configuration{
|
||||||
|
EntryPoints: map[string]*static.EntryPoint{
|
||||||
|
"web": {
|
||||||
|
Address: ":80",
|
||||||
|
HTTP: static.HTTPConfig{
|
||||||
|
Redirections: &static.Redirections{
|
||||||
|
EntryPoint: &static.RedirectEntryPoint{
|
||||||
|
To: "websecure",
|
||||||
|
Scheme: "https",
|
||||||
|
Permanent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"websecure": {
|
||||||
|
Address: ":443/tcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
|
|
@ -276,7 +276,7 @@ func TestBuilder_BuildChainWithContext(t *testing.T) {
|
||||||
|
|
||||||
handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
|
handlers, err := result.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }))
|
||||||
if test.expectedError != nil {
|
if test.expectedError != nil {
|
||||||
require.NotNil(t, err)
|
require.Error(t, err)
|
||||||
require.Equal(t, test.expectedError.Error(), err.Error())
|
require.Equal(t, test.expectedError.Error(), err.Error())
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/config/runtime"
|
"github.com/containous/traefik/v2/pkg/config/runtime"
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
@ -99,14 +101,13 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
|
log.FromContext(ctx).Errorf("Error during the build of the default TLS configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.HTTPSHandler(handlerHTTPS, defaultTLSConf)
|
|
||||||
|
|
||||||
if len(configsHTTP) > 0 {
|
if len(configsHTTP) > 0 {
|
||||||
router.AddRouteHTTPTLS("*", defaultTLSConf)
|
router.AddRouteHTTPTLS("*", defaultTLSConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyed by domain, then by options reference.
|
// Keyed by domain, then by options reference.
|
||||||
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
|
tlsOptionsForHostSNI := map[string]map[string]nameAndConfig{}
|
||||||
|
tlsOptionsForHost := map[string]string{}
|
||||||
for routerHTTPName, routerHTTPConfig := range configsHTTP {
|
for routerHTTPName, routerHTTPConfig := range configsHTTP {
|
||||||
if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName {
|
if len(routerHTTPConfig.TLS.Options) == 0 || routerHTTPConfig.TLS.Options == defaultTLSConfigName {
|
||||||
continue
|
continue
|
||||||
|
@ -141,6 +142,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// domain is already in lower case thanks to the domain parsing
|
||||||
if tlsOptionsForHostSNI[domain] == nil {
|
if tlsOptionsForHostSNI[domain] == nil {
|
||||||
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
|
tlsOptionsForHostSNI[domain] = make(map[string]nameAndConfig)
|
||||||
}
|
}
|
||||||
|
@ -148,9 +150,51 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
routerName: routerHTTPName,
|
routerName: routerHTTPName,
|
||||||
TLSConfig: tlsConf,
|
TLSConfig: tlsConf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := tlsOptionsForHost[domain]; ok {
|
||||||
|
// Multiple tlsOptions fallback to default
|
||||||
|
tlsOptionsForHost[domain] = "default"
|
||||||
|
} else {
|
||||||
|
tlsOptionsForHost[domain] = routerHTTPConfig.TLS.Options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sniCheck := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.TLS == nil {
|
||||||
|
handlerHTTPS.ServeHTTP(rw, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, err := net.SplitHostPort(req.Host)
|
||||||
|
if err != nil {
|
||||||
|
host = req.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
serverName := strings.TrimSpace(req.TLS.ServerName)
|
||||||
|
|
||||||
|
// Domain Fronting
|
||||||
|
if !strings.EqualFold(host, serverName) {
|
||||||
|
tlsOptionSNI := findTLSOptionName(tlsOptionsForHost, serverName)
|
||||||
|
tlsOptionHeader := findTLSOptionName(tlsOptionsForHost, host)
|
||||||
|
|
||||||
|
if tlsOptionHeader != tlsOptionSNI {
|
||||||
|
log.WithoutContext().
|
||||||
|
WithField("host", host).
|
||||||
|
WithField("req.Host", req.Host).
|
||||||
|
WithField("req.TLS.ServerName", req.TLS.ServerName).
|
||||||
|
Debugf("TLS options difference: SNI=%s, Header:%s", tlsOptionSNI, tlsOptionHeader)
|
||||||
|
http.Error(rw, http.StatusText(http.StatusMisdirectedRequest), http.StatusMisdirectedRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlerHTTPS.ServeHTTP(rw, req)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.HTTPSHandler(sniCheck, defaultTLSConf)
|
||||||
|
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
|
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
|
||||||
|
@ -248,3 +292,17 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||||
|
|
||||||
return router, nil
|
return router, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findTLSOptionName(tlsOptionsForHost map[string]string, host string) string {
|
||||||
|
tlsOptions, ok := tlsOptionsForHost[host]
|
||||||
|
if ok {
|
||||||
|
return tlsOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsOptions, ok = tlsOptionsForHost[strings.ToLower(host)]
|
||||||
|
if ok {
|
||||||
|
return tlsOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
|
@ -193,7 +193,7 @@ func TestManager_BuildTCP(t *testing.T) {
|
||||||
assert.EqualError(t, err, test.expectedError)
|
assert.EqualError(t, err, test.expectedError)
|
||||||
require.Nil(t, handler)
|
require.Nil(t, handler)
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, handler)
|
require.NotNil(t, handler)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -193,7 +193,7 @@ func TestManager_BuildUDP(t *testing.T) {
|
||||||
assert.EqualError(t, err, test.expectedError)
|
assert.EqualError(t, err, test.expectedError)
|
||||||
require.Nil(t, handler)
|
require.Nil(t, handler)
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
require.NotNil(t, handler)
|
require.NotNil(t, handler)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containous/traefik/v2/pkg/log"
|
"github.com/containous/traefik/v2/pkg/log"
|
||||||
|
"github.com/containous/traefik/v2/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Router is a TCP router.
|
// Router is a TCP router.
|
||||||
|
@ -65,7 +66,7 @@ func (r *Router) ServeTCP(conn WriteCloser) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME Optimize and test the routing table before helloServerName
|
// FIXME Optimize and test the routing table before helloServerName
|
||||||
serverName = strings.ToLower(serverName)
|
serverName = types.CanonicalDomain(serverName)
|
||||||
if r.routingTable != nil && serverName != "" {
|
if r.routingTable != nil && serverName != "" {
|
||||||
if target, ok := r.routingTable[serverName]; ok {
|
if target, ok := r.routingTable[serverName]; ok {
|
||||||
target.ServeTCP(r.GetConn(conn, peeked))
|
target.ServeTCP(r.GetConn(conn, peeked))
|
||||||
|
|
|
@ -13,14 +13,14 @@ import (
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CertificateStore store for dynamic and static certificates.
|
// CertificateStore store for dynamic certificates.
|
||||||
type CertificateStore struct {
|
type CertificateStore struct {
|
||||||
DynamicCerts *safe.Safe
|
DynamicCerts *safe.Safe
|
||||||
DefaultCertificate *tls.Certificate
|
DefaultCertificate *tls.Certificate
|
||||||
CertCache *cache.Cache
|
CertCache *cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCertificateStore create a store for dynamic and static certificates.
|
// NewCertificateStore create a store for dynamic certificates.
|
||||||
func NewCertificateStore() *CertificateStore {
|
func NewCertificateStore() *CertificateStore {
|
||||||
return &CertificateStore{
|
return &CertificateStore{
|
||||||
DynamicCerts: &safe.Safe{},
|
DynamicCerts: &safe.Safe{},
|
||||||
|
@ -37,7 +37,7 @@ func (c CertificateStore) getDefaultCertificateDomains() []string {
|
||||||
|
|
||||||
x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0])
|
x509Cert, err := x509.ParseCertificate(c.DefaultCertificate.Certificate[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithoutContext().Errorf("Could not parse default certicate: %v", err)
|
log.WithoutContext().Errorf("Could not parse default certificate: %v", err)
|
||||||
return allCerts
|
return allCerts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue