pref: 支持子域名托管的域名证书申请

This commit is contained in:
xiaojunnuo
2025-04-11 12:13:57 +08:00
parent f68af7dcf2
commit 67f956d4a0
21 changed files with 700 additions and 130 deletions

View File

@@ -27,6 +27,7 @@ export type DnsProviderContext = {
logger: ILogger;
http: HttpClient;
utils: typeof utils;
domainParser: IDomainParser;
};
export interface IDnsProvider<T = any> {
@@ -35,3 +36,11 @@ export interface IDnsProvider<T = any> {
removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
setCtx(ctx: DnsProviderContext): void;
}
export interface ISubDomainsGetter {
getSubDomains(): Promise<string[]>;
}
export interface IDomainParser {
parse(fullDomain: string): Promise<string>;
}

View File

@@ -1,6 +1,4 @@
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js";
//@ts-ignore
import psl from "psl";
import { dnsProviderRegistry } from "./registry.js";
import { Decorator } from "@certd/pipeline";
import { HttpClient, ILogger } from "@certd/basic";
@@ -16,6 +14,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
this.http = ctx.http;
}
async parseDomain(fullDomain: string) {
return await this.ctx.domainParser.parse(fullDomain);
}
abstract createRecord(options: CreateRecordOptions): Promise<T>;
abstract onInstance(): Promise<void>;
@@ -23,14 +25,6 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
}
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;
}
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {
const { dnsProviderType, context } = opts;
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);

View File

@@ -0,0 +1,32 @@
import { IDomainParser, ISubDomainsGetter } from "./api";
//@ts-ignore
import psl from "psl";
export class DomainParser implements IDomainParser {
subDomainsGetter: ISubDomainsGetter;
constructor(subDomainsGetter: ISubDomainsGetter) {
this.subDomainsGetter = subDomainsGetter;
}
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 parse(fullDomain: string) {
const subDomains = await this.subDomainsGetter.getSubDomains();
if (subDomains && subDomains.length > 0) {
for (const subDomain of subDomains) {
if (fullDomain.endsWith(subDomain)) {
//找到子域名托管
return subDomain;
}
}
}
return this.parseDomain(fullDomain);
}
}

View File

@@ -5,8 +5,9 @@ import * as _ from "lodash-es";
import { Challenge } from "@certd/acme-client/types/rfc8555";
import { IContext } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
import { IDnsProvider, parseDomain } from "../../dns-provider/index.js";
import { IDnsProvider, IDomainParser } from "../../dns-provider/index.js";
import { HttpChallengeUploader } from "./uploads/api.js";
export type CnameVerifyPlan = {
type?: string;
domain: string;
@@ -61,6 +62,8 @@ type AcmeServiceOptions = {
privateKeyType?: PrivateKeyType;
signal?: AbortSignal;
maxCheckRetryCount?: number;
userId: number;
domainParser: IDomainParser;
};
export class AcmeService {
@@ -174,7 +177,7 @@ export class AcmeService {
this.logger.info("Triggered challengeCreateFn()");
const fullDomain = authz.identifier.value;
let domain = parseDomain(fullDomain);
let domain = await this.options.domainParser.parse(fullDomain);
this.logger.info("主域名为:" + domain);
const getChallenge = (type: string) => {
@@ -240,7 +243,7 @@ export class AcmeService {
const cname = cnameVerifyPlan[fullDomain];
if (cname) {
dnsProvider = cname.dnsProvider;
domain = parseDomain(cname.domain);
domain = await this.options.domainParser.parse(cname.domain);
fullRecord = cname.fullRecord;
}
} else {

View File

@@ -4,12 +4,13 @@ import { utils } from "@certd/basic";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, HttpVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import * as _ from "lodash-es";
import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js";
import { createDnsProvider, DnsProviderContext, IDnsProvider, 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 { httpChallengeUploaderFactory } from "./uploads/factory.js";
import { DomainParser } from "../../dns-provider/domain-parser.js";
export * from "./base.js";
export type { CertInfo };
export * from "./cert-reader.js";
@@ -314,7 +315,10 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
}
}
this.eab = eab;
const subDomainsGetter = await this.ctx.serviceGetter.get<ISubDomainsGetter>("subDomainsGetter");
const domainParser = new DomainParser(subDomainsGetter);
this.acme = new AcmeService({
userId: this.ctx.user.id,
userContext: this.userContext,
logger: this.logger,
sslProvider: this.sslProvider,
@@ -325,8 +329,7 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
privateKeyType: this.privateKeyType,
signal: this.ctx.signal,
maxCheckRetryCount: this.maxCheckRetryCount,
// cnameProxyService: this.ctx.cnameProxyService,
// dnsProviderCreator: this.createDnsProvider.bind(this),
domainParser,
});
}
@@ -387,7 +390,8 @@ HTTP文件验证不支持泛域名需要配置网站文件上传`,
}
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils };
const domainParser = this.acme.options.domainParser;
const context: DnsProviderContext = { access: dnsProviderAccess, logger: this.logger, http: this.ctx.http, utils, domainParser };
return await createDnsProvider({
dnsProviderType,
context,