perf: 站点监控支持监控IP

This commit is contained in:
xiaojunnuo
2025-05-28 00:57:52 +08:00
parent 88022747be
commit 9cc4c017ae
15 changed files with 999 additions and 52 deletions
@@ -40,6 +40,17 @@ export class SiteInfoEntity {
@Column({ name: 'cert_info_id', comment: '证书id' })
certInfoId: number;
@Column({ name: 'ip_check', comment: '是否检查IP' })
ipCheck: boolean;
@Column({ name: 'ip_count', comment: 'ip数量' })
ipCount: number
@Column({ name: 'ip_error_count', comment: 'ip异常数量' })
ipErrorCount: number
@Column({ name: 'disabled', comment: '禁用启用' })
disabled: boolean;
@@ -0,0 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
*/
@Entity('cd_site_ip')
export class SiteIpEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ name: 'site_id', comment: '站点id' })
siteId: number;
@Column({ name: 'ip_address', comment: 'IP', length: 100 })
ipAddress: string;
@Column({ name: 'cert_domains', comment: '证书域名', length: 4096 })
certDomains: string;
@Column({ name: 'cert_status', comment: '证书状态', length: 100 })
certStatus: string;
@Column({ name: 'cert_provider', comment: '证书颁发机构', length: 100 })
certProvider: string;
@Column({ name: 'cert_expires_time', comment: '证书到期时间' })
certExpiresTime: number;
@Column({ name: 'last_check_time', comment: '上次检查时间' })
lastCheckTime: number;
@Column({ name: 'check_status', comment: '检查状态' })
checkStatus: string;
@Column({ name: 'error', comment: '错误信息' })
error: string;
@Column({ name: 'from', comment: '来源' })
from: string
@Column({ name: 'remark', comment: '备注' })
remark: string;
@Column({ name: "disabled", comment: "禁用启用" })
disabled: boolean;
@Column({ name: 'create_time', comment: '创建时间', default: () => 'CURRENT_TIMESTAMP' })
createTime: Date;
@Column({ name: 'update_time', comment: '修改时间', default: () => 'CURRENT_TIMESTAMP' })
updateTime: Date;
}
@@ -94,7 +94,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
await this.update({
id: site.id,
checkStatus: 'checking',
lastCheckTime: dayjs,
lastCheckTime: dayjs().valueOf(),
});
const res = await siteTester.test({
host: site.domain,
@@ -0,0 +1,203 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, SysSettingsService } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { SiteInfoEntity } from "../entity/site-info.js";
import { NotificationService } from "../../pipeline/service/notification-service.js";
import { UserSuiteService } from "@certd/commercial-core";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { SiteIpEntity } from "../entity/site-ip.js";
import dns from "dns";
import { logger, safePromise } from "@certd/basic";
import dayjs from "dayjs";
import { siteTester } from "./site-tester.js";
import { PeerCertificate } from "tls";
import { SiteInfoService } from "./site-info-service.js";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class SiteIpService extends BaseService<SiteIpEntity> {
@InjectEntityModel(SiteIpEntity)
repository: Repository<SiteIpEntity>;
@Inject()
notificationService: NotificationService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userSuiteService: UserSuiteService;
@Inject()
userSettingsService: UserSettingsService;
@Inject()
siteInfoService: SiteInfoService;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(data: SiteInfoEntity) {
if (!data.userId) {
throw new Error("userId is required");
}
data.disabled = false;
return await super.add(data);
}
async update(data: any) {
if (!data.id) {
throw new Error("id is required");
}
delete data.userId;
await super.update(data);
}
async sync(entity: SiteInfoEntity) {
const domain = entity.domain;
//从域名解析中获取所有ip
const ips = await this.getAllIpsFromDomain(domain);
if (ips.length === 0 ) {
throw new Error(`没有发现${domain}的IP`)
}
//删除所有的ip
await this.repository.delete({
siteId: entity.id,
from: "sync"
});
//添加新的ip
for (const ip of ips) {
await this.repository.save({
ipAddress: ip,
userId: entity.userId,
siteId: entity.id,
from: "sync",
disabled:false,
});
}
await this.checkAll(entity.id);
}
async check(ipId: number, domain?: string, port?: number) {
if(!ipId){
return
}
const entity = await this.info(ipId);
if (!entity) {
return;
}
if (domain == null || port == null){
const siteEntity = await this.siteInfoService.info(entity.siteId);
domain = siteEntity.domain;
port = siteEntity.httpsPort;
}
try {
await this.update({
id: entity.id,
checkStatus: "checking",
lastCheckTime: dayjs().valueOf()
});
const res = await siteTester.test({
host: domain,
port: port,
retryTimes: 3,
ipAddress: entity.ipAddress
});
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: entity.id,
certDomains: domains.join(","),
certStatus: status,
certProvider: issuer,
certExpiresTime: dayjs(expires).valueOf(),
lastCheckTime: dayjs().valueOf(),
error: null,
checkStatus: "ok"
};
await this.update(updateData);
} catch (e) {
logger.error("check site ip error", e);
await this.update({
id: entity.id,
checkStatus: "error",
lastCheckTime: dayjs().valueOf(),
error: e.message
});
}
}
async checkAll(siteId: number) {
const siteInfo = await this.siteInfoService.info(siteId);
const ips = await this.repository.find({
where: {
siteId: siteId
}
});
const domain = siteInfo.domain;
const port = siteInfo.httpsPort;
const promiseList = [];
for (const ip of ips) {
promiseList.push(async () => {
try {
await this.check(ip.id, domain, port);
} catch (e) {
logger.error("check site ip error", e);
}
});
}
Promise.all(promiseList);
}
async getAllIpsFromDomain(domain: string) {
const getFromV4 = safePromise<string[]>((resolve, reject) => {
dns.resolve4(domain, (err, addresses) => {
if (err) {
logger.error(`[${domain}] resolve4 error`, err)
resolve([])
return;
}
resolve(addresses);
});
});
const getFromV6 = safePromise<string[]>((resolve, reject) => {
dns.resolve6(domain, (err, addresses) => {
if (err) {
logger.error("[${domain}] resolve6 error", err)
resolve([])
return;
}
resolve(addresses);
});
});
return Promise.all([getFromV4, getFromV6]).then(res => {
return [...res[0], ...res[1]];
});
}
}
@@ -1,20 +1,23 @@
import {logger, safePromise, utils} from '@certd/basic';
import { merge } from 'lodash-es';
import https from 'https';
import { PeerCertificate } from 'tls';
import { logger, safePromise, utils } 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;
retryTimes?: number;
ipAddress?: string;
};
export type SiteTestRes = {
certificate?: PeerCertificate;
};
export class SiteTester {
async test(req: SiteTestReq): Promise<SiteTestRes> {
logger.info('测试站点:', JSON.stringify(req));
logger.info("测试站点:", JSON.stringify(req));
const maxRetryTimes = req.retryTimes ?? 3;
let tryCount = 0;
let result: SiteTestRes = {};
@@ -37,17 +40,34 @@ export class SiteTester {
}
async doTestOnce(req: SiteTestReq): Promise<SiteTestRes> {
const agent = new https.Agent({ keepAlive: false });
const options: any = merge(
{
port: 443,
method: 'GET',
rejectUnauthorized: false,
method: "GET",
rejectUnauthorized: false
},
req
);
options.agent = agent;
const agentOptions:any = {}
if (req.ipAddress) {
//使用固定的ip
const ipAddress = req.ipAddress;
agentOptions.lookup = (hostname: string, options: any, callback: any) => {
//判断ip是v4 还是v6
console.log("options",options)
console.log("ipaddress",ipAddress)
if (ipAddress.indexOf(":") > -1) {
callback(null, [ipAddress], 6);
} else {
callback(null, [ipAddress], 4);
}
};
options.lookup = agentOptions.lookup;
}
options.agent = new https.Agent({ keepAlive: false, ...agentOptions });
// 创建 HTTPS 请求
const requestPromise = safePromise((resolve, reject) => {
const req = https.request(options, res => {
@@ -56,20 +76,20 @@ export class SiteTester {
const certificate = res.socket.getPeerCertificate();
// logger.info('证书信息', certificate);
if (certificate.subject == null) {
logger.warn('证书信息为空');
logger.warn("证书信息为空");
resolve({
certificate: null,
certificate: null
});
}
resolve({
certificate,
certificate
});
res.socket.end();
// 关闭响应
res.destroy();
});
req.on('error', e => {
req.on("error", e => {
reject(e);
});
req.end();