diff --git a/packages/libs/lib-server/src/user/access/service/access-service.ts b/packages/libs/lib-server/src/user/access/service/access-service.ts index bc19f9644..f99f6330d 100644 --- a/packages/libs/lib-server/src/user/access/service/access-service.ts +++ b/packages/libs/lib-server/src/user/access/service/access-service.ts @@ -1,6 +1,6 @@ import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core'; import {InjectEntityModel} from '@midwayjs/typeorm'; -import {Repository} from 'typeorm'; +import { In, Repository } from "typeorm"; import {AccessGetter, BaseService, PageReq, PermissionException, ValidateException} from '../../../index.js'; import {AccessEntity} from '../entity/access.js'; import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline'; @@ -175,4 +175,27 @@ export class AccessService extends BaseService { getDefineByType(type: string) { return accessRegistry.getDefine(type); } + + + async getSimpleByIds(ids: number[], userId: any) { + if (ids.length === 0) { + return []; + } + if (!userId) { + return []; + } + return await this.repository.find({ + where: { + id: In(ids), + userId, + }, + select: { + id: true, + name: true, + type: true, + userId:true + }, + }); + + } } diff --git a/packages/plugins/plugin-cert/src/dns-provider/api.ts b/packages/plugins/plugin-cert/src/dns-provider/api.ts index 523159fe5..0fd36b771 100644 --- a/packages/plugins/plugin-cert/src/dns-provider/api.ts +++ b/packages/plugins/plugin-cert/src/dns-provider/api.ts @@ -59,3 +59,38 @@ export interface ISubDomainsGetter { export interface IDomainParser { parse(fullDomain: string): Promise; } + +export type DnsVerifier = { + // dns直接校验 + dnsProviderType?: string; + dnsProviderAccessId?: number; +}; + +export type CnameVerifier = { + hostRecord: string; + domain: string; + recordValue: string; +}; + +export type HttpVerifier = { + // http校验 + httpUploaderType: string; + httpUploaderAccess: number; + httpUploadRootDir: string; +}; +export type DomainVerifier = { + domain: string; + mainDomain: string; + type: string; + dns?: DnsVerifier; + cname?: CnameVerifier; + http?: HttpVerifier; +}; + +export type DomainVerifiers = { + [key: string]: DomainVerifier; +}; + +export interface IDomainVerifierGetter { + getVerifiers(domains: string[]): Promise; +} 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 c12a8d084..2d1e4362d 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -23,10 +23,11 @@ export type HttpVerifyPlan = { export type DomainVerifyPlan = { domain: string; + mainDomain: string; type: "cname" | "dns" | "http"; dnsProvider?: IDnsProvider; - cnameVerifyPlan?: Record; - httpVerifyPlan?: Record; + cnameVerifyPlan?: CnameVerifyPlan; + httpVerifyPlan?: HttpVerifyPlan; }; export type DomainsVerifyPlan = { [key: string]: DomainVerifyPlan; @@ -233,23 +234,20 @@ export class AcmeService { let dnsProvider = providers.dnsProvider; let fullRecord = `_acme-challenge.${fullDomain}`; - const origDomain = punycode.toUnicode(domain); + // const origDomain = punycode.toUnicode(domain); const origFullDomain = punycode.toUnicode(fullDomain); if (providers.domainsVerifyPlan) { //按照计划执行 - const domainVerifyPlan = providers.domainsVerifyPlan[origDomain]; + const domainVerifyPlan = providers.domainsVerifyPlan[origFullDomain]; if (domainVerifyPlan) { if (domainVerifyPlan.type === "dns") { dnsProvider = domainVerifyPlan.dnsProvider; } else if (domainVerifyPlan.type === "cname") { - const cnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan; - if (cnameVerifyPlan) { - const cname = cnameVerifyPlan[origFullDomain]; - if (cname) { - dnsProvider = cname.dnsProvider; - domain = await this.options.domainParser.parse(cname.domain); - fullRecord = cname.fullRecord; - } + const cname: CnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan; + if (cname) { + dnsProvider = cname.dnsProvider; + domain = await this.options.domainParser.parse(cname.domain); + fullRecord = cname.fullRecord; } else { this.logger.error(`未找到域名${fullDomain}的CNAME校验计划,请修改证书申请配置`); } @@ -257,13 +255,12 @@ export class AcmeService { throw new Error(`未找到域名${fullDomain}CNAME校验计划的DnsProvider,请修改证书申请配置`); } } else if (domainVerifyPlan.type === "http") { - const httpVerifyPlan = domainVerifyPlan.httpVerifyPlan; - if (httpVerifyPlan) { + const plan: HttpVerifyPlan = domainVerifyPlan.httpVerifyPlan; + if (plan) { const httpChallenge = getChallenge("http-01"); if (httpChallenge == null) { throw new Error("该域名不支持http-01方式校验"); } - const plan = httpVerifyPlan[fullDomain]; return await doHttpVerify(httpChallenge, plan.httpUploader); } else { throw new Error("未找到域名【" + fullDomain + "】的http校验配置"); @@ -272,9 +269,12 @@ export class AcmeService { throw new Error("不支持的校验类型", domainVerifyPlan.type); } } else { - this.logger.info("未找到域名校验计划,使用默认的dnsProvider"); + this.logger.warn(`未找到域名${fullDomain}的校验计划,使用默认的dnsProvider`); } } + if (!dnsProvider) { + throw new Error(`域名${fullDomain}没有匹配到任何校验方式,证书申请失败`); + } const dnsChallenge = getChallenge("dns-01"); return await doDnsVerify(dnsChallenge, fullRecord, dnsProvider); 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 6b6d864a1..f41afe97b 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -1,16 +1,16 @@ import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { utils } from "@certd/basic"; -import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, HttpVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js"; -import { AcmeService } from "./acme.js"; +import { AcmeService, CertInfo, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js"; import * as _ from "lodash-es"; -import { createDnsProvider, DnsProviderContext, IDnsProvider, ISubDomainsGetter } from "../../dns-provider/index.js"; +import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "../../dns-provider/index.js"; import { CertReader } from "./cert-reader.js"; import { CertApplyBasePlugin } from "./base.js"; import { GoogleClient } from "../../libs/google.js"; import { EabAccess } from "../../access"; import { DomainParser } from "../../dns-provider/domain-parser.js"; import { ossClientFactory } from "@certd/plugin-lib"; + export * from "./base.js"; export type { CertInfo }; export * from "./cert-reader.js"; @@ -66,13 +66,15 @@ export class CertApplyPlugin extends CertApplyBasePlugin { { value: "cname", label: "CNAME代理验证" }, { value: "http", label: "HTTP文件验证" }, { value: "dnses", label: "多DNS提供商" }, + { value: "auto", label: "自动匹配" }, ], }, required: true, helper: `1. DNS直接验证:域名dns解析是在阿里云/腾讯云/华为云/CF/NameSilo/西数/火山/dns.la/京东云/51dns的,选它 -2. CNAME代理验证:支持任何注册商的域名,第一次需要手动添加CNAME记录(建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证) +2. CNAME代理验证:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)(建议将DNS服务器修改为阿里云/腾讯云的,然后使用DNS直接验证) 3. HTTP文件验证:不支持泛域名,需要配置网站文件上传 4. 多DNS提供商:每个域名可以选择独立的DNS提供商 +5. 自动匹配:需要在[域名管理](#/certd/cert/domain)中事先配置好校验方式 `, }) challengeType!: string; @@ -333,6 +335,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { acme!: AcmeService; eab!: EabAccess; + async onInit() { let eab: EabAccess = null; @@ -408,7 +411,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin { let dnsProvider: IDnsProvider = null; let domainsVerifyPlan: DomainsVerifyPlan = null; if (this.challengeType === "cname" || this.challengeType === "http" || this.challengeType === "dnses") { - domainsVerifyPlan = await this.createDomainsVerifyPlan(); + domainsVerifyPlan = await this.createDomainsVerifyPlan(domains, this.domainsVerifyPlan); + } else if (this.challengeType === "auto") { + domainsVerifyPlan = await this.createDomainsVerifyPlanByAuto(domains); } else { const dnsProviderType = this.dnsProviderType; const access = await this.getAccess(this.dnsProviderAccess); @@ -444,73 +449,126 @@ export class CertApplyPlugin extends CertApplyBasePlugin { async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise { const domainParser = this.acme.options.domainParser; - const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils, domainParser }; + const context: DnsProviderContext = { + access: dnsProviderAccess, + logger: this.logger, + http: this.ctx.http, + utils, + domainParser, + }; return await createDnsProvider({ dnsProviderType, context, }); } - async createDomainsVerifyPlan(): Promise { + async createDomainsVerifyPlan(domains: string[], verifyPlanSetting: DomainsVerifyPlanInput): Promise { const plan: DomainsVerifyPlan = {}; - for (const domain in this.domainsVerifyPlan) { - const domainVerifyPlan = this.domainsVerifyPlan[domain]; - let dnsProvider = null; - const cnameVerifyPlan: Record = {}; - const httpVerifyPlan: Record = {}; - if (domainVerifyPlan.type === "dns") { - const access = await this.getAccess(domainVerifyPlan.dnsProviderAccessId); - dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, access); - } else if (domainVerifyPlan.type === "cname") { - for (const key in domainVerifyPlan.cnameVerifyPlan) { - const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key); - let dnsProvider = cnameRecord.commonDnsProvider; - if (cnameRecord.cnameProvider.id > 0) { - dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access); - } - cnameVerifyPlan[key] = { - type: "cname", - domain: cnameRecord.cnameProvider.domain, - fullRecord: cnameRecord.recordValue, - dnsProvider, - }; - } - } else if (domainVerifyPlan.type === "http") { - const httpUploaderContext = { - accessService: this.ctx.accessService, - logger: this.logger, - utils, - }; - for (const key in domainVerifyPlan.httpVerifyPlan) { - const httpRecord = domainVerifyPlan.httpVerifyPlan[key]; - const access = await this.getAccess(httpRecord.httpUploaderAccess); - let rootDir = httpRecord.httpUploadRootDir; - if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) { - rootDir = rootDir + "/"; - } - this.logger.info("上传方式", httpRecord.httpUploaderType); - const httpUploader = await ossClientFactory.createOssClientByType(httpRecord.httpUploaderType, { - access, - rootDir: rootDir, - ctx: httpUploaderContext, - }); - httpVerifyPlan[key] = { - type: "http", - domain: key, - httpUploader, - }; - } + + const domainParser = this.acme.options.domainParser; + for (const fullDomain of domains) { + const domain = fullDomain.replaceAll("*.", ""); + const mainDomain = await domainParser.parse(domain); + const planSetting: DomainVerifyPlanInput = verifyPlanSetting[mainDomain]; + if (planSetting.type === "dns") { + plan[domain] = await this.createDnsDomainVerifyPlan(planSetting, domain, mainDomain); + } else if (planSetting.type === "cname") { + plan[domain] = await this.createCnameDomainVerifyPlan(domain, mainDomain); + } else if (planSetting.type === "http") { + plan[domain] = await this.createHttpDomainVerifyPlan(planSetting.httpVerifyPlan[domain], domain, mainDomain); } - plan[domain] = { - domain, - type: domainVerifyPlan.type, - dnsProvider, - cnameVerifyPlan, - httpVerifyPlan, - }; } return plan; } + + private async createDomainsVerifyPlanByAuto(domains: string[]) { + //从数据库里面自动选择校验方式 + // domain list + const domainList = new Set(); + //整理域名 + for (let domain of domains) { + domain = domain.replaceAll("*.", ""); + domainList.add(domain); + } + const domainVerifierGetter: IDomainVerifierGetter = await this.ctx.serviceGetter.get("domainVerifierGetter"); + + const verifiers: DomainVerifiers = await domainVerifierGetter.getVerifiers([...domainList]); + + const plan: DomainsVerifyPlan = {}; + + for (const domain in verifiers) { + const verifier = verifiers[domain]; + if (verifier.type === "dns") { + plan[domain] = await this.createDnsDomainVerifyPlan(verifier.dns, domain, verifier.mainDomain); + } else if (verifier.type === "cname") { + plan[domain] = await this.createCnameDomainVerifyPlan(domain, verifier.mainDomain); + } else if (verifier.type === "http") { + plan[domain] = await this.createHttpDomainVerifyPlan(verifier.http, domain, verifier.mainDomain); + } + } + return plan; + } + + private async createDnsDomainVerifyPlan(planSetting: DnsVerifier, domain: string, mainDomain: string): Promise { + const access = await this.getAccess(planSetting.dnsProviderAccessId); + return { + type: "dns", + mainDomain, + domain, + dnsProvider: await this.createDnsProvider(planSetting.dnsProviderType, access), + }; + } + + private async createHttpDomainVerifyPlan(httpSetting: HttpVerifier, domain: string, mainDomain: string): Promise { + const httpUploaderContext = { + accessService: this.ctx.accessService, + logger: this.logger, + utils, + }; + + const access = await this.getAccess(httpSetting.httpUploaderAccess); + let rootDir = httpSetting.httpUploadRootDir; + if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) { + rootDir = rootDir + "/"; + } + this.logger.info("上传方式", httpSetting.httpUploaderType); + const httpUploader = await ossClientFactory.createOssClientByType(httpSetting.httpUploaderType, { + access, + rootDir: rootDir, + ctx: httpUploaderContext, + }); + return { + type: "http", + domain, + mainDomain, + httpVerifyPlan: { + type: "http", + domain, + httpUploader, + }, + }; + } + + private async createCnameDomainVerifyPlan(domain: string, mainDomain: string): Promise { + const cnameRecord = await this.ctx.cnameProxyService.getByDomain(domain); + if (cnameRecord == null) { + throw new Error(`请先配置${domain}的CNAME记录,并通过校验`); + } + let dnsProvider = cnameRecord.commonDnsProvider; + if (cnameRecord.cnameProvider.id > 0) { + dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access); + } + return { + type: "cname", + domain, + mainDomain, + cnameVerifyPlan: { + domain: cnameRecord.cnameProvider.domain, + fullRecord: cnameRecord.recordValue, + dnsProvider, + }, + }; + } } new CertApplyPlugin(); diff --git a/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/http-verify-plan.vue b/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/http-verify-plan.vue index f0c0bebd9..16c664973 100644 --- a/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/http-verify-plan.vue +++ b/packages/ui/certd-client/src/components/plugins/cert/domains-verify-plan-editor/http-verify-plan.vue @@ -33,6 +33,7 @@ import { Ref, ref, watch, nextTick } from "vue"; import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type"; import { dict } from "@fast-crud/fast-crud"; +import { Dicts } from "/@/components/plugins/lib/dicts"; defineOptions({ name: "HttpVerifyPlan", @@ -68,17 +69,7 @@ async function onRecordChange() { emit("change", records.value); } -const uploaderTypeDict = dict({ - data: [ - { label: "SFTP", value: "sftp" }, - { label: "FTP", value: "ftp" }, - { label: "阿里云OSS", value: "alioss" }, - { label: "腾讯云COS", value: "tencentcos" }, - { label: "七牛OSS", value: "qiniuoss" }, - { label: "S3/Minio", value: "s3" }, - { label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true }, - ], -}); +const uploaderTypeDict = Dicts.uploaderTypeDict; diff --git a/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx b/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx index 769da2de5..ddad615df 100644 --- a/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/cname/record/crud.tsx @@ -1,5 +1,5 @@ import * as api from "./api"; -import { useI18n } from "vue-i18n"; +import { useI18n } from "/src/locales"; import { Ref, ref } from "vue"; import { useRouter } from "vue-router"; import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; diff --git a/packages/ui/certd-client/src/views/certd/cname/record/index.vue b/packages/ui/certd-client/src/views/certd/cname/record/index.vue index 52eadb50b..4ed382d3f 100644 --- a/packages/ui/certd-client/src/views/certd/cname/record/index.vue +++ b/packages/ui/certd-client/src/views/certd/cname/record/index.vue @@ -26,7 +26,7 @@ import { useFs } from "@fast-crud/fast-crud"; import createCrudOptions from "./crud"; import { message, Modal } from "ant-design-vue"; import { DeleteBatch } from "./api"; -import { useI18n } from "vue-i18n"; +import { useI18n } from "/src/locales"; const { t } = useI18n(); diff --git a/packages/ui/certd-client/src/views/certd/history/crud.tsx b/packages/ui/certd-client/src/views/certd/history/crud.tsx index c745721c4..2c2b2a669 100644 --- a/packages/ui/certd-client/src/views/certd/history/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/history/crud.tsx @@ -1,5 +1,5 @@ import * as api from "./api"; -import { useI18n } from "vue-i18n"; +import { useI18n } from "/src/locales"; import { computed, Ref, ref } from "vue"; import { useRouter } from "vue-router"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud"; diff --git a/packages/ui/certd-client/src/views/certd/history/index.vue b/packages/ui/certd-client/src/views/certd/history/index.vue index 17bfb9bcc..dea2c720d 100644 --- a/packages/ui/certd-client/src/views/certd/history/index.vue +++ b/packages/ui/certd-client/src/views/certd/history/index.vue @@ -19,7 +19,7 @@ import { useFs } from "@fast-crud/fast-crud"; import createCrudOptions from "./crud"; import { message, Modal } from "ant-design-vue"; import { DeleteBatch } from "./api"; -import { useI18n } from "vue-i18n"; +import { useI18n } from "/src/locales"; const { t } = useI18n(); diff --git a/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue b/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue index 1fc265e2a..b6615fbef 100644 --- a/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue +++ b/packages/ui/certd-client/src/views/certd/mine/change-password-button.vue @@ -6,7 +6,7 @@