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
|
||||
|
||||
```yaml tab="Docker"
|
||||
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",
|
||||
- "traefik.http.middlewares.testHeader.Headers.CustomRequestHeaders.X-Script-Name=test"
|
||||
- "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"
|
||||
|
@ -34,51 +45,101 @@ 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.
|
||||
|
||||
??? 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
|
||||
[http.middlewares]
|
||||
[http.middlewares.testHeader.headers]
|
||||
[http.middlewares.testHeader.headers.CustomRequestHeaders]
|
||||
X-Script-Name = "test"
|
||||
[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"
|
||||
```
|
||||
|
||||
??? 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",
|
||||
```
|
||||
```toml tab="File"
|
||||
[http.middlewares]
|
||||
[http.middlewares.testHeader.headers]
|
||||
[http.middlewares.testHeader.headers.CustomRequestHeaders]
|
||||
X-Script-Name = "test"
|
||||
[http.middlewares.testHeader.headers.CustomResponseHeaders]
|
||||
X-Custom-Response-Header = "True"
|
||||
```
|
||||
|
||||
### 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.
|
||||
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
|
||||
[http.middlewares]
|
||||
[http.middlewares.testHeader.headers]
|
||||
FrameDeny = true
|
||||
SSLRedirect = true
|
||||
```
|
||||
```yaml tab="Kubernetes"
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: testHeader
|
||||
spec:
|
||||
headers:
|
||||
FrameDeny: "true"
|
||||
SSLRedirect: "true"
|
||||
```
|
||||
|
||||
??? example "Docker"
|
||||
```toml tab="File"
|
||||
[http.middlewares]
|
||||
[http.middlewares.testHeader.headers]
|
||||
FrameDeny = true
|
||||
SSLRedirect = true
|
||||
```
|
||||
|
||||
### CORS Headers
|
||||
|
||||
CORS (Cross-Origin Resource Sharing) headers can be added and configured per frontend in a similar manner to the custom headers above.
|
||||
This functionality allows for more advanced security features to quickly be set.
|
||||
|
||||
```yaml tab="Docker"
|
||||
labels:
|
||||
- "traefik.http.middlewares.testHeader.Headers.AccessControlAllowMethods=GET,OPTIONS,PUT"
|
||||
- "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
|
||||
```
|
||||
|
||||
```yml
|
||||
a-container:
|
||||
image: a-container-image
|
||||
labels:
|
||||
- "traefik.http.middlewares.testHeader.Headers.FrameDeny=true",
|
||||
- "traefik.http.middlewares.testHeader.Headers.SSLRedirect=true",
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### General
|
||||
|
@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
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"]
|
||||
|
||||
[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"]
|
||||
hostsProxyHeaders = ["foobar", "foobar"]
|
||||
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(&GRPCSuite{})
|
||||
check.Suite(&HealthCheckSuite{})
|
||||
check.Suite(&HeadersSuite{})
|
||||
check.Suite(&HostResolverSuite{})
|
||||
check.Suite(&HTTPSSuite{})
|
||||
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"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"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.
|
||||
// It returns an error
|
||||
type DoCondition func() error
|
||||
|
|
|
@ -126,6 +126,21 @@ type Headers struct {
|
|||
CustomRequestHeaders map[string]string `json:"customRequestHeaders,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"`
|
||||
HostsProxyHeaders []string `json:"hostsProxyHeaders,omitempty"`
|
||||
SSLRedirect bool `json:"sslRedirect,omitempty"`
|
||||
|
@ -154,6 +169,17 @@ func (h *Headers) HasCustomHeadersDefined() bool {
|
|||
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
|
||||
func (h *Headers) HasSecureHeadersDefined() bool {
|
||||
return h != nil && (len(h.AllowedHosts) != 0 ||
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/traefik/pkg/config"
|
||||
"github.com/containous/traefik/pkg/middlewares"
|
||||
|
@ -14,7 +16,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
typeName = "Headers"
|
||||
typeName = "Headers"
|
||||
originHeaderKey = "X-Request-Origin"
|
||||
)
|
||||
|
||||
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.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")
|
||||
}
|
||||
|
||||
var handler http.Handler
|
||||
nextHandler := next
|
||||
|
||||
if config.HasSecureHeadersDefined() {
|
||||
if hasSecureHeaders {
|
||||
logger.Debug("Setting up secureHeaders from %v", config)
|
||||
handler = newSecure(next, config)
|
||||
nextHandler = handler
|
||||
}
|
||||
|
||||
if config.HasCustomHeadersDefined() {
|
||||
logger.Debug("Setting up customHeaders from %v", config)
|
||||
handler = newHeader(nextHandler, config)
|
||||
if hasCustomHeaders || hasCorsHeaders {
|
||||
logger.Debug("Setting up customHeaders/Cors from %v", config)
|
||||
handler = NewHeader(nextHandler, config)
|
||||
}
|
||||
|
||||
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
|
||||
// provided to configure which features should be enabled, and the ability to override a few of the default values.
|
||||
type header struct {
|
||||
next http.Handler
|
||||
// If Custom request headers are set, these will be added to the request
|
||||
customRequestHeaders map[string]string
|
||||
type Header struct {
|
||||
next http.Handler
|
||||
headers *config.Headers
|
||||
}
|
||||
|
||||
// NewHeader constructs a new header instance from supplied frontend header struct.
|
||||
func newHeader(next http.Handler, headers config.Headers) *header {
|
||||
return &header{
|
||||
next: next,
|
||||
customRequestHeaders: headers.CustomRequestHeaders,
|
||||
func NewHeader(next http.Handler, headers config.Headers) *Header {
|
||||
return &Header{
|
||||
next: next,
|
||||
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.next.ServeHTTP(rw, req)
|
||||
// If there is a next, call it.
|
||||
if s.next != nil {
|
||||
s.next.ServeHTTP(rw, req)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
for header, value := range s.customRequestHeaders {
|
||||
for header, value := range s.headers.CustomRequestHeaders {
|
||||
if value == "" {
|
||||
req.Header.Del(header)
|
||||
} 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/testhelpers"
|
||||
"github.com/containous/traefik/pkg/tracing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -17,7 +18,7 @@ import (
|
|||
func TestCustomRequestHeader(t *testing.T) {
|
||||
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
header := newHeader(emptyHandler, config.Headers{
|
||||
header := NewHeader(emptyHandler, config.Headers{
|
||||
CustomRequestHeaders: map[string]string{
|
||||
"X-Custom-Request-Header": "test_request",
|
||||
},
|
||||
|
@ -35,7 +36,7 @@ func TestCustomRequestHeader(t *testing.T) {
|
|||
func TestCustomRequestHeaderEmptyValue(t *testing.T) {
|
||||
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
header := newHeader(emptyHandler, config.Headers{
|
||||
header := NewHeader(emptyHandler, config.Headers{
|
||||
CustomRequestHeaders: map[string]string{
|
||||
"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, "test_request", req.Header.Get("X-Custom-Request-Header"))
|
||||
|
||||
header = newHeader(emptyHandler, config.Headers{
|
||||
header = NewHeader(emptyHandler, config.Headers{
|
||||
CustomRequestHeaders: map[string]string{
|
||||
"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.key": "foobar",
|
||||
"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.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.contentsecuritypolicy": "foobar",
|
||||
"traefik.http.middlewares.Middleware8.headers.contenttypenosniff": "true",
|
||||
|
@ -377,6 +384,22 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"name0": "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{
|
||||
"foobar",
|
||||
"fiibar",
|
||||
|
@ -710,6 +733,22 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"name0": "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{
|
||||
"foobar",
|
||||
"fiibar",
|
||||
|
@ -854,6 +893,13 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar",
|
||||
"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.BrowserXSSFilter": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar",
|
||||
|
|
|
@ -4,46 +4,43 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/containous/traefik/pkg/config"
|
||||
"github.com/containous/traefik/pkg/middlewares/headers"
|
||||
"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{
|
||||
BrowserXssFilter: headers.BrowserXSSFilter,
|
||||
ContentTypeNosniff: headers.ContentTypeNosniff,
|
||||
ForceSTSHeader: headers.ForceSTSHeader,
|
||||
FrameDeny: headers.FrameDeny,
|
||||
IsDevelopment: headers.IsDevelopment,
|
||||
SSLRedirect: headers.SSLRedirect,
|
||||
SSLForceHost: headers.SSLForceHost,
|
||||
SSLTemporaryRedirect: headers.SSLTemporaryRedirect,
|
||||
STSIncludeSubdomains: headers.STSIncludeSubdomains,
|
||||
STSPreload: headers.STSPreload,
|
||||
ContentSecurityPolicy: headers.ContentSecurityPolicy,
|
||||
CustomBrowserXssValue: headers.CustomBrowserXSSValue,
|
||||
CustomFrameOptionsValue: headers.CustomFrameOptionsValue,
|
||||
PublicKey: headers.PublicKey,
|
||||
ReferrerPolicy: headers.ReferrerPolicy,
|
||||
SSLHost: headers.SSLHost,
|
||||
AllowedHosts: headers.AllowedHosts,
|
||||
HostsProxyHeaders: headers.HostsProxyHeaders,
|
||||
SSLProxyHeaders: headers.SSLProxyHeaders,
|
||||
STSSeconds: headers.STSSeconds,
|
||||
BrowserXssFilter: hdrs.BrowserXSSFilter,
|
||||
ContentTypeNosniff: hdrs.ContentTypeNosniff,
|
||||
ForceSTSHeader: hdrs.ForceSTSHeader,
|
||||
FrameDeny: hdrs.FrameDeny,
|
||||
IsDevelopment: hdrs.IsDevelopment,
|
||||
SSLRedirect: hdrs.SSLRedirect,
|
||||
SSLForceHost: hdrs.SSLForceHost,
|
||||
SSLTemporaryRedirect: hdrs.SSLTemporaryRedirect,
|
||||
STSIncludeSubdomains: hdrs.STSIncludeSubdomains,
|
||||
STSPreload: hdrs.STSPreload,
|
||||
ContentSecurityPolicy: hdrs.ContentSecurityPolicy,
|
||||
CustomBrowserXssValue: hdrs.CustomBrowserXSSValue,
|
||||
CustomFrameOptionsValue: hdrs.CustomFrameOptionsValue,
|
||||
PublicKey: hdrs.PublicKey,
|
||||
ReferrerPolicy: hdrs.ReferrerPolicy,
|
||||
SSLHost: hdrs.SSLHost,
|
||||
AllowedHosts: hdrs.AllowedHosts,
|
||||
HostsProxyHeaders: hdrs.HostsProxyHeaders,
|
||||
SSLProxyHeaders: hdrs.SSLProxyHeaders,
|
||||
STSSeconds: hdrs.STSSeconds,
|
||||
}
|
||||
|
||||
return func(resp *http.Response) error {
|
||||
if headers.HasCustomHeadersDefined() {
|
||||
// Loop through Custom response headers
|
||||
for header, value := range headers.CustomResponseHeaders {
|
||||
if value == "" {
|
||||
resp.Header.Del(header)
|
||||
} else {
|
||||
resp.Header.Set(header, value)
|
||||
}
|
||||
if hdrs.HasCustomHeadersDefined() || hdrs.HasCorsHeadersDefined() {
|
||||
err := headers.NewHeader(nil, *hdrs).ModifyResponseHeaders(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if headers.HasSecureHeadersDefined() {
|
||||
if hdrs.HasSecureHeadersDefined() {
|
||||
err := secure.New(opt).ModifyResponseHeaders(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
Loading…
Reference in a new issue