mirror of
https://github.com/alibaba/higress.git
synced 2026-02-24 20:50:51 +08:00
272 lines
6.3 KiB
Go
272 lines
6.3 KiB
Go
// 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"
|
||
"sort"
|
||
"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 isValidMapSource(source string) bool {
|
||
switch source {
|
||
case "headers", "querys", "body":
|
||
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})
|
||
}
|
||
}
|
||
sort.SliceStable(ret, func(i, j int) bool {
|
||
return ret[i][0] < ret[j][0]
|
||
})
|
||
return ret
|
||
}
|