Enable CORS configuration
This commit is contained in:
parent
883f90dded
commit
d2b38e6ac4
13 changed files with 888 additions and 89 deletions
|
@ -14,11 +14,22 @@ The Headers middleware can manage the requests/responses headers.
|
||||||
Add the `X-Script-Name` header to the proxied request and the `X-Custom-Response-Header` to the response
|
Add the `X-Script-Name` header to the proxied request and the `X-Custom-Response-Header` to the response
|
||||||
|
|
||||||
```yaml tab="Docker"
|
```yaml tab="Docker"
|
||||||
a-container:
|
|
||||||
image: a-container-image
|
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.testHeader.Headers.CustomRequestHeaders.X-Script-Name=test",
|
- "traefik.http.middlewares.testHeader.Headers.CustomRequestHeaders.X-Script-Name=test"
|
||||||
- "traefik.http.middlewares.testHeader.Headers.CustomResponseHeaders.X-Custom-Response-Header=True",
|
- "traefik.http.middlewares.testHeader.Headers.CustomResponseHeaders.X-Custom-Response-Header=True"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: testHeader
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
CustomRequestHeaders:
|
||||||
|
X-Script-Name: "test"
|
||||||
|
CustomResponseHeaders:
|
||||||
|
X-Custom-Response-Header: "True"
|
||||||
```
|
```
|
||||||
|
|
||||||
```toml tab="File"
|
```toml tab="File"
|
||||||
|
@ -34,9 +45,26 @@ labels:
|
||||||
|
|
||||||
`X-Script-Name` header added to the proxied request, the `X-Custom-Request-Header` header removed from the request, and the `X-Custom-Response-Header` header removed from the response.
|
`X-Script-Name` header added to the proxied request, the `X-Custom-Request-Header` header removed from the request, and the `X-Custom-Response-Header` header removed from the response.
|
||||||
|
|
||||||
??? example "File"
|
```yaml tab="Docker"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.testHeader.Headers.CustomRequestHeaders.X-Script-Name=test"
|
||||||
|
- "traefik.http.middlewares.testHeader.Headers.CustomResponseHeaders.X-Custom-Response-Header=True"
|
||||||
|
```
|
||||||
|
|
||||||
```toml
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: testHeader
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
CustomRequestHeaders:
|
||||||
|
X-Script-Name: "test"
|
||||||
|
CustomResponseHeaders:
|
||||||
|
X-Custom-Response-Header: "True"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File"
|
||||||
[http.middlewares]
|
[http.middlewares]
|
||||||
[http.middlewares.testHeader.headers]
|
[http.middlewares.testHeader.headers]
|
||||||
[http.middlewares.testHeader.headers.CustomRequestHeaders]
|
[http.middlewares.testHeader.headers.CustomRequestHeaders]
|
||||||
|
@ -45,38 +73,71 @@ labels:
|
||||||
X-Custom-Response-Header = "True"
|
X-Custom-Response-Header = "True"
|
||||||
```
|
```
|
||||||
|
|
||||||
??? example "Docker"
|
|
||||||
|
|
||||||
```yml
|
|
||||||
a-container:
|
|
||||||
image: a-container-image
|
|
||||||
labels:
|
|
||||||
- "traefik.http.middlewares.testHeader.Headers.CustomRequestHeaders.X-Script-Name=test",
|
|
||||||
- "traefik.http.middlewares.testHeader.Headers.CustomResponseHeaders.X-Custom-Response-Header=True",
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Security Headers
|
### Using Security Headers
|
||||||
|
|
||||||
Security related headers (HSTS headers, SSL redirection, Browser XSS filter, etc) can be added and configured per frontend in a similar manner to the custom headers above.
|
Security related headers (HSTS headers, SSL redirection, Browser XSS filter, etc) can be added and configured per frontend in a similar manner to the custom headers above.
|
||||||
This functionality allows for some easy security features to quickly be set.
|
This functionality allows for some easy security features to quickly be set.
|
||||||
|
|
||||||
??? example "File"
|
```yaml tab="Docker"
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.testHeader.Headers.FrameDeny=true"
|
||||||
|
- "traefik.http.middlewares.testHeader.Headers.SSLRedirect=true"
|
||||||
|
```
|
||||||
|
|
||||||
```toml
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: testHeader
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
FrameDeny: "true"
|
||||||
|
SSLRedirect: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File"
|
||||||
[http.middlewares]
|
[http.middlewares]
|
||||||
[http.middlewares.testHeader.headers]
|
[http.middlewares.testHeader.headers]
|
||||||
FrameDeny = true
|
FrameDeny = true
|
||||||
SSLRedirect = true
|
SSLRedirect = true
|
||||||
```
|
```
|
||||||
|
|
||||||
??? example "Docker"
|
### CORS Headers
|
||||||
|
|
||||||
```yml
|
CORS (Cross-Origin Resource Sharing) headers can be added and configured per frontend in a similar manner to the custom headers above.
|
||||||
a-container:
|
This functionality allows for more advanced security features to quickly be set.
|
||||||
image: a-container-image
|
|
||||||
|
```yaml tab="Docker"
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.testHeader.Headers.FrameDeny=true",
|
- "traefik.http.middlewares.testHeader.Headers.AccessControlAllowMethods=GET,OPTIONS,PUT"
|
||||||
- "traefik.http.middlewares.testHeader.Headers.SSLRedirect=true",
|
- "traefik.http.middlewares.testHeader.Headers.AccessControlAllowOrigin=origin-list-or-null"
|
||||||
|
- "traefik.http.middlewares.testHeader.Headers.AccessControlMaxAge=100"
|
||||||
|
- "traefik.http.middlewares.testHeader.Headers.AddVaryHeader=true"
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml tab="Kubernetes"
|
||||||
|
apiVersion: traefik.containo.us/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: testHeader
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
AccessControlAllowMethods:
|
||||||
|
- "GET"
|
||||||
|
- "OPTIONS"
|
||||||
|
- "PUT"
|
||||||
|
AccessControlAllowOrigin: "origin-list-or-null"
|
||||||
|
AccessControlMaxAge: 100
|
||||||
|
AddVaryHeader: "true"
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml tab="File"
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.testHeader.headers]
|
||||||
|
AccessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
|
||||||
|
AccessControlAllowOrigin = "origin-list-or-null"
|
||||||
|
AccessControlMaxAge = 100
|
||||||
|
AddVaryHeader = true
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
|
@ -93,6 +154,42 @@ This functionality allows for some easy security features to quickly be set.
|
||||||
|
|
||||||
The `customRequestHeaders` option lists the Header names and values to apply to the request.
|
The `customRequestHeaders` option lists the Header names and values to apply to the request.
|
||||||
|
|
||||||
|
### customResponseHeaders
|
||||||
|
|
||||||
|
The `customResponseHeaders` option lists the Header names and values to apply to the response.
|
||||||
|
|
||||||
|
### accessControlAllowCredentials
|
||||||
|
|
||||||
|
The `accessControlAllowCredentials` indicates whether the request can include user credentials.
|
||||||
|
|
||||||
|
### accessControlAllowHeaders
|
||||||
|
|
||||||
|
The `accessControlAllowHeaders` indicates which header field names can be used as part of the request.
|
||||||
|
|
||||||
|
### accessControlAllowMethods
|
||||||
|
|
||||||
|
The `accessControlAllowMethods` indicates which methods can be used during requests.
|
||||||
|
|
||||||
|
### accessControlAllowOrigin
|
||||||
|
|
||||||
|
The `accessControlAllowOrigin` indicates whether a resource can be shared by returning different values. The three options for this value are:
|
||||||
|
|
||||||
|
- `origin-list-or-null`
|
||||||
|
- `*`
|
||||||
|
- `null`
|
||||||
|
|
||||||
|
### accessControlExposeHeaders
|
||||||
|
|
||||||
|
The `accessControlExposeHeaders` indicates which headers are safe to expose to the api of a CORS API specification.
|
||||||
|
|
||||||
|
### accessControlMaxAge
|
||||||
|
|
||||||
|
The `accessControlMaxAge` indicates how long a preflight request can be cached.
|
||||||
|
|
||||||
|
### addVaryHeader
|
||||||
|
|
||||||
|
The `addVaryHeader` is used in conjunction with `accessControlAllowOrigin` to determine whether the vary header should be added or modified to demonstrate that server responses can differ beased on the value of the origin header.
|
||||||
|
|
||||||
### allowedHosts
|
### allowedHosts
|
||||||
|
|
||||||
The `allowedHosts` option lists fully qualified domain names that are allowed.
|
The `allowedHosts` option lists fully qualified domain names that are allowed.
|
||||||
|
|
|
@ -70,6 +70,13 @@
|
||||||
excludedIPs = ["127.0.0.1/16", "192.168.1.7"]
|
excludedIPs = ["127.0.0.1/16", "192.168.1.7"]
|
||||||
|
|
||||||
[http.middlewares.my-headers.Headers]
|
[http.middlewares.my-headers.Headers]
|
||||||
|
accessControlAllowCredentials = true
|
||||||
|
accessControlAllowHeaders = ["X-foobar", "X-fiibar"]
|
||||||
|
accessControlAllowMethods = ["GET", "PUT"]
|
||||||
|
accessControlAllowOrigin = "*"
|
||||||
|
accessControlExposeHeaders = ["X-foobar", "X-fiibar"]
|
||||||
|
accessControlMaxAge = 200
|
||||||
|
addVaryHeader = true
|
||||||
allowedHosts = ["foobar", "foobar"]
|
allowedHosts = ["foobar", "foobar"]
|
||||||
hostsProxyHeaders = ["foobar", "foobar"]
|
hostsProxyHeaders = ["foobar", "foobar"]
|
||||||
sslRedirect = true
|
sslRedirect = true
|
||||||
|
|
21
integration/fixtures/headers/basic.toml
Normal file
21
integration/fixtures/headers/basic.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
[entrypoints]
|
||||||
|
[entrypoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
logLevel = "DEBUG"
|
||||||
|
|
||||||
|
[providers]
|
||||||
|
[providers.file]
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
[http.routers.router1]
|
||||||
|
rule = "Host(`test.localhost`)"
|
||||||
|
service = "service1"
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
[http.services.service1.loadbalancer]
|
||||||
|
[[http.services.service1.loadbalancer.servers]]
|
||||||
|
url = "http://172.17.0.2:80"
|
||||||
|
weight = 1
|
28
integration/fixtures/headers/cors.toml
Normal file
28
integration/fixtures/headers/cors.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
[entrypoints]
|
||||||
|
[entrypoints.web]
|
||||||
|
address = ":8000"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
logLevel = "DEBUG"
|
||||||
|
|
||||||
|
[providers]
|
||||||
|
[providers.file]
|
||||||
|
|
||||||
|
[http.routers]
|
||||||
|
[http.routers.router1]
|
||||||
|
rule = "Host(`test.localhost`)"
|
||||||
|
service = "service1"
|
||||||
|
|
||||||
|
[http.middlewares]
|
||||||
|
[http.middlewares.cors.Headers]
|
||||||
|
AccessControlAllowMethods= ["GET", "OPTIONS", "PUT"]
|
||||||
|
AccessControlAllowOrigin = "origin-list-or-null"
|
||||||
|
AccessControlMaxAge = 100
|
||||||
|
AddVaryHeader = true
|
||||||
|
|
||||||
|
[http.services]
|
||||||
|
[http.services.service1.loadbalancer]
|
||||||
|
[[http.services.service1.loadbalancer.servers]]
|
||||||
|
url = "http://172.17.0.2:80"
|
||||||
|
weight = 1
|
106
integration/headers_test.go
Normal file
106
integration/headers_test.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/integration/try"
|
||||||
|
"github.com/go-check/check"
|
||||||
|
checker "github.com/vdemeester/shakers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Headers test suites
|
||||||
|
type HeadersSuite struct{ BaseSuite }
|
||||||
|
|
||||||
|
func (s *HeadersSuite) SetUpSuite(c *check.C) {
|
||||||
|
s.createComposeProject(c, "headers")
|
||||||
|
|
||||||
|
s.composeProject.Start(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HeadersSuite) TestSimpleConfiguration(c *check.C) {
|
||||||
|
cmd, display := s.traefikCmd(withConfigFile("fixtures/headers/basic.toml"))
|
||||||
|
defer display(c)
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
// Expected a 404 as we did not configure anything
|
||||||
|
err = try.GetRequest("http://127.0.0.1:8000/", 1000*time.Millisecond, try.StatusCodeIs(http.StatusNotFound))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HeadersSuite) TestCorsResponses(c *check.C) {
|
||||||
|
cmd, display := s.traefikCmd(withConfigFile("fixtures/headers/cors.toml"))
|
||||||
|
defer display(c)
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
testCase := []struct {
|
||||||
|
desc string
|
||||||
|
requestHeaders http.Header
|
||||||
|
expected http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple access control allow origin",
|
||||||
|
requestHeaders: http.Header{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||||
|
"Vary": {"Origin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCase {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
req.Host = "test.localhost"
|
||||||
|
req.Header = test.requestHeaders
|
||||||
|
|
||||||
|
err = try.Request(req, 500*time.Millisecond, try.HasBody(), try.HasHeaderStruct(test.expected))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HeadersSuite) TestCorsPreflightResponses(c *check.C) {
|
||||||
|
cmd, display := s.traefikCmd(withConfigFile("fixtures/headers/cors.toml"))
|
||||||
|
defer display(c)
|
||||||
|
|
||||||
|
err := cmd.Start()
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
defer cmd.Process.Kill()
|
||||||
|
|
||||||
|
testCase := []struct {
|
||||||
|
desc string
|
||||||
|
requestHeaders http.Header
|
||||||
|
expected http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple preflight request",
|
||||||
|
requestHeaders: http.Header{
|
||||||
|
"Access-Control-Request-Headers": {"origin"},
|
||||||
|
"Access-Control-Request-Method": {"GET", "OPTIONS"},
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: http.Header{
|
||||||
|
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||||
|
"Access-Control-Max-Age": {"100"},
|
||||||
|
"Access-Control-Allow-Methods": {"GET,OPTIONS,PUT"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCase {
|
||||||
|
req, err := http.NewRequest(http.MethodOptions, "http://127.0.0.1:8000/", nil)
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
req.Host = "test.localhost"
|
||||||
|
req.Header = test.requestHeaders
|
||||||
|
|
||||||
|
err = try.Request(req, 500*time.Millisecond, try.HasBody(), try.HasHeaderStruct(test.expected))
|
||||||
|
c.Assert(err, checker.IsNil)
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ func init() {
|
||||||
check.Suite(&FileSuite{})
|
check.Suite(&FileSuite{})
|
||||||
check.Suite(&GRPCSuite{})
|
check.Suite(&GRPCSuite{})
|
||||||
check.Suite(&HealthCheckSuite{})
|
check.Suite(&HealthCheckSuite{})
|
||||||
|
check.Suite(&HeadersSuite{})
|
||||||
check.Suite(&HostResolverSuite{})
|
check.Suite(&HostResolverSuite{})
|
||||||
check.Suite(&HTTPSSuite{})
|
check.Suite(&HTTPSSuite{})
|
||||||
check.Suite(&LogRotationSuite{})
|
check.Suite(&LogRotationSuite{})
|
||||||
|
|
4
integration/resources/compose/headers.yml
Normal file
4
integration/resources/compose/headers.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
whoami1:
|
||||||
|
image: containous/whoami
|
||||||
|
ports:
|
||||||
|
- "8881:80"
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/abronan/valkeyrie/store"
|
"github.com/abronan/valkeyrie/store"
|
||||||
|
@ -125,6 +126,62 @@ func StatusCodeIs(status int) ResponseCondition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasHeader returns a retry condition function.
|
||||||
|
// The condition returns an error if the response does not have a header set.
|
||||||
|
func HasHeader(header string) ResponseCondition {
|
||||||
|
return func(res *http.Response) error {
|
||||||
|
if _, ok := res.Header[header]; !ok {
|
||||||
|
return errors.New("response doesn't contain header: " + header)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasHeaderValue returns a retry condition function.
|
||||||
|
// The condition returns an error if the response does not have a header set, and a value for that header.
|
||||||
|
// Has an option to test for an exact header match only, not just contains.
|
||||||
|
func HasHeaderValue(header, value string, exactMatch bool) ResponseCondition {
|
||||||
|
return func(res *http.Response) error {
|
||||||
|
if _, ok := res.Header[header]; !ok {
|
||||||
|
return errors.New("response doesn't contain header: " + header)
|
||||||
|
}
|
||||||
|
|
||||||
|
matchFound := false
|
||||||
|
for _, hdr := range res.Header[header] {
|
||||||
|
if value != hdr && exactMatch {
|
||||||
|
return fmt.Errorf("got header %s with value %s, wanted %s", header, hdr, value)
|
||||||
|
}
|
||||||
|
if value == hdr {
|
||||||
|
matchFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matchFound {
|
||||||
|
return fmt.Errorf("response doesn't contain header %s with value %s", header, value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasHeaderStruct returns a retry condition function.
|
||||||
|
// The condition returns an error if the response does contain the headers set, and matching contents.
|
||||||
|
func HasHeaderStruct(header http.Header) ResponseCondition {
|
||||||
|
return func(res *http.Response) error {
|
||||||
|
for key := range header {
|
||||||
|
if _, ok := res.Header[key]; ok {
|
||||||
|
// Header exists in the response, test it.
|
||||||
|
eq := reflect.DeepEqual(header[key], res.Header[key])
|
||||||
|
if !eq {
|
||||||
|
return fmt.Errorf("for header %s got values %v, wanted %v", key, res.Header[key], header[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// DoCondition is a retry condition function.
|
// DoCondition is a retry condition function.
|
||||||
// It returns an error
|
// It returns an error
|
||||||
type DoCondition func() error
|
type DoCondition func() error
|
||||||
|
|
|
@ -126,6 +126,21 @@ type Headers struct {
|
||||||
CustomRequestHeaders map[string]string `json:"customRequestHeaders,omitempty"`
|
CustomRequestHeaders map[string]string `json:"customRequestHeaders,omitempty"`
|
||||||
CustomResponseHeaders map[string]string `json:"customResponseHeaders,omitempty"`
|
CustomResponseHeaders map[string]string `json:"customResponseHeaders,omitempty"`
|
||||||
|
|
||||||
|
// AccessControlAllowCredentials is only valid if true. false is ignored.
|
||||||
|
AccessControlAllowCredentials bool `json:"AccessControlAllowCredentials,omitempty"`
|
||||||
|
// AccessControlAllowHeaders must be used in response to a preflight request with Access-Control-Request-Headers set.
|
||||||
|
AccessControlAllowHeaders []string `json:"AccessControlAllowHeaders,omitempty"`
|
||||||
|
// AccessControlAllowMethods must be used in response to a preflight request with Access-Control-Request-Method set.
|
||||||
|
AccessControlAllowMethods []string `json:"AccessControlAllowMethods,omitempty"`
|
||||||
|
// AccessControlAllowOrigin Can be "origin-list-or-null" or "*". From (https://www.w3.org/TR/cors/#access-control-allow-origin-response-header)
|
||||||
|
AccessControlAllowOrigin string `json:"AccessControlAllowOrigin,omitempty"`
|
||||||
|
// AccessControlExposeHeaders sets valid headers for the response.
|
||||||
|
AccessControlExposeHeaders []string `json:"AccessControlExposeHeaders,omitempty"`
|
||||||
|
// AccessControlMaxAge sets the time that a preflight request may be cached.
|
||||||
|
AccessControlMaxAge int64 `json:"AccessControlMaxAge,omitempty"`
|
||||||
|
// AddVaryHeader controls if the Vary header is automatically added/updated when the AccessControlAllowOrigin is set.
|
||||||
|
AddVaryHeader bool `json:"AddVaryHeader,omitempty"`
|
||||||
|
|
||||||
AllowedHosts []string `json:"allowedHosts,omitempty"`
|
AllowedHosts []string `json:"allowedHosts,omitempty"`
|
||||||
HostsProxyHeaders []string `json:"hostsProxyHeaders,omitempty"`
|
HostsProxyHeaders []string `json:"hostsProxyHeaders,omitempty"`
|
||||||
SSLRedirect bool `json:"sslRedirect,omitempty"`
|
SSLRedirect bool `json:"sslRedirect,omitempty"`
|
||||||
|
@ -154,6 +169,17 @@ func (h *Headers) HasCustomHeadersDefined() bool {
|
||||||
len(h.CustomRequestHeaders) != 0)
|
len(h.CustomRequestHeaders) != 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasCorsHeadersDefined checks to see if any of the cors header elements have been set
|
||||||
|
func (h *Headers) HasCorsHeadersDefined() bool {
|
||||||
|
return h != nil && (h.AccessControlAllowCredentials ||
|
||||||
|
len(h.AccessControlAllowHeaders) != 0 ||
|
||||||
|
len(h.AccessControlAllowMethods) != 0 ||
|
||||||
|
h.AccessControlAllowOrigin != "" ||
|
||||||
|
len(h.AccessControlExposeHeaders) != 0 ||
|
||||||
|
h.AccessControlMaxAge != 0 ||
|
||||||
|
h.AddVaryHeader)
|
||||||
|
}
|
||||||
|
|
||||||
// HasSecureHeadersDefined checks to see if any of the secure header elements have been set
|
// HasSecureHeadersDefined checks to see if any of the secure header elements have been set
|
||||||
func (h *Headers) HasSecureHeadersDefined() bool {
|
func (h *Headers) HasSecureHeadersDefined() bool {
|
||||||
return h != nil && (len(h.AllowedHosts) != 0 ||
|
return h != nil && (len(h.AllowedHosts) != 0 ||
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/containous/traefik/pkg/config"
|
"github.com/containous/traefik/pkg/config"
|
||||||
"github.com/containous/traefik/pkg/middlewares"
|
"github.com/containous/traefik/pkg/middlewares"
|
||||||
|
@ -15,6 +17,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
typeName = "Headers"
|
typeName = "Headers"
|
||||||
|
originHeaderKey = "X-Request-Origin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type headers struct {
|
type headers struct {
|
||||||
|
@ -28,22 +31,26 @@ func New(ctx context.Context, next http.Handler, config config.Headers, name str
|
||||||
logger := middlewares.GetLogger(ctx, name, typeName)
|
logger := middlewares.GetLogger(ctx, name, typeName)
|
||||||
logger.Debug("Creating middleware")
|
logger.Debug("Creating middleware")
|
||||||
|
|
||||||
if !config.HasSecureHeadersDefined() && !config.HasCustomHeadersDefined() {
|
hasSecureHeaders := config.HasSecureHeadersDefined()
|
||||||
|
hasCustomHeaders := config.HasCustomHeadersDefined()
|
||||||
|
hasCorsHeaders := config.HasCorsHeadersDefined()
|
||||||
|
|
||||||
|
if !hasSecureHeaders && !hasCustomHeaders && !hasCorsHeaders {
|
||||||
return nil, errors.New("headers configuration not valid")
|
return nil, errors.New("headers configuration not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
var handler http.Handler
|
var handler http.Handler
|
||||||
nextHandler := next
|
nextHandler := next
|
||||||
|
|
||||||
if config.HasSecureHeadersDefined() {
|
if hasSecureHeaders {
|
||||||
logger.Debug("Setting up secureHeaders from %v", config)
|
logger.Debug("Setting up secureHeaders from %v", config)
|
||||||
handler = newSecure(next, config)
|
handler = newSecure(next, config)
|
||||||
nextHandler = handler
|
nextHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HasCustomHeadersDefined() {
|
if hasCustomHeaders || hasCorsHeaders {
|
||||||
logger.Debug("Setting up customHeaders from %v", config)
|
logger.Debug("Setting up customHeaders/Cors from %v", config)
|
||||||
handler = newHeader(nextHandler, config)
|
handler = NewHeader(nextHandler, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &headers{
|
return &headers{
|
||||||
|
@ -102,29 +109,67 @@ func (s secureHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
// Header is a middleware that helps setup a few basic security features. A single headerOptions struct can be
|
// Header is a middleware that helps setup a few basic security features. A single headerOptions struct can be
|
||||||
// provided to configure which features should be enabled, and the ability to override a few of the default values.
|
// provided to configure which features should be enabled, and the ability to override a few of the default values.
|
||||||
type header struct {
|
type Header struct {
|
||||||
next http.Handler
|
next http.Handler
|
||||||
// If Custom request headers are set, these will be added to the request
|
headers *config.Headers
|
||||||
customRequestHeaders map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHeader constructs a new header instance from supplied frontend header struct.
|
// NewHeader constructs a new header instance from supplied frontend header struct.
|
||||||
func newHeader(next http.Handler, headers config.Headers) *header {
|
func NewHeader(next http.Handler, headers config.Headers) *Header {
|
||||||
return &header{
|
return &Header{
|
||||||
next: next,
|
next: next,
|
||||||
customRequestHeaders: headers.CustomRequestHeaders,
|
headers: &headers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *header) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
reqAcMethod := req.Header.Get("Access-Control-Request-Method")
|
||||||
|
reqAcHeaders := req.Header.Get("Access-Control-Request-Headers")
|
||||||
|
originHeader := req.Header.Get("Origin")
|
||||||
|
|
||||||
|
if reqAcMethod != "" && reqAcHeaders != "" && originHeader != "" && req.Method == http.MethodOptions {
|
||||||
|
// If the request is an OPTIONS request with an Access-Control-Request-Method header, and Access-Control-Request-Headers headers,
|
||||||
|
// and Origin headers, then it is a CORS preflight request, and we need to build a custom response: https://www.w3.org/TR/cors/#preflight-request
|
||||||
|
if s.headers.AccessControlAllowCredentials {
|
||||||
|
rw.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
allowHeaders := strings.Join(s.headers.AccessControlAllowHeaders, ",")
|
||||||
|
if allowHeaders != "" {
|
||||||
|
rw.Header().Set("Access-Control-Allow-Headers", allowHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowMethods := strings.Join(s.headers.AccessControlAllowMethods, ",")
|
||||||
|
if allowMethods != "" {
|
||||||
|
rw.Header().Set("Access-Control-Allow-Methods", allowMethods)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowOrigin := s.getAllowOrigin(originHeader)
|
||||||
|
|
||||||
|
if allowOrigin != "" {
|
||||||
|
rw.Header().Set("Access-Control-Allow-Origin", allowOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Set("Access-Control-Max-Age", strconv.Itoa(int(s.headers.AccessControlMaxAge)))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(originHeader) > 0 {
|
||||||
|
rw.Header().Set(originHeaderKey, originHeader)
|
||||||
|
}
|
||||||
|
|
||||||
s.modifyRequestHeaders(req)
|
s.modifyRequestHeaders(req)
|
||||||
|
// If there is a next, call it.
|
||||||
|
if s.next != nil {
|
||||||
s.next.ServeHTTP(rw, req)
|
s.next.ServeHTTP(rw, req)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// modifyRequestHeaders set or delete request headers.
|
// modifyRequestHeaders set or delete request headers.
|
||||||
func (s *header) modifyRequestHeaders(req *http.Request) {
|
func (s *Header) modifyRequestHeaders(req *http.Request) {
|
||||||
// Loop through Custom request headers
|
// Loop through Custom request headers
|
||||||
for header, value := range s.customRequestHeaders {
|
for header, value := range s.headers.CustomRequestHeaders {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
req.Header.Del(header)
|
req.Header.Del(header)
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,3 +177,57 @@ func (s *header) modifyRequestHeaders(req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ModifyResponseHeaders set or delete response headers
|
||||||
|
func (s *Header) ModifyResponseHeaders(res *http.Response) error {
|
||||||
|
// Loop through Custom response headers
|
||||||
|
for header, value := range s.headers.CustomResponseHeaders {
|
||||||
|
if value == "" {
|
||||||
|
res.Header.Del(header)
|
||||||
|
} else {
|
||||||
|
res.Header.Set(header, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originHeader := res.Header.Get(originHeaderKey)
|
||||||
|
allowOrigin := s.getAllowOrigin(originHeader)
|
||||||
|
// Delete the origin header key, since it is only used to pass data from the request for response handling
|
||||||
|
res.Header.Del(originHeaderKey)
|
||||||
|
if allowOrigin != "" {
|
||||||
|
res.Header.Set("Access-Control-Allow-Origin", allowOrigin)
|
||||||
|
|
||||||
|
if s.headers.AddVaryHeader {
|
||||||
|
varyHeader := res.Header.Get("Vary")
|
||||||
|
if varyHeader != "" {
|
||||||
|
varyHeader += ","
|
||||||
|
}
|
||||||
|
varyHeader += "Origin"
|
||||||
|
|
||||||
|
res.Header.Set("Vary", varyHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.headers.AccessControlAllowCredentials {
|
||||||
|
res.Header.Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
exposeHeaders := strings.Join(s.headers.AccessControlExposeHeaders, ",")
|
||||||
|
if exposeHeaders != "" {
|
||||||
|
res.Header.Set("Access-Control-Expose-Headers", exposeHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Header) getAllowOrigin(header string) string {
|
||||||
|
switch s.headers.AccessControlAllowOrigin {
|
||||||
|
case "origin-list-or-null":
|
||||||
|
if len(header) == 0 {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
case "*":
|
||||||
|
return "*"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/containous/traefik/pkg/config"
|
"github.com/containous/traefik/pkg/config"
|
||||||
"github.com/containous/traefik/pkg/testhelpers"
|
"github.com/containous/traefik/pkg/testhelpers"
|
||||||
|
"github.com/containous/traefik/pkg/tracing"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -17,7 +18,7 @@ import (
|
||||||
func TestCustomRequestHeader(t *testing.T) {
|
func TestCustomRequestHeader(t *testing.T) {
|
||||||
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
header := newHeader(emptyHandler, config.Headers{
|
header := NewHeader(emptyHandler, config.Headers{
|
||||||
CustomRequestHeaders: map[string]string{
|
CustomRequestHeaders: map[string]string{
|
||||||
"X-Custom-Request-Header": "test_request",
|
"X-Custom-Request-Header": "test_request",
|
||||||
},
|
},
|
||||||
|
@ -35,7 +36,7 @@ func TestCustomRequestHeader(t *testing.T) {
|
||||||
func TestCustomRequestHeaderEmptyValue(t *testing.T) {
|
func TestCustomRequestHeaderEmptyValue(t *testing.T) {
|
||||||
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
header := newHeader(emptyHandler, config.Headers{
|
header := NewHeader(emptyHandler, config.Headers{
|
||||||
CustomRequestHeaders: map[string]string{
|
CustomRequestHeaders: map[string]string{
|
||||||
"X-Custom-Request-Header": "test_request",
|
"X-Custom-Request-Header": "test_request",
|
||||||
},
|
},
|
||||||
|
@ -49,7 +50,7 @@ func TestCustomRequestHeaderEmptyValue(t *testing.T) {
|
||||||
assert.Equal(t, http.StatusOK, res.Code)
|
assert.Equal(t, http.StatusOK, res.Code)
|
||||||
assert.Equal(t, "test_request", req.Header.Get("X-Custom-Request-Header"))
|
assert.Equal(t, "test_request", req.Header.Get("X-Custom-Request-Header"))
|
||||||
|
|
||||||
header = newHeader(emptyHandler, config.Headers{
|
header = NewHeader(emptyHandler, config.Headers{
|
||||||
CustomRequestHeaders: map[string]string{
|
CustomRequestHeaders: map[string]string{
|
||||||
"X-Custom-Request-Header": "",
|
"X-Custom-Request-Header": "",
|
||||||
},
|
},
|
||||||
|
@ -188,3 +189,312 @@ func TestSSLForceHost(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCORSPreflights(t *testing.T) {
|
||||||
|
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
header *Header
|
||||||
|
requestHeaders http.Header
|
||||||
|
expected http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Test Simple Preflight",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
|
||||||
|
AccessControlAllowOrigin: "origin-list-or-null",
|
||||||
|
AccessControlMaxAge: 600,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Access-Control-Request-Headers": {"origin"},
|
||||||
|
"Access-Control-Request-Method": {"GET", "OPTIONS"},
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||||
|
"Access-Control-Max-Age": {"600"},
|
||||||
|
"Access-Control-Allow-Methods": {"GET,OPTIONS,PUT"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Wildcard origin Preflight",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
|
||||||
|
AccessControlAllowOrigin: "*",
|
||||||
|
AccessControlMaxAge: 600,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Access-Control-Request-Headers": {"origin"},
|
||||||
|
"Access-Control-Request-Method": {"GET", "OPTIONS"},
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"*"},
|
||||||
|
"Access-Control-Max-Age": {"600"},
|
||||||
|
"Access-Control-Allow-Methods": {"GET,OPTIONS,PUT"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Allow Credentials Preflight",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
|
||||||
|
AccessControlAllowOrigin: "*",
|
||||||
|
AccessControlAllowCredentials: true,
|
||||||
|
AccessControlMaxAge: 600,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Access-Control-Request-Headers": {"origin"},
|
||||||
|
"Access-Control-Request-Method": {"GET", "OPTIONS"},
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"*"},
|
||||||
|
"Access-Control-Max-Age": {"600"},
|
||||||
|
"Access-Control-Allow-Methods": {"GET,OPTIONS,PUT"},
|
||||||
|
"Access-Control-Allow-Credentials": {"true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Allow Headers Preflight",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
|
||||||
|
AccessControlAllowOrigin: "*",
|
||||||
|
AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"},
|
||||||
|
AccessControlMaxAge: 600,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Access-Control-Request-Headers": {"origin"},
|
||||||
|
"Access-Control-Request-Method": {"GET", "OPTIONS"},
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"*"},
|
||||||
|
"Access-Control-Max-Age": {"600"},
|
||||||
|
"Access-Control-Allow-Methods": {"GET,OPTIONS,PUT"},
|
||||||
|
"Access-Control-Allow-Headers": {"origin,X-Forwarded-For"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
req := testhelpers.MustNewRequest(http.MethodOptions, "/foo", nil)
|
||||||
|
req.Header = test.requestHeaders
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
test.header.ServeHTTP(rw, req)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, rw.Result().Header)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyHeaderObject(t *testing.T) {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
|
_, err := New(context.Background(), next, config.Headers{}, "testing")
|
||||||
|
require.Errorf(t, err, "headers configuration not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomHeaderHandler(t *testing.T) {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
|
header, _ := New(context.Background(), next, config.Headers{
|
||||||
|
CustomRequestHeaders: map[string]string{
|
||||||
|
"X-Custom-Request-Header": "test_request",
|
||||||
|
},
|
||||||
|
}, "testing")
|
||||||
|
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil)
|
||||||
|
|
||||||
|
header.ServeHTTP(res, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, res.Code)
|
||||||
|
assert.Equal(t, "test_request", req.Header.Get("X-Custom-Request-Header"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTracingInformation(t *testing.T) {
|
||||||
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
|
header := &headers{
|
||||||
|
handler: next,
|
||||||
|
name: "testing",
|
||||||
|
}
|
||||||
|
|
||||||
|
name, trace := header.GetTracingInformation()
|
||||||
|
|
||||||
|
assert.Equal(t, "testing", name)
|
||||||
|
assert.Equal(t, tracing.SpanKindNoneEnum, trace)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCORSResponses(t *testing.T) {
|
||||||
|
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
nonEmptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Testing") })
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
header *Header
|
||||||
|
requestHeaders http.Header
|
||||||
|
expected http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Test Simple Request",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "origin-list-or-null",
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Wildcard origin Request",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "*",
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Empty origin Request",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "origin-list-or-null",
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"null"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Not Defined origin Request",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{}),
|
||||||
|
requestHeaders: map[string][]string{},
|
||||||
|
expected: map[string][]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Allow Credentials Request",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "*",
|
||||||
|
AccessControlAllowCredentials: true,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"*"},
|
||||||
|
"Access-Control-Allow-Credentials": {"true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Expose Headers Request",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "*",
|
||||||
|
AccessControlExposeHeaders: []string{"origin", "X-Forwarded-For"},
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"*"},
|
||||||
|
"Access-Control-Expose-Headers": {"origin,X-Forwarded-For"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Test Simple Request with Vary Headers",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "origin-list-or-null",
|
||||||
|
AddVaryHeader: true,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||||
|
"Vary": {"Origin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Test Simple Request with Vary Headers and non-empty response",
|
||||||
|
header: NewHeader(nonEmptyHandler, config.Headers{
|
||||||
|
AccessControlAllowOrigin: "origin-list-or-null",
|
||||||
|
AddVaryHeader: true,
|
||||||
|
}),
|
||||||
|
requestHeaders: map[string][]string{
|
||||||
|
"Origin": {"https://foo.bar.org"},
|
||||||
|
},
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||||
|
"Vary": {"Testing,Origin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil)
|
||||||
|
req.Header = test.requestHeaders
|
||||||
|
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
test.header.ServeHTTP(rw, req)
|
||||||
|
err := test.header.ModifyResponseHeaders(rw.Result())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, rw.Result().Header)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomResponseHeaders(t *testing.T) {
|
||||||
|
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
header *Header
|
||||||
|
expected http.Header
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Test Simple Response",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
CustomResponseHeaders: map[string]string{
|
||||||
|
"Testing": "foo",
|
||||||
|
"Testing2": "bar",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Testing": {"foo"},
|
||||||
|
"Testing2": {"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Deleting Custom Header",
|
||||||
|
header: NewHeader(emptyHandler, config.Headers{
|
||||||
|
CustomResponseHeaders: map[string]string{
|
||||||
|
"Testing": "foo",
|
||||||
|
"Testing2": "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
expected: map[string][]string{
|
||||||
|
"Testing": {"foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil)
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
test.header.ServeHTTP(rw, req)
|
||||||
|
err := test.header.ModifyResponseHeaders(rw.Result())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, rw.Result().Header)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -42,7 +42,14 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||||
"traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true",
|
"traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true",
|
||||||
"traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar",
|
"traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar",
|
||||||
"traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true",
|
"traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials": "true",
|
||||||
"traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar",
|
"traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200",
|
||||||
|
"traefik.http.middlewares.Middleware8.headers.addvaryheader": "true",
|
||||||
"traefik.http.middlewares.Middleware8.headers.browserxssfilter": "true",
|
"traefik.http.middlewares.Middleware8.headers.browserxssfilter": "true",
|
||||||
"traefik.http.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar",
|
"traefik.http.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar",
|
||||||
"traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true",
|
"traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true",
|
||||||
|
@ -377,6 +384,22 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||||
"name0": "foobar",
|
"name0": "foobar",
|
||||||
"name1": "foobar",
|
"name1": "foobar",
|
||||||
},
|
},
|
||||||
|
AccessControlAllowCredentials: true,
|
||||||
|
AccessControlAllowHeaders: []string{
|
||||||
|
"X-foobar",
|
||||||
|
"X-fiibar",
|
||||||
|
},
|
||||||
|
AccessControlAllowMethods: []string{
|
||||||
|
"GET",
|
||||||
|
"PUT",
|
||||||
|
},
|
||||||
|
AccessControlAllowOrigin: "foobar",
|
||||||
|
AccessControlExposeHeaders: []string{
|
||||||
|
"X-foobar",
|
||||||
|
"X-fiibar",
|
||||||
|
},
|
||||||
|
AccessControlMaxAge: 200,
|
||||||
|
AddVaryHeader: true,
|
||||||
AllowedHosts: []string{
|
AllowedHosts: []string{
|
||||||
"foobar",
|
"foobar",
|
||||||
"fiibar",
|
"fiibar",
|
||||||
|
@ -710,6 +733,22 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
"name0": "foobar",
|
"name0": "foobar",
|
||||||
"name1": "foobar",
|
"name1": "foobar",
|
||||||
},
|
},
|
||||||
|
AccessControlAllowCredentials: true,
|
||||||
|
AccessControlAllowHeaders: []string{
|
||||||
|
"X-foobar",
|
||||||
|
"X-fiibar",
|
||||||
|
},
|
||||||
|
AccessControlAllowMethods: []string{
|
||||||
|
"GET",
|
||||||
|
"PUT",
|
||||||
|
},
|
||||||
|
AccessControlAllowOrigin: "foobar",
|
||||||
|
AccessControlExposeHeaders: []string{
|
||||||
|
"X-foobar",
|
||||||
|
"X-fiibar",
|
||||||
|
},
|
||||||
|
AccessControlMaxAge: 200,
|
||||||
|
AddVaryHeader: true,
|
||||||
AllowedHosts: []string{
|
AllowedHosts: []string{
|
||||||
"foobar",
|
"foobar",
|
||||||
"fiibar",
|
"fiibar",
|
||||||
|
@ -854,6 +893,13 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true",
|
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true",
|
||||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar",
|
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar",
|
||||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true",
|
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200",
|
||||||
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true",
|
||||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar",
|
"traefik.HTTP.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar",
|
||||||
"traefik.HTTP.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true",
|
"traefik.HTTP.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true",
|
||||||
"traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar",
|
"traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar",
|
||||||
|
|
|
@ -4,46 +4,43 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/containous/traefik/pkg/config"
|
"github.com/containous/traefik/pkg/config"
|
||||||
|
"github.com/containous/traefik/pkg/middlewares/headers"
|
||||||
"github.com/unrolled/secure"
|
"github.com/unrolled/secure"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildHeaders(headers *config.Headers) func(*http.Response) error {
|
func buildHeaders(hdrs *config.Headers) func(*http.Response) error {
|
||||||
opt := secure.Options{
|
opt := secure.Options{
|
||||||
BrowserXssFilter: headers.BrowserXSSFilter,
|
BrowserXssFilter: hdrs.BrowserXSSFilter,
|
||||||
ContentTypeNosniff: headers.ContentTypeNosniff,
|
ContentTypeNosniff: hdrs.ContentTypeNosniff,
|
||||||
ForceSTSHeader: headers.ForceSTSHeader,
|
ForceSTSHeader: hdrs.ForceSTSHeader,
|
||||||
FrameDeny: headers.FrameDeny,
|
FrameDeny: hdrs.FrameDeny,
|
||||||
IsDevelopment: headers.IsDevelopment,
|
IsDevelopment: hdrs.IsDevelopment,
|
||||||
SSLRedirect: headers.SSLRedirect,
|
SSLRedirect: hdrs.SSLRedirect,
|
||||||
SSLForceHost: headers.SSLForceHost,
|
SSLForceHost: hdrs.SSLForceHost,
|
||||||
SSLTemporaryRedirect: headers.SSLTemporaryRedirect,
|
SSLTemporaryRedirect: hdrs.SSLTemporaryRedirect,
|
||||||
STSIncludeSubdomains: headers.STSIncludeSubdomains,
|
STSIncludeSubdomains: hdrs.STSIncludeSubdomains,
|
||||||
STSPreload: headers.STSPreload,
|
STSPreload: hdrs.STSPreload,
|
||||||
ContentSecurityPolicy: headers.ContentSecurityPolicy,
|
ContentSecurityPolicy: hdrs.ContentSecurityPolicy,
|
||||||
CustomBrowserXssValue: headers.CustomBrowserXSSValue,
|
CustomBrowserXssValue: hdrs.CustomBrowserXSSValue,
|
||||||
CustomFrameOptionsValue: headers.CustomFrameOptionsValue,
|
CustomFrameOptionsValue: hdrs.CustomFrameOptionsValue,
|
||||||
PublicKey: headers.PublicKey,
|
PublicKey: hdrs.PublicKey,
|
||||||
ReferrerPolicy: headers.ReferrerPolicy,
|
ReferrerPolicy: hdrs.ReferrerPolicy,
|
||||||
SSLHost: headers.SSLHost,
|
SSLHost: hdrs.SSLHost,
|
||||||
AllowedHosts: headers.AllowedHosts,
|
AllowedHosts: hdrs.AllowedHosts,
|
||||||
HostsProxyHeaders: headers.HostsProxyHeaders,
|
HostsProxyHeaders: hdrs.HostsProxyHeaders,
|
||||||
SSLProxyHeaders: headers.SSLProxyHeaders,
|
SSLProxyHeaders: hdrs.SSLProxyHeaders,
|
||||||
STSSeconds: headers.STSSeconds,
|
STSSeconds: hdrs.STSSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(resp *http.Response) error {
|
return func(resp *http.Response) error {
|
||||||
if headers.HasCustomHeadersDefined() {
|
if hdrs.HasCustomHeadersDefined() || hdrs.HasCorsHeadersDefined() {
|
||||||
// Loop through Custom response headers
|
err := headers.NewHeader(nil, *hdrs).ModifyResponseHeaders(resp)
|
||||||
for header, value := range headers.CustomResponseHeaders {
|
if err != nil {
|
||||||
if value == "" {
|
return err
|
||||||
resp.Header.Del(header)
|
|
||||||
} else {
|
|
||||||
resp.Header.Set(header, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if headers.HasSecureHeadersDefined() {
|
if hdrs.HasSecureHeadersDefined() {
|
||||||
err := secure.New(opt).ModifyResponseHeaders(resp)
|
err := secure.New(opt).ModifyResponseHeaders(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in a new issue