api/API.go

592 lines
14 KiB
Go
Raw Normal View History

2025-09-11 23:42:22 +08:00
package plugin
import (
"bufio"
"bytes"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
xml2json "github.com/basgys/goxml2json"
"github.com/ssgo/httpclient"
"github.com/ssgo/u"
)
var Signers = map[string]func(req *Request, cfg *SignerConfig) error{
// 基础认证
"basic": makeBasicAuthSign,
"bearer": makeBearerSign,
"jwt": makeJWTSign,
// "tc3": makeTC3Sign,
// "cos": makeCOSSign,
// "hmac": makeHmacSign,
}
var signersLock = sync.RWMutex{}
func GetSigner(name string) func(req *Request, cfg *SignerConfig) error {
if name == "" {
return nil
}
signersLock.RLock()
defer signersLock.RUnlock()
return Signers[name]
}
func RegisterSigner(name string, f func(req *Request, cfg *SignerConfig) error) {
signersLock.Lock()
defer signersLock.Unlock()
Signers[name] = f
}
type SignerConfig struct {
data map[string]any
}
var signerConfig = map[string]*map[string]any{}
var confLock = sync.RWMutex{}
func GetSignerConfig(signer string) *SignerConfig {
confLock.RLock()
cfg := signerConfig[signer]
confLock.RUnlock()
if cfg == nil {
cfg = &map[string]any{}
}
return &SignerConfig{data: *cfg}
}
// func (cfg *SignerConfig) Signer() string {
// return u.String(cfg.data["signer"])
// }
func (cfg *SignerConfig) Get(k string, defaultValue any) any {
if v, ok := cfg.data[k]; ok {
return v
}
return defaultValue
}
func (cfg *SignerConfig) Set(k string, v any) {
cfg.data[k] = v
}
func (cfg *SignerConfig) String(k string, defaultValue string) string {
return u.String(cfg.Get(k, defaultValue))
}
func (cfg *SignerConfig) Int(k string, defaultValue int64) int64 {
return u.Int64(cfg.Get(k, defaultValue))
}
func (cfg *SignerConfig) Float(k string, defaultValue float64) float64 {
return u.Float64(cfg.Get(k, defaultValue))
}
func (cfg *SignerConfig) Bool(k string, defaultValue bool) bool {
return u.Bool(cfg.Get(k, defaultValue))
}
func (cfg *SignerConfig) To(k string, to any) {
from := cfg.Get(k, nil)
if from != nil {
u.Convert(from, to)
}
}
func (cfg *SignerConfig) Map(k string) map[string]any {
m := map[string]any{}
cfg.To(k, &m)
return m
}
func getConfigValue(cfg map[string]any, k string) string {
a := u.SplitWithoutNoneN(k, ".", 2)
if len(a) > 1 {
m := map[string]any{}
u.Convert(cfg[a[0]], &m)
return u.String(m[a[1]])
} else if len(a) == 1 {
return u.String(cfg[a[0]])
}
return ""
}
// func (cfg *SignerConfig) Value(k string) string {
// a := u.SplitWithoutNoneN(k, ".", 2)
// if len(a) > 1 {
// m := cfg.Map(a[0])
// return u.String(m[a[1]])
// } else if len(a) == 1 {
// return cfg.String(a[0], "")
// }
// return ""
// }
var httpclients = map[uint]*httpclient.ClientPool{}
var httpclientsLock = sync.RWMutex{}
func GetHttpClient(timeout uint) *httpclient.ClientPool {
httpclientsLock.RLock()
c := httpclients[timeout]
httpclientsLock.RUnlock()
if c != nil {
return c
}
c = httpclient.GetClient(time.Duration(timeout) * time.Millisecond)
httpclientsLock.Lock()
httpclients[timeout] = c
httpclientsLock.Unlock()
return c
}
var RequestType = struct {
Json string // JSON请求体默认
Form string // 表单请求体
Multi string // 多部分请求体
Text string // 文本请求体
Binary string // 二进制请求体
}{
Json: "json",
Form: "form",
Multi: "multi",
Text: "text",
Binary: "binary",
}
var ResponseType = struct {
Json string // JSON响应体默认
Text string // 文本响应体
Binary string // 二进制响应体
}{
Json: "json",
Text: "text",
Binary: "binary",
}
type Request struct {
Url string
Method string
Config map[string]string // 配置信息,可以被 {{}} 引用
Headers map[string]string // 请求头
Query map[string]string // URL中的参数
Data map[string]any // 用JSON发送的请求主体对象
Form map[string]string // 用表单发送的主体内容
File map[string]any // 用multi-form发送的文件对象
Text string // 纯文本格式的主体内容
Binary []byte // 二进制格式的主体内容
RequestType string
ResponseType string
Callback func(data any)
Timeout uint // 单位毫秒默认0表示不超时
FinalUrl string // 提前处理好的完整URL
FinalHost string // 主机名
FinalPath string // URL路径
FinalQuery string // 查询字符串
FinalBody []byte // 提前处理好的请求主体
}
func (req *Request) SetUrl(v string) {
req.Url = v
}
func (req *Request) GetHost() string {
return req.FinalHost
}
func (req *Request) SetMethod(v string) {
req.Method = v
}
func (req *Request) SetQuery(k string, v string) {
req.Query[k] = v
}
func (req *Request) SetForm(k string, v string) {
req.Form[k] = v
}
func (req *Request) SetText(v string) {
req.Text = v
}
func (req *Request) SetBinary(v []byte) {
req.Binary = v
}
func (req *Request) SetFile(k string, v any) {
req.File[k] = v
}
func (req *Request) SetData(k string, v any) {
req.Data[k] = v
}
func (req *Request) SetHeader(key, value string) {
req.Headers[key] = value
}
func (req *Request) MakeQuery() {
if urlInfo, err := url.Parse(req.Url); err == nil {
q := url.Values{}
for k, v := range req.Query {
q.Set(k, v)
}
urlInfo.RawQuery = q.Encode()
req.FinalUrl = urlInfo.String()
req.FinalHost = urlInfo.Host
req.FinalPath = urlInfo.Path
req.FinalQuery = urlInfo.RawQuery
if req.FinalPath == "" {
req.FinalPath = "/"
}
} else {
req.FinalUrl = req.Url
req.FinalHost = ""
req.FinalPath = "/"
req.FinalQuery = ""
}
// fmt.Println(">>>a", req.Url, req.FinalUrl)
}
func (req *Request) makeConfig(str string) string {
if strings.Contains(str, "{{config.") {
if m := configMatcher.FindAllStringSubmatch(str, 100); m != nil {
for _, m1 := range m {
if v1, ok := req.Config[m1[1]]; ok && v1 != "" {
str = strings.ReplaceAll(str, m1[0], v1)
}
}
}
}
if strings.Contains(str, "{{/") {
if m := fnMatcher.FindAllStringSubmatch(str, 100); m != nil {
for _, m1 := range m {
fn := m1[1]
args := m1[2]
switch fn {
case "date":
str = strings.ReplaceAll(str, m1[0], time.Now().Format(args))
case "timestamp":
str = strings.ReplaceAll(str, m1[0], u.String(time.Now().Unix()))
case "timestampMilli":
str = strings.ReplaceAll(str, m1[0], u.String(time.Now().UnixMilli()))
}
}
}
}
return str
}
func (req *Request) MakeBody() {
switch req.RequestType {
case RequestType.Json:
data := map[string]any{}
if req.Data != nil {
for k, v := range req.Data {
if str, ok := v.(string); ok {
data[k] = req.makeConfig(str)
} else {
data[k] = v
}
}
}
req.FinalBody = u.JsonBytesP(data)
case RequestType.Form:
formData := url.Values{}
for k, v := range req.Form {
formData.Set(k, req.makeConfig(v))
}
req.FinalBody = []byte(formData.Encode())
case RequestType.Multi:
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if len(req.Form) > 0 {
for k, v := range req.Form {
writer.WriteField(k, req.makeConfig(v))
}
}
if len(req.File) > 0 {
for k, v := range req.File {
if filename, ok := v.(string); ok && u.FileExists(filename) {
if fp, err := os.Open(filename); err == nil {
if part, err := writer.CreateFormFile(k, filepath.Base(filename)); err == nil {
io.Copy(part, fp)
}
_ = fp.Close()
}
} else if dataUrl, ok := v.(string); ok && strings.HasPrefix(dataUrl, "data:") && strings.ContainsRune(dataUrl, ',') {
parts := strings.SplitN(dataUrl, ",", 2)
metaPart := strings.TrimSuffix(parts[0], "data:")
metaParts := strings.SplitN(metaPart, ";", 2)
mimeType := "application/octet-stream"
if metaParts[0] != "" {
mimeType = metaParts[0]
}
var data []byte
if len(metaParts) > 1 && metaParts[1] == "base64" {
data = u.UnBase64(parts[1])
} else {
data = []byte(parts[1])
}
h := make(textproto.MIMEHeader)
filename := k
if exts, err := mime.ExtensionsByType(mimeType); err == nil && len(exts) > 0 {
filename = k + exts[0]
}
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, filename))
h.Set("Content-Type", mimeType)
if part, err := writer.CreatePart(h); err == nil {
part.Write(data)
}
} else {
h := make(textproto.MIMEHeader)
buf := u.Bytes(v)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, k))
mt := mime.TypeByExtension(filepath.Ext(k))
if mt == "" {
mt = "application/octet-stream"
}
h.Set("Content-Type", mt)
if part, err := writer.CreatePart(h); err == nil {
part.Write(buf)
}
}
}
}
writer.Close()
req.FinalBody = body.Bytes()
case RequestType.Binary:
req.FinalBody = req.Binary
default:
req.FinalBody = []byte(req.makeConfig(req.Text))
}
}
func MakeSign(cfg *SignerConfig, req *Request) error {
if req.RequestType == "" {
if req.Data != nil {
req.RequestType = RequestType.Json
} else if req.Form != nil {
req.RequestType = RequestType.Form
} else if req.File != nil {
req.RequestType = RequestType.Multi
} else if req.Binary != nil {
req.RequestType = RequestType.Binary
} else {
req.RequestType = RequestType.Text
}
}
if req.Method == "" {
req.Method = "POST"
}
req.Method = strings.ToUpper(req.Method)
if req.Headers == nil {
req.Headers = make(map[string]string)
}
if req.Query == nil {
req.Query = make(map[string]string)
}
// 自动将URL中的参数放入req.Query
if strings.ContainsRune(req.Url, '?') {
if urlInfo, err := url.Parse(req.Url); err == nil {
for k, v := range urlInfo.Query() {
if len(v) > 0 {
req.Query[k] = v[0]
}
}
urlInfo.RawQuery = ""
req.Url = urlInfo.String()
}
}
if (req.Method == "POST" || req.Method == "PUT") && req.Headers["Content-Type"] == "" {
switch req.RequestType {
case RequestType.Json:
req.Headers["Content-Type"] = "application/json"
case RequestType.Form:
req.Headers["Content-Type"] = "application/x-www-form-urlencoded"
case RequestType.Multi:
req.Headers["Content-Type"] = "multipart/form-data"
case RequestType.Binary:
req.Headers["Content-Type"] = "application/octet-stream"
default:
req.Headers["Content-Type"] = "text/plain"
}
}
if cfg == nil {
cfg = &SignerConfig{}
}
// 从signer中设置header
for k, v := range cfg.Map("headers") {
req.Headers[k] = req.makeConfig(u.String(v))
// fmt.Println(u.BCyan(k), req.Headers[k])
}
// 从signer中设置query
for k, v := range cfg.Map("query") {
req.Query[k] = req.makeConfig(u.String(v))
}
// 从signer中设置data
for k, v := range cfg.Map("data") {
req.Data[k] = v
}
// 从signer中设置form
for k, v := range cfg.Map("form") {
req.Form[k] = u.String(v)
}
req.MakeQuery()
req.MakeBody()
if signer := GetSigner(u.String(cfg.data["signer"])); signer != nil {
// fmt.Println(111, req.Method, 111)
if err := signer(req, cfg); err != nil {
// fmt.Println(333, err)
return err
// } else {
// fmt.Println(333, 1)
}
}
// 拼接生成完整的URL
return nil
}
func Get(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
req.Method = "GET"
return Do(cfg, req)
}
func Post(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
req.Method = "POST"
return Do(cfg, req)
}
func Put(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
req.Method = "PUT"
return Do(cfg, req)
}
func Delete(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
req.Method = "DELETE"
return Do(cfg, req)
}
func Head(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
req.Method = "HEAD"
return Do(cfg, req)
}
func Options(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
req.Method = "OPTIONS"
return Do(cfg, req)
}
func Do(cfg *SignerConfig, req *Request) (any, *http.Response, error) {
err := MakeSign(cfg, req)
if err != nil {
return nil, nil, err
}
// fmt.Println(u.BMagenta(u.JsonP(req.Headers)))
// return nil, nil, err
headers := make([]string, 0)
for k, v := range req.Headers {
headers = append(headers, k, v)
}
c := GetHttpClient(req.Timeout)
// c.Debug = true
// fmt.Println(req.Url, req.finalUrl, u.BMagenta(string(req.finalBody)), "===")
// fmt.Println(u.BCyan(u.JsonP(req.Headers)), "===")
var r *httpclient.Result
var lastData any
if req.Callback != nil {
r = c.ManualDo(req.Method, req.FinalUrl, req.FinalBody, headers...)
if r.Error != nil {
return nil, r.Response, r.Error
}
respType := req.ResponseType
if respType == "" {
respType = strings.Split(r.Response.Header.Get("Content-Type"), ";")[0]
}
if r.Response.Body != nil {
func() {
reader := bufio.NewScanner(r.Response.Body)
defer r.Response.Body.Close()
for reader.Scan() {
str := strings.TrimSpace(reader.Text())
if str == "" {
continue
}
switch respType {
case "text/event-stream":
if strings.HasPrefix(str, "data:") {
str = strings.TrimPrefix(str, "data:")
str = strings.TrimSpace(str)
}
var d any
if str == "[DONE]" {
break
}
d = u.UnJson(str, nil)
req.Callback(d)
if d != nil {
lastData = d
}
case "application/json":
req.Callback(u.UnJson(str, nil))
case "application/xml":
if jData, err := xml2json.Convert(strings.NewReader(str)); err == nil {
req.Callback(u.UnJsonBytes(jData.Bytes(), nil))
}
req.Callback(str)
default:
req.Callback(str)
}
}
}()
}
return lastData, r.Response, nil
} else {
r = c.Do(req.Method, req.FinalUrl, req.FinalBody, headers...)
if r.Error != nil {
return nil, r.Response, r.Error
}
respType := req.ResponseType
if respType == "" {
respType = strings.Split(r.Response.Header.Get("Content-Type"), ";")[0]
}
switch respType {
case "application/json":
return u.UnJsonBytes(r.Bytes(), nil), r.Response, nil
case "application/xml":
if jData, err := xml2json.Convert(strings.NewReader(string(r.Bytes()))); err == nil {
return u.UnJsonBytes(jData.Bytes(), nil), r.Response, nil
}
return string(r.Bytes()), r.Response, nil
case "application/octet-stream":
return r.Bytes(), r.Response, nil
default:
return r.String(), r.Response, nil
}
}
}