Allow to use regular expressions for AccessControlAllowOriginList
This commit is contained in:
parent
699cf71652
commit
b5198e63c4
11 changed files with 133 additions and 17 deletions
|
@ -306,7 +306,7 @@ The `accessControlAllowOriginList` indicates whether a resource can be shared by
|
|||
A wildcard origin `*` can also be configured, and will match all requests.
|
||||
If this value is set by a backend server, it will be overwritten by Traefik
|
||||
|
||||
This value can contains a list of allowed origins.
|
||||
This value can contain a list of allowed origins.
|
||||
|
||||
More information including how to use the settings can be found on:
|
||||
|
||||
|
@ -316,6 +316,14 @@ More information including how to use the settings can be found on:
|
|||
|
||||
Traefik no longer supports the null value, as it is [no longer recommended as a return value](https://w3c.github.io/webappsec-cors-for-developers/#avoid-returning-access-control-allow-origin-null).
|
||||
|
||||
### `accessControlAllowOriginListRegex`
|
||||
|
||||
The `accessControlAllowOriginListRegex` option is the counterpart of the `accessControlAllowOriginList` option with regular expressions instead of origin values.
|
||||
It will allow all origin that contains any match of a regular expression in the `accessControlAllowOriginList`.
|
||||
|
||||
!!! tip
|
||||
Regular expressions can be tested using online tools such as [Go Playground](https://play.golang.org/p/mWU9p-wk2ru) or the [Regex101](https://regex101.com/r/58sIgx/2).
|
||||
|
||||
### `accessControlExposeHeaders`
|
||||
|
||||
The `accessControlExposeHeaders` indicates which headers are safe to expose to the api of a CORS API specification.
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
- "traefik.http.middlewares.middleware10.headers.accesscontrolallowmethods=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworigin=foobar"
|
||||
- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlist=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware10.headers.accesscontrolalloworiginlistregex=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware10.headers.accesscontrolexposeheaders=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware10.headers.accesscontrolmaxage=42"
|
||||
- "traefik.http.middlewares.middleware10.headers.addvaryheader=true"
|
||||
|
|
|
@ -153,6 +153,7 @@
|
|||
accessControlAllowMethods = ["foobar", "foobar"]
|
||||
accessControlAllowOrigin = "foobar"
|
||||
accessControlAllowOriginList = ["foobar", "foobar"]
|
||||
accessControlAllowOriginListRegex = ["foobar", "foobar"]
|
||||
accessControlExposeHeaders = ["foobar", "foobar"]
|
||||
accessControlMaxAge = 42
|
||||
addVaryHeader = true
|
||||
|
|
|
@ -180,6 +180,9 @@ http:
|
|||
accessControlAllowOriginList:
|
||||
- foobar
|
||||
- foobar
|
||||
accessControlAllowOriginListRegex:
|
||||
- foobar
|
||||
- foobar
|
||||
accessControlExposeHeaders:
|
||||
- foobar
|
||||
- foobar
|
||||
|
|
|
@ -45,6 +45,8 @@
|
|||
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOrigin` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginList/0` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginList/1` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginListRegex/0` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlAllowOriginListRegex/1` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/0` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlExposeHeaders/1` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/headers/accessControlMaxAge` | `42` |
|
||||
|
|
|
@ -164,6 +164,8 @@ type Headers struct {
|
|||
AccessControlAllowOrigin string `json:"accessControlAllowOrigin,omitempty" toml:"accessControlAllowOrigin,omitempty" yaml:"accessControlAllowOrigin,omitempty"` // Deprecated
|
||||
// AccessControlAllowOriginList is a list of allowable origins. Can also be a wildcard origin "*".
|
||||
AccessControlAllowOriginList []string `json:"accessControlAllowOriginList,omitempty" toml:"accessControlAllowOriginList,omitempty" yaml:"accessControlAllowOriginList,omitempty"`
|
||||
// AccessControlAllowOriginListRegex is a list of allowable origins written following the Regular Expression syntax (https://golang.org/pkg/regexp/).
|
||||
AccessControlAllowOriginListRegex []string `json:"accessControlAllowOriginListRegex,omitempty" toml:"accessControlAllowOriginListRegex,omitempty" yaml:"accessControlAllowOriginListRegex,omitempty"`
|
||||
// AccessControlExposeHeaders sets valid headers for the response.
|
||||
AccessControlExposeHeaders []string `json:"accessControlExposeHeaders,omitempty" toml:"accessControlExposeHeaders,omitempty" yaml:"accessControlExposeHeaders,omitempty"`
|
||||
// AccessControlMaxAge sets the time that a preflight request may be cached.
|
||||
|
@ -206,6 +208,7 @@ func (h *Headers) HasCorsHeadersDefined() bool {
|
|||
len(h.AccessControlAllowHeaders) != 0 ||
|
||||
len(h.AccessControlAllowMethods) != 0 ||
|
||||
len(h.AccessControlAllowOriginList) != 0 ||
|
||||
len(h.AccessControlAllowOriginListRegex) != 0 ||
|
||||
len(h.AccessControlExposeHeaders) != 0 ||
|
||||
h.AccessControlMaxAge != 0 ||
|
||||
h.AddVaryHeader)
|
||||
|
|
|
@ -49,6 +49,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowmethods": "GET, PUT",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolalloworigin": "foobar",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolalloworiginList": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolalloworiginListRegex": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolexposeheaders": "X-foobar, X-fiibar",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolmaxage": "200",
|
||||
"traefik.http.middlewares.Middleware8.headers.addvaryheader": "true",
|
||||
|
@ -527,6 +528,10 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
AccessControlAllowOriginListRegex: []string{
|
||||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
AccessControlExposeHeaders: []string{
|
||||
"X-foobar",
|
||||
"X-fiibar",
|
||||
|
@ -999,6 +1004,10 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
AccessControlAllowOriginListRegex: []string{
|
||||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
AccessControlExposeHeaders: []string{
|
||||
"X-foobar",
|
||||
"X-fiibar",
|
||||
|
@ -1155,6 +1164,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOrigin": "foobar",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOriginList": "foobar, fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowOriginListRegex": "foobar, fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlExposeHeaders": "X-foobar, X-fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlMaxAge": "200",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AddVaryHeader": "true",
|
||||
|
|
|
@ -2,7 +2,9 @@ package headers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -14,26 +16,37 @@ import (
|
|||
// 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
|
||||
next http.Handler
|
||||
hasCustomHeaders bool
|
||||
hasCorsHeaders bool
|
||||
headers *dynamic.Headers
|
||||
allowOriginRegexes []*regexp.Regexp
|
||||
}
|
||||
|
||||
// NewHeader constructs a new header instance from supplied frontend header struct.
|
||||
func NewHeader(next http.Handler, cfg dynamic.Headers) *Header {
|
||||
func NewHeader(next http.Handler, cfg dynamic.Headers) (*Header, error) {
|
||||
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,
|
||||
regexes := make([]*regexp.Regexp, len(cfg.AccessControlAllowOriginListRegex))
|
||||
for i, str := range cfg.AccessControlAllowOriginListRegex {
|
||||
reg, err := regexp.Compile(str)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error occurred during origin parsing: %w", err)
|
||||
}
|
||||
regexes[i] = reg
|
||||
}
|
||||
|
||||
return &Header{
|
||||
next: next,
|
||||
headers: &cfg,
|
||||
hasCustomHeaders: hasCustomHeaders,
|
||||
hasCorsHeaders: hasCorsHeaders,
|
||||
allowOriginRegexes: regexes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Header) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
@ -166,5 +179,11 @@ func (s *Header) isOriginAllowed(origin string) (bool, string) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, regex := range s.allowOriginRegexes {
|
||||
if regex.MatchString(origin) {
|
||||
return true, origin
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||
)
|
||||
|
||||
|
@ -52,7 +53,8 @@ func TestNewHeader_customRequestHeader(t *testing.T) {
|
|||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mid := NewHeader(emptyHandler, test.cfg)
|
||||
mid, err := NewHeader(emptyHandler, test.cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
req.Header.Set("Foo", "bar")
|
||||
|
@ -94,7 +96,8 @@ func TestNewHeader_customRequestHeader_Host(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
mid := NewHeader(emptyHandler, dynamic.Headers{CustomRequestHeaders: test.customHeaders})
|
||||
mid, err := NewHeader(emptyHandler, dynamic.Headers{CustomRequestHeaders: test.customHeaders})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://example.org/foo", nil)
|
||||
|
||||
|
@ -217,7 +220,8 @@ func TestNewHeader_CORSPreflights(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
mid := NewHeader(emptyHandler, test.cfg)
|
||||
mid, err := NewHeader(emptyHandler, test.cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodOptions, "/foo", nil)
|
||||
req.Header = test.requestHeaders
|
||||
|
@ -240,6 +244,7 @@ func TestNewHeader_CORSResponses(t *testing.T) {
|
|||
cfg dynamic.Headers
|
||||
requestHeaders http.Header
|
||||
expected http.Header
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
desc: "Test Simple Request",
|
||||
|
@ -267,6 +272,54 @@ func TestNewHeader_CORSResponses(t *testing.T) {
|
|||
"Access-Control-Allow-Origin": {"*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Regexp Origin Request",
|
||||
next: emptyHandler,
|
||||
cfg: dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{"^https?://([a-z]+)\\.bar\\.org$"},
|
||||
},
|
||||
requestHeaders: map[string][]string{
|
||||
"Origin": {"https://foo.bar.org"},
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Partial Regexp Origin Request",
|
||||
next: emptyHandler,
|
||||
cfg: dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{"([a-z]+)\\.bar"},
|
||||
},
|
||||
requestHeaders: map[string][]string{
|
||||
"Origin": {"https://foo.bar.org"},
|
||||
},
|
||||
expected: map[string][]string{
|
||||
"Access-Control-Allow-Origin": {"https://foo.bar.org"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Regexp Malformed Origin Request",
|
||||
next: emptyHandler,
|
||||
cfg: dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{"a(b"},
|
||||
},
|
||||
requestHeaders: map[string][]string{
|
||||
"Origin": {"https://foo.bar.org"},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
desc: "Regexp Origin Request without matching",
|
||||
next: emptyHandler,
|
||||
cfg: dynamic.Headers{
|
||||
AccessControlAllowOriginListRegex: []string{"([a-z]+)\\.bar\\.org"},
|
||||
},
|
||||
requestHeaders: map[string][]string{
|
||||
"Origin": {"https://bar.org"},
|
||||
},
|
||||
expected: map[string][]string{},
|
||||
},
|
||||
{
|
||||
desc: "Empty origin Request",
|
||||
next: emptyHandler,
|
||||
|
@ -416,7 +469,12 @@ func TestNewHeader_CORSResponses(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
mid := NewHeader(test.next, test.cfg)
|
||||
mid, err := NewHeader(test.next, test.cfg)
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
req.Header = test.requestHeaders
|
||||
|
@ -478,7 +536,8 @@ func TestNewHeader_customResponseHeaders(t *testing.T) {
|
|||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
mid := NewHeader(emptyHandler, dynamic.Headers{CustomResponseHeaders: test.config})
|
||||
mid, err := NewHeader(emptyHandler, dynamic.Headers{CustomResponseHeaders: test.config})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
|
||||
|
||||
|
|
|
@ -58,7 +58,11 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
|
|||
|
||||
if hasCustomHeaders || hasCorsHeaders {
|
||||
logger.Debugf("Setting up customHeaders/Cors from %v", cfg)
|
||||
handler = NewHeader(nextHandler, cfg)
|
||||
var err error
|
||||
handler, err = NewHeader(nextHandler, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &headers{
|
||||
|
|
|
@ -100,6 +100,8 @@ func Test_buildConfiguration(t *testing.T) {
|
|||
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOrigin": "foobar",
|
||||
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginList/0": "foobar",
|
||||
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginList/1": "foobar",
|
||||
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginListRegex/0": "foobar",
|
||||
"traefik/http/middlewares/Middleware09/headers/accessControlAllowOriginListRegex/1": "foobar",
|
||||
"traefik/http/middlewares/Middleware09/headers/contentTypeNosniff": "true",
|
||||
"traefik/http/middlewares/Middleware09/headers/accessControlAllowCredentials": "true",
|
||||
"traefik/http/middlewares/Middleware09/headers/featurePolicy": "foobar",
|
||||
|
@ -557,6 +559,10 @@ func Test_buildConfiguration(t *testing.T) {
|
|||
"foobar",
|
||||
"foobar",
|
||||
},
|
||||
AccessControlAllowOriginListRegex: []string{
|
||||
"foobar",
|
||||
"foobar",
|
||||
},
|
||||
AccessControlExposeHeaders: []string{
|
||||
"foobar",
|
||||
"foobar",
|
||||
|
|
Loading…
Reference in a new issue