mirror of
https://github.com/alibaba/higress.git
synced 2026-07-27 23:00:27 +08:00
fix: align cors plugin behavior with cors semantics (#4036)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com> Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
This commit is contained in:
@@ -10,8 +10,25 @@ description: 跨域资源共享插件配置参考
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`授权阶段`
|
||||
插件执行优先级:`340`
|
||||
插件执行阶段:`认证阶段`
|
||||
插件执行优先级:`2000`
|
||||
|
||||
## 版本说明
|
||||
|
||||
### 2.0.1
|
||||
|
||||
相比 `2.0.0`,本版本对齐浏览器 CORS 语义,并将插件执行优先级调整为 `2000`。
|
||||
|
||||
具体变更和升级注意事项:
|
||||
|
||||
* 实际 CORS 请求的 Origin 或 Method 不匹配时,插件不再直接返回 `403`,而是继续转发到后端;插件不会添加 CORS 允许响应头,并会移除后端返回的 CORS policy 响应头,由浏览器按 CORS 规则拦截。若已有监控或客户端逻辑依赖网关直接返回 `403`,升级后需要相应调整。
|
||||
* CORS 预检请求统一由插件直接返回 `204 No Content`。非法预检请求不会返回 `Access-Control-Allow-*`、`Access-Control-Expose-Headers`、`Access-Control-Allow-Credentials` 或 `Access-Control-Max-Age`,浏览器会判定预检失败;若已有逻辑依赖非法预检返回 `403`,升级后需要相应调整。
|
||||
* 同源 `OPTIONS` 请求即使携带类似预检的请求头,也会继续转发到后端,避免被 CORS 插件误拦截。
|
||||
* `allow_methods: ["*"]` 会在预检响应中回显本次请求的 `Access-Control-Request-Method`;`allow_headers: ["*"]` 会回显规范化后的 `Access-Control-Request-Headers`,没有请求头时不返回 `Access-Control-Allow-Headers`。
|
||||
* 默认 Method/Header 解析会按逗号拆分并去除空格,避免默认值被当作单个不可匹配的条目。
|
||||
* Origin 模式匹配会锚定完整 Origin,避免类似 `http://api.example.com.evil.com` 误匹配 `http://*.example.com`;如果历史配置依赖非完整匹配,需要调整为明确的 Origin 模式。
|
||||
* 当 `Access-Control-Allow-Origin` 返回具体 Origin 而不是 `*` 时,插件会合并返回 `Vary: Origin`,避免缓存复用错误的跨域响应。
|
||||
* `expose_headers: ["*"]` 在携带凭据的请求中仍按兼容方式接受配置,但浏览器会把 `Access-Control-Expose-Headers: *` 当作字面量 Header 名称,而不是暴露所有 Header。
|
||||
|
||||
## 配置字段
|
||||
|
||||
@@ -19,16 +36,18 @@ description: 跨域资源共享插件配置参考
|
||||
|-----------------------|-----------------|----------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| allow_origins | array of string | 选填 | * | 允许跨域访问的 Origin,格式为 scheme://host:port,示例如 http://example.com:8081。当 allow_credentials 为 false 时,可以使用 * 来表示允许所有 Origin 通过 |
|
||||
| allow_origin_patterns | array of string | 选填 | - | 允许跨域访问的 Origin 模式匹配, 用 * 匹配域名或者端口, <br/>比如 http://*.example.com -- 匹配域名, http://*.example.com:[8080,9090] -- 匹配域名和指定端口, http://*.example.com:[*] -- 匹配域名和所有端口。单独 * 表示匹配所有域名和端口 |
|
||||
| allow_methods | array of string | 选填 | GET, PUT, POST, DELETE, PATCH, OPTIONS | 允许跨域访问的 Method,比如:GET,POST 等。可以使用 * 来表示允许所有 Method。 |
|
||||
| allow_headers | array of string | 选填 | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | 允许跨域访问时请求方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 |
|
||||
| expose_headers | array of string | 选填 | - | 允许跨域访问时响应方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 |
|
||||
| allow_methods | array of string | 选填 | GET, PUT, POST, DELETE, PATCH, OPTIONS | 允许跨域访问的 Method,比如:GET,POST 等。可以使用 * 来表示允许所有 Method;预检响应会回显本次请求的 `Access-Control-Request-Method`。 |
|
||||
| allow_headers | array of string | 选填 | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | 允许跨域访问时请求方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header;预检响应会回显规范化后的 `Access-Control-Request-Headers`,没有请求头时不返回 `Access-Control-Allow-Headers`。 |
|
||||
| expose_headers | array of string | 选填 | - | 允许跨域访问时响应方携带哪些非 CORS 规范以外的 Header。可以使用 *;但当请求携带凭据时,浏览器会把 `Access-Control-Expose-Headers: *` 当作字面量 Header 名称,而不是暴露所有 Header。 |
|
||||
| allow_credentials | bool | 选填 | false | 是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 true,在 allow_origins 不能使用 *, 替换成使用 allow_origin_patterns * |
|
||||
| max_age | number | 选填 | 86400秒 | 浏览器缓存 CORS 结果的最大时间,单位为秒。<br/>在这个时间范围内,浏览器会复用上一次的检查结果 |
|
||||
|
||||
> 注意
|
||||
> * allow_credentials 是一个很敏感的选项,请谨慎开启。开启之后,allow_credentials 和 allow_origins 为 * 不能同时使用,同时设置时, allow_origins 值为 "*" 生效。
|
||||
> * allow_origins 和 allow_origin_patterns 可以同时设置, 先检查 allow_origins 是否匹配,然后再检查 allow_origin_patterns 是否匹配
|
||||
> * 非法 CORS 请求, HTTP 状态码返回是 403, 返回体内容为 "Invalid CORS request"
|
||||
> * 对于实际 CORS 请求,如果 Origin 或 Method 不匹配配置,插件会继续转发到后端,但不会添加 CORS 响应头,并会移除后端返回的 CORS policy 响应头,由浏览器根据 CORS 规则拦截结果。
|
||||
> * 对于 CORS 预检请求,合法请求由插件直接返回 `204 No Content` 和配置对应的 CORS 响应头;非法请求由插件直接返回 `204 No Content`,但不返回 `Access-Control-Allow-*`、`Access-Control-Expose-Headers`、`Access-Control-Allow-Credentials` 或 `Access-Control-Max-Age`,浏览器会判定预检失败。
|
||||
> * 当 `Access-Control-Allow-Origin` 返回具体 Origin 而不是 `*` 时,插件会合并返回 `Vary: Origin`,避免缓存复用错误的跨域响应。
|
||||
|
||||
## 配置示例
|
||||
|
||||
@@ -150,7 +169,7 @@ spec:
|
||||
configDisable: false
|
||||
ingress:
|
||||
- ingress-cors-httpbin
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:1.0.0
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:2.0.1
|
||||
imagePullPolicy: Always
|
||||
```
|
||||
|
||||
@@ -161,10 +180,11 @@ spec:
|
||||
curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" http://127.0.0.1/anything/get\?foo\=1
|
||||
|
||||
< HTTP/1.1 200 OK
|
||||
> x-cors-version: 1.0.0
|
||||
> x-cors-version: 2.0.1
|
||||
> access-control-allow-origin: http://httpbin2.example.org:9090
|
||||
> access-control-expose-headers: X-Custom-Header,X-Env-UTM
|
||||
> access-control-allow-credentials: true
|
||||
> vary: Origin
|
||||
```
|
||||
|
||||
#### 预检请求
|
||||
@@ -172,14 +192,15 @@ curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, Token" http://127.0.0.1/anything/get\?foo\=1
|
||||
|
||||
< HTTP/1.1 200 OK
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< access-control-allow-origin: http://httpbin2.example.org:9090
|
||||
< access-control-allow-methods: GET,POST,PATCH
|
||||
< access-control-allow-headers: Content-Type,Token,Authorization
|
||||
< access-control-expose-headers: X-Custom-Header,X-Env-UTM
|
||||
< access-control-allow-credentials: true
|
||||
< access-control-max-age: 3600
|
||||
< vary: Origin
|
||||
< date: Tue, 23 May 2023 11:41:28 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
@@ -190,18 +211,18 @@ curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: http
|
||||
|
||||
#### 非法 CORS Origin 预检请求
|
||||
|
||||
非法预检请求会返回 `204 No Content`,但不包含 `access-control-allow-*` 等 CORS policy 响应头,浏览器会判定预检失败。
|
||||
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" http://127.0.0.1/anything/get\?foo\=1
|
||||
|
||||
HTTP/1.1 403 Forbidden
|
||||
< content-length: 70
|
||||
< content-type: text/plain
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< date: Tue, 23 May 2023 11:27:01 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
<
|
||||
* Connection #0 to host 127.0.0.1 left intact
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
#### 非法 CORS Method 预检请求
|
||||
@@ -209,15 +230,13 @@ Invalid CORS request
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: DELETE" http://127.0.0.1/anything/get\?foo\=1
|
||||
|
||||
< HTTP/1.1 403 Forbidden
|
||||
< content-length: 49
|
||||
< content-type: text/plain
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< date: Tue, 23 May 2023 11:28:51 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
<
|
||||
* Connection #0 to host 127.0.0.1 left intact
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
#### 非法 CORS Header 预检请求
|
||||
@@ -225,15 +244,13 @@ Invalid CORS request
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: TokenView" http://127.0.0.1/anything/get\?foo\=1
|
||||
|
||||
< HTTP/1.1 403 Forbidden
|
||||
< content-length: 52
|
||||
< content-type: text/plain
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< date: Tue, 23 May 2023 11:31:03 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
<
|
||||
* Connection #0 to host 127.0.0.1 left intact
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
## 参考文档
|
||||
|
||||
@@ -7,23 +7,42 @@ description: Cross-Origin Resource Sharing plugin configuration reference
|
||||
The `cors` plugin can enable CORS (Cross-Origin Resource Sharing) HTTP response headers for the server.
|
||||
|
||||
## Execution Attributes
|
||||
Plugin execution phase: `Authorization Phase`
|
||||
Plugin execution priority: `340`
|
||||
Plugin execution phase: `AUTHN`
|
||||
Plugin execution priority: `2000`
|
||||
|
||||
## Release Notes
|
||||
|
||||
### 2.0.1
|
||||
|
||||
Compared with `2.0.0`, this version aligns the plugin behavior with browser CORS semantics and changes the plugin execution priority to `2000`.
|
||||
|
||||
Changes and upgrade notes:
|
||||
|
||||
* When the Origin or Method of an actual CORS request does not match the configuration, the plugin no longer returns a direct `403`. The request continues upstream; the plugin does not add CORS allow response headers and removes upstream CORS policy response headers, so browsers block the result according to CORS rules. If monitoring or client logic depends on a gateway-level `403`, adjust it before upgrading.
|
||||
* CORS preflight requests are answered directly with `204 No Content`. Invalid preflight responses omit `Access-Control-Allow-*`, `Access-Control-Expose-Headers`, `Access-Control-Allow-Credentials`, and `Access-Control-Max-Age`, so browsers treat the preflight as failed. If existing logic depends on invalid preflights returning `403`, adjust it before upgrading.
|
||||
* Same-origin `OPTIONS` requests continue upstream even when they carry preflight-like request headers, preventing the CORS plugin from intercepting them incorrectly.
|
||||
* `allow_methods: ["*"]` echoes the current `Access-Control-Request-Method` in preflight responses; `allow_headers: ["*"]` echoes normalized `Access-Control-Request-Headers` and omits `Access-Control-Allow-Headers` when no request headers were requested.
|
||||
* Default Method/Header values are split by comma and trimmed, preventing defaults from being treated as one unmatchable item.
|
||||
* Origin pattern matching is anchored to the full Origin value, avoiding accidental matches such as `http://api.example.com.evil.com` for `http://*.example.com`. If an existing configuration relied on partial matching, update it to an explicit Origin pattern.
|
||||
* When `Access-Control-Allow-Origin` is a specific Origin instead of `*`, the plugin merges `Vary: Origin` into the response to prevent cache reuse with the wrong CORS headers.
|
||||
* `expose_headers: ["*"]` remains accepted for compatibility when credentials are allowed, but browsers treat `Access-Control-Expose-Headers: *` as a literal header name for credentialed requests instead of exposing all headers.
|
||||
|
||||
## Configuration Fields
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
|-----------------------|------------------|----------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| allow_origins | array of string | Optional | * | Allowed Origins for cross-origin access, formatted as scheme://host:port, for example, http://example.com:8081. When allow_credentials is false, * can be used to allow all Origins through. |
|
||||
| allow_origin_patterns | array of string | Optional | - | Patterns for matching allowed Origins for cross-origin access, using * to match domain or port, <br/>for example http://*.example.com -- matches domain, http://*.example.com:[8080,9090] -- matches domain and specified ports, http://*.example.com:[*] -- matches domain and all ports. A single * indicates matching all domains and ports. |
|
||||
| allow_methods | array of string | Optional | GET, PUT, POST, DELETE, PATCH, OPTIONS | Allowed Methods for cross-origin access, for example: GET, POST, etc. * can be used to indicate all Methods are allowed. |
|
||||
| allow_headers | array of string | Optional | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | Allowed Headers for the requester to carry that are not part of CORS specifications during cross-origin access. * can be used to indicate any Header is allowed. |
|
||||
| expose_headers | array of string | Optional | - | Allowed Headers for the responder to carry that are not part of CORS specifications during cross-origin access. * can be used to indicate any Header is allowed. |
|
||||
| allow_methods | array of string | Optional | GET, PUT, POST, DELETE, PATCH, OPTIONS | Allowed Methods for cross-origin access, for example: GET, POST, etc. `*` can be used to indicate all Methods are allowed; preflight responses echo the current `Access-Control-Request-Method`. |
|
||||
| allow_headers | array of string | Optional | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | Allowed Headers for the requester to carry that are not part of CORS specifications during cross-origin access. `*` can be used to indicate any Header is allowed; preflight responses echo normalized `Access-Control-Request-Headers`, and omit `Access-Control-Allow-Headers` when no request headers were requested. |
|
||||
| expose_headers | array of string | Optional | - | Allowed Headers for the responder to expose during cross-origin access. `*` can be used, but for credentialed requests browsers treat `Access-Control-Expose-Headers: *` as a literal header name instead of exposing all headers. |
|
||||
| allow_credentials | bool | Optional | false | Whether to allow the requester to carry credentials (e.g. Cookies) during cross-origin access. According to CORS specifications, if this option is set to true, * cannot be used for allow_origins, replace it with allow_origin_patterns. |
|
||||
| max_age | number | Optional | 86400 seconds | Maximum time for browsers to cache CORS results, in seconds. <br/>Within this time frame, browsers will reuse the previous inspection results. |
|
||||
> Note
|
||||
> * allow_credentials is a very sensitive option, please enable it with caution. Once enabled, allow_credentials and allow_origins cannot both be *, if both are set, the allow_origins value of "*" takes effect.
|
||||
> * allow_origins and allow_origin_patterns can be set simultaneously. First, check if allow_origins matches, then check if allow_origin_patterns matches.
|
||||
> * Illegal CORS requests will return HTTP status code 403, with the response body content as "Invalid CORS request".
|
||||
> * For actual CORS requests, if the Origin or Method does not match the configuration, the plugin continues the request upstream, does not add CORS response headers, and removes upstream CORS policy response headers. Browsers then block the result according to CORS rules.
|
||||
> * For CORS preflight requests, valid requests are answered directly with `204 No Content` and the configured CORS response headers. Invalid requests are answered directly with `204 No Content`, but without `Access-Control-Allow-*`, `Access-Control-Expose-Headers`, `Access-Control-Allow-Credentials`, or `Access-Control-Max-Age`; browsers treat the preflight as failed.
|
||||
> * When `Access-Control-Allow-Origin` is a specific Origin instead of `*`, the plugin merges `Vary: Origin` into the response to prevent cache reuse with the wrong CORS headers.
|
||||
|
||||
## Configuration Examples
|
||||
### Allow all cross-origin access, without allowing the requester to carry credentials
|
||||
@@ -142,7 +161,7 @@ spec:
|
||||
configDisable: false
|
||||
ingress:
|
||||
- ingress-cors-httpbin
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:1.0.0
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:2.0.1
|
||||
imagePullPolicy: Always
|
||||
```
|
||||
|
||||
@@ -151,23 +170,25 @@ spec:
|
||||
```shell
|
||||
curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" http://127.0.0.1/anything/get\?foo\=1
|
||||
< HTTP/1.1 200 OK
|
||||
> x-cors-version: 1.0.0
|
||||
> x-cors-version: 2.0.1
|
||||
> access-control-allow-origin: http://httpbin2.example.org:9090
|
||||
> access-control-expose-headers: X-Custom-Header,X-Env-UTM
|
||||
> access-control-allow-credentials: true
|
||||
> vary: Origin
|
||||
```
|
||||
|
||||
#### Preflight Request
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, Token" http://127.0.0.1/anything/get\?foo\=1
|
||||
< HTTP/1.1 200 OK
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< access-control-allow-origin: http://httpbin2.example.org:9090
|
||||
< access-control-allow-methods: GET,POST,PATCH
|
||||
< access-control-allow-headers: Content-Type,Token,Authorization
|
||||
< access-control-expose-headers: X-Custom-Header,X-Env-UTM
|
||||
< access-control-allow-credentials: true
|
||||
< access-control-max-age: 3600
|
||||
< vary: Origin
|
||||
< date: Tue, 23 May 2023 11:41:28 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
@@ -177,45 +198,41 @@ curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpb
|
||||
```
|
||||
|
||||
#### Illegal CORS Origin Preflight Request
|
||||
Invalid preflight requests return `204 No Content` without `access-control-allow-*` CORS policy response headers, so browsers treat the preflight as failed.
|
||||
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" http://127.0.0.1/anything/get\?foo\=1
|
||||
HTTP/1.1 403 Forbidden
|
||||
< content-length: 70
|
||||
< content-type: text/plain
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< date: Tue, 23 May 2023 11:27:01 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
<
|
||||
* Connection #0 to host 127.0.0.1 left intact
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
#### Illegal CORS Method Preflight Request
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: DELETE" http://127.0.0.1/anything/get\?foo\=1
|
||||
< HTTP/1.1 403 Forbidden
|
||||
< content-length: 49
|
||||
< content-type: text/plain
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< date: Tue, 23 May 2023 11:28:51 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
<
|
||||
* Connection #0 to host 127.0.0.1 left intact
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
#### Illegal CORS Header Preflight Request
|
||||
```shell
|
||||
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: TokenView" http://127.0.0.1/anything/get\?foo\=1
|
||||
< HTTP/1.1 403 Forbidden
|
||||
< content-length: 52
|
||||
< content-type: text/plain
|
||||
< x-cors-version: 1.0.0
|
||||
< HTTP/1.1 204 No Content
|
||||
< x-cors-trace: trace
|
||||
< date: Tue, 23 May 2023 11:31:03 GMT
|
||||
< server: istio-envoy
|
||||
< content-length: 0
|
||||
<
|
||||
* Connection #0 to host 127.0.0.1 left intact
|
||||
Invalid CORS request
|
||||
```
|
||||
|
||||
## Reference Documents
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.0.0
|
||||
2.0.1
|
||||
|
||||
@@ -24,10 +24,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMatchAll = "*"
|
||||
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
defaultAllAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS, HEAD, TRACE, CONNECT"
|
||||
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
|
||||
defaultMatchAll = "*"
|
||||
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
|
||||
"If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||
defaultMaxAge = 86400
|
||||
protocolHttpName = "http"
|
||||
@@ -80,6 +79,7 @@ func newOriginPatternFromString(declaredPattern string) OriginPattern {
|
||||
patternValue += ":(" + strings.ReplaceAll(portList, ",", "|") + ")"
|
||||
}
|
||||
}
|
||||
patternValue = "^" + patternValue + "$"
|
||||
|
||||
return OriginPattern{
|
||||
declaredPattern: declaredPattern,
|
||||
@@ -143,7 +143,7 @@ type HttpCorsContext struct {
|
||||
}
|
||||
|
||||
func (c *CorsConfig) GetVersion() string {
|
||||
return "1.0.0"
|
||||
return "2.0.1"
|
||||
}
|
||||
|
||||
func (c *CorsConfig) FillDefaultValues() {
|
||||
@@ -151,10 +151,10 @@ func (c *CorsConfig) FillDefaultValues() {
|
||||
c.allowOrigins = []string{defaultMatchAll}
|
||||
}
|
||||
if len(c.allowHeaders) == 0 {
|
||||
c.allowHeaders = []string{defaultAllowHeaders}
|
||||
c.allowHeaders = splitCommaSeparatedValues(defaultAllowHeaders)
|
||||
}
|
||||
if len(c.allowMethods) == 0 {
|
||||
c.allowMethods = strings.Split(defaultAllowMethods, ",")
|
||||
c.allowMethods = splitCommaSeparatedValues(defaultAllowMethods)
|
||||
}
|
||||
if c.maxAge == 0 {
|
||||
c.maxAge = defaultMaxAge
|
||||
@@ -360,21 +360,29 @@ func (c *CorsConfig) checkHeaders(requestHeaders string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
allowHeaders := splitCommaSeparatedValues(strings.Join(c.allowHeaders, ","))
|
||||
if len(allowHeaders) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Return normalized request headers when allowHeaders contains *.
|
||||
if containsMatchAll(allowHeaders) {
|
||||
return strings.Join(splitCommaSeparatedValues(requestHeaders), ","), true
|
||||
}
|
||||
|
||||
if len(requestHeaders) == 0 {
|
||||
return strings.Join(c.allowHeaders, ","), true
|
||||
return strings.Join(allowHeaders, ","), true
|
||||
}
|
||||
|
||||
// Return all request headers when allowHeaders contains *
|
||||
if c.allowHeaders[0] == defaultMatchAll {
|
||||
return requestHeaders, true
|
||||
checkHeaders := splitCommaSeparatedValues(requestHeaders)
|
||||
if len(checkHeaders) == 0 {
|
||||
return strings.Join(allowHeaders, ","), true
|
||||
}
|
||||
|
||||
checkHeaders := strings.Split(requestHeaders, ",")
|
||||
// Each request header should be existed in allowHeaders configuration
|
||||
for _, h := range checkHeaders {
|
||||
isExist := false
|
||||
for _, allowHeader := range c.allowHeaders {
|
||||
if strings.ToLower(h) == strings.ToLower(allowHeader) {
|
||||
for _, allowHeader := range allowHeaders {
|
||||
if strings.EqualFold(h, allowHeader) {
|
||||
isExist = true
|
||||
break
|
||||
}
|
||||
@@ -391,22 +399,48 @@ func (c *CorsConfig) checkMethods(requestMethod string) (string, bool) {
|
||||
if len(requestMethod) == 0 {
|
||||
return "", false
|
||||
}
|
||||
requestMethod = strings.TrimSpace(requestMethod)
|
||||
if len(requestMethod) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Find method existed in allowMethods configuration
|
||||
for _, method := range c.allowMethods {
|
||||
allowMethods := splitCommaSeparatedValues(strings.Join(c.allowMethods, ","))
|
||||
for _, method := range allowMethods {
|
||||
if method == defaultMatchAll {
|
||||
return defaultAllAllowMethods, true
|
||||
return requestMethod, true
|
||||
}
|
||||
if strings.ToLower(method) == strings.ToLower(requestMethod) {
|
||||
return strings.Join(c.allowMethods, ","), true
|
||||
if strings.EqualFold(method, requestMethod) {
|
||||
return strings.Join(allowMethods, ","), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (c *CorsConfig) isPreFlight(origin, method, controllerRequestMethod string) bool {
|
||||
return len(origin) > 0 && strings.ToLower(method) == strings.ToLower(HttpMethodOptions) && len(controllerRequestMethod) > 0
|
||||
func splitCommaSeparatedValues(values string) []string {
|
||||
parts := strings.Split(values, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if len(part) > 0 {
|
||||
result = append(result, part)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func containsMatchAll(values []string) bool {
|
||||
for _, value := range values {
|
||||
if value == defaultMatchAll {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *CorsConfig) isPreFlight(origin, method, controlRequestMethod string) bool {
|
||||
return len(origin) > 0 && strings.ToLower(method) == strings.ToLower(HttpMethodOptions) && len(controlRequestMethod) > 0
|
||||
}
|
||||
|
||||
func (c *CorsConfig) isCorsRequest(scheme, host, origin string) bool {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCorsConfigIssue1743FillDefaultValuesSplitsMethodsAndHeaders(t *testing.T) {
|
||||
c := &CorsConfig{}
|
||||
c.FillDefaultValues()
|
||||
|
||||
assert.Equal(t, []string{"GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"}, c.allowMethods)
|
||||
assert.Contains(t, c.allowHeaders, "Content-Type")
|
||||
assert.Contains(t, c.allowHeaders, "Authorization")
|
||||
assert.NotContains(t, c.allowHeaders, defaultAllowHeaders)
|
||||
|
||||
allowMethods, methodOk := c.checkMethods("POST")
|
||||
assert.True(t, methodOk)
|
||||
assert.Equal(t, "GET,PUT,POST,DELETE,PATCH,OPTIONS", allowMethods)
|
||||
|
||||
allowHeaders, headerOk := c.checkHeaders("Content-Type, Authorization")
|
||||
assert.True(t, headerOk)
|
||||
assert.Contains(t, allowHeaders, "Content-Type")
|
||||
assert.Contains(t, allowHeaders, "Authorization")
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743CheckHeadersTrimsRequestTokens(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowHeaders: []string{"Content-Type", "Authorization"},
|
||||
}
|
||||
|
||||
allowHeaders, ok := c.checkHeaders("Content-Type, Authorization")
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "Content-Type,Authorization", allowHeaders)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743WildcardMethodsEchoRequestedMethod(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowMethods: []string{"*"},
|
||||
}
|
||||
|
||||
allowMethods, ok := c.checkMethods("PROPFIND")
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "PROPFIND", allowMethods)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743WildcardHeadersEchoTrimmedRequestHeaders(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowHeaders: []string{"*"},
|
||||
}
|
||||
|
||||
allowHeaders, ok := c.checkHeaders(" Content-Type, Authorization, ")
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "Content-Type,Authorization", allowHeaders)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743WildcardHeadersOmitAllowHeadersWhenNoRequestHeaders(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowHeaders: []string{"*"},
|
||||
}
|
||||
|
||||
allowHeaders, ok := c.checkHeaders("")
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Empty(t, allowHeaders)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743ProcessWildcardPreflight(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowOrigins: []string{"*"},
|
||||
allowMethods: []string{"*"},
|
||||
allowHeaders: []string{"*"},
|
||||
}
|
||||
|
||||
httpCorsContext, err := c.Process("https", "api.example.com", "OPTIONS", [][2]string{
|
||||
{HeaderOrigin, "https://client.example.com"},
|
||||
{HeaderControlRequestMethod, "PROPFIND"},
|
||||
{HeaderControlRequestHeaders, " Content-Type, Authorization, "},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, httpCorsContext.IsValid)
|
||||
assert.True(t, httpCorsContext.IsPreFlight)
|
||||
assert.True(t, httpCorsContext.IsCorsRequest)
|
||||
assert.Equal(t, "PROPFIND", httpCorsContext.AllowMethods)
|
||||
assert.Equal(t, "Content-Type,Authorization", httpCorsContext.AllowHeaders)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743ProcessWildcardPreflightOmitsAllowHeadersWithoutRequestHeaders(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowOrigins: []string{"*"},
|
||||
allowMethods: []string{"*"},
|
||||
allowHeaders: []string{"*"},
|
||||
}
|
||||
|
||||
httpCorsContext, err := c.Process("https", "api.example.com", "OPTIONS", [][2]string{
|
||||
{HeaderOrigin, "https://client.example.com"},
|
||||
{HeaderControlRequestMethod, "PROPFIND"},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, httpCorsContext.IsValid)
|
||||
assert.Equal(t, "PROPFIND", httpCorsContext.AllowMethods)
|
||||
assert.Empty(t, httpCorsContext.AllowHeaders)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743OriginPatternAnchored(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowOriginPatterns: []OriginPattern{
|
||||
newOriginPatternFromString("http://*.example.com"),
|
||||
},
|
||||
}
|
||||
|
||||
allowOrigin, ok := c.checkOrigin("http://api.example.com")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "http://api.example.com", allowOrigin)
|
||||
|
||||
allowOrigin, ok = c.checkOrigin("http://api.example.com.evil.com")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, allowOrigin)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743OriginPatternWithPortsAnchored(t *testing.T) {
|
||||
c := &CorsConfig{
|
||||
allowOriginPatterns: []OriginPattern{
|
||||
newOriginPatternFromString("http://*.example.com:[8080,9090]"),
|
||||
},
|
||||
}
|
||||
|
||||
allowOrigin, ok := c.checkOrigin("http://api.example.com:8080")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "http://api.example.com:8080", allowOrigin)
|
||||
|
||||
allowOrigin, ok = c.checkOrigin("http://api.example.com:8080.evil.com")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, allowOrigin)
|
||||
}
|
||||
|
||||
func TestCorsConfigIssue1743ExposeHeadersWildcardWithCredentialsAccepted(t *testing.T) {
|
||||
c := &CorsConfig{}
|
||||
|
||||
c.AddExposeHeader("*")
|
||||
err := c.SetAllowCredentials(true)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"*"}, c.exposeHeaders)
|
||||
assert.True(t, c.allowCredentials)
|
||||
}
|
||||
@@ -143,35 +143,35 @@ func TestCorsConfig_isPreFlight(t *testing.T) {
|
||||
name string
|
||||
origin string
|
||||
method string
|
||||
controllerRequestMethod string
|
||||
controlRequestMethod string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "blank case",
|
||||
origin: "",
|
||||
method: "",
|
||||
controllerRequestMethod: "",
|
||||
controlRequestMethod:"",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "normal case",
|
||||
origin: "http://httpbin.example.com",
|
||||
method: "Options",
|
||||
controllerRequestMethod: "PUT",
|
||||
controlRequestMethod:"PUT",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "bad case with diff method",
|
||||
origin: "http://httpbin.example.com",
|
||||
method: "GET",
|
||||
controllerRequestMethod: "PUT",
|
||||
controlRequestMethod:"PUT",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &CorsConfig{}
|
||||
assert.Equalf(t, tt.want, c.isPreFlight(tt.origin, tt.method, tt.controllerRequestMethod), "isPreFlight(%v, %v, %v)", tt.origin, tt.method, tt.controllerRequestMethod)
|
||||
assert.Equalf(t, tt.want, c.isPreFlight(tt.origin, tt.method, tt.controlRequestMethod), "isPreFlight(%v, %v, %v)", tt.origin, tt.method, tt.controlRequestMethod)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func TestCorsConfig_checkMethods(t *testing.T) {
|
||||
name: "default *",
|
||||
allowMethods: []string{"*"},
|
||||
requestMethod: "GET",
|
||||
wantMethods: defaultAllAllowMethods,
|
||||
wantMethods: "GET",
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -63,5 +63,5 @@ spec:
|
||||
configDisable: false
|
||||
ingress:
|
||||
- ingress-cors-httpbin
|
||||
url: oci://docker.io/2456868764/cors:1.0.0
|
||||
imagePullPolicy: Always
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:2.0.1
|
||||
imagePullPolicy: Always
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"cors/config"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
@@ -28,6 +29,11 @@ import (
|
||||
|
||||
func main() {}
|
||||
|
||||
const (
|
||||
headerVary = "Vary"
|
||||
varyOrigin = "Origin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
wrapper.SetCtx(
|
||||
"cors",
|
||||
@@ -94,19 +100,17 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig,
|
||||
// Set HttpContext
|
||||
ctx.SetContext(config.HttpContextKey, httpCorsContext)
|
||||
|
||||
// Response forbidden when it is not valid cors request
|
||||
if !httpCorsContext.IsValid {
|
||||
headers := make([][2]string, 0)
|
||||
headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"})
|
||||
proxywasm.SendHttpResponseWithDetail(http.StatusForbidden, "cors.forbidden", headers, []byte("Invalid CORS request"), -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
// Response directly when it is cors preflight request
|
||||
if httpCorsContext.IsPreFlight {
|
||||
if httpCorsContext.IsCorsRequest && httpCorsContext.IsPreFlight {
|
||||
headers := make([][2]string, 0)
|
||||
headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"})
|
||||
proxywasm.SendHttpResponseWithDetail(http.StatusOK, "cores.preflight", headers, nil, -1)
|
||||
if !httpCorsContext.IsValid {
|
||||
proxywasm.SendHttpResponseWithDetail(http.StatusNoContent, "cors.preflight.invalid", headers, nil, -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
headers = append(headers, buildCorsHeaders(httpCorsContext)...)
|
||||
headers = appendVaryOriginHeader(headers, httpCorsContext.AllowOrigin)
|
||||
proxywasm.SendHttpResponseWithDetail(http.StatusNoContent, "cors.preflight", headers, nil, -1)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
@@ -120,6 +124,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig
|
||||
// Remove upstream cors response headers if existed
|
||||
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowOrigin)
|
||||
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowMethods)
|
||||
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowHeaders)
|
||||
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlExposeHeaders)
|
||||
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowCredentials)
|
||||
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlMaxAge)
|
||||
@@ -140,24 +145,52 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig
|
||||
}
|
||||
|
||||
// Add Cors headers when it is cors and valid request
|
||||
if len(httpCorsContext.AllowOrigin) > 0 {
|
||||
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowOrigin, httpCorsContext.AllowOrigin)
|
||||
}
|
||||
if len(httpCorsContext.AllowMethods) > 0 {
|
||||
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowMethods, httpCorsContext.AllowMethods)
|
||||
}
|
||||
if len(httpCorsContext.AllowHeaders) > 0 {
|
||||
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowHeaders, httpCorsContext.AllowHeaders)
|
||||
}
|
||||
if len(httpCorsContext.ExposeHeaders) > 0 {
|
||||
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlExposeHeaders, httpCorsContext.ExposeHeaders)
|
||||
}
|
||||
if httpCorsContext.AllowCredentials {
|
||||
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowCredentials, "true")
|
||||
}
|
||||
if httpCorsContext.MaxAge > 0 {
|
||||
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlMaxAge, fmt.Sprintf("%d", httpCorsContext.MaxAge))
|
||||
for _, header := range buildCorsHeaders(httpCorsContext) {
|
||||
proxywasm.AddHttpResponseHeader(header[0], header[1])
|
||||
}
|
||||
addVaryOriginHeader(httpCorsContext.AllowOrigin)
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func buildCorsHeaders(httpCorsContext config.HttpCorsContext) [][2]string {
|
||||
headers := make([][2]string, 0, 6)
|
||||
if len(httpCorsContext.AllowOrigin) > 0 {
|
||||
headers = append(headers, [2]string{config.HeaderAccessControlAllowOrigin, httpCorsContext.AllowOrigin})
|
||||
}
|
||||
if len(httpCorsContext.AllowMethods) > 0 {
|
||||
headers = append(headers, [2]string{config.HeaderAccessControlAllowMethods, httpCorsContext.AllowMethods})
|
||||
}
|
||||
if len(httpCorsContext.AllowHeaders) > 0 {
|
||||
headers = append(headers, [2]string{config.HeaderAccessControlAllowHeaders, httpCorsContext.AllowHeaders})
|
||||
}
|
||||
if len(httpCorsContext.ExposeHeaders) > 0 {
|
||||
headers = append(headers, [2]string{config.HeaderAccessControlExposeHeaders, httpCorsContext.ExposeHeaders})
|
||||
}
|
||||
if httpCorsContext.AllowCredentials {
|
||||
headers = append(headers, [2]string{config.HeaderAccessControlAllowCredentials, "true"})
|
||||
}
|
||||
if httpCorsContext.MaxAge > 0 {
|
||||
headers = append(headers, [2]string{config.HeaderAccessControlMaxAge, fmt.Sprintf("%d", httpCorsContext.MaxAge)})
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func appendVaryOriginHeader(headers [][2]string, allowOrigin string) [][2]string {
|
||||
if !shouldVaryOrigin(allowOrigin) {
|
||||
return headers
|
||||
}
|
||||
return append(headers, [2]string{headerVary, varyOrigin})
|
||||
}
|
||||
|
||||
func addVaryOriginHeader(allowOrigin string) {
|
||||
if !shouldVaryOrigin(allowOrigin) {
|
||||
return
|
||||
}
|
||||
proxywasm.AddHttpResponseHeader(headerVary, varyOrigin)
|
||||
}
|
||||
|
||||
func shouldVaryOrigin(allowOrigin string) bool {
|
||||
allowOrigin = strings.TrimSpace(allowOrigin)
|
||||
return len(allowOrigin) > 0 && allowOrigin != "*"
|
||||
}
|
||||
|
||||
154
plugins/wasm-go/extensions/cors/main_issue1743_test.go
Normal file
154
plugins/wasm-go/extensions/cors/main_issue1743_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"cors/config"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIssue1743InvalidActualCorsRequestContinuesUpstream(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":scheme", "http"},
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://invalid.com"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
require.Nil(t, host.GetLocalResponse())
|
||||
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{config.HeaderAccessControlAllowOrigin, "http://invalid.com"},
|
||||
{config.HeaderAccessControlAllowMethods, "GET,POST"},
|
||||
{config.HeaderAccessControlAllowHeaders, "X-Upstream"},
|
||||
{config.HeaderAccessControlExposeHeaders, "X-Upstream-Expose"},
|
||||
{config.HeaderAccessControlAllowCredentials, "true"},
|
||||
{config.HeaderAccessControlMaxAge, "600"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowOrigin))
|
||||
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowMethods))
|
||||
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowHeaders))
|
||||
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlExposeHeaders))
|
||||
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowCredentials))
|
||||
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlMaxAge))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssue1743SameOriginOptionsWithPreflightHeadersContinuesUpstream(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":scheme", "http"},
|
||||
{":authority", "example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "OPTIONS"},
|
||||
{"origin", "http://example.com"},
|
||||
{"access-control-request-method", "POST"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
require.Nil(t, host.GetLocalResponse())
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssue1743ValidPreflightReturnsNoContentWithCorsHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":scheme", "http"},
|
||||
{":authority", "api.example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "OPTIONS"},
|
||||
{"origin", "http://example.com"},
|
||||
{"access-control-request-method", "POST"},
|
||||
{"access-control-request-headers", "Content-Type"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(http.StatusNoContent), localResponse.StatusCode)
|
||||
require.Equal(t, "cors.preflight", localResponse.StatusCodeDetail)
|
||||
require.Empty(t, localResponse.Data)
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderPluginTrace, "trace"))
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowOrigin, "http://example.com"))
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowMethods, "GET,POST,OPTIONS"))
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowHeaders, "Content-Type,Authorization"))
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlExposeHeaders, "X-Custom-Header"))
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlMaxAge, "3600"))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssue1743InvalidPreflightReturnsNoContentWithoutCorsHeaders(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":scheme", "http"},
|
||||
{":authority", "api.example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "OPTIONS"},
|
||||
{"origin", "http://invalid.com"},
|
||||
{"access-control-request-method", "POST"},
|
||||
})
|
||||
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.Equal(t, uint32(http.StatusNoContent), localResponse.StatusCode)
|
||||
require.Equal(t, "cors.preflight.invalid", localResponse.StatusCodeDetail)
|
||||
require.Empty(t, localResponse.Data)
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderPluginTrace, "trace"))
|
||||
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowOrigin))
|
||||
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowMethods))
|
||||
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowHeaders))
|
||||
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlExposeHeaders))
|
||||
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowCredentials))
|
||||
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlMaxAge))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
}
|
||||
@@ -243,8 +243,8 @@ func TestOnHttpRequestHeaders(t *testing.T) {
|
||||
{"origin", "http://invalid.com"},
|
||||
})
|
||||
|
||||
// 无效的 CORS 请求应该返回 ActionPause
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
// 无效的实际 CORS 请求应该继续转发,由浏览器根据响应头执行 CORS 结果
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
|
||||
107
plugins/wasm-go/extensions/cors/main_vary_test.go
Normal file
107
plugins/wasm-go/extensions/cors/main_vary_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cors/config"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIssue1743SpecificOriginResponseAddsVaryOrigin(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":scheme", "http"},
|
||||
{":authority", "api.example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "GET"},
|
||||
{"origin", "http://example.com"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
action = host.CallOnHttpResponseHeaders([][2]string{
|
||||
{":status", "200"},
|
||||
{"content-type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
responseHeaders := host.GetResponseHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, config.HeaderAccessControlAllowOrigin, "http://example.com"))
|
||||
require.True(t, test.HasHeaderWithValue(responseHeaders, headerVary, varyOrigin))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssue1743SpecificOriginPreflightAddsVaryOrigin(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
host, status := test.NewTestHost(basicCorsConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":scheme", "http"},
|
||||
{":authority", "api.example.com"},
|
||||
{":path", "/api/test"},
|
||||
{":method", "OPTIONS"},
|
||||
{"origin", "http://example.com"},
|
||||
{"access-control-request-method", "POST"},
|
||||
{"access-control-request-headers", "Content-Type"},
|
||||
})
|
||||
require.Equal(t, types.ActionPause, action)
|
||||
|
||||
localResponse := host.GetLocalResponse()
|
||||
require.NotNil(t, localResponse)
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowOrigin, "http://example.com"))
|
||||
require.True(t, test.HasHeaderWithValue(localResponse.Headers, headerVary, varyOrigin))
|
||||
|
||||
host.CompleteHttp()
|
||||
})
|
||||
}
|
||||
|
||||
func TestIssue1743AppendVaryOriginPreservesExistingVaryHeader(t *testing.T) {
|
||||
headers := appendVaryOriginHeader([][2]string{
|
||||
{headerVary, "Accept-Encoding"},
|
||||
}, "http://example.com")
|
||||
|
||||
require.ElementsMatch(t, []string{"Accept-Encoding", varyOrigin}, headerValues(headers, headerVary))
|
||||
}
|
||||
|
||||
func TestIssue1743LiteralWildcardAllowOriginDoesNotAddVaryOrigin(t *testing.T) {
|
||||
headers := appendVaryOriginHeader([][2]string{
|
||||
{config.HeaderAccessControlAllowOrigin, "*"},
|
||||
}, "*")
|
||||
|
||||
require.False(t, test.HasHeader(headers, headerVary))
|
||||
}
|
||||
|
||||
func headerValues(headers [][2]string, name string) []string {
|
||||
values := make([]string, 0)
|
||||
for _, header := range headers {
|
||||
if strings.EqualFold(header[0], name) {
|
||||
values = append(values, header[1])
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
Reference in New Issue
Block a user