feat: Add request and response transformer wasm plugin (#549)

This commit is contained in:
WeixinX
2023-10-07 15:47:20 +08:00
committed by GitHub
parent be8563765e
commit 1ded5322a5
12 changed files with 2402 additions and 4 deletions

View File

@@ -0,0 +1,258 @@
// 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 (
"bytes"
"encoding/json"
"io"
"mime"
"mime/multipart"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/tidwall/pretty"
)
const (
ContentTypeApplicationJson = "application/json"
ContentTypeFormUrlencoded = "application/x-www-form-urlencoded"
ContentTypeMultipartForm = "multipart/form-data"
)
var (
errGetRequestHost = errors.New("failed to get request host")
errGetRequestPath = errors.New("failed to get request path")
errEmptyBody = errors.New("body is empty")
errBodyType = errors.New("unsupported body type")
errGetContentType = errors.New("failed to get content-type from http context")
errRemove = errors.New("failed to remove")
errRename = errors.New("failed to rename")
errReplace = errors.New("failed to replace")
errAdd = errors.New("failed to add")
errAppend = errors.New("failed to append")
errMap = errors.New("failed to map")
errDedupe = errors.New("failed to dedupe")
errContentTypeFmt = "unsupported content-type: %s"
)
func isValidOperation(op string) bool {
switch op {
case "remove", "rename", "replace", "add", "append", "map", "dedupe":
return true
default:
return false
}
}
func parseQueryByPath(path string) (map[string][]string, error) {
u, err := url.Parse(path)
if err != nil {
return nil, err
}
qs := make(map[string][]string)
for k, vs := range u.Query() {
qs[k] = vs
}
return qs, nil
}
func constructPath(path string, qs map[string][]string) (string, error) {
u, err := url.Parse(path)
if err != nil {
return path, err
}
query := url.Values{}
for k, vs := range qs {
for _, v := range vs {
query.Add(k, v)
}
}
u.RawQuery = query.Encode()
return u.String(), nil
}
// 返回值为 map[string]interface{} 或 map[string][]string使用时断言即可
func parseBody(contentType string, body []byte) (interface{}, error) {
if len(body) == 0 {
return nil, errEmptyBody
}
typ, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, err
}
switch typ {
case ContentTypeApplicationJson:
return map[string]interface{}{"body": body}, nil
case ContentTypeFormUrlencoded:
ret := make(map[string][]string)
kvs, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
}
for k, vs := range kvs {
ret[k] = vs
}
return ret, nil
case ContentTypeMultipartForm:
ret := make(map[string][]string)
mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
for {
p, err := mr.NextPart()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
formName := p.FormName()
fileName := p.FileName()
if formName == "" || fileName != "" {
continue
}
formValue, err := io.ReadAll(p)
if err != nil {
return nil, err
}
ret[formName] = append(ret[formName], string(formValue))
}
return ret, nil
default:
return nil, errors.Errorf(errContentTypeFmt, contentType)
}
}
func constructBody(contentType string, body interface{}) ([]byte, error) {
typ, params, err := mime.ParseMediaType(contentType)
if err != nil {
return nil, err
}
switch typ {
case ContentTypeApplicationJson:
bd, ok := body.(map[string]interface{})["body"].([]byte)
if !ok {
return nil, errBodyType
}
return pretty.Pretty(bd), nil
case ContentTypeFormUrlencoded:
bd, ok := body.(map[string][]string)
if !ok {
return nil, errBodyType
}
query := url.Values{}
for k, vs := range bd {
for _, v := range vs {
query.Add(k, v)
}
}
return []byte(query.Encode()), nil
case ContentTypeMultipartForm:
bd, ok := body.(map[string][]string)
if !ok {
return nil, errBodyType
}
buf := new(bytes.Buffer)
w := multipart.NewWriter(buf)
if err = w.SetBoundary(params["boundary"]); err != nil {
return nil, err
}
for k, vs := range bd {
for _, v := range vs {
if err = w.WriteField(k, v); err != nil {
return nil, err
}
}
}
if err = w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
default:
return nil, errors.Errorf(errContentTypeFmt, contentType)
}
}
func isValidRequestContentType(contentType string) bool {
typ, _, err := mime.ParseMediaType(contentType)
if err != nil {
return false
}
return typ == ContentTypeApplicationJson || typ == ContentTypeFormUrlencoded || typ == ContentTypeMultipartForm
}
func isValidResponseContentType(contentType string) bool {
typ, _, err := mime.ParseMediaType(contentType)
if err != nil {
return false
}
return typ == ContentTypeApplicationJson
}
func convertByJsonType(typ string, value string) (ret interface{}, err error) {
switch strings.ToLower(typ) {
case "object":
err = json.Unmarshal([]byte(value), &ret)
case "boolean":
ret, err = strconv.ParseBool(value)
case "number":
ret, err = strconv.ParseFloat(value, 64)
case "string":
fallthrough
default:
ret = value
}
return
}
func isValidJsonType(typ string) bool {
switch typ {
case "object", "boolean", "number", "string":
return true
default:
return false
}
}
// headers: [][2]string -> map[string][]string
func convertHeaders(hs [][2]string) map[string][]string {
ret := make(map[string][]string)
for _, h := range hs {
k, v := strings.ToLower(h[0]), h[1]
ret[k] = append(ret[k], v)
}
return ret
}
// headers: map[string][]string -> [][2]string
func reconvertHeaders(hs map[string][]string) [][2]string {
var ret [][2]string
for k, vs := range hs {
for _, v := range vs {
ret = append(ret, [2]string{k, v})
}
}
return ret
}