diff --git a/plugins/wasm-go/extensions/gw-error-format/README.md b/plugins/wasm-go/extensions/gw-error-format/README.md new file mode 100644 index 000000000..a72b80a35 --- /dev/null +++ b/plugins/wasm-go/extensions/gw-error-format/README.md @@ -0,0 +1,50 @@ +# 功能说明 +`gw-error-format`本插件实现了匹配网关未转发到后端服务时的响应状态码和响应内容体并替换返回自定义响应内容 + +# 配置字段 +| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 | +| -------- | -------- | -------- | -------- | -------- | +| rules.match.statuscode | string | 必填 | - | 匹配响应状态码 | +| rules.match.responsebody | string | 必填 | - | 匹配响应体 | +| rules.replace.statuscode | string | 必填 | - | 替换后的响应状态码 | +| rules.replace.responsebody | string | 必填 | - | 替换后的响应体 | +| set_header | array of object | 选填 | - | 添加/替换响应头,例如:- content-type: "application/json" | + +# 配置示例 +```yaml +rules: +- match: + statuscode: "403" + responsebody: "RBAC: access denied" + replace: + statuscode: "200" + responsebody: "{\"code\":401,\"message\":\"User is not authenticated\"}" +- match: + statuscode: "503" + responsebody: "no healthy upstream" + replace: + statuscode: "200" + responsebody: "{\"code\":404,\"message\":\"No Healthy Service\"}" +set_header: +- Access-Control-Allow-Credentials: "true" +- Access-Control-Allow-Origin: "*" +- Access-Control-Allow-Headers: "*" +- Access-Control-Allow-Methods: "*" +- Access-Control-Expose-Headers: "*" +- Content-Type: "application/json;charset=UTF-8" +``` + +## 示例说明: +以上配置示例作用于当前实例全局生效 + +match下指定的statuscode和responsebody将被替换为同级中的replace下的statuscode和responsebody + +以上示例当某个请求返回的响应状态码是403并且响应内容体是RBAC: access denied的则替换状态码为200和响应内容体为json格式"{"code":401,"message":"User is not authenticated"}" + +如果需要新增/替换response header则可以在rules同级中添加set_header字段,当有match下的statuscode匹配上之后会将set_header的内容带在response header + + +## 小提示: +当envoy网关还未转发至后端服务时response header里面不会带有这个header:x-envoy-upstream-service-time +本插件只在没有获取到此x-envoy-upstream-service-time响应头时生效 + diff --git a/plugins/wasm-go/extensions/gw-error-format/VERSION b/plugins/wasm-go/extensions/gw-error-format/VERSION new file mode 100644 index 000000000..afaf360d3 --- /dev/null +++ b/plugins/wasm-go/extensions/gw-error-format/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/gw-error-format/go.mod b/plugins/wasm-go/extensions/gw-error-format/go.mod new file mode 100644 index 000000000..b5c437da5 --- /dev/null +++ b/plugins/wasm-go/extensions/gw-error-format/go.mod @@ -0,0 +1,20 @@ +module wasm-demo + +go 1.18 + +require ( + github.com/mse-group/wasm-extensions-go v1.0.1 + github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c + github.com/tidwall/gjson v1.14.3 +) + +require ( + github.com/alibaba/higress/plugins/wasm-go v0.0.0-20221116034346-4eb91e6918b8 + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-redis/redis v6.15.9+incompatible // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/plugins/wasm-go/extensions/gw-error-format/go.sum b/plugins/wasm-go/extensions/gw-error-format/go.sum new file mode 100644 index 000000000..761e83cc0 --- /dev/null +++ b/plugins/wasm-go/extensions/gw-error-format/go.sum @@ -0,0 +1,24 @@ +github.com/alibaba/higress/plugins/wasm-go v0.0.0-20221116034346-4eb91e6918b8 h1:mpxRyDnAED+3xv5Lx92jVJZyEm1lKlTpryNnGK/Ikz4= +github.com/alibaba/higress/plugins/wasm-go v0.0.0-20221116034346-4eb91e6918b8/go.mod h1:JZEtmL2/oa24moc8fVXug1gMsOd/dnQM38e3pR5tZ/M= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +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/mse-group/wasm-extensions-go v1.0.0 h1:hYkU8sIs8/rTEThrG8kEl8woh3tklEWeljGJS11rJe0= +github.com/mse-group/wasm-extensions-go v1.0.0/go.mod h1:N9MtZ4Oreog4gx67BBVJGM+cl/SgRy1Vm5OEKidQEYM= +github.com/mse-group/wasm-extensions-go v1.0.1 h1:9AotUmzsc6R0X8uezQj3OHgId0YCNPCPubXT+8ciY0E= +github.com/mse-group/wasm-extensions-go v1.0.1/go.mod h1:N9MtZ4Oreog4gx67BBVJGM+cl/SgRy1Vm5OEKidQEYM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c h1:OCUFXVGixHLfNjg6/QYEhv+jHJ5mRGhpEUVFv9eWPJE= +github.com/tetratelabs/proxy-wasm-go-sdk v0.19.1-0.20220822060051-f9d179a57f8c/go.mod h1:5t/pWFNJ9eMyu/K/Z+OeGhDJ9sN9eCo8fc2pyM/Qjg4= +github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= +github.com/tidwall/gjson v1.14.3/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugins/wasm-go/extensions/gw-error-format/gw-error-format.yaml b/plugins/wasm-go/extensions/gw-error-format/gw-error-format.yaml new file mode 100644 index 000000000..57046cb66 --- /dev/null +++ b/plugins/wasm-go/extensions/gw-error-format/gw-error-format.yaml @@ -0,0 +1,30 @@ +apiVersion: extensions.istio.io/v1alpha1 +kind: WasmPlugin +metadata: + name: gw-error-format + namespace: higress-system +spec: + selector: + matchLabels: + higress: higress-system-higress-gateway + pluginConfig: + rules: + - match: + statuscode: "200" + responsebody: "bar" + replace: + statuscode: "401" + responsebody: "{\"code\":401,\"message\":\"User is not authenticated\"}" + - match: + statuscode: "503" + responsebody: "no healthy upstream" + replace: + statuscode: "200" + responsebody: "{\"code\":404,\"message\":\"No Healthy Service\"}" + set_header: + - access-control-allow-credentials: "true" + - access-control-allow-origin: "*" + - access-control-expose-headers: "*" + - content-type: "application/json;charset=UTF-8" + - custom-header: "HelloWorld" + url: oci://docker.io/zhangjiahaol/envoy-plugin:gw-error-format-2.0.0 diff --git a/plugins/wasm-go/extensions/gw-error-format/main.go b/plugins/wasm-go/extensions/gw-error-format/main.go new file mode 100644 index 000000000..89fbf22b9 --- /dev/null +++ b/plugins/wasm-go/extensions/gw-error-format/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "errors" + + "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( + "gw-error-format", + wrapper.ParseConfigBy(parseConfig), + wrapper.ProcessResponseHeadersBy(onHttpResponseHeader), + wrapper.ProcessResponseBodyBy(onHttpResponseBody), + ) +} + +type MyConfig struct { + rules []gjson.Result + set_header []gjson.Result +} + +func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error { + config.set_header = json.Get("set_header").Array() + config.rules = json.Get("rules").Array() + for _, item := range config.rules { + log.Info("config.rules: " + item.String()) + if item.Get("match.statuscode").String() == "" { + return errors.New("missing match.statuscode in config") + } + if item.Get("replace.statuscode").String() == "" { + return errors.New("missing replace.statuscode in config") + } + } + + return nil +} + +func onHttpResponseHeader(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action { + dontReadResponseBody := false + currentStatuscode, _ := proxywasm.GetHttpResponseHeader(":status") + + for _, item := range config.rules { + configMatchStatuscode := item.Get("match.statuscode").String() + configReplaceStatuscode := item.Get("replace.statuscode").String() + switch currentStatuscode { + // configMatchStatuscode value example: "403" or "503": + case configMatchStatuscode: + // If the response header `x-envoy-upstream-service-time` is not found, the request has not been forwarded to the backend service + _, err := proxywasm.GetHttpResponseHeader("x-envoy-upstream-service-time") + if err != nil { + proxywasm.RemoveHttpResponseHeader("content-length") + proxywasm.ReplaceHttpResponseHeader(":status", configReplaceStatuscode) + for _, item_header := range config.set_header { + item_header.ForEach(func(key, value gjson.Result) bool { + err := proxywasm.ReplaceHttpResponseHeader(key.String(), value.String()) + if err != nil { + log.Critical("failed ReplaceHttpResponseHeader" + item_header.String()) + } + return true + }) + } + // goto func onHttpResponseBody + return types.ActionContinue + } else { + dontReadResponseBody = true + break + } + default: + // There is no matching rule + dontReadResponseBody = true + } + } + + // If there is no rule match or no header for x-envoy-upstream-service-time, the onHttpResponseBody is not exec + if dontReadResponseBody == true { + ctx.DontReadResponseBody() + } + return types.ActionContinue +} + +func onHttpResponseBody(ctx wrapper.HttpContext, config MyConfig, body []byte, log wrapper.Log) types.Action { + bodyStr := string(body) + + for _, item := range config.rules { + configMatchResponsebody := item.Get("match.responsebody").String() + configReplaceResponsebody := item.Get("replace.responsebody").String() + if bodyStr == configMatchResponsebody { + proxywasm.ReplaceHttpResponseBody([]byte(configReplaceResponsebody)) + return types.ActionContinue + } + } + + return types.ActionContinue +}