mirror of https://github.com/certd/certd
perf: 证书检查支持自定义dns服务器
parent
0cea26c628
commit
c53bb7cf67
|
@ -34,6 +34,7 @@ import { locker } from "./util.lock.js";
|
|||
import { mitter } from "./util.mitter.js";
|
||||
|
||||
import * as request from "./util.request.js";
|
||||
export * from "./util.cache.js";
|
||||
export const utils = {
|
||||
sleep,
|
||||
http,
|
||||
|
|
|
@ -1,8 +1,53 @@
|
|||
// LRUCache
|
||||
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import { LRUCache } from "lru-cache";
|
||||
|
||||
export const cache = new LRUCache<string, any>({
|
||||
max: 1000,
|
||||
ttl: 1000 * 60 * 10,
|
||||
});
|
||||
|
||||
export class LocalCache<V = any> {
|
||||
cache: Map<string, { value: V; expiresAt: number }>;
|
||||
constructor(opts: { clearInterval?: number } = {}) {
|
||||
this.cache = new Map();
|
||||
setInterval(() => {
|
||||
this.clearExpires();
|
||||
}, opts.clearInterval ?? 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
get(key: string): V | undefined {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > entry.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
set(key: string, value: V, ttl = 300000) {
|
||||
// 默认5分钟 (300000毫秒)
|
||||
this.cache.set(key, {
|
||||
value,
|
||||
expiresAt: Date.now() + ttl,
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
clearExpires() {
|
||||
for (const [key, entry] of this.cache) {
|
||||
if (entry.expiresAt < Date.now()) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,3 +67,8 @@ footer {
|
|||
margin-inline-end: calc(-3em - 8px);
|
||||
padding-inline-end: calc(3em + 8px);
|
||||
}
|
||||
|
||||
|
||||
.ant-progress .ant-progress-text{
|
||||
width:3em;
|
||||
}
|
|
@ -47,6 +47,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
const { openSiteIpMonitorDialog } = useSiteIpMonitor();
|
||||
const { openSiteImportDialog } = useSiteImport();
|
||||
return {
|
||||
id: "siteMonitorCrud",
|
||||
crudOptions: {
|
||||
request: {
|
||||
pageRequest,
|
||||
|
@ -117,7 +118,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
},
|
||||
rowHandle: {
|
||||
fixed: "right",
|
||||
width: 240,
|
||||
width: 280,
|
||||
buttons: {
|
||||
check: {
|
||||
order: 0,
|
||||
|
|
|
@ -45,6 +45,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
|||
const { openSiteIpImportDialog } = useSiteIpMonitor();
|
||||
return {
|
||||
crudOptions: {
|
||||
id: "siteIpCrud",
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
|
|
|
@ -17,12 +17,12 @@
|
|||
</div>
|
||||
<div class="helper">{{ t("certd.monitor.setting.monitorRetryTimes") }}</div>
|
||||
</a-form-item>
|
||||
<!-- <a-form-item :label="t('certd.monitor.setting.dnsServer')" :name="['dnsServer']">
|
||||
<a-form-item :label="t('certd.monitor.setting.dnsServer')" :name="['dnsServer']">
|
||||
<div class="flex">
|
||||
<a-select v-model:value="formState.dnsServer" mode="tags" :open="false" />
|
||||
</div>
|
||||
<div class="helper">{{ t("certd.monitor.setting.dnsServerHelper") }}</div>
|
||||
</a-form-item> -->
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.monitor.setting.monitorCronSetting')" :name="['cron']">
|
||||
<div class="flex flex-baseline">
|
||||
<cron-editor v-model="formState.cron" :disabled="!settingsStore.isPlus" :allow-every-min="userStore.isAdmin" />
|
||||
|
|
|
@ -82,7 +82,6 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dns2": "^2.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
|
|
|
@ -27,6 +27,7 @@ export class UserSiteMonitorSetting extends BaseSettings {
|
|||
notificationId?:number= 0;
|
||||
cron?:string = undefined;
|
||||
retryTimes?:number = 3;
|
||||
dnsServer?:string[] = undefined;
|
||||
}
|
||||
|
||||
export class UserEmailSetting extends BaseSettings {
|
||||
|
|
|
@ -3,8 +3,13 @@ import { InjectEntityModel } from "@midwayjs/typeorm";
|
|||
import { Repository } from "typeorm";
|
||||
import { BaseService, BaseSettings } from "@certd/lib-server";
|
||||
import { UserSettingsEntity } from "../entity/user-settings.js";
|
||||
import { mergeUtils } from "@certd/basic";
|
||||
import { LocalCache, mergeUtils } from "@certd/basic";
|
||||
const {merge} = mergeUtils
|
||||
|
||||
const UserSettingCache = new LocalCache({
|
||||
clearInterval: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
/**
|
||||
* 授权
|
||||
*/
|
||||
|
@ -75,14 +80,26 @@ export class UserSettingsService extends BaseService<UserSettingsEntity> {
|
|||
}
|
||||
|
||||
|
||||
async getSetting<T>( userId: number,type: any): Promise<T> {
|
||||
async getSetting<T>( userId: number,type: any, cache:boolean = false): Promise<T> {
|
||||
if(!userId){
|
||||
throw new Error('userId is required');
|
||||
}
|
||||
const key = type.__key__;
|
||||
const cacheKey = key + '_' + userId;
|
||||
if (cache) {
|
||||
const settings: T = UserSettingCache.get(cacheKey);
|
||||
if (settings) {
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
let newSetting: T = new type();
|
||||
const savedSettings = await this.getSettingByKey(key, userId);
|
||||
newSetting = merge(newSetting, savedSettings);
|
||||
|
||||
if (cache) {
|
||||
UserSettingCache.set(cacheKey, newSetting);
|
||||
}
|
||||
return newSetting;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { LocalCache } from '@certd/basic';
|
||||
import dnsSdk from 'dns'
|
||||
const dns = dnsSdk.promises
|
||||
|
||||
export class DnsCustom{
|
||||
resolver: any;
|
||||
|
||||
constructor(dnsServers:string[]) {
|
||||
const resolver = new dns.Resolver();
|
||||
resolver.setServers(dnsServers);
|
||||
this.resolver = resolver;
|
||||
}
|
||||
async resolve(hostname:string,options:any):Promise<string[]>{
|
||||
// { family: undefined, hints: 0, all: true }
|
||||
|
||||
const cnames = await this.resolver.resolveCname(hostname)
|
||||
let cnameIps = []
|
||||
// deep
|
||||
if (cnames && cnames.length > 0) {
|
||||
for (let cname of cnames) {
|
||||
const cnameIp = await this.resolve(cname,options)
|
||||
if (cnameIp && cnameIp.length > 0) {
|
||||
cnameIps.push(...cnameIp)
|
||||
}
|
||||
}
|
||||
}
|
||||
let v4 = []
|
||||
let v6 = []
|
||||
|
||||
const {family, all} = options
|
||||
if(family === 6 && !all){
|
||||
v6= await this.resolver.resolve6(hostname)
|
||||
}
|
||||
if(family === 4 && !all){
|
||||
v4 = await this.resolver.resolve4(hostname)
|
||||
}
|
||||
|
||||
if(all){
|
||||
v4 = await this.resolver.resolve4(hostname)
|
||||
v6 = await this.resolver.resolve6(hostname)
|
||||
}
|
||||
|
||||
return [...v4,...v6,...cnameIps]
|
||||
}
|
||||
|
||||
async resolve4(hostname:string,options:any):Promise<string[]>{
|
||||
return await this.resolver.resolve4(hostname,options)
|
||||
}
|
||||
async resolve6(hostname:string,options:any):Promise<string[]>{
|
||||
return await this.resolver.resolve6(hostname,options)
|
||||
}
|
||||
async resolveAny(hostname:string,options:any):Promise<string[]>{
|
||||
return await this.resolver.resolveAny(hostname,options)
|
||||
}
|
||||
|
||||
async resolveCname(hostname:string,options:any):Promise<string[]>{
|
||||
return await this.resolver.resolveCname(hostname,options)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class DnsContainer{
|
||||
bucket: LocalCache<DnsCustom> = new LocalCache()
|
||||
|
||||
constructor() {}
|
||||
getDns(server:string[]){
|
||||
const key = server.join(',')
|
||||
let dns = this.bucket.get(key)
|
||||
if (dns){
|
||||
return dns
|
||||
}
|
||||
dns = new DnsCustom(server)
|
||||
this.bucket.set(key,dns)
|
||||
return dns
|
||||
}
|
||||
}
|
||||
|
||||
export const dnsContainer = new DnsContainer()
|
|
@ -15,6 +15,7 @@ import {UserSiteMonitorSetting} from "../../mine/service/models.js";
|
|||
import {SiteIpService} from "./site-ip-service.js";
|
||||
import {SiteIpEntity} from "../entity/site-ip.js";
|
||||
import {Cron} from "../../cron/cron.js";
|
||||
import { dnsContainer } from "./dns-custom.js";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, {allowDowngrade: true})
|
||||
|
@ -108,6 +109,14 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
|||
if (!site?.domain) {
|
||||
throw new Error("站点域名不能为空");
|
||||
}
|
||||
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting);
|
||||
const dnsServer = setting.dnsServer
|
||||
let resolver = null
|
||||
if (dnsServer && dnsServer.length > 0) {
|
||||
resolver = dnsContainer.getDns(dnsServer) as any
|
||||
}
|
||||
|
||||
try {
|
||||
await this.update({
|
||||
id: site.id,
|
||||
|
@ -117,7 +126,8 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
|
|||
const res = await siteTester.test({
|
||||
host: site.domain,
|
||||
port: site.httpsPort,
|
||||
retryTimes
|
||||
retryTimes,
|
||||
resolver
|
||||
});
|
||||
|
||||
const certi: PeerCertificate = res.certificate;
|
||||
|
|
|
@ -7,11 +7,15 @@ import {NotificationService} from "../../pipeline/service/notification-service.j
|
|||
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 dnsSdk from "dns";
|
||||
import {logger} from "@certd/basic";
|
||||
import dayjs from "dayjs";
|
||||
import {siteTester} from "./site-tester.js";
|
||||
import {PeerCertificate} from "tls";
|
||||
import { UserSiteMonitorSetting } from "../../mine/service/models.js";
|
||||
import { dnsContainer } from "./dns-custom.js";
|
||||
|
||||
const dns = dnsSdk.promises;
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
|
@ -62,11 +66,20 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
|
|||
async sync(entity: SiteInfoEntity,check:boolean = true) {
|
||||
|
||||
const domain = entity.domain;
|
||||
|
||||
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(entity.userId, UserSiteMonitorSetting);
|
||||
|
||||
const dnsServer = setting.dnsServer
|
||||
let resolver = dns
|
||||
if (dnsServer && dnsServer.length > 0) {
|
||||
resolver = dnsContainer.getDns(dnsServer) as any
|
||||
}
|
||||
|
||||
//从域名解析中获取所有ip
|
||||
const ips = await this.getAllIpsFromDomain(domain);
|
||||
const ips = await this.getAllIpsFromDomain(domain,resolver);
|
||||
if (ips.length === 0 ) {
|
||||
logger.warn(`没有发现${domain}的IP`)
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
const oldIps = await this.repository.find({
|
||||
|
@ -86,7 +99,7 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
|
|||
hasChanged = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(hasChanged){
|
||||
logger.info(`发现${domain}的IP变化,需要更新,旧IP:${oldIps.map(ip=>ip.ipAddress).join(",")},新IP:${ips.join(",")}`)
|
||||
//有变化需要更新
|
||||
|
@ -213,30 +226,26 @@ export class SiteIpService extends BaseService<SiteIpEntity> {
|
|||
})
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
async getAllIpsFromDomain(domain: string,resolver:any = dns):Promise<string[]> {
|
||||
const getFromV4 = async ():Promise<string[]> => {
|
||||
try{
|
||||
return await resolver.resolve4(domain);
|
||||
}catch (err) {
|
||||
logger.error(`[${domain}] resolve4 error`, err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
const getFromV6 = async ():Promise<string[]> => {
|
||||
try{
|
||||
return await resolver.resolve6(domain);
|
||||
}catch (err) {
|
||||
logger.error(`[${domain}] resolve6 error`, err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
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 Promise.all([getFromV4(), getFromV6()]).then(res => {
|
||||
return [...res[0], ...res[1]];
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import { logger, safePromise, utils } from "@certd/basic";
|
|||
import { merge } from "lodash-es";
|
||||
import https from "https";
|
||||
import { PeerCertificate } from "tls";
|
||||
// import { TCPClient } from "dns2";
|
||||
|
||||
export type SiteTestReq = {
|
||||
host: string; // 只用域名部分
|
||||
|
@ -11,7 +10,7 @@ export type SiteTestReq = {
|
|||
retryTimes?: number;
|
||||
ipAddress?: string;
|
||||
|
||||
dnsServer?: string[];
|
||||
resolver?: any;
|
||||
};
|
||||
|
||||
export type SiteTestRes = {
|
||||
|
@ -52,6 +51,7 @@ export class SiteTester {
|
|||
req
|
||||
);
|
||||
|
||||
let customLookup = null
|
||||
if (req.ipAddress) {
|
||||
//使用固定的ip
|
||||
const ipAddress = req.ipAddress;
|
||||
|
@ -61,37 +61,20 @@ export class SiteTester {
|
|||
servername: options.host
|
||||
};
|
||||
options.host = ipAddress;
|
||||
}else if (req.resolver ) {
|
||||
// 非ip address 请求时
|
||||
const resolver = req.resolver
|
||||
customLookup = async (hostname:string, options:any, callback)=> {
|
||||
console.log(hostname, options);
|
||||
|
||||
// { family: undefined, hints: 0, all: true }
|
||||
const res = await resolver.resolve(hostname, options)
|
||||
console.log("custom lookup res:",res)
|
||||
callback(null, res);
|
||||
}
|
||||
}
|
||||
|
||||
// let dnsClients = [];
|
||||
// if (req.dnsServer && req.dnsServer.length > 0) {
|
||||
// for (let dns of req.dnsServer) {
|
||||
// const dnsClient = TCPClient({ dns });
|
||||
// dnsClients.push(dnsClient);
|
||||
// }
|
||||
// }
|
||||
|
||||
// async function customLookup(hostname, options, callback) {
|
||||
// for (let client of dnsClients) {
|
||||
// try {
|
||||
// const result = await client.resolve(hostname, options);
|
||||
// return callback(null, result);
|
||||
// } catch (e) {
|
||||
// this.logger.error(e);
|
||||
// }
|
||||
// }
|
||||
// try {
|
||||
// // 使用自定义DNS解析
|
||||
// const response = await dnsClients
|
||||
// const address = response.answers[0].address;
|
||||
// callback(null, address, 4);
|
||||
// } catch (err) {
|
||||
// // 解析失败时回退到系统DNS
|
||||
// require('dns').lookup(hostname, options, callback);
|
||||
// }
|
||||
// }
|
||||
|
||||
options.agent = new https.Agent({ keepAlive: false });
|
||||
options.agent = new https.Agent({ keepAlive: false, lookup: customLookup });
|
||||
|
||||
// 创建 HTTPS 请求
|
||||
const requestPromise = safePromise((resolve, reject) => {
|
||||
|
|
Loading…
Reference in New Issue