perf: 新增橙域网络(asia-isp) CDN证书部署插件

This commit is contained in:
xiaojunnuo
2026-06-25 22:53:22 +08:00
parent 095791cdc2
commit b48831e60b
9 changed files with 458 additions and 31 deletions
+7 -1
View File
@@ -211,6 +211,9 @@ export class DemoTest extends AbstractTaskPlugin {
//当以下参数变化时,触发获取选项
watches: ['certDomains', 'accessId'],
required: true,
single: false, // 是否单选
pager: true, // 是否卡其分页查询
search: true, // 是否开启搜索
})
)
siteName!: string | string[];
@@ -260,9 +263,12 @@ export class DemoTest extends AbstractTaskPlugin {
throw new Error('请选择Access授权');
}
const pager = new Pager(req);
// @ts-ignore
const access = await this.getAccess(this.accessId);
//
// 根据接口情况是否支持翻页查询,和关键字查询, 传递对应的参数,pager.pageNo,pager.pageSize, req.searchKey
// const siteRes = await access.GetDomainList(req);
//以下是模拟数据
const siteRes = [
-1
View File
@@ -41,7 +41,6 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"esmock": "^2.7.5",
"jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.6.0",
"nock": "^13.5.4",
"prettier": "3.3.3",
+2 -1
View File
@@ -25,7 +25,8 @@
"@certd/pipeline": "^1.41.4",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"psl": "^1.15.0"
"psl": "^1.15.0",
"punycode.js": "^2.3.1"
},
"devDependencies": {
"rimraf": "^5.0.5",
+26 -26
View File
@@ -115,9 +115,32 @@
"uuid": "^10.0.0",
"wechatpay-node-v3": "^2.2.1",
"whoiser": "2.0.0-beta.10",
"xml2js": "^0.6.2"
"xml2js": "^0.6.2",
"mwtsc": "^1.15.1"
},
"lazyDependencies": {
"devDependencies": {
"mwts": "^1.3.0",
"@midwayjs/mock": "3.20.11",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^4.0.6",
"@types/jest": "^29.5.13",
"@types/koa": "2.15.0",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2",
"why-is-node-running": "^3.2.2"
},
"lazyDependencies": {
"@alicloud/fc20230330": "^4.1.7",
"@alicloud/tea-typescript": "^1.8.0",
"@alicloud/openapi-client": "^0.4.12",
@@ -147,30 +170,7 @@
"@google-cloud/publicca": "^1.3.0",
"basic-ftp": "^5.0.5",
"esdk-obs-nodejs": "^3.25.6",
"qiniu": "^7.12.0",
"mwtsc": "^1.15.1"
},
"devDependencies": {
"mwts": "^1.3.0",
"@midwayjs/mock": "3.20.11",
"@types/ali-oss": "^6.16.11",
"@types/cache-manager": "^4.0.6",
"@types/jest": "^29.5.13",
"@types/koa": "2.15.0",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.6",
"@types/node": "^18",
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"mocha": "^10.6.0",
"prettier": "3.3.3",
"rimraf": "^5.0.5",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2",
"why-is-node-running": "^3.2.2"
"qiniu": "^7.12.0"
},
"engines": {
"node": ">=20.0.0"
@@ -30,8 +30,9 @@
// export * from './plugin-wangsu/index.js'
// export * from './plugin-admin/index.js'
// export * from './plugin-ksyun/index.js'
// export * from './plugin-apisix/index.js'
// export * from './plugin-dokploy/index.js'
// export * from './plugin-apisix/index.js';
// export * from './plugin-asiaisp/index.js';
// export * from './plugin-dokploy/index.js';
// export * from './plugin-godaddy/index.js'
// export * from './plugin-captcha/index.js'
// export * from './plugin-xinnet/index.js'
@@ -0,0 +1,56 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import { AsiaIspClient } from "./client.js";
@IsAccess({
name: "asiaisp",
title: "橙域网络(asia-isp)授权",
desc: "橙域网络CDN API授权,用于部署证书到橙域CDN",
icon: "clarity:plugin-line",
})
export class AsiaIspAccess extends BaseAccess {
@AccessInput({
title: "AccessKeyId",
component: {
placeholder: "请输入 AccessKeyId",
},
required: true,
})
accessKeyId = "";
@AccessInput({
title: "AccessKeySecret",
component: {
placeholder: "请输入 AccessKeySecret",
},
required: true,
encrypt: true,
})
accessKeySecret = "";
@AccessInput({
title: "测试连接",
component: {
name: "api-test",
action: "TestRequest",
},
helper: "点击测试接口是否正常",
})
testRequest = true;
async onTestRequest() {
const client = await this.getClient();
const list = await client.getCertList();
return `连接成功,共 ${list.length} 个证书`;
}
async getClient() {
return new AsiaIspClient({
accessKeyId: this.accessKeyId,
accessKeySecret: this.accessKeySecret,
http: this.ctx.http,
logger: this.ctx.logger,
});
}
}
new AsiaIspAccess();
@@ -0,0 +1,240 @@
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();
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;
}
/**
* 删除证书
* 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}`);
}
}
@@ -0,0 +1,2 @@
export * from "./access.js";
export * from "./plugin-deploy-to-cdn.js";
@@ -0,0 +1,122 @@
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { AsiaIspAccess } from "./access.js";
@IsTaskPlugin({
name: "AsiaIspDeployToCDN",
title: "橙域网络-部署证书到CDN",
desc: "部署证书到橙域网络(asia-isp) CDN加速域名",
icon: "clarity:plugin-line",
group: pluginGroups.cdn.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class AsiaIspDeployToCDN extends AbstractTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames],
},
required: true,
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: "橙域网络授权",
component: {
name: "access-selector",
type: "asiaisp",
},
required: true,
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: "加速域名",
helper: "选择要部署证书的橙域CDN加速域名",
action: AsiaIspDeployToCDN.prototype.onGetDomainList.name,
pager: true,
search: true,
pageSize: 10,
single: false,
watches: ["certDomains", "accessId"],
required: true,
})
)
domainList!: string[];
async onInstance() {}
async execute(): Promise<void> {
const access = await this.getAccess<AsiaIspAccess>(this.accessId);
const client = await access.getClient();
this.logger.info("开始部署证书到橙域网络CDN");
// 1. 上传证书到橙域平台
this.logger.info("上传证书到橙域网络...");
const certId = await client.uploadCert({
cert: this.cert,
});
// 2. 为每个选中的域名绑定证书
for (const domain of this.domainList) {
this.logger.info(`部署证书到域名: ${domain}`);
await client.deployCertToDomain({
domain,
certId,
protocol: "https",
});
}
this.logger.info(`证书部署完成,共处理 ${this.domainList.length} 个域名`);
}
async onGetDomainList(data: PageSearch = {}) {
const access = await this.getAccess<AsiaIspAccess>(this.accessId);
const client = await access.getClient();
const list = await client.getDomainList();
if (!list || list.length === 0) {
throw new Error("没有找到加速域名");
}
// 客户端过滤:按搜索关键词匹配域名
let filtered = list;
const keyword = data.searchKey?.trim().toLowerCase();
if (keyword) {
filtered = list.filter((item: any) => item.domain?.toLowerCase().includes(keyword));
}
// 客户端分页
const pageNo = data.pageNo || 1;
const pageSize = data.pageSize || 10;
const start = (pageNo - 1) * pageSize;
const paged = filtered.slice(start, start + pageSize);
const options = paged.map((item: any) => {
return {
label: `${item.domain}${item.protocol === "https" ? " (HTTPS)" : ""}`,
value: item.domain,
domain: item.domain,
};
});
return {
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
total: filtered.length,
};
}
}
new AsiaIspDeployToCDN();