144 lines
4.3 KiB
TypeScript
144 lines
4.3 KiB
TypeScript
/**
|
||
* 统一代理工具 — 支持 HTTP/HTTPS/SOCKS5/SOCKS5h 协议
|
||
*
|
||
* Node 20+ 原生 fetch() 使用 undici Dispatcher,但 socks-proxy-agent 不实现此接口。
|
||
* 解决方案:使用 http.Agent 接口 + http/https.request()。
|
||
*/
|
||
|
||
let HttpsProxyAgent: any;
|
||
let SocksProxyAgent: any;
|
||
|
||
try {
|
||
HttpsProxyAgent = require('https-proxy-agent').HttpsProxyAgent;
|
||
} catch {
|
||
try { HttpsProxyAgent = require('https-proxy-agent'); } catch {}
|
||
}
|
||
|
||
try {
|
||
SocksProxyAgent = require('socks-proxy-agent').SocksProxyAgent;
|
||
} catch {
|
||
try { SocksProxyAgent = require('socks-proxy-agent'); } catch {}
|
||
}
|
||
|
||
/** Create an http.Agent for the given proxy URL (works with https.request) */
|
||
function createProxyAgent(proxyUrl: string): any | null {
|
||
if (!proxyUrl || typeof proxyUrl !== 'string') return null;
|
||
const trimmed = proxyUrl.trim();
|
||
if (!trimmed) return null;
|
||
const lower = trimmed.toLowerCase();
|
||
|
||
try {
|
||
if (lower.startsWith('socks5://') || lower.startsWith('socks5h://')) {
|
||
if (!SocksProxyAgent) {
|
||
console.warn('[Proxy] socks-proxy-agent not installed');
|
||
return null;
|
||
}
|
||
return new SocksProxyAgent(trimmed);
|
||
}
|
||
if (lower.startsWith('http://') || lower.startsWith('https://')) {
|
||
if (!HttpsProxyAgent) {
|
||
console.warn('[Proxy] No HTTP proxy agent available');
|
||
return null;
|
||
}
|
||
return new HttpsProxyAgent(trimmed);
|
||
}
|
||
// Unknown scheme — try as HTTP proxy
|
||
if (HttpsProxyAgent) return new HttpsProxyAgent(trimmed);
|
||
return null;
|
||
} catch (err: any) {
|
||
console.error(`[Proxy] Failed to create proxy agent: ${err.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fetch with proxy support.
|
||
* Uses native fetch() when no proxy, or http/https.request() with agent when proxy is set.
|
||
*/
|
||
export async function proxiedFetch(
|
||
url: string,
|
||
init?: RequestInit,
|
||
proxyUrl?: string,
|
||
): Promise<Response> {
|
||
if (!proxyUrl) return fetch(url, init);
|
||
|
||
const agent = createProxyAgent(proxyUrl);
|
||
if (!agent) return fetch(url, init);
|
||
|
||
const parsedUrl = new URL(url);
|
||
const mod = parsedUrl.protocol === 'https:' ? require('https') : require('http');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const headers: Record<string, string> = {};
|
||
if (init?.headers) {
|
||
const h = init.headers as any;
|
||
if (h instanceof Headers) {
|
||
h.forEach((v, k) => { headers[k] = v; });
|
||
} else if (typeof h === 'object') {
|
||
Object.assign(headers, h);
|
||
}
|
||
}
|
||
|
||
const options: any = {
|
||
hostname: parsedUrl.hostname,
|
||
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||
path: parsedUrl.pathname + parsedUrl.search,
|
||
method: init?.method || 'GET',
|
||
headers,
|
||
agent,
|
||
};
|
||
|
||
const req = mod.request(options, (res: any) => {
|
||
const chunks: Buffer[] = [];
|
||
res.on('data', (c: Buffer) => chunks.push(c));
|
||
res.on('end', () => {
|
||
const body = Buffer.concat(chunks);
|
||
resolve(new Response(body, {
|
||
status: res.statusCode || 502,
|
||
statusText: res.statusMessage || '',
|
||
headers: new Headers(res.headers || {}),
|
||
}));
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
|
||
if (init?.signal) {
|
||
init.signal.addEventListener('abort', () => req.destroy());
|
||
}
|
||
|
||
if (init?.body) {
|
||
req.write(
|
||
typeof init.body === 'string' ? init.body :
|
||
init.body instanceof Buffer ? init.body :
|
||
init.body instanceof ArrayBuffer ? Buffer.from(init.body) :
|
||
Buffer.from(String(init.body))
|
||
);
|
||
}
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
export async function testProxyConnection(
|
||
proxyUrl: string,
|
||
testUrl?: string,
|
||
): Promise<{ ok: boolean; latency: number; info: string }> {
|
||
const target = testUrl || 'https://www.baidu.com';
|
||
const start = Date.now();
|
||
try {
|
||
const res = await proxiedFetch(target, {
|
||
signal: AbortSignal.timeout(10000),
|
||
}, proxyUrl);
|
||
const latency = Date.now() - start;
|
||
return { ok: true, latency, info: `连接成功 (${res.status})` };
|
||
} catch (err: any) {
|
||
return { ok: false, latency: Date.now() - start, info: `代理连接失败: ${err.message}` };
|
||
}
|
||
}
|
||
|
||
// Legacy compat — no longer returns dispatcher, kept for type compatibility
|
||
export function createProxyDispatcher(proxyUrl: string): { agent?: any } | null {
|
||
const agent = createProxyAgent(proxyUrl);
|
||
return agent ? { agent } : null;
|
||
}
|