diff --git a/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go b/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go index dbab4007..0af574da 100644 --- a/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go +++ b/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go @@ -50,7 +50,7 @@ func NewNotifier(config *NotifierConfig) (*Notifier, error) { client := resty.New(). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if config.Secret != "" { timestamp := fmt.Sprintf("%d", time.Now().UnixMilli()) diff --git a/pkg/sdk3rd/1panel/client.go b/pkg/sdk3rd/1panel/client.go index 5cff90d0..ded7e98e 100644 --- a/pkg/sdk3rd/1panel/client.go +++ b/pkg/sdk3rd/1panel/client.go @@ -1,9 +1,7 @@ package onepanel import ( - "crypto/md5" "crypto/tls" - "encoding/hex" "encoding/json" "fmt" "net/http" @@ -36,22 +34,23 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + signer := &signer{ + apiKey: opts.ApiKey, + } + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/api/v1"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - timestamp := fmt.Sprintf("%d", time.Now().Unix()) - tokenMd5 := md5.Sum([]byte("1panel" + opts.ApiKey + timestamp)) - tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) - req.Header.Set("1Panel-Timestamp", timestamp) - req.Header.Set("1Panel-Token", tokenMd5Hex) + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/1panel/signer.go b/pkg/sdk3rd/1panel/signer.go new file mode 100644 index 00000000..d075fd3f --- /dev/null +++ b/pkg/sdk3rd/1panel/signer.go @@ -0,0 +1,28 @@ +package onepanel + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "net/http" + "time" +) + +type signer struct { + apiKey string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://1panel.cn/docs/v1/dev_manual/api_manual/ + + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + tokenMd5 := md5.Sum([]byte("1panel" + s.apiKey + timestamp)) + tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) + + req.Header.Set("1Panel-Timestamp", timestamp) + req.Header.Set("1Panel-Token", tokenMd5Hex) + + return nil +} diff --git a/pkg/sdk3rd/1panel/v2/client.go b/pkg/sdk3rd/1panel/v2/client.go index b5b7e416..5a879f0d 100644 --- a/pkg/sdk3rd/1panel/v2/client.go +++ b/pkg/sdk3rd/1panel/v2/client.go @@ -1,9 +1,7 @@ package v2 import ( - "crypto/md5" "crypto/tls" - "encoding/hex" "encoding/json" "fmt" "net/http" @@ -36,23 +34,24 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + signer := &signer{ + apiKey: opts.ApiKey, + } + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/api/v2"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("CurrentNode", opts.CurrentNode). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - timestamp := fmt.Sprintf("%d", time.Now().Unix()) - tokenMd5 := md5.Sum([]byte("1panel" + opts.ApiKey + timestamp)) - tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) - req.Header.Set("1Panel-Timestamp", timestamp) - req.Header.Set("1Panel-Token", tokenMd5Hex) + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/1panel/v2/signer.go b/pkg/sdk3rd/1panel/v2/signer.go new file mode 100644 index 00000000..fcaf758d --- /dev/null +++ b/pkg/sdk3rd/1panel/v2/signer.go @@ -0,0 +1,28 @@ +package v2 + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "net/http" + "time" +) + +type signer struct { + apiKey string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://1panel.cn/docs/v2/dev_manual/api_manual/ + + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + tokenMd5 := md5.Sum([]byte("1panel" + s.apiKey + timestamp)) + tokenMd5Hex := hex.EncodeToString(tokenMd5[:]) + + req.Header.Set("1Panel-Timestamp", timestamp) + req.Header.Set("1Panel-Token", tokenMd5Hex) + + return nil +} diff --git a/pkg/sdk3rd/alibabacloud/oss/client.go b/pkg/sdk3rd/alibabacloud/oss/client.go index a7a584d8..3b0b1804 100644 --- a/pkg/sdk3rd/alibabacloud/oss/client.go +++ b/pkg/sdk3rd/alibabacloud/oss/client.go @@ -1,19 +1,12 @@ package oss import ( - "bytes" - "crypto/hmac" "crypto/md5" - "crypto/sha256" "encoding/base64" - "encoding/hex" "encoding/xml" "fmt" - "hash" "net/http" "net/url" - "sort" - "strings" "time" "github.com/go-resty/resty/v2" @@ -57,127 +50,25 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { } } - restyClient := resty.New(). + signer := &signer{ + accessKeyId: opts.AccessKeyId, + accessKeySecret: opts.AccessKeySecret, + region: opts.Region, + } + httper := resty.New(). SetBaseURL(endpoint). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // API 签名机制: - // https://help.aliyun.com/zh/oss/developer-reference/recommend-to-use-signature-version-4 - // https://www.alibabacloud.com/help/en/oss/developer-reference/recommend-to-use-signature-version-4 - - method := strings.ToUpper(req.Method) - - nowUtc := time.Now().UTC() - headerDateStr := nowUtc.Format(http.TimeFormat) - requestDateStr := nowUtc.Format("20060102T150405Z") - signDateStr := nowUtc.Format("20060102") - - requestResStr := req.Header.Get("X-API-Resource") - req.Header.Del("X-API-Resource") - - canonicalUrl := escapePath(req.URL.Path) - if canonicalUrl == "" { - canonicalUrl = "/" + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - if canonicalUrl == "/" && requestResStr != "" { - canonicalUrl = escapePath(requestResStr) - } - - canonicalQueryStr := "" - if len(req.URL.Query()) > 0 { - query := req.URL.Query() - - keys := make([]string, 0, len(query)) - for key := range query { - keys = append(keys, key) - } - sort.Strings(keys) - - for i, key := range keys { - if i > 0 { - canonicalQueryStr += "&" - } - - value := query.Get(key) - if value == "" { - canonicalQueryStr += escapeQuery(key) - } else { - canonicalQueryStr += escapeQuery(key) + "=" + escapeQuery(value) - } - } - } - - canonicalHeaders := "" - additionalHeaders := "" - if len(req.Header) > 0 { - if req.Header.Get("X-OSS-Date") == "" { - req.Header.Set("X-OSS-Date", requestDateStr) - } - if req.Header.Get("X-OSS-Content-SHA256") == "" { - req.Header.Set("X-OSS-Content-SHA256", "UNSIGNED-PAYLOAD") - } - - keys := make([]string, 0, len(req.Header)) - for key := range req.Header { - key = strings.ToLower(key) - if strings.HasPrefix(key, "x-oss-") { - keys = append(keys, key) - } - if key == "content-type" || key == "content-md5" { - keys = append(keys, key) - } - } - sort.Strings(keys) - - for i, key := range keys { - if i > 0 { - canonicalHeaders += "\n" - } - - value := strings.TrimSpace(req.Header.Get(key)) - canonicalHeaders += key + ":" + value - } - - canonicalHeaders += "\n" - } - - hashedPayload := req.Header.Get("X-OSS-Content-SHA256") - - canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, additionalHeaders, hashedPayload) - canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) - canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) - - const signAlgorithmHeader = "OSS4-HMAC-SHA256" - scope := fmt.Sprintf("%s/%s/oss/aliyun_v4_request", signDateStr, opts.Region) - stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", signAlgorithmHeader, requestDateStr, scope, canonicalRequestHashHex) - - var h hash.Hash - h = hmac.New(sha256.New, []byte("aliyun_v4"+opts.AccessKeySecret)) - h.Write([]byte(signDateStr)) - kDate := h.Sum(nil) - h = hmac.New(sha256.New, kDate) - h.Write([]byte(opts.Region)) - kRegion := h.Sum(nil) - h = hmac.New(sha256.New, kRegion) - h.Write([]byte("oss")) - kService := h.Sum(nil) - h = hmac.New(sha256.New, kService) - h.Write([]byte("aliyun_v4_request")) - kSigning := h.Sum(nil) - - h = hmac.New(sha256.New, kSigning) - h.Write([]byte(stringToSign)) - signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) - - req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s/%s, Signature=%s", signAlgorithmHeader, opts.AccessKeyId, scope, signature)) - req.Header.Set("Date", headerDateStr) return nil }) return &Client{ bucket: opts.Bucket, - rc: restyClient, + rc: httper, }, nil } @@ -267,30 +158,3 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest return resp, nil } - -func escapeQuery(str string) string { - res := url.QueryEscape(str) - res = strings.ReplaceAll(res, "+", "%20") - return res -} - -func escapePath(path string) string { - var buf bytes.Buffer - for i := 0; i < len(path); i++ { - c := path[i] - noEscape := (c >= 'A' && c <= 'Z') || - (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || - c == '-' || - c == '.' || - c == '_' || - c == '~' || - c == '/' - if noEscape { - buf.WriteByte(c) - } else { - fmt.Fprintf(&buf, "%%%02X", c) - } - } - return buf.String() -} diff --git a/pkg/sdk3rd/alibabacloud/oss/signer.go b/pkg/sdk3rd/alibabacloud/oss/signer.go new file mode 100644 index 00000000..7d240571 --- /dev/null +++ b/pkg/sdk3rd/alibabacloud/oss/signer.go @@ -0,0 +1,165 @@ +package oss + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +type signer struct { + accessKeyId string + accessKeySecret string + region string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://help.aliyun.com/zh/oss/developer-reference/recommend-to-use-signature-version-4 + // https://www.alibabacloud.com/help/en/oss/developer-reference/recommend-to-use-signature-version-4 + + method := strings.ToUpper(req.Method) + + nowUtc := time.Now().UTC() + headerDateStr := nowUtc.Format(http.TimeFormat) + requestDateStr := nowUtc.Format("20060102T150405Z") + signDateStr := nowUtc.Format("20060102") + + requestResStr := req.Header.Get("X-API-Resource") + req.Header.Del("X-API-Resource") + + canonicalUrl := escapePath(req.URL.Path) + if canonicalUrl == "" { + canonicalUrl = "/" + } + if canonicalUrl == "/" && requestResStr != "" { + canonicalUrl = escapePath(requestResStr) + } + + canonicalQueryStr := "" + if len(req.URL.Query()) > 0 { + query := req.URL.Query() + + keys := make([]string, 0, len(query)) + for key := range query { + keys = append(keys, key) + } + sort.Strings(keys) + + for i, key := range keys { + if i > 0 { + canonicalQueryStr += "&" + } + + value := query.Get(key) + if value == "" { + canonicalQueryStr += escapeQuery(key) + } else { + canonicalQueryStr += escapeQuery(key) + "=" + escapeQuery(value) + } + } + } + + canonicalHeaders := "" + additionalHeaders := "" + if len(req.Header) > 0 { + if req.Header.Get("X-OSS-Date") == "" { + req.Header.Set("X-OSS-Date", requestDateStr) + } + if req.Header.Get("X-OSS-Content-SHA256") == "" { + req.Header.Set("X-OSS-Content-SHA256", "UNSIGNED-PAYLOAD") + } + + keys := make([]string, 0, len(req.Header)) + for key := range req.Header { + key = strings.ToLower(key) + if strings.HasPrefix(key, "x-oss-") { + keys = append(keys, key) + } + if key == "content-type" || key == "content-md5" { + keys = append(keys, key) + } + } + sort.Strings(keys) + + for i, key := range keys { + if i > 0 { + canonicalHeaders += "\n" + } + + value := strings.TrimSpace(req.Header.Get(key)) + canonicalHeaders += key + ":" + value + } + + canonicalHeaders += "\n" + } + + hashedPayload := req.Header.Get("X-OSS-Content-SHA256") + + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, additionalHeaders, hashedPayload) + canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) + canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) + + const signAlgorithmHeader = "OSS4-HMAC-SHA256" + scope := fmt.Sprintf("%s/%s/oss/aliyun_v4_request", signDateStr, s.region) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", signAlgorithmHeader, requestDateStr, scope, canonicalRequestHashHex) + + var h hash.Hash + h = hmac.New(sha256.New, []byte("aliyun_v4"+s.accessKeySecret)) + h.Write([]byte(signDateStr)) + kDate := h.Sum(nil) + h = hmac.New(sha256.New, kDate) + h.Write([]byte(s.region)) + kRegion := h.Sum(nil) + h = hmac.New(sha256.New, kRegion) + h.Write([]byte("oss")) + kService := h.Sum(nil) + h = hmac.New(sha256.New, kService) + h.Write([]byte("aliyun_v4_request")) + kSigning := h.Sum(nil) + + h = hmac.New(sha256.New, kSigning) + h.Write([]byte(stringToSign)) + signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) + + authorization := fmt.Sprintf("%s Credential=%s/%s, Signature=%s", signAlgorithmHeader, s.accessKeyId, scope, signature) + + req.Header.Set("Authorization", authorization) + req.Header.Set("Date", headerDateStr) + + return nil +} + +func escapeQuery(str string) string { + res := url.QueryEscape(str) + res = strings.ReplaceAll(res, "+", "%20") + return res +} + +func escapePath(path string) string { + var buf bytes.Buffer + for i := 0; i < len(path); i++ { + c := path[i] + noEscape := (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '.' || + c == '_' || + c == '~' || + c == '/' + if noEscape { + buf.WriteByte(c) + } else { + fmt.Fprintf(&buf, "%%%02X", c) + } + } + return buf.String() +} diff --git a/pkg/sdk3rd/apisix/client.go b/pkg/sdk3rd/apisix/client.go index 2c7db08b..400899c9 100644 --- a/pkg/sdk3rd/apisix/client.go +++ b/pkg/sdk3rd/apisix/client.go @@ -33,14 +33,14 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/apisix/admin"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-Api-Key", opts.ApiKey) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/baishan/client.go b/pkg/sdk3rd/baishan/client.go index fcd820bb..6d7124a1 100644 --- a/pkg/sdk3rd/baishan/client.go +++ b/pkg/sdk3rd/baishan/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://cdn.api.baishan.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetQueryParam("token", opts.ApiToken) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/btpanel/client.go b/pkg/sdk3rd/btpanel/client.go index 802c3e8a..f65f9017 100644 --- a/pkg/sdk3rd/btpanel/client.go +++ b/pkg/sdk3rd/btpanel/client.go @@ -38,7 +38,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/x-www-form-urlencoded"). @@ -46,7 +46,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return &Client{ apiKey: opts.ApiKey, - rc: restyClient, + rc: httper, }, nil } @@ -96,9 +96,9 @@ func (c *Client) newRequest(method string, path string, params any) (*resty.Requ } } - timestamp := time.Now().Unix() - data["request_time"] = fmt.Sprintf("%d", timestamp) - data["request_token"] = generateSignature(fmt.Sprintf("%d", timestamp), c.apiKey) + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + data["request_time"] = timestamp + data["request_token"] = generateSignature(timestamp, c.apiKey) req := c.rc.R() req.Method = method diff --git a/pkg/sdk3rd/btpanelgo/client.go b/pkg/sdk3rd/btpanelgo/client.go index 1a3c6d6a..e297e67a 100644 --- a/pkg/sdk3rd/btpanelgo/client.go +++ b/pkg/sdk3rd/btpanelgo/client.go @@ -39,7 +39,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/x-www-form-urlencoded"). @@ -47,7 +47,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return &Client{ apiKey: opts.ApiKey, - rc: restyClient, + rc: httper, }, nil } @@ -97,9 +97,9 @@ func (c *Client) newRequest(method string, path string, params any, multipart bo } } - timestamp := time.Now().Unix() - data["request_time"] = fmt.Sprintf("%d", timestamp) - data["request_token"] = generateSignature(fmt.Sprintf("%d", timestamp), c.apiKey) + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + data["request_time"] = timestamp + data["request_token"] = generateSignature(timestamp, c.apiKey) req := c.rc.R() req.Method = method diff --git a/pkg/sdk3rd/btwaf/client.go b/pkg/sdk3rd/btwaf/client.go index 75cfc252..66c0d158 100644 --- a/pkg/sdk3rd/btwaf/client.go +++ b/pkg/sdk3rd/btwaf/client.go @@ -1,9 +1,7 @@ package btwaf import ( - "crypto/md5" "crypto/tls" - "encoding/hex" "encoding/json" "fmt" "net/http" @@ -36,24 +34,23 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + signer := &signer{ + apiKey: opts.ApiKey, + } + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - timestamp := fmt.Sprintf("%d", time.Now().Unix()) - keyMd5 := md5.Sum([]byte(opts.ApiKey)) - keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:])) - signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex)) - signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:])) - req.Header.Set("waf_request_time", timestamp) - req.Header.Set("waf_request_token", signMd5Hex) + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/btwaf/signer.go b/pkg/sdk3rd/btwaf/signer.go new file mode 100644 index 00000000..e830d77a --- /dev/null +++ b/pkg/sdk3rd/btwaf/signer.go @@ -0,0 +1,32 @@ +package btwaf + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "net/http" + "strings" + "time" +) + +type signer struct { + apiKey string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://github.com/aaPanel/aaWAF/blob/main/API.md + + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + keyMd5 := md5.Sum([]byte(s.apiKey)) + keyMd5Hex := strings.ToLower(hex.EncodeToString(keyMd5[:])) + + signMd5 := md5.Sum([]byte(timestamp + keyMd5Hex)) + signMd5Hex := strings.ToLower(hex.EncodeToString(signMd5[:])) + + req.Header.Set("waf_request_time", timestamp) + req.Header.Set("waf_request_token", signMd5Hex) + + return nil +} diff --git a/pkg/sdk3rd/bunny/client.go b/pkg/sdk3rd/bunny/client.go index 731ccd40..dd648d05 100644 --- a/pkg/sdk3rd/bunny/client.go +++ b/pkg/sdk3rd/bunny/client.go @@ -23,14 +23,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.bunny.net"). SetHeader("Accept", "application/json"). SetHeader("AccessKey", opts.ApiKey). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/cachefly/client.go b/pkg/sdk3rd/cachefly/client.go index 0d2ba287..2430055b 100644 --- a/pkg/sdk3rd/cachefly/client.go +++ b/pkg/sdk3rd/cachefly/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.cachefly.com/api/2.5"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-CF-Authorization", "Bearer "+opts.ApiToken) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/cdnfly/client.go b/pkg/sdk3rd/cdnfly/client.go index eee5dd86..a85feb26 100644 --- a/pkg/sdk3rd/cdnfly/client.go +++ b/pkg/sdk3rd/cdnfly/client.go @@ -36,7 +36,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiSecret") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/v1"). SetHeader("Accept", "application/json"). SetHeader("API-Key", opts.ApiKey). @@ -44,7 +44,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/cloudflare/client.go b/pkg/sdk3rd/cloudflare/client.go index deb06d1e..5a91eddf 100644 --- a/pkg/sdk3rd/cloudflare/client.go +++ b/pkg/sdk3rd/cloudflare/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.cloudflare.com/client/v4"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+opts.ApiToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/conoha/vps/v3/client.go b/pkg/sdk3rd/conoha/vps/v3/client.go index 2a3f0d8f..63a018a2 100644 --- a/pkg/sdk3rd/conoha/vps/v3/client.go +++ b/pkg/sdk3rd/conoha/vps/v3/client.go @@ -54,7 +54,7 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Auth-Token", client.accessToken) } diff --git a/pkg/sdk3rd/cpanel/client.go b/pkg/sdk3rd/cpanel/client.go index f325e992..d4c56785 100644 --- a/pkg/sdk3rd/cpanel/client.go +++ b/pkg/sdk3rd/cpanel/client.go @@ -36,13 +36,13 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/execute"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "cpanel "+opts.Username+":"+opts.ApiToken). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/ctyun/openapi/client.go b/pkg/sdk3rd/ctyun/openapi/client.go index 4dfa1010..97f4685c 100644 --- a/pkg/sdk3rd/ctyun/openapi/client.go +++ b/pkg/sdk3rd/ctyun/openapi/client.go @@ -1,21 +1,13 @@ package openapi import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/hex" "encoding/json" "fmt" - "hash" - "io" "net/http" "net/url" "time" "github.com/go-resty/resty/v2" - "github.com/pocketbase/pocketbase/tools/security" "github.com/certimate-go/certimate/internal/app" ) @@ -43,62 +35,24 @@ func NewClient(baseUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset secretAccessKey") } - restyClient := resty.New(). + signer := &signer{ + accessKeyId: opts.AccessKeyId, + secretAccessKey: opts.SecretAccessKey, + } + httper := resty.New(). SetBaseURL(baseUrl). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // API 签名机制: - // https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=77&api=%u6784%u9020%u8BF7%u6C42&data=114&vid=107 - - now := time.Now() - eopDate := now.Format("20060102T150405Z") - eopReqId := security.RandomString(32) - - queryStr := "" - if req.URL != nil { - queryStr = req.URL.Query().Encode() + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - payloadStr := "" - if req.Method != http.MethodGet && req.Body != nil { - payloadb, err := io.ReadAll(req.Body) - if err != nil { - return err - } - - payloadStr = string(payloadb) - req.Body = io.NopCloser(bytes.NewReader(payloadb)) - } - payloadHash := sha256.Sum256([]byte(payloadStr)) - payloadHashHex := hex.EncodeToString(payloadHash[:]) - - var h hash.Hash - h = hmac.New(sha256.New, []byte(opts.SecretAccessKey)) - h.Write([]byte(eopDate)) - kTime := h.Sum(nil) - h = hmac.New(sha256.New, kTime) - h.Write([]byte(opts.AccessKeyId)) - kAk := h.Sum(nil) - h = hmac.New(sha256.New, kAk) - h.Write([]byte(now.Format("20060102"))) - kDate := h.Sum(nil) - - stringToSign := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n\n%s\n%s", eopReqId, eopDate, queryStr, payloadHashHex) - - h = hmac.New(sha256.New, kDate) - h.Write([]byte(stringToSign)) - signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) - - req.Header.Set("ctyun-eop-request-id", eopReqId) - req.Header.Set("eop-date", eopDate) - req.Header.Set("eop-authorization", fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", opts.AccessKeyId, signature)) - return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/ctyun/openapi/signer.go b/pkg/sdk3rd/ctyun/openapi/signer.go new file mode 100644 index 00000000..abeb78eb --- /dev/null +++ b/pkg/sdk3rd/ctyun/openapi/signer.go @@ -0,0 +1,73 @@ +package openapi + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "hash" + "io" + "net/http" + "time" + + "github.com/pocketbase/pocketbase/tools/security" +) + +type signer struct { + accessKeyId string + secretAccessKey string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=77&api=%u6784%u9020%u8BF7%u6C42&data=114&vid=107 + + now := time.Now() + eopDate := now.Format("20060102T150405Z") + eopReqId := security.RandomString(32) + + queryStr := "" + if req.URL != nil { + queryStr = req.URL.Query().Encode() + } + + payloadStr := "" + if req.Method != http.MethodGet && req.Body != nil { + payloadb, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + payloadStr = string(payloadb) + req.Body = io.NopCloser(bytes.NewReader(payloadb)) + } + payloadHash := sha256.Sum256([]byte(payloadStr)) + payloadHashHex := hex.EncodeToString(payloadHash[:]) + + var h hash.Hash + h = hmac.New(sha256.New, []byte(s.secretAccessKey)) + h.Write([]byte(eopDate)) + kTime := h.Sum(nil) + h = hmac.New(sha256.New, kTime) + h.Write([]byte(s.accessKeyId)) + kAk := h.Sum(nil) + h = hmac.New(sha256.New, kAk) + h.Write([]byte(now.Format("20060102"))) + kDate := h.Sum(nil) + + stringToSign := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n\n%s\n%s", eopReqId, eopDate, queryStr, payloadHashHex) + + h = hmac.New(sha256.New, kDate) + h.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + eopAuthorization := fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", s.accessKeyId, signature) + + req.Header.Set("ctyun-eop-request-id", eopReqId) + req.Header.Set("eop-date", eopDate) + req.Header.Set("eop-authorization", eopAuthorization) + + return nil +} diff --git a/pkg/sdk3rd/dcloud/unicloud/client.go b/pkg/sdk3rd/dcloud/unicloud/client.go index 688269c0..ef58a1e9 100644 --- a/pkg/sdk3rd/dcloud/unicloud/client.go +++ b/pkg/sdk3rd/dcloud/unicloud/client.go @@ -72,7 +72,7 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.apiUserToken != "" { req.Header.Set("Token", client.apiUserToken) } diff --git a/pkg/sdk3rd/digitalocean/client.go b/pkg/sdk3rd/digitalocean/client.go index 7ff11736..7f713499 100644 --- a/pkg/sdk3rd/digitalocean/client.go +++ b/pkg/sdk3rd/digitalocean/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset accessToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.digitalocean.com/v2"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+opts.AccessToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/dogecloud/client.go b/pkg/sdk3rd/dogecloud/client.go index 69a21ba5..39ac75ec 100644 --- a/pkg/sdk3rd/dogecloud/client.go +++ b/pkg/sdk3rd/dogecloud/client.go @@ -1,13 +1,8 @@ package dogecloud import ( - "bytes" - "crypto/hmac" - "crypto/sha1" - "encoding/hex" "encoding/json" "fmt" - "io" "net/http" "time" @@ -33,44 +28,24 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset secretKey") } - restyClient := resty.New(). + signer := &signer{ + accessKey: opts.AccessKey, + secretKey: opts.SecretKey, + } + httper := resty.New(). SetBaseURL("https://api.dogecloud.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetPreRequestHook(func(ctx *resty.Client, req *http.Request) error { - // API 签名机制: - // https://docs.dogecloud.com/cdn/api-access-token - - path := req.URL.Path - queryStr := req.URL.Query().Encode() - if queryStr != "" { - path += "?" + queryStr + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - payloadStr := "" - if req.Body != nil { - payloadb, err := io.ReadAll(req.Body) - if err != nil { - return err - } - - payloadStr = string(payloadb) - req.Body = io.NopCloser(bytes.NewReader(payloadb)) - } - - stringToSign := fmt.Sprintf("%s\n%s", path, payloadStr) - - h := hmac.New(sha1.New, []byte(opts.SecretKey)) - h.Write([]byte(stringToSign)) - signature := hex.EncodeToString(h.Sum(nil)) - - req.Header.Set("Authorization", fmt.Sprintf("TOKEN %s:%s", opts.AccessKey, signature)) - return nil }) - return &Client{restyClient}, nil + return &Client{httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/dogecloud/signer.go b/pkg/sdk3rd/dogecloud/signer.go new file mode 100644 index 00000000..7078964e --- /dev/null +++ b/pkg/sdk3rd/dogecloud/signer.go @@ -0,0 +1,50 @@ +package dogecloud + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "fmt" + "io" + "net/http" +) + +type signer struct { + accessKey string + secretKey string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://docs.dogecloud.com/cdn/api-access-token + + path := req.URL.Path + queryStr := req.URL.Query().Encode() + if queryStr != "" { + path += "?" + queryStr + } + + payloadStr := "" + if req.Body != nil { + payloadb, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + payloadStr = string(payloadb) + req.Body = io.NopCloser(bytes.NewReader(payloadb)) + } + + stringToSign := fmt.Sprintf("%s\n%s", path, payloadStr) + + h := hmac.New(sha1.New, []byte(s.secretKey)) + h.Write([]byte(stringToSign)) + signature := hex.EncodeToString(h.Sum(nil)) + + authorization := fmt.Sprintf("TOKEN %s:%s", s.accessKey, signature) + + req.Header.Set("Authorization", authorization) + + return nil +} diff --git a/pkg/sdk3rd/dokploy/client.go b/pkg/sdk3rd/dokploy/client.go index 6f47d696..088e2967 100644 --- a/pkg/sdk3rd/dokploy/client.go +++ b/pkg/sdk3rd/dokploy/client.go @@ -33,14 +33,14 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-Api-Key", opts.ApiKey) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/dynv6/client.go b/pkg/sdk3rd/dynv6/client.go index d6a04ad0..92d54d6b 100644 --- a/pkg/sdk3rd/dynv6/client.go +++ b/pkg/sdk3rd/dynv6/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset httpToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://dynv6.com/api/v2"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+opts.HttpToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/flexcdn/client.go b/pkg/sdk3rd/flexcdn/client.go index cd1c264a..6e443e43 100644 --- a/pkg/sdk3rd/flexcdn/client.go +++ b/pkg/sdk3rd/flexcdn/client.go @@ -62,7 +62,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Cloud-Access-Token", client.accessToken) } diff --git a/pkg/sdk3rd/flyio/client.go b/pkg/sdk3rd/flyio/client.go index d8ed475e..a8ec09ad 100644 --- a/pkg/sdk3rd/flyio/client.go +++ b/pkg/sdk3rd/flyio/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.machines.dev/v1"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+opts.ApiToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/goedge/client.go b/pkg/sdk3rd/goedge/client.go index f0251251..d7e21607 100644 --- a/pkg/sdk3rd/goedge/client.go +++ b/pkg/sdk3rd/goedge/client.go @@ -62,7 +62,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Edge-Access-Token", client.accessToken) } diff --git a/pkg/sdk3rd/huaweicloud/obs/client.go b/pkg/sdk3rd/huaweicloud/obs/client.go index 4ef6f4ee..96ef9856 100644 --- a/pkg/sdk3rd/huaweicloud/obs/client.go +++ b/pkg/sdk3rd/huaweicloud/obs/client.go @@ -1,17 +1,12 @@ package obs import ( - "bytes" - "crypto/hmac" "crypto/md5" - "crypto/sha1" "encoding/base64" "encoding/xml" "fmt" "net/http" "net/url" - "sort" - "strings" "time" "github.com/go-resty/resty/v2" @@ -53,82 +48,23 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { } } - restyClient := resty.New(). + signer := &signer{ + accessKeyId: opts.AccessKeyId, + secretAccessKey: opts.SecretAccessKey, + bucket: opts.Bucket, + } + httper := resty.New(). SetBaseURL(endpoint). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // API 签名机制: - // https://support.huaweicloud.com/api-obs/obs_04_0010.html - - canonicalizedHeaders := "" - if len(req.Header) > 0 { - keys := make([]string, 0, len(req.Header)) - for key := range req.Header { - key = strings.ToLower(key) - if strings.HasPrefix(key, "x-obs-") { - keys = append(keys, key) - } - } - sort.Strings(keys) - - for _, key := range keys { - value := strings.TrimSpace(req.Header.Get(key)) - canonicalizedHeaders += key + ":" + escapeQuery(value) - canonicalizedHeaders += "\n" - } + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - bucketName := opts.Bucket - objectName := strings.Trim(req.URL.Path, "/") - canonicalizedResources := escapePath(fmt.Sprintf("/%s/%s", bucketName, objectName)) - if bucketName == "" && objectName == "" { - canonicalizedResources = "/" - } - if len(req.URL.Query()) > 0 { - query := req.URL.Query() - - keys := make([]string, 0, len(query)) - for key := range query { - keys = append(keys, key) - } - sort.Strings(keys) - - for i, key := range keys { - if i == 0 { - canonicalizedResources += "?" - } else { - canonicalizedResources += "&" - } - - value := query.Get(key) - if value == "" { - canonicalizedResources += escapeQuery(key) - } else { - canonicalizedResources += escapeQuery(key) + "=" + escapeQuery(value) - } - } - } - - method := strings.ToUpper(req.Method) - - dateStr := time.Now().UTC().Format(http.TimeFormat) - - contentMd5 := req.Header.Get("Content-MD5") - contentType := req.Header.Get("Content-Type") - - stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s%s", method, contentMd5, contentType, dateStr, canonicalizedHeaders, canonicalizedResources) - - h := hmac.New(sha1.New, []byte(opts.SecretAccessKey)) - h.Write([]byte(stringToSign)) - signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) - - req.Header.Set("Authorization", fmt.Sprintf("OBS %s:%s", opts.AccessKeyId, signature)) - req.Header.Set("Date", dateStr) - return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { @@ -216,32 +152,3 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest return resp, nil } - -func escapeQuery(str string) string { - res := url.QueryEscape(str) - res = strings.ReplaceAll(res, "%7E", "~") - res = strings.ReplaceAll(res, "%2F", "/") - res = strings.ReplaceAll(res, "%20", "+") - return res -} - -func escapePath(path string) string { - var buf bytes.Buffer - for i := 0; i < len(path); i++ { - c := path[i] - noEscape := (c >= 'A' && c <= 'Z') || - (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || - c == '-' || - c == '.' || - c == '_' || - c == '~' || - c == '/' - if noEscape { - buf.WriteByte(c) - } else { - fmt.Fprintf(&buf, "%%%02X", c) - } - } - return buf.String() -} diff --git a/pkg/sdk3rd/huaweicloud/obs/signer.go b/pkg/sdk3rd/huaweicloud/obs/signer.go new file mode 100644 index 00000000..53282629 --- /dev/null +++ b/pkg/sdk3rd/huaweicloud/obs/signer.go @@ -0,0 +1,123 @@ +package obs + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +type signer struct { + accessKeyId string + secretAccessKey string + bucket string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://support.huaweicloud.com/api-obs/obs_04_0010.html + + canonicalizedHeaders := "" + if len(req.Header) > 0 { + keys := make([]string, 0, len(req.Header)) + for key := range req.Header { + key = strings.ToLower(key) + if strings.HasPrefix(key, "x-obs-") { + keys = append(keys, key) + } + } + sort.Strings(keys) + + for _, key := range keys { + value := strings.TrimSpace(req.Header.Get(key)) + canonicalizedHeaders += key + ":" + escapeQuery(value) + canonicalizedHeaders += "\n" + } + } + + bucketName := s.bucket + objectName := strings.Trim(req.URL.Path, "/") + canonicalizedResources := escapePath(fmt.Sprintf("/%s/%s", bucketName, objectName)) + if bucketName == "" && objectName == "" { + canonicalizedResources = "/" + } + if len(req.URL.Query()) > 0 { + query := req.URL.Query() + + keys := make([]string, 0, len(query)) + for key := range query { + keys = append(keys, key) + } + sort.Strings(keys) + + for i, key := range keys { + if i == 0 { + canonicalizedResources += "?" + } else { + canonicalizedResources += "&" + } + + value := query.Get(key) + if value == "" { + canonicalizedResources += escapeQuery(key) + } else { + canonicalizedResources += escapeQuery(key) + "=" + escapeQuery(value) + } + } + } + + method := strings.ToUpper(req.Method) + + dateStr := time.Now().UTC().Format(http.TimeFormat) + + contentMd5 := req.Header.Get("Content-MD5") + contentType := req.Header.Get("Content-Type") + + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s%s", method, contentMd5, contentType, dateStr, canonicalizedHeaders, canonicalizedResources) + + h := hmac.New(sha1.New, []byte(s.secretAccessKey)) + h.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + authorization := fmt.Sprintf("OBS %s:%s", s.accessKeyId, signature) + + req.Header.Set("Authorization", authorization) + req.Header.Set("Date", dateStr) + + return nil +} + +func escapeQuery(str string) string { + res := url.QueryEscape(str) + res = strings.ReplaceAll(res, "%7E", "~") + res = strings.ReplaceAll(res, "%2F", "/") + res = strings.ReplaceAll(res, "%20", "+") + return res +} + +func escapePath(path string) string { + var buf bytes.Buffer + for i := 0; i < len(path); i++ { + c := path[i] + noEscape := (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '.' || + c == '_' || + c == '~' || + c == '/' + if noEscape { + buf.WriteByte(c) + } else { + fmt.Fprintf(&buf, "%%%02X", c) + } + } + return buf.String() +} diff --git a/pkg/sdk3rd/kong/client.go b/pkg/sdk3rd/kong/client.go index af3fdd1b..3c186078 100644 --- a/pkg/sdk3rd/kong/client.go +++ b/pkg/sdk3rd/kong/client.go @@ -38,14 +38,14 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { baseUrl += fmt.Sprintf("/%s", url.PathEscape(opts.Workspace)) } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(baseUrl). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("Kong-Admin-Token", opts.ApiToken) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/lecdn/v3/client/client.go b/pkg/sdk3rd/lecdn/v3/client/client.go index 48fb0c03..be9a28a2 100644 --- a/pkg/sdk3rd/lecdn/v3/client/client.go +++ b/pkg/sdk3rd/lecdn/v3/client/client.go @@ -51,7 +51,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { client.rc = resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/prod-api"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) } diff --git a/pkg/sdk3rd/lecdn/v3/master/client.go b/pkg/sdk3rd/lecdn/v3/master/client.go index 4fd32092..339e0a52 100644 --- a/pkg/sdk3rd/lecdn/v3/master/client.go +++ b/pkg/sdk3rd/lecdn/v3/master/client.go @@ -53,7 +53,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) } diff --git a/pkg/sdk3rd/linode/client.go b/pkg/sdk3rd/linode/client.go index 806aea59..d6d59335 100644 --- a/pkg/sdk3rd/linode/client.go +++ b/pkg/sdk3rd/linode/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset accessToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.linode.com/v4"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+opts.AccessToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/mohua/client.go b/pkg/sdk3rd/mohua/client.go index 89ef2d87..ac05e161 100644 --- a/pkg/sdk3rd/mohua/client.go +++ b/pkg/sdk3rd/mohua/client.go @@ -43,7 +43,7 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.jwtToken != "" { req.Header.Set("JWT", "Bearer "+client.jwtToken) } diff --git a/pkg/sdk3rd/netlify/client.go b/pkg/sdk3rd/netlify/client.go index d76d0d20..093bbca1 100644 --- a/pkg/sdk3rd/netlify/client.go +++ b/pkg/sdk3rd/netlify/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.netlify.com/api/v1"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+opts.ApiToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/nginxproxymanager/client.go b/pkg/sdk3rd/nginxproxymanager/client.go index 01469d76..d25b07f9 100644 --- a/pkg/sdk3rd/nginxproxymanager/client.go +++ b/pkg/sdk3rd/nginxproxymanager/client.go @@ -51,7 +51,7 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.jwtToken != "" { req.Header.Set("Authorization", "Bearer "+client.jwtToken) } diff --git a/pkg/sdk3rd/proxmoxve/client.go b/pkg/sdk3rd/proxmoxve/client.go index 86e89179..6e23b38e 100644 --- a/pkg/sdk3rd/proxmoxve/client.go +++ b/pkg/sdk3rd/proxmoxve/client.go @@ -36,14 +36,14 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset tokenSecret") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/api2/json"). SetHeader("Accept", "application/json"). SetHeader("Authorization", fmt.Sprintf("PVEAPIToken=%s=%s", opts.TokenId, opts.TokenSecret)). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/qingcloud/dns/client.go b/pkg/sdk3rd/qingcloud/dns/client.go index c4c60fb4..d98d9dcf 100644 --- a/pkg/sdk3rd/qingcloud/dns/client.go +++ b/pkg/sdk3rd/qingcloud/dns/client.go @@ -1,13 +1,9 @@ package dns import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" "encoding/json" "fmt" "net/http" - "net/url" "time" "github.com/go-resty/resty/v2" @@ -32,43 +28,24 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset secretAccessKey") } - restyClient := resty.New(). + signer := &signer{ + accessKeyId: options.AccessKeyId, + secretAccessKey: options.SecretAccessKey, + } + httper := resty.New(). SetBaseURL("http://api.routewize.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // 签名机制: - // https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/calling_method/signature/ - - date := time.Now().UTC().Format(time.RFC1123) - - verb := req.Method - - canonicalizedResource := "/" - if req.URL != nil { - canonicalizedResource = req.URL.Path - if req.URL.RawQuery != "" { - values, _ := url.ParseQuery(req.URL.RawQuery) - canonicalizedResource += "?" + values.Encode() - } + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - stringToSign := verb + "\n" + - date + "\n" + - canonicalizedResource - - h := hmac.New(sha256.New, []byte(options.SecretAccessKey)) - h.Write([]byte(stringToSign)) - signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) - - req.Header.Set("Date", date) - req.Header.Set("Authorization", fmt.Sprintf("QC-HMAC-SHA256 %s:%s", options.AccessKeyId, signature)) - return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/qingcloud/dns/signer.go b/pkg/sdk3rd/qingcloud/dns/signer.go new file mode 100644 index 00000000..0896967e --- /dev/null +++ b/pkg/sdk3rd/qingcloud/dns/signer.go @@ -0,0 +1,49 @@ +package dns + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "time" +) + +type signer struct { + accessKeyId string + secretAccessKey string +} + +func (s *signer) Sign(req *http.Request) error { + // 签名机制: + // https://docsv4.qingcloud.com/user_guide/development_docs/api/api_list/dns/calling_method/signature/ + + date := time.Now().UTC().Format(time.RFC1123) + + verb := req.Method + + canonicalizedResource := "/" + if req.URL != nil { + canonicalizedResource = req.URL.Path + if req.URL.RawQuery != "" { + values, _ := url.ParseQuery(req.URL.RawQuery) + canonicalizedResource += "?" + values.Encode() + } + } + + stringToSign := verb + "\n" + + date + "\n" + + canonicalizedResource + + h := hmac.New(sha256.New, []byte(s.secretAccessKey)) + h.Write([]byte(stringToSign)) + signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + authorization := fmt.Sprintf("QC-HMAC-SHA256 %s:%s", s.accessKeyId, signature) + + req.Header.Set("Date", date) + req.Header.Set("Authorization", authorization) + + return nil +} diff --git a/pkg/sdk3rd/rainyun/client.go b/pkg/sdk3rd/rainyun/client.go index 0b0b701f..1396d3d8 100644 --- a/pkg/sdk3rd/rainyun/client.go +++ b/pkg/sdk3rd/rainyun/client.go @@ -24,14 +24,14 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.v2.rainyun.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-API-Key", options.ApiKey) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/ratpanel/client.go b/pkg/sdk3rd/ratpanel/client.go index 8fb25268..0361584e 100644 --- a/pkg/sdk3rd/ratpanel/client.go +++ b/pkg/sdk3rd/ratpanel/client.go @@ -1,14 +1,9 @@ package ratpanel import ( - "bytes" - "crypto/hmac" - "crypto/sha256" "crypto/tls" - "encoding/hex" "encoding/json" "fmt" - "io" "net/http" "net/url" "strings" @@ -42,58 +37,24 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset accessToken") } - restyClient := resty.New(). + signer := &signer{ + accessTokenId: options.AccessTokenId, + accessToken: options.AccessToken, + } + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // API 签名机制: - // https://ratpanel.github.io/advanced/api#authentication-mechanism - - payloadStr := "" - if req.Body != nil { - payloadb, err := io.ReadAll(req.Body) - if err != nil { - return err - } - - payloadStr = string(payloadb) - req.Body = io.NopCloser(bytes.NewReader(payloadb)) + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - canonicalPath := req.URL.Path - if !strings.HasPrefix(canonicalPath, "/api") { - index := strings.Index(canonicalPath, "/api") - if index != -1 { - canonicalPath = canonicalPath[index:] - } - } - - canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s", - req.Method, - canonicalPath, - req.URL.Query().Encode(), - sumSha256(payloadStr), - ) - - timestamp := time.Now().Unix() - - stringToSign := fmt.Sprintf("%s\n%d\n%s", - "HMAC-SHA256", - timestamp, - sumSha256(canonicalRequest), - ) - - signature := sumHmacSha256(stringToSign, options.AccessToken) - - req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp)) - req.Header.Set("Authorization", fmt.Sprintf("HMAC-SHA256 Credential=%d, Signature=%s", options.AccessTokenId, signature)) - return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { @@ -163,16 +124,3 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest return resp, nil } - -func sumSha256(str string) string { - sum := sha256.Sum256([]byte(str)) - dst := make([]byte, hex.EncodedLen(len(sum))) - hex.Encode(dst, sum[:]) - return string(dst) -} - -func sumHmacSha256(data string, secret string) string { - h := hmac.New(sha256.New, []byte(secret)) - h.Write([]byte(data)) - return hex.EncodeToString(h.Sum(nil)) -} diff --git a/pkg/sdk3rd/ratpanel/signer.go b/pkg/sdk3rd/ratpanel/signer.go new file mode 100644 index 00000000..74c62eab --- /dev/null +++ b/pkg/sdk3rd/ratpanel/signer.go @@ -0,0 +1,75 @@ +package ratpanel + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type signer struct { + accessTokenId int64 + accessToken string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://ratpanel.github.io/advanced/api#authentication-mechanism + + payloadStr := "" + if req.Body != nil { + payloadb, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + payloadStr = string(payloadb) + req.Body = io.NopCloser(bytes.NewReader(payloadb)) + } + + canonicalPath := req.URL.Path + if !strings.HasPrefix(canonicalPath, "/api") { + index := strings.Index(canonicalPath, "/api") + if index != -1 { + canonicalPath = canonicalPath[index:] + } + } + + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s", + req.Method, + canonicalPath, + req.URL.Query().Encode(), + sumSha256(payloadStr), + ) + + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + stringToSign := fmt.Sprintf("%s\n%s\n%s", "HMAC-SHA256", timestamp, sumSha256(canonicalRequest)) + + signature := sumHmacSha256(stringToSign, s.accessToken) + + authorization := fmt.Sprintf("HMAC-SHA256 Credential=%d, Signature=%s", s.accessTokenId, signature) + + req.Header.Set("X-Timestamp", timestamp) + req.Header.Set("Authorization", authorization) + + return nil +} + +func sumSha256(str string) string { + sum := sha256.Sum256([]byte(str)) + dst := make([]byte, hex.EncodedLen(len(sum))) + hex.Encode(dst, sum[:]) + return string(dst) +} + +func sumHmacSha256(data string, secret string) string { + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/pkg/sdk3rd/safeline/client.go b/pkg/sdk3rd/safeline/client.go index f59f29cd..70ec310a 100644 --- a/pkg/sdk3rd/safeline/client.go +++ b/pkg/sdk3rd/safeline/client.go @@ -33,14 +33,14 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-SLCE-API-TOKEN", options.ApiToken) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/samwaf/client.go b/pkg/sdk3rd/samwaf/client.go index dd32cb12..a262d7b1 100644 --- a/pkg/sdk3rd/samwaf/client.go +++ b/pkg/sdk3rd/samwaf/client.go @@ -33,14 +33,14 @@ func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiKey") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api/v1"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). SetHeader("X-API-Key", options.ApiKey) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/synologydsm/client.go b/pkg/sdk3rd/synologydsm/client.go index a194d2cd..5dd94c11 100644 --- a/pkg/sdk3rd/synologydsm/client.go +++ b/pkg/sdk3rd/synologydsm/client.go @@ -38,7 +38,7 @@ func NewClient(serverUrl string) (*Client, error) { client.rc = resty.New(). SetBaseURL(strings.TrimSuffix(serverUrl, "/")). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.synoToken != "" { req.Header.Set("X-SYNO-TOKEN", client.synoToken) } diff --git a/pkg/sdk3rd/upyun/console/client.go b/pkg/sdk3rd/upyun/console/client.go index 989a2562..2baf1da4 100644 --- a/pkg/sdk3rd/upyun/console/client.go +++ b/pkg/sdk3rd/upyun/console/client.go @@ -44,7 +44,7 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if client.loginCookie != "" { req.Header.Set("Cookie", client.loginCookie) } diff --git a/pkg/sdk3rd/vercel/client.go b/pkg/sdk3rd/vercel/client.go index 8c679b2b..12886060 100644 --- a/pkg/sdk3rd/vercel/client.go +++ b/pkg/sdk3rd/vercel/client.go @@ -25,13 +25,13 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset apiToken") } - restyClient := resty.New(). + httper := resty.New(). SetBaseURL("https://api.vercel.com/v8"). SetHeader("Accept", "application/json"). SetHeader("Authorization", "Bearer "+options.ApiToken). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { if options.TeamId != "" { qs := req.URL.Query() qs.Set("teamId", options.TeamId) @@ -41,7 +41,7 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/volcengine/tos/client.go b/pkg/sdk3rd/volcengine/tos/client.go index 17ee1d4e..8a7743f1 100644 --- a/pkg/sdk3rd/volcengine/tos/client.go +++ b/pkg/sdk3rd/volcengine/tos/client.go @@ -1,19 +1,12 @@ package tos import ( - "bytes" - "crypto/hmac" "crypto/md5" - "crypto/sha256" "encoding/base64" - "encoding/hex" "encoding/json" "fmt" - "hash" "net/http" "net/url" - "sort" - "strings" "time" "github.com/go-resty/resty/v2" @@ -55,121 +48,23 @@ func NewClient(endpoint string, optFns ...OptionsFunc) (*Client, error) { } } - restyClient := resty.New(). + signer := &signer{ + accessKeyId: opts.AccessKeyId, + secretAccessKey: opts.SecretAccessKey, + region: opts.Region, + } + httper := resty.New(). SetBaseURL(endpoint). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // API 签名机制: - // https://www.volcengine.com/docs/6349/74839 - // https://docs.byteplus.com/en/docs/tos/reference-signature-mechanism_1 - - method := strings.ToUpper(req.Method) - - nowUtc := time.Now().UTC() - headerDateStr := nowUtc.Format(http.TimeFormat) - requestDateStr := nowUtc.Format("20060102T150405Z") - credentialDateStr := nowUtc.Format("20060102") - - canonicalUrl := escapePath(req.URL.Path) - if canonicalUrl == "" { - canonicalUrl = "/" + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - canonicalQueryStr := "" - if len(req.URL.Query()) > 0 { - query := req.URL.Query() - - keys := make([]string, 0, len(query)) - for key := range query { - keys = append(keys, key) - } - sort.Strings(keys) - - for i, key := range keys { - if i > 0 { - canonicalQueryStr += "&" - } - - value := query.Get(key) - canonicalQueryStr += escapeQuery(key) + "=" + escapeQuery(value) - } - } - - canonicalHeaders := "" - signedHeaders := "" - if len(req.Header) > 0 || req.Host != "" { - if req.Header.Get("Host") == "" { - req.Header.Set("Host", req.Host) - } - if req.Header.Get("X-TOS-Date") == "" { - req.Header.Set("X-TOS-Date", requestDateStr) - } - - keys := make([]string, 0, len(req.Header)) - for key := range req.Header { - key = strings.ToLower(key) - if strings.HasPrefix(key, "x-tos-") || key == "host" { - keys = append(keys, key) - } - if key == "content-type" && req.Header.Get("X-TOS-Content-SHA256") != "" { - keys = append(keys, key) - } - } - sort.Strings(keys) - - for i, key := range keys { - if i > 0 { - canonicalHeaders += "\n" - signedHeaders += ";" - } - - value := strings.TrimSpace(req.Header.Get(key)) - canonicalHeaders += key + ":" + value - signedHeaders += key - } - - canonicalHeaders += "\n" - } - - hashedPayload := req.Header.Get("X-TOS-Content-SHA256") - if hashedPayload == "" { - temp := sha256.Sum256([]byte{}) - hashedPayload = strings.ToLower(hex.EncodeToString(temp[:])) - } - - canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, signedHeaders, hashedPayload) - canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) - canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) - - const signAlgorithmHeader = "TOS4-HMAC-SHA256" - credentialScope := fmt.Sprintf("%s/%s/tos/request", credentialDateStr, opts.Region) - stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", signAlgorithmHeader, requestDateStr, credentialScope, canonicalRequestHashHex) - - var h hash.Hash - h = hmac.New(sha256.New, []byte(opts.SecretAccessKey)) - h.Write([]byte(credentialDateStr)) - kDate := h.Sum(nil) - h = hmac.New(sha256.New, kDate) - h.Write([]byte(opts.Region)) - kRegion := h.Sum(nil) - h = hmac.New(sha256.New, kRegion) - h.Write([]byte("tos")) - kService := h.Sum(nil) - h = hmac.New(sha256.New, kService) - h.Write([]byte("request")) - kSigning := h.Sum(nil) - - h = hmac.New(sha256.New, kSigning) - h.Write([]byte(stringToSign)) - signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) - - req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", signAlgorithmHeader, opts.AccessKeyId, credentialScope, signedHeaders, signature)) - req.Header.Set("Date", headerDateStr) - return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { @@ -257,30 +152,3 @@ func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*rest return resp, nil } - -func escapeQuery(str string) string { - res := url.QueryEscape(str) - res = strings.ReplaceAll(res, "+", "%20") - return res -} - -func escapePath(path string) string { - var buf bytes.Buffer - for i := 0; i < len(path); i++ { - c := path[i] - noEscape := (c >= 'A' && c <= 'Z') || - (c >= 'a' && c <= 'z') || - (c >= '0' && c <= '9') || - c == '-' || - c == '.' || - c == '_' || - c == '~' || - c == '/' - if noEscape { - buf.WriteByte(c) - } else { - fmt.Fprintf(&buf, "%%%02X", c) - } - } - return buf.String() -} diff --git a/pkg/sdk3rd/volcengine/tos/signer.go b/pkg/sdk3rd/volcengine/tos/signer.go new file mode 100644 index 00000000..917dbcf0 --- /dev/null +++ b/pkg/sdk3rd/volcengine/tos/signer.go @@ -0,0 +1,161 @@ +package tos + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +type signer struct { + accessKeyId string + secretAccessKey string + region string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://www.volcengine.com/docs/6349/74839 + // https://docs.byteplus.com/en/docs/tos/reference-signature-mechanism_1 + + method := strings.ToUpper(req.Method) + + nowUtc := time.Now().UTC() + headerDateStr := nowUtc.Format(http.TimeFormat) + requestDateStr := nowUtc.Format("20060102T150405Z") + credentialDateStr := nowUtc.Format("20060102") + + canonicalUrl := escapePath(req.URL.Path) + if canonicalUrl == "" { + canonicalUrl = "/" + } + + canonicalQueryStr := "" + if len(req.URL.Query()) > 0 { + query := req.URL.Query() + + keys := make([]string, 0, len(query)) + for key := range query { + keys = append(keys, key) + } + sort.Strings(keys) + + for i, key := range keys { + if i > 0 { + canonicalQueryStr += "&" + } + + value := query.Get(key) + canonicalQueryStr += escapeQuery(key) + "=" + escapeQuery(value) + } + } + + canonicalHeaders := "" + signedHeaders := "" + if len(req.Header) > 0 || req.Host != "" { + if req.Header.Get("Host") == "" { + req.Header.Set("Host", req.Host) + } + if req.Header.Get("X-TOS-Date") == "" { + req.Header.Set("X-TOS-Date", requestDateStr) + } + + keys := make([]string, 0, len(req.Header)) + for key := range req.Header { + key = strings.ToLower(key) + if strings.HasPrefix(key, "x-tos-") || key == "host" { + keys = append(keys, key) + } + if key == "content-type" && req.Header.Get("X-TOS-Content-SHA256") != "" { + keys = append(keys, key) + } + } + sort.Strings(keys) + + for i, key := range keys { + if i > 0 { + canonicalHeaders += "\n" + signedHeaders += ";" + } + + value := strings.TrimSpace(req.Header.Get(key)) + canonicalHeaders += key + ":" + value + signedHeaders += key + } + + canonicalHeaders += "\n" + } + + hashedPayload := req.Header.Get("X-TOS-Content-SHA256") + if hashedPayload == "" { + temp := sha256.Sum256([]byte{}) + hashedPayload = strings.ToLower(hex.EncodeToString(temp[:])) + } + + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, canonicalUrl, canonicalQueryStr, canonicalHeaders, signedHeaders, hashedPayload) + canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) + canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) + + const signAlgorithmHeader = "TOS4-HMAC-SHA256" + credentialScope := fmt.Sprintf("%s/%s/tos/request", credentialDateStr, s.region) + stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", signAlgorithmHeader, requestDateStr, credentialScope, canonicalRequestHashHex) + + var h hash.Hash + h = hmac.New(sha256.New, []byte(s.secretAccessKey)) + h.Write([]byte(credentialDateStr)) + kDate := h.Sum(nil) + h = hmac.New(sha256.New, kDate) + h.Write([]byte(s.region)) + kRegion := h.Sum(nil) + h = hmac.New(sha256.New, kRegion) + h.Write([]byte("tos")) + kService := h.Sum(nil) + h = hmac.New(sha256.New, kService) + h.Write([]byte("request")) + kSigning := h.Sum(nil) + + h = hmac.New(sha256.New, kSigning) + h.Write([]byte(stringToSign)) + signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) + + authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", signAlgorithmHeader, s.accessKeyId, credentialScope, signedHeaders, signature) + + req.Header.Set("Authorization", authorization) + req.Header.Set("Date", headerDateStr) + + return nil +} + +func escapeQuery(str string) string { + res := url.QueryEscape(str) + res = strings.ReplaceAll(res, "+", "%20") + return res +} + +func escapePath(path string) string { + var buf bytes.Buffer + for i := 0; i < len(path); i++ { + c := path[i] + noEscape := (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + c == '-' || + c == '.' || + c == '_' || + c == '~' || + c == '/' + if noEscape { + buf.WriteByte(c) + } else { + fmt.Fprintf(&buf, "%%%02X", c) + } + } + return buf.String() +} diff --git a/pkg/sdk3rd/wangsu/openapi/client.go b/pkg/sdk3rd/wangsu/openapi/client.go index 3fa8ae4e..ce84c72d 100644 --- a/pkg/sdk3rd/wangsu/openapi/client.go +++ b/pkg/sdk3rd/wangsu/openapi/client.go @@ -1,17 +1,9 @@ package openapi import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" "encoding/json" "fmt" - "io" "net/http" - "net/url" - "strconv" - "strings" "time" "github.com/go-resty/resty/v2" @@ -36,85 +28,24 @@ func NewClient(optFns ...OptionsFunc) (*Client, error) { return nil, fmt.Errorf("sdkerr: unset secretKey") } - restyClient := resty.New(). + signer := &signer{ + accessKey: options.AccessKey, + secretKey: options.SecretKey, + } + httper := resty.New(). SetBaseURL("https://open.chinanetcenter.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", app.AppUserAgent). - SetPreRequestHook(func(c *resty.Client, req *http.Request) error { - // API 签名机制: - // https://www.wangsu.com/document/openapi/api-authentication - - method := strings.ToUpper(req.Method) - - path := "/" - if req.URL != nil { - path = req.URL.Path + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) } - queryStr := "" - if method != http.MethodPost && req.URL != nil { - queryStr = req.URL.RawQuery - - s, err := url.QueryUnescape(queryStr) - if err != nil { - return err - } - - queryStr = s - } - - canonicalHeaders := "" + - "content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" + - "host:" + strings.TrimSpace(strings.ToLower(req.Host)) + "\n" - signedHeaders := "content-type;host" - - payloadStr := "" - if method != http.MethodGet && req.Body != nil { - payloadb, err := io.ReadAll(req.Body) - if err != nil { - return err - } - - payloadStr = string(payloadb) - req.Body = io.NopCloser(bytes.NewReader(payloadb)) - } - payloadHash := sha256.Sum256([]byte(payloadStr)) - payloadHashHex := strings.ToLower(hex.EncodeToString(payloadHash[:])) - - nowUtc := time.Now().UTC() - timestampStr := req.Header.Get("X-CNC-Timestamp") - if timestampStr == "" { - timestampStr = fmt.Sprintf("%d", nowUtc.Unix()) - } else { - timestamp, err := strconv.ParseInt(timestampStr, 10, 64) - if err != nil { - return err - } - nowUtc = time.Unix(timestamp, 0).UTC() - } - - canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryStr, canonicalHeaders, signedHeaders, payloadHashHex) - canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) - canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) - - const signAlgorithmHeader = "CNC-HMAC-SHA256" - stringToSign := fmt.Sprintf("%s\n%s\n%s", signAlgorithmHeader, timestampStr, canonicalRequestHashHex) - - h := hmac.New(sha256.New, []byte(options.SecretKey)) - h.Write([]byte(stringToSign)) - signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) - - req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", signAlgorithmHeader, options.AccessKey, signedHeaders, signature)) - req.Header.Set("Date", nowUtc.Format(http.TimeFormat)) - req.Header.Set("X-CNC-Auth-Method", "AKSK") - req.Header.Set("X-CNC-AccessKey", options.AccessKey) - req.Header.Set("X-CNC-Timestamp", timestampStr) - return nil }) - return &Client{rc: restyClient}, nil + return &Client{rc: httper}, nil } func (c *Client) SetTimeout(timeout time.Duration) *Client { diff --git a/pkg/sdk3rd/wangsu/openapi/signer.go b/pkg/sdk3rd/wangsu/openapi/signer.go new file mode 100644 index 00000000..a8dc2410 --- /dev/null +++ b/pkg/sdk3rd/wangsu/openapi/signer.go @@ -0,0 +1,95 @@ +package openapi + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +type signer struct { + accessKey string + secretKey string +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://www.wangsu.com/document/openapi/api-authentication + + method := strings.ToUpper(req.Method) + + path := "/" + if req.URL != nil { + path = req.URL.Path + } + + queryStr := "" + if method != http.MethodPost && req.URL != nil { + queryStr = req.URL.RawQuery + + s, err := url.QueryUnescape(queryStr) + if err != nil { + return err + } + + queryStr = s + } + + canonicalHeaders := "" + + "content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" + + "host:" + strings.TrimSpace(strings.ToLower(req.Host)) + "\n" + signedHeaders := "content-type;host" + + payloadStr := "" + if method != http.MethodGet && req.Body != nil { + payloadb, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + payloadStr = string(payloadb) + req.Body = io.NopCloser(bytes.NewReader(payloadb)) + } + payloadHash := sha256.Sum256([]byte(payloadStr)) + payloadHashHex := strings.ToLower(hex.EncodeToString(payloadHash[:])) + + nowUtc := time.Now().UTC() + timestampStr := req.Header.Get("X-CNC-Timestamp") + if timestampStr == "" { + timestampStr = fmt.Sprintf("%d", nowUtc.Unix()) + } else { + timestamp, err := strconv.ParseInt(timestampStr, 10, 64) + if err != nil { + return err + } + nowUtc = time.Unix(timestamp, 0).UTC() + } + + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryStr, canonicalHeaders, signedHeaders, payloadHashHex) + canonicalRequestHash := sha256.Sum256([]byte(canonicalRequest)) + canonicalRequestHashHex := strings.ToLower(hex.EncodeToString(canonicalRequestHash[:])) + + const signAlgorithmHeader = "CNC-HMAC-SHA256" + stringToSign := fmt.Sprintf("%s\n%s\n%s", signAlgorithmHeader, timestampStr, canonicalRequestHashHex) + + h := hmac.New(sha256.New, []byte(s.secretKey)) + h.Write([]byte(stringToSign)) + signature := strings.ToLower(hex.EncodeToString(h.Sum(nil))) + + authorization := fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", signAlgorithmHeader, s.accessKey, signedHeaders, signature) + + req.Header.Set("Authorization", authorization) + req.Header.Set("Date", nowUtc.Format(http.TimeFormat)) + req.Header.Set("X-CNC-Auth-Method", "AKSK") + req.Header.Set("X-CNC-AccessKey", s.accessKey) + req.Header.Set("X-CNC-Timestamp", timestampStr) + + return nil +}