fix: 修复aliyun域名超过100个找不到域名的bug

pull/68/head
xiaojunnuo 2024-06-14 01:22:07 +08:00
parent ebf2a820cc
commit 5b1494b3ce
11 changed files with 183 additions and 188 deletions

View File

@ -176,21 +176,31 @@ module.exports = async function(client, userOpts) {
await challengeFunc(authz); await challengeFunc(authz);
}); });
log('开始challenge');
let promise = Promise.resolve(); // let promise = Promise.resolve();
function runPromisesSerially(tasks) { // function runPromisesSerially(tasks) {
tasks.forEach((task) => { // tasks.forEach((task) => {
promise = promise.then(task); // promise = promise.then(task);
}); // });
return promise; // return promise;
// }
function runPromiseParallel(tasks) {
return Promise.all(tasks.map((task) => task()));
} }
try { try {
await runPromisesSerially(challengePromises); log('开始challenge');
await runPromiseParallel(challengePromises);
}
catch (e) {
log('challenge失败');
throw e;
} }
finally { finally {
await runPromisesSerially(clearTasks); log('清理challenge痕迹');
await runPromiseParallel(clearTasks);
} }
// try { // try {

View File

@ -20,7 +20,8 @@
"@certd/acme-client": "workspace:^1.20.10", "@certd/acme-client": "workspace:^1.20.10",
"@certd/pipeline": "workspace:^1.20.10", "@certd/pipeline": "workspace:^1.20.10",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"node-forge": "^0.10.0" "node-forge": "^0.10.0",
"psl": "^1.9.0"
}, },
"devDependencies": { "devDependencies": {
"@alicloud/cs20151215": "^3.0.3", "@alicloud/cs20151215": "^3.0.3",
@ -35,6 +36,7 @@
"@types/lodash": "^4.14.186", "@types/lodash": "^4.14.186",
"@types/mocha": "^10.0.0", "@types/mocha": "^10.0.0",
"@types/node-forge": "^1.3.0", "@types/node-forge": "^1.3.0",
"@types/psl": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1", "@typescript-eslint/parser": "^5.38.1",
"chai": "^4.3.6", "chai": "^4.3.6",

View File

@ -1,4 +1,4 @@
import { Registrable } from "@certd/pipeline"; import { HttpClient, IAccess, ILogger, Registrable } from "@certd/pipeline";
export type DnsProviderDefine = Registrable & { export type DnsProviderDefine = Registrable & {
accessType: string; accessType: string;
@ -11,13 +11,21 @@ export type CreateRecordOptions = {
fullRecord: string; fullRecord: string;
type: string; type: string;
value: any; value: any;
domain: string;
}; };
export type RemoveRecordOptions = CreateRecordOptions & { export type RemoveRecordOptions = CreateRecordOptions & {
record: any; record: any;
}; };
export type DnsProviderContext = {
access: IAccess;
logger: ILogger;
http: HttpClient;
};
export interface IDnsProvider { export interface IDnsProvider {
onInstance(): Promise<void>; onInstance(): Promise<void>;
createRecord(options: CreateRecordOptions): Promise<any>; createRecord(options: CreateRecordOptions): Promise<any>;
removeRecord(options: RemoveRecordOptions): Promise<any>; removeRecord(options: RemoveRecordOptions): Promise<any>;
setCtx(ctx: DnsProviderContext): void;
} }

View File

@ -0,0 +1,15 @@
import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from "./api";
export abstract class AbstractDnsProvider implements IDnsProvider {
ctx!: DnsProviderContext;
setCtx(ctx: DnsProviderContext) {
this.ctx = ctx;
}
abstract createRecord(options: CreateRecordOptions): Promise<any>;
abstract onInstance(): Promise<void>;
abstract removeRecord(options: RemoveRecordOptions): Promise<any>;
}

View File

@ -1,3 +1,4 @@
export * from "./api"; export * from "./api";
export * from "./registry"; export * from "./registry";
export * from "./decorator"; export * from "./decorator";
export * from "./base";

View File

@ -5,6 +5,8 @@ import { Challenge } from "@certd/acme-client/types/rfc8555";
import { Logger } from "log4js"; import { Logger } from "log4js";
import { IContext } from "@certd/pipeline"; import { IContext } from "@certd/pipeline";
import { IDnsProvider } from "../../dns-provider"; import { IDnsProvider } from "../../dns-provider";
import psl from "psl";
export type CertInfo = { export type CertInfo = {
crt: string; crt: string;
key: string; key: string;
@ -65,33 +67,43 @@ export class AcmeService {
return key.toString(); return key.toString();
} }
parseDomain(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed.domain as string;
}
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) { async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
this.logger.info("Triggered challengeCreateFn()"); this.logger.info("Triggered challengeCreateFn()");
/* http-01 */ /* http-01 */
const fullDomain = authz.identifier.value;
if (challenge.type === "http-01") { if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`; const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization; const fileContents = keyAuthorization;
this.logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`); this.logger.info(`Creating challenge response for ${fullDomain} at path: ${filePath}`);
/* Replace this */ /* Replace this */
this.logger.info(`Would write "${fileContents}" to path "${filePath}"`); this.logger.info(`Would write "${fileContents}" to path "${filePath}"`);
// await fs.writeFileAsync(filePath, fileContents); // await fs.writeFileAsync(filePath, fileContents);
} else if (challenge.type === "dns-01") { } else if (challenge.type === "dns-01") {
/* dns-01 */ /* dns-01 */
const dnsRecord = `_acme-challenge.${authz.identifier.value}`; const dnsRecord = `_acme-challenge.${fullDomain}`;
const recordValue = keyAuthorization; const recordValue = keyAuthorization;
this.logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`); this.logger.info(`Creating TXT record for ${fullDomain}: ${dnsRecord}`);
/* Replace this */ /* Replace this */
this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`); this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
const domain = this.parseDomain(fullDomain);
this.logger.info("解析到域名domain=", domain);
return await dnsProvider.createRecord({ return await dnsProvider.createRecord({
fullRecord: dnsRecord, fullRecord: dnsRecord,
type: "TXT", type: "TXT",
value: recordValue, value: recordValue,
domain,
}); });
} }
} }
@ -111,28 +123,33 @@ export class AcmeService {
this.logger.info("Triggered challengeRemoveFn()"); this.logger.info("Triggered challengeRemoveFn()");
/* http-01 */ /* http-01 */
const fullDomain = authz.identifier.value;
if (challenge.type === "http-01") { if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`; const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
this.logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`); this.logger.info(`Removing challenge response for ${fullDomain} at path: ${filePath}`);
/* Replace this */ /* Replace this */
this.logger.info(`Would remove file on path "${filePath}"`); this.logger.info(`Would remove file on path "${filePath}"`);
// await fs.unlinkAsync(filePath); // await fs.unlinkAsync(filePath);
} else if (challenge.type === "dns-01") { } else if (challenge.type === "dns-01") {
const dnsRecord = `_acme-challenge.${authz.identifier.value}`; const dnsRecord = `_acme-challenge.${fullDomain}`;
const recordValue = keyAuthorization; const recordValue = keyAuthorization;
this.logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`); this.logger.info(`Removing TXT record for ${fullDomain}: ${dnsRecord}`);
/* Replace this */ /* Replace this */
this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`); this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
const domain = this.parseDomain(fullDomain);
try { try {
await dnsProvider.removeRecord({ await dnsProvider.removeRecord({
fullRecord: dnsRecord, fullRecord: dnsRecord,
type: "TXT", type: "TXT",
value: keyAuthorization, value: keyAuthorization,
record: recordItem, record: recordItem,
domain,
}); });
} catch (e) { } catch (e) {
this.logger.error("删除解析记录出错:", e); this.logger.error("删除解析记录出错:", e);

View File

@ -1,21 +1,10 @@
import { import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
AbstractTaskPlugin,
Decorator,
HttpClient,
IAccessService,
IContext,
IsTaskPlugin,
RunStrategy,
Step,
TaskInput,
TaskOutput
} from "@certd/pipeline";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {AcmeService, CertInfo} from "./acme"; import { AcmeService, CertInfo } from "./acme";
import _ from "lodash"; import _ from "lodash";
import {Logger} from "log4js"; import { Logger } from "log4js";
import {DnsProviderDefine, dnsProviderRegistry} from "../../dns-provider"; import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider";
import {CertReader} from "./cert-reader"; import { CertReader } from "./cert-reader";
import JSZip from "jszip"; import JSZip from "jszip";
export { CertReader }; export { CertReader };
@ -242,8 +231,9 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
// @ts-ignore // @ts-ignore
const dnsProvider: IDnsProvider = new DnsProviderClass(); const dnsProvider: IDnsProvider = new DnsProviderClass();
const context = { access, logger: this.logger, http: this.http }; const context: DnsProviderContext = { access, logger: this.logger, http: this.http };
Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context); Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context);
dnsProvider.setCtx(context);
await dnsProvider.onInstance(); await dnsProvider.onInstance();
const cert = await this.acme.order({ const cert = await this.acme.order({

View File

@ -26,10 +26,10 @@
"@ant-design/icons-vue": "^6.1.0", "@ant-design/icons-vue": "^6.1.0",
"@aws-sdk/client-s3": "^3.383.0", "@aws-sdk/client-s3": "^3.383.0",
"@aws-sdk/s3-request-presigner": "^3.383.0", "@aws-sdk/s3-request-presigner": "^3.383.0",
"@fast-crud/fast-crud": "^1.20.2", "@fast-crud/fast-crud": "^1.21.0",
"@fast-crud/fast-extends": "^1.20.2", "@fast-crud/fast-extends": "^1.21.0",
"@fast-crud/ui-antdv4": "^1.20.2", "@fast-crud/ui-antdv4": "^1.21.0",
"@fast-crud/ui-interface": "^1.20.2", "@fast-crud/ui-interface": "^1.21.0",
"@iconify/vue": "^4.1.1", "@iconify/vue": "^4.1.1",
"@soerenmartius/vue3-clipboard": "^0.1.2", "@soerenmartius/vue3-clipboard": "^0.1.2",
"ant-design-vue": "^4.1.2", "ant-design-vue": "^4.1.2",

View File

@ -1,13 +1,7 @@
import Core from '@alicloud/pop-core'; import Core from "@alicloud/pop-core";
import _ from 'lodash'; import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { import { Autowire, ILogger } from "@certd/pipeline";
CreateRecordOptions, import { AliyunAccess } from "../access";
IDnsProvider,
IsDnsProvider,
RemoveRecordOptions,
} from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { AliyunAccess } from '../access';
@IsDnsProvider({ @IsDnsProvider({
name: 'aliyun', name: 'aliyun',
@ -15,7 +9,7 @@ import { AliyunAccess } from '../access';
desc: '阿里云DNS解析提供商', desc: '阿里云DNS解析提供商',
accessType: 'aliyun', accessType: 'aliyun',
}) })
export class AliyunDnsProvider implements IDnsProvider { export class AliyunDnsProvider extends AbstractDnsProvider{
client: any; client: any;
@Autowire() @Autowire()
access!: AliyunAccess; access!: AliyunAccess;
@ -30,71 +24,71 @@ export class AliyunDnsProvider implements IDnsProvider {
apiVersion: '2015-01-09', apiVersion: '2015-01-09',
}); });
} }
//
async getDomainList() { // async getDomainList() {
const params = { // const params = {
RegionId: 'cn-hangzhou', // RegionId: 'cn-hangzhou',
PageSize: 100, // PageSize: 100,
}; // };
//
const requestOption = { // const requestOption = {
method: 'POST', // method: 'POST',
}; // };
//
const ret = await this.client.request( // const ret = await this.client.request(
'DescribeDomains', // 'DescribeDomains',
params, // params,
requestOption // requestOption
); // );
return ret.Domains.Domain; // return ret.Domains.Domain;
} // }
//
async matchDomain(dnsRecord: string) { // async matchDomain(dnsRecord: string) {
const list = await this.getDomainList(); // const list = await this.getDomainList();
let domain = null; // let domain = null;
const domainList = []; // const domainList = [];
for (const item of list) { // for (const item of list) {
domainList.push(item.DomainName); // domainList.push(item.DomainName);
if (_.endsWith(dnsRecord, item.DomainName)) { // if (_.endsWith(dnsRecord, item.DomainName)) {
domain = item.DomainName; // domain = item.DomainName;
break; // break;
} // }
} // }
if (!domain) { // if (!domain) {
throw new Error( // throw new Error(
`can not find Domain :${dnsRecord} ,list: ${JSON.stringify(domainList)}` // `can not find Domain :${dnsRecord} ,list: ${JSON.stringify(domainList)}`
); // );
} // }
return domain; // return domain;
} // }
//
async getRecords(domain: string, rr: string, value: string) { // async getRecords(domain: string, rr: string, value: string) {
const params: any = { // const params: any = {
RegionId: 'cn-hangzhou', // RegionId: 'cn-hangzhou',
DomainName: domain, // DomainName: domain,
RRKeyWord: rr, // RRKeyWord: rr,
ValueKeyWord: undefined, // ValueKeyWord: undefined,
}; // };
if (value) { // if (value) {
params.ValueKeyWord = value; // params.ValueKeyWord = value;
} // }
//
const requestOption = { // const requestOption = {
method: 'POST', // method: 'POST',
}; // };
//
const ret = await this.client.request( // const ret = await this.client.request(
'DescribeDomainRecords', // 'DescribeDomainRecords',
params, // params,
requestOption // requestOption
); // );
return ret.DomainRecords.Record; // return ret.DomainRecords.Record;
} // }
async createRecord(options: CreateRecordOptions): Promise<any> { async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type } = options; const { fullRecord, value, type,domain } = options;
this.logger.info('添加域名解析:', fullRecord, value); this.logger.info('添加域名解析:', fullRecord, value,domain);
const domain = await this.matchDomain(fullRecord); // const domain = await this.matchDomain(fullRecord);
const rr = fullRecord.replace('.' + domain, ''); const rr = fullRecord.replace('.' + domain, '');
const params = { const params = {

View File

@ -1,12 +1,6 @@
import _ from 'lodash'; import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { import { Autowire, HttpClient, ILogger } from "@certd/pipeline";
CreateRecordOptions, import { CloudflareAccess } from "./access";
IDnsProvider,
IsDnsProvider,
RemoveRecordOptions,
} from '@certd/plugin-cert';
import { Autowire, ILogger } from '@certd/pipeline';
import { CloudflareAccess } from './access';
// TODO 这里注册一个dnsProvider // TODO 这里注册一个dnsProvider
@IsDnsProvider({ @IsDnsProvider({
@ -15,50 +9,41 @@ import { CloudflareAccess } from './access';
desc: 'cloudflare dns provider示例', desc: 'cloudflare dns provider示例',
accessType: 'cloudflare', accessType: 'cloudflare',
}) })
export class CloudflareDnsProvider implements IDnsProvider { export class CloudflareDnsProvider extends AbstractDnsProvider{
@Autowire() @Autowire()
logger! : ILogger;
access!: CloudflareAccess; access!: CloudflareAccess;
@Autowire() http!: HttpClient;
logger!: ILogger;
async onInstance() { async onInstance() {
const access: any = this.access; //一些初始化的操作
this.logger.debug('access', access); this.access = this.ctx.access as CloudflareAccess;
//初始化的操作 this.http = this.ctx.http
//...
} }
async getDomainList(): Promise<any[]> {
// TODO 这里你要实现一个获取域名列表的方法
const access = this.access;
this.logger.debug('access', access);
return [];
}
async matchDomain(dnsRecord: string): Promise<any> {
const domainList = await this.getDomainList();
let domainRecord = null;
for (const item of domainList) {
//TODO 根据域名去匹配账户中是否有该域名, 这里不一定是item.name 具体要看你要实现的平台的接口而定
if (_.endsWith(dnsRecord + '.', item.name)) {
domainRecord = item;
break;
}
}
if (!domainRecord) {
this.logger.info('账户中域名列表:', domainList);
this.logger.error('找不到域名,请确认账户中是否真的有此域名');
throw new Error('can not find Domain:' + dnsRecord);
}
return domainRecord;
}
/**
* curl --request POST \
* --url https://api.cloudflare.com/client/v4/zones/zone_id/dns_records \
* --header 'Content-Type: application/json' \
* --header 'X-Auth-Email: ' \
* --data '{
* "content": "198.51.100.4",
* "name": "example.com",
* "proxied": false,
* "type": "A",
* "comment": "Domain verification record",
* "tags": [
* "owner:dns-team"
* ],
* "ttl": 60
* }'
*/
async createRecord(options: CreateRecordOptions): Promise<any> { async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type } = options; const { fullRecord, value, type,domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type); this.logger.info('添加域名解析:', fullRecord, value, type,domain);
//先确定账户中是否有该域名
const domainRecord = await this.matchDomain(fullRecord); this.http.post('https://api.cloudflare.com/client/v4/zones/zone_id/dns_records')
this.logger.debug('matchDomain:', domainRecord);
//TODO 然后调用接口创建txt类型的dns解析记录 //TODO 然后调用接口创建txt类型的dns解析记录
// .. 这里调用对应平台的后台接口 // .. 这里调用对应平台的后台接口
const access = this.access; const access = this.access;

View File

@ -28,37 +28,10 @@ export class DemoDnsProvider implements IDnsProvider {
//... //...
} }
async getDomainList(): Promise<any[]> {
// TODO 这里你要实现一个获取域名列表的方法
const access = this.access;
this.logger.debug('access', access);
return [];
}
async matchDomain(dnsRecord: string): Promise<any> {
const domainList = await this.getDomainList();
let domainRecord = null;
for (const item of domainList) {
//TODO 根据域名去匹配账户中是否有该域名, 这里不一定是item.name 具体要看你要实现的平台的接口而定
if (_.endsWith(dnsRecord + '.', item.name)) {
domainRecord = item;
break;
}
}
if (!domainRecord) {
this.logger.info('账户中域名列表:', domainList);
this.logger.error('找不到域名,请确认账户中是否真的有此域名');
throw new Error('can not find Domain:' + dnsRecord);
}
return domainRecord;
}
async createRecord(options: CreateRecordOptions): Promise<any> { async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type } = options; const { fullRecord, value, type,domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type); this.logger.info('添加域名解析:', fullRecord, value, type,domain);
//先确定账户中是否有该域名
const domainRecord = await this.matchDomain(fullRecord);
this.logger.debug('matchDomain:', domainRecord);
//TODO 然后调用接口创建txt类型的dns解析记录 //TODO 然后调用接口创建txt类型的dns解析记录
// .. 这里调用对应平台的后台接口 // .. 这里调用对应平台的后台接口
const access = this.access; const access = this.access;