2025-06-06 16:12:30 +08:00
|
|
|
|
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
|
2026-04-11 21:50:44 +08:00
|
|
|
|
import {BaseService, Constants, NeedSuiteException, NeedVIPException, SysSettingsService} from "@certd/lib-server";
|
2025-06-06 16:12:30 +08:00
|
|
|
|
import {InjectEntityModel} from "@midwayjs/typeorm";
|
2026-02-13 00:41:40 +08:00
|
|
|
|
import {In, Repository} from "typeorm";
|
2025-06-06 16:12:30 +08:00
|
|
|
|
import {SiteInfoEntity} from "../entity/site-info.js";
|
|
|
|
|
|
import {siteTester} from "./site-tester.js";
|
2025-06-05 23:31:36 +08:00
|
|
|
|
import dayjs from "dayjs";
|
2025-06-06 16:12:30 +08:00
|
|
|
|
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";
|
2025-06-06 18:20:30 +08:00
|
|
|
|
import {Cron} from "../../cron/cron.js";
|
2025-07-07 00:10:51 +08:00
|
|
|
|
import { dnsContainer } from "./dns-custom.js";
|
2026-03-11 22:38:48 +08:00
|
|
|
|
import { merge } from "lodash-es";
|
2026-04-05 23:49:25 +08:00
|
|
|
|
import { JobHistoryService } from "./job-history-service.js";
|
|
|
|
|
|
import { JobHistoryEntity } from "../entity/job-history.js";
|
2026-04-11 21:50:44 +08:00
|
|
|
|
import { UserService } from "../../sys/authority/service/user-service.js";
|
|
|
|
|
|
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
2024-12-22 14:01:10 +08:00
|
|
|
|
|
|
|
|
|
|
@Provide()
|
2025-06-06 16:12:30 +08:00
|
|
|
|
@Scope(ScopeEnum.Request, {allowDowngrade: true})
|
2024-12-22 14:01:10 +08:00
|
|
|
|
export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
|
|
|
|
|
@InjectEntityModel(SiteInfoEntity)
|
|
|
|
|
|
repository: Repository<SiteInfoEntity>;
|
|
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
@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
|
|
|
|
|
2026-04-05 23:49:25 +08:00
|
|
|
|
@Inject()
|
|
|
|
|
|
jobHistoryService: JobHistoryService;
|
|
|
|
|
|
|
2026-04-11 21:50:44 +08:00
|
|
|
|
@Inject()
|
|
|
|
|
|
userService: UserService;
|
|
|
|
|
|
@Inject()
|
|
|
|
|
|
projectService: ProjectService;
|
2025-06-06 18:20:30 +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) {
|
2026-02-13 23:51:19 +08:00
|
|
|
|
if (data.userId == null) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
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) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
throw new NeedSuiteException("站点监控数量已达上限,请购买或升级套餐");
|
2024-12-23 23:33:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-05 23:31:36 +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
|
|
|
|
}
|
2025-06-05 23:31:36 +08:00
|
|
|
|
data.disabled = false;
|
2024-12-23 23:33:13 +08:00
|
|
|
|
|
2025-06-05 23:31:36 +08:00
|
|
|
|
const found = await this.repository.findOne({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
domain: data.domain,
|
|
|
|
|
|
userId: data.userId,
|
|
|
|
|
|
httpsPort: data.httpsPort || 443
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if (found) {
|
2025-06-06 16:12:30 +08:00
|
|
|
|
return {id: found.id};
|
2025-06-05 23:31:36 +08:00
|
|
|
|
}
|
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) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
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) {
|
2026-02-13 23:51:19 +08:00
|
|
|
|
if (userId==null) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
throw new Error("userId is required");
|
2024-12-22 14:01:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
return await this.repository.count({
|
2025-06-06 16:12:30 +08:00
|
|
|
|
where: {userId}
|
2024-12-22 14:01:10 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查站点证书过期时间
|
|
|
|
|
|
* @param site
|
|
|
|
|
|
* @param notify
|
2025-01-04 20:10:00 +08:00
|
|
|
|
* @param retryTimes
|
2024-12-23 18:11:06 +08:00
|
|
|
|
*/
|
2025-06-13 00:25:08 +08:00
|
|
|
|
async doCheck(site: SiteInfoEntity, notify = true, retryTimes = null) {
|
2024-12-23 18:11:06 +08:00
|
|
|
|
if (!site?.domain) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
throw new Error("站点域名不能为空");
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
2025-07-07 00:10:51 +08:00
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId,site.projectId, UserSiteMonitorSetting);
|
2025-07-07 00:10:51 +08:00
|
|
|
|
const dnsServer = setting.dnsServer
|
2025-07-31 10:44:50 +08:00
|
|
|
|
let customDns = null
|
2025-07-07 00:10:51 +08:00
|
|
|
|
if (dnsServer && dnsServer.length > 0) {
|
2025-07-31 10:44:50 +08:00
|
|
|
|
customDns = dnsContainer.getDns(dnsServer) as any
|
2025-07-07 00:10:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
try {
|
2025-01-04 20:10:00 +08:00
|
|
|
|
await this.update({
|
|
|
|
|
|
id: site.id,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
checkStatus: "checking",
|
|
|
|
|
|
lastCheckTime: dayjs().valueOf()
|
2025-01-04 20:10:00 +08:00
|
|
|
|
});
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const res = await siteTester.test({
|
|
|
|
|
|
host: site.domain,
|
|
|
|
|
|
port: site.httpsPort,
|
2025-07-07 00:10:51 +08:00
|
|
|
|
retryTimes,
|
2026-02-13 23:51:19 +08:00
|
|
|
|
customDns,
|
|
|
|
|
|
ipAddress: site.ipAddress,
|
2024-12-23 18:11:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const certi: PeerCertificate = res.certificate;
|
|
|
|
|
|
if (!certi) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
throw new Error("没有发现证书");
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
2025-09-13 23:40:06 +08:00
|
|
|
|
const effective = certi.valid_from;
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const expires = certi.valid_to;
|
2025-06-05 23:31:36 +08:00
|
|
|
|
const allDomains = certi.subjectaltname?.replaceAll("DNS:", "").split(",") || [];
|
2025-01-04 20:10:00 +08:00
|
|
|
|
const mainDomain = certi.subject?.CN;
|
|
|
|
|
|
let domains = allDomains;
|
|
|
|
|
|
if (!allDomains.includes(mainDomain)) {
|
|
|
|
|
|
domains = [mainDomain, ...allDomains];
|
|
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const issuer = `${certi.issuer.O}<${certi.issuer.CN}>`;
|
|
|
|
|
|
const isExpired = dayjs().valueOf() > dayjs(expires).valueOf();
|
2025-06-05 23:31:36 +08:00
|
|
|
|
const status = isExpired ? "expired" : "ok";
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const updateData = {
|
|
|
|
|
|
id: site.id,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
certDomains: domains.join(","),
|
2024-12-23 18:11:06 +08:00
|
|
|
|
certStatus: status,
|
|
|
|
|
|
certProvider: issuer,
|
2025-09-13 23:40:06 +08:00
|
|
|
|
certEffectiveTime: dayjs(effective).valueOf(),
|
2024-12-23 18:11:06 +08:00
|
|
|
|
certExpiresTime: dayjs(expires).valueOf(),
|
|
|
|
|
|
lastCheckTime: dayjs().valueOf(),
|
|
|
|
|
|
error: null,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
checkStatus: "ok"
|
2024-12-23 18:11:06 +08:00
|
|
|
|
};
|
2025-09-13 23:40:06 +08:00
|
|
|
|
logger.info(`测试站点成功:id=${updateData.id},site=${site.name},certEffectiveTime=${updateData.certEffectiveTime},expiresTime=${updateData.certExpiresTime}`)
|
2025-06-05 23:31:36 +08:00
|
|
|
|
if (site.ipCheck) {
|
2025-06-06 16:12:30 +08:00
|
|
|
|
delete updateData.checkStatus
|
2025-06-05 23:31:36 +08:00
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
await this.update(updateData);
|
2025-05-28 13:57:31 +08:00
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId,site.projectId, UserSiteMonitorSetting)
|
2026-03-11 22:38:48 +08:00
|
|
|
|
|
|
|
|
|
|
merge(site,updateData)
|
2025-05-28 13:57:31 +08:00
|
|
|
|
//检查ip
|
2026-01-09 12:25:56 +08:00
|
|
|
|
await this.checkAllIp(site,retryTimes,setting);
|
2025-05-28 13:57:31 +08:00
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
if (!notify) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-14 23:24:47 +08:00
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
try {
|
2026-01-09 12:25:56 +08:00
|
|
|
|
await this.sendExpiresNotify(site.id,setting);
|
2024-12-23 18:11:06 +08:00
|
|
|
|
} catch (e) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
logger.error("send notify error", e);
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
logger.error("check site error", e);
|
2026-01-28 19:22:00 +08:00
|
|
|
|
let message = e.message
|
|
|
|
|
|
if (!message){
|
|
|
|
|
|
message = e.code
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.errors &&e.errors.length > 0){
|
|
|
|
|
|
message += "\n"+e.errors.map((item:any)=>item.message).join("\n")
|
|
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
await this.update({
|
|
|
|
|
|
id: site.id,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
checkStatus: "error",
|
2024-12-23 18:11:06 +08:00
|
|
|
|
lastCheckTime: dayjs().valueOf(),
|
2026-01-28 19:22:00 +08:00
|
|
|
|
error: message
|
2024-12-23 18:11:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
if (!notify) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2026-01-09 12:25:56 +08:00
|
|
|
|
await this.sendCheckErrorNotify(site.id,false,setting);
|
2024-12-23 18:11:06 +08:00
|
|
|
|
} catch (e) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
logger.error("send notify error", e);
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 12:25:56 +08:00
|
|
|
|
async checkAllIp(site: SiteInfoEntity,retryTimes = null,setting: UserSiteMonitorSetting) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
if (!site.ipCheck) {
|
2025-05-28 15:12:54 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const certExpiresTime = site.certExpiresTime;
|
2026-01-09 12:25:56 +08:00
|
|
|
|
const tipDays = setting?.certValidDays || 10;
|
2025-06-05 23:31:36 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-06-05 23:31:36 +08:00
|
|
|
|
errorCount++;
|
2026-01-09 12:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
const isExpired = dayjs().valueOf() > dayjs(item.certExpiresTime).valueOf();
|
|
|
|
|
|
const isWillExpired = dayjs().valueOf() > dayjs(item.certExpiresTime).subtract(tipDays, "day").valueOf();
|
|
|
|
|
|
|
2025-06-05 23:31:36 +08:00
|
|
|
|
if (item.error) {
|
|
|
|
|
|
errorMessage += `${item.ipAddress}:${item.error}; \n`;
|
2026-01-09 12:25:56 +08:00
|
|
|
|
} else if (item.certExpiresTime !== certExpiresTime && !site.ipIgnoreCoherence) {
|
2025-12-29 10:31:11 +08:00
|
|
|
|
errorMessage += `${item.ipAddress}:与主站证书过期时间不一致(主站:${dayjs(certExpiresTime).format("YYYY-MM-DD")},IP:${dayjs(item.certExpiresTime).format("YYYY-MM-DD")}); \n`;
|
2026-01-09 12:25:56 +08:00
|
|
|
|
} else if (isExpired){
|
|
|
|
|
|
errorMessage += `${item.ipAddress}:证书已过期(过期时间:${dayjs(item.certExpiresTime).format("YYYY-MM-DD")}); \n`;
|
|
|
|
|
|
}else if (isWillExpired){
|
|
|
|
|
|
errorMessage += `${item.ipAddress}:证书将过期(过期时间:${dayjs(item.certExpiresTime).format("YYYY-MM-DD")}); \n`;
|
|
|
|
|
|
}else {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
errorCount--;
|
2025-05-28 15:12:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-05 23:31:36 +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,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
checkStatus: "error",
|
2025-05-28 15:12:54 +08:00
|
|
|
|
error: errorMessage,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
ipErrorCount: errorCount
|
|
|
|
|
|
});
|
2025-05-28 15:12:54 +08:00
|
|
|
|
try {
|
2026-01-09 12:25:56 +08:00
|
|
|
|
await this.sendCheckErrorNotify(site.id, true,setting);
|
2025-05-28 15:12:54 +08:00
|
|
|
|
} catch (e) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
logger.error("send notify error", e);
|
2025-05-28 15:12:54 +08:00
|
|
|
|
}
|
2025-06-05 23:31:36 +08:00
|
|
|
|
};
|
2026-01-09 12:25:56 +08:00
|
|
|
|
if (site.ipSyncAuto === false) {
|
2026-01-09 01:20:04 +08:00
|
|
|
|
await this.siteIpService.checkAll(site, retryTimes,onFinished);
|
|
|
|
|
|
}else{
|
|
|
|
|
|
await this.siteIpService.syncAndCheck(site, retryTimes,onFinished);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-28 15:12:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
/**
|
2026-01-09 12:25:56 +08:00
|
|
|
|
* 检查,不等待返回
|
2024-12-23 18:11:06 +08:00
|
|
|
|
* @param id
|
|
|
|
|
|
* @param notify
|
2025-01-04 20:10:00 +08:00
|
|
|
|
* @param retryTimes
|
2024-12-23 18:11:06 +08:00
|
|
|
|
*/
|
2025-06-13 00:25:08 +08:00
|
|
|
|
async check(id: number, notify = false, retryTimes = null) {
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const site = await this.info(id);
|
|
|
|
|
|
if (!site) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
throw new Error("站点不存在");
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
2026-01-09 12:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
this.doCheck(site, notify, retryTimes).catch((err) => {
|
|
|
|
|
|
logger.error("check site error", err);
|
|
|
|
|
|
});
|
|
|
|
|
|
return
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 12:25:56 +08:00
|
|
|
|
async sendCheckErrorNotify(siteId: number, fromIpCheck = false,setting: UserSiteMonitorSetting) {
|
2025-10-14 23:24:47 +08:00
|
|
|
|
const site = await this.info(siteId);
|
2025-06-05 23:31:36 +08:00
|
|
|
|
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
2024-12-23 18:11:06 +08:00
|
|
|
|
// 发邮件
|
|
|
|
|
|
await this.notificationService.send(
|
|
|
|
|
|
{
|
2025-06-06 16:12:30 +08:00
|
|
|
|
id: setting?.notificationId,
|
2024-12-23 18:11:06 +08:00
|
|
|
|
useDefault: true,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
body: {
|
|
|
|
|
|
url,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
title: `站点证书${fromIpCheck ? "(IP)" : ""}检查出错<${site.name}>`,
|
2025-03-17 18:20:15 +08:00
|
|
|
|
content: `站点名称: ${site.name} \n站点域名: ${site.domain} \n错误信息:${site.error}`,
|
2025-12-14 01:36:20 +08:00
|
|
|
|
errorMessage: site.error,
|
|
|
|
|
|
notificationType: "siteCheckError",
|
2025-06-05 23:31:36 +08:00
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
site.userId
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-06-05 23:31:36 +08:00
|
|
|
|
|
2026-01-09 12:25:56 +08:00
|
|
|
|
async sendExpiresNotify(siteId: number,setting: UserSiteMonitorSetting) {
|
2025-10-14 22:25:04 +08:00
|
|
|
|
const tipDays = setting?.certValidDays || 10;
|
2026-01-09 12:25:56 +08:00
|
|
|
|
const site = await this.info(siteId);
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const expires = site.certExpiresTime;
|
2025-06-05 23:31:36 +08:00
|
|
|
|
const validDays = dayjs(expires).diff(dayjs(), "day");
|
|
|
|
|
|
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
|
|
|
|
|
|
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) {
|
2024-12-23 18:11:06 +08:00
|
|
|
|
// 发通知
|
|
|
|
|
|
await this.notificationService.send(
|
|
|
|
|
|
{
|
2025-06-06 16:12:30 +08:00
|
|
|
|
id: setting?.notificationId,
|
2024-12-23 18:11:06 +08:00
|
|
|
|
useDefault: true,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
body: {
|
|
|
|
|
|
title: `站点证书即将过期,剩余${validDays}天,<${site.name}>`,
|
|
|
|
|
|
content,
|
2025-07-10 17:02:48 +08:00
|
|
|
|
url,
|
2025-12-14 01:36:20 +08:00
|
|
|
|
errorMessage: "站点证书即将过期",
|
|
|
|
|
|
notificationType: "siteCertExpireRemind",
|
2025-06-05 23:31:36 +08:00
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
site.userId
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (validDays < 0) {
|
|
|
|
|
|
//发过期通知
|
|
|
|
|
|
await this.notificationService.send(
|
|
|
|
|
|
{
|
2025-06-06 16:12:30 +08:00
|
|
|
|
id: setting?.notificationId,
|
2024-12-23 18:11:06 +08:00
|
|
|
|
useDefault: true,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
body: {
|
|
|
|
|
|
title: `站点证书已过期${-validDays}天<${site.name}>`,
|
|
|
|
|
|
content,
|
|
|
|
|
|
url,
|
2025-12-14 01:36:20 +08:00
|
|
|
|
errorMessage: "站点证书已过期",
|
|
|
|
|
|
notificationType: "siteCertExpireRemind",
|
2025-06-05 23:31:36 +08:00
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
site.userId
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 21:50:44 +08:00
|
|
|
|
async checkList(sites: SiteInfoEntity[]) {
|
2025-06-06 18:20:30 +08:00
|
|
|
|
const cache = {}
|
2026-02-13 22:24:04 +08:00
|
|
|
|
const getFromCache = async (userId: number,projectId?: number) =>{
|
2026-03-04 23:15:48 +08:00
|
|
|
|
const key = `${userId}_${projectId??""}`
|
2026-02-13 22:24:04 +08:00
|
|
|
|
if (cache[key]) {
|
|
|
|
|
|
return cache[key];
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
2026-02-13 22:24:04 +08:00
|
|
|
|
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId, UserSiteMonitorSetting)
|
|
|
|
|
|
cache[key] = setting
|
2025-06-06 18:20:30 +08:00
|
|
|
|
return setting;
|
|
|
|
|
|
}
|
2024-12-23 18:11:06 +08:00
|
|
|
|
for (const site of sites) {
|
2026-02-13 22:24:04 +08:00
|
|
|
|
const setting = await getFromCache(site.userId,site.projectId)
|
2025-06-13 00:25:08 +08:00
|
|
|
|
let retryTimes = setting?.retryTimes
|
2025-06-06 18:20:30 +08:00
|
|
|
|
this.doCheck(site,true,retryTimes).catch(e => {
|
2025-01-04 20:10:00 +08:00
|
|
|
|
logger.error(`检查站点证书失败,${site.domain}`, e.message);
|
|
|
|
|
|
});
|
2025-06-06 18:20:30 +08:00
|
|
|
|
await utils.sleep(100);
|
2024-12-23 18:11:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-25 23:38:25 +08:00
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
async getSetting(userId: number,projectId?: number) {
|
|
|
|
|
|
return await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId, UserSiteMonitorSetting);
|
2025-05-25 23:38:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
async saveSetting(userId: number,projectId: number, bean: UserSiteMonitorSetting) {
|
|
|
|
|
|
await this.userSettingsService.saveSetting(userId,projectId, bean);
|
2025-06-06 18:20:30 +08:00
|
|
|
|
if(bean.cron){
|
|
|
|
|
|
//注册job
|
2026-02-13 22:24:04 +08:00
|
|
|
|
await this.registerSiteMonitorJob(userId,projectId);
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}else{
|
2026-02-13 22:24:04 +08:00
|
|
|
|
this.clearSiteMonitorJob(userId,projectId);
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
2025-05-25 23:38:25 +08:00
|
|
|
|
}
|
2025-05-28 13:57:31 +08:00
|
|
|
|
|
2025-06-05 23:31:36 +08:00
|
|
|
|
async ipCheckChange(req: { id: any; ipCheck: any }) {
|
2025-05-28 13:57:31 +08:00
|
|
|
|
|
|
|
|
|
|
await this.update({
|
|
|
|
|
|
id: req.id,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
ipCheck: req.ipCheck
|
2025-05-28 13:57:31 +08:00
|
|
|
|
});
|
2025-06-05 23:31:36 +08:00
|
|
|
|
if (req.ipCheck) {
|
2025-05-28 13:57:31 +08:00
|
|
|
|
const site = await this.info(req.id);
|
2025-06-05 23:31:36 +08:00
|
|
|
|
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,
|
2025-06-05 23:31:36 +08:00
|
|
|
|
disabled: req.disabled
|
2025-05-28 15:49:48 +08:00
|
|
|
|
});
|
2025-06-05 23:31:36 +08:00
|
|
|
|
if (!req.disabled) {
|
2025-05-28 15:49:48 +08:00
|
|
|
|
const site = await this.info(req.id);
|
2025-06-05 23:31:36 +08:00
|
|
|
|
await this.doCheck(site);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 00:41:40 +08:00
|
|
|
|
async doImport(req: { text: string; userId: number,groupId?:number,projectId?:number }) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
if (!req.text) {
|
|
|
|
|
|
throw new Error("text is required");
|
|
|
|
|
|
}
|
2026-03-04 23:15:48 +08:00
|
|
|
|
if (req.userId == null) {
|
2025-06-05 23:31:36 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-10-21 22:38:02 +08:00
|
|
|
|
let remark:string = "";
|
|
|
|
|
|
if (arr.length > 3) {
|
|
|
|
|
|
remark = arr[3] || "";
|
|
|
|
|
|
}
|
2025-06-05 23:31:36 +08:00
|
|
|
|
|
|
|
|
|
|
list.push({
|
|
|
|
|
|
domain,
|
|
|
|
|
|
name,
|
|
|
|
|
|
httpsPort: port,
|
2025-10-21 22:38:02 +08:00
|
|
|
|
userId: req.userId,
|
|
|
|
|
|
remark,
|
2026-02-13 00:41:40 +08:00
|
|
|
|
groupId: req.groupId,
|
|
|
|
|
|
projectId: req.projectId
|
2025-06-05 23:31:36 +08:00
|
|
|
|
});
|
2025-05-28 15:49:48 +08:00
|
|
|
|
}
|
2025-06-05 23:31:36 +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
|
|
|
|
}
|
2025-06-06 18:20:30 +08:00
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
clearSiteMonitorJob(userId: number,projectId?: number) {
|
2026-03-04 23:15:48 +08:00
|
|
|
|
this.cron.remove(`siteMonitor_${userId}_${projectId||""}`);
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
async registerSiteMonitorJob(userId?: number,projectId?: number) {
|
2026-04-11 21:50:44 +08:00
|
|
|
|
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId, UserSiteMonitorSetting);
|
|
|
|
|
|
if (!setting.cron) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
//注册个人的 或项目的
|
|
|
|
|
|
this.cron.register({
|
|
|
|
|
|
name: `siteMonitor_${userId}_${projectId||""}`,
|
|
|
|
|
|
cron: setting.cron,
|
|
|
|
|
|
job: () => this.triggerJobOnce(userId,projectId),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-06-06 18:20:30 +08:00
|
|
|
|
|
2026-04-11 21:50:44 +08:00
|
|
|
|
async triggerCommonJob(){
|
|
|
|
|
|
//遍历用户
|
|
|
|
|
|
const userIds = await this.userService.getAllUserIds()
|
|
|
|
|
|
for (const userId of userIds) {
|
|
|
|
|
|
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,null,UserSiteMonitorSetting)
|
|
|
|
|
|
if(setting && setting.cron){
|
|
|
|
|
|
//该用户有自定义检查时间,跳过公共job
|
|
|
|
|
|
continue
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
2026-04-11 21:50:44 +08:00
|
|
|
|
await this.triggerJobOnce(userId)
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 21:50:44 +08:00
|
|
|
|
//遍历项目
|
|
|
|
|
|
const projectIds = await this.projectService.getAllProjectIds()
|
|
|
|
|
|
for (const projectId of projectIds) {
|
|
|
|
|
|
const userId = Constants.enterpriseUserId
|
|
|
|
|
|
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(userId,projectId,UserSiteMonitorSetting)
|
|
|
|
|
|
if(setting && setting.cron){
|
|
|
|
|
|
//该项目有自定义检查时间,跳过公共job
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.triggerJobOnce(userId,projectId)
|
|
|
|
|
|
}
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 22:24:04 +08:00
|
|
|
|
async triggerJobOnce(userId?:number,projectId?:number) {
|
2026-04-11 21:50:44 +08:00
|
|
|
|
if(userId==null){
|
|
|
|
|
|
throw new Error("userId is required");
|
|
|
|
|
|
}
|
2025-06-06 18:20:30 +08:00
|
|
|
|
const query:any = { disabled: false };
|
2026-04-11 21:50:44 +08:00
|
|
|
|
query.userId = userId;
|
|
|
|
|
|
if(projectId){
|
|
|
|
|
|
query.projectId = projectId;
|
|
|
|
|
|
}
|
|
|
|
|
|
const siteCount = await this.repository.count({
|
|
|
|
|
|
where: query,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (siteCount === 0) {
|
|
|
|
|
|
logger.info(`用户/项目[${userId}_${projectId||""}]没有站点证书需要检查`)
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(`站点证书检查开始执行[${userId}_${projectId||""}]`);
|
|
|
|
|
|
|
2026-04-05 23:49:25 +08:00
|
|
|
|
let jobEntity :Partial<JobHistoryEntity> = null;
|
2026-04-11 21:50:44 +08:00
|
|
|
|
|
|
|
|
|
|
jobEntity = {
|
|
|
|
|
|
userId,
|
|
|
|
|
|
projectId,
|
|
|
|
|
|
type:"siteCertMonitor",
|
|
|
|
|
|
title: '站点证书检查',
|
|
|
|
|
|
result:"start",
|
|
|
|
|
|
startAt:new Date().getTime(),
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
2026-04-11 21:50:44 +08:00
|
|
|
|
await this.jobHistoryService.add(jobEntity);
|
2025-06-06 18:20:30 +08:00
|
|
|
|
let offset = 0;
|
|
|
|
|
|
const limit = 50;
|
2026-04-05 23:49:25 +08:00
|
|
|
|
let count = 0;
|
2025-06-06 18:20:30 +08:00
|
|
|
|
while (true) {
|
|
|
|
|
|
const res = await this.page({
|
|
|
|
|
|
query: query,
|
|
|
|
|
|
page: { offset, limit },
|
|
|
|
|
|
});
|
|
|
|
|
|
const { records } = res;
|
|
|
|
|
|
|
|
|
|
|
|
if (records.length === 0) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
offset += records.length;
|
2026-04-05 23:49:25 +08:00
|
|
|
|
count += records.length;
|
2026-04-11 21:50:44 +08:00
|
|
|
|
await this.checkList(records);
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 21:50:44 +08:00
|
|
|
|
logger.info(`站点证书检查完成[${userId}_${projectId||""}]`);
|
|
|
|
|
|
await this.jobHistoryService.update({
|
|
|
|
|
|
id: jobEntity.id,
|
|
|
|
|
|
result: "done",
|
|
|
|
|
|
content:`共检查${count}个站点`,
|
|
|
|
|
|
endAt:new Date().getTime(),
|
|
|
|
|
|
updateTime:new Date(),
|
|
|
|
|
|
});
|
2025-06-06 18:20:30 +08:00
|
|
|
|
}
|
2026-02-13 00:41:40 +08:00
|
|
|
|
|
|
|
|
|
|
async batchDelete(ids: number[], userId: number,projectId?:number): Promise<void> {
|
|
|
|
|
|
await this.repository.delete({
|
|
|
|
|
|
id: In(ids),
|
|
|
|
|
|
userId,
|
|
|
|
|
|
projectId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-12-22 14:01:10 +08:00
|
|
|
|
}
|