mirror of
https://github.com/certd/certd.git
synced 2026-04-03 14:10:54 +08:00
352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
import crypto from 'crypto';
|
||
import querystring from 'querystring'
|
||
import {HttpClient, HttpRequestConfig, ILogger} from "@certd/basic";
|
||
|
||
export class KsyunClient {
|
||
|
||
accessKeyId: string;
|
||
secretAccessKey: string;
|
||
region: string;
|
||
service: string;
|
||
endpoint: string;
|
||
logger: ILogger;
|
||
http: HttpClient
|
||
constructor(opts:{accessKeyId:string; secretAccessKey:string; region?:string; service :string;endpoint :string,logger:ILogger,http:HttpClient}) {
|
||
this.accessKeyId = opts.accessKeyId;
|
||
this.secretAccessKey = opts.secretAccessKey;
|
||
this.region = opts.region || 'cn-beijing-6';
|
||
this.service = opts.service;
|
||
this.endpoint =opts.endpoint
|
||
this.logger = opts.logger;
|
||
this.http = opts.http;
|
||
}
|
||
|
||
|
||
async doRequest(opts: {action:string;version:string} &HttpRequestConfig){
|
||
const config = this.signRequest({
|
||
method: opts.method || 'GET',
|
||
url: opts.url || '/2016-09-01/domain/GetCdnDomains',
|
||
baseURL: `https://${this.endpoint}`,
|
||
params: opts.params,
|
||
headers: {
|
||
'X-Action': opts.action,
|
||
'X-Version': opts.version
|
||
},
|
||
data: opts.data
|
||
});
|
||
|
||
try{
|
||
return await this.http.request(config)
|
||
}catch (e) {
|
||
if (e.response?.data?.Error?.Message){
|
||
throw new Error(e.response?.data?.Error?.Message)
|
||
}
|
||
throw e
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* 签名请求
|
||
* @param {Object} config Axios 请求配置
|
||
* @returns {Object} 签名后的请求配置
|
||
*/
|
||
signRequest(config) {
|
||
// 确保有必要的配置
|
||
if (!this.accessKeyId || !this.secretAccessKey) {
|
||
throw new Error('AccessKeyId and SecretAccessKey are required');
|
||
}
|
||
|
||
// 设置默认值
|
||
config.method = config.method || 'GET';
|
||
config.headers = config.headers || {};
|
||
|
||
// 获取当前时间并设置 X-Amz-Date
|
||
const requestDate = this.getRequestDate();
|
||
config.headers['x-amz-date'] = requestDate;
|
||
|
||
// 处理不同的请求方法
|
||
let canonicalQueryString = '';
|
||
let hashedPayload = this.hashPayload(config.data || '');
|
||
|
||
if (config.method.toUpperCase() === 'GET') {
|
||
// GET 请求 - 参数在 URL 中
|
||
const urlParts = config.url.split('?');
|
||
const path = urlParts[0];
|
||
const query = urlParts[1] || '';
|
||
|
||
// 合并现有查询参数和额外参数
|
||
const queryParams = {
|
||
...querystring.parse(query),
|
||
...(config.params || {})
|
||
};
|
||
|
||
// 生成规范查询字符串
|
||
canonicalQueryString = this.createCanonicalQueryString(queryParams);
|
||
config.url = `${path}?${canonicalQueryString}`;
|
||
config.params = {}; // 清空 params,因为已经合并到 URL 中
|
||
} else {
|
||
// POST/PUT 等请求 - 参数在 body 中
|
||
canonicalQueryString = '';
|
||
if (config.data && typeof config.data === 'object') {
|
||
// 如果 data 是对象,转换为 JSON 字符串
|
||
config.data = JSON.stringify(config.data);
|
||
hashedPayload = this.hashPayload(config.data);
|
||
}
|
||
}
|
||
|
||
// 生成规范请求
|
||
const canonicalRequest = this.createCanonicalRequest(
|
||
config.method,
|
||
config.url,
|
||
canonicalQueryString,
|
||
config.headers,
|
||
hashedPayload
|
||
);
|
||
|
||
// 生成签名字符串
|
||
const credentialScope = this.createCredentialScope(requestDate);
|
||
const stringToSign = this.createStringToSign(requestDate, credentialScope, canonicalRequest);
|
||
|
||
// 计算签名
|
||
const signature = this.calculateSignature(requestDate, stringToSign);
|
||
|
||
// 生成 Authorization 头
|
||
const signedHeaders = this.getSignedHeaders(config.headers);
|
||
const authorizationHeader = this.createAuthorizationHeader(
|
||
credentialScope,
|
||
signedHeaders,
|
||
signature
|
||
);
|
||
|
||
// 添加 Authorization 头
|
||
config.headers.Authorization = authorizationHeader;
|
||
|
||
return config;
|
||
}
|
||
|
||
/**
|
||
* 获取当前时间 (格式: YYYYMMDD'T'HHMMSS'Z')
|
||
* @returns {string} 格式化后的时间字符串
|
||
*/
|
||
getRequestDate() {
|
||
const now = new Date();
|
||
const year = now.getUTCFullYear();
|
||
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getUTCDate()).padStart(2, '0');
|
||
const hours = String(now.getUTCHours()).padStart(2, '0');
|
||
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
|
||
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
|
||
|
||
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
||
}
|
||
|
||
/**
|
||
* 哈希 payload
|
||
* @param {string} payload 请求体内容
|
||
* @returns {string} 哈希后的16进制字符串
|
||
*/
|
||
hashPayload(payload) {
|
||
if (typeof payload !== 'string') {
|
||
payload = '';
|
||
}
|
||
return crypto.createHash('sha256').update(payload).digest('hex').toLowerCase();
|
||
}
|
||
|
||
/**
|
||
* 创建规范查询字符串
|
||
* @param {Object} params 查询参数对象
|
||
* @returns {string} 规范化的查询字符串
|
||
*/
|
||
createCanonicalQueryString(params) {
|
||
// 对参数名和值进行 URI 编码
|
||
const encodedParams = {};
|
||
for (const key in params) {
|
||
if (params.hasOwnProperty(key)) {
|
||
const encodedKey = this.uriEncode(key);
|
||
const encodedValue = this.uriEncode(params[key].toString());
|
||
encodedParams[encodedKey] = encodedValue;
|
||
}
|
||
}
|
||
|
||
// 按 ASCII 顺序排序
|
||
const sortedKeys = Object.keys(encodedParams).sort();
|
||
|
||
// 构建查询字符串
|
||
return sortedKeys.map(key => `${key}=${encodedParams[key]}`).join('&');
|
||
}
|
||
|
||
/**
|
||
* URI 编码 (符合 AWS 规范)
|
||
* @param {string} str 要编码的字符串
|
||
* @returns {string} 编码后的字符串
|
||
*/
|
||
uriEncode(str) {
|
||
return encodeURIComponent(str)
|
||
.replace(/[^A-Za-z0-9\-_.~]/g, c =>
|
||
'%' + c.charCodeAt(0).toString(16).toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* 创建规范请求
|
||
* @param {string} method HTTP 方法
|
||
* @param {string} url 请求 URL
|
||
* @param {string} queryString 查询字符串
|
||
* @param {Object} headers 请求头
|
||
* @param {string} hashedPayload 哈希后的 payload
|
||
* @returns {string} 规范化的请求字符串
|
||
*/
|
||
createCanonicalRequest(method, url, queryString, headers, hashedPayload) {
|
||
// 获取规范 URI
|
||
const urlObj = new URL(url, 'http://dummy.com'); // 使用虚拟基础 URL 来解析路径
|
||
const canonicalUri = this.uriEncodePath(urlObj.pathname) || '/';
|
||
|
||
// 获取规范 headers 和 signed headers
|
||
const { canonicalHeaders, signedHeaders } = this.createCanonicalHeaders(headers);
|
||
|
||
return [
|
||
method.toUpperCase(),
|
||
canonicalUri,
|
||
queryString,
|
||
canonicalHeaders,
|
||
signedHeaders,
|
||
hashedPayload
|
||
].join('\n');
|
||
}
|
||
|
||
/**
|
||
* URI 编码路径部分
|
||
* @param {string} path 路径
|
||
* @returns {string} 编码后的路径
|
||
*/
|
||
uriEncodePath(path) {
|
||
// 分割路径为各个部分,分别编码
|
||
return path.split('/').map(part => this.uriEncode(part)).join('/');
|
||
}
|
||
|
||
/**
|
||
* 创建规范 headers 和 signed headers
|
||
* @param {Object} headers 原始请求头
|
||
* @returns {Object} { canonicalHeaders: string, signedHeaders: string }
|
||
*/
|
||
createCanonicalHeaders(headers) {
|
||
// 处理 headers
|
||
const headerMap:any = {};
|
||
|
||
// 标准化 headers
|
||
for (const key in headers) {
|
||
if (headers.hasOwnProperty(key)) {
|
||
const lowerKey = key.toLowerCase();
|
||
let value = headers[key]
|
||
if (value) {
|
||
value = value.toString().replace(/\s+/g, ' ').trim();
|
||
headerMap[lowerKey] = value;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 确保 host 和 x-amz-date 存在
|
||
if (!headerMap.host) {
|
||
const url = headers.host ||this.endpoint || 'cdn.api.ksyun.com'; // 默认值
|
||
headerMap.host = url.replace(/^https?:\/\//, '').split('/')[0];
|
||
}
|
||
|
||
// 按 header 名称排序
|
||
const sortedHeaderNames = Object.keys(headerMap).sort();
|
||
|
||
// 构建规范 headers
|
||
let canonicalHeaders = '';
|
||
for (const name of sortedHeaderNames) {
|
||
canonicalHeaders += `${name}:${headerMap[name]}\n`;
|
||
}
|
||
|
||
// 构建 signed headers
|
||
const signedHeaders = sortedHeaderNames.join(';');
|
||
|
||
return { canonicalHeaders, signedHeaders };
|
||
}
|
||
|
||
/**
|
||
* 获取 signed headers
|
||
* @param {Object} headers 请求头
|
||
* @returns {string} signed headers 字符串
|
||
*/
|
||
getSignedHeaders(headers) {
|
||
const { signedHeaders } = this.createCanonicalHeaders(headers);
|
||
return signedHeaders;
|
||
}
|
||
|
||
/**
|
||
* 创建信任状范围
|
||
* @param {string} requestDate 请求日期 (YYYYMMDDTHHMMSSZ)
|
||
* @returns {string} 信任状范围字符串
|
||
*/
|
||
createCredentialScope(requestDate) {
|
||
const date = requestDate.split('T')[0];
|
||
return `${date}/${this.region}/${this.service}/aws4_request`;
|
||
}
|
||
|
||
/**
|
||
* 创建签名字符串
|
||
* @param {string} requestDate 请求日期
|
||
* @param {string} credentialScope 信任状范围
|
||
* @param {string} canonicalRequest 规范请求
|
||
* @returns {string} 签名字符串
|
||
*/
|
||
createStringToSign(requestDate, credentialScope, canonicalRequest) {
|
||
const algorithm = 'AWS4-HMAC-SHA256';
|
||
const hashedCanonicalRequest = crypto.createHash('sha256')
|
||
.update(canonicalRequest)
|
||
.digest('hex')
|
||
.toLowerCase();
|
||
|
||
return [
|
||
algorithm,
|
||
requestDate,
|
||
credentialScope,
|
||
hashedCanonicalRequest
|
||
].join('\n');
|
||
}
|
||
|
||
/**
|
||
* 计算签名
|
||
* @param {string} requestDate 请求日期
|
||
* @param {string} stringToSign 签名字符串
|
||
* @returns {string} 签名值
|
||
*/
|
||
calculateSignature(requestDate, stringToSign) {
|
||
const date = requestDate.split('T')[0];
|
||
const kDate = this.hmac(`AWS4${this.secretAccessKey}`, date);
|
||
const kRegion = this.hmac(kDate, this.region);
|
||
const kService = this.hmac(kRegion, this.service);
|
||
const kSigning = this.hmac(kService, 'aws4_request');
|
||
|
||
return this.hmac(kSigning, stringToSign, 'hex');
|
||
}
|
||
|
||
/**
|
||
* HMAC-SHA256 计算
|
||
* @param {string|Buffer} key 密钥
|
||
* @param {string} data 数据
|
||
* @param {string} [encoding] 输出编码
|
||
* @returns {string|Buffer} HMAC 结果
|
||
*/
|
||
hmac(key, data, encoding = null) {
|
||
const hmac = crypto.createHmac('sha256', key);
|
||
hmac.update(data);
|
||
return encoding ? hmac.digest(encoding) : hmac.digest();
|
||
}
|
||
|
||
/**
|
||
* 创建 Authorization 头
|
||
* @param {string} credentialScope 信任状范围
|
||
* @param {string} signedHeaders signed headers
|
||
* @param {string} signature 签名值
|
||
* @returns {string} Authorization 头值
|
||
*/
|
||
createAuthorizationHeader(credentialScope, signedHeaders, signature) {
|
||
return `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
||
}
|
||
}
|
||
|