1
0
Fork 0

Headers response modifier is directly applied by headers middleware

Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
Julien Salleyron 2020-09-01 18:16:04 +02:00 committed by GitHub
parent 3677252e17
commit 52790d3c37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1144 additions and 1240 deletions

View file

@ -0,0 +1,170 @@
package headers
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log"
)
// 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
hasCustomHeaders bool
hasCorsHeaders bool
headers *dynamic.Headers
}
// NewHeader constructs a new header instance from supplied frontend header struct.
func NewHeader(next http.Handler, cfg dynamic.Headers) *Header {
hasCustomHeaders := cfg.HasCustomHeadersDefined()
hasCorsHeaders := cfg.HasCorsHeadersDefined()
ctx := log.With(context.Background(), log.Str(log.MiddlewareType, typeName))
handleDeprecation(ctx, &cfg)
return &Header{
next: next,
headers: &cfg,
hasCustomHeaders: hasCustomHeaders,
hasCorsHeaders: hasCorsHeaders,
}
}
func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Handle Cors headers and preflight if configured.
if isPreflight := s.processCorsHeaders(rw, req); isPreflight {
return
}
if s.hasCustomHeaders {
s.modifyCustomRequestHeaders(req)
}
// If there is a next, call it.
if s.next != nil {
s.next.ServeHTTP(newResponseModifier(rw, req, s.PostRequestModifyResponseHeaders), req)
}
}
// modifyCustomRequestHeaders sets or deletes custom request headers.
func (s *Header) modifyCustomRequestHeaders(req *http.Request) {
// Loop through Custom request headers
for header, value := range s.headers.CustomRequestHeaders {
switch {
case value == "":
req.Header.Del(header)
case strings.EqualFold(header, "Host"):
req.Host = value
default:
req.Header.Set(header, value)
}
}
}
// PostRequestModifyResponseHeaders set or delete response headers.
// This method is called AFTER the response is generated from the backend
// and can merge/override headers from the backend response.
func (s *Header) PostRequestModifyResponseHeaders(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)
}
}
if res != nil && res.Request != nil {
originHeader := res.Request.Header.Get("Origin")
allowed, match := s.isOriginAllowed(originHeader)
if allowed {
res.Header.Set("Access-Control-Allow-Origin", match)
}
}
if s.headers.AccessControlAllowCredentials {
res.Header.Set("Access-Control-Allow-Credentials", "true")
}
if len(s.headers.AccessControlExposeHeaders) > 0 {
exposeHeaders := strings.Join(s.headers.AccessControlExposeHeaders, ",")
res.Header.Set("Access-Control-Expose-Headers", exposeHeaders)
}
if !s.headers.AddVaryHeader {
return nil
}
varyHeader := res.Header.Get("Vary")
if varyHeader == "Origin" {
return nil
}
if varyHeader != "" {
varyHeader += ","
}
varyHeader += "Origin"
res.Header.Set("Vary", varyHeader)
return nil
}
// processCorsHeaders processes the incoming request,
// and returns if it is a preflight request.
// If not a preflight, it handles the preRequestModifyCorsResponseHeaders.
func (s *Header) processCorsHeaders(rw http.ResponseWriter, req *http.Request) bool {
if !s.hasCorsHeaders {
return false
}
reqAcMethod := req.Header.Get("Access-Control-Request-Method")
originHeader := req.Header.Get("Origin")
if reqAcMethod != "" && originHeader != "" && req.Method == http.MethodOptions {
// If the request is an OPTIONS request with an Access-Control-Request-Method header,
// 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)
}
allowed, match := s.isOriginAllowed(originHeader)
if allowed {
rw.Header().Set("Access-Control-Allow-Origin", match)
}
rw.Header().Set("Access-Control-Max-Age", strconv.Itoa(int(s.headers.AccessControlMaxAge)))
return true
}
return false
}
func (s *Header) isOriginAllowed(origin string) (bool, string) {
for _, item := range s.headers.AccessControlAllowOriginList {
if item == "*" || item == origin {
return true, item
}
}
return false, ""
}

View file

@ -0,0 +1,492 @@
package headers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/stretchr/testify/assert"
)
func TestNewHeader_customRequestHeader(t *testing.T) {
testCases := []struct {
desc string
cfg dynamic.Headers
expected http.Header
}{
{
desc: "adds a header",
cfg: dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-Custom-Request-Header": "test_request",
},
},
expected: http.Header{"Foo": []string{"bar"}, "X-Custom-Request-Header": []string{"test_request"}},
},
{
desc: "delete a header",
cfg: dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-Custom-Request-Header": "",
"Foo": "",
},
},
expected: http.Header{},
},
{
desc: "override a header",
cfg: dynamic.Headers{
CustomRequestHeaders: map[string]string{
"Foo": "test",
},
},
expected: http.Header{"Foo": []string{"test"}},
},
}
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
mid := NewHeader(emptyHandler, test.cfg)
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
req.Header.Set("Foo", "bar")
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, test.expected, req.Header)
})
}
}
func TestNewHeader_customRequestHeader_Host(t *testing.T) {
testCases := []struct {
desc string
customHeaders map[string]string
expectedHost string
expectedURLHost string
}{
{
desc: "standard Host header",
customHeaders: map[string]string{},
expectedHost: "example.org",
expectedURLHost: "example.org",
},
{
desc: "custom Host header",
customHeaders: map[string]string{
"Host": "example.com",
},
expectedHost: "example.com",
expectedURLHost: "example.org",
},
}
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mid := NewHeader(emptyHandler, dynamic.Headers{CustomRequestHeaders: test.customHeaders})
req := httptest.NewRequest(http.MethodGet, "http://example.org/foo", nil)
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, test.expectedHost, req.Host)
assert.Equal(t, test.expectedURLHost, req.URL.Host)
})
}
}
func TestNewHeader_CORSPreflights(t *testing.T) {
testCases := []struct {
desc string
cfg dynamic.Headers
requestHeaders http.Header
expected http.Header
}{
{
desc: "Test Simple Preflight",
cfg: dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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",
cfg: dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
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",
cfg: dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
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",
cfg: dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
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"},
},
},
{
desc: "No Request Headers Preflight",
cfg: dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"},
AccessControlMaxAge: 600,
},
requestHeaders: map[string][]string{
"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"},
},
},
}
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mid := NewHeader(emptyHandler, test.cfg)
req := httptest.NewRequest(http.MethodOptions, "/foo", nil)
req.Header = test.requestHeaders
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, test.expected, rw.Result().Header)
})
}
}
func TestNewHeader_CORSResponses(t *testing.T) {
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
testCases := []struct {
desc string
next http.Handler
cfg dynamic.Headers
requestHeaders http.Header
expected http.Header
}{
{
desc: "Test Simple Request",
next: emptyHandler,
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
},
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",
next: emptyHandler,
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
},
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Empty origin Request",
next: emptyHandler,
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
},
requestHeaders: map[string][]string{},
expected: map[string][]string{},
},
{
desc: "Not Defined origin Request",
next: emptyHandler,
requestHeaders: map[string][]string{},
expected: map[string][]string{},
},
{
desc: "Allow Credentials Request",
next: emptyHandler,
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
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",
next: emptyHandler,
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
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",
next: emptyHandler,
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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",
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// nonEmptyHandler
w.Header().Set("Vary", "Testing")
w.WriteHeader(http.StatusOK)
}),
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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"},
},
},
{
desc: "Test Simple Request with Vary Headers and existing vary:origin response",
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// existingOriginHandler
w.Header().Set("Vary", "Origin")
w.WriteHeader(http.StatusOK)
}),
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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 non-empty response: set ACAO",
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// existingAccessControlAllowOriginHandlerSet
w.Header().Set("Access-Control-Allow-Origin", "http://foo.bar.org")
w.WriteHeader(http.StatusOK)
}),
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
},
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Test Simple Request with non-empty response: add ACAO",
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// existingAccessControlAllowOriginHandlerAdd
w.Header().Add("Access-Control-Allow-Origin", "http://foo.bar.org")
w.WriteHeader(http.StatusOK)
}),
cfg: dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
},
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Test Simple CustomRequestHeaders Not Hijacked by CORS",
next: emptyHandler,
cfg: dynamic.Headers{
CustomRequestHeaders: map[string]string{"foo": "bar"},
},
requestHeaders: map[string][]string{
"Access-Control-Request-Headers": {"origin"},
"Access-Control-Request-Method": {"GET", "OPTIONS"},
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mid := NewHeader(test.next, test.cfg)
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
req.Header = test.requestHeaders
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, test.expected, rw.Result().Header)
})
}
}
func TestNewHeader_customResponseHeaders(t *testing.T) {
testCases := []struct {
desc string
config map[string]string
expected http.Header
}{
{
desc: "Test Simple Response",
config: map[string]string{
"Testing": "foo",
"Testing2": "bar",
},
expected: map[string][]string{
"Foo": {"bar"},
"Testing": {"foo"},
"Testing2": {"bar"},
},
},
{
desc: "empty Custom Header",
config: map[string]string{
"Testing": "foo",
"Testing2": "",
},
expected: map[string][]string{
"Foo": {"bar"},
"Testing": {"foo"},
},
},
{
desc: "Deleting Custom Header",
config: map[string]string{
"Testing": "foo",
"Foo": "",
},
expected: map[string][]string{
"Testing": {"foo"},
},
},
}
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Foo", "bar")
w.WriteHeader(http.StatusOK)
})
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mid := NewHeader(emptyHandler, dynamic.Headers{CustomResponseHeaders: test.config})
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, test.expected, rw.Result().Header)
})
}
}

View file

@ -5,15 +5,12 @@ import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/middlewares"
"github.com/containous/traefik/v2/pkg/tracing"
"github.com/opentracing/opentracing-go/ext"
"github.com/unrolled/secure"
)
const (
@ -55,7 +52,7 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
if hasSecureHeaders {
logger.Debugf("Setting up secureHeaders from %v", cfg)
handler = newSecure(next, cfg)
handler = newSecure(next, cfg, name)
nextHandler = handler
}
@ -77,203 +74,3 @@ func (h *headers) GetTracingInformation() (string, ext.SpanKindEnum) {
func (h *headers) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
h.handler.ServeHTTP(rw, req)
}
type secureHeader struct {
next http.Handler
secure *secure.Secure
}
// newSecure constructs a new secure instance with supplied options.
func newSecure(next http.Handler, cfg dynamic.Headers) *secureHeader {
opt := secure.Options{
BrowserXssFilter: cfg.BrowserXSSFilter,
ContentTypeNosniff: cfg.ContentTypeNosniff,
ForceSTSHeader: cfg.ForceSTSHeader,
FrameDeny: cfg.FrameDeny,
IsDevelopment: cfg.IsDevelopment,
SSLRedirect: cfg.SSLRedirect,
SSLForceHost: cfg.SSLForceHost,
SSLTemporaryRedirect: cfg.SSLTemporaryRedirect,
STSIncludeSubdomains: cfg.STSIncludeSubdomains,
STSPreload: cfg.STSPreload,
ContentSecurityPolicy: cfg.ContentSecurityPolicy,
CustomBrowserXssValue: cfg.CustomBrowserXSSValue,
CustomFrameOptionsValue: cfg.CustomFrameOptionsValue,
PublicKey: cfg.PublicKey,
ReferrerPolicy: cfg.ReferrerPolicy,
SSLHost: cfg.SSLHost,
AllowedHosts: cfg.AllowedHosts,
HostsProxyHeaders: cfg.HostsProxyHeaders,
SSLProxyHeaders: cfg.SSLProxyHeaders,
STSSeconds: cfg.STSSeconds,
FeaturePolicy: cfg.FeaturePolicy,
}
return &secureHeader{
next: next,
secure: secure.New(opt),
}
}
func (s secureHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.secure.HandlerFuncWithNextForRequestOnly(rw, req, s.next.ServeHTTP)
}
// 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
hasCustomHeaders bool
hasCorsHeaders bool
headers *dynamic.Headers
}
// NewHeader constructs a new header instance from supplied frontend header struct.
func NewHeader(next http.Handler, cfg dynamic.Headers) *Header {
hasCustomHeaders := cfg.HasCustomHeadersDefined()
hasCorsHeaders := cfg.HasCorsHeadersDefined()
ctx := log.With(context.Background(), log.Str(log.MiddlewareType, typeName))
handleDeprecation(ctx, &cfg)
return &Header{
next: next,
headers: &cfg,
hasCustomHeaders: hasCustomHeaders,
hasCorsHeaders: hasCorsHeaders,
}
}
func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Handle Cors headers and preflight if configured.
if isPreflight := s.processCorsHeaders(rw, req); isPreflight {
return
}
if s.hasCustomHeaders {
s.modifyCustomRequestHeaders(req)
}
// If there is a next, call it.
if s.next != nil {
s.next.ServeHTTP(rw, req)
}
}
// modifyCustomRequestHeaders sets or deletes custom request headers.
func (s *Header) modifyCustomRequestHeaders(req *http.Request) {
// Loop through Custom request headers
for header, value := range s.headers.CustomRequestHeaders {
switch {
case value == "":
req.Header.Del(header)
case strings.EqualFold(header, "Host"):
req.Host = value
default:
req.Header.Set(header, value)
}
}
}
// PostRequestModifyResponseHeaders set or delete response headers.
// This method is called AFTER the response is generated from the backend
// and can merge/override headers from the backend response.
func (s *Header) PostRequestModifyResponseHeaders(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)
}
}
if res != nil && res.Request != nil {
originHeader := res.Request.Header.Get("Origin")
allowed, match := s.isOriginAllowed(originHeader)
if allowed {
res.Header.Set("Access-Control-Allow-Origin", match)
}
}
if s.headers.AccessControlAllowCredentials {
res.Header.Set("Access-Control-Allow-Credentials", "true")
}
if len(s.headers.AccessControlExposeHeaders) > 0 {
exposeHeaders := strings.Join(s.headers.AccessControlExposeHeaders, ",")
res.Header.Set("Access-Control-Expose-Headers", exposeHeaders)
}
if !s.headers.AddVaryHeader {
return nil
}
varyHeader := res.Header.Get("Vary")
if varyHeader == "Origin" {
return nil
}
if varyHeader != "" {
varyHeader += ","
}
varyHeader += "Origin"
res.Header.Set("Vary", varyHeader)
return nil
}
// processCorsHeaders processes the incoming request,
// and returns if it is a preflight request.
// If not a preflight, it handles the preRequestModifyCorsResponseHeaders.
func (s *Header) processCorsHeaders(rw http.ResponseWriter, req *http.Request) bool {
if !s.hasCorsHeaders {
return false
}
reqAcMethod := req.Header.Get("Access-Control-Request-Method")
originHeader := req.Header.Get("Origin")
if reqAcMethod != "" && originHeader != "" && req.Method == http.MethodOptions {
// If the request is an OPTIONS request with an Access-Control-Request-Method header,
// 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)
}
allowed, match := s.isOriginAllowed(originHeader)
if allowed {
rw.Header().Set("Access-Control-Allow-Origin", match)
}
rw.Header().Set("Access-Control-Max-Age", strconv.Itoa(int(s.headers.AccessControlMaxAge)))
return true
}
return false
}
func (s *Header) isOriginAllowed(origin string) (bool, string) {
for _, item := range s.headers.AccessControlAllowOriginList {
if item == "*" || item == origin {
return true, item
}
}
return false, ""
}

View file

@ -9,104 +9,21 @@ import (
"testing"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/containous/traefik/v2/pkg/tracing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCustomRequestHeader(t *testing.T) {
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
func TestNew_withoutOptions(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
header := NewHeader(emptyHandler, dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-Custom-Request-Header": "test_request",
},
})
mid, err := New(context.Background(), next, dynamic.Headers{}, "testing")
require.Errorf(t, err, "headers configuration not valid")
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"))
assert.Nil(t, mid)
}
func TestCustomRequestHeader_Host(t *testing.T) {
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
testCases := []struct {
desc string
customHeaders map[string]string
expectedHost string
expectedURLHost string
}{
{
desc: "standard Host header",
customHeaders: map[string]string{},
expectedHost: "example.org",
expectedURLHost: "example.org",
},
{
desc: "custom Host header",
customHeaders: map[string]string{
"Host": "example.com",
},
expectedHost: "example.com",
expectedURLHost: "example.org",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
header := NewHeader(emptyHandler, dynamic.Headers{
CustomRequestHeaders: test.customHeaders,
})
res := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, "http://example.org/foo", nil)
require.NoError(t, err)
header.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, test.expectedHost, req.Host)
assert.Equal(t, test.expectedURLHost, req.URL.Host)
})
}
}
func TestCustomRequestHeaderEmptyValue(t *testing.T) {
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
header := NewHeader(emptyHandler, dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-Custom-Request-Header": "test_request",
},
})
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"))
header = NewHeader(emptyHandler, dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-Custom-Request-Header": "",
},
})
header.ServeHTTP(res, req)
assert.Equal(t, http.StatusOK, res.Code)
assert.Equal(t, "", req.Header.Get("X-Custom-Request-Header"))
}
func TestSecureHeader(t *testing.T) {
func TestNew_allowedHosts(t *testing.T) {
testCases := []struct {
desc string
fromHost string
@ -129,10 +46,13 @@ func TestSecureHeader(t *testing.T) {
},
}
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
header, err := New(context.Background(), emptyHandler, dynamic.Headers{
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
cfg := dynamic.Headers{
AllowedHosts: []string{"foo.com", "bar.com"},
}, "foo")
}
mid, err := New(context.Background(), emptyHandler, cfg, "foo")
require.NoError(t, err)
for _, test := range testCases {
@ -140,479 +60,54 @@ func TestSecureHeader(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
res := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil)
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
req.Host = test.fromHost
header.ServeHTTP(res, req)
assert.Equal(t, test.expected, res.Code)
})
}
}
func TestSSLForceHost(t *testing.T) {
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, _ = rw.Write([]byte("OK"))
})
testCases := []struct {
desc string
host string
secureMiddleware *secureHeader
expected int
}{
{
desc: "http should return a 301",
host: "http://powpow.example.com",
secureMiddleware: newSecure(next, dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
}),
expected: http.StatusMovedPermanently,
},
{
desc: "http sub domain should return a 301",
host: "http://www.powpow.example.com",
secureMiddleware: newSecure(next, dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
}),
expected: http.StatusMovedPermanently,
},
{
desc: "https should return a 200",
host: "https://powpow.example.com",
secureMiddleware: newSecure(next, dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
}),
expected: http.StatusOK,
},
{
desc: "https sub domain should return a 301",
host: "https://www.powpow.example.com",
secureMiddleware: newSecure(next, dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
}),
expected: http.StatusMovedPermanently,
},
{
desc: "http without force host and sub domain should return a 301",
host: "http://www.powpow.example.com",
secureMiddleware: newSecure(next, dynamic.Headers{
SSLRedirect: true,
SSLForceHost: false,
SSLHost: "powpow.example.com",
}),
expected: http.StatusMovedPermanently,
},
{
desc: "https without force host and sub domain should return a 301",
host: "https://www.powpow.example.com",
secureMiddleware: newSecure(next, dynamic.Headers{
SSLRedirect: true,
SSLForceHost: false,
SSLHost: "powpow.example.com",
}),
expected: http.StatusOK,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
req := testhelpers.MustNewRequest(http.MethodGet, test.host, nil)
rw := httptest.NewRecorder()
test.secureMiddleware.ServeHTTP(rw, req)
assert.Equal(t, test.expected, rw.Result().StatusCode)
mid.ServeHTTP(rw, req)
assert.Equal(t, test.expected, rw.Code)
})
}
}
func TestCORSPreflights(t *testing.T) {
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
func TestNew_customHeaders(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
testCases := []struct {
desc string
header *Header
requestHeaders http.Header
expected http.Header
}{
{
desc: "Test Simple Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
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, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
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, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
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"},
},
},
{
desc: "No Request Headers Preflight",
header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowMethods: []string{"GET", "OPTIONS", "PUT"},
AccessControlAllowOriginList: []string{"*"},
AccessControlAllowHeaders: []string{"origin", "X-Forwarded-For"},
AccessControlMaxAge: 600,
}),
requestHeaders: map[string][]string{
"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, dynamic.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, dynamic.Headers{
cfg := dynamic.Headers{
CustomRequestHeaders: map[string]string{
"X-Custom-Request-Header": "test_request",
},
}, "testing")
CustomResponseHeaders: map[string]string{
"X-Custom-Response-Header": "test_response",
},
}
res := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "/foo", nil)
mid, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
header.ServeHTTP(res, req)
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
assert.Equal(t, http.StatusOK, res.Code)
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, "test_request", req.Header.Get("X-Custom-Request-Header"))
assert.Equal(t, "test_response", rw.Header().Get("X-Custom-Response-Header"))
}
func TestGetTracingInformation(t *testing.T) {
func Test_headers_getTracingInformation(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
header := &headers{
mid := &headers{
handler: next,
name: "testing",
}
name, trace := header.GetTracingInformation()
name, trace := mid.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") })
existingOriginHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Origin") })
existingAccessControlAllowOriginHandlerSet := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "http://foo.bar.org")
})
existingAccessControlAllowOriginHandlerAdd := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "http://foo.bar.org")
})
testCases := []struct {
desc string
header *Header
requestHeaders http.Header
expected http.Header
}{
{
desc: "Test Simple Request",
header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
}),
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, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
}),
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Empty origin Request",
header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
}),
requestHeaders: map[string][]string{},
expected: map[string][]string{},
},
{
desc: "Not Defined origin Request",
header: NewHeader(emptyHandler, dynamic.Headers{}),
requestHeaders: map[string][]string{},
expected: map[string][]string{},
},
{
desc: "Allow Credentials Request",
header: NewHeader(emptyHandler, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
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, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
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, dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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, dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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"},
},
},
{
desc: "Test Simple Request with Vary Headers and existing vary:origin response",
header: NewHeader(existingOriginHandler, dynamic.Headers{
AccessControlAllowOriginList: []string{"https://foo.bar.org"},
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 non-empty response: set ACAO",
header: NewHeader(existingAccessControlAllowOriginHandlerSet, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
}),
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Test Simple Request with non-empty response: add ACAO",
header: NewHeader(existingAccessControlAllowOriginHandlerAdd, dynamic.Headers{
AccessControlAllowOriginList: []string{"*"},
}),
requestHeaders: map[string][]string{
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{
"Access-Control-Allow-Origin": {"*"},
},
},
{
desc: "Test Simple CustomRequestHeaders Not Hijacked by CORS",
header: NewHeader(emptyHandler, dynamic.Headers{
CustomRequestHeaders: map[string]string{"foo": "bar"},
}),
requestHeaders: map[string][]string{
"Access-Control-Request-Headers": {"origin"},
"Access-Control-Request-Method": {"GET", "OPTIONS"},
"Origin": {"https://foo.bar.org"},
},
expected: map[string][]string{},
},
}
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)
res := rw.Result()
res.Request = req
err := test.header.PostRequestModifyResponseHeaders(res)
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, dynamic.Headers{
CustomResponseHeaders: map[string]string{
"Testing": "foo",
"Testing2": "bar",
},
}),
expected: map[string][]string{
"Testing": {"foo"},
"Testing2": {"bar"},
},
},
{
desc: "Deleting Custom Header",
header: NewHeader(emptyHandler, dynamic.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.PostRequestModifyResponseHeaders(rw.Result())
require.NoError(t, err)
assert.Equal(t, test.expected, rw.Result().Header)
})
}
}

View file

@ -0,0 +1,75 @@
package headers
import (
"net/http"
"github.com/containous/traefik/v2/pkg/log"
)
type responseModifier struct {
r *http.Request
w http.ResponseWriter
headersSent bool // whether headers have already been sent
code int // status code, must default to 200
modifier func(*http.Response) error // can be nil
modified bool // whether modifier has already been called for the current request
modifierErr error // returned by modifier call
}
// modifier can be nil.
func newResponseModifier(w http.ResponseWriter, r *http.Request, modifier func(*http.Response) error) *responseModifier {
return &responseModifier{
r: r,
w: w,
modifier: modifier,
code: http.StatusOK,
}
}
func (w *responseModifier) WriteHeader(code int) {
if w.headersSent {
return
}
defer func() {
w.code = code
w.headersSent = true
}()
if w.modifier == nil || w.modified {
w.w.WriteHeader(code)
return
}
resp := http.Response{
Header: w.w.Header(),
Request: w.r,
}
if err := w.modifier(&resp); err != nil {
w.modifierErr = err
// we are propagating when we are called in Write, but we're logging anyway,
// because we could be called from another place which does not take care of
// checking w.modifierErr.
log.WithoutContext().Errorf("Error when applying response modifier: %v", err)
w.w.WriteHeader(http.StatusInternalServerError)
return
}
w.modified = true
w.w.WriteHeader(code)
}
func (w *responseModifier) Header() http.Header {
return w.w.Header()
}
func (w *responseModifier) Write(b []byte) (int, error) {
w.WriteHeader(w.code)
if w.modifierErr != nil {
return 0, w.modifierErr
}
return w.w.Write(b)
}

View file

@ -0,0 +1,54 @@
package headers
import (
"net/http"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/unrolled/secure"
)
type secureHeader struct {
next http.Handler
secure *secure.Secure
cfg dynamic.Headers
}
// newSecure constructs a new secure instance with supplied options.
func newSecure(next http.Handler, cfg dynamic.Headers, contextKey string) *secureHeader {
opt := secure.Options{
BrowserXssFilter: cfg.BrowserXSSFilter,
ContentTypeNosniff: cfg.ContentTypeNosniff,
ForceSTSHeader: cfg.ForceSTSHeader,
FrameDeny: cfg.FrameDeny,
IsDevelopment: cfg.IsDevelopment,
SSLRedirect: cfg.SSLRedirect,
SSLForceHost: cfg.SSLForceHost,
SSLTemporaryRedirect: cfg.SSLTemporaryRedirect,
STSIncludeSubdomains: cfg.STSIncludeSubdomains,
STSPreload: cfg.STSPreload,
ContentSecurityPolicy: cfg.ContentSecurityPolicy,
CustomBrowserXssValue: cfg.CustomBrowserXSSValue,
CustomFrameOptionsValue: cfg.CustomFrameOptionsValue,
PublicKey: cfg.PublicKey,
ReferrerPolicy: cfg.ReferrerPolicy,
SSLHost: cfg.SSLHost,
AllowedHosts: cfg.AllowedHosts,
HostsProxyHeaders: cfg.HostsProxyHeaders,
SSLProxyHeaders: cfg.SSLProxyHeaders,
STSSeconds: cfg.STSSeconds,
FeaturePolicy: cfg.FeaturePolicy,
SecureContextKey: contextKey,
}
return &secureHeader{
next: next,
secure: secure.New(opt),
cfg: cfg,
}
}
func (s secureHeader) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.secure.HandlerFuncWithNextForRequestOnly(rw, req, func(writer http.ResponseWriter, request *http.Request) {
s.next.ServeHTTP(newResponseModifier(writer, request, s.secure.ModifyResponseHeaders), request)
})
}

View file

@ -0,0 +1,191 @@
package headers
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/stretchr/testify/assert"
)
// Middleware tests based on https://github.com/unrolled/secure
func Test_newSecure_sslForceHost(t *testing.T) {
type expected struct {
statusCode int
location string
}
testCases := []struct {
desc string
host string
cfg dynamic.Headers
expected
}{
{
desc: "http should return a 301",
host: "http://powpow.example.com",
cfg: dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
},
expected: expected{
statusCode: http.StatusMovedPermanently,
location: "https://powpow.example.com",
},
},
{
desc: "http sub domain should return a 301",
host: "http://www.powpow.example.com",
cfg: dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
},
expected: expected{
statusCode: http.StatusMovedPermanently,
location: "https://powpow.example.com",
},
},
{
desc: "https should return a 200",
host: "https://powpow.example.com",
cfg: dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
},
expected: expected{statusCode: http.StatusOK},
},
{
desc: "https sub domain should return a 301",
host: "https://www.powpow.example.com",
cfg: dynamic.Headers{
SSLRedirect: true,
SSLForceHost: true,
SSLHost: "powpow.example.com",
},
expected: expected{
statusCode: http.StatusMovedPermanently,
location: "https://powpow.example.com",
},
},
{
desc: "http without force host and sub domain should return a 301",
host: "http://www.powpow.example.com",
cfg: dynamic.Headers{
SSLRedirect: true,
SSLForceHost: false,
SSLHost: "powpow.example.com",
},
expected: expected{
statusCode: http.StatusMovedPermanently,
location: "https://powpow.example.com",
},
},
{
desc: "https without force host and sub domain should return a 301",
host: "https://www.powpow.example.com",
cfg: dynamic.Headers{
SSLRedirect: true,
SSLForceHost: false,
SSLHost: "powpow.example.com",
},
expected: expected{statusCode: http.StatusOK},
},
}
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, _ = rw.Write([]byte("OK"))
})
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
mid := newSecure(next, test.cfg, "mymiddleware")
req := httptest.NewRequest(http.MethodGet, test.host, nil)
rw := httptest.NewRecorder()
mid.ServeHTTP(rw, req)
assert.Equal(t, test.expected.statusCode, rw.Result().StatusCode)
assert.Equal(t, test.expected.location, rw.Header().Get("Location"))
})
}
}
func Test_newSecure_modifyResponse(t *testing.T) {
testCases := []struct {
desc string
cfg dynamic.Headers
expected http.Header
}{
{
desc: "FeaturePolicy",
cfg: dynamic.Headers{
FeaturePolicy: "vibrate 'none';",
},
expected: http.Header{"Feature-Policy": []string{"vibrate 'none';"}},
},
{
desc: "STSSeconds",
cfg: dynamic.Headers{
STSSeconds: 1,
ForceSTSHeader: true,
},
expected: http.Header{"Strict-Transport-Security": []string{"max-age=1"}},
},
{
desc: "STSSeconds and STSPreload",
cfg: dynamic.Headers{
STSSeconds: 1,
ForceSTSHeader: true,
STSPreload: true,
},
expected: http.Header{"Strict-Transport-Security": []string{"max-age=1; preload"}},
},
{
desc: "CustomFrameOptionsValue",
cfg: dynamic.Headers{
CustomFrameOptionsValue: "foo",
},
expected: http.Header{"X-Frame-Options": []string{"foo"}},
},
{
desc: "FrameDeny",
cfg: dynamic.Headers{
FrameDeny: true,
},
expected: http.Header{"X-Frame-Options": []string{"DENY"}},
},
{
desc: "ContentTypeNosniff",
cfg: dynamic.Headers{
ContentTypeNosniff: true,
},
expected: http.Header{"X-Content-Type-Options": []string{"nosniff"}},
},
}
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
secure := newSecure(emptyHandler, test.cfg, "mymiddleware")
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
rw := httptest.NewRecorder()
secure.ServeHTTP(rw, req)
assert.Equal(t, test.expected, rw.Result().Header)
})
}
}