2025-01-15 01:05:34 +08:00
|
|
|
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
2024-12-26 09:02:04 +08:00
|
|
|
import { BaseService, NeedSuiteException, NeedVIPException, SysSettingsService } from '@certd/lib-server';
|
2024-12-22 14:01:10 +08:00
|
|
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
|
|
|
import { Repository } from 'typeorm';
|
|
|
|
|
import { SiteInfoEntity } from '../entity/site-info.js';
|
2024-12-23 18:11:06 +08:00
|
|
|
import { siteTester } from './site-tester.js';
|
|
|
|
|
import dayjs from 'dayjs';
|
2025-01-04 20:10:00 +08:00
|
|
|
import { logger, utils } from '@certd/basic';
|
2024-12-23 18:11:06 +08:00
|
|
|
import { PeerCertificate } from 'tls';
|
|
|
|
|
import { NotificationService } from '../../pipeline/service/notification-service.js';
|
2024-12-23 23:33:13 +08:00
|
|
|
import { isComm, isPlus } from '@certd/plus-core';
|
|
|
|
|
import { UserSuiteService } from '@certd/commercial-core';
|
2024-12-22 14:01:10 +08:00
|
|
|
|
|
|
|
|
@Provide()
|
2025-01-15 01:05:34 +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;
|
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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('站点监控数量已达上限,请购买或升级套餐');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-10 00:06:49 +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-03-10 00:06:49 +08:00
|
|
|
|
|
|
|
|
|
2024-12-24 01:12:12 +08:00
|
|
|
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);
|
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');
|
|
|
|
|
}
|
|
|
|
|
return await this.repository.count({
|
|
|
|
|
where: { userId },
|
|
|
|
|
});
|
|
|
|
|
}
|
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-01-04 20:10:00 +08:00
|
|
|
async doCheck(site: SiteInfoEntity, notify = true, retryTimes = 3) {
|
2024-12-23 18:11:06 +08:00
|
|
|
if (!site?.domain) {
|
|
|
|
|
throw new Error('站点域名不能为空');
|
|
|
|
|
}
|
|
|
|
|
try {
|
2025-01-04 20:10:00 +08:00
|
|
|
await this.update({
|
|
|
|
|
id: site.id,
|
|
|
|
|
checkStatus: 'checking',
|
|
|
|
|
lastCheckTime: dayjs,
|
|
|
|
|
});
|
2024-12-23 18:11:06 +08:00
|
|
|
const res = await siteTester.test({
|
|
|
|
|
host: site.domain,
|
|
|
|
|
port: site.httpsPort,
|
2025-01-04 20:10:00 +08:00
|
|
|
retryTimes,
|
2024-12-23 18:11:06 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const certi: PeerCertificate = res.certificate;
|
|
|
|
|
if (!certi) {
|
2025-01-04 20:10:00 +08:00
|
|
|
throw new Error('没有发现证书');
|
2024-12-23 18:11:06 +08:00
|
|
|
}
|
|
|
|
|
const expires = certi.valid_to;
|
2025-03-27 17:10:46 +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();
|
|
|
|
|
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
|
2025-01-04 20:10:00 +08:00
|
|
|
* @param retryTimes
|
2024-12-23 18:11:06 +08:00
|
|
|
*/
|
2025-01-04 20:10:00 +08:00
|
|
|
async check(id: number, notify = false, retryTimes = 3) {
|
2024-12-23 18:11:06 +08:00
|
|
|
const site = await this.info(id);
|
|
|
|
|
if (!site) {
|
|
|
|
|
throw new Error('站点不存在');
|
|
|
|
|
}
|
2025-01-04 20:10:00 +08:00
|
|
|
return await this.doCheck(site, notify, retryTimes);
|
2024-12-23 18:11:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}>`,
|
2025-03-17 18:20:15 +08:00
|
|
|
content: `站点名称: ${site.name} \n站点域名: ${site.domain} \n错误信息:${site.error}`,
|
2024-12-23 18:11:06 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
site.userId
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
async sendExpiresNotify(site: SiteInfoEntity) {
|
2025-03-17 00:19:01 +08:00
|
|
|
|
|
|
|
|
const tipDays = 10
|
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
const expires = site.certExpiresTime;
|
|
|
|
|
const validDays = dayjs(expires).diff(dayjs(), 'day');
|
2025-03-17 18:28:33 +08:00
|
|
|
const url = await this.notificationService.getBindUrl('#/certd/monitor/site');
|
2025-03-17 18:27:52 +08:00
|
|
|
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(
|
|
|
|
|
{
|
|
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 20:10:00 +08:00
|
|
|
async checkAllByUsers(userId: any) {
|
2024-12-23 18:11:06 +08:00
|
|
|
if (!userId) {
|
|
|
|
|
throw new Error('userId is required');
|
|
|
|
|
}
|
|
|
|
|
const sites = await this.repository.find({
|
|
|
|
|
where: { userId },
|
|
|
|
|
});
|
2025-01-04 20:10:00 +08:00
|
|
|
this.checkList(sites);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async checkList(sites: SiteInfoEntity[]) {
|
2024-12-23 18:11:06 +08:00
|
|
|
for (const site of sites) {
|
2025-01-04 20:10:00 +08:00
|
|
|
this.doCheck(site).catch(e => {
|
|
|
|
|
logger.error(`检查站点证书失败,${site.domain}`, e.message);
|
|
|
|
|
});
|
|
|
|
|
await utils.sleep(200);
|
2024-12-23 18:11:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
2024-12-22 14:01:10 +08:00
|
|
|
}
|