258 lines
7.0 KiB
Go

package unicloud
import (
"crypto/hmac"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
username string
password string
serverlessJwtToken string
serverlessJwtTokenExp time.Time
serverlessJwtTokenMtx sync.Mutex
serverlessClient *resty.Client
apiUserToken string
apiUserTokenMtx sync.Mutex
apiClient *resty.Client
}
const (
uniIdentityEndpoint = "https://account.dcloud.net.cn/client"
uniIdentityClientSecret = "ba461799-fde8-429f-8cc4-4b6d306e2339"
uniIdentityAppId = "__UNI__uniid_server"
uniIdentitySpaceId = "uni-id-server"
uniConsoleEndpoint = "https://unicloud.dcloud.net.cn/client"
uniConsoleClientSecret = "4c1f7fbf-c732-42b0-ab10-4634a8bbe834"
uniConsoleAppId = "__UNI__unicloud_console"
uniConsoleSpaceId = "dc-6nfabcn6ada8d3dd"
)
func NewClient(username, password string) *Client {
client := &Client{
username: username,
password: password,
}
client.serverlessClient = resty.New()
client.apiClient = resty.New().
SetBaseURL("https://unicloud-api.dcloud.net.cn/unicloud/api").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
if client.apiUserToken != "" {
req.Header.Set("Token", client.apiUserToken)
}
return nil
})
return client
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.serverlessClient.SetTimeout(timeout)
return c
}
func (c *Client) generateSignature(params map[string]any, secret string) string {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
canonicalStr := ""
for i, k := range keys {
if i > 0 {
canonicalStr += "&"
}
canonicalStr += k + "=" + fmt.Sprintf("%v", params[k])
}
mac := hmac.New(md5.New, []byte(secret))
mac.Write([]byte(canonicalStr))
sign := mac.Sum(nil)
signHex := hex.EncodeToString(sign)
return signHex
}
func (c *Client) buildServerlessClientInfo(appId string) (_clientInfo map[string]any, _err error) {
return map[string]any{
"PLATFORM": "web",
"OS": strings.ToUpper(runtime.GOOS),
"APPID": appId,
"DEVICEID": "certimate",
"LOCALE": "zh-Hans",
"osName": runtime.GOOS,
"appId": appId,
"appName": "uniCloud",
"deviceId": "certimate",
"deviceType": "pc",
"uniPlatform": "web",
"uniCompilerVersion": "4.45",
"uniRuntimeVersion": "4.45",
}, nil
}
func (c *Client) buildServerlessPayloadInfo(appId, spaceId, target, method, action string, params, data interface{}) (map[string]any, error) {
clientInfo, err := c.buildServerlessClientInfo(appId)
if err != nil {
return nil, err
}
functionArgsParams := make([]any, 0)
if params != nil {
functionArgsParams = append(functionArgsParams, params)
}
functionArgs := map[string]any{
"clientInfo": clientInfo,
"uniIdToken": c.serverlessJwtToken,
}
if method != "" {
functionArgs["method"] = method
functionArgs["params"] = make([]any, 0)
}
if action != "" {
type _obj struct{}
functionArgs["action"] = action
functionArgs["data"] = &_obj{}
}
if params != nil {
functionArgs["params"] = []any{params}
}
if data != nil {
functionArgs["data"] = data
}
jsonb, err := json.Marshal(map[string]any{
"functionTarget": target,
"functionArgs": functionArgs,
})
if err != nil {
return nil, err
}
payload := map[string]any{
"method": "serverless.function.runtime.invoke",
"params": string(jsonb),
"spaceId": spaceId,
"timestamp": time.Now().UnixMilli(),
}
return payload, nil
}
func (c *Client) invokeServerless(endpoint, clientSecret, appId, spaceId, target, method, action string, params, data interface{}) (*resty.Response, error) {
if endpoint == "" {
return nil, fmt.Errorf("unicloud api error: endpoint cannot be empty")
}
payload, err := c.buildServerlessPayloadInfo(appId, spaceId, target, method, action, params, data)
if err != nil {
return nil, fmt.Errorf("unicloud api error: failed to build request: %w", err)
}
clientInfo, _ := c.buildServerlessClientInfo(appId)
clientInfoJsonb, _ := json.Marshal(clientInfo)
sign := c.generateSignature(payload, clientSecret)
req := c.serverlessClient.R().
SetHeader("Origin", "https://unicloud.dcloud.net.cn").
SetHeader("Referer", "https://unicloud.dcloud.net.cn").
SetHeader("Content-Type", "application/json").
SetHeader("X-Client-Info", string(clientInfoJsonb)).
SetHeader("X-Client-Token", c.serverlessJwtToken).
SetHeader("X-Serverless-Sign", sign).
SetBody(payload)
resp, err := req.Post(endpoint)
if err != nil {
return resp, fmt.Errorf("unicloud api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("unicloud api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) invokeServerlessWithResult(endpoint, clientSecret, appId, spaceId, target, method, action string, params, data interface{}, result BaseResponse) error {
resp, err := c.invokeServerless(endpoint, clientSecret, appId, spaceId, target, method, action, params, data)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
}
return err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("unicloud api error: failed to unmarshal response: %w", err)
} else if success := result.GetSuccess(); !success {
return fmt.Errorf("unicloud api error: code='%s', message='%s'", result.GetErrorCode(), result.GetErrorMessage())
}
return nil
}
func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) {
req := c.apiClient.R()
if strings.EqualFold(method, http.MethodGet) {
qs := make(map[string]string)
if params != nil {
temp := make(map[string]any)
jsonb, _ := json.Marshal(params)
json.Unmarshal(jsonb, &temp)
for k, v := range temp {
if v != nil {
qs[k] = fmt.Sprintf("%v", v)
}
}
}
req = req.SetQueryParams(qs)
} else {
req = req.SetHeader("Content-Type", "application/json").SetBody(params)
}
resp, err := req.Execute(method, path)
if err != nil {
return resp, fmt.Errorf("unicloud api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("unicloud api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error {
resp, err := c.sendRequest(method, path, params)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
}
return err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("unicloud api error: failed to unmarshal response: %w", err)
} else if retcode := result.GetReturnCode(); retcode != 0 {
return fmt.Errorf("unicloud api error: ret='%d', desc='%s'", retcode, result.GetReturnDesc())
}
return nil
}