mirror of https://github.com/certd/certd
perf: 站点证书监控通知发送,每天定时检查
parent
89c7f07034
commit
bb4910f4e5
|
@ -1,4 +1,4 @@
|
|||
export function isDev() {
|
||||
const nodeEnv = process.env.NODE_ENV || '';
|
||||
return nodeEnv === 'development' || nodeEnv.indexOf('local') >= 0 || nodeEnv.includes('dev');
|
||||
return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev');
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { In, Repository, SelectQueryBuilder } from 'typeorm';
|
|||
import { Inject } from '@midwayjs/core';
|
||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
|
||||
import { FindManyOptions } from 'typeorm';
|
||||
|
||||
export type PageReq<T = any> = {
|
||||
page?: { offset: number; limit: number };
|
||||
|
@ -15,6 +16,7 @@ export type ListReq<T = any> = {
|
|||
asc: boolean;
|
||||
};
|
||||
buildQuery?: (bq: SelectQueryBuilder<any>) => void;
|
||||
select?: any;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -53,7 +55,7 @@ export abstract class BaseService<T> {
|
|||
* 非分页查询
|
||||
* @param options
|
||||
*/
|
||||
async find(options) {
|
||||
async find(options: FindManyOptions<T>) {
|
||||
return await this.getRepository().find(options);
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,10 @@ export class CertReader {
|
|||
}
|
||||
|
||||
getCrtDetail(crt: string = this.cert.crt) {
|
||||
return CertReader.readCertDetail(crt);
|
||||
}
|
||||
|
||||
static readCertDetail(crt: string) {
|
||||
const detail = crypto.readCertificateInfo(crt.toString());
|
||||
const expires = detail.notAfter;
|
||||
return { detail, expires };
|
||||
|
|
|
@ -41,7 +41,7 @@ export const certdResources = [
|
|||
}
|
||||
},
|
||||
{
|
||||
title: "证书监控",
|
||||
title: "站点证书监控",
|
||||
name: "SiteCertMonitor",
|
||||
path: "/certd/monitor/site",
|
||||
component: "/certd/monitor/site/index.vue",
|
||||
|
|
|
@ -170,6 +170,20 @@ export const sysResources = [
|
|||
permission: "sys:auth:user:view"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: "套餐管理",
|
||||
name: "SuiteManager",
|
||||
path: "/sys/suite",
|
||||
meta: {
|
||||
icon: "ion:cart-outline",
|
||||
permission: "sys:settings:edit",
|
||||
show: () => {
|
||||
const settingStore = useSettingStore();
|
||||
return settingStore.isComm;
|
||||
}
|
||||
},
|
||||
children: [
|
||||
{
|
||||
title: "套餐设置",
|
||||
name: "SuiteSetting",
|
||||
|
@ -197,6 +211,22 @@ export const sysResources = [
|
|||
icon: "ion:bag-check",
|
||||
permission: "sys:settings:edit"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "用户套餐",
|
||||
name: "UserSuites",
|
||||
path: "/sys/suite/user-suite",
|
||||
component: "/certd/suite/user-suite/index.vue",
|
||||
meta: {
|
||||
show: () => {
|
||||
const settingStore = useSettingStore();
|
||||
return settingStore.isComm;
|
||||
},
|
||||
icon: "ion:gift-outline",
|
||||
auth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { request } from "/src/api/service";
|
||||
|
||||
export function createApi() {
|
||||
const apiPrefix = "/monitor/site";
|
||||
return {
|
||||
|
||||
export const siteInfoApi = {
|
||||
async GetList(query: any) {
|
||||
return await request({
|
||||
url: apiPrefix + "/page",
|
||||
|
@ -42,13 +42,17 @@ export function createApi() {
|
|||
params: { id }
|
||||
});
|
||||
},
|
||||
async ListAll() {
|
||||
async DoCheck(id: number) {
|
||||
return await request({
|
||||
url: apiPrefix + "/all",
|
||||
url: apiPrefix + "/check",
|
||||
method: "post",
|
||||
data: { id }
|
||||
});
|
||||
},
|
||||
async CheckAll() {
|
||||
return await request({
|
||||
url: apiPrefix + "/checkAll",
|
||||
method: "post"
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const pipelineGroupApi = createApi();
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// @ts-ignore
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref } from "vue";
|
||||
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||
import { pipelineGroupApi } from "./api";
|
||||
import { siteInfoApi } from "./api";
|
||||
import dayjs from "dayjs";
|
||||
import { notification } from "ant-design-vue";
|
||||
|
||||
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||
const { t } = useI18n();
|
||||
const api = pipelineGroupApi;
|
||||
const api = siteInfoApi;
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return await api.GetList(query);
|
||||
};
|
||||
|
@ -51,7 +52,24 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
}
|
||||
},
|
||||
rowHandle: {
|
||||
width: 200
|
||||
fixed: "right",
|
||||
width: 240,
|
||||
buttons: {
|
||||
check: {
|
||||
order: 0,
|
||||
type: "link",
|
||||
text: null,
|
||||
title: "立即检查",
|
||||
icon: "ion:play-sharp",
|
||||
click: async ({ row }) => {
|
||||
await api.DoCheck(row.id);
|
||||
await crudExpose.doRefresh();
|
||||
notification.success({
|
||||
message: "检查完成"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
|
@ -62,44 +80,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
editable: {
|
||||
disabled: true
|
||||
}
|
||||
width: 80,
|
||||
align: "center"
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
|
||||
domain: {
|
||||
title: "网站域名",
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
rules: [{ required: true, message: "请输入域名" }]
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
sorter: true
|
||||
}
|
||||
},
|
||||
port: {
|
||||
title: "HTTPS端口",
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
type: "number",
|
||||
form: {
|
||||
value: 443,
|
||||
rules: [{ required: true, message: "请输入端口" }]
|
||||
},
|
||||
column: {
|
||||
width: 100
|
||||
}
|
||||
},
|
||||
name: {
|
||||
title: "站点名称",
|
||||
search: {
|
||||
|
@ -110,11 +97,40 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
rules: [{ required: true, message: "请输入站点名称" }]
|
||||
},
|
||||
column: {
|
||||
width: 200
|
||||
width: 160
|
||||
}
|
||||
},
|
||||
domains: {
|
||||
title: "其他域名",
|
||||
domain: {
|
||||
title: "网站域名",
|
||||
search: {
|
||||
show: true
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
rules: [{ required: true, message: "请输入域名" }]
|
||||
},
|
||||
column: {
|
||||
width: 160,
|
||||
sorter: true
|
||||
}
|
||||
},
|
||||
httpsPort: {
|
||||
title: "HTTPS端口",
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
type: "number",
|
||||
form: {
|
||||
value: 443,
|
||||
rules: [{ required: true, message: "请输入端口" }]
|
||||
},
|
||||
column: {
|
||||
align: "center",
|
||||
width: 100
|
||||
}
|
||||
},
|
||||
certDomains: {
|
||||
title: "证书域名",
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
|
@ -123,13 +139,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 300,
|
||||
width: 200,
|
||||
sorter: true,
|
||||
show: false
|
||||
show: true
|
||||
}
|
||||
},
|
||||
certInfo: {
|
||||
title: "证书详情",
|
||||
certProvider: {
|
||||
title: "证书颁发者",
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
|
@ -138,8 +154,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
show: false
|
||||
width: 200,
|
||||
sorter: true
|
||||
}
|
||||
},
|
||||
certStatus: {
|
||||
|
@ -147,13 +163,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
search: {
|
||||
show: true
|
||||
},
|
||||
type: "text",
|
||||
type: "dict-select",
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "正常", value: "ok", color: "green" },
|
||||
{ label: "过期", value: "expired", color: "red" }
|
||||
]
|
||||
}),
|
||||
form: {
|
||||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
sorter: true
|
||||
sorter: true,
|
||||
show: false
|
||||
}
|
||||
},
|
||||
certExpiresTime: {
|
||||
|
@ -166,7 +189,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
show: false
|
||||
},
|
||||
column: {
|
||||
sorter: true
|
||||
sorter: true,
|
||||
cellRender({ value }) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const expireDate = dayjs(value).format("YYYY-MM-DD");
|
||||
const leftDays = dayjs(value).diff(dayjs(), "day");
|
||||
const color = leftDays < 20 ? "red" : "#389e0d";
|
||||
const percent = (leftDays / 90) * 100;
|
||||
return <a-progress title={expireDate + "过期"} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}天`} />;
|
||||
}
|
||||
}
|
||||
},
|
||||
lastCheckTime: {
|
||||
|
@ -187,12 +220,33 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
search: {
|
||||
show: false
|
||||
},
|
||||
type: "text",
|
||||
type: "dict-select",
|
||||
dict: dict({
|
||||
data: [
|
||||
{ label: "正常", value: "ok", color: "green" },
|
||||
{ label: "异常", value: "error", color: "red" }
|
||||
]
|
||||
}),
|
||||
form: {
|
||||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
align: "center",
|
||||
sorter: true
|
||||
}
|
||||
},
|
||||
error: {
|
||||
title: "错误信息",
|
||||
search: {
|
||||
show: false
|
||||
},
|
||||
type: "text",
|
||||
form: {
|
||||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 200,
|
||||
sorter: true
|
||||
}
|
||||
},
|
||||
|
@ -205,7 +259,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
type: "number",
|
||||
column: {
|
||||
width: 200,
|
||||
sorter: true
|
||||
sorter: true,
|
||||
show: false
|
||||
}
|
||||
},
|
||||
certInfoId: {
|
||||
|
@ -234,7 +289,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
]
|
||||
}),
|
||||
form: {
|
||||
value: true
|
||||
value: false
|
||||
},
|
||||
column: {
|
||||
width: 100,
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<template #header>
|
||||
<div class="title">
|
||||
站点证书监控
|
||||
<span class="sub">监控网站证书的过期时间,并发出提醒【未完成,开发中】</span>
|
||||
<span class="sub">每天0点,检查网站证书的过期时间,并发出提醒</span>
|
||||
</div>
|
||||
<div class="more">
|
||||
<a-button type="primary" @click="checkAll">检查全部</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
|
@ -11,15 +14,30 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, onActivated, onMounted } from "vue";
|
||||
import { onActivated, onMounted } from "vue";
|
||||
import { useFs } from "@fast-crud/fast-crud";
|
||||
import createCrudOptions from "./crud";
|
||||
import { createApi } from "./api";
|
||||
import { siteInfoApi } from "./api";
|
||||
import { Modal, notification } from "ant-design-vue";
|
||||
defineOptions({
|
||||
name: "SiteCertMonitor"
|
||||
});
|
||||
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
|
||||
|
||||
function checkAll() {
|
||||
Modal.confirm({
|
||||
title: "确认",
|
||||
content: "确认触发检查全部站点证书吗?",
|
||||
onOk: async () => {
|
||||
await siteInfoApi.CheckAll();
|
||||
notification.success({
|
||||
message: "检查任务已提交",
|
||||
description: "请稍后刷新页面查看结果"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
|
|
|
@ -107,14 +107,16 @@ CREATE TABLE "cd_site_info"
|
|||
|
||||
"name" varchar(100),
|
||||
"domain" varchar(100),
|
||||
"domains" varchar(10240),
|
||||
"cert_info" varchar(10240),
|
||||
|
||||
"https_port" integer,
|
||||
"cert_domains" varchar(10240),
|
||||
"cert_info" varchar(10240),
|
||||
"cert_provider" varchar(100),
|
||||
"cert_status" varchar(100),
|
||||
"cert_expires_time" integer,
|
||||
"last_check_time" integer,
|
||||
"check_status" varchar(100),
|
||||
"error" varchar(4096),
|
||||
"pipeline_id" integer,
|
||||
"cert_info_id" integer,
|
||||
"disabled" boolean NOT NULL DEFAULT (false),
|
||||
|
@ -125,7 +127,6 @@ CREATE TABLE "cd_site_info"
|
|||
|
||||
CREATE INDEX "index_site_info_user_id" ON "cd_site_info" ("user_id");
|
||||
CREATE INDEX "index_site_info_domain" ON "cd_site_info" ("domain");
|
||||
CREATE INDEX "index_site_info_domains" ON "cd_site_info" ("domains");
|
||||
CREATE INDEX "index_site_info_pipeline" ON "cd_site_info" ("pipeline_id");
|
||||
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ const development = {
|
|||
cron: {
|
||||
//启动时立即触发一次
|
||||
immediateTriggerOnce: false,
|
||||
immediateTriggerSiteMonitor: false,
|
||||
//启动时仅注册admin(id=1)用户的
|
||||
onlyAdminUser: false,
|
||||
},
|
||||
|
|
|
@ -60,13 +60,17 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
|
|||
return await super.delete(id);
|
||||
}
|
||||
|
||||
@Post('/all', { summary: Constants.per.authOnly })
|
||||
async all() {
|
||||
const list: any = await this.service.find({
|
||||
where: {
|
||||
userId: this.getUserId(),
|
||||
},
|
||||
});
|
||||
return this.ok(list);
|
||||
@Post('/check', { summary: Constants.per.authOnly })
|
||||
async check(@Body('id') id: number) {
|
||||
await this.service.checkUserId(id, this.getUserId());
|
||||
await this.service.check(id, false);
|
||||
return this.ok();
|
||||
}
|
||||
|
||||
@Post('/checkAll', { summary: Constants.per.authOnly })
|
||||
async checkAll() {
|
||||
const userId = this.getUserId();
|
||||
this.service.checkAll(userId);
|
||||
return this.ok();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core
|
|||
import { PipelineService } from '../pipeline/service/pipeline-service.js';
|
||||
import { logger } from '@certd/basic';
|
||||
import { SysSettingsService } from '@certd/lib-server';
|
||||
import { SiteInfoService } from '../monitor/index.js';
|
||||
import { Cron } from '../cron/cron.js';
|
||||
|
||||
@Autoload()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
|
@ -17,9 +19,18 @@ export class AutoRegisterCron {
|
|||
@Config('cron.immediateTriggerOnce')
|
||||
private immediateTriggerOnce = false;
|
||||
|
||||
@Config('cron.immediateTriggerSiteMonitor')
|
||||
private immediateTriggerSiteMonitor = false;
|
||||
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
@Inject()
|
||||
siteInfoService: SiteInfoService;
|
||||
|
||||
@Inject()
|
||||
cron: Cron;
|
||||
|
||||
@Init()
|
||||
async init() {
|
||||
logger.info('加载定时trigger开始');
|
||||
|
@ -30,5 +41,45 @@ export class AutoRegisterCron {
|
|||
// console.log('meta', meta);
|
||||
// const metas = listPropertyDataFromClass(CLASS_KEY, this.echoPlugin);
|
||||
// console.log('metas', metas);
|
||||
this.registerSiteMonitorCron();
|
||||
}
|
||||
|
||||
registerSiteMonitorCron() {
|
||||
const job = async () => {
|
||||
logger.info('站点证书检查开始执行');
|
||||
|
||||
let offset = 0;
|
||||
const limit = 50;
|
||||
while (true) {
|
||||
const res = await this.siteInfoService.page({
|
||||
query: { disabled: false },
|
||||
page: { offset, limit },
|
||||
});
|
||||
const { records } = res;
|
||||
|
||||
if (records.length === 0) {
|
||||
break;
|
||||
}
|
||||
offset += records.length;
|
||||
for (const record of records) {
|
||||
try {
|
||||
await this.siteInfoService.doCheck(record, true);
|
||||
} catch (e) {
|
||||
logger.error(`站点${record.name}检查出错:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('站点证书检查完成');
|
||||
};
|
||||
|
||||
this.cron.register({
|
||||
name: 'siteMonitor',
|
||||
cron: '0 0 0 * * *',
|
||||
job,
|
||||
});
|
||||
if (this.immediateTriggerSiteMonitor) {
|
||||
job();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
|
@ -1,5 +1,5 @@
|
|||
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
||||
import { BaseService, SysSettingsService, SysSiteInfo, ValidateException } from '@certd/lib-server';
|
||||
import { BaseService, SysInstallInfo, SysSettingsService, SysSiteInfo, ValidateException } from '@certd/lib-server';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotificationEntity } from '../entity/notification.js';
|
||||
|
@ -187,4 +187,10 @@ export class NotificationService extends BaseService<NotificationEntity> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getBindUrl(path: string) {
|
||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||
const bindUrl = installInfo.bindUrl || 'http://127.0.0.1:7001';
|
||||
return bindUrl + path;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue