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