diff --git a/packages/core/pipeline/src/context/index.ts b/packages/core/pipeline/src/context/index.ts index a4687574..38aab45e 100644 --- a/packages/core/pipeline/src/context/index.ts +++ b/packages/core/pipeline/src/context/index.ts @@ -2,3 +2,18 @@ import { IContext } from "../core/index.js"; export type UserContext = IContext; export type PipelineContext = IContext; + +export type PageReq = { + offset?: number; + limit?: number; + query?: string; + sortBy?: string; + sortOrder?: "asc" | "desc"; +}; + +export type PageRes = { + offset?: number; + limit?: number; + total?: string; + list: any[]; +}; diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx index 74033fb0..7d017d18 100644 --- a/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx +++ b/packages/ui/certd-client/src/views/certd/monitor/site/crud.tsx @@ -7,6 +7,7 @@ import { notification } from "ant-design-vue"; import { useSettingStore } from "/@/store/settings"; import { mySuiteApi } from "/@/views/certd/suite/mine/api"; import { mitter } from "/@/utils/util.mitt"; +import { useSiteIpMonitor } from "./ip/use"; export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { const { t } = useI18n(); @@ -41,6 +42,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat { label: "异常", value: "error", color: "red" }, ], }); + + const { openSiteIpMonitorDialog } = useSiteIpMonitor(); return { crudOptions: { request: { @@ -116,6 +119,18 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat }); }, }, + ipMonitor: { + order: 0, + type: "link", + text: null, + tooltip: { + title: "IP管理", + }, + icon: "entypo:address", + click: async ({ row }) => { + openSiteIpMonitorDialog({ siteId: row.id }); + }, + }, }, }, columns: { @@ -311,6 +326,42 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat align: "center", }, }, + ipCheck: { + title: "检查IP", + search: { + show: false, + }, + type: "dict-switch", + dict: dict({ + data: [ + { label: "启用", value: false, color: "green" }, + { label: "禁用", value: true, color: "red" }, + ], + }), + form: { + value: false, + }, + column: { + width: 100, + sorter: true, + align: "center", + }, + }, + ipCount: { + title: "IP数量", + search: { + show: false, + }, + type: "text", + form: { + show: false, + }, + column: { + width: 100, + sorter: true, + align: "center", + }, + }, checkStatus: { title: "检查状态", search: { diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/ip/api.ts b/packages/ui/certd-client/src/views/certd/monitor/site/ip/api.ts new file mode 100644 index 00000000..2cc40972 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/monitor/site/ip/api.ts @@ -0,0 +1,71 @@ +import { request } from "/src/api/service"; + +const apiPrefix = "/monitor/site/ip"; + +export const siteIpApi = { + async GetList(query: any) { + return await request({ + url: apiPrefix + "/page", + method: "post", + data: query, + }); + }, + + async AddObj(obj: any) { + return await request({ + url: apiPrefix + "/add", + method: "post", + data: obj, + }); + }, + + async UpdateObj(obj: any) { + return await request({ + url: apiPrefix + "/update", + method: "post", + data: obj, + }); + }, + + async DelObj(id: number) { + return await request({ + url: apiPrefix + "/delete", + method: "post", + params: { id }, + }); + }, + + async GetObj(id: number) { + return await request({ + url: apiPrefix + "/info", + method: "post", + params: { id }, + }); + }, + async DoCheck(id: number) { + return await request({ + url: apiPrefix + "/check", + method: "post", + data: { id }, + }); + }, + async CheckAll(siteId: number) { + return await request({ + url: apiPrefix + "/checkAll", + method: "post", + data: { + siteId, + }, + }); + }, + + async DoSync(siteId: number) { + return await request({ + url: apiPrefix + "/sync", + method: "post", + data: { + siteId, + }, + }); + }, +}; diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/ip/crud.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/ip/crud.tsx new file mode 100644 index 00000000..8b8ba1c3 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/monitor/site/ip/crud.tsx @@ -0,0 +1,324 @@ +// @ts-ignore +import { useI18n } from "vue-i18n"; +import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud"; +import { siteIpApi } from "./api"; +import dayjs from "dayjs"; +import { Modal, notification } from "ant-design-vue"; +import { useSettingStore } from "/@/store/settings"; + +export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { + const { t } = useI18n(); + const api = siteIpApi; + + const { crudBinding } = crudExpose; + const pageRequest = async (query: UserPageQuery): Promise => { + return await api.GetList(query); + }; + const editRequest = async (req: EditReq) => { + const { form, row } = req; + form.id = row.id; + form.siteId = context.props.siteId; + const res = await api.UpdateObj(form); + return res; + }; + const delRequest = async (req: DelReq) => { + const { row } = req; + return await api.DelObj(row.id); + }; + + const addRequest = async (req: AddReq) => { + const { form } = req; + const res = await api.AddObj(form); + return res; + }; + + const settingsStore = useSettingStore(); + + const checkStatusDict = dict({ + data: [ + { label: "成功", value: "ok", color: "green" }, + { label: "检查中", value: "checking", color: "blue" }, + { label: "异常", value: "error", color: "red" }, + ], + }); + return { + crudOptions: { + request: { + pageRequest, + addRequest, + editRequest, + delRequest, + }, + form: { + labelCol: { + //固定label宽度 + span: null, + style: { + width: "100px", + }, + }, + col: { + span: 22, + }, + wrapper: { + width: 600, + }, + }, + actionbar: { + buttons: { + add: { + async click() { + await crudExpose.openAdd({}); + }, + }, + load: { + text: "同步IP", + async click() { + Modal.confirm({ + title: "同步IP", + content: "确定要同步IP吗?", + onOk: async () => { + await api.DoSync(context.props.siteId); + await crudExpose.doRefresh(); + notification.success({ + message: "同步完成", + }); + }, + }); + }, + }, + }, + }, + rowHandle: { + fixed: "right", + width: 240, + buttons: { + check: { + order: 0, + type: "link", + text: null, + tooltip: { + title: "立即检查", + }, + icon: "ion:play-sharp", + click: async ({ row }) => { + await api.DoCheck(row.id); + await crudExpose.doRefresh(); + notification.success({ + message: "检查完成", + }); + }, + }, + }, + }, + columns: { + id: { + title: "ID", + key: "id", + type: "number", + search: { + show: false, + }, + column: { + width: 80, + align: "center", + }, + form: { + show: false, + }, + }, + ipAddress: { + title: "IP", + search: { + show: true, + }, + type: "text", + form: { + rules: [{ required: true, message: "请输入IP" }], + }, + column: { + width: 160, + }, + }, + certDomains: { + title: "证书域名", + search: { + show: false, + }, + type: "text", + form: { + show: false, + }, + column: { + width: 200, + sorter: true, + show: true, + cellRender({ value }) { + return ( + + {value} + + ); + }, + }, + }, + certProvider: { + title: "颁发机构", + search: { + show: false, + }, + type: "text", + form: { + show: false, + }, + column: { + width: 200, + sorter: true, + cellRender({ value }) { + return {value}; + }, + }, + }, + certStatus: { + title: "证书状态", + search: { + show: true, + }, + type: "dict-select", + dict: dict({ + data: [ + { label: "正常", value: "ok", color: "green" }, + { label: "过期", value: "expired", color: "red" }, + ], + }), + form: { + show: false, + }, + column: { + width: 100, + sorter: true, + show: true, + align: "center", + }, + }, + certExpiresTime: { + title: "证书到期时间", + search: { + show: false, + }, + type: "date", + form: { + show: false, + }, + column: { + 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 `${leftDays}天`} />; + }, + }, + }, + lastCheckTime: { + title: "上次检查时间", + search: { + show: false, + }, + type: "datetime", + form: { + show: false, + }, + column: { + sorter: true, + width: 155, + }, + }, + from: { + title: "来源", + search: { + show: false, + }, + type: "dict-switch", + dict: dict({ + data: [ + { label: "同步", value: "sync", color: "green" }, + { label: "手动", value: "manual", color: "blue" }, + ], + }), + form: { + value: false, + }, + column: { + width: 100, + sorter: true, + align: "center", + }, + }, + disabled: { + title: "禁用启用", + search: { + show: false, + }, + type: "dict-switch", + dict: dict({ + data: [ + { label: "启用", value: false, color: "green" }, + { label: "禁用", value: true, color: "red" }, + ], + }), + form: { + value: false, + }, + column: { + width: 100, + sorter: true, + align: "center", + }, + }, + checkStatus: { + title: "检查状态", + search: { + show: false, + }, + type: "dict-select", + dict: checkStatusDict, + form: { + show: false, + }, + column: { + width: 100, + align: "center", + sorter: true, + cellRender({ value, row, key }) { + return ( + + + + ); + }, + }, + }, + remark: { + title: "备注", + search: { + show: false, + }, + type: "text", + form: { + show: false, + }, + column: { + width: 200, + sorter: true, + tooltip: true, + }, + }, + }, + }, + }; +} diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/ip/index.vue b/packages/ui/certd-client/src/views/certd/monitor/site/ip/index.vue new file mode 100644 index 00000000..d4c31fd6 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/monitor/site/ip/index.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/ui/certd-client/src/views/certd/monitor/site/ip/use.tsx b/packages/ui/certd-client/src/views/certd/monitor/site/ip/use.tsx new file mode 100644 index 00000000..764cdd51 --- /dev/null +++ b/packages/ui/certd-client/src/views/certd/monitor/site/ip/use.tsx @@ -0,0 +1,40 @@ +import { useFormWrapper } from "@fast-crud/fast-crud"; +import { useRouter } from "vue-router"; + +import SiteIpCertMonitor from "./index.vue"; + +export function useSiteIpMonitor() { + const { openDialog } = useFormWrapper(); + const router = useRouter(); + + async function openSiteIpMonitorDialog(opts: { siteId: number }) { + await openDialog({ + wrapper: { + title: "站点IP监控", + width: "80%", + is: "a-modal", + footer: false, + buttons: { + cancel: { + show: false, + }, + reset: { + show: false, + }, + ok: { + show: false, + }, + }, + slots: { + "form-body-top": () => { + return ; + }, + }, + }, + }); + } + + return { + openSiteIpMonitorDialog, + }; +} diff --git a/packages/ui/certd-server/db/migration/v10023__site_ip.sql b/packages/ui/certd-server/db/migration/v10023__site_ip.sql new file mode 100644 index 00000000..b0e8338a --- /dev/null +++ b/packages/ui/certd-server/db/migration/v10023__site_ip.sql @@ -0,0 +1,28 @@ + +ALTER TABLE cd_site_info ADD COLUMN "ip_check" boolean; +ALTER TABLE cd_site_info ADD COLUMN "ip_count" integer; +ALTER TABLE cd_site_info ADD COLUMN "ip_error_count" integer; + + +CREATE TABLE "cd_site_ip" +( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "user_id" integer, + "site_id" integer, + "ip_address" varchar(100), + "cert_domains" 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), + "remark" varchar(4096), + "from" varchar(100), + "disabled" boolean NOT NULL DEFAULT (false), + "create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), + "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) +); + +CREATE INDEX "index_site_ip_user_id" ON "cd_site_ip" ("user_id"); +CREATE INDEX "index_site_ip_site_id" ON "cd_site_ip" ("site_id"); diff --git a/packages/ui/certd-server/src/controller/user/monitor/site-ip-controller.ts b/packages/ui/certd-server/src/controller/user/monitor/site-ip-controller.ts new file mode 100644 index 00000000..9217830a --- /dev/null +++ b/packages/ui/certd-server/src/controller/user/monitor/site-ip-controller.ts @@ -0,0 +1,97 @@ +import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core"; +import { Constants, CrudController } from "@certd/lib-server"; +import { AuthService } from "../../../modules/sys/authority/service/auth-service.js"; +import { SiteIpService } from "../../../modules/monitor/service/site-ip-service.js"; +import { SiteInfoService } from "../../../modules/monitor/index.js"; + +/** + */ +@Provide() +@Controller('/api/monitor/site/ip') +export class SiteInfoController extends CrudController { + @Inject() + service: SiteIpService; + @Inject() + authService: AuthService; + @Inject() + siteInfoService: SiteInfoService; + + getService(): SiteIpService { + return this.service; + } + + @Post('/page', { summary: Constants.per.authOnly }) + async page(@Body(ALL) body: any) { + body.query = body.query ?? {}; + body.query.userId = this.getUserId(); + const res = await this.service.page({ + query: body.query, + page: body.page, + sort: body.sort, + }); + return this.ok(res); + } + + @Post('/list', { summary: Constants.per.authOnly }) + async list(@Body(ALL) body: any) { + body.query = body.query ?? {}; + body.query.userId = this.getUserId(); + return await super.list(body); + } + + @Post('/add', { summary: Constants.per.authOnly }) + async add(@Body(ALL) bean: any) { + bean.userId = this.getUserId(); + bean.from = "manual" + const res = await this.service.add(bean); + this.service.check(res.id); + return this.ok(res); + } + + @Post('/update', { summary: Constants.per.authOnly }) + async update(@Body(ALL) bean) { + await this.service.checkUserId(bean.id, this.getUserId()); + delete bean.userId; + await this.service.update(bean); + this.service.check(bean.id); + return this.ok(); + } + @Post('/info', { summary: Constants.per.authOnly }) + async info(@Query('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + return await super.info(id); + } + + @Post('/delete', { summary: Constants.per.authOnly }) + async delete(@Query('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + return await super.delete(id); + } + + @Post('/check', { summary: Constants.per.authOnly }) + async check(@Body('id') id: number) { + await this.service.checkUserId(id, this.getUserId()); + this.service.check(id); + return this.ok(); + } + + @Post('/checkAll', { summary: Constants.per.authOnly }) + async checkAll(@Body('siteId') siteId: number) { + const userId = this.getUserId(); + await this.siteInfoService.checkUserId(siteId, userId); + await this.service.checkAll(siteId); + return this.ok(); + } + + @Post('/sync', { summary: Constants.per.authOnly }) + async sync(@Body('siteId') siteId: number) { + const userId = this.getUserId(); + const entity = await this.siteInfoService.info(siteId) + if(entity.userId != userId){ + throw new Error('无权限') + } + await this.service.sync(entity); + return this.ok(); + } + +} diff --git a/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts b/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts index 130f2abd..1c1e4eda 100644 --- a/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts +++ b/packages/ui/certd-server/src/modules/monitor/entity/site-info.ts @@ -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; diff --git a/packages/ui/certd-server/src/modules/monitor/entity/site-ip.ts b/packages/ui/certd-server/src/modules/monitor/entity/site-ip.ts new file mode 100644 index 00000000..3cfa50c3 --- /dev/null +++ b/packages/ui/certd-server/src/modules/monitor/entity/site-ip.ts @@ -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; +} diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts index 29485ddb..b8d12cfe 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/site-info-service.ts @@ -94,7 +94,7 @@ export class SiteInfoService extends BaseService { await this.update({ id: site.id, checkStatus: 'checking', - lastCheckTime: dayjs, + lastCheckTime: dayjs().valueOf(), }); const res = await siteTester.test({ host: site.domain, diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-ip-service.ts b/packages/ui/certd-server/src/modules/monitor/service/site-ip-service.ts new file mode 100644 index 00000000..34a665e9 --- /dev/null +++ b/packages/ui/certd-server/src/modules/monitor/service/site-ip-service.ts @@ -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 { + @InjectEntityModel(SiteIpEntity) + repository: Repository; + + @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((resolve, reject) => { + dns.resolve4(domain, (err, addresses) => { + if (err) { + logger.error(`[${domain}] resolve4 error`, err) + resolve([]) + return; + } + resolve(addresses); + }); + }); + + const getFromV6 = safePromise((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]]; + }); + } +} diff --git a/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts b/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts index 903fbdc5..bee5be38 100644 --- a/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts +++ b/packages/ui/certd-server/src/modules/monitor/service/site-tester.ts @@ -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 { - 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 { - 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(); diff --git a/packages/ui/certd-server/src/plugins/plugin-farcdn/access.ts b/packages/ui/certd-server/src/plugins/plugin-farcdn/access.ts index 0c6e65b4..c404e14d 100644 --- a/packages/ui/certd-server/src/plugins/plugin-farcdn/access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-farcdn/access.ts @@ -14,6 +14,7 @@ import { CertInfo, CertReader } from "@certd/plugin-cert"; export class FarcdnAccess extends BaseAccess { @AccessInput({ title: "接口地址", + value:"https://open.farcdn.net/api/source", component: { placeholder: "https://open.farcdn.net/api/source", name: "a-input", @@ -79,21 +80,16 @@ export class FarcdnAccess extends BaseAccess { testRequest = true; async onTestRequest() { - try{ - const data = await this.findSSLCertConfig(2106); - if (data) { - return "ok"; - } - }catch (e) { - if(e.message.indexOf("11111111")>-1){ - return "ok"; - } - throw e; - } - - throw "测试失败,未知错误"; + await this.getSSLCertList({size:1}); + return "ok" } + async getSSLCertList(req:{offset?:number,size?:number}){ + return await this.doRequest({ + url: "/getSSLCertList", + data: req + }); + } async findSSLCertConfig(sslCertId: number) { /** @@ -120,7 +116,7 @@ export class FarcdnAccess extends BaseAccess { sslCertId, }; const res= await this.doRequest({ - url: "/api/source/findSSLCertConfig", + url: "/findSSLCertConfig", data: params }); this.ctx.logger.info(`找到证书${sslCertId}: name=${res.name},domain=${res.commonNames},dnsNames=${res.dnsNames}`); @@ -186,7 +182,7 @@ export class FarcdnAccess extends BaseAccess { logData:true, }); - if (res.code === "200") { + if (res.code === 200) { return res.data; } throw new Error(res.message || res); diff --git a/packages/ui/certd-server/src/plugins/plugin-farcdn/plugins/plugin-refresh-cert.ts b/packages/ui/certd-server/src/plugins/plugin-farcdn/plugins/plugin-refresh-cert.ts index 36fed3e7..2f3dc9cf 100644 --- a/packages/ui/certd-server/src/plugins/plugin-farcdn/plugins/plugin-refresh-cert.ts +++ b/packages/ui/certd-server/src/plugins/plugin-farcdn/plugins/plugin-refresh-cert.ts @@ -1,4 +1,4 @@ -import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { IsTaskPlugin, PageReq, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { FarcdnAccess } from "../access.js"; @@ -8,6 +8,7 @@ import { AbstractPlusTaskPlugin } from "@certd/plugin-plus"; //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 name: "FarcdnRefreshCert", title: "farcdn-更新证书", + desc:"www.farcdn.net", icon: "svg:icon-lucky", //插件分组 group: pluginGroups.cdn.key, @@ -77,28 +78,31 @@ export class FarcdnRefreshCert extends AbstractPlusTaskPlugin { this.logger.info("部署完成"); } - async onGetCertList() { - throw new Error("暂无查询证书列表接口,您需要手动输入证书id"); - // const access = await this.getAccess(this.accessId); + async onGetCertList(data:PageReq = {}) { + const access = await this.getAccess(this.accessId); - // const res = await access.doRequest({ - // url: "/SSLCertService/listSSLCerts", - // data: { size: 1000 }, - // method: "POST" - // }); - // const list = JSON.parse(this.ctx.utils.hash.base64Decode(res.sslCertsJSON)); - // if (!list || list.length === 0) { - // throw new Error("没有找到证书,请先在控制台上传一次证书且关联网站"); - // } - // - // const options = list.map((item: any) => { - // return { - // label: `${item.name}<${item.id}-${item.dnsNames[0]}>`, - // value: item.id, - // domain: item.dnsNames - // }; - // }); - // return this.ctx.utils.options.buildGroupOptions(options, this.certDomains); + const res = await access.getSSLCertList({ + offset: data.offset?? 0, + size: data.limit?? 100, + }); + const list = res.list + if (!list || list.length === 0) { + throw new Error("没有找到证书,请先在控制台上传一次证书且关联网站"); + } + + const options = list.map((item: any) => { + return { + label: `${item.name}<${item.id}>`, + value: item.id, + domain: item.dnsNames + }; + }); + return { + list:this.ctx.utils.options.buildGroupOptions(options, this.certDomains), + total:res.total, + offset: res.offset, + limit:res.size + } } }