feat(kv): add custom headers configuration.
This commit is contained in:
parent
79ae52aca7
commit
944008661f
3 changed files with 401 additions and 0 deletions
|
@ -2,6 +2,7 @@ package kv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -38,6 +39,7 @@ func (p *Provider) buildConfiguration() *types.Configuration {
|
||||||
"getRedirect": p.getRedirect,
|
"getRedirect": p.getRedirect,
|
||||||
"getErrorPages": p.getErrorPages,
|
"getErrorPages": p.getErrorPages,
|
||||||
"getRateLimit": p.getRateLimit,
|
"getRateLimit": p.getRateLimit,
|
||||||
|
"getHeaders": p.getHeaders,
|
||||||
|
|
||||||
// Backend functions
|
// Backend functions
|
||||||
"getSticky": p.getSticky,
|
"getSticky": p.getSticky,
|
||||||
|
@ -160,6 +162,37 @@ func (p *Provider) getRateLimit(rootPath string) *types.RateLimit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) getHeaders(rootPath string) *types.Headers {
|
||||||
|
headers := &types.Headers{
|
||||||
|
CustomRequestHeaders: p.getMap(rootPath, pathFrontendCustomRequestHeaders),
|
||||||
|
CustomResponseHeaders: p.getMap(rootPath, pathFrontendCustomResponseHeaders),
|
||||||
|
SSLProxyHeaders: p.getMap(rootPath, pathFrontendSSLProxyHeaders),
|
||||||
|
AllowedHosts: p.splitGet("", rootPath, pathFrontendAllowedHosts),
|
||||||
|
HostsProxyHeaders: p.splitGet(rootPath, pathFrontendHostsProxyHeaders),
|
||||||
|
SSLRedirect: p.getBool(false, rootPath, pathFrontendSSLRedirect),
|
||||||
|
SSLTemporaryRedirect: p.getBool(false, rootPath, pathFrontendSSLTemporaryRedirect),
|
||||||
|
SSLHost: p.get("", rootPath, pathFrontendSSLHost),
|
||||||
|
STSSeconds: p.getInt64(0, rootPath, pathFrontendSTSSeconds),
|
||||||
|
STSIncludeSubdomains: p.getBool(false, rootPath, pathFrontendSTSIncludeSubdomains),
|
||||||
|
STSPreload: p.getBool(false, rootPath, pathFrontendSTSPreload),
|
||||||
|
ForceSTSHeader: p.getBool(false, rootPath, pathFrontendForceSTSHeader),
|
||||||
|
FrameDeny: p.getBool(false, rootPath, pathFrontendFrameDeny),
|
||||||
|
CustomFrameOptionsValue: p.get("", rootPath, pathFrontendCustomFrameOptionsValue),
|
||||||
|
ContentTypeNosniff: p.getBool(false, rootPath, pathFrontendContentTypeNosniff),
|
||||||
|
BrowserXSSFilter: p.getBool(false, rootPath, pathFrontendBrowserXSSFilter),
|
||||||
|
ContentSecurityPolicy: p.get("", rootPath, pathFrontendContentSecurityPolicy),
|
||||||
|
PublicKey: p.get("", rootPath, pathFrontendPublicKey),
|
||||||
|
ReferrerPolicy: p.get("", rootPath, pathFrontendReferrerPolicy),
|
||||||
|
IsDevelopment: p.getBool(false, rootPath, pathFrontendIsDevelopment),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Provider) listServers(backend string) []string {
|
func (p *Provider) listServers(backend string) []string {
|
||||||
serverNames := p.list(backend, pathBackendServers)
|
serverNames := p.list(backend, pathBackendServers)
|
||||||
return fun.Filter(p.serverFilter, serverNames).([]string)
|
return fun.Filter(p.serverFilter, serverNames).([]string)
|
||||||
|
@ -298,3 +331,18 @@ func (p *Provider) last(key string) string {
|
||||||
index := strings.LastIndex(key, pathSeparator)
|
index := strings.LastIndex(key, pathSeparator)
|
||||||
return key[index+1:]
|
return key[index+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) getMap(keyParts ...string) map[string]string {
|
||||||
|
var mapData map[string]string
|
||||||
|
|
||||||
|
list := p.list(keyParts...)
|
||||||
|
for _, name := range list {
|
||||||
|
if mapData == nil {
|
||||||
|
mapData = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapData[http.CanonicalHeaderKey(p.last(name))] = p.get("", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapData
|
||||||
|
}
|
||||||
|
|
|
@ -655,6 +655,51 @@ func TestProviderGetInt64(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderGetMap(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
keyParts []string
|
||||||
|
kvPairs []*store.KVPair
|
||||||
|
expected map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "when several keys",
|
||||||
|
keyParts: []string{"traefik/frontends/foo", pathFrontendCustomRequestHeaders},
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendCustomRequestHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
|
||||||
|
withPair(pathFrontendCustomRequestHeaders+"Content-Type", "application/json; charset=utf-8"),
|
||||||
|
withPair(pathFrontendCustomRequestHeaders+"X-Custom-Header", "test"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expected: map[string]string{
|
||||||
|
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"X-Custom-Header": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "when no keys",
|
||||||
|
keyParts: []string{"traefik/frontends/foo", pathFrontendCustomRequestHeaders},
|
||||||
|
kvPairs: filler("traefik", frontend("foo")),
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p := newProviderMock(test.kvPairs)
|
||||||
|
|
||||||
|
result := p.getMap(test.keyParts...)
|
||||||
|
|
||||||
|
assert.EqualValues(t, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProviderHasStickinessLabel(t *testing.T) {
|
func TestProviderHasStickinessLabel(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
@ -882,3 +927,258 @@ func TestProviderGetRateLimit(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderGetHeaders(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
rootPath string
|
||||||
|
kvPairs []*store.KVPair
|
||||||
|
expected *types.Headers
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Custom Request Headers",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendCustomRequestHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
|
||||||
|
withPair(pathFrontendCustomRequestHeaders+"Content-Type", "application/json; charset=utf-8"),
|
||||||
|
withPair(pathFrontendCustomRequestHeaders+"X-Custom-Header", "test"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
CustomRequestHeaders: map[string]string{
|
||||||
|
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"X-Custom-Header": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Custom esponse Headers",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendCustomResponseHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
|
||||||
|
withPair(pathFrontendCustomResponseHeaders+"Content-Type", "application/json; charset=utf-8"),
|
||||||
|
withPair(pathFrontendCustomResponseHeaders+"X-Custom-Header", "test"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
CustomResponseHeaders: map[string]string{
|
||||||
|
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"X-Custom-Header": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SSL Proxy Headers",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSSLProxyHeaders+"Access-Control-Allow-Methods", "POST,GET,OPTIONS"),
|
||||||
|
withPair(pathFrontendSSLProxyHeaders+"Content-Type", "application/json; charset=utf-8"),
|
||||||
|
withPair(pathFrontendSSLProxyHeaders+"X-Custom-Header", "test"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
SSLProxyHeaders: map[string]string{
|
||||||
|
"Access-Control-Allow-Methods": "POST,GET,OPTIONS",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"X-Custom-Header": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Allowed Hosts",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendAllowedHosts, "foo, bar, goo, hor"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
AllowedHosts: []string{"foo", "bar", "goo", "hor"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Hosts Proxy Headers",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendHostsProxyHeaders, "foo, bar, goo, hor"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
HostsProxyHeaders: []string{"foo", "bar", "goo", "hor"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SSL Redirect",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSSLRedirect, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
SSLRedirect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SSL Temporary Redirect",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSSLTemporaryRedirect, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
SSLTemporaryRedirect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SSL Host",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSSLHost, "foo"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
SSLHost: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "STS Seconds",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSTSSeconds, "666"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
STSSeconds: 666,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "STS Include Subdomains",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSTSIncludeSubdomains, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
STSIncludeSubdomains: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "STS Preload",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendSTSPreload, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
STSPreload: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Force STS Header",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendForceSTSHeader, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
ForceSTSHeader: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Frame Deny",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendFrameDeny, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
FrameDeny: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Custom Frame Options Value",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendCustomFrameOptionsValue, "foo"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
CustomFrameOptionsValue: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Content Type Nosniff",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendContentTypeNosniff, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
ContentTypeNosniff: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Browser XSS Filter",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendBrowserXSSFilter, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
BrowserXSSFilter: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Content Security Policy",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendContentSecurityPolicy, "foo"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
ContentSecurityPolicy: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Public Key",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendPublicKey, "foo"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
PublicKey: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Referrer Policy",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendReferrerPolicy, "foo"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
ReferrerPolicy: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Is Development",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendIsDevelopment, "true"))),
|
||||||
|
expected: &types.Headers{
|
||||||
|
IsDevelopment: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return nil when not significant configuration",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik",
|
||||||
|
frontend("foo",
|
||||||
|
withPair(pathFrontendIsDevelopment, "false"))),
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should return nil when no headers configuration",
|
||||||
|
rootPath: "traefik/frontends/foo",
|
||||||
|
kvPairs: filler("traefik", frontend("foo")),
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p := newProviderMock(test.kvPairs)
|
||||||
|
|
||||||
|
headers := p.getHeaders(test.rootPath)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, headers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -104,6 +104,59 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{ $headers := getHeaders $frontend }}
|
||||||
|
{{ if $headers }}
|
||||||
|
[frontends."{{ $frontendName }}".headers]
|
||||||
|
SSLRedirect = {{ $headers.SSLRedirect }}
|
||||||
|
SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }}
|
||||||
|
SSLHost = "{{ $headers.SSLHost }}"
|
||||||
|
STSSeconds = {{ $headers.STSSeconds }}
|
||||||
|
STSIncludeSubdomains = {{ $headers.STSIncludeSubdomains }}
|
||||||
|
STSPreload = {{ $headers.STSPreload }}
|
||||||
|
ForceSTSHeader = {{ $headers.ForceSTSHeader }}
|
||||||
|
FrameDeny = {{ $headers.FrameDeny }}
|
||||||
|
CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}"
|
||||||
|
ContentTypeNosniff = {{ $headers.ContentTypeNosniff }}
|
||||||
|
BrowserXSSFilter = {{ $headers.BrowserXSSFilter }}
|
||||||
|
ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}"
|
||||||
|
PublicKey = "{{ $headers.PublicKey }}"
|
||||||
|
ReferrerPolicy = "{{ $headers.ReferrerPolicy }}"
|
||||||
|
IsDevelopment = {{ $headers.IsDevelopment }}
|
||||||
|
|
||||||
|
{{ if $headers.AllowedHosts }}
|
||||||
|
AllowedHosts = [{{ range $headers.AllowedHosts }}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{ if $headers.HostsProxyHeaders }}
|
||||||
|
HostsProxyHeaders = [{{ range $headers.HostsProxyHeaders }}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{ if $headers.CustomRequestHeaders }}
|
||||||
|
[frontends."{{ $frontendName }}".headers.customRequestHeaders]
|
||||||
|
{{ range $k, $v := $headers.CustomRequestHeaders }}
|
||||||
|
{{$k}} = "{{$v}}"
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{ if $headers.CustomResponseHeaders }}
|
||||||
|
[frontends."{{ $frontendName }}".headers.customResponseHeaders]
|
||||||
|
{{ range $k, $v := $headers.CustomResponseHeaders }}
|
||||||
|
{{$k}} = "{{$v}}"
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{ if $headers.SSLProxyHeaders }}
|
||||||
|
[frontends."{{ $frontendName }}".headers.SSLProxyHeaders]
|
||||||
|
{{range $k, $v := $headers.SSLProxyHeaders}}
|
||||||
|
{{$k}} = "{{$v}}"
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{range $route := List $frontend "/routes/"}}
|
{{range $route := List $frontend "/routes/"}}
|
||||||
[frontends."{{$frontendName}}".routes."{{Last $route}}"]
|
[frontends."{{$frontendName}}".routes."{{Last $route}}"]
|
||||||
rule = "{{Get "" $route "/rule"}}"
|
rule = "{{Get "" $route "/rule"}}"
|
||||||
|
|
Loading…
Reference in a new issue