| import { parseProxyConfig } from './proxy-parser.js'; |
| import { buildProxyDispatcher } from './proxy-dispatcher.js'; |
| import type { ProxySettings, TestProxyInput, TestProxyResult } from '@maka/core'; |
| import { fetch, type Dispatcher } from 'undici'; |
| |
| const DEFAULT_PROBE_URL = 'https://icanhazip.com'; |
| const DEFAULT_TIMEOUT_MS = 8_000; |
| |
| export async function testProxyConnection( |
| input: TestProxyInput = {}, |
| storedProxy?: ProxySettings, |
| ): Promise<TestProxyResult> { |
| const proxy = parseProxyConfig(input.proxy ?? storedProxy); |
| const probeUrl = input.url ?? DEFAULT_PROBE_URL; |
| const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
| |
| if (!proxy.enabled) return { ok: false, latencyMs: 0, error: 'Proxy disabled' }; |
| if (!proxy.host || !proxy.port) |
| return { ok: false, latencyMs: 0, error: 'Proxy host/port required' }; |
| |
| const dispatcher = buildProxyDispatcher(proxy); |
| const controller = new AbortController(); |
| let timedOut = false; |
| const disposeDispatcher = async (force = false) => { |
| const disposable = dispatcher as { |
| close?: () => Promise<void>; |
| destroy?: (error?: Error) => void | Promise<void>; |
| }; |
| if (force && typeof disposable.destroy === 'function') { |
| await Promise.resolve( |
| disposable.destroy.call(dispatcher, new Error('Proxy test timeout')), |
| ).catch(() => {}); |
| return; |
| } |
| if (typeof disposable.close === 'function') |
| await disposable.close.call(dispatcher).catch(() => {}); |
| }; |
| const timeout = new Promise<never>((_resolve, reject) => { |
| const timer = setTimeout(() => { |
| timedOut = true; |
| controller.abort(new Error('Proxy test timeout')); |
| void disposeDispatcher(true); |
| reject(new Error('Proxy test timeout')); |
| }, timeoutMs); |
| controller.signal.addEventListener('abort', () => clearTimeout(timer), { once: true }); |
| }); |
| const startedAt = Date.now(); |
| |
| try { |
| const request = fetch(probeUrl, { dispatcher, signal: controller.signal }).catch((error) => { |
| if (timedOut) return new Promise<never>(() => {}); |
| throw error; |
| }); |
| const response = await Promise.race([request, timeout]); |
| const latencyMs = Date.now() - startedAt; |
| if (!response.ok) |
| return { ok: false, status: response.status, latencyMs, error: `HTTP ${response.status}` }; |
| |
| const ip = (await response.text()).trim() || undefined; |
| const countryCode = ip |
| ? await lookupCountry(ip, dispatcher as Dispatcher, controller.signal) |
| : undefined; |
| const countryFlag = |
| countryCode && countryCode.length === 2 |
| ? String.fromCodePoint( |
| ...countryCode |
| .toUpperCase() |
| .split('') |
| .map((char) => 127_397 + char.charCodeAt(0)), |
| ) |
| : undefined; |
| |
| return { ok: true, status: response.status, latencyMs, ip, countryCode, countryFlag }; |
| } catch (error) { |
| return { |
| ok: false, |
| latencyMs: Date.now() - startedAt, |
| error: error instanceof Error ? error.message : String(error), |
| }; |
| } finally { |
| controller.abort(); |
| await disposeDispatcher(timedOut); |
| } |
| } |
| |
| async function lookupCountry( |
| ip: string, |
| dispatcher: Dispatcher, |
| signal: AbortSignal, |
| ): Promise<string | undefined> { |
| try { |
| const response = await fetch(`https://api.country.is/${encodeURIComponent(ip)}`, { |
| dispatcher, |
| signal, |
| }); |
| if (!response.ok) return undefined; |
| const json = (await response.json()) as { country?: string }; |
| return json.country; |
| } catch { |
| return undefined; |
| } |
| } |