对外开放接入文档
云控制台支持外部直接调用API的使用方式,具体步骤为:
- 创建秘钥对
- 签名请求并调用
被签名的内容
- pathname
- method
- content-type(如果有)
- queries
- body(如果有)
- custom headers(x-ty-*)
- current timestamp(取值顺序:custom header(x-ty-timestamp) > query(timestamp)> header(date))
- signature version
签名过程
- 将 queries 按 key 的 ascii 顺序排序。双/多字节字符仍然按照单字节排序;key 与 value 分别做escape 转换(注1),再以 &key=value 形式拼接
- custom headers 使用 encodeURIComponent 转换后使用 &key=value 形式拼接
- 其他的 value 均使用 encodeURIComponent 转换
- body buffer 取 sha256 (注2)
- 将以上内容使用换行符 \n 连接后取 HMAC-SHA256 (注2)
注:
- escape不转义字符为 A-Z a-z 0-9 - _ . ~
- 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))
}