Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CORS headers #151

Merged
merged 5 commits into from
May 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ The following annotations are supported:
||[`ingress.kubernetes.io/auth-tls-secret`](#auth-tls)|namespace/secret name|[doc](/examples/auth/client-certs)|
|`[1]`|[`ingress.kubernetes.io/balance-algorithm`](#balance-algorithm)|algorithm name|-|
|`[1]`|[`ingress.kubernetes.io/blue-green-deploy`](#blue-green)|label=value=weight,...|[doc](/examples/blue-green)|
|`[1]`|[`ingress.kubernetes.io/cors-allow-origin`](#cors)|URL|-|
|`[1]`|[`ingress.kubernetes.io/cors-allow-methods`](#cors)|methods list|-|
|`[1]`|[`ingress.kubernetes.io/cors-allow-headers`](#cors)|headers list|-|
|`[1]`|[`ingress.kubernetes.io/cors-allow-credentials`](#cors)|[true\|false]|-|
|`[1]`|[`ingress.kubernetes.io/cors-enable`](#cors)|[true\|false]|-|
|`[1]`|[`ingress.kubernetes.io/cors-max-age`](#cors)|time (seconds)|-|
|`[0]`|[`ingress.kubernetes.io/hsts`](#hsts)|[true\|false]|-|
|`[0]`|[`ingress.kubernetes.io/hsts-include-subdomains`](#hsts)|[true\|false]|-|
|`[0]`|[`ingress.kubernetes.io/hsts-max-age`](#hsts)|qty of seconds|-|
Expand Down Expand Up @@ -146,6 +152,19 @@ See also the [example](/examples/blue-green) page.

http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#5.2-weight

### CORS

Add CORS headers on OPTIONS http command (preflight) and reponses.

* `ingress.kubernetes.io/cors-enable`: Enable CORS if defined as `true`.
* `ingress.kubernetes.io/cors-allow-origin`: Optional, configures `Access-Control-Allow-Origin` header which defines the URL that may access the resource. Defaults to `*`.
* `ingress.kubernetes.io/cors-allow-methods`: Optional, configures `Access-Control-Allow-Methods` header which defines the allowed methods. See defaults [here](/pkg/common/ingress/annotations/cors/main.go#L34).
* `ingress.kubernetes.io/cors-allow-headers`: Optional, configures `Access-Control-Allow-Headers` header which defines the allowed headers. See defaults [here](/pkg/common/ingress/annotations/cors/main.go#L34).
* `ingress.kubernetes.io/cors-allow-credentials`: Optional, configures `Access-Control-Allow-Credentials` header which defines whether or not credentials (cookies, authorization headers or client certificates) should be exposed. Defaults to `true`.
* `ingress.kubernetes.io/cors-max-age`: Optional, configures `Access-Control-Max-Age` header which defines the time in seconds the result should be cached. Defaults to `86400` (1 day).

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

### Limit

Configure rate limit and concurrent connections per client IP address in order to mitigate DDoS attack.
Expand Down
24 changes: 21 additions & 3 deletions pkg/common/ingress/annotations/cors/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ import (
)

const (
annotationCorsEnabled = "ingress.kubernetes.io/enable-cors"
annotationCorsEnabledDepr = "ingress.kubernetes.io/enable-cors"
annotationCorsEnabled = "ingress.kubernetes.io/cors-enable"
annotationCorsAllowOrigin = "ingress.kubernetes.io/cors-allow-origin"
annotationCorsAllowMethods = "ingress.kubernetes.io/cors-allow-methods"
annotationCorsAllowHeaders = "ingress.kubernetes.io/cors-allow-headers"
annotationCorsAllowCredentials = "ingress.kubernetes.io/cors-allow-credentials"
annotationCorsMaxAge = "ingress.kubernetes.io/cors-max-age"
// Default values
defaultCorsMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultCorsHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization"
defaultCorsMaxAge = 86400
)

var (
Expand All @@ -58,6 +61,7 @@ type CorsConfig struct {
CorsAllowMethods string `json:"corsAllowMethods"`
CorsAllowHeaders string `json:"corsAllowHeaders"`
CorsAllowCredentials bool `json:"corsAllowCredentials"`
CorsMaxAge int `json:"corsMaxAge"`
}

// NewParser creates a new CORS annotation parser
Expand Down Expand Up @@ -88,6 +92,9 @@ func (c1 *CorsConfig) Equal(c2 *CorsConfig) bool {
if c1.CorsEnabled != c2.CorsEnabled {
return false
}
if c1.CorsMaxAge != c2.CorsMaxAge {
return false
}

return true
}
Expand All @@ -97,7 +104,12 @@ func (c1 *CorsConfig) Equal(c2 *CorsConfig) bool {
func (a cors) Parse(ing *extensions.Ingress) (interface{}, error) {
corsenabled, err := parser.GetBoolAnnotation(annotationCorsEnabled, ing)
if err != nil {
corsenabled = false
corsenabled, _ = parser.GetBoolAnnotation(annotationCorsEnabledDepr, ing)
}
if !corsenabled {
return &CorsConfig{
CorsEnabled: false,
}, nil
}

corsalloworigin, err := parser.GetStringAnnotation(annotationCorsAllowOrigin, ing)
Expand All @@ -120,12 +132,18 @@ func (a cors) Parse(ing *extensions.Ingress) (interface{}, error) {
corsallowcredentials = true
}

corsMaxAge, err := parser.GetIntAnnotation(annotationCorsMaxAge, ing)
if err != nil {
corsMaxAge = defaultCorsMaxAge
}

return &CorsConfig{
CorsEnabled: corsenabled,
CorsEnabled: true,
CorsAllowOrigin: corsalloworigin,
CorsAllowHeaders: corsallowheaders,
CorsAllowMethods: corsallowmethods,
CorsAllowCredentials: corsallowcredentials,
CorsMaxAge: corsMaxAge,
}, nil

}
4 changes: 2 additions & 2 deletions pkg/common/ingress/controller/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@ func TestCors(t *testing.T) {
{map[string]string{annotationCorsEnabled: "true"}, true, defaultCorsMethods, defaultCorsHeaders, "*", true},
{map[string]string{annotationCorsEnabled: "true", annotationCorsAllowMethods: "POST, GET, OPTIONS", annotationCorsAllowHeaders: "$nginx_version", annotationCorsAllowCredentials: "false"}, true, "POST, GET, OPTIONS", defaultCorsHeaders, "*", false},
{map[string]string{annotationCorsEnabled: "true", annotationCorsAllowCredentials: "false"}, true, defaultCorsMethods, defaultCorsHeaders, "*", false},
{map[string]string{}, false, defaultCorsMethods, defaultCorsHeaders, "*", true},
{nil, false, defaultCorsMethods, defaultCorsHeaders, "*", true},
{map[string]string{}, false, "", "", "", false},
{nil, false, "", "", "", false},
}

for _, foo := range fooAnns {
Expand Down
16 changes: 16 additions & 0 deletions pkg/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/golang/glog"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/file"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/cors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/hsts"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/defaults"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/net/ssl"
Expand Down Expand Up @@ -190,6 +191,7 @@ func (cfg *haConfig) createHAProxyServers() {
Locations: haLocations,
SSLRedirect: sslRedirect,
HSTS: serverHSTS(server),
CORS: serverCORS(server),
HasRateLimit: serverHasRateLimit(server),
CertificateAuth: server.CertificateAuth,
Alias: server.Alias,
Expand Down Expand Up @@ -240,6 +242,7 @@ func (cfg *haConfig) newHAProxyLocations(server *ingress.Server) ([]*types.HAPro
IsRootLocation: location.Path == "/",
Path: location.Path,
Backend: location.Backend,
CORS: location.CorsConfig,
HSTS: location.HSTS,
Rewrite: location.Rewrite,
Redirect: location.Redirect,
Expand Down Expand Up @@ -411,6 +414,19 @@ func serverHSTS(server *ingress.Server) *hsts.Config {
return hsts
}

func serverCORS(server *ingress.Server) *cors.CorsConfig {
var cors *cors.CorsConfig
cors = nil
for _, location := range server.Locations {
if cors == nil {
cors = &location.CorsConfig
} else if !location.CorsConfig.Equal(cors) {
return nil
}
}
return cors
}

func serverHasRateLimit(server *ingress.Server) bool {
for _, location := range server.Locations {
if location.RateLimit.Connections.Limit > 0 || location.RateLimit.RPS.Limit > 0 {
Expand Down
3 changes: 3 additions & 0 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package types
import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/authtls"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/cors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/hsts"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/proxy"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/ratelimit"
Expand Down Expand Up @@ -117,6 +118,7 @@ type (
Locations []*HAProxyLocation `json:"locations,omitempty"`
SSLRedirect bool `json:"sslRedirect"`
HSTS *hsts.Config `json:"hsts"`
CORS *cors.CorsConfig `json:"cors"`
HasRateLimit bool `json:"hasRateLimit"`
CertificateAuth authtls.AuthSSLConfig `json:"certificateAuth,omitempty"`
Alias string `json:"alias,omitempty"`
Expand All @@ -127,6 +129,7 @@ type (
IsRootLocation bool `json:"isDefaultLocation"`
Path string `json:"path"`
Backend string `json:"backend"`
CORS cors.CorsConfig `json:"cors"`
HSTS hsts.Config `json:"hsts"`
Rewrite rewrite.Redirect `json:"rewrite,omitempty"`
Redirect redirect.Redirect `json:"redirect,omitempty"`
Expand Down
67 changes: 62 additions & 5 deletions rootfs/etc/haproxy/template/haproxy.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ global
log {{ $cfg.Syslog }} format rfc5424 local0
log-tag ingress
{{- end }}
lua-load /usr/local/etc/haproxy/lua/send-response.lua
{{- if ne $cfg.SSLDHParam.Filename "" }}
# DH PEM checksum: {{ $cfg.SSLDHParam.PemSHA }}
ssl-dh-param-file {{ $cfg.SSLDHParam.Filename }}
Expand Down Expand Up @@ -132,7 +133,7 @@ frontend httpsfront

{{- range $server := $ing.HAServers }}
{{- if $server.IsCACert }}
{{- template "acl" map $cfg $server "req.ssl_sni" false }}
{{- template "acl" map $cfg $server "req.ssl_sni" false }}
{{- end }}
{{- end }}

Expand All @@ -156,7 +157,7 @@ backend httpback-shared-backend
backend httpsback-shared-backend
mode tcp
server shared-https-frontend unix@/var/run/haproxy-https.sock send-proxy-v2
{{- template "http_front" map $ing $cfg nil }}
{{- template "http_front" map $ing $cfg nil }}

{{- range $server := $ing.HAServers }}
{{- if $server.IsCACert }}
Expand All @@ -167,7 +168,7 @@ backend httpsback-shared-backend
backend httpsback-{{ $server.HostnameLabel }}
mode tcp
server {{ $server.HostnameLabel }} unix@/var/run/haproxy-https-{{ $server.HostnameSocket }}.sock send-proxy-v2
{{- template "http_front" map $ing $cfg $server }}
{{- template "http_front" map $ing $cfg $server }}
{{- end }}
{{- end }}

Expand All @@ -177,7 +178,7 @@ backend httpsback-{{ $server.HostnameLabel }}
backend httpback-default-backend
mode http
server shared-http-frontend unix@/var/run/haproxy-http-{{ $ing.DefaultServer.HostnameSocket }}.sock send-proxy-v2
{{- template "http_front" map $ing $cfg $ing.DefaultServer }}
{{- template "http_front" map $ing $cfg $ing.DefaultServer }}

{{- /*------------------------------------*/}}
{{- /*------------------------------------*/}}
Expand Down Expand Up @@ -235,7 +236,7 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa

{{- /*------------------------------------*/}}
{{- range $server := $servers }}
{{- template "acl" map $cfg $server "var(txn.hdr_host)" true }}
{{- template "acl" map $cfg $server "var(txn.hdr_host)" true }}
{{- end }}

{{- /*------------------------------------*/}}
Expand Down Expand Up @@ -307,6 +308,23 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa
option forwardfor if-none
{{- end }}

{{- /*------------------------------------*/}}
{{- range $server := $servers }}
{{- if $server.CORS }}
{{- if $server.CORS.CorsEnabled }}
# skip backend on CORS preflight - {{ $server.Hostname }}
http-request use-service lua.send-response if METH_OPTIONS {{ $server.ACLLabel }}
{{- end }}
{{- else }}
{{- range $location := $server.Locations }}
{{- if $location.CORS.CorsEnabled }}
# skip backend on CORS preflight - {{ $server.Hostname }}/{{ $location.Path }}
http-request use-service lua.send-response if METH_OPTIONS {{ $server.ACLLabel }}{{ $location.HAMatchTxnPath }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

{{- /*------------------------------------*/}}
{{- range $server := $servers }}
{{- range $location := $server.Locations }}
Expand Down Expand Up @@ -376,8 +394,19 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa
http-response set-header Strict-Transport-Security "max-age={{ $hsts.MaxAge }}{{ if $hsts.Subdomains }}; includeSubDomains{{ end }}{{ if $hsts.Preload }}; preload{{ end }}" if from-https {{ $server.ACLLabel }}{{ $location.HAMatchTxnPath }}
{{- end }}
{{- end }}
{{- end }}{{/* if/else $server.HSTS */}}
{{- if $server.CORS }}
{{- if $server.CORS.CorsEnabled }}
{{- template "CORS" map $server.CORS $server false }}
{{- end }}
{{- else }}
{{- range $location := $server.Locations }}
{{- if $location.CORS.CorsEnabled }}
{{- template "CORS" map $location.CORS $server $location }}
{{- end }}
{{- end }}{{/* if $cors.CorsEnabled */}}
{{- end }}{{/* if/else $server.CORS */}}
{{- end }}{{/* range $servers */}}

{{- /*------------------------------------*/}}
{{- range $server := $servers }}
Expand All @@ -396,6 +425,7 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa
{{- else if $isCACert }}
default_backend httpback-shared-backend
{{- end }}

{{- end }}{{/* define "http_front" */}}

{{- /*------------------------------------*/}}
Expand All @@ -421,6 +451,33 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa
{{- end }}
{{- end }}{{/* define "acl" */}}

{{- /*------------------------------------*/}}
{{- /*------------------------------------*/}}
{{- define "CORS" }}
{{- $cors := .p1 }}
{{- $server := .p2 }}
{{- $location := .p3 }}
# start CORS preflight - {{ $server.Hostname }}{{ if $location }}{{ $location.Path }}{{ end }}
http-response set-header Access-Control-Allow-Origin "{{ $cors.CorsAllowOrigin }}" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
http-response set-header Access-Control-Allow-Methods "{{ $cors.CorsAllowMethods }}" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
http-response set-header Access-Control-Allow-Headers "{{ $cors.CorsAllowHeaders }}" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
{{- if $cors.CorsAllowCredentials }}
http-response set-header Access-Control-Allow-Credentials "{{ $cors.CorsAllowCredentials }}" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
{{- end }}
http-response set-header Access-Control-Max-Age "{{ $cors.CorsMaxAge }}" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
http-response set-header Content-Type "text/plain" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
http-response set-header Content-Length "0" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
http-response set-status 204 reason "No Content" if METH_OPTIONS {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
# end CORS preflight - {{ $server.Hostname }}{{ if $location }}{{ $location.Path }}{{ end }}
http-response set-header Access-Control-Allow-Origin "{{ $cors.CorsAllowOrigin }}" if {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
{{- if $cors.CorsAllowCredentials }}
http-response set-header Access-Control-Allow-Credentials "{{ $cors.CorsAllowCredentials }}" if {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
{{- end }}
http-response set-header Access-Control-Allow-Methods "{{ $cors.CorsAllowMethods }}" if {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
http-response set-header Access-Control-Allow-Headers "{{ $cors.CorsAllowHeaders }}" if {{ $server.ACLLabel }}{{ if $location }}{{ $location.HAMatchTxnPath }}{{ end }}
# end CORS - {{ $server.Hostname }}{{ if $location }}{{ $location.Path }}{{ end }}
{{- end }}{{/* define "CORS" */}}

######
###### Error pages
######
Expand Down
3 changes: 3 additions & 0 deletions rootfs/usr/local/etc/haproxy/lua/send-response.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
core.register_service("send-response", "http", function(applet)
applet:start_response()
end)