Enable CORS configuration

This commit is contained in:
Daniel Tomcej 2019-04-02 03:40:04 -05:00 committed by Traefiker Bot
parent 883f90dded
commit d2b38e6ac4
13 changed files with 888 additions and 89 deletions

View file

@ -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,50 +45,100 @@ 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
```
```yml
a-container:
image: a-container-image
labels:
- "traefik.http.middlewares.testHeader.Headers.FrameDeny=true",
- "traefik.http.middlewares.testHeader.Headers.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
```
## 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.
### 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.

View file

@ -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

View 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

View 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
View 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)
}
}

View file

@ -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{})

View file

@ -0,0 +1,4 @@
whoami1:
image: containous/whoami
ports:
- "8881:80"

View file

@ -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

View file

@ -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 ||

View file

@ -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 ""
}
}

View file

@ -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)
})
}
}

View file

@ -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",

View file

@ -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