api/API.go
2025-12-01 00:30:00 +08:00

671 lines
16 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"
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
xml2json "github.com/basgys/goxml2json"
"github.com/ssgo/httpclient"
"github.com/ssgo/u"
)
type SignerCode struct {
Code string
Version uint64
}
var jsSigners = map[string]*SignerCode{}
var actionConfigs = map[string]*ApiConfig{}
var goSigners = map[string]func(req *Request, cfg *ApiConfig) error{
// 基础认证
"basic": makeBasicAuthSign,
"bearer": makeBearerSign,
"jwt": makeJWTSign,
// "tc3": makeTC3Sign,
// "cos": makeCOSSign,
// "hmac": makeHmacSign,
}
var actionsLock = sync.RWMutex{}
type ApiConfig struct {
data map[string]any
}
var apiConfigs = map[string]*map[string]any{}
var confLock = sync.RWMutex{}
func GetSigner(name string, vm *goja.Runtime) func(req *Request, cfg *ApiConfig) error {
if name == "" {
return nil
}
var jsi *SignerCode
actionsLock.RLock()
si := goSigners[name]
if si == nil {
jsi = jsSigners[name]
}
actionsLock.RUnlock()
// 获取当前VM的Signer
if jsi != nil {
if jsi.Version != u.Uint64(vm.GetData("__api_signer_version_"+name)) {
// 从代码更新
vm.SetData("__api_signer_version_"+name, jsi.Version)
if fnV, err := vm.RunString("(" + jsi.Code + ")"); err == nil {
if fn, ok := goja.AssertFunction(fnV); ok {
si = func(req *Request, cfg *ApiConfig) error {
_, err := fn(nil, vm.ToValue(gojs.ToMap(req)), vm.ToValue(gojs.ToMap(cfg)))
return err
}
vm.SetData("__signer_"+name, si)
}
}
} else {
// 从vm中直接获取
if si1, ok := vm.GetData("__signer_" + name).(func(req *Request, cfg *ApiConfig) error); ok {
si = si1
}
}
}
return si
}
func GetSignerVersion(name string) uint64 {
actionsLock.Lock()
defer actionsLock.Unlock()
si := jsSigners[name]
if si == nil {
return 0
}
return si.Version
}
// func RegisterSigner(name string, f func(req *Request, cfg *SignerConfig) error) {
func RegisterSigner(name string, fnCode string, fnVersion *uint64, vm *goja.Runtime) error {
version := u.Uint64(fnVersion)
if version == 0 {
version = 1
}
// fmt.Println(name, fnCode, version)
if fnV, err := vm.RunString("(" + fnCode + ")"); err == nil {
if _, ok := goja.AssertFunction(fnV); !ok {
return gojs.Err("signer must be a function")
}
} else {
return gojs.Err(err)
}
actionsLock.Lock()
defer actionsLock.Unlock()
jsSigners[name] = &SignerCode{
Code: fnCode,
Version: version,
}
return nil
}
func GetSignerConfig(signer string) *ApiConfig {
confLock.RLock()
cfg := apiConfigs[signer]
confLock.RUnlock()
if cfg == nil {
cfg = &map[string]any{}
}
return &ApiConfig{data: *cfg}
}
// func (cfg *SignerConfig) Signer() string {
// return u.String(cfg.data["signer"])
// }
func (cfg *ApiConfig) Get(k string, defaultValue any) any {
if v, ok := cfg.data[k]; ok {
return v
}
return defaultValue
}
func (cfg *ApiConfig) Set(k string, v any) {
cfg.data[k] = v
}
func (cfg *ApiConfig) String(k string, defaultValue string) string {
return u.String(cfg.Get(k, defaultValue))
}
func (cfg *ApiConfig) Int(k string, defaultValue int64) int64 {
return u.Int64(cfg.Get(k, defaultValue))
}
func (cfg *ApiConfig) Float(k string, defaultValue float64) float64 {
return u.Float64(cfg.Get(k, defaultValue))
}
func (cfg *ApiConfig) Bool(k string, defaultValue bool) bool {
return u.Bool(cfg.Get(k, defaultValue))
}
func (cfg *ApiConfig) To(k string, to any) {
from := cfg.Get(k, nil)
if from != nil {
u.Convert(from, to)
}
}
func (cfg *ApiConfig) 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 *ApiConfig, req *Request, vm *goja.Runtime) 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 = &ApiConfig{}
}
// 从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"]), vm); 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, vm *goja.Runtime) (any, *http.Response, error) {
// req.Method = "GET"
// return Do(cfg, req, vm)
// }
// func Post(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
// req.Method = "POST"
// return Do(cfg, req, vm)
// }
// func Put(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
// req.Method = "PUT"
// return Do(cfg, req, vm)
// }
// func Delete(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
// req.Method = "DELETE"
// return Do(cfg, req, vm)
// }
// func Head(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
// req.Method = "HEAD"
// return Do(cfg, req, vm)
// }
// func Options(cfg *SignerConfig, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
// req.Method = "OPTIONS"
// return Do(cfg, req, vm)
// }
func Do(action string, req *Request, vm *goja.Runtime) (any, *http.Response, error) {
var cfg *ApiConfig
if action != "" {
action1 := u.SplitTrimN(action, ".", 2)[0]
actionsLock.RLock()
cfg = actionConfigs[action]
if cfg == nil {
cfg = actionConfigs[action1]
}
actionsLock.RUnlock()
}
if cfg == nil {
cfg = &ApiConfig{data: map[string]any{}}
}
err := MakeSign(cfg, req, vm)
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
}
}
}