perf: 站点证书监控通知发送,每天定时检查

This commit is contained in:
xiaojunnuo
2024-12-23 18:11:06 +08:00
parent 89c7f07034
commit bb4910f4e5
16 changed files with 536 additions and 143 deletions
@@ -8,13 +8,16 @@ export class SiteInfoEntity {
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ comment: '站点名称', length: 100 })
@Column({ name: 'name', comment: '站点名称', length: 100 })
name: string;
@Column({ comment: '域名', length: 100 })
@Column({ name: 'domain', comment: '域名', length: 100 })
domain: string;
@Column({ comment: '其他域名', length: 4096 })
domains: string;
@Column({ name: 'https_port', comment: '端口' })
httpsPort: number;
@Column({ name: 'cert_domains', comment: '证书域名', length: 4096 })
certDomains: string;
@Column({ name: 'cert_info', comment: '证书详情', length: 4096 })
certInfo: string;
@Column({ name: 'cert_status', comment: '证书状态', length: 100 })
@@ -29,7 +32,8 @@ export class SiteInfoEntity {
lastCheckTime: number;
@Column({ name: 'check_status', comment: '检查状态' })
checkStatus: string;
@Column({ name: 'error', comment: '错误信息' })
error: string;
@Column({ name: 'pipeline_id', comment: '关联流水线id' })
pipelineId: number;
@@ -1,14 +1,22 @@
import { Provide } from '@midwayjs/core';
import { Inject, Provide } from '@midwayjs/core';
import { BaseService } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm';
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 { PeerCertificate } from 'tls';
import { NotificationService } from '../../pipeline/service/notification-service.js';
@Provide()
export class SiteInfoService extends BaseService<SiteInfoEntity> {
@InjectEntityModel(SiteInfoEntity)
repository: Repository<SiteInfoEntity>;
@Inject()
notificationService: NotificationService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -22,4 +30,150 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
where: { userId },
});
}
/**
* 检查站点证书过期时间
* @param site
* @param notify
*/
async doCheck(site: SiteInfoEntity, notify = true) {
if (!site?.domain) {
throw new Error('站点域名不能为空');
}
try {
const res = await siteTester.test({
host: site.domain,
port: site.httpsPort,
});
const certi: PeerCertificate = res.certificate;
if (!certi) {
return;
}
const expires = certi.valid_to;
const domains = [certi.subject?.CN, ...certi.subjectaltname?.replaceAll('DNS:', '').split(',')];
const issuer = `${certi.issuer.O}<${certi.issuer.CN}>`;
const isExpired = dayjs().valueOf() > dayjs(expires).valueOf();
const status = isExpired ? 'expired' : 'ok';
const updateData = {
id: site.id,
certDomains: domains.join(','),
certStatus: status,
certProvider: issuer,
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
checkStatus: 'ok',
};
await this.update(updateData);
if (!notify) {
return;
}
try {
await this.sendExpiresNotify(site);
} catch (e) {
logger.error('send notify error', e);
}
} catch (e) {
logger.error('check site error', e);
await this.update({
id: site.id,
checkStatus: 'error',
lastCheckTime: dayjs().valueOf(),
error: e.message,
});
if (!notify) {
return;
}
try {
await this.sendCheckErrorNotify(site);
} catch (e) {
logger.error('send notify error', e);
}
}
}
/**
* 检查,但不发邮件
* @param id
* @param notify
*/
async check(id: number, notify = false) {
const site = await this.info(id);
if (!site) {
throw new Error('站点不存在');
}
return await this.doCheck(site, notify);
}
async sendCheckErrorNotify(site: SiteInfoEntity) {
const url = await this.notificationService.getBindUrl('#/certd/monitor/site');
// 发邮件
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
url,
title: `站点证书检查出错<${site.name}>`,
content: `站点名称: ${site.name} \n
站点域名: ${site.domain} \n
错误信息:${site.error}`,
},
},
site.userId
);
}
async sendExpiresNotify(site: SiteInfoEntity) {
const expires = site.certExpiresTime;
const validDays = dayjs(expires).diff(dayjs(), 'day');
const url = await this.notificationService.getBindUrl('#/monitor/site');
const content = `站点名称: ${site.name} \n
站点域名: ${site.domain} \n
证书域名: ${site.certDomains} \n
证书颁发者: ${site.certProvider} \n
过期时间: ${dayjs(site.certExpiresTime).format('YYYY-MM-DD')} \n`;
if (validDays >= 0 && validDays < 10) {
// 发通知
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
title: `站点证书即将过期,剩余${validDays}天,<${site.name}>`,
content,
url,
},
},
site.userId
);
} else if (validDays < 0) {
//发过期通知
await this.notificationService.send(
{
useDefault: true,
logger: logger,
body: {
title: `站点证书已过期${-validDays}天<${site.name}>`,
content,
url,
},
},
site.userId
);
}
}
async checkAll(userId: any) {
if (!userId) {
throw new Error('userId is required');
}
const sites = await this.repository.find({
where: { userId },
});
for (const site of sites) {
await this.doCheck(site);
}
}
}
@@ -0,0 +1,59 @@
import { logger } from '@certd/basic';
import { merge } from 'lodash-es';
import https from 'https';
import { PeerCertificate } from 'tls';
export type SiteTestReq = {
host: string; // 只用域名部分
port?: number;
method?: string;
};
export type SiteTestRes = {
certificate?: PeerCertificate;
};
export class SiteTester {
async test(req: SiteTestReq): Promise<SiteTestRes> {
logger.info('测试站点:', JSON.stringify(req));
const agent = new https.Agent({ keepAlive: false });
const options: any = merge(
{
port: 443,
method: 'GET',
rejectUnauthorized: false,
},
req
);
options.agent = agent;
// 创建 HTTPS 请求
const requestPromise = new Promise((resolve, reject) => {
const req = https.request(options, res => {
// 获取证书
// @ts-ignore
const certificate = res.socket.getPeerCertificate();
// logger.info('证书信息', certificate);
if (certificate.subject == null) {
logger.warn('证书信息为空');
resolve({
certificate: null,
});
}
resolve({
certificate,
});
res.socket.end();
// 关闭响应
res.destroy();
});
req.on('error', e => {
reject(e);
});
req.end();
});
return await requestPromise;
}
}
export const siteTester = new SiteTester();