perf: 支持公共cname服务

pull/243/head
xiaojunnuo 2024-11-08 01:31:20 +08:00
parent fdc6eef921
commit 3c919ee5d1
9 changed files with 90 additions and 34 deletions

View File

@ -1,3 +1,4 @@
export function isDev() { export function isDev() {
return process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'local'; const nodeEnv = process.env.NODE_ENV || '';
return nodeEnv === 'development' || nodeEnv.indexOf('local') >= 0;
} }

View File

@ -3,9 +3,10 @@ import { IAccess } from "../access";
export type CnameProvider = { export type CnameProvider = {
id: any; id: any;
domain: string; domain: string;
dnsProviderType: string; title?: string;
dnsProviderType?: string;
access?: IAccess; access?: IAccess;
accessId: any; accessId?: any;
}; };
export type CnameRecord = { export type CnameRecord = {
@ -15,6 +16,7 @@ export type CnameRecord = {
recordValue: string; recordValue: string;
cnameProvider: CnameProvider; cnameProvider: CnameProvider;
status: string; status: string;
commonDnsProvider?: any;
}; };
export type ICnameProxyService = { export type ICnameProxyService = {
getByDomain: (domain: string) => Promise<CnameRecord>; getByDomain: (domain: string) => Promise<CnameRecord>;

View File

@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { AppKey, PlusRequestService } from '@certd/plus-core'; import { AppKey, PlusRequestService } from '@certd/plus-core';
import { http, HttpRequestConfig, logger } from '@certd/basic'; import { cache, http, HttpRequestConfig, logger } from '@certd/basic';
import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from '../../settings/index.js'; import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from '../../settings/index.js';
import { merge } from 'lodash-es'; import { merge } from 'lodash-es';
@ -75,7 +75,7 @@ export class PlusService {
data: { data: {
userId, userId,
appKey: AppKey, appKey: AppKey,
subjectId: this.getSubjectId(), subjectId: plusRequestService.getSubjectId(),
}, },
}); });
} }
@ -93,9 +93,19 @@ export class PlusService {
} }
async getAccessToken() { async getAccessToken() {
const cacheKey = 'certd:subject:access_token';
const token = cache.get(cacheKey);
if (token) {
return token;
}
const plusRequestService = await this.getPlusRequestService(); const plusRequestService = await this.getPlusRequestService();
await this.register(); await this.register();
return await plusRequestService.getAccessToken(); const res = await plusRequestService.getAccessToken();
const ttl = res.expiresIn * 1000 - Date.now().valueOf();
cache.set(cacheKey, res.accessToken, {
ttl,
});
return res.accessToken;
} }
async requestWithToken(config: HttpRequestConfig) { async requestWithToken(config: HttpRequestConfig) {
@ -103,10 +113,15 @@ export class PlusService {
const token = await this.getAccessToken(); const token = await this.getAccessToken();
merge(config, { merge(config, {
baseURL: plusRequestService.getBaseURL(), baseURL: plusRequestService.getBaseURL(),
method: 'post',
headers: { headers: {
Authorization: token, Authorization: `Berear ${token}`,
}, },
}); });
return await http.request(config); const res = await http.request(config);
if (res.code !== 0) {
throw new Error(res.message);
}
return res.data;
} }
} }

View File

@ -378,10 +378,14 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
} else { } else {
for (const key in domainVerifyPlan.cnameVerifyPlan) { for (const key in domainVerifyPlan.cnameVerifyPlan) {
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key); 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] = { cnameVerifyPlan[key] = {
domain: cnameRecord.cnameProvider.domain, domain: cnameRecord.cnameProvider.domain,
fullRecord: cnameRecord.recordValue, fullRecord: cnameRecord.recordValue,
dnsProvider: await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access), dnsProvider,
}; };
} }
} }

View File

@ -20,7 +20,7 @@
<cname-tip :record="cnameRecord"></cname-tip> <cname-tip :record="cnameRecord"></cname-tip>
</template> </template>
<div v-else class="helper">不要删除CNAME</div> <div v-else class="helper" title="后续自动申请证书需要">不要删除CNAME</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -37,7 +37,8 @@ const statusDict = dict({
{ label: "待设置CNAME", value: "cname", color: "warning" }, { label: "待设置CNAME", value: "cname", color: "warning" },
{ label: "验证中", value: "validating", color: "blue" }, { label: "验证中", value: "validating", color: "blue" },
{ label: "验证成功", value: "valid", color: "green" }, { label: "验证成功", value: "valid", color: "green" },
{ label: "验证失败", value: "failed", color: "red" } { label: "验证失败", value: "failed", color: "red" },
{ label: "验证超时", value: "timeout", color: "red" }
] ]
}); });
@ -67,12 +68,24 @@ function onRecordChange() {
}); });
} }
let refreshIntervalId: any = null;
async function doRefresh() { async function doRefresh() {
if (!props.domain) { if (!props.domain) {
return; return;
} }
cnameRecord.value = await GetByDomain(props.domain); cnameRecord.value = await GetByDomain(props.domain);
onRecordChange(); onRecordChange();
if (cnameRecord.value.status === "validating") {
if (!refreshIntervalId) {
refreshIntervalId = setInterval(async () => {
await doRefresh();
}, 9000);
}
} else {
clearInterval(refreshIntervalId);
refreshIntervalId = null;
}
} }
watch( watch(

View File

@ -171,7 +171,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{ label: "待设置CNAME", value: "cname", color: "warning" }, { label: "待设置CNAME", value: "cname", color: "warning" },
{ label: "验证中", value: "validating", color: "blue" }, { label: "验证中", value: "validating", color: "blue" },
{ label: "验证成功", value: "valid", color: "green" }, { label: "验证成功", value: "valid", color: "green" },
{ label: "验证失败", value: "failed", color: "red" } { label: "验证失败", value: "failed", color: "red" },
{ label: "验证超时", value: "timeout", color: "red" }
] ]
}), }),
addForm: { addForm: {
@ -204,7 +205,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
if (res === true) { if (res === true) {
message.success("验证成功"); message.success("验证成功");
row.status = "valid"; row.status = "valid";
} else if (res === false) {
message.success("验证超时");
row.status = "timeout";
} else {
message.success("开始验证,请耐心等待");
} }
await crudExpose.doRefresh();
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
message.error(e.message); message.error(e.message);

View File

@ -1,5 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export type CnameRecordStatusType = 'cname' | 'validating' | 'valid' | 'error'; export type CnameRecordStatusType = 'cname' | 'validating' | 'valid' | 'error' | 'timeout';
/** /**
* cname record * cname record
*/ */

View File

@ -4,11 +4,11 @@ import { Repository } from 'typeorm';
import { BaseService, PlusService, ValidateException } from '@certd/lib-server'; import { BaseService, PlusService, ValidateException } from '@certd/lib-server';
import { CnameRecordEntity, CnameRecordStatusType } from '../entity/cname-record.js'; import { CnameRecordEntity, CnameRecordStatusType } from '../entity/cname-record.js';
import { createDnsProvider, IDnsProvider, parseDomain } from '@certd/plugin-cert'; import { createDnsProvider, IDnsProvider, parseDomain } from '@certd/plugin-cert';
import { CnameProvider } from '@certd/pipeline'; import { CnameProvider, CnameRecord } from '@certd/pipeline';
import { cache, http, logger, utils } from '@certd/basic'; import { cache, http, logger, utils } from '@certd/basic';
import { AccessService } from '../../pipeline/service/access-service.js'; import { AccessService } from '../../pipeline/service/access-service.js';
import { isDev } from '../../../utils/env.js'; import { isDev } from '@certd/basic';
import { walkTxtRecord } from '@certd/acme-client'; import { walkTxtRecord } from '@certd/acme-client';
import { CnameProviderService } from './cname-provider-service.js'; import { CnameProviderService } from './cname-provider-service.js';
import { CnameProviderEntity } from '../entity/cname-provider.js'; import { CnameProviderEntity } from '../entity/cname-provider.js';
@ -128,8 +128,17 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
// } // }
async getWithAccessByDomain(domain: string, userId: number) { async getWithAccessByDomain(domain: string, userId: number) {
const record = await this.getByDomain(domain, userId); const record: CnameRecord = await this.getByDomain(domain, userId);
record.cnameProvider.access = await this.accessService.getAccessById(record.cnameProvider.accessId, false); if (record.cnameProvider.id > 0) {
//自定义cname服务
record.cnameProvider.access = await this.accessService.getAccessById(record.cnameProvider.accessId, false);
} else {
record.commonDnsProvider = new CommonDnsProvider({
config: record.cnameProvider,
plusService: this.plusService,
});
}
return record; return record;
} }
@ -158,7 +167,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
cnameProvider: { cnameProvider: {
...provider, ...provider,
} as CnameProvider, } as CnameProvider,
}; } as CnameRecord;
} }
/** /**
@ -184,17 +193,20 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
startTime: new Date().getTime(), startTime: new Date().getTime(),
}; };
} }
let ttl = 60 * 60 * 15 * 1000; let ttl = 15 * 60 * 1000;
if (isDev()) { if (isDev()) {
ttl = 30 * 1000; ttl = 30 * 1000;
} }
const recordValue = bean.recordValue.substring(0, bean.recordValue.indexOf('.')); const testRecordValue = 'certd-cname-verify';
const buildDnsProvider = async () => { const buildDnsProvider = async () => {
const cnameProvider = await this.cnameProviderService.info(bean.cnameProviderId); const cnameProvider = await this.cnameProviderService.info(bean.cnameProviderId);
if (cnameProvider == null) { if (cnameProvider == null) {
throw new ValidateException(`CNAME服务:${bean.cnameProviderId} 已被删除请修改CNAME记录重新选择CNAME服务`); throw new ValidateException(`CNAME服务:${bean.cnameProviderId} 已被删除请修改CNAME记录重新选择CNAME服务`);
} }
if (cnameProvider.disabled === true) {
throw new Error(`CNAME服务:${bean.cnameProviderId} 已被禁用`);
}
if (cnameProvider.id < 0) { if (cnameProvider.id < 0) {
//公共CNAME //公共CNAME
@ -218,16 +230,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return true; return true;
} }
if (value.startTime + ttl < new Date().getTime()) { if (value.startTime + ttl < new Date().getTime()) {
logger.warn(`cname验证超时,停止检查,${bean.domain} ${recordValue}`); logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`);
clearInterval(value.intervalId); clearInterval(value.intervalId);
await this.updateStatus(bean.id, 'cname'); await this.updateStatus(bean.id, 'timeout');
return false; return false;
} }
const originDomain = parseDomain(bean.domain); const originDomain = parseDomain(bean.domain);
const fullDomain = `${bean.hostRecord}.${originDomain}`; const fullDomain = `${bean.hostRecord}.${originDomain}`;
logger.info(`检查CNAME配置 ${fullDomain} ${recordValue}`); logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
// const txtRecords = await dns.promises.resolveTxt(fullDomain); // const txtRecords = await dns.promises.resolveTxt(fullDomain);
// if (txtRecords.length) { // if (txtRecords.length) {
@ -240,10 +252,10 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.error(`获取TXT记录失败${e.message}`); logger.error(`获取TXT记录失败${e.message}`);
} }
logger.info(`检查到TXT记录 ${JSON.stringify(records)}`); logger.info(`检查到TXT记录 ${JSON.stringify(records)}`);
const success = records.includes(recordValue); const success = records.includes(testRecordValue);
if (success) { if (success) {
clearInterval(value.intervalId); clearInterval(value.intervalId);
logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${recordValue}`); logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`);
await this.updateStatus(bean.id, 'valid'); await this.updateStatus(bean.id, 'valid');
value.pass = true; value.pass = true;
cache.delete(cacheKey); cache.delete(cacheKey);
@ -257,8 +269,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} 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);
} }
return success;
} }
return success;
}; };
if (value.validating) { if (value.validating) {
@ -278,7 +290,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
fullRecord: fullRecord, fullRecord: fullRecord,
hostRecord: hostRecord, hostRecord: hostRecord,
type: 'TXT', type: 'TXT',
value: recordValue, value: testRecordValue,
}; };
const dnsProvider = await buildDnsProvider(); const dnsProvider = await buildDnsProvider();
const recordRes = await dnsProvider.createRecord(req); const recordRes = await dnsProvider.createRecord(req);

View File

@ -1,10 +1,10 @@
import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert'; import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { PlusService } from '@certd/lib-server'; import { PlusService } from '@certd/lib-server';
export type CnameProvider = { export type CommonCnameProvider = {
id: number; id: number;
domain: string; domain: string;
title: string; title?: string;
}; };
export const CommonProviders = [ export const CommonProviders = [
{ {
@ -16,10 +16,10 @@ export const CommonProviders = [
export class CommonDnsProvider implements IDnsProvider { export class CommonDnsProvider implements IDnsProvider {
ctx: DnsProviderContext; ctx: DnsProviderContext;
config: CnameProvider; config: CommonCnameProvider;
plusService: PlusService; plusService: PlusService;
constructor(opts: { config: CnameProvider; plusService: PlusService }) { constructor(opts: { config: CommonCnameProvider; plusService: PlusService }) {
this.config = opts.config; this.config = opts.config;
this.plusService = opts.plusService; this.plusService = opts.plusService;
} }
@ -34,8 +34,9 @@ export class CommonDnsProvider implements IDnsProvider {
const res = await this.plusService.requestWithToken({ const res = await this.plusService.requestWithToken({
url: '/activation/certd/cname/recordCreate', url: '/activation/certd/cname/recordCreate',
method: 'post',
data: { data: {
subjectId: this.plusService.getSubjectId(), subjectId: await this.plusService.getSubjectId(),
domain: options.domain, domain: options.domain,
hostRecord: options.hostRecord, hostRecord: options.hostRecord,
recordValue: options.value, recordValue: options.value,
@ -47,12 +48,13 @@ export class CommonDnsProvider implements IDnsProvider {
async removeRecord(options: RemoveRecordOptions<any>) { async removeRecord(options: RemoveRecordOptions<any>) {
const res = await this.plusService.requestWithToken({ const res = await this.plusService.requestWithToken({
url: '/activation/certd/cname/recordRemove', url: '/activation/certd/cname/recordRemove',
method: 'post',
data: { data: {
subjectId: this.plusService.getSubjectId(), subjectId: await this.plusService.getSubjectId(),
domain: options.recordReq.domain, domain: options.recordReq.domain,
hostRecord: options.recordReq.hostRecord, hostRecord: options.recordReq.hostRecord,
recordValue: options.recordReq.value, recordValue: options.recordReq.value,
recordId: options.recordRes.id, recordId: options.recordRes.recordId,
providerId: this.config.id, providerId: this.config.id,
}, },
}); });