perf: 支持腾讯云teo dns解析

This commit is contained in:
xiaojunnuo
2025-11-13 00:45:05 +08:00
parent 86ce00adf9
commit 1d23dd2426
9 changed files with 187 additions and 16 deletions
+4 -1
View File
@@ -5,5 +5,8 @@
"git.scanRepositories": [ "git.scanRepositories": [
"./packages/pro" "./packages/pro"
], ],
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint",
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
} }
+4 -1
View File
@@ -7,7 +7,7 @@ import { createHash } from 'crypto';
import { getPemBodyAsB64u } from './crypto/index.js'; import { getPemBodyAsB64u } from './crypto/index.js';
import HttpClient from './http.js'; import HttpClient from './http.js';
import AcmeApi from './api.js'; import AcmeApi from './api.js';
import verify from './verify.js'; import {createChallengeFn} from './verify.js';
import * as util from './util.js'; import * as util from './util.js';
import auto from './auto.js'; import auto from './auto.js';
import { CancelError } from './error.js'; import { CancelError } from './error.js';
@@ -492,6 +492,9 @@ class AcmeClient {
throw new Error('Unable to verify ACME challenge, URL not found'); 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') { if (typeof verify[challenge.type] === 'undefined') {
throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`); throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`);
} }
+20 -11
View File
@@ -4,14 +4,22 @@
import dnsSdk from "dns" import dnsSdk from "dns"
import https from 'https' import https from 'https'
import {log} from './logger.js' import {log as defaultLog} from './logger.js'
import axios from './axios.js' import axios from './axios.js'
import * as util from './util.js' import * as util from './util.js'
import {isAlpnCertificateAuthorizationValid} from './crypto/index.js' import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
const dns = dnsSdk.promises 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 * Verify ACME HTTP challenge
* *
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
@@ -112,7 +120,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
return records return records
} }
export async function walkTxtRecord(recordName,deep = 0) { async function walkTxtRecord(recordName,deep = 0) {
if(deep >5){ if(deep >5){
log(`walkTxtRecord too deep (#${deep}) , skip walk`) log(`walkTxtRecord too deep (#${deep}) , skip walk`)
return [] return []
@@ -207,12 +215,13 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
return true; return true;
} }
/** return {
* Export API challenges:{
*/ 'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge,
},
walkTxtRecord,
}
export default { }
'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge,
};
+2 -1
View File
@@ -207,7 +207,8 @@ export const agents: any;
export function setLogger(fn: (message: any, ...args: any[]) => void): void; export function setLogger(fn: (message: any, ...args: any[]) => void): void;
export function walkTxtRecord(record: any): Promise<string[]>; export function createChallengeFn(opts?: {logger?:any}): any;
// export function walkTxtRecord(record: any): Promise<string[]>;
export function getAuthoritativeDnsResolver(record:string): Promise<any>; export function getAuthoritativeDnsResolver(record:string): Promise<any>;
export const CancelError: typeof CancelError; export const CancelError: typeof CancelError;
@@ -337,7 +337,7 @@ export class AcmeService {
domains = encodingDomains; domains = encodingDomains;
/* Create CSR */ /* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains); const { altNames } = this.buildCommonNameByDomains(domains);
let privateKey = null; let privateKey = null;
const privateKeyType = options.privateKeyType || "rsa_2048"; const privateKeyType = options.privateKeyType || "rsa_2048";
const privateKeyArr = privateKeyType.split("_"); const privateKeyArr = privateKeyType.split("_");
@@ -64,4 +64,8 @@ export class TencentAccess extends BaseAccess {
intlDomain() { intlDomain() {
return this.isIntl() ? "intl." : ""; return this.isIntl() ? "intl." : "";
} }
buildEndpoint(endpoint: string) {
return `${this.intlDomain()}${endpoint}`;
}
} }
@@ -13,7 +13,7 @@ import { CnameRecordEntity, CnameRecordStatusType } from "../entity/cname-record
import { createDnsProvider, IDnsProvider } from "@certd/plugin-cert"; import { createDnsProvider, IDnsProvider } from "@certd/plugin-cert";
import { CnameProvider, CnameRecord } from "@certd/pipeline"; import { CnameProvider, CnameRecord } from "@certd/pipeline";
import { cache, http, isDev, logger, utils } from "@certd/basic"; 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 { CnameProviderService } from "./cname-provider-service.js";
import { CnameProviderEntity } from "../entity/cname-provider.js"; import { CnameProviderEntity } from "../entity/cname-provider.js";
import { CommonDnsProvider } from "./common-provider.js"; import { CommonDnsProvider } from "./common-provider.js";
@@ -241,6 +241,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
* @param id * @param id
*/ */
async verify(id: number) { async verify(id: number) {
const {walkTxtRecord} = createChallengeFn({logger});
const bean = await this.info(id); const bean = await this.info(id);
if (!bean) { if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`); throw new ValidateException(`CnameRecord:${id} 不存在`);
@@ -416,6 +418,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) { async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
let dnsResolver = null; let dnsResolver = null;
try { try {
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain); dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
@@ -460,6 +463,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//如果权威服务器中查不到txt,无需继续检查 //如果权威服务器中查不到txt,无需继续检查
return; return;
} }
const {walkTxtRecord} = createChallengeFn({logger});
if (cnameRecords.length > 0) { if (cnameRecords.length > 0) {
// 从cname记录中获取txt记录 // 从cname记录中获取txt记录
// 对比是否存在,如果不存在于cname中获取的txt中,说明本体有创建多余的txt记录 // 对比是否存在,如果不存在于cname中获取的txt中,说明本体有创建多余的txt记录
@@ -1,2 +1,3 @@
import './dnspod-dns-provider.js'; import './dnspod-dns-provider.js';
import './tencent-dns-provider.js'; import './tencent-dns-provider.js';
import './teo-dns-provider.js';
@@ -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<any> {
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<any> {
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<any>) {
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();