mirror of
https://github.com/certd/certd.git
synced 2026-04-14 20:40:53 +08:00
perf: 支持部署到金山云CDN
This commit is contained in:
351
packages/ui/certd-server/src/plugins/plugin-ksyun/client.ts
Normal file
351
packages/ui/certd-server/src/plugins/plugin-ksyun/client.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user