chore: initial commit - CloudSearch v0.0.2
This commit is contained in:
143
packages/backend/src/utils/proxy-agent.ts
Normal file
143
packages/backend/src/utils/proxy-agent.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 统一代理工具 — 支持 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;
|
||||
}
|
||||
Reference in New Issue
Block a user