diff --git a/packages/core/acme-client/src/auto.js b/packages/core/acme-client/src/auto.js index 193a72eb..d8ee534e 100644 --- a/packages/core/acme-client/src/auto.js +++ b/packages/core/acme-client/src/auto.js @@ -118,16 +118,16 @@ module.exports = async (client, userOpts) => { /* Trigger challengeCreateFn() */ log(`[auto] [${d}] Trigger challengeCreateFn()`); const keyAuthorization = await client.getChallengeKeyAuthorization(challenge); - let recordItem = null; + try { - recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization); + const { recordReq, recordRes, dnsProvider } = await opts.challengeCreateFn(authz, challenge, keyAuthorization); log(`[auto] [${d}] challengeCreateFn success`); log(`[auto] [${d}] add challengeRemoveFn()`); clearTasks.push(async () => { /* Trigger challengeRemoveFn(), suppress errors */ log(`[auto] [${d}] Trigger challengeRemoveFn()`); try { - await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem); + await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider); } catch (e) { log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`); diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js index 1b30f4f7..93e147a3 100644 --- a/packages/core/acme-client/src/verify.js +++ b/packages/core/acme-client/src/verify.js @@ -68,6 +68,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) { if (txtRecords.length) { log(`Found ${txtRecords.length} TXT records at ${recordName}`); + log(`TXT records: ${JSON.stringify(txtRecords)}`); return [].concat(...txtRecords); } } diff --git a/packages/core/acme-client/types/index.d.ts b/packages/core/acme-client/types/index.d.ts index b0fc9657..ac469cb9 100644 --- a/packages/core/acme-client/types/index.d.ts +++ b/packages/core/acme-client/types/index.d.ts @@ -55,8 +55,8 @@ export interface ClientExternalAccountBindingOptions { export interface ClientAutoOptions { csr: CsrBuffer | CsrString; - challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise; - challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string, recordRes:any) => Promise; + challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<{recordReq:any,recordRes:any,dnsProvider:any}>; + challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any) => Promise; email?: string; termsOfServiceAgreed?: boolean; skipChallengeVerification?: boolean; diff --git a/packages/core/pipeline/src/core/executor.ts b/packages/core/pipeline/src/core/executor.ts index b4ee8d53..99fb2eaa 100644 --- a/packages/core/pipeline/src/core/executor.ts +++ b/packages/core/pipeline/src/core/executor.ts @@ -10,7 +10,7 @@ import { createAxiosService } from "../utils/util.request.js"; import { IAccessService } from "../access/index.js"; import { RegistryItem } from "../registry/index.js"; import { Decorator } from "../decorator/index.js"; -import { IEmailService } from "../service/index.js"; +import { ICnameProxyService, IEmailService } from "../service/index.js"; import { FileStore } from "./file-store.js"; import { hashUtils, utils } from "../utils/index.js"; // import { TimeoutPromise } from "../utils/util.promise.js"; @@ -21,6 +21,7 @@ export type ExecutorOptions = { onChanged: (history: RunHistory) => Promise; accessService: IAccessService; emailService: IEmailService; + cnameProxyService: ICnameProxyService; fileRootDir?: string; user: UserInfo; }; @@ -221,7 +222,7 @@ export class Executor { //从outputContext读取输入参数 const input = _.cloneDeep(step.input); Decorator.inject(define.input, instance, input, (item, key) => { - if (item.component?.name === "pi-output-selector") { + if (item.component?.name === "output-selector") { const contextKey = input[key]; if (contextKey != null) { if (typeof contextKey !== "string") { @@ -268,6 +269,7 @@ export class Executor { inputChanged, accessService: this.options.accessService, emailService: this.options.emailService, + cnameProxyService: this.options.cnameProxyService, pipelineContext: this.pipelineContext, userContext: this.contextFactory.getContext("user", this.options.user.id), fileStore: new FileStore({ diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index e327adb6..7dc9a4c7 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -3,7 +3,7 @@ import { FileItem, FormItemProps, Pipeline, Runnable, Step } from "../dt/index.j import { FileStore } from "../core/file-store.js"; import { Logger } from "log4js"; import { IAccessService } from "../access/index.js"; -import { IEmailService } from "../service/index.js"; +import { ICnameProxyService, IEmailService } from "../service/index.js"; import { IContext, PluginRequestHandleReq, RunnableCollection } from "../core/index.js"; import { ILogger, logger, utils } from "../utils/index.js"; import { HttpClient } from "../utils/util.request.js"; @@ -70,6 +70,8 @@ export type TaskInstanceContext = { accessService: IAccessService; //邮件服务 emailService: IEmailService; + //cname记录服务 + cnameProxyService: ICnameProxyService; //流水线上下文 pipelineContext: IContext; //用户上下文 @@ -84,7 +86,7 @@ export type TaskInstanceContext = { signal: AbortSignal; //工具类 utils: typeof utils; - + //用户信息 user: UserInfo; }; diff --git a/packages/core/pipeline/src/service/cname.ts b/packages/core/pipeline/src/service/cname.ts new file mode 100644 index 00000000..4cd12a60 --- /dev/null +++ b/packages/core/pipeline/src/service/cname.ts @@ -0,0 +1,16 @@ +export type CnameProvider = { + id: any; + domain: string; + dnsProviderType: string; + accessId: any; +}; +export type CnameRecord = { + id: any; + domain: string; + hostRecord: string; + recordValue: string; + cnameProvider: CnameProvider; +}; +export type ICnameProxyService = { + getByDomain: (domain: string) => Promise; +}; diff --git a/packages/core/pipeline/src/service/index.ts b/packages/core/pipeline/src/service/index.ts index 33c5af08..bee36485 100644 --- a/packages/core/pipeline/src/service/index.ts +++ b/packages/core/pipeline/src/service/index.ts @@ -1 +1,2 @@ export * from "./email.js"; +export * from "./cname.js"; diff --git a/packages/libs/lib-server/src/basic/base-service.ts b/packages/libs/lib-server/src/basic/base-service.ts index 6e35c4cf..1c3fcb8d 100644 --- a/packages/libs/lib-server/src/basic/base-service.ts +++ b/packages/libs/lib-server/src/basic/base-service.ts @@ -192,18 +192,17 @@ export abstract class BaseService { return await qb.getMany(); } - async checkUserId(id: any = 0, userId, userKey = 'userId') { - // @ts-ignore + async checkUserId(id: any = 0, userId: number, userKey = 'userId') { const res = await this.getRepository().findOne({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore select: { [userKey]: true }, - // @ts-ignore where: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore id, }, }); - // @ts-ignore if (!res || res[userKey] === userId) { return; } diff --git a/packages/plugins/plugin-cert/src/dns-provider/api.ts b/packages/plugins/plugin-cert/src/dns-provider/api.ts index 94115e28..015d4d99 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/api.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/api.ts @@ -8,14 +8,16 @@ export type DnsProviderDefine = Registrable & { }; export type CreateRecordOptions = { + domain: string; fullRecord: string; + hostRecord: string; type: string; value: any; - domain: string; }; -export type RemoveRecordOptions = CreateRecordOptions & { +export type RemoveRecordOptions = { + recordReq: CreateRecordOptions; // 本次创建的dns解析记录,实际上就是createRecord接口的返回值 - record: T; + recordRes: T; }; export type DnsProviderContext = { diff --git a/packages/plugins/plugin-cert/src/dns-provider/base.ts b/packages/plugins/plugin-cert/src/dns-provider/base.ts index 9771640f..03cdaf50 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/base.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/base.ts @@ -1,4 +1,5 @@ import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from "./api.js"; +import psl from "psl"; export abstract class AbstractDnsProvider implements IDnsProvider { ctx!: DnsProviderContext; @@ -13,3 +14,11 @@ export abstract class AbstractDnsProvider implements IDnsProvider { abstract removeRecord(options: RemoveRecordOptions): Promise; } + +export function 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; +} 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 1789f534..9b5c2cd3 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -1,13 +1,28 @@ // @ts-ignore import * as acme from "@certd/acme-client"; +import { ClientExternalAccountBindingOptions, UrlMapping } from "@certd/acme-client"; import _ from "lodash-es"; import { Challenge } from "@certd/acme-client/types/rfc8555"; import { Logger } from "log4js"; -import { IContext } from "@certd/pipeline"; -import { IDnsProvider } from "../../dns-provider/index.js"; -import psl from "psl"; -import { ClientExternalAccountBindingOptions, UrlMapping } from "@certd/acme-client"; -import { utils } from "@certd/pipeline"; +import { IContext, utils } from "@certd/pipeline"; +import { IDnsProvider, parseDomain } from "../../dns-provider/index.js"; + +export type CnameVerifyPlan = { + domain: string; + fullRecord: string; + dnsProvider: IDnsProvider; +}; + +export type DomainVerifyPlan = { + domain: string; + type: "cname" | "dns"; + dnsProvider?: IDnsProvider; + cnameVerifyPlan?: Record; +}; +export type DomainsVerifyPlan = { + [key: string]: DomainVerifyPlan; +}; + export type CertInfo = { crt: string; key: string; @@ -132,14 +147,7 @@ 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) { + async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider, domainsVerifyPlan: DomainsVerifyPlan) { this.logger.info("Triggered challengeCreateFn()"); /* http-01 */ @@ -155,21 +163,62 @@ export class AcmeService { // await fs.writeFileAsync(filePath, fileContents); } else if (challenge.type === "dns-01") { /* dns-01 */ - const dnsRecord = `_acme-challenge.${fullDomain}`; + let fullRecord = `_acme-challenge.${fullDomain}`; const recordValue = keyAuthorization; - this.logger.info(`Creating TXT record for ${fullDomain}: ${dnsRecord}`); + this.logger.info(`Creating TXT record for ${fullDomain}: ${fullRecord}`); /* Replace this */ - this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`); + this.logger.info(`Would create TXT record "${fullRecord}" with value "${recordValue}"`); - const domain = this.parseDomain(fullDomain); + let domain = parseDomain(fullDomain); this.logger.info("解析到域名domain=", domain); - return await dnsProvider.createRecord({ - fullRecord: dnsRecord, + + if (domainsVerifyPlan) { + //按照计划执行 + const domainVerifyPlan = domainsVerifyPlan[domain]; + if (domainVerifyPlan) { + if (domainVerifyPlan.type === "dns") { + dnsProvider = domainVerifyPlan.dnsProvider; + } else if (domainVerifyPlan.type === "cname") { + const cnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan; + if (cnameVerifyPlan) { + const cname = cnameVerifyPlan[fullDomain]; + if (cname) { + dnsProvider = cname.dnsProvider; + domain = parseDomain(cname.domain); + fullRecord = cname.fullRecord; + } + } else { + this.logger.error("未找到域名Cname校验计划,使用默认的dnsProvider"); + } + } else { + this.logger.error("不支持的校验类型", domainVerifyPlan.type); + } + } else { + this.logger.info("未找到域名校验计划,使用默认的dnsProvider"); + } + } + + let hostRecord = fullRecord.replace(`${domain}`, ""); + if (hostRecord.endsWith(".")) { + hostRecord = hostRecord.substring(0, hostRecord.length - 1); + } + + const recordReq = { + domain, + fullRecord, + hostRecord, type: "TXT", value: recordValue, - domain, - }); + }; + this.logger.info("添加 TXT 解析记录", JSON.stringify(recordReq)); + const recordRes = await dnsProvider.createRecord(recordReq); + this.logger.info("添加 TXT 解析记录成功", JSON.stringify(recordRes)); + return { + recordReq, + recordRes, + dnsProvider, + }; } } @@ -179,12 +228,13 @@ export class AcmeService { * @param {object} authz Authorization object * @param {object} challenge Selected challenge * @param {string} keyAuthorization Authorization key - * @param recordItem challengeCreateFn create record item + * @param recordReq + * @param recordRes * @param dnsProvider dnsProvider * @returns {Promise} */ - async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) { + async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider) { this.logger.info("Triggered challengeRemoveFn()"); /* http-01 */ @@ -198,24 +248,13 @@ export class AcmeService { this.logger.info(`Would remove file on path "${filePath}"`); // await fs.unlinkAsync(filePath); } else if (challenge.type === "dns-01") { - const dnsRecord = `_acme-challenge.${fullDomain}`; - const recordValue = keyAuthorization; - - 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); - + this.logger.info(`删除 TXT 解析记录:${JSON.stringify(recordReq)} ,recordRes = ${JSON.stringify(recordRes)}`); try { await dnsProvider.removeRecord({ - fullRecord: dnsRecord, - type: "TXT", - value: keyAuthorization, - record: recordItem, - domain, + recordReq, + recordRes, }); + this.logger.info("删除解析记录成功"); } catch (e) { this.logger.error("删除解析记录出错:", e); throw e; @@ -226,12 +265,13 @@ export class AcmeService { async order(options: { email: string; domains: string | string[]; - dnsProvider: any; + dnsProvider?: any; + domainsVerifyPlan?: DomainsVerifyPlan; csrInfo: any; isTest?: boolean; privateKeyType?: string; }): Promise { - const { email, isTest, domains, csrInfo, dnsProvider } = options; + const { email, isTest, domains, csrInfo, dnsProvider, domainsVerifyPlan } = options; const client: acme.Client = await this.getAcmeClient(email, isTest); /* Create CSR */ @@ -271,8 +311,8 @@ export class AcmeService { privateKey ); - if (dnsProvider == null) { - throw new Error("dnsProvider 不能为空"); + if (dnsProvider == null && domainsVerifyPlan == null) { + throw new Error("dnsProvider 、 domainsVerifyPlan 不能都为空"); } /* 自动申请证书 */ const crt = await client.auto({ @@ -281,11 +321,22 @@ export class AcmeService { termsOfServiceAgreed: true, skipChallengeVerification: this.skipLocalVerify, challengePriority: ["dns-01"], - challengeCreateFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string): Promise => { - return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider); + challengeCreateFn: async ( + authz: acme.Authorization, + challenge: Challenge, + keyAuthorization: string + ): Promise<{ recordReq: any; recordRes: any; dnsProvider: any }> => { + return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider, domainsVerifyPlan); }, - challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise => { - return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider); + challengeRemoveFn: async ( + authz: acme.Authorization, + challenge: Challenge, + keyAuthorization: string, + recordReq: any, + recordRes: any, + dnsProvider: IDnsProvider + ): Promise => { + return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider); }, signal: this.options.signal, }); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts index 7b6b31b3..97b24892 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts @@ -24,7 +24,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { col: { span: 24, }, - order: -1, + order: -999, helper: "1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" + "2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" + @@ -39,6 +39,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { name: "a-input", vModel: "value", }, + rules: [{ type: "email" }], required: true, order: -1, helper: "请输入邮箱", 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 3fee9830..832215ed 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -1,13 +1,27 @@ import { Decorator, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from "@certd/pipeline"; -import type { CertInfo, PrivateKeyType, SSLProvider } from "./acme.js"; +import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js"; import { AcmeService } from "./acme.js"; import _ from "lodash-es"; -import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js"; +import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry, IDnsProvider } from "../../dns-provider/index.js"; import { CertReader } from "./cert-reader.js"; import { CertApplyBasePlugin } from "./base.js"; export type { CertInfo }; export * from "./cert-reader.js"; +export type CnameRecordInput = { + id: number; + status: string; +}; +export type DomainVerifyPlanInput = { + domain: string; + type: "cname" | "dns"; + dnsProviderType?: string; + dnsProviderAccessId?: number; + cnameVerifyPlan?: Record; +}; +export type DomainsVerifyPlanInput = { + [key: string]: DomainVerifyPlanInput; +}; @IsTaskPlugin({ name: "CertApply", @@ -26,6 +40,85 @@ export * from "./cert-reader.js"; }, }) export class CertApplyPlugin extends CertApplyBasePlugin { + @TaskInput({ + title: "域名验证方式", + value: "dns", + component: { + name: "a-select", + vModel: "value", + options: [ + { value: "dns", label: "DNS直接验证" }, + { value: "cname", label: "CNAME间接验证" }, + ], + }, + required: true, + helper: + "DNS直接验证:适合域名是在阿里云、腾讯云、华为云、Cloudflare、西数注册的,需要提供Access授权信息。\nCNAME间接验证:支持任何注册商注册的域名,并且不需要提供Access授权信息,但第一次需要手动添加CNAME记录", + }) + challengeType!: string; + + @TaskInput({ + title: "DNS提供商", + component: { + name: "dns-provider-selector", + }, + mergeScript: ` + return { + show: ctx.compute(({form})=>{ + return form.challengeType === 'dns' + }) + } + `, + required: true, + helper: "请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商,请选择CNAME间接验证校验方式", + }) + dnsProviderType!: string; + + @TaskInput({ + title: "DNS解析授权", + component: { + name: "access-selector", + }, + required: true, + helper: "请选择dns解析提供商授权", + mergeScript: `return { + component:{ + type: ctx.compute(({form})=>{ + return form.dnsProviderType + }) + }, + show: ctx.compute(({form})=>{ + return form.challengeType === 'dns' + }) + } + `, + }) + dnsProviderAccess!: number; + + @TaskInput({ + title: "域名验证配置", + component: { + name: "domains-verify-plan-editor", + }, + required: true, + helper: "如果选择CNAME方式,请按照上面的显示,给域名添加CNAME记录", + col: { + span: 24, + }, + mergeScript: `return { + component:{ + domains: ctx.compute(({form})=>{ + return form.domains + }) + }, + show: ctx.compute(({form})=>{ + return form.challengeType === 'cname' + }) + } + `, + }) + domainsVerifyPlan!: DomainsVerifyPlanInput; + @TaskInput({ title: "证书提供商", value: "letsencrypt", @@ -38,7 +131,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { { value: "zerossl", label: "ZeroSSL" }, ], }, - helper: "Let's Encrypt最简单,如果使用ZeroSSL、google证书,需要提供EAB授权", + helper: "Let's Encrypt最简单,如果使用ZeroSSL、Google证书,需要提供EAB授权", required: true, }) sslProvider!: SSLProvider; @@ -46,7 +139,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { @TaskInput({ title: "EAB授权", component: { - name: "pi-access-selector", + name: "access-selector", type: "eab", }, maybeNeed: true, @@ -80,39 +173,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin { // { value: "ec_521", label: "EC 521" }, ], }, + helper: "如无特殊需求,默认即可", required: true, }) privateKeyType!: PrivateKeyType; - @TaskInput({ - title: "DNS提供商", - component: { - name: "pi-dns-provider-selector", - }, - required: true, - helper: - "请选择dns解析提供商,您的域名是在哪里注册的,或者域名的dns解析服务器属于哪个平台\n如果这里没有您需要的dns解析提供商,您需要将域名解析服务器设置成上面的任意一个提供商", - }) - dnsProviderType!: string; - - @TaskInput({ - title: "DNS解析授权", - component: { - name: "pi-access-selector", - }, - required: true, - helper: "请选择dns解析提供商授权", - mergeScript: `return { - component:{ - type: ctx.compute(({form})=>{ - return form.dnsProviderType - }) - } - } - `, - }) - dnsProviderAccess!: string; - @TaskInput({ title: "使用代理", value: false, @@ -120,7 +185,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { name: "a-switch", vModel: "checked", }, - helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问,请尝试开启此选项", + helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问,请尝试开启此选项\n默认情况会进行测试,如果无法访问,将会自动使用代理", }) useProxy = false; @@ -131,7 +196,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { name: "a-switch", vModel: "checked", }, - helper: "如果重试多次出现Authorization not found TXT record,导致无法申请成功,请尝试开启此选项", + helper: "跳过本地校验可以加快申请速度,同时也会增加失败概率。", }) skipLocalVerify = false; @@ -150,14 +215,15 @@ export class CertApplyPlugin extends CertApplyBasePlugin { skipLocalVerify: this.skipLocalVerify, useMappingProxy: this.useProxy, privateKeyType: this.privateKeyType, + // cnameProxyService: this.ctx.cnameProxyService, + // dnsProviderCreator: this.createDnsProvider.bind(this), }); } async doCertApply() { const email = this["email"]; const domains = this["domains"]; - const dnsProviderType = this["dnsProviderType"]; - const dnsProviderAccessId = this["dnsProviderAccess"]; + const csrInfo = _.merge( { country: "CN", @@ -171,26 +237,22 @@ export class CertApplyPlugin extends CertApplyBasePlugin { ); this.logger.info("开始申请证书,", email, domains); - const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType); - const DnsProviderClass = dnsProviderPlugin.target; - const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine; - if (dnsProviderDefine.deprecated) { - throw new Error(dnsProviderDefine.deprecated); + let dnsProvider: any = null; + let domainsVerifyPlan: DomainsVerifyPlan = null; + if (this.challengeType === "cname") { + domainsVerifyPlan = await this.createDomainsVerifyPlan(); + } else { + const dnsProviderType = this.dnsProviderType; + const dnsProviderAccessId = this.dnsProviderAccess; + dnsProvider = await this.createDnsProvider(dnsProviderType, dnsProviderAccessId); } - const access = await this.accessService.getById(dnsProviderAccessId); - - // @ts-ignore - const dnsProvider: IDnsProvider = new DnsProviderClass(); - const context: DnsProviderContext = { access, logger: this.logger, http: this.http, utils }; - Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context); - dnsProvider.setCtx(context); - await dnsProvider.onInstance(); try { const cert = await this.acme.order({ email, domains, dnsProvider, + domainsVerifyPlan, csrInfo, isTest: false, privateKeyType: this.privateKeyType, @@ -207,6 +269,52 @@ export class CertApplyPlugin extends CertApplyBasePlugin { throw e; } } + + async createDnsProvider(dnsProviderType: string, dnsProviderAccessId: number): Promise { + const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType); + const DnsProviderClass = dnsProviderPlugin.target; + const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine; + if (dnsProviderDefine.deprecated) { + throw new Error(dnsProviderDefine.deprecated); + } + const access = await this.accessService.getById(dnsProviderAccessId); + + // @ts-ignore + const dnsProvider: IDnsProvider = new DnsProviderClass(); + const context: DnsProviderContext = { access, logger: this.logger, http: this.http, utils }; + Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context); + dnsProvider.setCtx(context); + await dnsProvider.onInstance(); + return dnsProvider; + } + + async createDomainsVerifyPlan(): Promise { + const plan: DomainsVerifyPlan = {}; + for (const domain in this.domainsVerifyPlan) { + const domainVerifyPlan = this.domainsVerifyPlan[domain]; + let dnsProvider = null; + const cnameVerifyPlan: Record = {}; + if (domainVerifyPlan.type === "dns") { + dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, domainVerifyPlan.dnsProviderAccessId); + } else { + for (const key in domainVerifyPlan.cnameVerifyPlan) { + const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key); + cnameVerifyPlan[key] = { + domain: cnameRecord.cnameProvider.domain, + fullRecord: cnameRecord.recordValue, + dnsProvider: await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.accessId), + }; + } + } + plan[domain] = { + domain, + type: domainVerifyPlan.type, + dnsProvider, + cnameVerifyPlan, + }; + } + return plan; + } } new CertApplyPlugin(); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts index de4e6213..881a7ac7 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts @@ -69,7 +69,7 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin { @TaskInput({ title: "EAB授权", component: { - name: "pi-access-selector", + name: "access-selector", type: "eab", }, maybeNeed: true, diff --git a/packages/ui/certd-client/index.html b/packages/ui/certd-client/index.html index 9abb0bdf..3e60def3 100644 --- a/packages/ui/certd-client/index.html +++ b/packages/ui/certd-client/index.html @@ -4,7 +4,7 @@ - Certd-让你的证书永不过期 + Loading diff --git a/packages/ui/certd-client/package.json b/packages/ui/certd-client/package.json index 258a8690..a3094af8 100644 --- a/packages/ui/certd-client/package.json +++ b/packages/ui/certd-client/package.json @@ -32,6 +32,7 @@ "@soerenmartius/vue3-clipboard": "^0.1.2", "@vue-js-cron/light": "^4.0.5", "ant-design-vue": "^4.1.2", + "async-validator": "^4.2.5", "axios": "^1.7.2", "axios-mock-adapter": "^1.22.0", "base64-js": "^1.5.1", @@ -49,6 +50,7 @@ "nprogress": "^0.2.0", "object-assign": "^4.1.1", "pinia": "2.1.7", + "psl": "^1.9.0", "qiniu-js": "^3.4.2", "sortablejs": "^1.15.2", "vue": "^3.4.21", diff --git a/packages/ui/certd-client/src/components/cron-editor/index.vue b/packages/ui/certd-client/src/components/cron-editor/index.vue index 087324cf..15bce87e 100644 --- a/packages/ui/certd-client/src/components/cron-editor/index.vue +++ b/packages/ui/certd-client/src/components/cron-editor/index.vue @@ -22,6 +22,9 @@ - - diff --git a/packages/ui/certd-client/src/components/editable.vue b/packages/ui/certd-client/src/components/editable.vue index 8e89782d..26a47a04 100644 --- a/packages/ui/certd-client/src/components/editable.vue +++ b/packages/ui/certd-client/src/components/editable.vue @@ -1,5 +1,5 @@