Files
higress/plugins/wasm-go/extensions/transformer/main.go

1468 lines
43 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"strings"
"regexp"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/log"
"github.com/pkg/errors"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/higress-group/wasm-go/pkg/wrapper"
)
func main() {}
func init() {
wrapper.SetCtx(
"transformer",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
)
}
// @Name transformer
// @Category custom
// @Phase UNSPECIFIED_PHASE
// @Priority 100
// @Title zh-CN 请求/响应转换器
// @Title en-US Request/Response Transformer
// @Description zh-CN transformer 插件可以对请求/响应头、请求查询参数、请求/响应体参数进行转换,支持的转换操作类型包括删除、重命名、更新、添加、追加、映射、去重。
// @Description en-US The transformer plugin can transform request/response headers, request query parameters, and request/response body parameters. Supported transform operations include remove, rename, replace, add, append, map, and dedupe.
// @IconUrl https://img.alicdn.com/imgextra/i1/O1CN018iKKih1iVx287RltL_!!6000000004419-2-tps-42-42.png
// @Version 1.0.0
//
// @Contact.name Higress Team
// @Contact.url http://higress.io/
// @Contact.email admin@higress.io
//
// @Example
// reqRules:
// - operate: remove
// headers:
// - key: X-remove
// querys:
// - key: k1
// body:
// - key: a1
// - operate: rename
// headers:
// - oldKey: X-not-renamed
// newKey: X-renamed
// - operate: replace
// headers:
// - key: X-replace
// newValue: replaced
// - operate: add
// headers:
// - key: X-add-append
// value: host-$1
// host_pattern: ^(.*)\.com$
// - operate: append
// headers:
// - key: X-add-append
// appendValue: path-$1
// path_pattern: ^.*?\/(\w+)[\?]{0,1}.*$
// body:
// - key: a1-new
// appendValue: t1-$1-append
// value_type: string
// host_pattern: ^(.*)\.com$
// - operate: map
// headers:
// - fromKey: X-add-append
// toKey: X-map
// - operate: dedupe
// headers:
// - key: X-dedupe-first
// stratergy: RETAIN_FIRST
//
// @End
type TransformerConfig struct {
// @Title 是否重新路由
// @Description 是否在请求转换过程中对路由目标进行重新选择,默认为 true
reroute bool `yaml:"reroute"`
// @Title 转换规则
// @Description 指定转换操作类型以及请求/响应头、请求查询参数、请求/响应体参数的转换规则
reqRules []TransformRule `yaml:"reqRules"`
respRules []TransformRule `yaml:"respRules"`
// this field is not exposed to the user and is used to store the request and response transformer instance
reqTrans Transformer `yaml:"-"`
respTrans Transformer `yaml:"-"`
}
type TransformRule struct {
// @Title 转换操作类型
// @Description 指定转换操作类型,可选值为 remove, rename, replace, add, append, map, dedupe
operate string `yaml:"operate"`
// @Title 映射来源类型
// @Description map操作可使用该字段进行跨类型映射可选值为headers, query, body若yaml中未出现该字段则默认map操作不做跨类型映射代码内设定为"self"若yaml中无map操作要求代码内设定为空字符串
mapSource string `yaml:"mapSource"`
// @Title 请求/响应头转换规则
// @Description 指定请求/响应头转换规则
headers []Param `yaml:"headers"`
// @Title 请求查询参数转换规则
// @Description 指定请求查询参数转换规则
querys []Param `yaml:"querys"`
// @Title 请求/响应体参数转换规则
// @Description 指定请求/响应体参数转换规则,请求体转换允许 content-type 为 application/json, application/x-www-form-urlencoded, multipart/form-data响应体转换仅允许 content-type 为 application/json
body []Param `yaml:"body"`
}
type RemoveParam struct {
// @Title 目标key
// @Description
key string `yaml:"key"`
}
type RenameParam struct {
// @Title 目标
// @Description
oldKey string `yaml:"oldKey"`
// @Title 新的key名称
// @Description
newKey string `yaml:"newKey"`
}
type ReplaceParam struct {
// @Title 目标key
// @Description
key string `yaml:"key"`
// @Title 新的value值
// @Description
newValue string `yaml:"newValue"`
}
type AddParam struct {
// @Title 添加的key
// @Description
key string `yaml:"key"`
// @Title 添加的value
// @Description
value string `yaml:"value"`
}
type AppendParam struct {
// @Title 目标key
// @Description
key string `yaml:"key"`
// @Title 追加的value值
// @Description
appendValue string `yaml:"appendValue"`
}
type MapParam struct {
// @Title 映射来源key
// @Description
fromKey string `yaml:"fromKey"`
// @Title 映射目标
// @Description
toKey string `yaml:"toKey"`
}
type DedupeParam struct {
// @Title 目标key
// @Description
key string `yaml:"key"`
// @Title 指定去重策略
// @Description
strategy string `yaml:"strategy"`
}
type Param struct {
removeParam RemoveParam
renameParam RenameParam
replaceParam ReplaceParam
addParam AddParam
appendParam AppendParam
mapParam MapParam
dedupeParam DedupeParam
// @Title 值类型
// @Description 当 content-type=application/json 时,为请求/响应体参数指定值类型,可选值为 object, boolean, number, string(default)
valueType string `yaml:"value_type"`
// @Title 请求主机名匹配规则
// @Description 指定主机名匹配规则,当转换操作类型为 replace, add, append 时有效
hostPattern string `yaml:"host_pattern"`
// @Title 请求路径匹配规则
// @Description 指定路径匹配规则,当转换操作类型为 replace, add, append 时有效
pathPattern string `yaml:"path_pattern"`
}
func parseConfig(json gjson.Result, config *TransformerConfig, log log.Log) (err error) {
reroute := json.Get("reroute")
if !reroute.Exists() {
config.reroute = true
} else {
config.reroute = reroute.Bool()
}
reqRulesInJson := json.Get("reqRules")
respRulesInJson := json.Get("respRules")
if !reqRulesInJson.Exists() && !respRulesInJson.Exists() {
return errors.New("transformer rule not exist in yaml")
}
if reqRulesInJson.Exists() {
config.reqRules, err = newTransformRule(reqRulesInJson.Array())
}
if respRulesInJson.Exists() {
config.respRules, err = newTransformRule(respRulesInJson.Array())
}
if err != nil {
return errors.Wrapf(err, "failed to new transform rule")
}
if config.reqRules != nil {
config.reqTrans, err = newRequestTransformer(config)
}
if config.respRules != nil {
config.respTrans, err = newResponseTransformer(config)
}
if err != nil {
return errors.Wrapf(err, "failed to new transformer")
}
log.Infof("transform config is: reqRules:%+v, respRules:%+v", config.reqRules, config.respRules)
return nil
}
// TODO: 增加检查某些字段比如oldKey&newKey未同时存在时的提示信息
func constructParam(item gjson.Result, op, valueType string) Param {
p := Param{
valueType: valueType,
}
switch op {
case "remove":
p.removeParam.key = item.Get("key").String()
case "rename":
p.renameParam.oldKey = item.Get("oldKey").String()
p.renameParam.newKey = item.Get("newKey").String()
case "replace":
p.replaceParam.key = item.Get("key").String()
p.replaceParam.newValue = item.Get("newValue").String()
case "add":
p.addParam.key = item.Get("key").String()
p.addParam.value = item.Get("value").String()
case "append":
p.appendParam.key = item.Get("key").String()
p.appendParam.appendValue = item.Get("appendValue").String()
case "map":
p.mapParam.fromKey = item.Get("fromKey").String()
p.mapParam.toKey = item.Get("toKey").String()
case "dedupe":
p.dedupeParam.key = item.Get("key").String()
p.dedupeParam.strategy = item.Get("strategy").String()
}
if op == "replace" || op == "add" || op == "append" {
p.hostPattern = item.Get("host_pattern").String()
p.pathPattern = item.Get("path_pattern").String()
}
return p
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log log.Log) types.Action {
if !config.reroute {
log.Debug("disable reroute")
ctx.DisableReroute()
}
// because it may be a response transformer, so the setting of host and path have to advance
host, path := ctx.Host(), ctx.Path()
ctx.SetContext("host", host)
ctx.SetContext("path", path)
if config.reqTrans == nil {
ctx.DontReadRequestBody()
return types.ActionContinue
}
log.Debug("on http request headers ...")
headers, err := proxywasm.GetHttpRequestHeaders()
if err != nil {
log.Warn("failed to get request headers")
ctx.DontReadRequestBody()
return types.ActionContinue
}
hs := convertHeaders(headers)
if hs[":authority"] == nil {
log.Warn(errGetRequestHost.Error())
ctx.DontReadRequestBody()
return types.ActionContinue
}
if hs[":path"] == nil {
log.Warn(errGetRequestPath.Error())
ctx.DontReadRequestBody()
return types.ActionContinue
}
contentType := ""
if hs["content-type"] != nil {
contentType = hs["content-type"][0]
}
ctx.SetContext("content-type", contentType)
isValidRequestContent := isValidRequestContentType(contentType)
isBodyChange := config.reqTrans.IsBodyChange()
needBodyMapSource := config.reqTrans.NeedBodyMapSource()
log.Debugf("contentType:%s, isValidRequestContent:%v, isBodyChange:%v, needBodyMapSource:%v",
contentType, isValidRequestContent, isBodyChange, needBodyMapSource)
if isBodyChange && isValidRequestContent {
delete(hs, "content-length")
}
qs, err := parseQueryByPath(path)
if err != nil {
log.Warnf("failed to parse query params by path: %v", err)
ctx.DontReadRequestBody()
return types.ActionContinue
}
ctx.SetContext("headers", hs)
ctx.SetContext("querys", qs)
if !isValidRequestContent || (!isBodyChange && !needBodyMapSource) {
ctx.DontReadRequestBody()
} else if needBodyMapSource {
// we need do transform during body phase
ctx.SetContext("need_head_trans", struct{}{})
log.Debug("delay header's transform to body phase")
return types.HeaderStopIteration
}
mapSourceData := make(map[string]MapSourceData)
mapSourceData["headers"] = MapSourceData{
mapSourceType: "headers",
kvs: hs,
}
mapSourceData["querys"] = MapSourceData{
mapSourceType: "querys",
kvs: qs,
}
if config.reqTrans.IsHeaderChange() {
if err = config.reqTrans.TransformHeaders(host, path, hs, mapSourceData); err != nil {
log.Warnf("failed to transform request headers: %v", err)
ctx.DontReadRequestBody()
return types.ActionContinue
}
}
if config.reqTrans.IsQueryChange() {
if err = config.reqTrans.TransformQuerys(host, path, qs, mapSourceData); err != nil {
log.Warnf("failed to transform request query params: %v", err)
ctx.DontReadRequestBody()
return types.ActionContinue
}
path, err = constructPath(path, qs)
if err != nil {
log.Warnf("failed to construct path: %v", err)
ctx.DontReadRequestBody()
return types.ActionContinue
}
hs[":path"] = []string{path}
}
headers = reconvertHeaders(hs)
if err = proxywasm.ReplaceHttpRequestHeaders(headers); err != nil {
log.Warnf("failed to replace request headers: %v", err)
ctx.DontReadRequestBody()
return types.ActionContinue
}
return types.ActionContinue
}
func onHttpRequestBody(ctx wrapper.HttpContext, config TransformerConfig, body []byte, log log.Log) types.Action {
if config.reqTrans == nil {
return types.ActionContinue
}
log.Debug("on http request body ...")
host, path, err := getHostAndPathFromHttpCtx(ctx)
if err != nil {
log.Warn(err.Error())
return types.ActionContinue
}
contentType, ok := ctx.GetContext("content-type").(string)
if !ok {
log.Warn(errGetContentType.Error())
return types.ActionContinue
}
structuredBody, err := parseBody(contentType, body)
if err != nil {
if !errors.Is(err, errEmptyBody) {
log.Warnf("failed to parse request body: %v", err)
}
log.Debug("request body is empty")
return types.ActionContinue
}
mapSourceData := make(map[string]MapSourceData)
var hs map[string][]string
var qs map[string][]string
hs = ctx.GetContext("headers").(map[string][]string)
if hs == nil {
log.Warn("failed to get request headers")
return types.ActionContinue
}
if hs[":authority"] == nil {
log.Warn(errGetRequestHost.Error())
return types.ActionContinue
}
if hs[":path"] == nil {
log.Warn(errGetRequestPath.Error())
return types.ActionContinue
}
mapSourceData["headers"] = MapSourceData{
mapSourceType: "headers",
kvs: hs,
}
qs = ctx.GetContext("querys").(map[string][]string)
if qs == nil {
log.Warn("failed to get request querys")
return types.ActionContinue
}
mapSourceData["querys"] = MapSourceData{
mapSourceType: "querys",
kvs: qs,
}
switch structuredBody.(type) {
case map[string]interface{}:
mapSourceData["body"] = MapSourceData{
mapSourceType: "bodyJson",
json: structuredBody.(map[string]interface{})["body"].([]byte),
}
case map[string][]string:
mapSourceData["body"] = MapSourceData{
mapSourceType: "bodyKv",
kvs: structuredBody.(map[string][]string),
}
}
if ctx.GetContext("need_head_trans") != nil {
if config.reqTrans.IsHeaderChange() {
if err = config.reqTrans.TransformHeaders(host, path, hs, mapSourceData); err != nil {
log.Warnf("failed to transform request headers: %v", err)
return types.ActionContinue
}
}
if config.reqTrans.IsQueryChange() {
if err = config.reqTrans.TransformQuerys(host, path, qs, mapSourceData); err != nil {
log.Warnf("failed to transform request query params: %v", err)
return types.ActionContinue
}
path, err = constructPath(path, qs)
if err != nil {
log.Warnf("failed to construct path: %v", err)
return types.ActionContinue
}
hs[":path"] = []string{path}
}
headers := reconvertHeaders(hs)
if err = proxywasm.ReplaceHttpRequestHeaders(headers); err != nil {
log.Warnf("failed to replace request headers: %v", err)
return types.ActionContinue
}
}
if !config.reqTrans.IsBodyChange() {
return types.ActionContinue
}
if err = config.reqTrans.TransformBody(host, path, structuredBody, mapSourceData); err != nil {
log.Warnf("failed to transform request body: %v", err)
return types.ActionContinue
}
body, err = constructBody(contentType, structuredBody)
if err != nil {
log.Warnf("failed to construct request body: %v", err)
return types.ActionContinue
}
if err = proxywasm.ReplaceHttpRequestBody(body); err != nil {
log.Warnf("failed to replace request body: %v", err)
return types.ActionContinue
}
return types.ActionContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config TransformerConfig, log log.Log) types.Action {
if config.respTrans == nil {
ctx.DontReadResponseBody()
return types.ActionContinue
}
log.Debug("on http response headers ...")
host, path, err := getHostAndPathFromHttpCtx(ctx)
if err != nil {
log.Warn(err.Error())
ctx.DontReadResponseBody()
return types.ActionContinue
}
headers, err := proxywasm.GetHttpResponseHeaders()
if err != nil {
log.Warnf("failed to get response headers: %v", err)
ctx.DontReadResponseBody()
return types.ActionContinue
}
hs := convertHeaders(headers)
ctx.SetContext("headers", hs)
contentType := ""
if hs["content-type"] != nil {
contentType = hs["content-type"][0]
}
ctx.SetContext("content-type", contentType)
isValidResponseContent := isValidResponseContentType(contentType)
isBodyChange := config.respTrans.IsBodyChange()
needBodyMapSource := config.respTrans.NeedBodyMapSource()
if isBodyChange && isValidResponseContent {
delete(hs, "content-length")
}
if !isValidResponseContent || (!isBodyChange && !needBodyMapSource) {
ctx.DontReadResponseBody()
} else if needBodyMapSource {
// we need do transform during body phase
ctx.SetContext("need_head_trans", struct{}{})
return types.HeaderStopIteration
}
mapSourceData := make(map[string]MapSourceData)
mapSourceData["headers"] = MapSourceData{
mapSourceType: "headers",
kvs: hs,
}
if config.respTrans.IsHeaderChange() {
if err = config.respTrans.TransformHeaders(host, path, hs, mapSourceData); err != nil {
log.Warnf("failed to transform response headers: %v", err)
ctx.DontReadResponseBody()
return types.ActionContinue
}
}
headers = reconvertHeaders(hs)
if err = proxywasm.ReplaceHttpResponseHeaders(headers); err != nil {
log.Warnf("failed to replace response headers: %v", err)
ctx.DontReadResponseBody()
return types.ActionContinue
}
return types.ActionContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, config TransformerConfig, body []byte, log log.Log) types.Action {
if config.respTrans == nil {
return types.ActionContinue
}
log.Debug("on http response body ...")
host, path, err := getHostAndPathFromHttpCtx(ctx)
if err != nil {
log.Warn(err.Error())
return types.ActionContinue
}
contentType, ok := ctx.GetContext("content-type").(string)
if !ok {
log.Warn(errGetContentType.Error())
return types.ActionContinue
}
structuredBody, err := parseBody(contentType, body)
if err != nil {
if !errors.Is(err, errEmptyBody) {
log.Warnf("failed to parse response body: %v", err)
}
log.Debug("response body is empty")
return types.ActionContinue
}
mapSourceData := make(map[string]MapSourceData)
var hs map[string][]string
hs = ctx.GetContext("headers").(map[string][]string)
if hs == nil {
log.Warn("failed to get response headers")
return types.ActionContinue
}
mapSourceData["headers"] = MapSourceData{
mapSourceType: "headers",
kvs: hs,
}
switch structuredBody.(type) {
case map[string]interface{}:
mapSourceData["body"] = MapSourceData{
mapSourceType: "bodyJson",
json: structuredBody.(map[string]interface{})["body"].([]byte),
}
case map[string][]string:
mapSourceData["body"] = MapSourceData{
mapSourceType: "bodyKv",
kvs: structuredBody.(map[string][]string),
}
}
if ctx.GetContext("need_head_trans") != nil {
if config.respTrans.IsHeaderChange() {
if err = config.respTrans.TransformHeaders(host, path, hs, mapSourceData); err != nil {
log.Warnf("failed to transform response headers: %v", err)
return types.ActionContinue
}
}
headers := reconvertHeaders(hs)
if err = proxywasm.ReplaceHttpResponseHeaders(headers); err != nil {
log.Warnf("failed to replace response headers: %v", err)
return types.ActionContinue
}
}
if !config.respTrans.IsBodyChange() {
return types.ActionContinue
}
if err = config.respTrans.TransformBody(host, path, structuredBody, mapSourceData); err != nil {
log.Warnf("failed to transform response body: %v", err)
return types.ActionContinue
}
body, err = constructBody(contentType, structuredBody)
if err != nil {
log.Warnf("failed to construct response body: %v", err)
return types.ActionContinue
}
if err = proxywasm.ReplaceHttpResponseBody(body); err != nil {
log.Warnf("failed to replace response body: %v", err)
return types.ActionContinue
}
return types.ActionContinue
}
func getHostAndPathFromHttpCtx(ctx wrapper.HttpContext) (host, path string, err error) {
host, ok := ctx.GetContext("host").(string)
if !ok {
return "", "", errGetRequestHost
}
path, ok = ctx.GetContext("path").(string)
if !ok {
return "", "", errGetRequestPath
}
return host, path, nil
}
func newTransformRule(rules []gjson.Result) (res []TransformRule, err error) {
for _, r := range rules {
var tRule TransformRule
tRule.operate = strings.ToLower(r.Get("operate").String())
if !isValidOperation(tRule.operate) {
errors.Wrapf(err, "invalid operate type %q", tRule.operate)
return
}
if tRule.operate == "map" {
mapSourceInJson := r.Get("mapSource")
if !mapSourceInJson.Exists() {
tRule.mapSource = "self"
} else {
tRule.mapSource = mapSourceInJson.String()
if !isValidMapSource(tRule.mapSource) {
errors.Wrapf(err, "invalid map source %q", tRule.mapSource)
return
}
}
}
for _, h := range r.Get("headers").Array() {
tRule.headers = append(tRule.headers, constructParam(h, tRule.operate, ""))
}
for _, q := range r.Get("querys").Array() {
tRule.querys = append(tRule.querys, constructParam(q, tRule.operate, ""))
}
for _, b := range r.Get("body").Array() {
valueType := strings.ToLower(b.Get("value_type").String())
if valueType == "" { // default
valueType = "string"
}
if !isValidJsonType(valueType) {
errors.Wrapf(err, "invalid body params type %q", valueType)
return
}
tRule.body = append(tRule.body, constructParam(b, tRule.operate, valueType))
}
res = append(res, tRule)
}
return
}
type Transformer interface {
TransformHeaders(host, path string, hs map[string][]string, mapSourceData map[string]MapSourceData) error
TransformQuerys(host, path string, qs map[string][]string, mapSourceData map[string]MapSourceData) error
TransformBody(host, path string, body interface{}, mapSourceData map[string]MapSourceData) error
IsHeaderChange() bool
IsQueryChange() bool
IsBodyChange() bool
NeedBodyMapSource() bool
}
var _ Transformer = (*requestTransformer)(nil)
var _ Transformer = (*responseTransformer)(nil)
type requestTransformer struct {
headerHandler *kvHandler
queryHandler *kvHandler
bodyHandler *requestBodyHandler
isHeaderChange bool
isQueryChange bool
isBodyChange bool
needBodyMapSource bool
}
func newRequestTransformer(config *TransformerConfig) (Transformer, error) {
headerKvtGroup, isHeaderChange, _, err := newKvtGroup(config.reqRules, "headers")
if err != nil {
return nil, errors.Wrap(err, "failed to new kvt group for headers")
}
queryKvtGroup, isQueryChange, _, err := newKvtGroup(config.reqRules, "querys")
if err != nil {
return nil, errors.Wrap(err, "failed to new kvt group for querys")
}
bodyKvtGroup, isBodyChange, _, err := newKvtGroup(config.reqRules, "body")
if err != nil {
return nil, errors.Wrap(err, "failed to new kvt group for body")
}
bodyMapSource := bodyMapSourceInRule(config.reqRules)
return &requestTransformer{
headerHandler: &kvHandler{headerKvtGroup},
queryHandler: &kvHandler{queryKvtGroup},
bodyHandler: &requestBodyHandler{
formDataHandler: &kvHandler{bodyKvtGroup},
jsonHandler: &jsonHandler{bodyKvtGroup},
},
isHeaderChange: isHeaderChange,
isQueryChange: isQueryChange,
isBodyChange: isBodyChange,
needBodyMapSource: bodyMapSource,
}, nil
}
func (t requestTransformer) TransformHeaders(host, path string, hs map[string][]string, mapSourceData map[string]MapSourceData) error {
return t.headerHandler.handle(host, path, hs, mapSourceData)
}
func (t requestTransformer) TransformQuerys(host, path string, qs map[string][]string, mapSourceData map[string]MapSourceData) error {
return t.queryHandler.handle(host, path, qs, mapSourceData)
}
func (t requestTransformer) TransformBody(host, path string, body interface{}, mapSourceData map[string]MapSourceData) error {
switch body.(type) {
case map[string][]string:
return t.bodyHandler.formDataHandler.handle(host, path, body.(map[string][]string), mapSourceData)
case map[string]interface{}:
m := body.(map[string]interface{})
newBody, err := t.bodyHandler.handle(host, path, m["body"].([]byte), mapSourceData)
if err != nil {
return err
}
m["body"] = newBody
default:
return errBodyType
}
return nil
}
func (t requestTransformer) IsHeaderChange() bool { return t.isHeaderChange }
func (t requestTransformer) IsQueryChange() bool { return t.isQueryChange }
func (t requestTransformer) IsBodyChange() bool { return t.isBodyChange }
func (t requestTransformer) NeedBodyMapSource() bool { return t.needBodyMapSource }
type responseTransformer struct {
headerHandler *kvHandler
bodyHandler *responseBodyHandler
isHeaderChange bool
isBodyChange bool
needBodyMapSource bool
}
func newResponseTransformer(config *TransformerConfig) (Transformer, error) {
headerKvtGroup, isHeaderChange, _, err := newKvtGroup(config.respRules, "headers")
if err != nil {
return nil, errors.Wrap(err, "failed to new kvt group for headers")
}
bodyKvtGroup, isBodyChange, _, err := newKvtGroup(config.respRules, "body")
if err != nil {
return nil, errors.Wrap(err, "failed to new kvt group for body")
}
bodyMapSource := bodyMapSourceInRule(config.respRules)
return &responseTransformer{
headerHandler: &kvHandler{headerKvtGroup},
bodyHandler: &responseBodyHandler{&jsonHandler{bodyKvtGroup}},
isHeaderChange: isHeaderChange,
isBodyChange: isBodyChange,
needBodyMapSource: bodyMapSource,
}, nil
}
func (t responseTransformer) TransformHeaders(host, path string, hs map[string][]string, mapSourceData map[string]MapSourceData) error {
return t.headerHandler.handle(host, path, hs, mapSourceData)
}
func (t responseTransformer) TransformQuerys(host, path string, qs map[string][]string, mapSourceData map[string]MapSourceData) error {
// the response does not need to transform the query params, always returns nil
return nil
}
func (t responseTransformer) TransformBody(host, path string, body interface{}, mapSourceData map[string]MapSourceData) error {
switch body.(type) {
case map[string]interface{}:
m := body.(map[string]interface{})
newBody, err := t.bodyHandler.handle(host, path, m["body"].([]byte), mapSourceData)
if err != nil {
return err
}
m["body"] = newBody
default:
return errBodyType
}
return nil
}
func (t responseTransformer) IsHeaderChange() bool { return t.isHeaderChange }
func (t responseTransformer) IsQueryChange() bool { return false } // the response does not need to transform the query params, always returns false
func (t responseTransformer) IsBodyChange() bool { return t.isBodyChange }
func (t responseTransformer) NeedBodyMapSource() bool { return t.needBodyMapSource }
type requestBodyHandler struct {
formDataHandler *kvHandler
*jsonHandler
}
type responseBodyHandler struct {
*jsonHandler
}
type kvHandler struct {
kvtOps []kvtOperation
}
type jsonHandler struct {
kvtOps []kvtOperation
}
func (h kvHandler) handle(host, path string, kvs map[string][]string, mapSourceData map[string]MapSourceData) error {
// arbitary order. for example: remove → rename → replace → add → append → map → dedupe
for _, kvtOp := range h.kvtOps {
switch kvtOp.kvtOpType {
case RemoveK:
// remove
for _, remove := range kvtOp.removeKvtGroup {
delete(kvs, remove.key)
}
case RenameK:
// rename: 若指定 oldKey 不存在则无操作;否则将 oldKey 的值追加给 newKey并删除 oldKey:value
for _, rename := range kvtOp.renameKvtGroup {
oldKey, newKey := rename.oldKey, rename.newKey
if ovs, ok := kvs[oldKey]; ok {
kvs[newKey] = append(kvs[newKey], ovs...)
delete(kvs, oldKey)
}
}
case ReplaceK:
// replace: 若指定 key 不存在,相当于添加操作;否则替换 value 为 newValue
for _, replace := range kvtOp.replaceKvtGroup {
key, newValue := replace.key, replace.newValue
if replace.reg != nil {
newValue = replace.reg.matchAndReplace(newValue, host, path)
}
kvs[key] = []string{newValue}
}
case AddK:
// add: 若指定 key 存在则无操作;否则添加 key:value
for _, add := range kvtOp.addKvtGroup {
key, value := add.key, add.value
if _, ok := kvs[key]; ok {
continue
}
if add.reg != nil {
value = add.reg.matchAndReplace(value, host, path)
}
kvs[key] = []string{value}
}
case AppendK:
// append: 若指定 key 存在,则追加同名 kv否则相当于添加操作
for _, append_ := range kvtOp.appendKvtGroup {
key, appendValue := append_.key, append_.appendValue
if append_.reg != nil {
appendValue = append_.reg.matchAndReplace(appendValue, host, path)
}
kvs[key] = append(kvs[key], appendValue)
}
case MapK:
// map: 若指定 fromKey 不存在则无操作;否则将 fromKey 的值映射给 toKey 的值
for _, map_ := range kvtOp.mapKvtGroup {
fromKey, toKey := map_.fromKey, map_.toKey
if kvtOp.mapSource == "headers" {
fromKey = strings.ToLower(fromKey)
}
source, exist := mapSourceData[kvtOp.mapSource]
if !exist {
proxywasm.LogWarnf("map key failed, source:%s not exists", kvtOp.mapSource)
continue
}
proxywasm.LogDebugf("search key:%s in source:%s", fromKey, kvtOp.mapSource)
if fromValue, ok := source.search(fromKey); ok {
switch source.mapSourceType {
case "headers", "querys", "bodyKv":
kvs[toKey] = fromValue.([]string)
proxywasm.LogDebugf("map key:%s to key:%s success, value is: %v", fromKey, toKey, fromValue)
case "bodyJson":
if valueJson, ok := fromValue.(gjson.Result); ok {
valueStr := valueJson.String()
if valueStr != "" {
kvs[toKey] = []string{valueStr}
proxywasm.LogDebugf("map key:%s to key:%s success, values is:%s", fromKey, toKey, valueStr)
}
}
}
}
}
case DedupeK:
// dedupe: 根据 strategy 去重RETAIN_UNIQUE 保留所有唯一值RETAIN_LAST 保留最后一个值RETAIN_FIRST 保留第一个值 (default)
for _, dedupe := range kvtOp.dedupeKvtGroup {
key, strategy := dedupe.key, dedupe.strategy
switch strings.ToUpper(strategy) {
case "RETAIN_UNIQUE":
uniSet, uniques := make(map[string]struct{}), make([]string, 0)
for _, v := range kvs[key] {
if _, ok := uniSet[v]; !ok {
uniSet[v] = struct{}{}
uniques = append(uniques, v)
}
}
kvs[key] = uniques
case "RETAIN_LAST":
if vs, ok := kvs[key]; ok && len(vs) >= 1 {
kvs[key] = vs[len(vs)-1:]
}
case "RETAIN_FIRST":
fallthrough
default:
if vs, ok := kvs[key]; ok && len(vs) >= 1 {
kvs[key] = vs[:1]
}
}
}
}
}
return nil
}
// only for body
func (h jsonHandler) handle(host, path string, oriData []byte, mapSourceData map[string]MapSourceData) (data []byte, err error) {
// arbitary order. for example: remove → rename → replace → add → append → map → dedupe
if !gjson.ValidBytes(oriData) {
return nil, errors.New("invalid json body")
}
data = oriData
for _, kvtOp := range h.kvtOps {
switch kvtOp.kvtOpType {
case RemoveK:
// remove
for _, remove := range kvtOp.removeKvtGroup {
if data, err = sjson.DeleteBytes(data, remove.key); err != nil {
return nil, errors.Wrap(err, errRemove.Error())
}
}
case RenameK:
// rename: 若指定 oldKey 不存在则无操作;否则将 oldKey 的值追加给 newKey并删除 oldKey:value
for _, rename := range kvtOp.renameKvtGroup {
oldKey, newKey := rename.oldKey, rename.newKey
value := gjson.GetBytes(data, oldKey)
if !value.Exists() {
continue
}
if data, err = sjson.SetBytes(data, newKey, value.Value()); err != nil {
return nil, errors.Wrap(err, errRename.Error())
}
if data, err = sjson.DeleteBytes(data, oldKey); err != nil {
return nil, errors.Wrap(err, errRename.Error())
}
}
case ReplaceK:
// replace: 若指定 key 不存在,则相当于添加操作;否则替换 value 为 newValue
for _, replace := range kvtOp.replaceKvtGroup {
key, newValue, valueType := replace.key, replace.newValue, replace.typ
if valueType == "string" && replace.reg != nil {
newValue = replace.reg.matchAndReplace(newValue, host, path)
}
convertedNewValue, err := convertByJsonType(valueType, newValue)
if err != nil {
return nil, errors.Wrap(err, errReplace.Error())
}
if data, err = sjson.SetBytes(data, key, convertedNewValue); err != nil {
return nil, errors.Wrap(err, errReplace.Error())
}
}
case AddK:
// add: 若指定 key 存在则无操作;否则添加 key:value
for _, add := range kvtOp.addKvtGroup {
key, value, valueType := add.key, add.value, add.typ
if gjson.GetBytes(data, key).Exists() {
continue
}
if valueType == "string" && add.reg != nil {
value = add.reg.matchAndReplace(value, host, path)
}
convertedValue, err := convertByJsonType(valueType, value)
if err != nil {
return nil, errors.Wrap(err, errAdd.Error())
}
if data, err = sjson.SetBytes(data, key, convertedValue); err != nil {
return nil, errors.Wrap(err, errAdd.Error())
}
}
case AppendK:
// append: 若指定 key 存在,则追加同名 kv否则相当于添加操作
// 当原本的 value 为数组时,追加;当原本的 value 不为数组时,将原本的 value 和 appendValue 组成数组
for _, append_ := range kvtOp.appendKvtGroup {
key, appendValue, valueType := append_.key, append_.appendValue, append_.typ
if valueType == "string" && append_.reg != nil {
appendValue = append_.reg.matchAndReplace(appendValue, host, path)
}
convertedAppendValue, err := convertByJsonType(valueType, appendValue)
if err != nil {
return nil, errors.Wrapf(err, errAppend.Error())
}
oldValue := gjson.GetBytes(data, key)
if !oldValue.Exists() {
if data, err = sjson.SetBytes(data, key, convertedAppendValue); err != nil { // key: appendValue
return nil, errors.Wrap(err, errAppend.Error())
}
continue
}
// oldValue exists
if oldValue.IsArray() {
if len(oldValue.Array()) == 0 {
if data, err = sjson.SetBytes(data, key, []interface{}{convertedAppendValue}); err != nil { // key: [appendValue]
return nil, errors.Wrap(err, errAppend.Error())
}
continue
}
// len(oldValue.Array()) != 0
oldValues := make([]interface{}, 0, len(oldValue.Array())+1)
for _, val := range oldValue.Array() {
oldValues = append(oldValues, val.Value())
}
if data, err = sjson.SetBytes(data, key, append(oldValues, convertedAppendValue)); err != nil { // key: [oldValue..., appendValue]
return nil, errors.Wrap(err, errAppend.Error())
}
continue
}
// oldValue is not array
if data, err = sjson.SetBytes(data, key, []interface{}{oldValue.Value(), convertedAppendValue}); err != nil { // key: [oldValue, appendValue]
return nil, errors.Wrap(err, errAppend.Error())
}
}
case MapK:
// map: 若指定 fromKey 不存在则无操作;否则将 fromKey 的值映射给 toKey 的值
for _, map_ := range kvtOp.mapKvtGroup {
fromKey, toKey := map_.fromKey, map_.toKey
if kvtOp.mapSource == "headers" {
fromKey = strings.ToLower(fromKey)
}
source, exist := mapSourceData[kvtOp.mapSource]
if !exist {
proxywasm.LogWarnf("map key failed, source:%s not exists", kvtOp.mapSource)
continue
}
proxywasm.LogDebugf("search key:%s in source:%s", fromKey, kvtOp.mapSource)
if fromValue, ok := source.search(fromKey); ok {
switch source.mapSourceType {
case "headers", "querys", "bodyKv":
if data, err = sjson.SetBytes(data, toKey, fromValue); err != nil {
return nil, errors.Wrap(err, errMap.Error())
}
proxywasm.LogDebugf("map key:%s to key:%s success, value is: %v", fromKey, toKey, fromValue)
case "bodyJson":
if valueJson, ok := fromValue.(gjson.Result); ok {
if data, err = sjson.SetBytes(data, toKey, valueJson.Value()); err != nil {
return nil, errors.Wrap(err, errMap.Error())
}
proxywasm.LogDebugf("map key:%s to key:%s success, value is: %v", fromKey, toKey, fromValue)
}
}
}
}
case DedupeK:
// dedupe: 根据 strategy 去重RETAIN_UNIQUE 保留所有唯一值RETAIN_LAST 保留最后一个值RETAIN_FIRST 保留第一个值 (default)
for _, dedupe := range kvtOp.dedupeKvtGroup {
key, strategy := dedupe.key, dedupe.strategy
value := gjson.GetBytes(data, key)
if !value.Exists() || !value.IsArray() {
continue
}
// value is array
values := value.Array()
if len(values) == 0 {
continue
}
var dedupedVal interface{}
switch strings.ToUpper(strategy) {
case "RETAIN_UNIQUE":
uniSet, uniques := make(map[string]struct{}), make([]interface{}, 0)
for _, v := range values {
vstr := v.String()
if _, ok := uniSet[vstr]; !ok {
uniSet[vstr] = struct{}{}
uniques = append(uniques, v.Value())
}
}
if len(uniques) == 1 {
dedupedVal = uniques[0] // key: uniques[0]
} else if len(uniques) > 1 {
dedupedVal = uniques // key: [uniques...]
}
case "RETAIN_LAST":
dedupedVal = values[len(values)-1].Value() // key: last
case "RETAIN_FIRST":
fallthrough
default:
dedupedVal = values[0].Value() // key: first
}
if dedupedVal == nil {
continue
}
if data, err = sjson.SetBytes(data, key, dedupedVal); err != nil {
return nil, errors.Wrap(err, errDedupe.Error())
}
}
}
}
return data, nil
}
type removeKvt struct {
key string
}
type renameKvt struct {
oldKey string
newKey string
typ string
}
type replaceKvt struct {
key string
newValue string
typ string
*reg
}
type addKvt struct {
key string
value string
typ string
*reg
}
type appendKvt struct {
key string
appendValue string
typ string
*reg
}
type mapKvt struct {
fromKey string
toKey string
}
type dedupeKvt struct {
key string
strategy string
}
type KvtOpType int
const (
RemoveK KvtOpType = iota
RenameK
ReplaceK
AddK
AppendK
MapK
DedupeK
)
type kvtOperation struct {
kvtOpType KvtOpType
removeKvtGroup []removeKvt
renameKvtGroup []renameKvt
replaceKvtGroup []replaceKvt
addKvtGroup []addKvt
appendKvtGroup []appendKvt
mapKvtGroup []mapKvt
dedupeKvtGroup []dedupeKvt
mapSource string
}
func newKvtGroup(rules []TransformRule, typ string) (g []kvtOperation, isChange bool, withMapKvt bool, err error) {
g = []kvtOperation{}
for _, r := range rules {
var prams []Param
switch typ {
case "headers":
prams = r.headers
case "querys":
prams = r.querys
case "body":
prams = r.body
}
var kvtOp kvtOperation
switch r.operate {
case "remove":
kvtOp.kvtOpType = RemoveK
case "rename":
kvtOp.kvtOpType = RenameK
case "map":
kvtOp.kvtOpType = MapK
case "replace":
kvtOp.kvtOpType = ReplaceK
case "dedupe":
kvtOp.kvtOpType = DedupeK
case "add":
kvtOp.kvtOpType = AddK
case "append":
kvtOp.kvtOpType = AppendK
default:
return nil, false, false, errors.Wrap(err, "invalid operation type")
}
for _, p := range prams {
switch r.operate {
case "remove":
key := p.removeParam.key
if typ == "headers" {
key = strings.ToLower(key)
}
kvtOp.removeKvtGroup = append(kvtOp.removeKvtGroup, removeKvt{key})
case "rename":
if typ == "headers" {
p.renameParam.oldKey = strings.ToLower(p.renameParam.oldKey)
p.renameParam.newKey = strings.ToLower(p.renameParam.newKey)
}
kvtOp.renameKvtGroup = append(kvtOp.renameKvtGroup, renameKvt{p.renameParam.oldKey, p.renameParam.newKey, p.valueType})
case "map":
if typ == "headers" {
p.mapParam.toKey = strings.ToLower(p.mapParam.toKey)
}
kvtOp.mapSource = r.mapSource
if kvtOp.mapSource == "self" {
kvtOp.mapSource = typ
r.mapSource = typ
}
if kvtOp.mapSource == "headers" {
p.mapParam.fromKey = strings.ToLower(p.mapParam.fromKey)
}
kvtOp.mapKvtGroup = append(kvtOp.mapKvtGroup, mapKvt{p.mapParam.fromKey, p.mapParam.toKey})
case "dedupe":
if typ == "headers" {
p.dedupeParam.key = strings.ToLower(p.dedupeParam.key)
}
kvtOp.dedupeKvtGroup = append(kvtOp.dedupeKvtGroup, dedupeKvt{p.dedupeParam.key, p.dedupeParam.strategy})
case "replace":
if typ == "headers" {
p.replaceParam.key = strings.ToLower(p.replaceParam.key)
}
var rg *reg
if p.hostPattern != "" || p.pathPattern != "" {
rg, err = newReg(p.hostPattern, p.pathPattern)
if err != nil {
return nil, false, false, errors.Wrap(err, "failed to new reg")
}
}
kvtOp.replaceKvtGroup = append(kvtOp.replaceKvtGroup, replaceKvt{p.replaceParam.key, p.replaceParam.newValue, p.valueType, rg})
case "add":
if typ == "headers" {
p.addParam.key = strings.ToLower(p.addParam.key)
}
var rg *reg
if p.hostPattern != "" || p.pathPattern != "" {
rg, err = newReg(p.hostPattern, p.pathPattern)
if err != nil {
return nil, false, false, errors.Wrap(err, "failed to new reg")
}
}
kvtOp.addKvtGroup = append(kvtOp.addKvtGroup, addKvt{p.addParam.key, p.addParam.value, p.valueType, rg})
case "append":
if typ == "headers" {
p.appendParam.key = strings.ToLower(p.appendParam.key)
}
var rg *reg
if p.hostPattern != "" || p.pathPattern != "" {
rg, err = newReg(p.hostPattern, p.pathPattern)
if err != nil {
return nil, false, false, errors.Wrap(err, "failed to new reg")
}
}
kvtOp.appendKvtGroup = append(kvtOp.appendKvtGroup, appendKvt{p.appendParam.key, p.appendParam.appendValue, p.valueType, rg})
}
}
isChange = isChange || len(kvtOp.removeKvtGroup) != 0 ||
len(kvtOp.renameKvtGroup) != 0 || len(kvtOp.replaceKvtGroup) != 0 ||
len(kvtOp.addKvtGroup) != 0 || len(kvtOp.appendKvtGroup) != 0 ||
len(kvtOp.mapKvtGroup) != 0 || len(kvtOp.dedupeKvtGroup) != 0
withMapKvt = withMapKvt || len(kvtOp.mapKvtGroup) != 0
g = append(g, kvtOp)
}
return g, isChange, withMapKvt, nil
}
type MapSourceData struct {
mapSourceType string
kvs map[string][]string // headers or querys or body in kvs
json []byte // body in json
}
func (msdata MapSourceData) search(fromKey string) (interface{}, bool) {
switch msdata.mapSourceType {
case "headers", "querys", "bodyKv":
fromValue, ok := msdata.kvs[fromKey]
return fromValue, ok
case "bodyJson":
fromValue := gjson.GetBytes(msdata.json, fromKey)
if !fromValue.Exists() {
return nil, false
}
return fromValue, true
default:
return "", false
}
}
func bodyMapSourceInRule(rules []TransformRule) bool {
for _, r := range rules {
if r.operate == "map" && r.mapSource == "body" {
return true
}
}
return false
}
type kvtReg struct {
kvt
*reg
}
type kvt struct {
key string
value string
typ string
}
type reg struct {
hostReg *regexp.Regexp
pathReg *regexp.Regexp
}
// you can only choose one between host and path
func newReg(hostPatten, pathPatten string) (r *reg, err error) {
r = &reg{}
if hostPatten != "" {
r.hostReg, err = regexp.Compile(hostPatten)
return
}
if pathPatten != "" {
r.pathReg, err = regexp.Compile(pathPatten)
return
}
return
}
func (r reg) matchAndReplace(value, host, path string) string {
if r.hostReg != nil && r.hostReg.MatchString(host) {
return r.hostReg.ReplaceAllString(host, value)
}
if r.pathReg != nil && r.pathReg.MatchString(path) {
return r.pathReg.ReplaceAllString(path, value)
}
return value
}