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 }