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
+}