diff --git a/packages/core/acme-client/src/auto.js b/packages/core/acme-client/src/auto.js index 3ca254c3..68fb6056 100644 --- a/packages/core/acme-client/src/auto.js +++ b/packages/core/acme-client/src/auto.js @@ -176,21 +176,31 @@ module.exports = async function(client, userOpts) { await challengeFunc(authz); }); - log('开始challenge'); - let promise = Promise.resolve(); - function runPromisesSerially(tasks) { - tasks.forEach((task) => { - promise = promise.then(task); - }); - return promise; + + // let promise = Promise.resolve(); + // function runPromisesSerially(tasks) { + // tasks.forEach((task) => { + // promise = promise.then(task); + // }); + // return promise; + // } + + function runPromiseParallel(tasks) { + return Promise.all(tasks.map((task) => task())); } try { - await runPromisesSerially(challengePromises); + log('开始challenge'); + await runPromiseParallel(challengePromises); + } + catch (e) { + log('challenge失败'); + throw e; } finally { - await runPromisesSerially(clearTasks); + log('清理challenge痕迹'); + await runPromiseParallel(clearTasks); } // try { diff --git a/packages/plugins/plugin-cert/package.json b/packages/plugins/plugin-cert/package.json index 43e8c3d1..bb6394b4 100644 --- a/packages/plugins/plugin-cert/package.json +++ b/packages/plugins/plugin-cert/package.json @@ -20,7 +20,8 @@ "@certd/acme-client": "workspace:^1.20.10", "@certd/pipeline": "workspace:^1.20.10", "jszip": "^3.10.1", - "node-forge": "^0.10.0" + "node-forge": "^0.10.0", + "psl": "^1.9.0" }, "devDependencies": { "@alicloud/cs20151215": "^3.0.3", @@ -35,6 +36,7 @@ "@types/lodash": "^4.14.186", "@types/mocha": "^10.0.0", "@types/node-forge": "^1.3.0", + "@types/psl": "^1.1.3", "@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/parser": "^5.38.1", "chai": "^4.3.6", diff --git a/packages/plugins/plugin-cert/src/dns-provider/api.ts b/packages/plugins/plugin-cert/src/dns-provider/api.ts index 7f5f3902..3c0800b2 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/api.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/api.ts @@ -1,4 +1,4 @@ -import { Registrable } from "@certd/pipeline"; +import { HttpClient, IAccess, ILogger, Registrable } from "@certd/pipeline"; export type DnsProviderDefine = Registrable & { accessType: string; @@ -11,13 +11,21 @@ export type CreateRecordOptions = { fullRecord: string; type: string; value: any; + domain: string; }; export type RemoveRecordOptions = CreateRecordOptions & { record: any; }; +export type DnsProviderContext = { + access: IAccess; + logger: ILogger; + http: HttpClient; +}; + export interface IDnsProvider { onInstance(): Promise; createRecord(options: CreateRecordOptions): Promise; removeRecord(options: RemoveRecordOptions): Promise; + setCtx(ctx: DnsProviderContext): void; } diff --git a/packages/plugins/plugin-cert/src/dns-provider/base.ts b/packages/plugins/plugin-cert/src/dns-provider/base.ts new file mode 100644 index 00000000..b76883cc --- /dev/null +++ b/packages/plugins/plugin-cert/src/dns-provider/base.ts @@ -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; + + abstract onInstance(): Promise; + + abstract removeRecord(options: RemoveRecordOptions): Promise; +} diff --git a/packages/plugins/plugin-cert/src/dns-provider/index.ts b/packages/plugins/plugin-cert/src/dns-provider/index.ts index c97ebdb4..ac60eece 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/index.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/index.ts @@ -1,3 +1,4 @@ export * from "./api"; export * from "./registry"; export * from "./decorator"; +export * from "./base"; 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 3024d82b..788e999c 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -5,6 +5,8 @@ import { Challenge } from "@certd/acme-client/types/rfc8555"; import { Logger } from "log4js"; import { IContext } from "@certd/pipeline"; import { IDnsProvider } from "../../dns-provider"; +import psl from "psl"; + export type CertInfo = { crt: string; key: string; @@ -65,33 +67,43 @@ export class AcmeService { 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) { this.logger.info("Triggered challengeCreateFn()"); /* http-01 */ + const fullDomain = authz.identifier.value; if (challenge.type === "http-01") { const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`; 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 */ this.logger.info(`Would write "${fileContents}" to path "${filePath}"`); // await fs.writeFileAsync(filePath, fileContents); } else if (challenge.type === "dns-01") { /* dns-01 */ - const dnsRecord = `_acme-challenge.${authz.identifier.value}`; + const dnsRecord = `_acme-challenge.${fullDomain}`; 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 */ 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({ fullRecord: dnsRecord, type: "TXT", value: recordValue, + domain, }); } } @@ -111,28 +123,33 @@ export class AcmeService { this.logger.info("Triggered challengeRemoveFn()"); /* http-01 */ + const fullDomain = authz.identifier.value; if (challenge.type === "http-01") { 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 */ this.logger.info(`Would remove file on path "${filePath}"`); // await fs.unlinkAsync(filePath); } else if (challenge.type === "dns-01") { - const dnsRecord = `_acme-challenge.${authz.identifier.value}`; + const dnsRecord = `_acme-challenge.${fullDomain}`; 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 */ this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`); + + const domain = this.parseDomain(fullDomain); + try { await dnsProvider.removeRecord({ fullRecord: dnsRecord, type: "TXT", value: keyAuthorization, record: recordItem, + domain, }); } catch (e) { this.logger.error("删除解析记录出错:", e); 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 b6acaa44..58852e8b 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -1,21 +1,10 @@ -import { - AbstractTaskPlugin, - Decorator, - HttpClient, - IAccessService, - IContext, - IsTaskPlugin, - RunStrategy, - Step, - TaskInput, - TaskOutput -} from "@certd/pipeline"; +import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline"; import dayjs from "dayjs"; -import {AcmeService, CertInfo} from "./acme"; +import { AcmeService, CertInfo } from "./acme"; import _ from "lodash"; -import {Logger} from "log4js"; -import {DnsProviderDefine, dnsProviderRegistry} from "../../dns-provider"; -import {CertReader} from "./cert-reader"; +import { Logger } from "log4js"; +import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider"; +import { CertReader } from "./cert-reader"; import JSZip from "jszip"; export { CertReader }; @@ -242,8 +231,9 @@ export class CertApplyPlugin extends AbstractTaskPlugin { // @ts-ignore 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); + dnsProvider.setCtx(context); await dnsProvider.onInstance(); const cert = await this.acme.order({ diff --git a/packages/ui/certd-client/package.json b/packages/ui/certd-client/package.json index 667b2ae6..ff25d96c 100644 --- a/packages/ui/certd-client/package.json +++ b/packages/ui/certd-client/package.json @@ -26,10 +26,10 @@ "@ant-design/icons-vue": "^6.1.0", "@aws-sdk/client-s3": "^3.383.0", "@aws-sdk/s3-request-presigner": "^3.383.0", - "@fast-crud/fast-crud": "^1.20.2", - "@fast-crud/fast-extends": "^1.20.2", - "@fast-crud/ui-antdv4": "^1.20.2", - "@fast-crud/ui-interface": "^1.20.2", + "@fast-crud/fast-crud": "^1.21.0", + "@fast-crud/fast-extends": "^1.21.0", + "@fast-crud/ui-antdv4": "^1.21.0", + "@fast-crud/ui-interface": "^1.21.0", "@iconify/vue": "^4.1.1", "@soerenmartius/vue3-clipboard": "^0.1.2", "ant-design-vue": "^4.1.2", diff --git a/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts index 7345b9d8..2906dc71 100644 --- a/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-aliyun/dns-provider/aliyun-dns-provider.ts @@ -1,13 +1,7 @@ -import Core from '@alicloud/pop-core'; -import _ from 'lodash'; -import { - CreateRecordOptions, - IDnsProvider, - IsDnsProvider, - RemoveRecordOptions, -} from '@certd/plugin-cert'; -import { Autowire, ILogger } from '@certd/pipeline'; -import { AliyunAccess } from '../access'; +import Core from "@alicloud/pop-core"; +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { Autowire, ILogger } from "@certd/pipeline"; +import { AliyunAccess } from "../access"; @IsDnsProvider({ name: 'aliyun', @@ -15,7 +9,7 @@ import { AliyunAccess } from '../access'; desc: '阿里云DNS解析提供商', accessType: 'aliyun', }) -export class AliyunDnsProvider implements IDnsProvider { +export class AliyunDnsProvider extends AbstractDnsProvider{ client: any; @Autowire() access!: AliyunAccess; @@ -30,71 +24,71 @@ export class AliyunDnsProvider implements IDnsProvider { apiVersion: '2015-01-09', }); } - - async getDomainList() { - const params = { - RegionId: 'cn-hangzhou', - PageSize: 100, - }; - - const requestOption = { - method: 'POST', - }; - - const ret = await this.client.request( - 'DescribeDomains', - params, - requestOption - ); - return ret.Domains.Domain; - } - - async matchDomain(dnsRecord: string) { - const list = await this.getDomainList(); - let domain = null; - const domainList = []; - for (const item of list) { - domainList.push(item.DomainName); - if (_.endsWith(dnsRecord, item.DomainName)) { - domain = item.DomainName; - break; - } - } - if (!domain) { - throw new Error( - `can not find Domain :${dnsRecord} ,list: ${JSON.stringify(domainList)}` - ); - } - return domain; - } - - async getRecords(domain: string, rr: string, value: string) { - const params: any = { - RegionId: 'cn-hangzhou', - DomainName: domain, - RRKeyWord: rr, - ValueKeyWord: undefined, - }; - if (value) { - params.ValueKeyWord = value; - } - - const requestOption = { - method: 'POST', - }; - - const ret = await this.client.request( - 'DescribeDomainRecords', - params, - requestOption - ); - return ret.DomainRecords.Record; - } + // + // async getDomainList() { + // const params = { + // RegionId: 'cn-hangzhou', + // PageSize: 100, + // }; + // + // const requestOption = { + // method: 'POST', + // }; + // + // const ret = await this.client.request( + // 'DescribeDomains', + // params, + // requestOption + // ); + // return ret.Domains.Domain; + // } + // + // async matchDomain(dnsRecord: string) { + // const list = await this.getDomainList(); + // let domain = null; + // const domainList = []; + // for (const item of list) { + // domainList.push(item.DomainName); + // if (_.endsWith(dnsRecord, item.DomainName)) { + // domain = item.DomainName; + // break; + // } + // } + // if (!domain) { + // throw new Error( + // `can not find Domain :${dnsRecord} ,list: ${JSON.stringify(domainList)}` + // ); + // } + // return domain; + // } + // + // async getRecords(domain: string, rr: string, value: string) { + // const params: any = { + // RegionId: 'cn-hangzhou', + // DomainName: domain, + // RRKeyWord: rr, + // ValueKeyWord: undefined, + // }; + // if (value) { + // params.ValueKeyWord = value; + // } + // + // const requestOption = { + // method: 'POST', + // }; + // + // const ret = await this.client.request( + // 'DescribeDomainRecords', + // params, + // requestOption + // ); + // return ret.DomainRecords.Record; + // } async createRecord(options: CreateRecordOptions): Promise { - const { fullRecord, value, type } = options; - this.logger.info('添加域名解析:', fullRecord, value); - const domain = await this.matchDomain(fullRecord); + const { fullRecord, value, type,domain } = options; + this.logger.info('添加域名解析:', fullRecord, value,domain); + // const domain = await this.matchDomain(fullRecord); const rr = fullRecord.replace('.' + domain, ''); const params = { diff --git a/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts index f0beec50..84039a6c 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cloudflare/dns-provider.ts @@ -1,12 +1,6 @@ -import _ from 'lodash'; -import { - CreateRecordOptions, - IDnsProvider, - IsDnsProvider, - RemoveRecordOptions, -} from '@certd/plugin-cert'; -import { Autowire, ILogger } from '@certd/pipeline'; -import { CloudflareAccess } from './access'; +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { Autowire, HttpClient, ILogger } from "@certd/pipeline"; +import { CloudflareAccess } from "./access"; // TODO 这里注册一个dnsProvider @IsDnsProvider({ @@ -15,50 +9,41 @@ import { CloudflareAccess } from './access'; desc: 'cloudflare dns provider示例', accessType: 'cloudflare', }) -export class CloudflareDnsProvider implements IDnsProvider { +export class CloudflareDnsProvider extends AbstractDnsProvider{ @Autowire() + logger! : ILogger; access!: CloudflareAccess; - @Autowire() - logger!: ILogger; - + http!: HttpClient; 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 { - // TODO 这里你要实现一个获取域名列表的方法 - const access = this.access; - this.logger.debug('access', access); - return []; - } - - async matchDomain(dnsRecord: string): Promise { - 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 { - const { fullRecord, value, type } = options; - this.logger.info('添加域名解析:', fullRecord, value, type); - //先确定账户中是否有该域名 - const domainRecord = await this.matchDomain(fullRecord); - this.logger.debug('matchDomain:', domainRecord); + const { fullRecord, value, type,domain } = options; + this.logger.info('添加域名解析:', fullRecord, value, type,domain); + + this.http.post('https://api.cloudflare.com/client/v4/zones/zone_id/dns_records') + //TODO 然后调用接口,创建txt类型的dns解析记录 // .. 这里调用对应平台的后台接口 const access = this.access; diff --git a/packages/ui/certd-server/src/plugins/plugin-demo/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-demo/dns-provider.ts index 7ae8c9d3..cf7c0455 100644 --- a/packages/ui/certd-server/src/plugins/plugin-demo/dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-demo/dns-provider.ts @@ -28,37 +28,10 @@ export class DemoDnsProvider implements IDnsProvider { //... } - async getDomainList(): Promise { - // TODO 这里你要实现一个获取域名列表的方法 - const access = this.access; - this.logger.debug('access', access); - return []; - } - - async matchDomain(dnsRecord: string): Promise { - 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 { - const { fullRecord, value, type } = options; - this.logger.info('添加域名解析:', fullRecord, value, type); - //先确定账户中是否有该域名 - const domainRecord = await this.matchDomain(fullRecord); - this.logger.debug('matchDomain:', domainRecord); + const { fullRecord, value, type,domain } = options; + this.logger.info('添加域名解析:', fullRecord, value, type,domain); //TODO 然后调用接口,创建txt类型的dns解析记录 // .. 这里调用对应平台的后台接口 const access = this.access;