diff --git a/packages/core/pipeline/src/service/cname.ts b/packages/core/pipeline/src/service/cname.ts index 735222fca..6b9ec9579 100644 --- a/packages/core/pipeline/src/service/cname.ts +++ b/packages/core/pipeline/src/service/cname.ts @@ -3,6 +3,7 @@ import { IAccess } from "../access/index.js"; export type CnameProvider = { id: any; domain: string; + subdomain?: string; title?: string; dnsProviderType?: string; access?: IAccess; diff --git a/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-cname.ts b/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-cname.ts index 66c1faaff..e23e36aae 100644 --- a/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-cname.ts +++ b/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-cname.ts @@ -1,13 +1,16 @@ export default { cnameTitle: "CNAME Service Configuration", cnameDescription: - "The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.", + "The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.", cnameLinkText: "CNAME principle and usage instructions", cnameDomain: "CNAME Domain", cnameDomainPlaceholder: "cname.handsfree.work", cnameDomainHelper: - "Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.", + "Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.", cnameDomainPattern: "Domain name cannot contain *", + cnameProviderSubdomain: "Delegated Subdomain", + cnameProviderSubdomainPlaceholder: "sub.example.com", + cnameProviderSubdomainHelper: "Fill this when the CNAME domain is hosted under a delegated subdomain, for example CNAME domain cname.sub.example.com and DNS zone sub.example.com.", dnsProvider: "DNS Provider", dnsProviderAuthorization: "DNS Provider Authorization", }; diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-cname.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-cname.ts index 0da608a2b..03401e66f 100644 --- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-cname.ts +++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-cname.ts @@ -6,6 +6,9 @@ export default { cnameDomainPlaceholder: "cname.handsfree.work", cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名", cnameDomainPattern: "域名不能使用星号", + cnameProviderSubdomain: "托管子域名", + cnameProviderSubdomainPlaceholder: "sub.example.com", + cnameProviderSubdomainHelper: "当CNAME域名本身托管在子域名下时填写,例如 CNAME域名为 cname.sub.example.com,实际DNS托管域为 sub.example.com", dnsProvider: "DNS提供商", dnsProviderAuthorization: "DNS提供商授权", }; diff --git a/packages/ui/certd-client/src/views/sys/cname/provider/crud.tsx b/packages/ui/certd-client/src/views/sys/cname/provider/crud.tsx index 6cd29a7ec..4f98606f0 100644 --- a/packages/ui/certd-client/src/views/sys/cname/provider/crud.tsx +++ b/packages/ui/certd-client/src/views/sys/cname/provider/crud.tsx @@ -97,6 +97,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat width: 200, }, }, + subdomain: { + title: t("certd.cnameProviderSubdomain"), + type: "text", + form: { + component: { + placeholder: t("certd.cnameProviderSubdomainPlaceholder"), + }, + helper: t("certd.cnameProviderSubdomainHelper"), + rules: [{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") }], + }, + column: { + width: 200, + show: false, + }, + }, dnsProviderType: { title: t("certd.dnsProvider"), type: "dict-select", diff --git a/packages/ui/certd-server/db/migration/v10043__cname_provider_subdomain.sql b/packages/ui/certd-server/db/migration/v10043__cname_provider_subdomain.sql new file mode 100644 index 000000000..c8055420b --- /dev/null +++ b/packages/ui/certd-server/db/migration/v10043__cname_provider_subdomain.sql @@ -0,0 +1 @@ +ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100); diff --git a/packages/ui/certd-server/src/modules/cname/entity/cname-provider.ts b/packages/ui/certd-server/src/modules/cname/entity/cname-provider.ts index 4da5fb633..956ef8f16 100644 --- a/packages/ui/certd-server/src/modules/cname/entity/cname-provider.ts +++ b/packages/ui/certd-server/src/modules/cname/entity/cname-provider.ts @@ -11,6 +11,8 @@ export class CnameProviderEntity { userId: number; @Column({ comment: '域名', length: 100 }) domain: string; + @Column({ comment: '子域名托管', length: 100, nullable: true }) + subdomain: string; @Column({ comment: 'DNS提供商类型', name: 'dns_provider_type', length: 20 }) dnsProviderType: string; @Column({ comment: 'DNS授权Id', name: 'access_id' }) diff --git a/packages/ui/certd-server/src/modules/cname/service/cname-provider-service.ts b/packages/ui/certd-server/src/modules/cname/service/cname-provider-service.ts index 26cc12311..ca452079a 100644 --- a/packages/ui/certd-server/src/modules/cname/service/cname-provider-service.ts +++ b/packages/ui/certd-server/src/modules/cname/service/cname-provider-service.ts @@ -98,6 +98,18 @@ export class CnameProviderService extends BaseService { return null; } + async getSubDomains() { + const list = await this.repository.find({ + select: { + subdomain: true, + }, + where: { + disabled: false, + }, + }); + return list.map(item => item.subdomain?.trim()).filter((item): item is string => !!item); + } + async list(req: ListReq): Promise { const list = await super.list(req); const sysPrivateSettings = await this.settingsService.getSetting(SysPrivateSettings); diff --git a/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.test.ts b/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.test.ts new file mode 100644 index 000000000..a1b75336a --- /dev/null +++ b/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.test.ts @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import { describe, it } from "mocha"; +import { SubDomainsGetter } from "./sub-domain-getter.js"; + +describe("SubDomainsGetter", () => { + it("returns subdomains configured on system cname providers", async () => { + const subDomainService = { + async getListByUserId() { + return ["example.com"]; + }, + } as any; + const domainService = { + async findOne() { + return null; + }, + } as any; + const cnameProviderService = { + async getSubDomains() { + return ["cname-hosted.example.com"]; + }, + } as any; + + const getter = new SubDomainsGetter(1, 2, subDomainService, domainService, cnameProviderService); + + assert.deepEqual(await getter.getSubDomains(), ["cname-hosted.example.com", "example.com"]); + assert.equal(await getter.hasSubDomain("txt.certd.cname-hosted.example.com"), "cname-hosted.example.com"); + }); +}); diff --git a/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.ts b/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.ts index 399a23a9d..f5b0f490a 100644 --- a/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.ts +++ b/packages/ui/certd-server/src/modules/pipeline/service/getter/sub-domain-getter.ts @@ -1,22 +1,30 @@ import { ISubDomainsGetter } from "@certd/plugin-cert"; import { SubDomainService } from "../sub-domain-service.js"; import { DomainService } from "../../../cert/service/domain-service.js"; +import { CnameProviderService } from "../../../cname/service/cname-provider-service.js"; export class SubDomainsGetter implements ISubDomainsGetter { userId: number; projectId: number; subDomainService: SubDomainService; domainService: DomainService; + cnameProviderService: CnameProviderService; - constructor(userId: number, projectId: number, subDomainService: SubDomainService, domainService: DomainService) { + constructor(userId: number, projectId: number, subDomainService: SubDomainService, domainService: DomainService, cnameProviderService: CnameProviderService) { this.userId = userId; this.projectId = projectId; this.subDomainService = subDomainService; this.domainService = domainService; + this.cnameProviderService = cnameProviderService; } async getSubDomains() { - return await this.subDomainService.getListByUserId(this.userId, this.projectId) + const projectSubDomains = await this.subDomainService.getListByUserId(this.userId, this.projectId) || []; + const cnameProviderSubDomains = await this.cnameProviderService.getSubDomains(); + return [...projectSubDomains, ...cnameProviderSubDomains] + .map(item => item?.trim()) + .filter((item): item is string => !!item) + .sort((a, b) => b.length - a.length); } async hasSubDomain(fullDomain: string) { diff --git a/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts b/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts index 682c4dd2a..e42b8f116 100644 --- a/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts +++ b/packages/ui/certd-server/src/modules/pipeline/service/getter/task-service-getter.ts @@ -12,6 +12,7 @@ import { SubDomainService } from "../sub-domain-service.js"; import { CertInfoGetter } from "./cert-info-getter.js"; import { CertInfoService } from "../../../monitor/index.js"; import { ICertInfoGetter } from "@certd/plugin-lib"; +import { CnameProviderService } from "../../../cname/service/cname-provider-service.js"; const serviceNames = [ 'ocrService', @@ -53,7 +54,8 @@ export class TaskServiceGetter implements IServiceGetter{ async getSubDomainsGetter(): Promise { const subDomainsService:SubDomainService = await this.appCtx.getAsync("subDomainService") const domainService:DomainService = await this.appCtx.getAsync("domainService") - return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService) + const cnameProviderService:CnameProviderService = await this.appCtx.getAsync("cnameProviderService") + return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService,cnameProviderService) } async getCertInfoGetter(): Promise { @@ -102,4 +104,3 @@ export type TaskServiceCreateReq = { - diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts index 54736f192..84c1c9fc5 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts @@ -112,3 +112,65 @@ describe("AcmeService account config", () => { assert.match(error.message, /请重新获取EAB授权并刷新ACME账号私钥后重试/); }); }); + +describe("AcmeService challenge", () => { + it("parses cname TXT full record to choose the delegated DNS zone", async () => { + const parseCalls: string[] = []; + const service = new AcmeService({ + userId: 1, + userContext: {} as any, + logger: logger as any, + sslProvider: "letsencrypt", + domainParser: { + async parse(fullDomain: string) { + parseCalls.push(fullDomain); + if (fullDomain === "certd-key.cname.sub.example.com") { + return "sub.example.com"; + } + return "example.com"; + }, + } as any, + }); + const dnsProvider = { + usePunyCode() { + return false; + }, + async createRecord(recordReq: any) { + assert.equal(recordReq.domain, "sub.example.com"); + assert.equal(recordReq.fullRecord, "certd-key.cname.sub.example.com"); + assert.equal(recordReq.hostRecord, "certd-key.cname"); + return { id: "record-id" }; + }, + } as any; + + await service.challengeCreateFn( + { + identifier: { + value: "www.example.com", + }, + challenges: [ + { + type: "dns-01", + }, + ], + }, + async () => "key-auth", + { + domainsVerifyPlan: { + "www.example.com": { + type: "cname", + domain: "www.example.com", + mainDomain: "example.com", + cnameVerifyPlan: { + domain: "cname.sub.example.com", + fullRecord: "certd-key.cname.sub.example.com", + dnsProvider, + }, + }, + }, + } + ); + + assert.deepEqual(parseCalls, ["www.example.com", "certd-key.cname.sub.example.com"]); + }); +}); diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts index 0a157d4b4..eb1f52af9 100644 --- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts +++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts @@ -324,8 +324,8 @@ export class AcmeService { const cname: CnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan; if (cname) { dnsProvider = cname.dnsProvider; - domain = await this.options.domainParser.parse(cname.domain); fullRecord = cname.fullRecord; + domain = await this.options.domainParser.parse(fullRecord); } else { this.logger.error(`未找到域名${fullDomain}的CNAME校验计划,请修改证书申请配置`); }