| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createConnection } from "node:net"; |
|
|
| |
| 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; |
| } |
|
|
| |
| const proxyHealthCache = new Map<string, ProxyHealthEntry>(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 { |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| 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; |
| return cached.healthy; |
| } |
|
|
| |
| |
| |
| export function invalidateProxyHealth(proxyUrl: string): void { |
| proxyHealthCache.delete(proxyUrl); |
| } |
|
|
| |
| |
| |
| 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, |
| })); |
| } |
|
|
| |
|
|
| 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); |
| }); |
| }); |
| } |
|
|