2025-05-28 00:57:52 +08:00
|
|
|
|
import { logger, safePromise, utils } from "@certd/basic";
|
|
|
|
|
|
import { merge } from "lodash-es";
|
|
|
|
|
|
import https from "https";
|
|
|
|
|
|
import { PeerCertificate } from "tls";
|
2025-07-31 10:44:50 +08:00
|
|
|
|
import {DnsCustom} from "./dns-custom.js";
|
2025-05-28 00:57:52 +08:00
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
export type SiteTestReq = {
|
|
|
|
|
|
host: string; // 只用域名部分
|
|
|
|
|
|
port?: number;
|
|
|
|
|
|
method?: string;
|
2025-01-04 20:10:00 +08:00
|
|
|
|
retryTimes?: number;
|
2025-05-28 00:57:52 +08:00
|
|
|
|
ipAddress?: string;
|
2025-06-30 23:48:00 +08:00
|
|
|
|
|
2025-07-31 10:44:50 +08:00
|
|
|
|
customDns?: DnsCustom;
|
2024-12-23 18:11:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export type SiteTestRes = {
|
|
|
|
|
|
certificate?: PeerCertificate;
|
|
|
|
|
|
};
|
2025-05-28 00:57:52 +08:00
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
export class SiteTester {
|
|
|
|
|
|
async test(req: SiteTestReq): Promise<SiteTestRes> {
|
2025-07-31 10:44:50 +08:00
|
|
|
|
const req_ = {...req}
|
|
|
|
|
|
delete req_.customDns
|
|
|
|
|
|
logger.info("测试站点:", JSON.stringify(req_));
|
2025-06-30 23:48:00 +08:00
|
|
|
|
const maxRetryTimes = req.retryTimes == null ? 3 : req.retryTimes;
|
2025-01-04 20:10:00 +08:00
|
|
|
|
let tryCount = 0;
|
|
|
|
|
|
let result: SiteTestRes = {};
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
result = await this.doTestOnce(req);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
tryCount++;
|
|
|
|
|
|
if (tryCount > maxRetryTimes) {
|
2025-06-13 00:25:08 +08:00
|
|
|
|
logger.error(`测试站点出错,已超过最大重试次数(${maxRetryTimes})`, e.message);
|
2025-01-04 20:10:00 +08:00
|
|
|
|
throw e;
|
|
|
|
|
|
}
|
|
|
|
|
|
//指数退避
|
|
|
|
|
|
const time = 2 ** tryCount;
|
2025-06-13 00:25:08 +08:00
|
|
|
|
logger.error(`测试站点出错,${time}s后重试(${tryCount}/${maxRetryTimes})`, e);
|
2025-01-04 20:10:00 +08:00
|
|
|
|
await utils.sleep(time * 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async doTestOnce(req: SiteTestReq): Promise<SiteTestRes> {
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const options: any = merge(
|
|
|
|
|
|
{
|
|
|
|
|
|
port: 443,
|
2025-05-28 00:57:52 +08:00
|
|
|
|
method: "GET",
|
|
|
|
|
|
rejectUnauthorized: false
|
2024-12-23 18:11:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
req
|
|
|
|
|
|
);
|
2025-05-28 00:57:52 +08:00
|
|
|
|
|
2025-07-07 00:10:51 +08:00
|
|
|
|
let customLookup = null
|
2025-05-28 00:57:52 +08:00
|
|
|
|
if (req.ipAddress) {
|
|
|
|
|
|
//使用固定的ip
|
|
|
|
|
|
const ipAddress = req.ipAddress;
|
2025-06-30 23:48:00 +08:00
|
|
|
|
options.headers = {
|
2025-05-28 13:57:31 +08:00
|
|
|
|
host: options.host,
|
|
|
|
|
|
//sni
|
|
|
|
|
|
servername: options.host
|
2025-06-30 23:48:00 +08:00
|
|
|
|
};
|
2025-05-28 13:57:31 +08:00
|
|
|
|
options.host = ipAddress;
|
2025-07-31 10:44:50 +08:00
|
|
|
|
}else if (req.customDns ) {
|
2025-07-07 00:10:51 +08:00
|
|
|
|
// 非ip address 请求时
|
2025-07-31 10:44:50 +08:00
|
|
|
|
const customDns = req.customDns
|
2025-07-07 00:10:51 +08:00
|
|
|
|
customLookup = async (hostname:string, options:any, callback)=> {
|
|
|
|
|
|
console.log(hostname, options);
|
2025-06-30 23:48:00 +08:00
|
|
|
|
|
2025-07-07 00:10:51 +08:00
|
|
|
|
// { family: undefined, hints: 0, all: true }
|
2025-07-31 10:44:50 +08:00
|
|
|
|
const res = await customDns.lookup(hostname, options)
|
2025-07-07 00:10:51 +08:00
|
|
|
|
console.log("custom lookup res:",res)
|
2025-07-31 10:44:50 +08:00
|
|
|
|
if (!res || res.length === 0) {
|
|
|
|
|
|
callback(new Error("没有解析到IP"));
|
|
|
|
|
|
}
|
2025-07-07 00:10:51 +08:00
|
|
|
|
callback(null, res);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-30 23:48:00 +08:00
|
|
|
|
|
2025-09-10 14:12:36 +08:00
|
|
|
|
const agentOptions:any = { keepAlive: false };
|
|
|
|
|
|
if (customLookup) {
|
|
|
|
|
|
agentOptions.lookup = customLookup
|
|
|
|
|
|
}
|
|
|
|
|
|
options.agent = new https.Agent(agentOptions);
|
2025-05-28 00:57:52 +08:00
|
|
|
|
|
2024-12-23 18:11:06 +08:00
|
|
|
|
// 创建 HTTPS 请求
|
2025-04-30 09:38:44 +08:00
|
|
|
|
const requestPromise = safePromise((resolve, reject) => {
|
2024-12-23 18:11:06 +08:00
|
|
|
|
const req = https.request(options, res => {
|
|
|
|
|
|
// 获取证书
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
const certificate = res.socket.getPeerCertificate();
|
|
|
|
|
|
// logger.info('证书信息', certificate);
|
|
|
|
|
|
if (certificate.subject == null) {
|
2025-05-28 00:57:52 +08:00
|
|
|
|
logger.warn("证书信息为空");
|
2024-12-23 18:11:06 +08:00
|
|
|
|
resolve({
|
2025-05-28 00:57:52 +08:00
|
|
|
|
certificate: null
|
2024-12-23 18:11:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve({
|
2025-05-28 00:57:52 +08:00
|
|
|
|
certificate
|
2024-12-23 18:11:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
res.socket.end();
|
|
|
|
|
|
// 关闭响应
|
|
|
|
|
|
res.destroy();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-05-28 00:57:52 +08:00
|
|
|
|
req.on("error", e => {
|
2024-12-23 18:11:06 +08:00
|
|
|
|
reject(e);
|
|
|
|
|
|
});
|
|
|
|
|
|
req.end();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return await requestPromise;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const siteTester = new SiteTester();
|