fix: add 'vary: origin' header if not set by backend

Signed-off-by: GitHub <noreply@github.com>
This commit is contained in:
Zadkiel Aharonian 2023-10-10 14:19:56 +02:00 committed by GitHub
parent da51393cac
commit cb71d7ea09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 2 deletions

View file

@ -388,6 +388,10 @@ CORS can be controlled with the following annotations:
!!! note !!! note
For more information please see [https://enable-cors.org](https://enable-cors.org/server_nginx.html) For more information please see [https://enable-cors.org](https://enable-cors.org/server_nginx.html)
#### Handling the `Vary: Origin` Header
When CORS support is enabled and the allowed origins contains either a wildcard host or more than one entry, the server will automatically add a `Vary: Origin` header to the response if the backend does not explicitly set one. The `Vary` header is used to indicate that the response may vary depending on the value of the `Origin` header in the request. This is important for caching and ensuring that responses are correctly handled based on the origin of the request.
### HTTP2 Push Preload. ### HTTP2 Push Preload.
Enables automatic conversion of preload links specified in the “Link” response header fields into push requests. Enables automatic conversion of preload links specified in the “Link” response header fields into push requests.

View file

@ -283,6 +283,7 @@ var funcMap = text_template.FuncMap{
"shouldLoadAuthDigestModule": shouldLoadAuthDigestModule, "shouldLoadAuthDigestModule": shouldLoadAuthDigestModule,
"buildServerName": buildServerName, "buildServerName": buildServerName,
"buildCorsOriginRegex": buildCorsOriginRegex, "buildCorsOriginRegex": buildCorsOriginRegex,
"isCorsVaryHeaderNeeded": isCorsVaryHeaderNeeded,
} }
// escapeLiteralDollar will replace the $ character with ${literal_dollar} // escapeLiteralDollar will replace the $ character with ${literal_dollar}
@ -1754,3 +1755,10 @@ func buildCorsOriginRegex(corsOrigins []string) string {
originsRegex += ")$ ) { set $cors 'true'; }" originsRegex += ")$ ) { set $cors 'true'; }"
return originsRegex return originsRegex
} }
func isCorsVaryHeaderNeeded(corsOrigins []string) bool {
hasMultipleOrigins := len(corsOrigins) > 1
hasAnyOrigin := len(corsOrigins) == 1 && corsOrigins[0] == "*"
hasSingleDynamicOrigin := len(corsOrigins) == 1 && strings.Contains(corsOrigins[0], "*")
return hasMultipleOrigins || hasAnyOrigin || hasSingleDynamicOrigin
}

View file

@ -441,6 +441,14 @@ http {
{{ end }} {{ end }}
# CORS with multiple allowed origin need to serve the 'Vary: Origin' header
# See https://github.com/kubernetes/ingress-nginx/issues/8469#issuecomment-1195714565
# Only set it's value if it is not already set by the backend
map $upstream_http_vary $cors_vary {
default "$upstream_http_vary";
'' "Origin";
}
# Create a variable that contains the literal $ character. # Create a variable that contains the literal $ character.
# This works because the geo module will not resolve variables. # This works because the geo module will not resolve variables.
geo $literal_dollar { geo $literal_dollar {
@ -954,6 +962,7 @@ stream {
more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}'; more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}';
{{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }} {{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }}
more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}'; more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}';
{{ if isCorsVaryHeaderNeeded $cors.CorsAllowOrigin }} more_set_headers 'Vary: $cors_vary'; {{ end }}
} }
if ($cors = "trueoptions") { if ($cors = "trueoptions") {
@ -963,6 +972,7 @@ stream {
more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}'; more_set_headers 'Access-Control-Allow-Headers: {{ $cors.CorsAllowHeaders }}';
{{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }} {{ if not (empty $cors.CorsExposeHeaders) }} more_set_headers 'Access-Control-Expose-Headers: {{ $cors.CorsExposeHeaders }}'; {{ end }}
more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}'; more_set_headers 'Access-Control-Max-Age: {{ $cors.CorsMaxAge }}';
{{ if isCorsVaryHeaderNeeded $cors.CorsAllowOrigin }} more_set_headers 'Vary: $cors_vary'; {{ end }}
more_set_headers 'Content-Type: text/plain charset=UTF-8'; more_set_headers 'Content-Type: text/plain charset=UTF-8';
more_set_headers 'Content-Length: 0'; more_set_headers 'Content-Length: 0';
return 204; return 204;

View file

@ -31,7 +31,7 @@ const (
) )
var _ = framework.DescribeAnnotation("cors-*", func() { var _ = framework.DescribeAnnotation("cors-*", func() {
f := framework.NewDefaultFramework("cors") f := framework.NewDefaultFramework("cors", framework.WithHTTPBunEnabled())
ginkgo.BeforeEach(func() { ginkgo.BeforeEach(func() {
f.NewEchoDeployment(framework.WithDeploymentReplicas(2)) f.NewEchoDeployment(framework.WithDeploymentReplicas(2))
@ -96,6 +96,134 @@ var _ = framework.DescribeAnnotation("cors-*", func() {
}) })
}) })
ginkgo.It("should include vary header - multiple origins", func() {
host := corsHost
origin := originHost
annotations := map[string]string{
"nginx.ingress.kubernetes.io/enable-cors": "true",
"nginx.ingress.kubernetes.io/cors-allow-origin": "http://origin.com:8080, http://cors.foo.com",
}
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 'Vary: $cors_vary';")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", host).
WithHeader("Origin", origin).
Expect().
Headers().
ValueEqual("Vary", []string{"Origin"})
})
ginkgo.It("should include vary header - any origin", func() {
host := corsHost
origin := originHost
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.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, "more_set_headers 'Vary: $cors_vary';")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", host).
WithHeader("Origin", origin).
Expect().
Headers().
ValueEqual("Vary", []string{"Origin"})
})
ginkgo.It("should include vary header - single dynamic origin", func() {
host := corsHost
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",
}
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 'Vary: $cors_vary';")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", host).
WithHeader("Origin", origin).
Expect().
Headers().
ValueEqual("Vary", []string{"Origin"})
})
ginkgo.It("should not include vary header - single static origin", func() {
host := corsHost
origin := originHost
annotations := map[string]string{
"nginx.ingress.kubernetes.io/enable-cors": "true",
"nginx.ingress.kubernetes.io/cors-allow-origin": originHost,
}
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 'Vary: $cors_vary';")
})
f.HTTPTestClient().
GET("/").
WithHeader("Host", host).
WithHeader("Origin", origin).
Expect().
Headers().
NotContainsKey("Vary")
})
ginkgo.It("should preserve upstream vary headers", func() {
host := corsHost
origin := originHost
annotations := map[string]string{
"nginx.ingress.kubernetes.io/enable-cors": "true",
"nginx.ingress.kubernetes.io/cors-allow-origin": "*",
}
f.NewHttpbunDeployment(framework.WithDeploymentName("cors-service"))
f.EnsureIngress(framework.NewSingleIngress(
host,
"/",
host,
f.Namespace,
framework.HTTPBunService,
80,
annotations))
f.HTTPTestClient().
GET("/response-headers").
WithQuery("Vary", "Origin, X-My-Custom-Header").
WithHeader("Host", host).
WithHeader("Origin", origin).
Expect().
Headers().
ValueEqual("Vary", []string{"Origin, X-My-Custom-Header"})
})
ginkgo.It("should disable cors allow credentials", func() { ginkgo.It("should disable cors allow credentials", func() {
host := corsHost host := corsHost
annotations := map[string]string{ annotations := map[string]string{