From 176ddc69636d347768b525de61b8b8a22deb46fa Mon Sep 17 00:00:00 2001 From: Jun <108045855+2456868764@users.noreply.github.com> Date: Thu, 25 May 2023 19:21:21 +0800 Subject: [PATCH] Plugin cors (#349) --- plugins/wasm-go/extensions/cors/README.md | 230 +++++++++ plugins/wasm-go/extensions/cors/VERSION | 1 + .../extensions/cors/config/cors_config.go | 457 ++++++++++++++++++ .../cors/config/cors_config_test.go | 408 ++++++++++++++++ plugins/wasm-go/extensions/cors/cors.yaml | 67 +++ plugins/wasm-go/extensions/cors/envoy.yaml | 78 +++ plugins/wasm-go/extensions/cors/go.mod | 21 + plugins/wasm-go/extensions/cors/go.sum | 20 + plugins/wasm-go/extensions/cors/main.go | 169 +++++++ 9 files changed, 1451 insertions(+) create mode 100644 plugins/wasm-go/extensions/cors/README.md create mode 100644 plugins/wasm-go/extensions/cors/VERSION create mode 100644 plugins/wasm-go/extensions/cors/config/cors_config.go create mode 100644 plugins/wasm-go/extensions/cors/config/cors_config_test.go create mode 100644 plugins/wasm-go/extensions/cors/cors.yaml create mode 100644 plugins/wasm-go/extensions/cors/envoy.yaml create mode 100644 plugins/wasm-go/extensions/cors/go.mod create mode 100644 plugins/wasm-go/extensions/cors/go.sum create mode 100644 plugins/wasm-go/extensions/cors/main.go diff --git a/plugins/wasm-go/extensions/cors/README.md b/plugins/wasm-go/extensions/cors/README.md new file mode 100644 index 000000000..0015481e0 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/README.md @@ -0,0 +1,230 @@ +# 功能说明 + +`cors` 插件可以为服务端启用 CORS(Cross-Origin Resource Sharing,跨域资源共享)的返回 http 响应头。 + +# 配置字段 + +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +|-----------------------|-----------------|-------|---------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| allow_origins | array of string | 选填 | * | 允许跨域访问的 Origin,格式为 scheme://host:port,示例如 http://example.com:8081。当 allow_credentials 为 false 时,可以使用 * 来表示允许所有 Origin 通过 | +| allow_origin_patterns | array of string | 选填 | - | 允许跨域访问的 Origin 模式匹配, 用 * 匹配域名或者端口,
比如 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,
If-Modified-Since,Cache-Control,Content-Type,Authorization | 允许跨域访问时请求方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 | +| expose_headers | array of string | 选填 | - | 允许跨域访问时响应方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 | +| allow_credentials | bool | 选填 | false | 是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 true,在 allow_origins 不能使用 *, 替换成使用 allow_origin_patterns * | +| max_age | number | 选填 | 86400秒 | 浏览器缓存 CORS 结果的最大时间,单位为秒。
在这个时间范围内,浏览器会复用上一次的检查结果 | + +> 注意 +> * allow_credentials 是一个很敏感的选项,请谨慎开启。开启之后,allow_credentials 和 allow_origins 为 * 不能同时使用,同时设置时, allow_origins 值为 "*" 生效。 +> * allow_origins 和 allow_origin_patterns 可以同时设置, 先检查 allow_origins 是否匹配,然后再检查 allow_origin_patterns 是否匹配 +> * 非法 CORS 请求, HTTP 状态码返回是 403, 返回体内容为 "Invalid CORS request" + +# 配置示例 + +## 允许所有跨域访问, 不允许请求方携带凭据 +```yaml +allow_origins: + - '*' +allow_methods: + - '*' +allow_headers: + - '*' +expose_headers: + - '*' +allow_credentials: false +max_age: 7200 +``` + +## 允许所有跨域访问,同时允许请求方携带凭据 +```yaml +allow_origin_patterns: + - '*' +allow_methods: + - '*' +allow_headers: + - '*' +expose_headers: + - '*' +allow_credentials: true +max_age: 7200 +``` + +## 允许特定子域,特定方法,特定请求头跨域访问,同时允许请求方携带凭据 +```yaml +allow_origin_patterns: + - http://*.example.com + - http://*.example.org:[8080,9090] +allow_methods: + - GET + - PUT + - POST + - DELETE +allow_headers: + - Token + - Content-Type + - Authorization +expose_headers: + - '*' +allow_credentials: true +max_age: 7200 +``` + +# 测试 + +## 测试配置 + +```yaml +apiVersion: networking.higress.io/v1 +kind: McpBridge +metadata: + name: mcp-cors-httpbin + namespace: higress-system +spec: + registries: + - domain: httpbin.org + name: httpbin + port: 80 + type: dns +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + higress.io/destination: httpbin.dns + higress.io/upstream-vhost: "httpbin.org" + higress.io/backend-protocol: HTTP + name: ingress-cors-httpbin + namespace: higress-system +spec: + ingressClassName: higress + rules: + - host: httpbin.example.com + http: + paths: + - backend: + resource: + apiGroup: networking.higress.io + kind: McpBridge + name: mcp-cors-httpbin + path: / + pathType: Prefix +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: wasm-cors-httpbin + namespace: higress-system +spec: + defaultConfigDisable: true + matchRules: + - config: + allow_origins: + - http://httpbin.example.net + allow_origin_patterns: + - http://*.example.com:[*] + - http://*.example.org:[9090,8080] + allow_methods: + - GET + - POST + - PATCH + allow_headers: + - Content-Type + - Token + - Authorization + expose_headers: + - X-Custom-Header + - X-Env-UTM + allow_credentials: true + max_age: 3600 + configDisable: false + ingress: + - ingress-cors-httpbin + url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:1.0.0 + imagePullPolicy: Always +``` + +## 请求测试 + +### 简单请求 +```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 +> access-control-allow-origin: http://httpbin2.example.org:9090 +> access-control-expose-headers: X-Custom-Header,X-Env-UTM +> access-control-allow-credentials: true +``` + +### 预检请求 + +```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 +< 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 +< date: Tue, 23 May 2023 11:41:28 GMT +< server: istio-envoy +< content-length: 0 +< +* Connection #0 to host 127.0.0.1 left intact +* Closing connection 0 +``` + +### 非法 CORS Origin 预检请求 + +```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 +< date: Tue, 23 May 2023 11:27:01 GMT +< server: istio-envoy +< +* Connection #0 to host 127.0.0.1 left intact +Invalid CORS request +``` + +### 非法 CORS Method 预检请求 + +```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 +< date: Tue, 23 May 2023 11:28:51 GMT +< server: istio-envoy +< +* Connection #0 to host 127.0.0.1 left intact +Invalid CORS request +``` + +### 非法 CORS Header 预检请求 + +```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 +< date: Tue, 23 May 2023 11:31:03 GMT +< server: istio-envoy +< +* Connection #0 to host 127.0.0.1 left intact +Invalid CORS request +``` + +# 参考文档 +- https://www.ruanyifeng.com/blog/2016/04/cors.html +- https://fetch.spec.whatwg.org/#http-cors-protocol diff --git a/plugins/wasm-go/extensions/cors/VERSION b/plugins/wasm-go/extensions/cors/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/cors/config/cors_config.go b/plugins/wasm-go/extensions/cors/config/cors_config.go new file mode 100644 index 000000000..0b332ef15 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/config/cors_config.go @@ -0,0 +1,457 @@ +// 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 ( + "errors" + "fmt" + "net/url" + "regexp" + "strings" +) + +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," + + "If-Modified-Since,Cache-Control,Content-Type,Authorization" + defaultMaxAge = 86400 + protocolHttpName = "http" + protocolHttpPort = "80" + protocolHttpsName = "https" + protocolHttpsPort = "443" + + HeaderPluginDebug = "X-Cors-Version" + HeaderPluginTrace = "X-Cors-Trace" + HeaderOrigin = "Origin" + HttpMethodOptions = "OPTIONS" + + HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" + HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderAccessControlMaxAge = "Access-Control-Max-Age" + + HeaderControlRequestMethod = "Access-Control-Request-Method" + HeaderControlRequestHeaders = "Access-Control-Request-Headers" + + HttpContextKey = "CORS" +) + +var portsRegex = regexp.MustCompile(`(.*):\[(\*|\d+(,\d+)*)]`) + +type OriginPattern struct { + declaredPattern string + pattern *regexp.Regexp + patternValue string +} + +func newOriginPatternFromString(declaredPattern string) OriginPattern { + declaredPattern = strings.ToLower(strings.TrimSuffix(declaredPattern, "/")) + matches := portsRegex.FindAllStringSubmatch(declaredPattern, -1) + portList := "" + patternValue := declaredPattern + if len(matches) > 0 { + patternValue = matches[0][1] + portList = matches[0][2] + } + + patternValue = "\\Q" + patternValue + "\\E" + patternValue = strings.ReplaceAll(patternValue, "*", "\\E.*\\Q") + if len(portList) > 0 { + if portList == defaultMatchAll { + patternValue += "(:\\d+)?" + } else { + patternValue += ":(" + strings.ReplaceAll(portList, ",", "|") + ")" + } + } + + return OriginPattern{ + declaredPattern: declaredPattern, + patternValue: patternValue, + pattern: regexp.MustCompile(patternValue), + } +} + +type CorsConfig struct { + // allowOrigins A list of origins for which cross-origin requests are allowed. + // Be a specific domain, e.g. "https://example.com", or the CORS defined special value "*" for all origins. + // Keep in mind however that the CORS spec does not allow "*" when allowCredentials is set to true, using allowOriginPatterns instead + // By default, it is set to "*" when allowOriginPatterns is not set too. + allowOrigins []string + + // allowOriginPatterns A list of origin patterns for which cross-origin requests are allowed + // origins patterns with "*" anywhere in the host name in addition to port + // lists Examples: + // https://*.example.com -- domains ending with example.com + // https://*.example.com:[8080,9090] -- domains ending with example.com on port 8080 or port 9090 + // https://*.example.com:[*] -- domains ending with example.com on any port, including the default port + // The special value "*" allows all origins + // By default, it is not set. + allowOriginPatterns []OriginPattern + + // allowMethods A list of method for which cross-origin requests are allowed + // The special value "*" allows all methods. + // By default, it is set to "GET, PUT, POST, DELETE, PATCH, OPTIONS". + allowMethods []string + + // allowHeaders A list of headers that a pre-flight request can list as allowed + // The special value "*" allows actual requests to send any header + // By default, it is set to "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization" + allowHeaders []string + + // exposeHeaders A list of response headers an actual response might have and can be exposed. + // The special value "*" allows all headers to be exposed for non-credentialed requests. + // By default, it is not set + exposeHeaders []string + + // allowCredentials Whether user credentials are supported. + // By default, it is not set (i.e. user credentials are not supported). + allowCredentials bool + + // maxAge Configure how long, in seconds, the response from a pre-flight request can be cached by clients. + // By default, it is set to 86400 seconds. + maxAge int +} + +type HttpCorsContext struct { + IsValid bool + ValidReason string + IsPreFlight bool + IsCorsRequest bool + AllowOrigin string + AllowMethods string + AllowHeaders string + ExposeHeaders string + AllowCredentials bool + MaxAge int +} + +func (c *CorsConfig) GetVersion() string { + return "1.0.0" +} + +func (c *CorsConfig) FillDefaultValues() { + if len(c.allowOrigins) == 0 && len(c.allowOriginPatterns) == 0 && c.allowCredentials == false { + c.allowOrigins = []string{defaultMatchAll} + } + if len(c.allowHeaders) == 0 { + c.allowHeaders = []string{defaultAllowHeaders} + } + if len(c.allowMethods) == 0 { + c.allowMethods = strings.Split(defaultAllowMethods, ",") + } + if c.maxAge == 0 { + c.maxAge = defaultMaxAge + } +} + +func (c *CorsConfig) AddAllowOrigin(origin string) error { + origin = strings.TrimSpace(origin) + if len(origin) == 0 { + return nil + } + if origin == defaultMatchAll { + if c.allowCredentials == true { + return errors.New("can't set origin to * when allowCredentials is true, use AllowOriginPatterns instead") + } + c.allowOrigins = []string{defaultMatchAll} + return nil + } + c.allowOrigins = append(c.allowOrigins, strings.TrimSuffix(origin, "/")) + return nil +} + +func (c *CorsConfig) AddAllowHeader(header string) { + header = strings.TrimSpace(header) + if len(header) == 0 { + return + } + if header == defaultMatchAll { + c.allowHeaders = []string{defaultMatchAll} + return + } + c.allowHeaders = append(c.allowHeaders, header) +} + +func (c *CorsConfig) AddAllowMethod(method string) { + method = strings.TrimSpace(method) + if len(method) == 0 { + return + } + if method == defaultMatchAll { + c.allowMethods = []string{defaultMatchAll} + return + } + c.allowMethods = append(c.allowMethods, strings.ToUpper(method)) +} + +func (c *CorsConfig) AddExposeHeader(header string) { + header = strings.TrimSpace(header) + if len(header) == 0 { + return + } + if header == defaultMatchAll { + c.exposeHeaders = []string{defaultMatchAll} + return + } + c.exposeHeaders = append(c.exposeHeaders, header) +} + +func (c *CorsConfig) AddAllowOriginPattern(pattern string) { + pattern = strings.TrimSpace(pattern) + if len(pattern) == 0 { + return + } + originPattern := newOriginPatternFromString(pattern) + c.allowOriginPatterns = append(c.allowOriginPatterns, originPattern) +} + +func (c *CorsConfig) SetAllowCredentials(allowCredentials bool) error { + if allowCredentials && len(c.allowOrigins) > 0 && c.allowOrigins[0] == defaultMatchAll { + return errors.New("can't set allowCredentials to true when allowOrigin is *") + } + c.allowCredentials = allowCredentials + return nil +} + +func (c *CorsConfig) SetMaxAge(maxAge int) { + if maxAge <= 0 { + c.maxAge = defaultMaxAge + } else { + c.maxAge = maxAge + } +} + +func (c *CorsConfig) Process(scheme string, host string, method string, headers [][2]string) (HttpCorsContext, error) { + scheme = strings.ToLower(strings.TrimSpace(scheme)) + host = strings.ToLower(strings.TrimSpace(host)) + method = strings.ToLower(strings.TrimSpace(method)) + + // Init httpCorsContext with default values + httpCorsContext := HttpCorsContext{IsValid: true, IsPreFlight: false, IsCorsRequest: false, AllowCredentials: false, MaxAge: 0} + + // Get request origin, controlRequestMethod, controlRequestHeaders from http headers + origin := "" + controlRequestMethod := "" + controlRequestHeaders := "" + for _, header := range headers { + key := header[0] + // Get origin + if strings.ToLower(key) == strings.ToLower(HeaderOrigin) { + origin = strings.TrimSuffix(strings.TrimSpace(header[1]), "/") + } + // Get control request method & headers + if strings.ToLower(key) == strings.ToLower(HeaderControlRequestMethod) { + controlRequestMethod = strings.TrimSpace(header[1]) + } + if strings.ToLower(key) == strings.ToLower(HeaderControlRequestHeaders) { + controlRequestHeaders = strings.TrimSpace(header[1]) + } + } + + // Parse if request is CORS and pre-flight request. + isCorsRequest := c.isCorsRequest(scheme, host, origin) + isPreFlight := c.isPreFlight(origin, method, controlRequestMethod) + httpCorsContext.IsCorsRequest = isCorsRequest + httpCorsContext.IsPreFlight = isPreFlight + + // Skip when it is not CORS request + if !isCorsRequest { + httpCorsContext.IsValid = true + return httpCorsContext, nil + } + + // Check origin + allowOrigin, originOk := c.checkOrigin(origin) + if !originOk { + // Reject: origin is not allowed + httpCorsContext.IsValid = false + httpCorsContext.ValidReason = fmt.Sprintf("origin:%s is not allowed", origin) + return httpCorsContext, nil + } + + // Check method + requestMethod := method + if isPreFlight { + requestMethod = controlRequestMethod + } + allowMethods, methodOk := c.checkMethods(requestMethod) + if !methodOk { + // Reject: method is not allowed + httpCorsContext.IsValid = false + httpCorsContext.ValidReason = fmt.Sprintf("method:%s is not allowed", requestMethod) + return httpCorsContext, nil + } + + // Check headers + allowHeaders, headerOK := c.checkHeaders(controlRequestHeaders) + + if isPreFlight && !headerOK { + // Reject: headers are not allowed + httpCorsContext.IsValid = false + httpCorsContext.ValidReason = "Reject: headers are not allowed" + return httpCorsContext, nil + } + + // Store result in httpCorsContext and return it. + httpCorsContext.AllowOrigin = allowOrigin + if isPreFlight { + httpCorsContext.AllowMethods = allowMethods + } + if isPreFlight && len(allowHeaders) > 0 { + httpCorsContext.AllowHeaders = allowHeaders + } + if isPreFlight && c.maxAge > 0 { + httpCorsContext.MaxAge = c.maxAge + } + if len(c.exposeHeaders) > 0 { + httpCorsContext.ExposeHeaders = strings.Join(c.exposeHeaders, ",") + } + httpCorsContext.AllowCredentials = c.allowCredentials + + return httpCorsContext, nil +} + +func (c *CorsConfig) checkOrigin(origin string) (string, bool) { + origin = strings.TrimSpace(origin) + if len(origin) == 0 { + return "", false + } + + matchOrigin := strings.ToLower(origin) + // Check exact match + for _, allowOrigin := range c.allowOrigins { + if allowOrigin == defaultMatchAll { + return origin, true + } + if strings.ToLower(allowOrigin) == matchOrigin { + return origin, true + } + } + + // Check pattern match + for _, allowOriginPattern := range c.allowOriginPatterns { + if allowOriginPattern.declaredPattern == defaultMatchAll || allowOriginPattern.pattern.MatchString(matchOrigin) { + return origin, true + } + } + + return "", false +} + +func (c *CorsConfig) checkHeaders(requestHeaders string) (string, bool) { + if len(c.allowHeaders) == 0 { + return "", false + } + + if len(requestHeaders) == 0 { + return strings.Join(c.allowHeaders, ","), true + } + + // Return all request headers when allowHeaders contains * + if c.allowHeaders[0] == defaultMatchAll { + return requestHeaders, 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) { + isExist = true + break + } + } + if !isExist { + return "", false + } + } + + return strings.Join(c.allowHeaders, ","), true +} + +func (c *CorsConfig) checkMethods(requestMethod string) (string, bool) { + if len(requestMethod) == 0 { + return "", false + } + + // Find method existed in allowMethods configuration + for _, method := range c.allowMethods { + if method == defaultMatchAll { + return defaultAllAllowMethods, true + } + if strings.ToLower(method) == strings.ToLower(requestMethod) { + return strings.Join(c.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 (c *CorsConfig) isCorsRequest(scheme, host, origin string) bool { + if len(origin) == 0 { + return false + } + + url, err := url.Parse(strings.TrimSpace(origin)) + if err != nil { + return false + } + + // Check scheme + if strings.ToLower(scheme) != strings.ToLower(url.Scheme) { + return true + } + + // Check host and port + port := "" + originPort := "" + originHost := "" + host, port = c.getHostAndPort(scheme, host) + originHost, originPort = c.getHostAndPort(url.Scheme, url.Host) + if host != originHost || port != originPort { + return true + } + + return false +} + +func (c *CorsConfig) getHostAndPort(scheme string, host string) (string, string) { + // Get host and port + scheme = strings.ToLower(scheme) + host = strings.ToLower(host) + port := "" + hosts := strings.Split(host, ":") + if len(hosts) > 1 { + host = hosts[0] + port = hosts[1] + } + // Get default port according scheme + if len(port) == 0 && scheme == protocolHttpName { + port = protocolHttpPort + } + if len(port) == 0 && scheme == protocolHttpsName { + port = protocolHttpsPort + } + return host, port +} diff --git a/plugins/wasm-go/extensions/cors/config/cors_config_test.go b/plugins/wasm-go/extensions/cors/config/cors_config_test.go new file mode 100644 index 000000000..d21e4c831 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/config/cors_config_test.go @@ -0,0 +1,408 @@ +// 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 ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCorsConfig_getHostAndPort(t *testing.T) { + + tests := []struct { + name string + scheme string + host string + wantHost string + wantPort string + }{ + { + name: "http without port", + scheme: "http", + host: "http.example.com", + wantHost: "http.example.com", + wantPort: "80", + }, + { + name: "https without port", + scheme: "https", + host: "http.example.com", + wantHost: "http.example.com", + wantPort: "443", + }, + + { + name: "http with port and case insensitive", + scheme: "hTTp", + host: "hTTp.Example.com:8080", + wantHost: "http.example.com", + wantPort: "8080", + }, + + { + name: "https with port and case insensitive", + scheme: "hTTps", + host: "hTTp.Example.com:8080", + wantHost: "http.example.com", + wantPort: "8080", + }, + + { + name: "protocal is not http", + scheme: "wss", + host: "hTTp.Example.com", + wantHost: "http.example.com", + wantPort: "", + }, + + { + name: "protocal is not http", + scheme: "wss", + host: "hTTp.Example.com:8080", + wantHost: "http.example.com", + wantPort: "8080", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CorsConfig{} + host, port := c.getHostAndPort(tt.scheme, tt.host) + assert.Equal(t, tt.wantHost, host) + assert.Equal(t, tt.wantPort, port) + }) + } +} + +func TestCorsConfig_isCorsRequest(t *testing.T) { + tests := []struct { + name string + scheme string + host string + origin string + want bool + }{ + { + name: "blank origin", + scheme: "http", + host: "httpbin.example.com", + origin: "", + want: false, + }, + { + name: "normal equal case with space and case ", + scheme: "http", + host: "httpbin.example.com", + origin: "http://hTTPbin.Example.com", + want: false, + }, + + { + name: "cors request with port diff", + scheme: "http", + host: "httpbin.example.com", + origin: " http://httpbin.example.com:8080 ", + want: true, + }, + { + name: "cors request with scheme diff", + scheme: "http", + host: "httpbin.example.com", + origin: " https://HTTPpbin.Example.com ", + want: true, + }, + { + name: "cors request with host diff", + scheme: "http", + host: "httpbin.example.com", + origin: " http://HTTPpbin.Example.org ", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CorsConfig{} + assert.Equalf(t, tt.want, c.isCorsRequest(tt.scheme, tt.host, tt.origin), "isCorsRequest(%v, %v, %v)", tt.scheme, tt.host, tt.origin) + }) + } +} + +func TestCorsConfig_isPreFlight(t *testing.T) { + tests := []struct { + name string + origin string + method string + controllerRequestMethod string + want bool + }{ + { + name: "blank case", + origin: "", + method: "", + controllerRequestMethod: "", + want: false, + }, + { + name: "normal case", + origin: "http://httpbin.example.com", + method: "Options", + controllerRequestMethod: "PUT", + want: true, + }, + { + name: "bad case with diff method", + origin: "http://httpbin.example.com", + method: "GET", + controllerRequestMethod: "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) + }) + } +} + +func TestCorsConfig_checkMethods(t *testing.T) { + tests := []struct { + name string + allowMethods []string + requestMethod string + wantMethods string + wantOk bool + }{ + { + name: "default *", + allowMethods: []string{"*"}, + requestMethod: "GET", + wantMethods: defaultAllAllowMethods, + wantOk: true, + }, + { + name: "normal allow case", + allowMethods: []string{"GET", "PUT", "HEAD"}, + requestMethod: "get", + wantMethods: "GET,PUT,HEAD", + wantOk: true, + }, + { + name: "forbidden case", + allowMethods: []string{"GET", "PUT", "HEAD"}, + requestMethod: "POST", + wantMethods: "", + wantOk: false, + }, + + { + name: "blank method", + allowMethods: []string{"GET", "PUT", "HEAD"}, + requestMethod: "", + wantMethods: "", + wantOk: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CorsConfig{ + allowMethods: tt.allowMethods, + } + allowMethods, allowOk := c.checkMethods(tt.requestMethod) + assert.Equalf(t, tt.wantMethods, allowMethods, "checkMethods(%v)", tt.requestMethod) + assert.Equalf(t, tt.wantOk, allowOk, "checkMethods(%v)", tt.requestMethod) + }) + } +} + +func TestCorsConfig_checkHeaders(t *testing.T) { + tests := []struct { + name string + allowHeaders []string + requestHeaders string + wantHeaders string + wantOk bool + }{ + { + name: "not pre-flight", + allowHeaders: []string{"Content-Type", "Authorization"}, + requestHeaders: "", + wantHeaders: "Content-Type,Authorization", + wantOk: true, + }, + { + name: "blank allowheaders case 1", + allowHeaders: []string{}, + requestHeaders: "", + wantHeaders: "", + wantOk: false, + }, + { + name: "blank allowheaders case 2", + requestHeaders: "Authorization", + wantHeaders: "", + wantOk: false, + }, + + { + name: "allowheaders *", + allowHeaders: []string{"*"}, + requestHeaders: "Content-Type,Authorization", + wantHeaders: "Content-Type,Authorization", + wantOk: true, + }, + + { + name: "allowheader values 1", + allowHeaders: []string{"Content-Type", "Authorization"}, + requestHeaders: "Content-Type,Authorization", + wantHeaders: "Content-Type,Authorization", + wantOk: true, + }, + + { + name: "allowheader values 2", + allowHeaders: []string{"Content-Type", "Authorization"}, + requestHeaders: "", + wantHeaders: "Content-Type,Authorization", + wantOk: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CorsConfig{ + allowHeaders: tt.allowHeaders, + } + allowHeaders, allowOk := c.checkHeaders(tt.requestHeaders) + assert.Equalf(t, tt.wantHeaders, allowHeaders, "checkHeaders(%v)", tt.requestHeaders) + assert.Equalf(t, tt.wantOk, allowOk, "checkHeaders(%v)", tt.requestHeaders) + }) + } +} + +func TestCorsConfig_checkOrigin(t *testing.T) { + + tests := []struct { + name string + allowOrigins []string + allowOriginPatterns []OriginPattern + origin string + wantOrigin string + wantOk bool + }{ + { + name: "allowOrigins *", + allowOrigins: []string{defaultMatchAll}, + allowOriginPatterns: []OriginPattern{}, + origin: "http://Httpbin.Example.COM", + wantOrigin: "http://Httpbin.Example.COM", + wantOk: true, + }, + + { + name: "allowOrigins exact match case 1", + allowOrigins: []string{"http://httpbin.example.com"}, + allowOriginPatterns: []OriginPattern{}, + origin: "http://HTTPBin.EXample.COM", + wantOrigin: "http://HTTPBin.EXample.COM", + wantOk: true, + }, + { + name: "allowOrigins exact match case 2", + allowOrigins: []string{"https://httpbin.example.com"}, + allowOriginPatterns: []OriginPattern{}, + origin: "http://HTTPBin.EXample.COM", + wantOrigin: "", + wantOk: false, + }, + + { + name: "OriginPattern pattern match with *", + allowOrigins: []string{}, + allowOriginPatterns: []OriginPattern{ + newOriginPatternFromString("*"), + }, + origin: "http://HTTPBin.EXample.COM", + wantOrigin: "http://HTTPBin.EXample.COM", + wantOk: true, + }, + + { + name: "OriginPattern pattern match case with any port", + allowOrigins: []string{}, + allowOriginPatterns: []OriginPattern{ + newOriginPatternFromString("http://*.example.com:[*]"), + }, + origin: "http://HTTPBin.EXample.COM", + wantOrigin: "http://HTTPBin.EXample.COM", + wantOk: true, + }, + { + name: "OriginPattern pattern match case with any port", + allowOrigins: []string{}, + allowOriginPatterns: []OriginPattern{ + newOriginPatternFromString("http://*.example.com:[*]"), + }, + origin: "http://HTTPBin.EXample.COM:10000", + wantOrigin: "http://HTTPBin.EXample.COM:10000", + wantOk: true, + }, + + { + name: "OriginPattern pattern match case with specail port 1", + allowOrigins: []string{}, + allowOriginPatterns: []OriginPattern{ + newOriginPatternFromString("http://*.example.com:[8080,9090]"), + }, + origin: "http://HTTPBin.EXample.COM:10000", + wantOrigin: "", + wantOk: false, + }, + + { + name: "OriginPattern pattern match case with specail port 2", + allowOrigins: []string{}, + allowOriginPatterns: []OriginPattern{ + newOriginPatternFromString("http://*.example.com:[8080,9090]"), + }, + origin: "http://HTTPBin.EXample.COM:9090", + wantOrigin: "http://HTTPBin.EXample.COM:9090", + wantOk: true, + }, + + { + name: "OriginPattern pattern match case with specail port 3", + allowOrigins: []string{}, + allowOriginPatterns: []OriginPattern{ + newOriginPatternFromString("http://*.example.com:[8080,9090]"), + }, + origin: "http://HTTPBin.EXample.org:9090", + wantOrigin: "", + wantOk: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &CorsConfig{ + allowOrigins: tt.allowOrigins, + allowOriginPatterns: tt.allowOriginPatterns, + } + allowOrigin, allowOk := c.checkOrigin(tt.origin) + assert.Equalf(t, tt.wantOrigin, allowOrigin, "checkOrigin(%v)", tt.origin) + assert.Equalf(t, tt.wantOk, allowOk, "checkOrigin(%v)", tt.origin) + }) + } +} diff --git a/plugins/wasm-go/extensions/cors/cors.yaml b/plugins/wasm-go/extensions/cors/cors.yaml new file mode 100644 index 000000000..90be1d4a4 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/cors.yaml @@ -0,0 +1,67 @@ +apiVersion: networking.higress.io/v1 +kind: McpBridge +metadata: + name: mcp-cors-httpbin + namespace: higress-system +spec: + registries: + - domain: httpbin.org + name: httpbin + port: 80 + type: dns +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + higress.io/destination: httpbin.dns + higress.io/upstream-vhost: "httpbin.org" + higress.io/backend-protocol: HTTP + name: ingress-cors-httpbin + namespace: higress-system +spec: + ingressClassName: higress + rules: + - host: httpbin.example.com + http: + paths: + - backend: + resource: + apiGroup: networking.higress.io + kind: McpBridge + name: mcp-cors-httpbin + path: / + pathType: Prefix +--- +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: wasm-cors-httpbin + namespace: higress-system +spec: + defaultConfigDisable: true + matchRules: + - config: + allow_origins: + - http://httpbin.example.net + allow_origin_patterns: + - http://*.example.com:[*] + - http://*.example.org:[9090,8080] + allow_methods: + - GET + - POST + - PATCH + allow_headers: + - Content-Type + - Token + - Authorization + expose_headers: + - X-Custom-Header + - X-Env-UTM + allow_credentials: true + max_age: 3600 + configDisable: false + ingress: + - ingress-cors-httpbin + url: oci://docker.io/2456868764/cors:1.0.0 + imagePullPolicy: Always \ No newline at end of file diff --git a/plugins/wasm-go/extensions/cors/envoy.yaml b/plugins/wasm-go/extensions/cors/envoy.yaml new file mode 100644 index 000000000..d2cca6414 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/envoy.yaml @@ -0,0 +1,78 @@ +static_resources: + listeners: + - name: main + address: + socket_address: + address: 0.0.0.0 + port_value: 18000 + filter_chains: + - filters: + - name: envoy.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: + - "httpbin.example.com" + routes: + - match: + prefix: "/" + route: + cluster: httpbin + http_filters: + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + value: + config: + configuration: + "@type": type.googleapis.com/google.protobuf.StringValue + value: |- + { + "allow_origins": ["http://httpbin.example.net"], + "allow_origin_patterns": ["http://*.example.com:[*]", "http://*.example.org:[9090,8080]"], + "allow_methods": ["GET","PUT","POST", "PATCH", "HEAD", "OPTIONS"], + "allow_credentials": true, + "allow_headers":["Content-Type", "Token","Authorization"], + "expose_headers":["X-Custom-Header"], + "max_age": 3600 + } + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "./main.wasm" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + + clusters: + - name: httpbin + connect_timeout: 0.5s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + dns_refresh_rate: 5s + dns_lookup_family: V4_ONLY + load_assignment: + cluster_name: httpbin + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin.org + port_value: 80 + + +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8001 diff --git a/plugins/wasm-go/extensions/cors/go.mod b/plugins/wasm-go/extensions/cors/go.mod new file mode 100644 index 000000000..9735611d2 --- /dev/null +++ b/plugins/wasm-go/extensions/cors/go.mod @@ -0,0 +1,21 @@ +module cors + +go 1.19 + +replace github.com/alibaba/higress/plugins/wasm-go => ../.. + +require ( + github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230519024024-625c06e58f91 + github.com/stretchr/testify v1.8.3 + github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 + github.com/tidwall/gjson v1.14.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/plugins/wasm-go/extensions/cors/go.sum b/plugins/wasm-go/extensions/cors/go.sum new file mode 100644 index 000000000..2df2b880a --- /dev/null +++ b/plugins/wasm-go/extensions/cors/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M= +github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/wasm-go/extensions/cors/main.go b/plugins/wasm-go/extensions/cors/main.go new file mode 100644 index 000000000..146d14d7c --- /dev/null +++ b/plugins/wasm-go/extensions/cors/main.go @@ -0,0 +1,169 @@ +// 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 ( + "cors/config" + "fmt" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" +) + +func main() { + wrapper.SetCtx( + "cors", + wrapper.ParseConfigBy(parseConfig), + wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ProcessRequestBodyBy(onHttpRequestBody), + wrapper.ProcessResponseBodyBy(onHttpResponseBody), + wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders), + ) +} + +func parseConfig(json gjson.Result, corsConfig *config.CorsConfig, log wrapper.Log) error { + log.Debug("parseConfig()") + allowOrigins := json.Get("allow_origins").Array() + for _, origin := range allowOrigins { + if err := corsConfig.AddAllowOrigin(origin.String()); err != nil { + log.Warnf("failed to AddAllowOrigin:%s, error:%v", origin, err) + } + } + allowOriginPatterns := json.Get("allow_origin_patterns").Array() + for _, pattern := range allowOriginPatterns { + corsConfig.AddAllowOriginPattern(pattern.String()) + } + allowMethods := json.Get("allow_methods").Array() + for _, method := range allowMethods { + corsConfig.AddAllowMethod(method.String()) + } + allowHeaders := json.Get("allow_headers").Array() + for _, header := range allowHeaders { + corsConfig.AddAllowHeader(header.String()) + } + exposeHeaders := json.Get("expose_headers").Array() + for _, header := range exposeHeaders { + corsConfig.AddExposeHeader(header.String()) + } + allowCredentials := json.Get("allow_credentials").Bool() + if err := corsConfig.SetAllowCredentials(allowCredentials); err != nil { + log.Warnf("failed to set AllowCredentials error: %v", err) + } + maxAge := json.Get("max_age").Int() + corsConfig.SetMaxAge(int(maxAge)) + + // Fill default values + corsConfig.FillDefaultValues() + log.Debugf("corsConfig:%+v", corsConfig) + return nil +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig, log wrapper.Log) types.Action { + log.Debug("onHttpRequestHeaders()") + requestUrl, _ := proxywasm.GetHttpRequestHeader(":path") + method, _ := proxywasm.GetHttpRequestHeader(":method") + host := ctx.Host() + scheme := ctx.Scheme() + log.Debugf("scheme:%s, host:%s, method:%s, request:%s", scheme, host, method, requestUrl) + // Get headers + headers, _ := proxywasm.GetHttpRequestHeaders() + // Process request + httpCorsContext, err := corsConfig.Process(scheme, host, method, headers) + if err != nil { + log.Warnf("failed to process %s : %v", requestUrl, err) + return types.ActionContinue + } + log.Debugf("Process httpCorsContext:%+v", httpCorsContext) + // 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.SendHttpResponse(403, headers, []byte("Invalid CORS request"), -1) + return types.ActionPause + } + + // Response directly when it is cors preflight request + if httpCorsContext.IsPreFlight { + headers := make([][2]string, 0) + headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"}) + proxywasm.SendHttpResponse(200, headers, nil, -1) + return types.ActionPause + } + + return types.ActionContinue +} + +func onHttpRequestBody(ctx wrapper.HttpContext, corsConfig config.CorsConfig, body []byte, log wrapper.Log) types.Action { + log.Debug("onHttpRequestBody()") + return types.ActionContinue +} + +func onHttpResponseHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig, log wrapper.Log) types.Action { + log.Debug("onHttpResponseHeaders()") + // Remove trace header if existed + proxywasm.RemoveHttpResponseHeader(config.HeaderPluginTrace) + // Remove upstream cors response headers if existed + proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowOrigin) + proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowMethods) + proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlExposeHeaders) + proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowCredentials) + proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlMaxAge) + // Add debug header + proxywasm.AddHttpResponseHeader(config.HeaderPluginDebug, corsConfig.GetVersion()) + + // Restore httpCorsContext from HttpContext + httpCorsContext, ok := ctx.GetContext(config.HttpContextKey).(config.HttpCorsContext) + if !ok { + log.Debug("restore httpCorsContext from HttpContext error") + return types.ActionContinue + } + log.Debugf("Restore httpCorsContext:%+v", httpCorsContext) + + // Skip which it is not valid or not cors request + if !httpCorsContext.IsValid || !httpCorsContext.IsCorsRequest { + return types.ActionContinue + } + + // 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)) + } + + return types.ActionContinue +} + +func onHttpResponseBody(ctx wrapper.HttpContext, corsConfig config.CorsConfig, body []byte, log wrapper.Log) types.Action { + log.Debug("onHttpResponseBody()") + return types.ActionContinue +}