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

View file

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

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(&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{})

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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