From 238acd9330aab50243675b79866197161b351852 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 2 Jan 2018 18:32:53 +0100 Subject: [PATCH] feat(consulcatalog): add custom Headers tags. --- provider/consul/consul_catalog_config.go | 42 +++++++++ provider/consul/consul_catalog_config_test.go | 85 +++++++++++++++++++ templates/consul_catalog.tmpl | 53 ++++++++++++ 3 files changed, 180 insertions(+) diff --git a/provider/consul/consul_catalog_config.go b/provider/consul/consul_catalog_config.go index 998595b7f..34e589977 100644 --- a/provider/consul/consul_catalog_config.go +++ b/provider/consul/consul_catalog_config.go @@ -55,6 +55,7 @@ func (p *CatalogProvider) buildConfiguration(catalog []catalogUpdate) *types.Con "getErrorPages": p.getErrorPages, "hasRateLimit": p.getFuncHasAttributePrefix(label.BaseFrontendRateLimit), "getRateLimit": p.getRateLimit, + "getHeaders": p.getHeaders, } var allNodes []*api.ServiceEntry @@ -336,6 +337,37 @@ func (p *CatalogProvider) getRateLimit(tags []string) *types.RateLimit { } } +func (p *CatalogProvider) getHeaders(tags []string) *types.Headers { + headers := &types.Headers{ + CustomRequestHeaders: p.getMapAttribute(label.SuffixFrontendRequestHeaders, tags), + CustomResponseHeaders: p.getMapAttribute(label.SuffixFrontendResponseHeaders, tags), + SSLProxyHeaders: p.getMapAttribute(label.SuffixFrontendHeadersSSLProxyHeaders, tags), + AllowedHosts: p.getSliceAttribute(label.SuffixFrontendHeadersAllowedHosts, tags), + HostsProxyHeaders: p.getSliceAttribute(label.SuffixFrontendHeadersHostsProxyHeaders, tags), + SSLHost: p.getAttribute(label.SuffixFrontendHeadersSSLHost, tags, ""), + CustomFrameOptionsValue: p.getAttribute(label.SuffixFrontendHeadersCustomFrameOptionsValue, tags, ""), + ContentSecurityPolicy: p.getAttribute(label.SuffixFrontendHeadersContentSecurityPolicy, tags, ""), + PublicKey: p.getAttribute(label.SuffixFrontendHeadersPublicKey, tags, ""), + ReferrerPolicy: p.getAttribute(label.SuffixFrontendHeadersReferrerPolicy, tags, ""), + STSSeconds: p.getInt64Attribute(label.SuffixFrontendHeadersSTSSeconds, tags, 0), + SSLRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLRedirect, tags, false), + SSLTemporaryRedirect: p.getBoolAttribute(label.SuffixFrontendHeadersSSLTemporaryRedirect, tags, false), + STSIncludeSubdomains: p.getBoolAttribute(label.SuffixFrontendHeadersSTSIncludeSubdomains, tags, false), + STSPreload: p.getBoolAttribute(label.SuffixFrontendHeadersSTSPreload, tags, false), + ForceSTSHeader: p.getBoolAttribute(label.SuffixFrontendHeadersForceSTSHeader, tags, false), + FrameDeny: p.getBoolAttribute(label.SuffixFrontendHeadersFrameDeny, tags, false), + ContentTypeNosniff: p.getBoolAttribute(label.SuffixFrontendHeadersContentTypeNosniff, tags, false), + BrowserXSSFilter: p.getBoolAttribute(label.SuffixFrontendHeadersBrowserXSSFilter, tags, false), + IsDevelopment: p.getBoolAttribute(label.SuffixFrontendHeadersIsDevelopment, tags, false), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers +} + // Base functions func (p *CatalogProvider) parseTagsToNeutralLabels(tags []string) map[string]string { @@ -372,6 +404,16 @@ func (p *CatalogProvider) getFuncSliceAttribute(name string) func(tags []string) } } +func (p *CatalogProvider) getMapAttribute(name string, tags []string) map[string]string { + rawValue := getTag(p.getPrefixedName(name), tags, "") + + if len(rawValue) == 0 { + return nil + } + + return label.ParseMapValue(p.getPrefixedName(name), rawValue) +} + func (p *CatalogProvider) getFuncIntAttribute(name string, defaultValue int) func(tags []string) int { return func(tags []string) int { return p.getIntAttribute(name, tags, defaultValue) diff --git a/provider/consul/consul_catalog_config_test.go b/provider/consul/consul_catalog_config_test.go index 51242d676..3b37bd672 100644 --- a/provider/consul/consul_catalog_config_test.go +++ b/provider/consul/consul_catalog_config_test.go @@ -1166,3 +1166,88 @@ func TestCatalogProviderGetRateLimit(t *testing.T) { }) } } + +func TestCatalogProviderGetHeaders(t *testing.T) { + p := &CatalogProvider{ + Prefix: "traefik", + } + + testCases := []struct { + desc string + tags []string + expected *types.Headers + }{ + { + desc: "should return nil when no tags", + tags: []string{}, + expected: nil, + }, + { + desc: "should return a struct when has tags", + tags: []string{ + label.TraefikFrontendRequestHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendResponseHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendSSLProxyHeaders + "=Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendAllowedHosts + "=foo,bar,bor", + label.TraefikFrontendHostsProxyHeaders + "=foo,bar,bor", + label.TraefikFrontendSSLHost + "=foo", + label.TraefikFrontendCustomFrameOptionsValue + "=foo", + label.TraefikFrontendContentSecurityPolicy + "=foo", + label.TraefikFrontendPublicKey + "=foo", + label.TraefikFrontendReferrerPolicy + "=foo", + label.TraefikFrontendSTSSeconds + "=666", + label.TraefikFrontendSSLRedirect + "=true", + label.TraefikFrontendSSLTemporaryRedirect + "=true", + label.TraefikFrontendSTSIncludeSubdomains + "=true", + label.TraefikFrontendSTSPreload + "=true", + label.TraefikFrontendForceSTSHeader + "=true", + label.TraefikFrontendFrameDeny + "=true", + label.TraefikFrontendContentTypeNosniff + "=true", + label.TraefikFrontendBrowserXSSFilter + "=true", + label.TraefikFrontendIsDevelopment + "=true", + }, + expected: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{"foo", "bar", "bor"}, + HostsProxyHeaders: []string{"foo", "bar", "bor"}, + SSLHost: "foo", + CustomFrameOptionsValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + STSSeconds: 666, + SSLRedirect: true, + SSLTemporaryRedirect: true, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXSSFilter: true, + IsDevelopment: true, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + result := p.getHeaders(test.tags) + + assert.Equal(t, test.expected, result) + }) + } +} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index 88847356b..7c8317725 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -101,6 +101,59 @@ {{end}} + {{ $headers := getHeaders $service.Attributes }} + {{ if $headers }} + [frontends."frontend-{{ $service.ServiceName }}".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."frontend-{{ $service.ServiceName }}".headers.customRequestHeaders] + {{ range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{ if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $service.ServiceName }}".headers.customResponseHeaders] + {{ range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{ if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $service.ServiceName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders}} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + {{end}} + [frontends."frontend-{{ $service.ServiceName }}".routes."route-host-{{ $service.ServiceName }}"] rule = "{{ getFrontendRule $service }}"