chore: v0.6.0 baseline — 主题美化前存档

已实现:HTTP/TCP/Banner 探测、90 天可用率、表格/卡片视图、
数据源过滤、UptimeRobot 双向兼容、服务端驱动的刷新节奏、
刷新接口滥用防护与信息泄露控制。
This commit is contained in:
tamakyi
2026-10-13 18:31:58 +08:00
commit 3db3ce570a
31 changed files with 5136 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import net from 'node:net';
// 协议族识别只用于生成「SSH 响应正常」这类摘要;版本号等细节默认不回显,避免写入公开的 status.json
const FAMILIES = [
{ re: /^SSH-\d/i, name: 'SSH' },
{ re: /^220[\s\S]*ESMTP/i, name: 'SMTP' },
{ re: /^220[\s\S]*FTP/i, name: 'FTP' },
{ re: /^\+OK/i, name: 'POP3' },
{ re: /^\* OK/i, name: 'IMAP' },
{ re: /^HTTP\/\d/i, name: 'HTTP' },
{ re: /^220\b/i, name: 'SMTP/TCP' },
];
/**
* 被动 banner 探测:连接后仅读取服务端主动发送的标识串(RFC 4253 的 SSH banner 即此类),
* 不做认证、不发送任何数据、不执行任何命令——因此不会触发目标侧的登录失败计数或封禁。
* 需要客户端先发字节的协议(Redis/MySQL)不适用,属于路线图内容。
*/
export function probeBanner(monitor) {
const started = Date.now();
const timeoutMs = (monitor.timeout ?? 10) * 1000;
const maxBytes = Math.max(32, Number(monitor.maxBannerBytes) || 512);
return new Promise((resolve) => {
const socket = net.createConnection({ host: monitor.host, port: monitor.port });
let raw = '';
let settled = false;
const done = (status, detail) => {
if (settled) return;
settled = true;
socket.destroy();
resolve({ status, latency: Date.now() - started, code: null, detail });
};
const evaluate = () => {
const line = (raw.split(/\r?\n/)[0] ?? '').trim();
const family = FAMILIES.find((f) => f.re.test(line))?.name ?? null;
if (monitor.expectBanner) {
let ok = false;
try {
ok = new RegExp(monitor.expectBanner, monitor.bannerFlags || 'i').test(line);
} catch {
return done('down', 'banner 规则无法编译(expectBanner 不是合法正则)');
}
// 不匹配时只报告策略结果,不回显实际收到的标识串
if (!ok) return done('down', '标识串与期望规则不符');
}
const label = family ? `${family} 响应正常` : '收到服务标识串';
return done('up', monitor.revealBanner ? `${label}:${line.slice(0, 80)}` : label);
};
socket.setTimeout(timeoutMs, () => done('down', '等待标识串超时'));
socket.once('error', (err) =>
done('down', err.code === 'ECONNREFUSED' ? '连接被拒绝(端口关闭)' : (err.code || err.message))
);
// 对端发完就断开(无换行结尾)也算有效响应
socket.once('end', () => (raw ? evaluate() : done('down', '连接被对端关闭且未发送标识串')));
// 只在收到换行或达到字节上限时判定,避免把分包到达的标识串截断
socket.on('data', (chunk) => {
raw += chunk.toString('latin1');
if (/[\r\n]/.test(raw) || raw.length >= maxBytes) evaluate();
});
});
}