add plugin gw-error-format (#116)

Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
This commit is contained in:
jiahao zhang
2023-05-04 09:42:41 +08:00
committed by GitHub
parent 48978e5135
commit daffd18674
6 changed files with 223 additions and 0 deletions

View File

@@ -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里面不会带有这个headerx-envoy-upstream-service-time
本插件只在没有获取到此x-envoy-upstream-service-time响应头时生效

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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

View File

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