跳转至

对外开放接入文档

云控制台支持外部直接调用API的使用方式,具体步骤为:

  1. 创建秘钥对
  2. 签名请求并调用

被签名的内容

  1. pathname
  2. method
  3. content-type(如果有)
  4. queries
  5. body(如果有)
  6. custom headers(x-ty-*)
  7. current timestamp(取值顺序:custom header(x-ty-timestamp) > query(timestamp)> header(date))
  8. signature version

签名过程

  1. 将 queries 按 key 的 ascii 顺序排序。双/多字节字符仍然按照单字节排序;key 与 value 分别做escape 转换(注1),再以 &key=value 形式拼接
  2. custom headers 使用 encodeURIComponent 转换后使用 &key=value 形式拼接
  3. 其他的 value 均使用 encodeURIComponent 转换
  4. body buffer 取 sha256 (注2)
  5. 将以上内容使用换行符 \n 连接后取 HMAC-SHA256 (注2)

注:

  1. escape不转义字符为 A-Z a-z 0-9 - _ . ~
  2. hash 全部为 hex 小写格式

一个标准的请求应包含:

  • 必要字段
    • (header) Authorization 或 (query) signature
    • (header) x-ty-accesskey 或 (query) accesskey
    • (header) x-ty-timestamp 或 (query) timestamp
    • (header) x-ty-signature-version 或 (query) signatureVersion
  • (header) content-type,非 GET 时必须设置,一般为application/json
  • body(如果有)
  • queries(如果有)

NodeJS签名示例

const got = require("got").default;
const { createHmac, createHash } = require("crypto");

var endpoint = "endpoint";
var accessKey = "accessKey";
var accessSecret = "secretKey";

const signatureVersion = "2.1";

async function call(pathname, method = "GET", query = {}, json = {}) {
  const contentType = "application/json";
  const requestTime = Date.now(); // 请求时间戳,单位为毫秒
  console.log("requestTime:", requestTime);
  const headers = {
    "x-ty-timestamp": requestTime,
    "x-ty-accesskey": accessKey,
    "x-ty-signature-version": signatureVersion,
    "content-type": contentType,
  };
  const customHeaders = {};
  for (const headerKey of Object.keys(headers)) {
    if (headerKey.startsWith("x-ty-")) {
      customHeaders[headerKey] = headers[headerKey];
    }
  }
  const orderedCustomHeaderString = keyPairsToOrderedString(customHeaders);
  const orderedQueryString = keyPairsToOrderedString(query);
  let body_hash = "";
  if (method.toUpperCase() !== "GET" && Object.keys(json).length > 0) {
    body_hash = createHash("sha256")
      .update(JSON.stringify(json))
      .digest("hex");
  }
  let signaturedString = "";
  if (body_hash === "") {
    signaturedString = [
      fixedEncodeURIComponent(pathname),
      fixedEncodeURIComponent(method),
      fixedEncodeURIComponent(contentType),
      orderedCustomHeaderString,
      orderedQueryString,
      requestTime,
      accessKey,
      signatureVersion,
    ].join("\n");
  } else {
    signaturedString = [
      fixedEncodeURIComponent(pathname),
      fixedEncodeURIComponent(method),
      fixedEncodeURIComponent(contentType),
      orderedCustomHeaderString,
      orderedQueryString,
      body_hash,
      requestTime,
      accessKey,
      signatureVersion,
    ].join("\n");
  }

  const targetSignature = createHmac("sha256", accessSecret)
    .update(signaturedString)
    .digest("hex");
  console.log("targetSignature:", targetSignature);
  const ret = await got(`${endpoint}${pathname}`, {
    method,
    headers: {
      ...headers,
      Authorization: targetSignature,
    },
    throwHttpErrors: false,
    searchParams: query,
    json: (method.toUpperCase() !== "GET" && Object.keys(json).length > 0) ? json : undefined,
  });
  console.log(ret.body);
}

function keyPairsToOrderedString(keyPairs) {
  return Object.keys(keyPairs)
    .sort()
    .map(
      (key) =>
        `${fixedEncodeURIComponent(key)}=${fixedEncodeURIComponent(
          String(keyPairs[key] || "")
        )}`
    )
    .join("&");
}

function fixedEncodeURIComponent(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
    return "%" + c.charCodeAt(0).toString(16);
  });
}

// 创建实例
// call(
//   "/v1/domains",
//   "POST",
//   {},
//   {
//     "name": "demo1",
//     "memory_gb": 8,
//     "cpu_count": 8,
//     "system_volume": {
//         "capacity": 40
//     },
//     "data_volumes": [
//         {
//             "capacity": 100,
//         },
//         {
//             "capacity": 200,
//         }
//     ],
//     "image_id": 1,
//     "count": 1,
//     "datacenter_id": 43
// }
// );

// 重启实例
// call("/v1/domains/2991/reboot", "PUT", {}, {});

// 查看实例
// call("/v1/domains", "GET", {}, {});

//删除实例
// call("/v1/domains/5473", "DELETE", { delete_volumes: "all" });

// 在指定服务器上创建存储卷
// call("/v1/storages/volumes", "POST", {}, {
//   "server_id": 3039,
//   "name": "test-2020219.qcow2",
//   "format": "qcow2",
//   "capacity": 400
// })

// 在指定数据中心创建存储卷
// call("/v1/storages/volumes", "POST", {}, {
//   "datacenter_id": 43,
//   "name": "game-base-v1.qcow2",
//   "storage_pool": "games",
//   "format": "qcow2",
//   "capacity": 400
// })

Golang签名示例

package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "net/url"
    "sort"
    "strconv"
    "strings"
    "time"
)

const (
    endpoint           = "endpoint"
    accessKey          = "accessKey"
    accessSecret       = "secretKey"
    signatureVersion   = "2.1"
    defaultContentType = "application/json"
)

// VirtualMachine 创建vm参数
type VirtualMachine struct {
    Name string `json:"name,omitempty"`
    Count int `json:"count,omitempty"`
    MemoryGB int64 `json:"memory_gb,omitempty"`
    CPUCount int32 `json:"cpu_count,omitempty"`
    GPUModel string `json:"gpu_model,omitempty"`
    GPUCount int8 `json:"gpu_count,omitempty"`
    ImageID uint64 `json:"image_id,omitempty"`
    DatacenterID uint64 `json:"datacenter_id"`
}

func main() {
    // Example usage:
    path := "/v1/domains"
    method := "POST"
    vm := VirtualMachine{
        Name:         "demo1",
        Count:        1,
        MemoryGB:     8,
        CPUCount:     8,
        ImageID:      1,
        DatacenterID: 43,
    }
    bodyBytes, err := json.Marshal(&vm)
    if err != nil {
        log.Fatal(err)
    }

    resp, err := call(path, method, nil, bodyBytes)
    if err != nil {
        log.Fatalln("call api failed:", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error reading response: %v\n", err)
        return
    }

    fmt.Printf("Response status: %s\n", resp.Status)
    fmt.Printf("Response body: %s\n", body)
}

func call(pathname, method string, query map[string]string, jsonBody []byte) (*http.Response, error) {
    requestTime := time.Now().UnixMilli()
    contentType := defaultContentType

    // Prepare headers
    headers := map[string]string{
        "x-ty-timestamp":         strconv.FormatInt(requestTime, 10),
        "x-ty-accesskey":         accessKey,
        "x-ty-signature-version": signatureVersion,
        "content-type":           contentType,
    }

    // Extract custom headers (starting with x-ty-)
    customHeaders := make(map[string]string)
    for k, v := range headers {
        if strings.HasPrefix(k, "x-ty-") {
            customHeaders[k] = v
        }
    }

    // Generate ordered strings for signature
    orderedCustomHeaderString := keyPairsToOrderedString(customHeaders)
    orderedQueryString := keyPairsToOrderedString(query)

    // For GET requests, we don't include a body hash
    bodyHash := ""
    if signatureVersion == "2.1" && len(jsonBody) > 0 {
        bodyHash = fmt.Sprintf("%x", sha256.Sum256(jsonBody))
    }

    // Create the signature string
    var signatureString string
    if bodyHash != "" {
        signatureString = strings.Join([]string{
            fixedEncodeURIComponent(pathname),
            fixedEncodeURIComponent(method),
            fixedEncodeURIComponent(contentType),
            orderedCustomHeaderString,
            orderedQueryString,
            bodyHash,
            strconv.FormatInt(requestTime, 10),
            accessKey,
            signatureVersion,
        }, "\n")
    } else {
        signatureString = strings.Join([]string{
            fixedEncodeURIComponent(pathname),
            fixedEncodeURIComponent(method),
            fixedEncodeURIComponent(contentType),
            orderedCustomHeaderString,
            orderedQueryString,
            strconv.FormatInt(requestTime, 10),
            accessKey,
            signatureVersion,
        }, "\n")
    }

    // Calculate HMAC-SHA256 signature
    signature := calculateHMAC(signatureString, accessSecret)

    // Create the request URL with query parameters
    reqURL, err := url.Parse(endpoint + pathname)
    if err != nil {
        return nil, fmt.Errorf("failed to parse URL: %v", err)
    }

    // Add query parameters
    q := reqURL.Query()
    for k, v := range query {
        q.Add(k, v)
    }
    reqURL.RawQuery = q.Encode()

    // Create the request
    var reqBody io.Reader
    if method != "GET" && len(jsonBody) > 0 {
        // In a real implementation, you would marshal the jsonBody to JSON
        // For this example, we'll just use a placeholder
        reqBody = bytes.NewReader(jsonBody) // Simplified for example
    }

    req, err := http.NewRequest(method, reqURL.String(), reqBody)
    if err != nil {
        return nil, fmt.Errorf("failed to create request: %v", err)
    }

    // Set headers
    for k, v := range headers {
        req.Header.Set(k, v)
    }
    req.Header.Set("Authorization", signature)

    // Make the request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("request failed: %v", err)
    }

    return resp, nil
}

func keyPairsToOrderedString(keyPairs map[string]string) string {
    // Get sorted keys
    keys := make([]string, 0, len(keyPairs))
    for k := range keyPairs {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    // Build key-value pairs
    var pairs []string
    for _, k := range keys {
        pairs = append(pairs, fixedEncodeURIComponent(k)+"="+fixedEncodeURIComponent(keyPairs[k]))
    }

    return strings.Join(pairs, "&")
}

func fixedEncodeURIComponent(s string) string {
    // This is a simplified version. For a complete implementation,
    // you might want to use url.PathEscape or url.QueryEscape
    // depending on the context, or implement the exact JavaScript behavior.
    return strings.ReplaceAll(url.QueryEscape(s), "+", "%20")
}

func calculateHMAC(message, secret string) string {
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(message))
    return hex.EncodeToString(h.Sum(nil))
}