From e6dd7cd54a3e23897031b5df6e0c3cdc0545d35a Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sat, 4 Jan 2025 20:10:00 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E7=AB=99=E7=82=B9?= =?UTF-8?q?=E8=AF=81=E4=B9=A6=E6=A3=80=E6=9F=A5=E9=A1=B5=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=A2=9E=E5=8A=A03=E6=AC=A1=E9=87=8D?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- .../src/views/certd/monitor/site/crud.tsx | 38 +++++++++++++++---- .../src/views/certd/suite/mine/crud.tsx | 7 +++- .../src/views/sys/suite/user-suite/crud.tsx | 6 ++- .../monitor/site-info-controller.ts | 8 ++-- .../src/modules/auto/auto-c-register-cron.ts | 10 +---- .../monitor/service/site-info-service.ts | 36 ++++++++++++++---- .../modules/monitor/service/site-tester.ts | 25 +++++++++++- 8 files changed, 99 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 92d8d89b..6a3b6b8d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系 * 全自动申请证书(支持所有注册商注册的域名) * 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等,目前已支持40+部署插件) +* 支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式 * 支持通配符域名/泛域名,支持多个域名打到一个证书上,支持pem、pfx、der、jks等多种证书格式 * 邮件通知、webhook通知 * 私有化部署,数据保存本地,授权信息加密存储,镜像由Github Actions构建,过程公开透明 @@ -155,9 +156,6 @@ services: ## 六、一些说明 * 本项目ssl证书提供商为letencrypt/Google/ZeroSSL * 申请过程遵循acme协议 -* 需要验证域名所有权,一般有两种方式(目前本项目仅支持dns-01) - * http-01: 在网站根目录下放置一份txt文件 - * dns-01: 需要给域名添加txt解析记录,通配符域名只能用这种方式 * 证书续期: * 实际上没有办法不改变证书文件本身情况下直接续期或者续签。 * 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。 diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx index 93aa68b8..ba7a19b9 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx @@ -145,7 +145,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat search: { show: true }, - type: "text", + type: "copyable", form: { rules: [ { required: true, message: "请输入域名" }, @@ -154,8 +154,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat ] }, column: { - width: 160, - sorter: true + width: 200, + sorter: true, + cellRender({ value }) { + return ( + + + + ); + } } }, httpsPort: { @@ -185,7 +192,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat column: { width: 200, sorter: true, - show: true + show: true, + cellRender({ value }) { + return ( + + {value} + + ); + } } }, certProvider: { @@ -199,7 +213,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }, column: { width: 200, - sorter: true + sorter: true, + cellRender({ value }) { + return {value}; + } } }, certStatus: { @@ -256,7 +273,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat show: false }, column: { - sorter: true + sorter: true, + width: 155 } }, checkStatus: { @@ -268,6 +286,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat dict: dict({ data: [ { label: "正常", value: "ok", color: "green" }, + { label: "检查中", value: "checking", color: "blue" }, { label: "异常", value: "error", color: "red" } ] }), @@ -291,7 +310,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }, column: { width: 200, - sorter: true + sorter: true, + cellRender({ value }) { + return {value}; + } } }, pipelineId: { @@ -336,7 +358,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat value: false }, column: { - width: 100, + width: 90, sorter: true } } diff --git a/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx b/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx index 06efebe5..a2948e00 100644 --- a/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/suite/mine/crud.tsx @@ -5,7 +5,7 @@ import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue"; import SuiteValue from "/@/views/sys/suite/product/suite-value.vue"; import DurationValue from "/@/views/sys/suite/product/duration-value.vue"; import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue"; - +import dayjs from "dayjs"; export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { const pageRequest = async (query: UserPageQuery): Promise => { return await api.GetList(query); @@ -286,7 +286,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat component: { name: "expires-time-text", vModel: "value", - mode: "tag" + mode: "tag", + title: compute(({ value }) => { + return dayjs(value).format("YYYY-MM-DD HH:mm:ss"); + }) } } }, diff --git a/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx b/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx index 62333995..1caf0062 100644 --- a/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx +++ b/packages/ui/certd-client/src/views/sys/suite/user-suite/crud.tsx @@ -7,6 +7,7 @@ import DurationValue from "/@/views/sys/suite/product/duration-value.vue"; import createCrudOptionsUser from "/@/views/sys/authority/user/crud"; import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue"; import SuiteDurationSelector from "../setting/suite-duration-selector.vue"; +import dayjs from "dayjs"; export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { const api = sysUserSuiteApi; const pageRequest = async (query: UserPageQuery): Promise => { @@ -345,7 +346,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat component: { name: "expires-time-text", vModel: "value", - mode: "tag" + mode: "tag", + title: compute(({ value }) => { + return dayjs(value).format("YYYY-MM-DD HH:mm:ss"); + }) } } }, diff --git a/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts b/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts index 53f5a861..f1c7e96f 100644 --- a/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts +++ b/packages/ui/certd-server/src/controller/monitor/site-info-controller.ts @@ -40,7 +40,7 @@ export class SiteInfoController extends CrudController { async add(@Body(ALL) bean: any) { bean.userId = this.getUserId(); const res = await this.service.add(bean); - await this.service.check(res.id); + await this.service.check(res.id, false, 0); return this.ok(res); } @@ -49,7 +49,7 @@ export class SiteInfoController extends CrudController { await this.service.checkUserId(bean.id, this.getUserId()); delete bean.userId; await this.service.update(bean); - await this.service.check(bean.id); + await this.service.check(bean.id, false, 0); return this.ok(); } @Post('/info', { summary: Constants.per.authOnly }) @@ -67,14 +67,14 @@ export class SiteInfoController extends CrudController { @Post('/check', { summary: Constants.per.authOnly }) async check(@Body('id') id: number) { await this.service.checkUserId(id, this.getUserId()); - await this.service.check(id, false); + await this.service.check(id, false, 0); return this.ok(); } @Post('/checkAll', { summary: Constants.per.authOnly }) async checkAll() { const userId = this.getUserId(); - this.service.checkAll(userId); + await this.service.checkAllByUsers(userId); return this.ok(); } } diff --git a/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts b/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts index e3ff67bd..bc873a10 100644 --- a/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts +++ b/packages/ui/certd-server/src/modules/auto/auto-c-register-cron.ts @@ -1,6 +1,6 @@ import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core'; import { PipelineService } from '../pipeline/service/pipeline-service.js'; -import { logger } from '@certd/basic'; +import { logger, utils } from '@certd/basic'; import { SysSettingsService } from '@certd/lib-server'; import { SiteInfoService } from '../monitor/index.js'; import { Cron } from '../cron/cron.js'; @@ -59,13 +59,7 @@ export class AutoCRegisterCron { break; } offset += records.length; - for (const record of records) { - try { - await this.siteInfoService.doCheck(record, true); - } catch (e) { - logger.error(`站点${record.name}检查出错:`, e); - } - } + await this.siteInfoService.checkList(records); } logger.info('站点证书检查完成'); diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts index a180de4f..bc897249 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts @@ -5,7 +5,7 @@ import { Repository } from 'typeorm'; import { SiteInfoEntity } from '../entity/site-info.js'; import { siteTester } from './site-tester.js'; import dayjs from 'dayjs'; -import { logger } from '@certd/basic'; +import { logger, utils } from '@certd/basic'; import { PeerCertificate } from 'tls'; import { NotificationService } from '../../pipeline/service/notification-service.js'; import { isComm, isPlus } from '@certd/plus-core'; @@ -76,23 +76,35 @@ export class SiteInfoService extends BaseService { * 检查站点证书过期时间 * @param site * @param notify + * @param retryTimes */ - async doCheck(site: SiteInfoEntity, notify = true) { + async doCheck(site: SiteInfoEntity, notify = true, retryTimes = 3) { if (!site?.domain) { throw new Error('站点域名不能为空'); } try { + await this.update({ + id: site.id, + checkStatus: 'checking', + lastCheckTime: dayjs, + }); const res = await siteTester.test({ host: site.domain, port: site.httpsPort, + retryTimes, }); const certi: PeerCertificate = res.certificate; if (!certi) { - return; + throw new Error('没有发现证书'); } const expires = certi.valid_to; - const domains = [certi.subject?.CN, ...certi.subjectaltname?.replaceAll('DNS:', '').split(',')]; + const allDomains = certi.subjectaltname?.replaceAll('DNS:', '').split(','); + const mainDomain = certi.subject?.CN; + let domains = allDomains; + if (!allDomains.includes(mainDomain)) { + domains = [mainDomain, ...allDomains]; + } const issuer = `${certi.issuer.O}<${certi.issuer.CN}>`; const isExpired = dayjs().valueOf() > dayjs(expires).valueOf(); const status = isExpired ? 'expired' : 'ok'; @@ -139,13 +151,14 @@ export class SiteInfoService extends BaseService { * 检查,但不发邮件 * @param id * @param notify + * @param retryTimes */ - async check(id: number, notify = false) { + async check(id: number, notify = false, retryTimes = 3) { const site = await this.info(id); if (!site) { throw new Error('站点不存在'); } - return await this.doCheck(site, notify); + return await this.doCheck(site, notify, retryTimes); } async sendCheckErrorNotify(site: SiteInfoEntity) { @@ -206,15 +219,22 @@ export class SiteInfoService extends BaseService { } } - async checkAll(userId: any) { + async checkAllByUsers(userId: any) { if (!userId) { throw new Error('userId is required'); } const sites = await this.repository.find({ where: { userId }, }); + this.checkList(sites); + } + + async checkList(sites: SiteInfoEntity[]) { for (const site of sites) { - await this.doCheck(site); + this.doCheck(site).catch(e => { + logger.error(`检查站点证书失败,${site.domain}`, e.message); + }); + await utils.sleep(200); } } } diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts b/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts index 93ae5835..464bfb16 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts @@ -1,4 +1,4 @@ -import { logger } from '@certd/basic'; +import { logger, utils } from '@certd/basic'; import { merge } from 'lodash-es'; import https from 'https'; import { PeerCertificate } from 'tls'; @@ -6,6 +6,7 @@ export type SiteTestReq = { host: string; // 只用域名部分 port?: number; method?: string; + retryTimes?: number; }; export type SiteTestRes = { @@ -14,6 +15,28 @@ export type SiteTestRes = { export class SiteTester { async test(req: SiteTestReq): Promise { logger.info('测试站点:', JSON.stringify(req)); + const maxRetryTimes = req.retryTimes ?? 3; + let tryCount = 0; + let result: SiteTestRes = {}; + while (true) { + try { + result = await this.doTestOnce(req); + return result; + } catch (e) { + tryCount++; + if (tryCount > maxRetryTimes) { + logger.error(`测试站点出错,重试${maxRetryTimes}次`, e); + throw e; + } + //指数退避 + const time = 2 ** tryCount; + logger.error(`测试站点出错,${time}s后重试`, e); + await utils.sleep(time * 1000); + } + } + } + + async doTestOnce(req: SiteTestReq): Promise { const agent = new https.Agent({ keepAlive: false }); const options: any = merge(