Files
ddns-agent/agent/ddns/volcengine/sign.ts
2025-11-14 20:43:59 +08:00

251 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const Service = "DNS";
const Version = "2018-08-01";
const Region = "cn-north-1";
const Host = "dns.volcengineapi.com";
interface Credential {
access_key_id: string;
secret_access_key: string;
service: string;
region: string;
}
interface RequestParam {
body: string;
host: string;
path: string;
method: string;
content_type: string;
date: Date;
query: Record<string, any>;
}
interface SignResult {
Host: string;
'X-Content-Sha256': string;
'X-Date': string;
'Content-Type': string;
Authorization?: string;
}
/**
* 字符串转ArrayBuffer
*/
function stringToArrayBuffer(str: string): ArrayBuffer {
const encoder = new TextEncoder();
return encoder.encode(str).buffer;
}
/**
* ArrayBuffer转十六进制字符串
*/
function arrayBufferToHex(buffer: ArrayBuffer): string {
const hashArray = Array.from(new Uint8Array(buffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
/**
* URL编码函数类似Python的quote函数
*/
function encodeURIComponentSafe(str: string): string {
return encodeURIComponent(str)
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
.replace(/%20/g, '%20');
}
/**
* 规范化查询参数
*/
function normQuery(params: Record<string, any>): string {
let query = "";
const sortedKeys = Object.keys(params).sort();
for (const key of sortedKeys) {
if (Array.isArray(params[key])) {
for (const k of params[key]) {
query += encodeURIComponentSafe(key) + "=" + encodeURIComponentSafe(k) + "&";
}
} else {
query += encodeURIComponentSafe(key) + "=" + encodeURIComponentSafe(params[key]) + "&";
}
}
query = query.slice(0, -1);
return query.replace(/\+/g, "%20");
}
/**
* HMAC-SHA256 加密 (使用Web Crypto API)
*/
async function hmacSha256(key: ArrayBuffer, content: string): Promise<ArrayBuffer> {
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
return crypto.subtle.sign('HMAC', cryptoKey, stringToArrayBuffer(content));
}
/**
* SHA256 哈希算法 (使用Web Crypto API)
*/
async function hashSha256(content: string): Promise<string> {
const hashBuffer = await crypto.subtle.digest('SHA-256', stringToArrayBuffer(content));
return arrayBufferToHex(hashBuffer);
}
/**
* 获取签名
*/
export async function getSignature(
method: string,
query: Record<string, any>,
header: Record<string, string>,
ak: string,
sk: string,
action: string,
body: any
): Promise<SignResult> {
const credential: Credential = {
access_key_id: ak,
secret_access_key: sk,
service: Service,
region: Region,
};
const fullQuery = { Action: action, Version: Version, ...query };
const sortedQuery: Record<string, any> = {};
Object.keys(fullQuery).sort().forEach(key => {
sortedQuery[key] = (fullQuery as any)[key];
});
const requestParam: RequestParam = {
body: "",
host: Host,
path: "/",
method: method,
content_type: "application/json",
date: new Date(),
query: sortedQuery,
};
if (method === "POST") {
requestParam.body = JSON.stringify(body);
}
const xDate = requestParam.date.toISOString().replace(/[:\-]|\.\d{3}/g, '').slice(0, -1) + 'Z';
const shortXDate = xDate.slice(0, 8);
const xContentSha256 = await hashSha256(requestParam.body);
const signResult: SignResult = {
Host: requestParam.host,
'X-Content-Sha256': xContentSha256,
'X-Date': xDate,
'Content-Type': requestParam.content_type,
};
const signedHeadersStr = ["content-type", "host", "x-content-sha256", "x-date"].join(";");
const canonicalRequestStr = [
requestParam.method,
requestParam.path,
normQuery(requestParam.query),
[
"content-type:" + requestParam.content_type,
"host:" + requestParam.host,
"x-content-sha256:" + xContentSha256,
"x-date:" + xDate,
].join("\n"),
"",
signedHeadersStr,
xContentSha256,
].join("\n");
const hashedCanonicalRequest = await hashSha256(canonicalRequestStr);
const credentialScope = [shortXDate, credential.region, credential.service, "request"].join("/");
const stringToSign = ["HMAC-SHA256", xDate, credentialScope, hashedCanonicalRequest].join("\n");
const secretKeyBuffer = stringToArrayBuffer(credential.secret_access_key);
const kDate = await hmacSha256(secretKeyBuffer, shortXDate);
const kRegion = await hmacSha256(kDate, credential.region);
const kService = await hmacSha256(kRegion, credential.service);
const kSigning = await hmacSha256(kService, "request");
const signatureBuffer = await hmacSha256(kSigning, stringToSign);
const signature = arrayBufferToHex(signatureBuffer);
signResult.Authorization = `HMAC-SHA256 Credential=${credential.access_key_id}/${credentialScope}, SignedHeaders=${signedHeadersStr}, Signature=${signature}`;
return signResult;
}
/**
* 发送API请求
*/
export async function request(
method: string,
query: Record<string, any>,
header: Record<string, string>,
ak: string,
sk: string,
action: string,
body: any
): Promise<any> {
const signResult = await getSignature(method, query, header, ak, sk, action, body);
const finalHeader = { ...header, ...signResult };
const requestParam: RequestParam = {
body: method === "POST" ? JSON.stringify(body) : "",
host: Host,
path: "/",
method: method,
content_type: "application/json",
date: new Date(),
query: { Action: action, Version: Version, ...query },
};
const url = new URL(`https://${requestParam.host}${requestParam.path}`);
Object.keys(requestParam.query).forEach(key => {
url.searchParams.append(key, requestParam.query[key]);
});
const fetchOptions: RequestInit = {
method: method,
headers: finalHeader,
};
if (method === "POST") {
fetchOptions.body = requestParam.body;
}
try {
const response = await fetch(url.toString(), fetchOptions);
return await response.json();
} catch (error) {
throw new Error(`Request failed: ${error}`);
}
}
/**
* 使用示例
*/
export async function example(ak: string, sk: string) {
try {
// POST 请求示例 - 调用 UpdateZone API
const requestBody = {
ZID: 100,
Remark: "example",
};
const updateZoneResult = await request("POST", {}, {}, ak, sk, "UpdateZone", requestBody);
console.log(updateZoneResult);
// GET 请求示例 - 调用 CheckZone API
const requestQuery = { ZoneName: "example.com" };
const checkZoneResult = await request("GET", requestQuery, {}, ak, sk, "CheckZone", {});
console.log(checkZoneResult);
} catch (error) {
console.error('API request failed:', error);
}
}