-
Notifications
You must be signed in to change notification settings - Fork 248
/
index.ts
155 lines (138 loc) · 4.63 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import * as http from 'http';
import * as https from 'https';
import { URL } from 'url';
import LRUCache from 'lru-cache';
import { Agent, AgentConnectOpts } from 'agent-base';
import createDebug from 'debug';
import { getProxyForUrl as envGetProxyForUrl } from 'proxy-from-env';
import { PacProxyAgent, PacProxyAgentOptions } from 'pac-proxy-agent';
import { HttpProxyAgent, HttpProxyAgentOptions } from 'http-proxy-agent';
import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent';
import { SocksProxyAgent, SocksProxyAgentOptions } from 'socks-proxy-agent';
const debug = createDebug('proxy-agent');
const PROTOCOLS = [
...HttpProxyAgent.protocols,
...SocksProxyAgent.protocols,
...PacProxyAgent.protocols,
] as const;
type ValidProtocol = (typeof PROTOCOLS)[number];
type AgentConstructor = new (...args: never[]) => Agent;
type GetProxyForUrlCallback = (url: string) => string;
/**
* Supported proxy types.
*/
export const proxies: {
[P in ValidProtocol]: [AgentConstructor, AgentConstructor];
} = {
http: [HttpProxyAgent, HttpsProxyAgent],
https: [HttpProxyAgent, HttpsProxyAgent],
socks: [SocksProxyAgent, SocksProxyAgent],
socks4: [SocksProxyAgent, SocksProxyAgent],
socks4a: [SocksProxyAgent, SocksProxyAgent],
socks5: [SocksProxyAgent, SocksProxyAgent],
socks5h: [SocksProxyAgent, SocksProxyAgent],
'pac+data': [PacProxyAgent, PacProxyAgent],
'pac+file': [PacProxyAgent, PacProxyAgent],
'pac+ftp': [PacProxyAgent, PacProxyAgent],
'pac+http': [PacProxyAgent, PacProxyAgent],
'pac+https': [PacProxyAgent, PacProxyAgent],
};
function isValidProtocol(v: string): v is ValidProtocol {
return (PROTOCOLS as readonly string[]).includes(v);
}
export type ProxyAgentOptions = HttpProxyAgentOptions<''> &
HttpsProxyAgentOptions<''> &
SocksProxyAgentOptions &
PacProxyAgentOptions<''> & {
/**
* Default `http.Agent` instance to use when no proxy is
* configured for a request. Defaults to a new `http.Agent()`
* instance with the proxy agent options passed in.
*/
httpAgent?: http.Agent;
/**
* Default `http.Agent` instance to use when no proxy is
* configured for a request. Defaults to a new `https.Agent()`
* instance with the proxy agent options passed in.
*/
httpsAgent?: http.Agent;
/**
* A callback for dynamic provision of proxy for url.
* Defaults to standard proxy environment variables,
* see https://www.npmjs.com/package/proxy-from-env for details
*/
getProxyForUrl?: GetProxyForUrlCallback;
};
/**
* Uses the appropriate `Agent` subclass based off of the "proxy"
* environment variables that are currently set.
*
* An LRU cache is used, to prevent unnecessary creation of proxy
* `http.Agent` instances.
*/
export class ProxyAgent extends Agent {
/**
* Cache for `Agent` instances.
*/
cache = new LRUCache<string, Agent>({ max: 20 });
connectOpts?: ProxyAgentOptions;
httpAgent: http.Agent;
httpsAgent: http.Agent;
getProxyForUrl: GetProxyForUrlCallback;
constructor(opts?: ProxyAgentOptions) {
super(opts);
debug('Creating new ProxyAgent instance: %o', opts);
this.connectOpts = opts;
this.httpAgent = opts?.httpAgent || new http.Agent(opts);
this.httpsAgent =
opts?.httpsAgent || new https.Agent(opts as https.AgentOptions);
this.getProxyForUrl = opts?.getProxyForUrl || envGetProxyForUrl;
}
async connect(
req: http.ClientRequest,
opts: AgentConnectOpts
): Promise<http.Agent> {
const { secureEndpoint } = opts;
const isWebSocket = req.getHeader('upgrade') === 'websocket';
const protocol = secureEndpoint
? isWebSocket
? 'wss:'
: 'https:'
: isWebSocket
? 'ws:'
: 'http:';
const host = req.getHeader('host');
const url = new URL(req.path, `${protocol}//${host}`).href;
const proxy = this.getProxyForUrl(url);
if (!proxy) {
debug('Proxy not enabled for URL: %o', url);
return secureEndpoint ? this.httpsAgent : this.httpAgent;
}
debug('Request URL: %o', url);
debug('Proxy URL: %o', proxy);
// attempt to get a cached `http.Agent` instance first
const cacheKey = `${protocol}+${proxy}`;
let agent = this.cache.get(cacheKey);
if (!agent) {
const proxyUrl = new URL(proxy);
const proxyProto = proxyUrl.protocol.replace(':', '');
if (!isValidProtocol(proxyProto)) {
throw new Error(`Unsupported protocol for proxy URL: ${proxy}`);
}
const ctor =
proxies[proxyProto][secureEndpoint || isWebSocket ? 1 : 0];
// @ts-expect-error meh…
agent = new ctor(proxy, this.connectOpts);
this.cache.set(cacheKey, agent);
} else {
debug('Cache hit for proxy URL: %o', proxy);
}
return agent;
}
destroy(): void {
for (const agent of this.cache.values()) {
agent.destroy();
}
super.destroy();
}
}