diff --git a/build/dev-env.sh b/build/dev-env.sh index b089de206..d98f0bfbd 100755 --- a/build/dev-env.sh +++ b/build/dev-env.sh @@ -61,7 +61,7 @@ echo "[dev-env] building image" make build image docker tag "${REGISTRY}/controller:${TAG}" "${DEV_IMAGE}" -export K8S_VERSION=${K8S_VERSION:-v1.20.2@sha256:8f7ea6e7642c0da54f04a7ee10431549c0257315b3a634f6ef2fecaaedb19bab} +export K8S_VERSION=${K8S_VERSION:-v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6} KIND_CLUSTER_NAME="ingress-nginx-dev" diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index ca515f9e2..19bd3947f 100755 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -354,10 +354,13 @@ CORS can be controlled with the following annotations: * `nginx.ingress.kubernetes.io/cors-allow-origin`: Controls what's the accepted Origin for CORS. - This is a single field value, with the following format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port` + This is a multi-valued field, separated by ','. It must follow this format: `http(s)://origin-site.com` or `http(s)://origin-site.com:port` - Default: `*` - - Example: `nginx.ingress.kubernetes.io/cors-allow-origin: "https://origin-site.com:4443"` + - Example: `nginx.ingress.kubernetes.io/cors-allow-origin: "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199"` + + It also supports single level wildcard subdomains and follows this format: `http(s)://*.foo.bar`, `http(s)://*.bar.foo:8080` or `http(s)://*.abc.bar.foo:9000` + - Example: `nginx.ingress.kubernetes.io/cors-allow-origin: "https://*.origin-site.com:4443, http://*.origin-site.com, https://example.org:1199"` * `nginx.ingress.kubernetes.io/cors-allow-credentials`: Controls if credentials can be passed during CORS operations. diff --git a/go.mod b/go.mod index 225d191a3..49e380774 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/go-openapi/jsonreference v0.19.3 // indirect github.com/go-openapi/spec v0.19.5 // indirect github.com/go-openapi/swag v0.19.5 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect diff --git a/go.sum b/go.sum index bdc542758..21a2bb5c4 100644 --- a/go.sum +++ b/go.sum @@ -269,6 +269,7 @@ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2K github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= diff --git a/internal/ingress/annotations/annotations_test.go b/internal/ingress/annotations/annotations_test.go index ada81117d..8eec999cc 100644 --- a/internal/ingress/annotations/annotations_test.go +++ b/internal/ingress/annotations/annotations_test.go @@ -215,15 +215,15 @@ func TestCors(t *testing.T) { corsenabled bool methods string headers string - origin string + origin []string credentials bool expose string }{ - {map[string]string{annotationCorsEnabled: "true"}, true, defaultCorsMethods, defaultCorsHeaders, "*", true, ""}, - {map[string]string{annotationCorsEnabled: "true", annotationCorsAllowMethods: "POST, GET, OPTIONS", annotationCorsAllowHeaders: "$nginx_version", annotationCorsAllowCredentials: "false", annotationCorsExposeHeaders: "X-CustomResponseHeader"}, true, "POST, GET, OPTIONS", defaultCorsHeaders, "*", false, "X-CustomResponseHeader"}, - {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{annotationCorsEnabled: "true"}, true, defaultCorsMethods, defaultCorsHeaders, []string{"*"}, true, ""}, + {map[string]string{annotationCorsEnabled: "true", annotationCorsAllowMethods: "POST, GET, OPTIONS", annotationCorsAllowHeaders: "$nginx_version", annotationCorsAllowCredentials: "false", annotationCorsExposeHeaders: "X-CustomResponseHeader"}, true, "POST, GET, OPTIONS", defaultCorsHeaders, []string{"*"}, false, "X-CustomResponseHeader"}, + {map[string]string{annotationCorsEnabled: "true", annotationCorsAllowCredentials: "false"}, true, defaultCorsMethods, defaultCorsHeaders, []string{"*"}, false, ""}, + {map[string]string{}, false, defaultCorsMethods, defaultCorsHeaders, []string{"*"}, true, ""}, + {nil, false, defaultCorsMethods, defaultCorsHeaders, []string{"*"}, true, ""}, } for _, foo := range fooAnns { @@ -243,12 +243,18 @@ func TestCors(t *testing.T) { t.Errorf("Returned %v but expected %v for Cors Methods", r.CorsAllowMethods, foo.methods) } - if r.CorsAllowOrigin != foo.origin { - t.Errorf("Returned %v but expected %v for Cors Methods", r.CorsAllowOrigin, foo.origin) + if len(r.CorsAllowOrigin) != len(foo.origin) { + t.Errorf("Lengths of Cors Origins are not equal. Expected %v - Actual %v", r.CorsAllowOrigin, foo.origin) + } + + for i, v := range r.CorsAllowOrigin { + if v != foo.origin[i] { + t.Errorf("Values of Cors Origins are not equal. Expected %v - Actual %v", r.CorsAllowOrigin, foo.origin) + } } if r.CorsAllowCredentials != foo.credentials { - t.Errorf("Returned %v but expected %v for Cors Methods", r.CorsAllowCredentials, foo.credentials) + t.Errorf("Returned %v but expected %v for Cors Credentials", r.CorsAllowCredentials, foo.credentials) } } diff --git a/internal/ingress/annotations/cors/main.go b/internal/ingress/annotations/cors/main.go index 44947a014..d2f232af1 100644 --- a/internal/ingress/annotations/cors/main.go +++ b/internal/ingress/annotations/cors/main.go @@ -18,8 +18,10 @@ package cors import ( "regexp" + "strings" networking "k8s.io/api/networking/v1" + "k8s.io/klog/v2" "k8s.io/ingress-nginx/internal/ingress/annotations/parser" "k8s.io/ingress-nginx/internal/ingress/resolver" @@ -36,7 +38,7 @@ var ( // Regex are defined here to prevent information leak, if user tries to set anything not valid // that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc) // Origin must contain a http/s Origin (including or not the port) or the value '*' - corsOriginRegex = regexp.MustCompile(`^(https?://[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`) + corsOriginRegex = regexp.MustCompile(`^(https?://(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`) // Method must contain valid methods list (PUT, GET, POST, BLA) // May contain or not spaces between each verb corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`) @@ -54,13 +56,13 @@ type cors struct { // Config contains the Cors configuration to be used in the Ingress type Config struct { - CorsEnabled bool `json:"corsEnabled"` - CorsAllowOrigin string `json:"corsAllowOrigin"` - CorsAllowMethods string `json:"corsAllowMethods"` - CorsAllowHeaders string `json:"corsAllowHeaders"` - CorsAllowCredentials bool `json:"corsAllowCredentials"` - CorsExposeHeaders string `json:"corsExposeHeaders"` - CorsMaxAge int `json:"corsMaxAge"` + CorsEnabled bool `json:"corsEnabled"` + CorsAllowOrigin []string `json:"corsAllowOrigin"` + CorsAllowMethods string `json:"corsAllowMethods"` + CorsAllowHeaders string `json:"corsAllowHeaders"` + CorsAllowCredentials bool `json:"corsAllowCredentials"` + CorsExposeHeaders string `json:"corsExposeHeaders"` + CorsMaxAge int `json:"corsMaxAge"` } // NewParser creates a new CORS annotation parser @@ -91,13 +93,20 @@ func (c1 *Config) Equal(c2 *Config) bool { if c1.CorsAllowMethods != c2.CorsAllowMethods { return false } - if c1.CorsAllowOrigin != c2.CorsAllowOrigin { - return false - } if c1.CorsEnabled != c2.CorsEnabled { return false } + if len(c1.CorsAllowOrigin) != len(c2.CorsAllowOrigin) { + return false + } + + for i, v := range c1.CorsAllowOrigin { + if v != c2.CorsAllowOrigin[i] { + return false + } + } + return true } @@ -112,9 +121,23 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) { config.CorsEnabled = false } - config.CorsAllowOrigin, err = parser.GetStringAnnotation("cors-allow-origin", ing) - if err != nil || !corsOriginRegex.MatchString(config.CorsAllowOrigin) { - config.CorsAllowOrigin = "*" + unparsedOrigins, err := parser.GetStringAnnotation("cors-allow-origin", ing) + if err == nil { + config.CorsAllowOrigin = strings.Split(unparsedOrigins, ",") + for i, origin := range config.CorsAllowOrigin { + origin = strings.TrimSpace(origin) + if origin == "*" { + config.CorsAllowOrigin = []string{"*"} + break + } + if !corsOriginRegex.MatchString(origin) { + klog.Errorf("Error parsing cors-allow-origin parameters. Supplied incorrect origin: %s. Skipping.", origin) + config.CorsAllowOrigin = append(config.CorsAllowOrigin[:i], config.CorsAllowOrigin[i+1:]...) + } + klog.Infof("Current config.corsAllowOrigin %v", config.CorsAllowOrigin) + } + } else { + config.CorsAllowOrigin = []string{"*"} } config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing) @@ -143,5 +166,4 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) { } return config, nil - } diff --git a/internal/ingress/annotations/cors/main_test.go b/internal/ingress/annotations/cors/main_test.go index a65bf12cd..086a59d89 100644 --- a/internal/ingress/annotations/cors/main_test.go +++ b/internal/ingress/annotations/cors/main_test.go @@ -110,7 +110,7 @@ func TestIngressCorsConfigValid(t *testing.T) { t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-methods")], nginxCors.CorsAllowMethods) } - if nginxCors.CorsAllowOrigin != "https://origin123.test.com:4443" { + if nginxCors.CorsAllowOrigin[0] != "https://origin123.test.com:4443" { t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin) } @@ -164,10 +164,6 @@ func TestIngressCorsConfigInvalid(t *testing.T) { t.Errorf("expected %v but returned %v", defaultCorsHeaders, nginxCors.CorsAllowMethods) } - if nginxCors.CorsAllowOrigin != "*" { - t.Errorf("expected %v but returned %v", "*", nginxCors.CorsAllowOrigin) - } - if nginxCors.CorsExposeHeaders != "" { t.Errorf("expected %v but returned %v", "", nginxCors.CorsExposeHeaders) } diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index dc4b78c1a..1147c8e5d 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -276,6 +276,7 @@ var ( "shouldLoadAuthDigestModule": shouldLoadAuthDigestModule, "shouldLoadInfluxDBModule": shouldLoadInfluxDBModule, "buildServerName": buildServerName, + "buildCorsOriginRegex": buildCorsOriginRegex, } ) @@ -1676,3 +1677,29 @@ func convertGoSliceIntoLuaTable(goSliceInterface interface{}, emptyStringAsNil b return "", fmt.Errorf("could not process type: %s", kind) } } + +func buildOriginRegex(origin string) string { + origin = regexp.QuoteMeta(origin) + origin = strings.Replace(origin, "\\*", "[A-Za-z0-9]+", 1) + return fmt.Sprintf("(%s)", origin) +} + +func buildCorsOriginRegex(corsOrigins []string) string { + if len(corsOrigins) == 1 && corsOrigins[0] == "*" { + return "set $http_origin *;\nset $cors 'true';" + } + + var originsRegex string = "if ($http_origin ~* (" + for i, origin := range corsOrigins { + originTrimmed := strings.TrimSpace(origin) + if len(originTrimmed) > 0 { + builtOrigin := buildOriginRegex(originTrimmed) + originsRegex += builtOrigin + if i != len(corsOrigins)-1 { + originsRegex = originsRegex + "|" + } + } + } + originsRegex = originsRegex + ")$ ) { set $cors 'true'; }" + return originsRegex +} diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 9beca463b..d579165b7 100755 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -867,8 +867,24 @@ stream { {{ define "CORS" }} {{ $cors := .CorsConfig }} # Cors Preflight methods needs additional options and different Return Code + {{ if $cors.CorsAllowOrigin }} + {{ buildCorsOriginRegex $cors.CorsAllowOrigin }} + {{ end }} if ($request_method = 'OPTIONS') { - more_set_headers 'Access-Control-Allow-Origin: {{ $cors.CorsAllowOrigin }}'; + set $cors ${cors}options; + } + + if ($cors = "true") { + more_set_headers 'Access-Control-Allow-Origin: $http_origin'; + {{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }} + more_set_headers 'Access-Control-Allow-Methods: {{ $cors.CorsAllowMethods }}'; + more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}'; + {{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }} + more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}'; + } + + if ($cors = "trueoptions") { + more_set_headers 'Access-Control-Allow-Origin: $http_origin'; {{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }} more_set_headers 'Access-Control-Allow-Methods: {{ $cors.CorsAllowMethods }}'; more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}'; @@ -878,11 +894,6 @@ stream { more_set_headers 'Content-Length: 0'; return 204; } - - more_set_headers 'Access-Control-Allow-Origin: {{ $cors.CorsAllowOrigin }}'; - {{ if $cors.CorsAllowCredentials }} more_set_headers 'Access-Control-Allow-Credentials: {{ $cors.CorsAllowCredentials }}'; {{ end }} - {{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }} - {{ end }} {{/* definition of server-template to avoid repetitions with server-alias */}} diff --git a/test/e2e/annotations/cors.go b/test/e2e/annotations/cors.go index e4cdd32b4..c17eb0b20 100644 --- a/test/e2e/annotations/cors.go +++ b/test/e2e/annotations/cors.go @@ -44,10 +44,12 @@ var _ = framework.DescribeAnnotation("cors-*", func() { f.WaitForNginxServer(host, func(server string) bool { return strings.Contains(server, "more_set_headers 'Access-Control-Allow-Methods: GET, PUT, POST, DELETE, PATCH, OPTIONS';") && - strings.Contains(server, "more_set_headers 'Access-Control-Allow-Origin: *';") && + strings.Contains(server, "more_set_headers 'Access-Control-Allow-Origin: $http_origin';") && strings.Contains(server, "more_set_headers 'Access-Control-Allow-Headers: DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';") && strings.Contains(server, "more_set_headers 'Access-Control-Max-Age: 1728000';") && - strings.Contains(server, "more_set_headers 'Access-Control-Allow-Credentials: true';") + strings.Contains(server, "more_set_headers 'Access-Control-Allow-Credentials: true';") && + strings.Contains(server, "set $http_origin *;") && + strings.Contains(server, "$cors 'true';") }) f.HTTPTestClient(). @@ -107,6 +109,7 @@ var _ = framework.DescribeAnnotation("cors-*", func() { ginkgo.It("should allow origin for cors", func() { host := "cors.foo.com" + origin := "https://origin.cors.com:8080" annotations := map[string]string{ "nginx.ingress.kubernetes.io/enable-cors": "true", "nginx.ingress.kubernetes.io/cors-allow-origin": "https://origin.cors.com:8080", @@ -115,10 +118,20 @@ var _ = framework.DescribeAnnotation("cors-*", func() { ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) f.EnsureIngress(ing) - f.WaitForNginxServer(host, - func(server string) bool { - return strings.Contains(server, "more_set_headers 'Access-Control-Allow-Origin: https://origin.cors.com:8080';") - }) + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin}) }) ginkgo.It("should allow headers for cors", func() { @@ -152,4 +165,450 @@ var _ = framework.DescribeAnnotation("cors-*", func() { return strings.Contains(server, "more_set_headers 'Access-Control-Expose-Headers: X-CustomResponseHeader, X-CustomSecondHeader';") }) }) + + ginkgo.It("should allow - single origin for multiple cors values", func() { + host := "cors.foo.com" + origin := "https://origin.cors.com:8080" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "https://origin.cors.com:8080, https://origin2.cors.com", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin}) + }) + + ginkgo.It("should not allow - single origin for multiple cors values", func() { + host := "cors.foo.com" + origin := "http://no.origin.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin2.cors.com, https://origin.com", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + }) + + ginkgo.It("should allow correct origins - single origin for multiple cors values", func() { + host := "cors.foo.com" + badOrigin := "origin.cors.com:8080" + origin1 := "https://origin2.cors.com" + origin2 := "https://origin.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "origin.cors.com:8080, https://origin2.cors.com, https://origin.com", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", badOrigin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin1). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin1). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin1}) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin2). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin2). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin2}) + }) + + ginkgo.It("should not break functionality", func() { + host := "cors.foo.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "*", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{"*"}) + }) + + ginkgo.It("should not break functionality - without `*`", func() { + host := "cors.foo.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{"*"}) + }) + + ginkgo.It("should not break functionality with extra domain", func() { + host := "cors.foo.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "*, foo.bar.com", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{"*"}) + }) + + ginkgo.It("should not match", func() { + host := "cors.foo.com" + origin := "https://fooxbar.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "https://foo.bar.com", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + }) + + ginkgo.It("should allow - single origin with required port", func() { + host := "cors.foo.com" + origin := "http://origin.com:8080" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin.cors.com:8080, http://origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin}) + }) + + ginkgo.It("should not allow - single origin with port and origin without port", func() { + host := "cors.foo.com" + origin := "http://origin.com:8080" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "https://origin2.cors.com, http://origin.com", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + }) + + ginkgo.It("should not allow - single origin without port and origin with required port", func() { + host := "cors.foo.com" + origin := "http://origin.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin.cors.com:8080, http://origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + }) + + ginkgo.It("should allow - matching origin with wildcard origin (2 subdomains)", func() { + host := "cors.foo.com" + origin := "http://foo.origin.cors.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://*.origin.cors.com, http://*.origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin}) + }) + + ginkgo.It("should not allow - unmatching origin with wildcard origin (2 subdomains)", func() { + host := "cors.foo.com" + origin := "http://bar.foo.origin.cors.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://*.origin.cors.com, http://*.origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + }) + + ginkgo.It("should allow - matching origin+port with wildcard origin", func() { + host := "cors.foo.com" + origin := "http://abc.origin.com:8080" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin.cors.com:8080, http://*.origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin}) + }) + + ginkgo.It("should not allow - portless origin with wildcard origin", func() { + host := "cors.foo.com" + origin := "http://abc.origin.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin.cors.com:8080, http://*.origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + }) + + ginkgo.It("should allow correct origins - missing subdomain + origin with wildcard origin and correct origin", func() { + host := "cors.foo.com" + badOrigin := "http://origin.com:8080" + origin := "http://bar.origin.com:8080" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin.cors.com:8080, http://*.origin.com:8080", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", badOrigin). + Expect(). + Headers().NotContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{origin}) + }) + + ginkgo.It("should allow - missing origins (should allow all origins)", func() { + host := "cors.foo.com" + origin := "http://origin.com" + origin2 := "http://book.origin.com" + origin3 := "test.origin.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/enable-cors": "true", + "nginx.ingress.kubernetes.io/cors-allow-origin": " ", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + // the client should still receive a response but browsers should block the request + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{"*"}) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin2). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin2). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{"*"}) + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin3). + Expect(). + Headers().ContainsKey("Access-Control-Allow-Origin") + + f.HTTPTestClient(). + GET("/"). + WithHeader("Host", host). + WithHeader("Origin", origin3). + Expect(). + Status(http.StatusOK).Headers(). + ValueEqual("Access-Control-Allow-Origin", []string{"*"}) + }) })