api/API.go
2025-09-11 23:42:22 +08:00

592 lines
14 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.

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