Files
certd/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts
T

498 lines
14 KiB
TypeScript
Raw Normal View History

import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {BaseService, NeedSuiteException, NeedVIPException, SysSettingsService} 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, utils} from "@certd/basic";
import {PeerCertificate} from "tls";
import {NotificationService} from "../../pipeline/service/notification-service.js";
import {isComm, isPlus} from "@certd/plus-core";
import {UserSuiteService} from "@certd/commercial-core";
import {UserSettingsService} from "../../mine/service/user-settings-service.js";
import {UserSiteMonitorSetting} from "../../mine/service/models.js";
import {SiteIpService} from "./site-ip-service.js";
import {SiteIpEntity} from "../entity/site-ip.js";
import {Cron} from "../../cron/cron.js";
2024-12-22 14:01:10 +08:00
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
2024-12-22 14:01:10 +08:00
export class SiteInfoService extends BaseService<SiteInfoEntity> {
@InjectEntityModel(SiteInfoEntity)
repository: Repository<SiteInfoEntity>;
@Inject()
notificationService: NotificationService;
2024-12-23 23:33:13 +08:00
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSuiteService: UserSuiteService;
2025-05-25 23:38:25 +08:00
@Inject()
userSettingsService: UserSettingsService;
2025-05-28 13:57:31 +08:00
@Inject()
siteIpService: SiteIpService;
2025-05-25 23:38:25 +08:00
@Inject()
cron: Cron;
2024-12-22 14:01:10 +08:00
//@ts-ignore
getRepository() {
return this.repository;
}
2024-12-23 23:33:13 +08:00
async add(data: SiteInfoEntity) {
if (!data.userId) {
throw new Error("userId is required");
2024-12-23 23:33:13 +08:00
}
if (isComm()) {
2024-12-26 09:02:04 +08:00
const suiteSetting = await this.userSuiteService.getSuiteSetting();
2024-12-23 23:33:13 +08:00
if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(data.userId);
if (userSuite.monitorCount.max != -1 && userSuite.monitorCount.max <= userSuite.monitorCount.used) {
throw new NeedSuiteException("站点监控数量已达上限,请购买或升级套餐");
2024-12-23 23:33:13 +08:00
}
}
} else if (!isPlus()) {
const count = await this.getUserMonitorCount(data.userId);
if (count >= 1) {
throw new NeedVIPException("站点监控数量已达上限,请升级专业版");
}
2024-12-23 23:33:13 +08:00
}
data.disabled = false;
2024-12-23 23:33:13 +08:00
const found = await this.repository.findOne({
where: {
domain: data.domain,
userId: data.userId,
httpsPort: data.httpsPort || 443
}
});
if (found) {
return {id: found.id};
}
2025-03-10 00:06:49 +08:00
2024-12-24 01:12:12 +08:00
return await super.add(data);
}
async update(data: any) {
if (!data.id) {
throw new Error("id is required");
2024-12-24 01:12:12 +08:00
}
delete data.userId;
await super.update(data);
2024-12-23 23:33:13 +08:00
}
2024-12-22 14:01:10 +08:00
async getUserMonitorCount(userId: number) {
if (!userId) {
throw new Error("userId is required");
2024-12-22 14:01:10 +08:00
}
return await this.repository.count({
where: {userId}
2024-12-22 14:01:10 +08:00
});
}
/**
* 检查站点证书过期时间
* @param site
* @param notify
* @param retryTimes
*/
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().valueOf()
});
const res = await siteTester.test({
host: site.domain,
port: site.httpsPort,
retryTimes
});
const certi: PeerCertificate = res.certificate;
if (!certi) {
throw new Error("没有发现证书");
}
const expires = certi.valid_to;
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";
const updateData = {
id: site.id,
certDomains: domains.join(","),
certStatus: status,
certProvider: issuer,
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
checkStatus: "ok"
};
if (site.ipCheck) {
delete updateData.checkStatus
}
await this.update(updateData);
2025-05-28 13:57:31 +08:00
//检查ip
await this.checkAllIp(site);
2025-05-28 13:57:31 +08:00
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);
}
}
}
async checkAllIp(site: SiteInfoEntity) {
if (!site.ipCheck) {
2025-05-28 15:12:54 +08:00
return;
}
const certExpiresTime = site.certExpiresTime;
const onFinished = async (list: SiteIpEntity[]) => {
let errorCount = 0;
let errorMessage = "";
2025-05-28 15:12:54 +08:00
for (const item of list) {
2025-05-28 23:01:55 +08:00
if (!item) {
continue;
}
errorCount++;
if (item.error) {
errorMessage += `${item.ipAddress}${item.error} \n`;
} else if (item.certExpiresTime !== certExpiresTime) {
errorMessage += `${item.ipAddress}:与主站证书过期时间不一致; \n`;
} else {
errorCount--;
2025-05-28 15:12:54 +08:00
}
}
if (errorCount <= 0) {
//检查正常
await this.update({
id: site.id,
checkStatus: "ok",
error: "",
ipErrorCount: 0
});
return;
2025-05-28 15:12:54 +08:00
}
await this.update({
id: site.id,
checkStatus: "error",
2025-05-28 15:12:54 +08:00
error: errorMessage,
ipErrorCount: errorCount
});
2025-05-28 15:12:54 +08:00
try {
site = await this.info(site.id);
await this.sendCheckErrorNotify(site, true);
2025-05-28 15:12:54 +08:00
} catch (e) {
logger.error("send notify error", e);
2025-05-28 15:12:54 +08:00
}
};
await this.siteIpService.checkAll(site, onFinished);
2025-05-28 15:12:54 +08:00
}
/**
2025-05-28 13:57:31 +08:00
* 检查
* @param id
* @param notify
* @param retryTimes
*/
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, retryTimes);
}
async sendCheckErrorNotify(site: SiteInfoEntity, fromIpCheck = false) {
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
// 发邮件
await this.notificationService.send(
{
id: setting?.notificationId,
useDefault: true,
logger: logger,
body: {
url,
title: `站点证书${fromIpCheck ? "(IP)" : ""}检查出错<${site.name}>`,
content: `站点名称: ${site.name} \n站点域名: ${site.domain} \n错误信息:${site.error}`,
errorMessage: site.error
}
},
site.userId
);
}
async sendExpiresNotify(site: SiteInfoEntity) {
2025-03-17 00:19:01 +08:00
const tipDays = 10;
2025-03-17 00:19:01 +08:00
const expires = site.certExpiresTime;
const validDays = dayjs(expires).diff(dayjs(), "day");
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
const content = `站点名称: ${site.name} \n站点域名: ${site.domain} \n证书域名: ${site.certDomains} \n颁发机构: ${site.certProvider} \n过期时间: ${dayjs(site.certExpiresTime).format("YYYY-MM-DD")} \n`;
2025-03-17 00:19:01 +08:00
if (validDays >= 0 && validDays < tipDays) {
// 发通知
await this.notificationService.send(
{
id: setting?.notificationId,
useDefault: true,
logger: logger,
body: {
title: `站点证书即将过期,剩余${validDays}天,<${site.name}>`,
content,
url
}
},
site.userId
);
} else if (validDays < 0) {
//发过期通知
await this.notificationService.send(
{
id: setting?.notificationId,
useDefault: true,
logger: logger,
body: {
title: `站点证书已过期${-validDays}天<${site.name}>`,
content,
url,
2025-05-28 15:12:54 +08:00
errorMessage: "站点证书已过期"
}
},
site.userId
);
}
}
async checkAllByUsers(userId: any) {
if (!userId) {
throw new Error("userId is required");
}
const sites = await this.repository.find({
where: {userId}
});
this.checkList(sites,false);
}
async checkList(sites: SiteInfoEntity[],isCommon: boolean) {
const cache = {}
const getFromCache = async (userId: number) =>{
if (cache[userId]) {
return cache[userId];
}
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting)
cache[userId] = setting
return setting;
}
for (const site of sites) {
let retryTimes = 3;
const setting = await getFromCache(site.userId)
if (isCommon) {
//公共的检查,排除有设置cron的用户
if (setting?.cron) {
//设置了cron,跳过公共检查
continue;
}
retryTimes = setting.retryTimes
}
this.doCheck(site,true,retryTimes).catch(e => {
logger.error(`检查站点证书失败,${site.domain}`, e.message);
});
await utils.sleep(100);
}
}
2025-05-25 23:38:25 +08:00
async getSetting(userId: number) {
2025-05-25 23:38:25 +08:00
return await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting);
}
async saveSetting(userId: number, bean: UserSiteMonitorSetting) {
await this.userSettingsService.saveSetting(userId, bean);
if(bean.cron){
//注册job
await this.registerSiteMonitorJob(userId);
}else{
this.clearSiteMonitorJob(userId);
}
2025-05-25 23:38:25 +08:00
}
2025-05-28 13:57:31 +08:00
async ipCheckChange(req: { id: any; ipCheck: any }) {
2025-05-28 13:57:31 +08:00
await this.update({
id: req.id,
ipCheck: req.ipCheck
2025-05-28 13:57:31 +08:00
});
if (req.ipCheck) {
2025-05-28 13:57:31 +08:00
const site = await this.info(req.id);
await this.siteIpService.sync(site);
2025-05-28 13:57:31 +08:00
}
}
2025-05-28 15:12:54 +08:00
2025-05-28 15:49:48 +08:00
async disabledChange(req: { disabled: any; id: any }) {
await this.update({
id: req.id,
disabled: req.disabled
2025-05-28 15:49:48 +08:00
});
if (!req.disabled) {
2025-05-28 15:49:48 +08:00
const site = await this.info(req.id);
await this.doCheck(site);
}
}
async doImport(req: { text: string; userId: number }) {
if (!req.text) {
throw new Error("text is required");
}
if (!req.userId) {
throw new Error("userId is required");
}
const rows = req.text.split("\n");
const list = [];
for (const item of rows) {
if (!item) {
continue;
}
const arr = item.trim().split(":");
if (arr.length === 0) {
continue;
}
const domain = arr[0];
let port = 443;
let name = domain;
if (arr.length > 1) {
try {
port = parseInt(arr[1] || "443");
} catch (e) {
throw new Error(`${item}格式错误`);
}
}
if (arr.length > 2) {
name = arr[2] || domain;
}
list.push({
domain,
name,
httpsPort: port,
userId: req.userId
});
2025-05-28 15:49:48 +08:00
}
const batchAdd = async (list: any[]) => {
for (const item of list) {
await this.add(item);
}
// await this.checkAllByUsers(req.userId);
};
await batchAdd(list);
2025-05-28 15:49:48 +08:00
}
clearSiteMonitorJob(userId: number) {
this.cron.remove(`siteMonitor-${userId}`);
}
async registerSiteMonitorJob(userId?: number) {
if(!userId){
//注册公共job
logger.info(`注册站点证书检查定时任务`)
this.cron.register({
name: 'siteMonitor',
cron: '0 0 0 * * *',
job:async ()=>{
await this.triggerJobOnce()
},
});
logger.info(`注册站点证书检查定时任务完成`)
}else{
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting);
if (!setting.cron) {
return;
}
//注册个人的
this.cron.register({
name: `siteMonitor-${userId}`,
cron: setting.cron,
job: () => this.triggerJobOnce(userId),
});
}
}
async triggerJobOnce(userId?:number) {
logger.info(`站点证书检查开始执行[${userId??'所有用户'}]`);
const query:any = { disabled: false };
if(userId){
query.userId = userId;
//判断是否已关闭
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId, UserSiteMonitorSetting);
if (!setting.cron) {
return;
}
}
let offset = 0;
const limit = 50;
while (true) {
const res = await this.page({
query: query,
page: { offset, limit },
});
const { records } = res;
if (records.length === 0) {
break;
}
offset += records.length;
const isCommon = !userId;
await this.checkList(records,isCommon);
}
logger.info(`站点证书检查完成[${userId??'所有用户'}]`);
}
2024-12-22 14:01:10 +08:00
}