mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
/**
|
|
* T14: Proxy Fast-Fail — TCP health check with in-memory cache.
|
|
*
|
|
* When a configured HTTP/SOCKS5 proxy is unreachable, every request
|
|
* through OmniRoute used to wait for the full PROXY_TIMEOUT_MS (30s)
|
|
* before failing. This module detects dead proxies in <2s via a quick
|
|
* TCP connection check, caching the result to avoid overhead per request.
|
|
*
|
|
* Ref: sub2api PR #1167 (fix: proxy-fast-fail)
|
|
*/
|
|
|
|
import { createConnection } from "node:net";
|
|
|
|
// Configurable via env vars
|
|
const FAST_FAIL_TIMEOUT_MS = parseInt(process.env.PROXY_FAST_FAIL_TIMEOUT_MS ?? "2000", 10);
|
|
const HEALTH_CACHE_TTL_MS = parseInt(process.env.PROXY_HEALTH_CACHE_TTL_MS ?? "30000", 10);
|
|
|
|
interface ProxyHealthEntry {
|
|
healthy: boolean;
|
|
checkedAt: number;
|
|
ttlMs: number;
|
|
}
|
|
|
|
// In-memory cache: proxyUrl → health entry
|
|
const proxyHealthCache = new Map<string, ProxyHealthEntry>();
|
|
|
|
/**
|
|
* T14: Perform a fast TCP check to see if a proxy host:port is reachable.
|
|
* Results are cached for `cacheTtlMs` (default 30s) to avoid checking every request.
|
|
*
|
|
* @param proxyUrl - Full proxy URL, e.g. http://user:pass@1.2.3.4:8080
|
|
* @param timeoutMs - TCP connection timeout (default 2000ms)
|
|
* @param cacheTtlMs - How long to cache the health result (default 30000ms)
|
|
* @returns true if proxy TCP port is open, false otherwise
|
|
*/
|
|
export async function isProxyReachable(
|
|
proxyUrl: string,
|
|
timeoutMs = FAST_FAIL_TIMEOUT_MS,
|
|
cacheTtlMs = HEALTH_CACHE_TTL_MS
|
|
): Promise<boolean> {
|
|
const cached = proxyHealthCache.get(proxyUrl);
|
|
if (cached && Date.now() - cached.checkedAt < cached.ttlMs) {
|
|
return cached.healthy;
|
|
}
|
|
|
|
let url: URL;
|
|
try {
|
|
url = new URL(proxyUrl);
|
|
} catch {
|
|
// Malformed URL — treat as unreachable
|
|
proxyHealthCache.set(proxyUrl, {
|
|
healthy: false,
|
|
checkedAt: Date.now(),
|
|
ttlMs: cacheTtlMs,
|
|
});
|
|
return false;
|
|
}
|
|
|
|
const host = url.hostname;
|
|
const port = parseInt(url.port || defaultPortForScheme(url.protocol), 10);
|
|
|
|
if (!host || isNaN(port)) {
|
|
proxyHealthCache.set(proxyUrl, {
|
|
healthy: false,
|
|
checkedAt: Date.now(),
|
|
ttlMs: cacheTtlMs,
|
|
});
|
|
return false;
|
|
}
|
|
|
|
const healthy = await tcpCheck(host, port, timeoutMs);
|
|
proxyHealthCache.set(proxyUrl, { healthy, checkedAt: Date.now(), ttlMs: cacheTtlMs });
|
|
return healthy;
|
|
}
|
|
|
|
/**
|
|
* Get the cached health status of a proxy without re-checking.
|
|
* Returns null if there is no cached entry.
|
|
*/
|
|
export function getCachedProxyHealth(proxyUrl: string): boolean | null {
|
|
const cached = proxyHealthCache.get(proxyUrl);
|
|
if (!cached) return null;
|
|
if (Date.now() - cached.checkedAt >= cached.ttlMs) return null; // stale
|
|
return cached.healthy;
|
|
}
|
|
|
|
/**
|
|
* Invalidate the cached health for a proxy URL (force re-check on next call).
|
|
*/
|
|
export function invalidateProxyHealth(proxyUrl: string): void {
|
|
proxyHealthCache.delete(proxyUrl);
|
|
}
|
|
|
|
/**
|
|
* Get all currently cached proxy health entries (for dashboard display).
|
|
*/
|
|
export function getAllProxyHealthStatuses(): Array<{
|
|
proxyUrl: string;
|
|
healthy: boolean;
|
|
checkedAt: number;
|
|
stale: boolean;
|
|
}> {
|
|
const now = Date.now();
|
|
return [...proxyHealthCache.entries()].map(([proxyUrl, entry]) => ({
|
|
proxyUrl,
|
|
healthy: entry.healthy,
|
|
checkedAt: entry.checkedAt,
|
|
stale: now - entry.checkedAt >= entry.ttlMs,
|
|
}));
|
|
}
|
|
|
|
// ─── Internals ────────────────────────────────────────────────────────────────
|
|
|
|
function defaultPortForScheme(protocol: string): string {
|
|
switch (protocol.replace(":", "").toLowerCase()) {
|
|
case "https":
|
|
return "443";
|
|
case "socks5":
|
|
case "socks5h":
|
|
return "1080";
|
|
case "http":
|
|
default:
|
|
return "8080";
|
|
}
|
|
}
|
|
|
|
function tcpCheck(host: string, port: number, timeoutMs: number): Promise<boolean> {
|
|
return new Promise<boolean>((resolve) => {
|
|
const socket = createConnection({ host, port }, () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.setTimeout(timeoutMs);
|
|
socket.on("error", () => resolve(false));
|
|
socket.on("timeout", () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
});
|
|
}
|