fix: 修复某些情况下cname申请证书报错主域名不一致的bug

v2
xiaojunnuo 2025-09-29 18:58:19 +08:00
parent 9291fa68aa
commit 2671781e1b
1 changed files with 103 additions and 91 deletions

View File

@ -37,7 +37,7 @@ type CnameCheckCacheValue = {
* *
*/ */
@Provide() @Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true}) @Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CnameRecordService extends BaseService<CnameRecordEntity> { export class CnameRecordService extends BaseService<CnameRecordEntity> {
@InjectEntityModel(CnameRecordEntity) @InjectEntityModel(CnameRecordEntity)
repository: Repository<CnameRecordEntity>; repository: Repository<CnameRecordEntity>;
@ -71,16 +71,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
*/ */
async add(param: any): Promise<CnameRecordEntity> { async add(param: any): Promise<CnameRecordEntity> {
if (!param.domain) { if (!param.domain) {
throw new ValidateException('域名不能为空'); throw new ValidateException("域名不能为空");
} }
if (!param.userId) { if (!param.userId) {
throw new ValidateException('userId不能为空'); throw new ValidateException("userId不能为空");
} }
if (param.domain.startsWith('*.')) { if (param.domain.startsWith("*.")) {
param.domain = param.domain.substring(2); param.domain = param.domain.substring(2);
} }
param.domain = param.domain.trim() param.domain = param.domain.trim();
const info = await this.getRepository().findOne({where: {domain: param.domain, userId: param.userId}}); const info = await this.getRepository().findOne({ where: { domain: param.domain, userId: param.userId } });
if (info) { if (info) {
return info; return info;
} }
@ -90,28 +90,28 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//获取默认的cnameProviderId //获取默认的cnameProviderId
cnameProvider = await this.cnameProviderService.getByPriority(); cnameProvider = await this.cnameProviderService.getByPriority();
if (cnameProvider == null) { if (cnameProvider == null) {
throw new ValidateException('找不到CNAME服务请先前往“系统管理->CNAME服务设置”添加CNAME服务'); throw new ValidateException("找不到CNAME服务请先前往“系统管理->CNAME服务设置”添加CNAME服务");
} }
} else { } else {
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId); cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
} }
await this.cnameProviderChanged(param.userId, param, cnameProvider); await this.cnameProviderChanged(param.userId, param, cnameProvider);
param.status = 'cname'; param.status = "cname";
const {id} = await super.add(param); const { id } = await super.add(param);
return await this.info(id); return await this.info(id);
} }
private async cnameProviderChanged(userId: number, param: any, cnameProvider: CnameProviderEntity) { private async cnameProviderChanged(userId: number, param: any, cnameProvider: CnameProviderEntity) {
param.cnameProviderId = cnameProvider.id; param.cnameProviderId = cnameProvider.id;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService) const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter); const domainParser = new DomainParser(subDomainGetter);
const realDomain = await domainParser.parse(param.domain); const realDomain = await domainParser.parse(param.domain);
const prefix = param.domain.replace(realDomain, ''); const prefix = param.domain.replace(realDomain, "");
let hostRecord = `_acme-challenge.${prefix}`; let hostRecord = `_acme-challenge.${prefix}`;
if (hostRecord.endsWith('.')) { if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1); hostRecord = hostRecord.substring(0, hostRecord.length - 1);
} }
param.hostRecord = hostRecord; param.hostRecord = hostRecord;
@ -119,33 +119,33 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
const randomKey = utils.id.simpleNanoId(6).toLowerCase(); const randomKey = utils.id.simpleNanoId(6).toLowerCase();
const userIdHex = utils.hash.toHex(userId) const userIdHex = utils.hash.toHex(userId);
let userKeyHash = "" let userKeyHash = "";
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo) const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}` userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`;
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10) userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10);
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`) logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`);
const cnameKey = `${userKeyHash}-${userIdHex}-${randomKey}`; const cnameKey = `${userKeyHash}-${userIdHex}-${randomKey}`;
const safeDomain = param.domain.replaceAll('.', '-'); const safeDomain = param.domain.replaceAll(".", "-");
param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`; param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`;
} }
async update(param: any) { async update(param: any) {
if (!param.id) { if (!param.id) {
throw new ValidateException('id不能为空'); throw new ValidateException("id不能为空");
} }
const old = await this.info(param.id); const old = await this.info(param.id);
if (!old) { if (!old) {
throw new ValidateException('数据不存在'); throw new ValidateException("数据不存在");
} }
if (param.domain && old.domain !== param.domain) { if (param.domain && old.domain !== param.domain) {
throw new ValidateException('域名不允许修改'); throw new ValidateException("域名不允许修改");
} }
if (param.cnameProviderId && old.cnameProviderId !== param.cnameProviderId) { if (param.cnameProviderId && old.cnameProviderId !== param.cnameProviderId) {
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId); const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
await this.cnameProviderChanged(old.userId, param, cnameProvider); await this.cnameProviderChanged(old.userId, param, cnameProvider);
param.status = 'cname'; param.status = "cname";
} }
return await super.update(param); return await super.update(param);
} }
@ -170,7 +170,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} else { } else {
record.commonDnsProvider = new CommonDnsProvider({ record.commonDnsProvider = new CommonDnsProvider({
config: record.cnameProvider, config: record.cnameProvider,
plusService: this.plusService, plusService: this.plusService
}); });
} }
@ -179,15 +179,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
async getByDomain(domain: string, userId: number, createOnNotFound = true) { async getByDomain(domain: string, userId: number, createOnNotFound = true) {
if (!domain) { if (!domain) {
throw new ValidateException('domain不能为空'); throw new ValidateException("domain不能为空");
} }
if (userId == null) { if (userId == null) {
throw new ValidateException('userId不能为空'); throw new ValidateException("userId不能为空");
} }
let record = await this.getRepository().findOne({where: {domain, userId}}); let record = await this.getRepository().findOne({ where: { domain, userId } });
if (record == null) { if (record == null) {
if (createOnNotFound) { if (createOnNotFound) {
record = await this.add({domain, userId}); record = await this.add({ domain, userId });
} else { } else {
throw new ValidateException(`找不到${domain}的CNAME记录`); throw new ValidateException(`找不到${domain}的CNAME记录`);
} }
@ -203,22 +203,34 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return { return {
...record, ...record,
cnameProvider: { cnameProvider: {
...provider, ...provider
} as CnameProvider, } as CnameProvider
} as CnameRecord; } as CnameRecord;
} }
private async fillMainDomain(record: CnameRecordEntity) { async fillMainDomain(record: CnameRecordEntity, update = true) {
if (!record.mainDomain) { const notMainDomain = !record.mainDomain;
const hasErrorMainDomain = record.mainDomain && !record.mainDomain.includes(".");
if (notMainDomain || hasErrorMainDomain) {
let domainPrefix = record.hostRecord.replace("_acme-challenge", ""); let domainPrefix = record.hostRecord.replace("_acme-challenge", "");
if (domainPrefix.startsWith(".")) { if (domainPrefix.startsWith(".")) {
domainPrefix = domainPrefix.substring(1); domainPrefix = domainPrefix.substring(1);
} }
record.mainDomain = record.domain.replace(domainPrefix+".", "");
await this.update({ if (domainPrefix) {
id: record.id, const prefixStr = domainPrefix + ".";
mainDomain: record.mainDomain record.mainDomain = record.domain.substring(prefixStr.length);
}); }else{
record.mainDomain = record.domain;
}
if (update) {
await this.update({
id: record.id,
mainDomain: record.mainDomain
});
}
} }
} }
@ -231,11 +243,11 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (!bean) { if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`); throw new ValidateException(`CnameRecord:${id} 不存在`);
} }
if (bean.status === 'valid') { if (bean.status === "valid") {
return true; return true;
} }
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService) const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter); const domainParser = new DomainParser(subDomainGetter);
const cacheKey = `cname.record.verify.${bean.id}`; const cacheKey = `cname.record.verify.${bean.id}`;
@ -245,7 +257,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
value = { value = {
validating: false, validating: false,
pass: false, pass: false,
startTime: new Date().getTime(), startTime: new Date().getTime()
}; };
} }
let ttl = 5 * 60 * 1000; let ttl = 5 * 60 * 1000;
@ -267,16 +279,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//公共CNAME //公共CNAME
return new CommonDnsProvider({ return new CommonDnsProvider({
config: cnameProvider, config: cnameProvider,
plusService: this.plusService, plusService: this.plusService
}); });
} }
const serviceGetter = this.taskServiceBuilder.create({userId:cnameProvider.userId}) const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId); const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
const context = {access, logger, http, utils, domainParser,serviceGetter}; const context = { access, logger, http, utils, domainParser, serviceGetter };
const dnsProvider: IDnsProvider = await createDnsProvider({ const dnsProvider: IDnsProvider = await createDnsProvider({
dnsProviderType: cnameProvider.dnsProviderType, dnsProviderType: cnameProvider.dnsProviderType,
context, context
}); });
return dnsProvider; return dnsProvider;
}; };
@ -284,15 +296,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
const clearVerifyRecord = async () => { const clearVerifyRecord = async () => {
cache.delete(cacheKey); cache.delete(cacheKey);
try { try {
let dnsProvider = value.dnsProvider let dnsProvider = value.dnsProvider;
if (!dnsProvider) { if (!dnsProvider) {
dnsProvider = await buildDnsProvider(); dnsProvider = await buildDnsProvider();
} }
await dnsProvider.removeRecord({ await dnsProvider.removeRecord({
recordReq: value.recordReq, recordReq: value.recordReq,
recordRes: value.recordRes, recordRes: value.recordRes
}); });
logger.info('删除CNAME的校验DNS记录成功'); logger.info("删除CNAME的校验DNS记录成功");
} catch (e) { } catch (e) {
logger.error(`删除CNAME的校验DNS记录失败 ${e.message}req:${JSON.stringify(value.recordReq)}recordRes:${JSON.stringify(value.recordRes)}`, e); logger.error(`删除CNAME的校验DNS记录失败 ${e.message}req:${JSON.stringify(value.recordReq)}recordRes:${JSON.stringify(value.recordRes)}`, e);
} }
@ -305,8 +317,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (value.startTime + ttl < new Date().getTime()) { if (value.startTime + ttl < new Date().getTime()) {
logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`); logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`);
clearInterval(value.intervalId); clearInterval(value.intervalId);
await this.updateStatus(bean.id, 'timeout'); await this.updateStatus(bean.id, "timeout");
await clearVerifyRecord() await clearVerifyRecord();
return false; return false;
} }
@ -317,7 +329,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`); logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
//检查是否有重复的acme配置 //检查是否有重复的acme配置
await this.checkRepeatAcmeChallengeRecords(fullDomain,bean.recordValue) await this.checkRepeatAcmeChallengeRecords(fullDomain, bean.recordValue);
// const txtRecords = await dns.promises.resolveTxt(fullDomain); // const txtRecords = await dns.promises.resolveTxt(fullDomain);
// if (txtRecords.length) { // if (txtRecords.length) {
@ -334,9 +346,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (success) { if (success) {
clearInterval(value.intervalId); clearInterval(value.intervalId);
logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`); logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`);
await this.updateStatus(bean.id, 'valid', ""); await this.updateStatus(bean.id, "valid", "");
value.pass = true; value.pass = true;
await clearVerifyRecord() await clearVerifyRecord();
return success; return success;
} }
}; };
@ -347,88 +359,88 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} }
cache.set(cacheKey, value, { cache.set(cacheKey, value, {
ttl: ttl, ttl: ttl
}); });
const domain = await domainParser.parse(bean.recordValue); const domain = await domainParser.parse(bean.recordValue);
const fullRecord = bean.recordValue; const fullRecord = bean.recordValue;
const hostRecord = fullRecord.replace(`.${domain}`, ''); const hostRecord = fullRecord.replace(`.${domain}`, "");
const req = { const req = {
domain: domain, domain: domain,
fullRecord: fullRecord, fullRecord: fullRecord,
hostRecord: hostRecord, hostRecord: hostRecord,
type: 'TXT', type: "TXT",
value: testRecordValue, value: testRecordValue
}; };
const dnsProvider = await buildDnsProvider(); const dnsProvider = await buildDnsProvider();
if(dnsProvider.usePunyCode()){ if (dnsProvider.usePunyCode()) {
//是否需要中文转英文 //是否需要中文转英文
req.domain = dnsProvider.punyCodeEncode(req.domain) req.domain = dnsProvider.punyCodeEncode(req.domain);
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord) req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord);
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord) req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord);
req.value = dnsProvider.punyCodeEncode(req.value) req.value = dnsProvider.punyCodeEncode(req.value);
} }
const recordRes = await dnsProvider.createRecord(req); const recordRes = await dnsProvider.createRecord(req);
value.dnsProvider = dnsProvider; value.dnsProvider = dnsProvider;
value.validating = true; value.validating = true;
value.recordReq = req; value.recordReq = req;
value.recordRes = recordRes; value.recordRes = recordRes;
await this.updateStatus(bean.id, 'validating', ""); await this.updateStatus(bean.id, "validating", "");
value.intervalId = setInterval(async () => { value.intervalId = setInterval(async () => {
try { try {
await checkRecordValue(); await checkRecordValue();
} catch (e) { } catch (e) {
logger.error('检查cname出错', e); logger.error("检查cname出错", e);
await this.updateError(bean.id, e.message); await this.updateError(bean.id, e.message);
} }
}, 10000); }, 10000);
} }
async updateStatus(id: number, status: CnameRecordStatusType, error?: string) { async updateStatus(id: number, status: CnameRecordStatusType, error?: string) {
const updated: any = {status} const updated: any = { status };
if (error != null) { if (error != null) {
updated.error = error updated.error = error;
} }
await this.getRepository().update(id, updated); await this.getRepository().update(id, updated);
} }
async updateError(id: number, error: string) { async updateError(id: number, error: string) {
await this.getRepository().update(id, {error}); await this.getRepository().update(id, { error });
} }
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string,targetCnameDomain:string) { async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
let dnsResolver = null let dnsResolver = null;
try{ try {
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain) dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
}catch (e) { } catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败${e.message}`) logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败${e.message}`);
return return;
} }
let cnameRecords = [] let cnameRecords = [];
try{ try {
cnameRecords = await dnsResolver.resolveCname(acmeRecordDomain); cnameRecords = await dnsResolver.resolveCname(acmeRecordDomain);
}catch (e) { } catch (e) {
logger.error(`查询CNAME记录失败${e.message}`) logger.error(`查询CNAME记录失败${e.message}`);
return return;
} }
targetCnameDomain = targetCnameDomain.toLowerCase() targetCnameDomain = targetCnameDomain.toLowerCase();
targetCnameDomain = punycode.toASCII(targetCnameDomain) targetCnameDomain = punycode.toASCII(targetCnameDomain);
if (cnameRecords.length > 0) { if (cnameRecords.length > 0) {
for (const cnameRecord of cnameRecords) { for (const cnameRecord of cnameRecords) {
if(cnameRecord.toLowerCase() !== targetCnameDomain){ if (cnameRecord.toLowerCase() !== targetCnameDomain) {
//确保只有一个cname记录 //确保只有一个cname记录
throw new Error(`${acmeRecordDomain}存在多个CNAME记录请删除多余的CNAME记录${cnameRecord}`) throw new Error(`${acmeRecordDomain}存在多个CNAME记录请删除多余的CNAME记录${cnameRecord}`);
} }
} }
} }
// 确保权威服务器里面没有纯粹的TXT记录 // 确保权威服务器里面没有纯粹的TXT记录
let txtRecords = [] let txtRecords = [];
try{ try {
const txtRecordRes = await dnsResolver.resolveTxt(acmeRecordDomain); const txtRecordRes = await dnsResolver.resolveTxt(acmeRecordDomain);
if (txtRecordRes && txtRecordRes.length > 0) { if (txtRecordRes && txtRecordRes.length > 0) {
@ -436,13 +448,13 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`TXT records: ${JSON.stringify(txtRecords)}`); logger.info(`TXT records: ${JSON.stringify(txtRecords)}`);
txtRecords = txtRecords.concat(...txtRecordRes); txtRecords = txtRecords.concat(...txtRecordRes);
} }
}catch (e) { } catch (e) {
logger.error(`查询Txt记录失败${e.message}`) logger.error(`查询Txt记录失败${e.message}`);
} }
if (txtRecords.length === 0) { if (txtRecords.length === 0) {
//如果权威服务器中查不到txt无需继续检查 //如果权威服务器中查不到txt无需继续检查
return return;
} }
if (cnameRecords.length > 0) { if (cnameRecords.length > 0) {
// 从cname记录中获取txt记录 // 从cname记录中获取txt记录
@ -451,7 +463,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (res.length > 0) { if (res.length > 0) {
for (const txtRecord of txtRecords) { for (const txtRecord of txtRecords) {
if (!res.includes(txtRecord)) { if (!res.includes(txtRecord)) {
throw new Error(`${acmeRecordDomain}存在多个TXT记录请删除多余的TXT记录:${txtRecord}`) throw new Error(`${acmeRecordDomain}存在多个TXT记录请删除多余的TXT记录:${txtRecord}`);
} }
} }
} }
@ -459,10 +471,10 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} }
async resetStatus (id: number) { async resetStatus(id: number) {
if (!id) { if (!id) {
throw new ValidateException('id不能为空'); throw new ValidateException("id不能为空");
} }
await this.getRepository().update(id, {status: 'cname',mainDomain: ""}); await this.getRepository().update(id, { status: "cname", mainDomain: "" });
} }
} }