Something went wrong. Try again.
Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
Something went wrong. Try again.
7.3 kB · 258 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259import * as https from 'node:https';import * as http from 'node:http';import * as net from 'node:net';
declare module 'https' { interface Agent { createConnection( opts: https.RequestOptions, callback?: (err: Error | null, socket: net.Socket | null) => void ): net.Socket | null; }}
declare module 'net' { export function _normalizeArgs( options: unknown ): asserts options is net.NetConnectOpts;}
const getHttpProxyUrl = () => process.env.HTTP_PROXY ?? process.env.http_proxy;const getHttpsProxyUrl = () => process.env.HTTPS_PROXY ?? process.env.https_proxy;const getNoProxy = () => process.env.NO_PROXY ?? process.env.no_proxy;
const createProxyPattern = (pattern: string): RegExp | null => { pattern = pattern.trim(); if (!pattern.startsWith('.')) pattern = `^${pattern}`; if (!pattern.endsWith('.') || pattern.includes(':')) pattern += '$'; pattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '[\\w.]+'); return pattern ? new RegExp(pattern, 'i') : null;};
const matchesNoProxy = (options: { host?: string | null; hostname?: string | null; port?: string | number | null; defaultPort?: string | number;}): boolean => { const NO_PROXY = getNoProxy(); if (NO_PROXY === '*' || NO_PROXY === '1' || NO_PROXY === 'true') { return true; } else if (NO_PROXY) { for (const noProxyPattern of NO_PROXY.split(',')) { const hostPattern = createProxyPattern(noProxyPattern); if (hostPattern) { const hostname = options.hostname || options.host; const origin = hostname && `${hostname}:${options.port || options.defaultPort || 80}`; if ( (hostname && hostPattern.test(hostname)) || (origin && hostPattern.test(origin)) ) { return true; } } } return false; } else { return false; }};
export const defaultAgentOpts = { keepAlive: true, keepAliveMsecs: 1000,};
let _httpAgentUrl: string | undefined;let _httpAgent: http.Agent | undefined;
export const getHttpAgent = ( options: http.RequestOptions): http.RequestOptions['agent'] => { const HTTP_PROXY = getHttpProxyUrl(); if (!HTTP_PROXY) { _httpAgent = undefined; return undefined; } else if (matchesNoProxy(options)) { return undefined; } else if (!_httpAgentUrl || _httpAgentUrl !== HTTP_PROXY) { _httpAgent = undefined; try { _httpAgentUrl = HTTP_PROXY; _httpAgent = new HttpProxyAgent(new URL(HTTP_PROXY), defaultAgentOpts); } catch (error: any) { const wrapped = new Error( `Invalid HTTP_PROXY URL: "${HTTP_PROXY}".\n` + error?.message || error ); (wrapped as any).cause = error; throw wrapped; } return _httpAgent; } else { return _httpAgent; }};
let _httpsAgentUrl: string | undefined;let _httpsAgent: https.Agent | undefined;
export const getHttpsAgent = ( options: https.RequestOptions): https.RequestOptions['agent'] => { const HTTPS_PROXY = getHttpsProxyUrl() ?? getHttpProxyUrl(); if (!HTTPS_PROXY) { _httpsAgent = undefined; return undefined; } else if (matchesNoProxy(options)) { return undefined; } else if (!_httpsAgentUrl || _httpsAgentUrl !== HTTPS_PROXY) { _httpsAgent = undefined; try { _httpsAgentUrl = HTTPS_PROXY; _httpsAgent = new HttpsProxyAgent(new URL(HTTPS_PROXY), defaultAgentOpts); } catch (error: any) { const wrapped = new Error( `Invalid HTTPS_PROXY URL: "${HTTPS_PROXY}".\n` + error?.message || error ); (wrapped as any).cause = error; throw wrapped; } return _httpsAgent; } else { return _httpsAgent; }};
const createRequestOptions = ( proxy: URL, keepAlive: boolean, options: http.RequestOptions) => { const proxyHeaders: Record<string, string> = { host: `${options.host}:${options.port}`, connection: keepAlive ? 'keep-alive' : 'close', }; if (proxy.username || proxy.password) { const username = decodeURIComponent(proxy.username || ''); const password = decodeURIComponent(proxy.password || ''); const auth = Buffer.from(`${username}:${password}`).toString('base64'); proxyHeaders['proxy-authorization'] = `Basic ${auth}`; } return { method: 'CONNECT', host: proxy.hostname, port: proxy.port, path: `${options.host}:${options.port}`, setHost: false, agent: false, proxyEnv: {}, timeout: 5_000, headers: proxyHeaders, servername: proxy.protocol === 'https:' ? proxy.hostname : undefined, };};
// See: https://github.com/delvedor/hpagent// `hpagent` served as a template for how to create proxy agents like below minimally// MIT License, Copyright (c) 2020 Tomas Della Vedova
class HttpProxyAgent extends http.Agent { _keepAlive: boolean; _proxy: URL;
constructor(proxy: URL, options: http.AgentOptions) { super(options); this._proxy = proxy; this._keepAlive = !!options.keepAlive; }
createConnection( options: http.RequestOptions, callback: (err: Error | null, socket: net.Socket | null) => void ): void { const request = (this._proxy.protocol === 'http:' ? http : https).request( createRequestOptions(this._proxy, this._keepAlive, options) );
request.once('connect', (response, socket, _head) => { request.removeAllListeners(); socket.removeAllListeners(); if (response.statusCode === 200) { callback(null, socket); } else { socket.destroy(); callback( new Error( `HTTP Proxy Network Error: ${response.statusMessage || response.statusCode}` ), null ); } });
request.once('timeout', () => { request.destroy(new Error('HTTP Proxy timed out')); });
request.once('error', error => { request.removeAllListeners(); callback(error, null); });
request.end(); }}
class HttpsProxyAgent extends https.Agent { _proxy: URL; _keepAlive: boolean;
constructor(proxy: URL, options: https.AgentOptions) { super(options); this._proxy = proxy; this._keepAlive = !!options.keepAlive; }
createConnection( options: https.RequestOptions, callback?: (err: Error | null, socket: net.Socket | null) => void ): net.Socket | null { const request = (this._proxy.protocol === 'http:' ? http : https).request( createRequestOptions(this._proxy, this._keepAlive, options) );
request.once('connect', (response, socket, _head) => { request.removeAllListeners(); socket.removeAllListeners(); if (response.statusCode === 200) { const netOpts = { ...options, socket }; net._normalizeArgs(netOpts); const secureSocket = super.createConnection(netOpts); callback?.(null, secureSocket); } else { socket.destroy(); callback?.( new Error( `HTTP Proxy Network Error: ${response.statusMessage || response.statusCode}` ), null ); } });
request.once('timeout', () => { request.destroy(new Error('HTTP Proxy timed out')); });
request.once('error', err => { request.removeAllListeners(); callback?.(err, null); });
request.end(); return request.socket; }}