Files
certd/packages/ui/certd-server/src/plugins/plugin-asiaisp/client.ts
T
xiaojunnuo eeb83f9024 chore(cert): add md5 hash naming and duplicate cert handling
1. 新增buildCertName方法的useHash参数,使用域名列表MD5哈希作为证书名后缀避免时间戳重复
2. 为asiaisp上传证书添加重复证书检测逻辑,已存在时直接复用已有证书
2026-06-25 23:12:38 +08:00

259 lines
7.1 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.
import { HttpClient, ILogger } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert";
import * as crypto from "crypto";
const BASE_URL = "https://api.asia-isp.com";
const URI = "/openapi/v3/stat";
export interface AsiaIspConfig {
accessKeyId: string;
accessKeySecret: string;
http: HttpClient;
logger: ILogger;
}
export interface AsiaIspDomain {
domain: string;
serviceType: string;
scope: string;
protocol: string;
certId: number;
cname: string;
originHost: string;
originAddr: string;
originProtocol: string;
originType: string;
domainStatus: number;
operatingStatus: number;
}
/**
* 橙域网络(asia-isp) CDN API 客户端
* 签名算法参照 Python 参考实现:
* message = accessKeySecret={sk}&[body={json}]&method={method}&nonce={nonce}&queryString={qs}&timestamp={ts}&uri={uri}
* signature = URL-safe-Base64(HMAC-SHA1(sk, message))
*/
export class AsiaIspClient {
private config: AsiaIspConfig;
private http: HttpClient;
private logger: ILogger;
constructor(config: AsiaIspConfig) {
this.config = config;
this.http = config.http;
this.logger = config.logger;
if (!this.config.accessKeyId) {
throw new Error("accessKeyId 不能为空");
}
if (!this.config.accessKeySecret) {
throw new Error("accessKeySecret 不能为空");
}
}
/**
* 生成 HMAC-SHA1 签名,结果做 URL-safe Base64(替换 + → -/ → _
*/
private buildSignature(opts: {
body?: any;
method: string;
nonce: string;
queryString: string;
timestamp: string;
}): string {
const { body, method, nonce, queryString, timestamp } = opts;
const sk = this.config.accessKeySecret;
const pieces: string[] = [`accessKeySecret=${sk}`];
// body 仅在 POST/PUT 时存在,对应 Python 的 body is not None
if (body !== undefined && body !== null) {
pieces.push(`body=${JSON.stringify(body)}`);
}
pieces.push(
`method=${method}`,
`nonce=${nonce}`,
`queryString=${queryString}`,
`timestamp=${timestamp}`,
`uri=${URI}`
);
const message = pieces.join("&");
const hmac = crypto.createHmac("sha1", sk).update(message).digest("base64");
// URL-safe Base64
return hmac.replace(/\+/g, "-").replace(/\//g, "_");
}
/**
* 通用 API 请求(完全对齐 Python 实现)
*/
async doRequest(req: {
method: string;
action: string;
data?: any;
}): Promise<any> {
const { method, action, data } = req;
const nonce = String(Math.floor(Math.random() * 90000000) + 10000000);
const timestamp = Date.now().toString();
const queryString = `action=${action}`;
const signature = this.buildSignature({
body: method === "POST" || method === "PUT" ? data : undefined,
method,
nonce,
queryString,
timestamp,
});
const headers: Record<string, string> = {
"Content-Type": "application/json",
accessKeyId: this.config.accessKeyId,
nonce,
timestamp,
signature,
};
const url = `${BASE_URL}${URI}?${queryString}`;
try {
const response = await this.http.request({
url,
method,
headers,
data: method === "POST" || method === "PUT" ? data : undefined,
skipSslVerify: true,
logParams: false,
logRes: false,
logData: false,
});
if (response.code !== "0") {
this.logger.error(`接口请求失败: code=${response.code}, msg=${response.msg}`);
throw new Error(response.msg || "接口请求失败");
}
return response;
} catch (error: any) {
if (error.message && !error.message.includes("接口请求失败")) {
this.logger.error(`接口请求异常: ${error.message}`);
throw new Error(`接口请求异常: ${error.message}`);
}
throw error;
}
}
/**
* 获取域名列表
* GET /openapi/v3/stat?action=domainQueryList
*/
async getDomainList(): Promise<AsiaIspDomain[]> {
const res = await this.doRequest({
method: "GET",
action: "domainQueryList",
});
const list = res.data || [];
this.logger.info(`获取域名列表成功,共 ${list.length} 个域名`);
return list;
}
/**
* 获取证书列表
* GET /openapi/v3/stat?action=certificateQueryList
*/
async getCertList(): Promise<Array<{ certId: string; name: string }>> {
const res = await this.doRequest({
method: "GET",
action: "certificateQueryList",
});
return res.data || [];
}
/**
* 查询证书详情
* GET /openapi/v3/stat?action=certificateQuery&certId=xxx
*/
async getCertDetail(certId: number): Promise<any> {
const res = await this.doRequest({
method: "GET",
action: `certificateQuery&certId=${certId}`,
});
return res.data;
}
/**
* 上传证书
* POST /openapi/v3/stat?action=certificateUpload
* 返回证书ID
*/
async uploadCert(req: { cert: CertInfo; name?: string }): Promise<number> {
const certReader = new CertReader(req.cert);
const certName = req.name || certReader.buildCertName("", true);
try {
const res = await this.doRequest({
method: "POST",
action: "certificateUpload",
data: {
name: certName,
publicKey: req.cert.crt,
privateKey: req.cert.key,
},
});
const certId = res.data;
this.logger.info(`上传证书成功,证书ID: ${certId}`);
return certId;
} catch (e: any) {
const msg = e.message || "";
const isExists = msg.includes("Certificate already exists") || e.code ==='80003' ||
msg.includes("Certificate note name already exists") || e.code ==='80010'
//返回数据: {"code":"80010","msg":"Certificate note name already exists","data":null}
if (!isExists) {
throw e;
}
this.logger.info(`证书已存在,按名称查找: ${certName}`);
const list = await this.getCertList();
const found = list.find((item: any) => item.name === certName);
if (!found) {
throw new Error(`证书已存在但无法查询到: ${certName}`);
}
const certId = Number(found.certId);
this.logger.info(`复用已有证书,证书ID: ${certId}`);
return certId;
}
}
/**
* 删除证书
* DELETE /openapi/v3/stat?action=certificateDelete&certId=xxx
*/
async deleteCert(certId: number): Promise<void> {
await this.doRequest({
method: "DELETE",
action: `certificateDelete&certId=${certId}`,
});
this.logger.info(`删除证书成功,证书ID: ${certId}`);
}
/**
* 部署证书到 CDN 域名(修改域名配置,绑定证书)
* PUT /openapi/v3/stat?action=domainModify
*/
async deployCertToDomain(req: {
domain: string;
certId: number;
protocol: string;
}): Promise<void> {
await this.doRequest({
method: "PUT",
action: "domainModify",
data: {
domain: req.domain,
certId: `${req.certId}`,
protocol: req.protocol || "https",
},
});
this.logger.info(`部署证书到域名成功: ${req.domain}, certId=${req.certId}`);
}
}