From 1d23dd2426bd1e4c4dfea0a9e561d665e045ba9d Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Thu, 13 Nov 2025 00:45:05 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91teo=20dns=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 +- packages/core/acme-client/src/client.js | 5 +- packages/core/acme-client/src/verify.js | 31 ++-- packages/core/acme-client/types/index.d.ts | 3 +- .../src/plugin/cert-plugin/acme.ts | 2 +- .../plugins/plugin-lib/src/tencent/access.ts | 4 + .../cname/service/cname-record-service.ts | 8 +- .../plugin-tencent/dns-provider/index.ts | 1 + .../dns-provider/teo-dns-provider.ts | 144 ++++++++++++++++++ 9 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/teo-dns-provider.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 217769ea..091a6a6d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ "git.scanRepositories": [ "./packages/pro" ], - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } \ No newline at end of file diff --git a/packages/core/acme-client/src/client.js b/packages/core/acme-client/src/client.js index 27026103..54a5ee33 100644 --- a/packages/core/acme-client/src/client.js +++ b/packages/core/acme-client/src/client.js @@ -7,7 +7,7 @@ import { createHash } from 'crypto'; import { getPemBodyAsB64u } from './crypto/index.js'; import HttpClient from './http.js'; import AcmeApi from './api.js'; -import verify from './verify.js'; +import {createChallengeFn} from './verify.js'; import * as util from './util.js'; import auto from './auto.js'; import { CancelError } from './error.js'; @@ -492,6 +492,9 @@ class AcmeClient { throw new Error('Unable to verify ACME challenge, URL not found'); } + const {challenges} = createChallengeFn({logger:this.opts.logger}); + + const verify = challenges if (typeof verify[challenge.type] === 'undefined') { throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`); } diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js index 0af93fd5..6c692005 100644 --- a/packages/core/acme-client/src/verify.js +++ b/packages/core/acme-client/src/verify.js @@ -4,14 +4,22 @@ import dnsSdk from "dns" import https from 'https' -import {log} from './logger.js' +import {log as defaultLog} from './logger.js' import axios from './axios.js' import * as util from './util.js' import {isAlpnCertificateAuthorizationValid} from './crypto/index.js' const dns = dnsSdk.promises -/** + + +export function createChallengeFn(opts = {}){ + const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog} + + const log = function(...args){ + logger.info(...args) + } + /** * Verify ACME HTTP challenge * * https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 @@ -112,7 +120,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) { return records } -export async function walkTxtRecord(recordName,deep = 0) { + async function walkTxtRecord(recordName,deep = 0) { if(deep >5){ log(`walkTxtRecord too deep (#${deep}) , skip walk`) return [] @@ -207,12 +215,13 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) { return true; } -/** - * Export API - */ + return { + challenges:{ + 'http-01': verifyHttpChallenge, + 'dns-01': verifyDnsChallenge, + 'tls-alpn-01': verifyTlsAlpnChallenge, + }, + walkTxtRecord, + } -export default { - 'http-01': verifyHttpChallenge, - 'dns-01': verifyDnsChallenge, - 'tls-alpn-01': verifyTlsAlpnChallenge, -}; +} \ No newline at end of file diff --git a/packages/core/acme-client/types/index.d.ts b/packages/core/acme-client/types/index.d.ts index fd33fb42..90b57a70 100644 --- a/packages/core/acme-client/types/index.d.ts +++ b/packages/core/acme-client/types/index.d.ts @@ -207,7 +207,8 @@ export const agents: any; export function setLogger(fn: (message: any, ...args: any[]) => void): void; -export function walkTxtRecord(record: any): Promise; +export function createChallengeFn(opts?: {logger?:any}): any; +// export function walkTxtRecord(record: any): Promise; export function getAuthoritativeDnsResolver(record:string): Promise; export const CancelError: typeof CancelError; diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts index 3b7effb8..4d84cde3 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -337,7 +337,7 @@ export class AcmeService { domains = encodingDomains; /* Create CSR */ - const { commonName, altNames } = this.buildCommonNameByDomains(domains); + const { altNames } = this.buildCommonNameByDomains(domains); let privateKey = null; const privateKeyType = options.privateKeyType || "rsa_2048"; const privateKeyArr = privateKeyType.split("_"); diff --git a/packages/plugins/plugin-lib/src/tencent/access.ts b/packages/plugins/plugin-lib/src/tencent/access.ts index aaa1c150..0b0fb41a 100644 --- a/packages/plugins/plugin-lib/src/tencent/access.ts +++ b/packages/plugins/plugin-lib/src/tencent/access.ts @@ -64,4 +64,8 @@ export class TencentAccess extends BaseAccess { intlDomain() { return this.isIntl() ? "intl." : ""; } + + buildEndpoint(endpoint: string) { + return `${this.intlDomain()}${endpoint}`; + } } diff --git a/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts b/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts index ea028b9b..ee4224bd 100644 --- a/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts +++ b/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts @@ -13,7 +13,7 @@ import { CnameRecordEntity, CnameRecordStatusType } from "../entity/cname-record import { createDnsProvider, IDnsProvider } from "@certd/plugin-cert"; import { CnameProvider, CnameRecord } from "@certd/pipeline"; import { cache, http, isDev, logger, utils } from "@certd/basic"; -import { getAuthoritativeDnsResolver, walkTxtRecord } from "@certd/acme-client"; +import { getAuthoritativeDnsResolver, createChallengeFn } from "@certd/acme-client"; import { CnameProviderService } from "./cname-provider-service.js"; import { CnameProviderEntity } from "../entity/cname-provider.js"; import { CommonDnsProvider } from "./common-provider.js"; @@ -241,6 +241,8 @@ export class CnameRecordService extends BaseService { * @param id */ async verify(id: number) { + + const {walkTxtRecord} = createChallengeFn({logger}); const bean = await this.info(id); if (!bean) { throw new ValidateException(`CnameRecord:${id} 不存在`); @@ -416,6 +418,7 @@ export class CnameRecordService extends BaseService { async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) { + let dnsResolver = null; try { dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain); @@ -460,6 +463,9 @@ export class CnameRecordService extends BaseService { //如果权威服务器中查不到txt,无需继续检查 return; } + + const {walkTxtRecord} = createChallengeFn({logger}); + if (cnameRecords.length > 0) { // 从cname记录中获取txt记录 // 对比是否存在,如果不存在于cname中获取的txt中,说明本体有创建多余的txt记录 diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/index.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/index.ts index 61eaff08..658355b5 100644 --- a/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/index.ts @@ -1,2 +1,3 @@ import './dnspod-dns-provider.js'; import './tencent-dns-provider.js'; +import './teo-dns-provider.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/teo-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/teo-dns-provider.ts new file mode 100644 index 00000000..96ede718 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-tencent/dns-provider/teo-dns-provider.ts @@ -0,0 +1,144 @@ +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert'; +import { TencentAccess } from '@certd/plugin-lib'; + +@IsDnsProvider({ + name: 'tencent-eo', + title: '腾讯云EO DNS', + desc: '腾讯云EO DNS解析提供者', + accessType: 'tencent', + icon: 'svg:icon-tencentcloud', +}) +export class TencentEoDnsProvider extends AbstractDnsProvider { + access!: TencentAccess; + + client!: any; + + + async onInstance() { + this.access = this.ctx.access as TencentAccess + const clientConfig = { + credential: this.access, + region: '', + profile: { + httpProfile: { + endpoint: this.access.buildEndpoint("teo.tencentcloudapi.com"), + }, + }, + }; + const teosdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/teo/v20220901/index.js'); + const TeoClient = teosdk.v20220901.Client; + // 实例化要请求产品的client对象,clientProfile是可选的 + this.client = new TeoClient(clientConfig); + } + + + async getZoneId(domain: string) { + + const params = { + "Filters": [ + { + "Name": "zone-name", + "Values": [ + domain + ] + } + ] + }; + const res = await this.client.DescribeZones(params); + if (res.Zones && res.Zones.length > 0) { + return res.Zones[0].ZoneId; + } + throw new Error('未找到对应的ZoneId'); + } + + async createRecord(options: CreateRecordOptions): Promise { + const { fullRecord, value, type, domain } = options; + this.logger.info('添加域名解析:', fullRecord, value); + + const zoneId = await this.getZoneId(domain); + const params = { + "ZoneId": zoneId, + "Name": fullRecord, + "Type": type, + "Content": value + }; + + try { + const ret = await this.client.CreateDnsRecord(params); + this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret)); + /* + { + "RecordId": 162, + "RequestId": "ab4f1426-ea15-42ea-8183-dc1b44151166" + } + */ + return { + RecordId: ret.RecordId, + ZoneId: zoneId, + }; + } catch (e: any) { + if (e?.code === 'ResourceInUse.DuplicateName') { + this.logger.info('域名解析已存在,无需重复添加:', fullRecord, value); + return await this.findRecord({ + ...options, + zoneId, + }); + } + throw e; + } + } + + async findRecord(options: CreateRecordOptions & { zoneId: string }): Promise { + + const { zoneId } = options; + const params = { + "ZoneId": zoneId, + "Filters": [ + { + "Name": "name", + "Values": [ + options.fullRecord + ] + }, + { + "Name": "content", + "Values": [ + options.value + ] + }, + { + "Name": "type", + "Values": [ + options.type + ] + } + ] + }; + const ret = await this.client.DescribeRecordFilterList(params); + if (ret.DnsRecords && ret.DnsRecords.length > 0) { + this.logger.info('已存在解析记录:', ret.DnsRecords); + return ret.DnsRecords[0]; + } + return {}; + } + + async removeRecord(options: RemoveRecordOptions) { + const { fullRecord, value } = options.recordReq; + const record = options.recordRes; + if (!record) { + this.logger.info('解析记录recordId为空,不执行删除', fullRecord, value); + } + + const params = { + "ZoneId": record.ZoneId, + "RecordIds": [ + record.RecordId + ] + }; + + const ret = await this.client.DeleteDnsRecords(params); + this.logger.info('删除域名解析成功:', fullRecord, value); + return ret; + } +} +new TencentEoDnsProvider();