mirror of https://github.com/certd/certd
perf: 优化站点证书检查页面,检查增加3次重试
parent
aa1da7c11a
commit
e6dd7cd54a
|
@ -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解析记录,通配符域名只能用这种方式
|
||||
* 证书续期:
|
||||
* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
|
||||
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。
|
||||
|
|
|
@ -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 (
|
||||
<a-tooltip title={value} placement="left">
|
||||
<fs-copyable modelValue={value}></fs-copyable>
|
||||
</a-tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
httpsPort: {
|
||||
|
@ -185,7 +192,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
column: {
|
||||
width: 200,
|
||||
sorter: true,
|
||||
show: true
|
||||
show: true,
|
||||
cellRender({ value }) {
|
||||
return (
|
||||
<a-tooltip title={value} placement="left">
|
||||
{value}
|
||||
</a-tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
certProvider: {
|
||||
|
@ -199,7 +213,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
},
|
||||
column: {
|
||||
width: 200,
|
||||
sorter: true
|
||||
sorter: true,
|
||||
cellRender({ value }) {
|
||||
return <a-tooltip title={value}>{value}</a-tooltip>;
|
||||
}
|
||||
}
|
||||
},
|
||||
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 <a-tooltip title={value}>{value}</a-tooltip>;
|
||||
}
|
||||
}
|
||||
},
|
||||
pipelineId: {
|
||||
|
@ -336,7 +358,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
value: false
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
width: 90,
|
||||
sorter: true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<UserPageRes> => {
|
||||
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");
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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<UserPageRes> => {
|
||||
|
@ -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");
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -40,7 +40,7 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
|
|||
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<SiteInfoService> {
|
|||
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<SiteInfoService> {
|
|||
@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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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('站点证书检查完成');
|
||||
|
|
|
@ -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<SiteInfoEntity> {
|
|||
* 检查站点证书过期时间
|
||||
* @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<SiteInfoEntity> {
|
|||
* 检查,但不发邮件
|
||||
* @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<SiteInfoEntity> {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SiteTestRes> {
|
||||
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<SiteTestRes> {
|
||||
const agent = new https.Agent({ keepAlive: false });
|
||||
|
||||
const options: any = merge(
|
||||
|
|
Loading…
Reference in New Issue