From f612509cac87b859e81a7a52fe94b2eaccad22f9 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Tue, 14 Oct 2025 12:05:31 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81=E6=96=B0=E7=BD=91?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/plugin/cert-plugin/index.ts | 6 +- .../src/plugins/plugin-xinnet/access-agent.ts | 216 ++++++++++++------ .../plugin-xinnet/dns-provider-agent.ts | 90 ++++++++ .../src/plugins/plugin-xinnet/index.ts | 3 + 4 files changed, 243 insertions(+), 72 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-xinnet/dns-provider-agent.ts diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index 5991f22b..3cac97e5 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -117,11 +117,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin { ], }, required: true, - helper: `1. DNS直接验证:域名dns解析是在阿里云/腾讯云/华为云/CF/NameSilo/西数/火山/dns.la/京东云/51dns的,选它 -2. CNAME代理验证:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证) + helper: `1. DNS直接验证:当域名dns解析已被本系统支持时(即下方DNS解析服务商选项中可选),推荐选择此方式 +2. CNAME代理验证:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(如果经常申请失败,建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证) 3. HTTP文件验证:不支持泛域名,需要配置网站文件上传 4. 多DNS提供商:每个域名可以选择独立的DNS提供商 -5. 自动匹配:需要在[域名管理](#/certd/cert/domain)中事先配置好校验方式 +5. 自动匹配:此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式 `, }) challengeType!: string; diff --git a/packages/ui/certd-server/src/plugins/plugin-xinnet/access-agent.ts b/packages/ui/certd-server/src/plugins/plugin-xinnet/access-agent.ts index 9fa65629..93234566 100644 --- a/packages/ui/certd-server/src/plugins/plugin-xinnet/access-agent.ts +++ b/packages/ui/certd-server/src/plugins/plugin-xinnet/access-agent.ts @@ -1,79 +1,157 @@ -// import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline"; -// import { XinnetClient } from "@certd/plugin-plus"; +import { IsAccess, AccessInput, BaseAccess, Pager, PageSearch } from "@certd/pipeline"; +import crypto from "crypto"; +/** + * 这个注解将注册一个授权配置 + * 在certd的后台管理系统中,用户可以选择添加此类型的授权 + */ +@IsAccess({ + name: "xinnetagent", + title: "新网授权(代理方式)", + icon: "lsicon:badge-new-filled", + desc: "" +}) +export class XinnetAgentAccess extends BaseAccess { -// /** -// * 这个注解将注册一个授权配置 -// * 在certd的后台管理系统中,用户可以选择添加此类型的授权 -// */ -// @IsAccess({ -// name: "xinnetagent", -// title: "新网授权(代理方式)", -// icon: "lsicon:badge-new-filled", -// desc: "" -// }) -// export class XinnetAccess extends BaseAccess { + /** + * 授权属性配置 + */ + @AccessInput({ + title: "代理账号", + component: { + placeholder: "代理账号,如:agent0001" + }, + required: true, + encrypt: false + }) + agentCode = ""; -// /** -// * 授权属性配置 -// */ -// @AccessInput({ -// title: "代理账号", -// component: { -// placeholder: "代理账号,如:agent0001" -// }, -// required: true, -// encrypt: false -// }) -// username = ""; + @AccessInput({ + title: "API密钥", + component: { + name: "a-input-password", + vModel: "value", + placeholder: "API密钥" + }, + required: true, + encrypt: true + }) + appSecret = ""; -// @AccessInput({ -// title: "API密钥", -// component: { -// name: "a-input-password", -// vModel: "value", -// placeholder: "API密钥" -// }, -// required: true, -// encrypt: true -// }) -// apikey = ""; + @AccessInput({ + title: "测试", + component: { + name: "api-test", + action: "TestRequest" + }, + helper: "点击测试接口是否正常" + }) + testRequest = true; -// @AccessInput({ -// title: "测试", -// component: { -// name: "api-test", -// action: "TestRequest" -// }, -// helper: "点击测试接口是否正常" -// }) -// testRequest = true; + async onTestRequest() { -// async onTestRequest() { + // const client = new XinnetClient({ + // access: this, + // logger: this.ctx.logger, + // http: this.ctx.http + // }); + await this.getDomainList({ pageNo: 1, pageSize: 1 }); -// // const client = new XinnetClient({ -// // access: this, -// // logger: this.ctx.logger, -// // http: this.ctx.http -// // }); - -// await client.getDomainList({ pageNo: 1, pageSize: 1 }); - -// return "ok"; -// } + return "ok"; + } -// getCacheKey () { -// let hashStr = "" -// for (const key in this) { -// if (Object.prototype.hasOwnProperty.call(this, key)) { -// const element = this[key]; -// hashStr += element; -// } -// } -// const hashCode = this.ctx.utils.hash.sha256(hashStr); -// return `xinnet-${hashCode}`; -// } -// } + async getDomainList(req:PageSearch) { + const pager = new Pager(req); + const conf = { + url: "/api/domain/list", + data: { + pageNo: String(pager.pageNo), + pageSize: String(pager.pageSize) + } + } + return await this.doRequest(conf); + } -// new XinnetAccess(); + + /** + * 生成 UTC 0 时区的时间戳 + */ + generateTimestamp() { + const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "").replaceAll("-", ""); + return timestamp; + } + + /** + * 字节转16进制字符串 + */ + bytesToHex(bytes:any) { + return bytes.toString('hex'); + } + + /** + * 生成签名 + */ + generateSignature(timestamp, urlPath, requestBody) { + const algorithm = 'HMAC-SHA256'; + const requestMethod = 'POST'; + + // 构建待签名字符串 + const stringToSign = `${algorithm}\n${timestamp}\n${requestMethod}\n${urlPath}\n${requestBody}`; + + // 使用 HMAC-SHA256 计算签名 + const hmac = crypto.createHmac('sha256', this.appSecret); + hmac.update(stringToSign); + const signatureBytes = hmac.digest(); + + // 转换为16进制字符串 + return this.bytesToHex(signatureBytes); + } + + /** + * 生成 authorization header + */ + generateAuthorization(timestamp, urlPath, requestBody) { + const signature = this.generateSignature(timestamp, urlPath, requestBody); + return `HMAC-SHA256 Access=${this.agentCode}, Signature=${signature}`; + } + + /** + * 查询域名分页列表 + */ + async doRequest(req:any) { + + const baseURL = 'https://apiv2.xinnet.com'; + const urlPath = req.url; + const requestURL = baseURL + urlPath; // 实际请求URL去掉最后的斜杠 + + // 请求体 + const requestBody = JSON.stringify(req.data); + + // 生成时间戳和授权头 + const timestamp = this.generateTimestamp(); + const authorization = this.generateAuthorization(timestamp, urlPath+"/", requestBody); + + // 请求配置 + const config = { + method: 'POST', + url: requestURL, + headers: { + 'Content-Type': 'application/json', + 'timestamp': timestamp, + 'authorization': authorization + }, + data: requestBody, + }; + + const res = await this.ctx.http.request(config); + + if (res.code !="0"){ + throw new Error(`API Error: ${res.code} ${res.requestId} - ${JSON.stringify(res.msg)}`); + } + return res.data; + } + +} + +new XinnetAgentAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-xinnet/dns-provider-agent.ts b/packages/ui/certd-server/src/plugins/plugin-xinnet/dns-provider-agent.ts new file mode 100644 index 00000000..c32b8da2 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-xinnet/dns-provider-agent.ts @@ -0,0 +1,90 @@ +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { XinnetAgentAccess } from "./access-agent.js"; + +export type XinnetAgentRecord = { + recordId: number; + domainName: string; +}; + +// 这里通过IsDnsProvider注册一个dnsProvider +@IsDnsProvider({ + name: "xinnetagent", + title: "新网(代理方式)", + desc: "新网域名解析(代理方式)", + icon: "lsicon:badge-new-filled", + // 这里是对应的 cloudflare的access类型名称 + accessType: "xinnetagent", + order: 7 +}) +export class XinnetAgentProvider extends AbstractDnsProvider { + access!: XinnetAgentAccess; + + async onInstance() { + //一些初始化的操作 + // 也可以通过ctx成员变量传递context + this.access = this.ctx.access as XinnetAgentAccess; + } + + /** + * 创建dns解析记录,用于验证域名所有权 + */ + async createRecord(options: CreateRecordOptions): Promise { + /** + * fullRecord: '_acme-challenge.test.example.com', + * value: 一串uuid + * type: 'TXT', + * domain: 'example.com' + */ + const { fullRecord, value, type, domain } = options; + this.logger.info("添加域名解析:", fullRecord, value, type, domain); + + + /** + * /api/dns/create + * domainName 是 string 域名名称 test-xinnet-0516-ceshi.cn +recordName 是 string 记录名 test1.test-xinnet-0516-ceshi.cn,如果是@和空字符只需要传域名即可 +type 是 string 解析记录的类型 可选择类型如下: NS A CNAME MX TXT URL SRV AAAA A +value 是 string 解析内容 192.168.1.50 +line 是 string 线路 只能传"默认" + */ + + const res = await this.access.doRequest({ + url:"/api/dns/create", + data:{ + domainName: domain, + recordName: fullRecord, + type: type, + value: value, + line: "默认" + } + }); + + + return { + recordId:res, + domainName: domain + }; + } + + + /** + * 删除dns解析记录,清理申请痕迹 + * @param options + */ + async removeRecord(options: RemoveRecordOptions): Promise { + + const {domainName,recordId} = options.recordRes; + await this.access.doRequest({ + url:"/api/dns/delete", + data:{ + recordId: recordId, + domainName: domainName + } + }); + + + } +} + +//实例化这个provider,将其自动注册到系统中 +new XinnetAgentProvider(); diff --git a/packages/ui/certd-server/src/plugins/plugin-xinnet/index.ts b/packages/ui/certd-server/src/plugins/plugin-xinnet/index.ts index db899c71..871ad205 100644 --- a/packages/ui/certd-server/src/plugins/plugin-xinnet/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-xinnet/index.ts @@ -1,2 +1,5 @@ export * from './dns-provider.js'; export * from './access.js'; + +export * from './access-agent.js'; +export * from './dns-provider-agent.js';