Skip to content

Commit 4333067

Browse files
authoredMay 3, 2023
Add keepAlive: true support (#147)
Enables `keepAlive: true` option for all proxy agent classes. When set, the `Proxy-Connection: Keep-Alive` HTTP header will be sent to HTTP proxies.
1 parent a03e785 commit 4333067

File tree

15 files changed

+529
-312
lines changed

15 files changed

+529
-312
lines changed
 

‎.changeset/tidy-deers-glow.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'https-proxy-agent': minor
3+
'socks-proxy-agent': minor
4+
'http-proxy-agent': minor
5+
'pac-proxy-agent': minor
6+
'proxy-agent': minor
7+
'agent-base': minor
8+
---
9+
10+
Add support for core `keepAlive: true`

‎packages/agent-base/src/index.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ export abstract class Agent extends http.Agent {
3333
_protocol?: string;
3434
_currentSocket?: Duplex;
3535

36-
constructor() {
37-
super();
36+
// Set by `http.Agent` - missing from `@types/node`
37+
options!: Partial<net.TcpNetConnectOpts & tls.ConnectionOptions>;
38+
keepAlive!: boolean;
39+
40+
constructor(opts?: http.AgentOptions) {
41+
super(opts);
3842
this._defaultPort = undefined;
3943
this._protocol = undefined;
4044
}
@@ -57,8 +61,8 @@ export abstract class Agent extends http.Agent {
5761
.then(() => this.connect(req, o))
5862
.then((socket) => {
5963
if (socket instanceof http.Agent) {
60-
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
61-
return socket.createSocket(req, o, cb);
64+
// @ts-expect-error `addRequest()` isn't defined in `@types/node`
65+
return socket.addRequest(req, o);
6266
}
6367
this._currentSocket = socket;
6468
// @ts-expect-error `createSocket()` isn't defined in `@types/node`
@@ -77,7 +81,7 @@ export abstract class Agent extends http.Agent {
7781
if (typeof this._defaultPort === 'number') {
7882
return this._defaultPort;
7983
}
80-
const port = isSecureEndpoint() ? 443 : 80;
84+
const port = this.protocol === 'https:' ? 443 : 80;
8185
return port;
8286
}
8387

‎packages/agent-base/test/test.ts

+135-42
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,20 @@
1+
import assert from 'assert';
12
import * as fs from 'fs';
23
import * as net from 'net';
34
import * as tls from 'tls';
4-
import * as url from 'url';
55
import * as http from 'http';
66
import * as https from 'https';
7-
import assert from 'assert';
8-
import listen from 'async-listen';
9-
import { Agent, AgentConnectOpts } from '../src';
7+
import { once } from 'events';
8+
import { listen } from 'async-listen';
9+
import { Agent, AgentConnectOpts, req, json } from '../src';
1010

1111
const sleep = (n: number) => new Promise((r) => setTimeout(r, n));
1212

13-
const req = (opts: https.RequestOptions): Promise<http.IncomingMessage> =>
14-
new Promise((resolve, reject) => {
15-
(opts.protocol === 'https:' ? https : http)
16-
.request(opts, resolve)
17-
.once('error', reject)
18-
.end();
19-
});
20-
2113
const sslOptions = {
2214
key: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.key`),
2315
cert: fs.readFileSync(`${__dirname}/ssl-cert-snakeoil.pem`),
2416
};
2517

26-
function json(res: http.IncomingMessage): Promise<Record<string, string>> {
27-
return new Promise((resolve) => {
28-
let data = '';
29-
res.setEncoding('utf8');
30-
res.on('data', (b) => {
31-
data += b;
32-
});
33-
res.on('end', () => resolve(JSON.parse(data)));
34-
});
35-
}
36-
3718
describe('Agent (TypeScript)', () => {
3819
describe('subclass', () => {
3920
it('should be extendable (direct return)', () => {
@@ -91,8 +72,9 @@ describe('Agent (TypeScript)', () => {
9172
const { port } = addr;
9273

9374
try {
94-
const info = url.parse(`http://127.0.0.1:${port}/foo`);
95-
const res = await req({ agent, ...info });
75+
const res = await req(`http://127.0.0.1:${port}/foo`, {
76+
agent,
77+
});
9678
assert.equal('bar', res.headers['x-foo']);
9779
assert.equal('/foo', res.headers['x-url']);
9880
assert(gotReq);
@@ -130,8 +112,9 @@ describe('Agent (TypeScript)', () => {
130112
agent.defaultPort = port;
131113

132114
try {
133-
const info = url.parse(`http://127.0.0.1:${port}/foo`);
134-
const res = await req({ agent, ...info });
115+
const res = await req(`http://127.0.0.1:${port}/foo`, {
116+
agent,
117+
});
135118
const body = await json(res);
136119
assert.equal(body.host, '127.0.0.1');
137120
} finally {
@@ -172,8 +155,9 @@ describe('Agent (TypeScript)', () => {
172155
const { port } = addr;
173156

174157
try {
175-
const info = url.parse(`http://127.0.0.1:${port}/foo`);
176-
const res = await req({ agent, ...info });
158+
const res = await req(`http://127.0.0.1:${port}/foo`, {
159+
agent,
160+
});
177161
assert.equal('bar', res.headers['x-foo']);
178162
assert.equal('/foo', res.headers['x-url']);
179163
assert(gotReq);
@@ -197,8 +181,7 @@ describe('Agent (TypeScript)', () => {
197181
const agent = new MyAgent();
198182

199183
try {
200-
const info = url.parse('http://127.0.0.1/throws');
201-
await req({ agent, ...info });
184+
await req('http://127.0.0.1/throws', { agent });
202185
} catch (err: unknown) {
203186
gotError = true;
204187
assert.equal((err as Error).message, 'bad');
@@ -223,8 +206,7 @@ describe('Agent (TypeScript)', () => {
223206
const agent = new MyAgent();
224207

225208
try {
226-
const info = url.parse('http://127.0.0.1/throws');
227-
await req({ agent, ...info });
209+
await req('http://127.0.0.1/throws', { agent });
228210
} catch (err: unknown) {
229211
gotError = true;
230212
assert.equal((err as Error).message, 'bad');
@@ -233,6 +215,72 @@ describe('Agent (TypeScript)', () => {
233215
assert(gotError);
234216
assert(gotCallback);
235217
});
218+
219+
it('should support `keepAlive: true`', async () => {
220+
let reqCount1 = 0;
221+
let reqCount2 = 0;
222+
let connectCount = 0;
223+
224+
class MyAgent extends Agent {
225+
async connect(
226+
_req: http.ClientRequest,
227+
opts: AgentConnectOpts
228+
) {
229+
connectCount++;
230+
assert(opts.secureEndpoint === false);
231+
return net.connect(opts);
232+
}
233+
}
234+
const agent = new MyAgent({ keepAlive: true });
235+
236+
const server1 = http.createServer((req, res) => {
237+
expect(req.headers.connection).toEqual('keep-alive');
238+
reqCount1++;
239+
res.end();
240+
});
241+
const addr1 = (await listen(server1)) as URL;
242+
243+
const server2 = http.createServer((req, res) => {
244+
expect(req.headers.connection).toEqual('keep-alive');
245+
reqCount2++;
246+
res.end();
247+
});
248+
const addr2 = (await listen(server2)) as URL;
249+
250+
try {
251+
const res = await req(new URL('/foo', addr1), { agent });
252+
expect(reqCount1).toEqual(1);
253+
expect(connectCount).toEqual(1);
254+
expect(res.headers.connection).toEqual('keep-alive');
255+
res.resume();
256+
const s1 = res.socket;
257+
258+
await once(s1, 'free');
259+
260+
const res2 = await req(new URL('/another', addr1), { agent });
261+
expect(reqCount1).toEqual(2);
262+
expect(connectCount).toEqual(1);
263+
expect(res2.headers.connection).toEqual('keep-alive');
264+
assert(res2.socket === s1);
265+
266+
res2.resume();
267+
await once(res2.socket, 'free');
268+
269+
// This is a different host, so a new socket should be used
270+
const res3 = await req(new URL('/another', addr2), { agent });
271+
expect(reqCount2).toEqual(1);
272+
expect(connectCount).toEqual(2);
273+
expect(res3.headers.connection).toEqual('keep-alive');
274+
assert(res3.socket !== s1);
275+
276+
res3.resume();
277+
await once(res3.socket, 'free');
278+
} finally {
279+
agent.destroy();
280+
server1.close();
281+
server2.close();
282+
}
283+
});
236284
});
237285

238286
describe('"https" module', () => {
@@ -268,11 +316,9 @@ describe('Agent (TypeScript)', () => {
268316
const { port } = addr;
269317

270318
try {
271-
const info = url.parse(`https://127.0.0.1:${port}/foo`);
272-
const res = await req({
319+
const res = await req(`https://127.0.0.1:${port}/foo`, {
273320
agent,
274321
rejectUnauthorized: false,
275-
...info,
276322
});
277323
assert.equal('bar', res.headers['x-foo']);
278324
assert.equal('/foo', res.headers['x-url']);
@@ -328,11 +374,9 @@ describe('Agent (TypeScript)', () => {
328374
const { port } = addr;
329375

330376
try {
331-
const info = url.parse(`https://127.0.0.1:${port}/foo`);
332-
const res = await req({
377+
const res = await req(`https://127.0.0.1:${port}/foo`, {
333378
agent: agent1,
334379
rejectUnauthorized: false,
335-
...info,
336380
});
337381
assert.equal('bar', res.headers['x-foo']);
338382
assert.equal('/foo', res.headers['x-url']);
@@ -376,11 +420,9 @@ describe('Agent (TypeScript)', () => {
376420
agent.defaultPort = port;
377421

378422
try {
379-
const info = url.parse(`https://127.0.0.1:${port}/foo`);
380-
const res = await req({
423+
const res = await req(`https://127.0.0.1:${port}/foo`, {
381424
agent,
382425
rejectUnauthorized: false,
383-
...info,
384426
});
385427
const body = await json(res);
386428
assert.equal(body.host, '127.0.0.1');
@@ -389,5 +431,56 @@ describe('Agent (TypeScript)', () => {
389431
server.close();
390432
}
391433
});
434+
435+
it('should support `keepAlive: true`', async () => {
436+
let gotReq = false;
437+
let connectCount = 0;
438+
439+
class MyAgent extends Agent {
440+
async connect(
441+
_req: http.ClientRequest,
442+
opts: AgentConnectOpts
443+
) {
444+
connectCount++;
445+
assert(opts.secureEndpoint === true);
446+
return tls.connect(opts);
447+
}
448+
}
449+
const agent = new MyAgent({ keepAlive: true });
450+
451+
const server = https.createServer(sslOptions, (req, res) => {
452+
gotReq = true;
453+
res.end();
454+
});
455+
const addr = (await listen(server)) as URL;
456+
457+
try {
458+
const res = await req(new URL('/foo', addr), {
459+
agent,
460+
rejectUnauthorized: false,
461+
});
462+
assert(gotReq);
463+
expect(connectCount).toEqual(1);
464+
expect(res.headers.connection).toEqual('keep-alive');
465+
res.resume();
466+
const s1 = res.socket;
467+
468+
await once(s1, 'free');
469+
470+
const res2 = await req(new URL('/another', addr), {
471+
agent,
472+
rejectUnauthorized: false,
473+
});
474+
expect(connectCount).toEqual(1);
475+
expect(res2.headers.connection).toEqual('keep-alive');
476+
assert(res2.socket === s1);
477+
478+
res2.resume();
479+
await once(res2.socket, 'free');
480+
} finally {
481+
agent.destroy();
482+
server.close();
483+
}
484+
});
392485
});
393486
});

‎packages/http-proxy-agent/src/index.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ type ConnectOptsMap = {
1515
https: Omit<tls.ConnectionOptions, 'host' | 'port'>;
1616
};
1717

18-
export type HttpProxyAgentOptions<T> = {
18+
type ConnectOpts<T> = {
1919
[P in keyof ConnectOptsMap]: Protocol<T> extends P
2020
? ConnectOptsMap[P]
2121
: never;
2222
}[keyof ConnectOptsMap];
2323

24+
export type HttpProxyAgentOptions<T> = ConnectOpts<T> & http.AgentOptions;
25+
2426
interface HttpProxyAgentClientRequest extends http.ClientRequest {
2527
outputData?: {
2628
data: string;
@@ -48,7 +50,7 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
4850
}
4951

5052
constructor(proxy: Uri | URL, opts?: HttpProxyAgentOptions<Uri>) {
51-
super();
53+
super(opts);
5254
this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy;
5355
debug('Creating new HttpProxyAgent instance: %o', this.proxy.href);
5456

@@ -99,6 +101,13 @@ export class HttpProxyAgent<Uri extends string> extends Agent {
99101
);
100102
}
101103

104+
if (!req.hasHeader('proxy-connection')) {
105+
req.setHeader(
106+
'Proxy-Connection',
107+
this.keepAlive ? 'Keep-Alive' : 'close'
108+
);
109+
}
110+
102111
// Create a socket connection to the proxy server.
103112
let socket: net.Socket;
104113
if (this.secureProxy) {

‎packages/https-proxy-agent/src/index.ts

+15-24
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ type ConnectOpts<T> = {
2323
: never;
2424
}[keyof ConnectOptsMap];
2525

26-
export type HttpsProxyAgentOptions<T> = ConnectOpts<T> & {
27-
headers?: OutgoingHttpHeaders;
28-
};
26+
export type HttpsProxyAgentOptions<T> = ConnectOpts<T> &
27+
http.AgentOptions & {
28+
headers?: OutgoingHttpHeaders;
29+
};
2930

3031
/**
3132
* The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to
@@ -51,7 +52,8 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
5152
}
5253

5354
constructor(proxy: Uri | URL, opts?: HttpsProxyAgentOptions<Uri>) {
54-
super();
55+
super(opts);
56+
this.options = { path: undefined };
5557
this.proxy = typeof proxy === 'string' ? new URL(proxy) : proxy;
5658
this.proxyHeaders = opts?.headers ?? {};
5759
debug('Creating new HttpsProxyAgent instance: %o', this.proxy.href);
@@ -100,7 +102,7 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
100102
}
101103

102104
const headers: OutgoingHttpHeaders = { ...this.proxyHeaders };
103-
let host = net.isIPv6(opts.host) ? `[${opts.host}]` : opts.host;
105+
const host = net.isIPv6(opts.host) ? `[${opts.host}]` : opts.host;
104106
let payload = `CONNECT ${host}:${opts.port} HTTP/1.1\r\n`;
105107

106108
// Inject the `Proxy-Authorization` header if necessary.
@@ -113,15 +115,13 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
113115
).toString('base64')}`;
114116
}
115117

116-
// The `Host` header should only include the port
117-
// number when it is not the default port.
118-
const { port, secureEndpoint } = opts;
119-
if (!isDefaultPort(port, secureEndpoint)) {
120-
host += `:${port}`;
121-
}
122-
headers.Host = host;
118+
headers.Host = `${host}:${opts.port}`;
123119

124-
headers.Connection = 'close';
120+
if (!headers['Proxy-Connection']) {
121+
headers['Proxy-Connection'] = this.keepAlive
122+
? 'Keep-Alive'
123+
: 'close';
124+
}
125125
for (const name of Object.keys(headers)) {
126126
payload += `${name}: ${headers[name]}\r\n`;
127127
}
@@ -140,16 +140,11 @@ export class HttpsProxyAgent<Uri extends string> extends Agent {
140140
// this socket connection to a TLS connection.
141141
debug('Upgrading socket connection to TLS');
142142
const servername = opts.servername || opts.host;
143-
const s = tls.connect({
143+
return tls.connect({
144144
...omit(opts, 'host', 'path', 'port'),
145145
socket,
146-
servername,
146+
servername: net.isIP(servername) ? undefined : servername,
147147
});
148-
//console.log(s);
149-
150-
//s.write('GET /foo HTTP/1.1\r\n\r\n');
151-
//await new Promise(r => setTimeout(r, 5000));
152-
return s;
153148
}
154149

155150
return socket;
@@ -191,10 +186,6 @@ function resume(socket: net.Socket | tls.TLSSocket): void {
191186
socket.resume();
192187
}
193188

194-
function isDefaultPort(port: number, secure: boolean): boolean {
195-
return Boolean((!secure && port === 80) || (secure && port === 443));
196-
}
197-
198189
function isHTTPS(protocol?: string | null): boolean {
199190
return typeof protocol === 'string' ? /^https:?$/i.test(protocol) : false;
200191
}

‎packages/https-proxy-agent/test/test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ describe('HttpsProxyAgent', () => {
5353
sslProxyUrl = (await listen(sslProxy)) as URL;
5454
});
5555

56+
beforeEach(() => {
57+
server.removeAllListeners('request');
58+
sslServer.removeAllListeners('request');
59+
});
60+
5661
// shut down the test HTTP servers
5762
afterAll(() => {
5863
server.close();
@@ -189,6 +194,31 @@ describe('HttpsProxyAgent', () => {
189194
assert.equal('bar', req.headers.foo);
190195
socket.destroy();
191196
});
197+
198+
it('should work with `keepAlive: true`', async () => {
199+
server.on('request', (req, res) => {
200+
res.end(JSON.stringify(req.headers));
201+
});
202+
203+
const agent = new HttpsProxyAgent(proxyUrl, { keepAlive: true });
204+
205+
try {
206+
const res = await req(serverUrl, { agent });
207+
expect(res.headers.connection).toEqual('keep-alive');
208+
res.resume();
209+
const s1 = res.socket;
210+
await once(s1, 'free');
211+
212+
const res2 = await req(serverUrl, { agent });
213+
expect(res2.headers.connection).toEqual('keep-alive');
214+
res2.resume();
215+
const s2 = res2.socket;
216+
assert(s1 === s2);
217+
await once(s2, 'free');
218+
} finally {
219+
agent.destroy();
220+
}
221+
});
192222
});
193223

194224
describe('"https" module', () => {
@@ -275,5 +305,33 @@ describe('HttpsProxyAgent', () => {
275305
assert.equal(sslServerUrl.hostname, body.host);
276306
}
277307
);
308+
309+
it('should work with `keepAlive: true`', async () => {
310+
sslServer.on('request', (req, res) => {
311+
res.end(JSON.stringify(req.headers));
312+
});
313+
314+
const agent = new HttpsProxyAgent(sslProxyUrl, {
315+
keepAlive: true,
316+
rejectUnauthorized: false,
317+
});
318+
319+
try {
320+
const res = await req(sslServerUrl, { agent, rejectUnauthorized: false });
321+
expect(res.headers.connection).toEqual('keep-alive');
322+
res.resume();
323+
const s1 = res.socket;
324+
await once(s1, 'free');
325+
326+
const res2 = await req(sslServerUrl, { agent, rejectUnauthorized: false });
327+
expect(res2.headers.connection).toEqual('keep-alive');
328+
res2.resume();
329+
const s2 = res2.socket;
330+
assert(s1 === s2);
331+
await once(s2, 'free');
332+
} finally {
333+
agent.destroy();
334+
}
335+
});
278336
});
279337
});

‎packages/pac-proxy-agent/src/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ type Protocol<T> = T extends `pac+${infer P}:${infer _}`
3333
? P
3434
: never;
3535

36-
export type PacProxyAgentOptions<T> = PacResolverOptions &
36+
export type PacProxyAgentOptions<T> = http.AgentOptions &
37+
PacResolverOptions &
3738
GetUriOptions<`${Protocol<T>}:`> &
3839
HttpProxyAgentOptions<''> &
3940
HttpsProxyAgentOptions<''> &
@@ -70,7 +71,7 @@ export class PacProxyAgent<Uri extends string> extends Agent {
7071
resolverPromise?: Promise<FindProxyForURL>;
7172

7273
constructor(uri: Uri | URL, opts?: PacProxyAgentOptions<Uri>) {
73-
super();
74+
super(opts);
7475

7576
// Strip the "pac+" prefix
7677
const uriStr = typeof uri === 'string' ? uri : uri.href;

‎packages/proxy-agent/src/index.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ export class ProxyAgent extends Agent {
6464
connectOpts?: ProxyAgentOptions;
6565

6666
constructor(opts?: ProxyAgentOptions) {
67-
super();
68-
debug('Creating new ProxyAgent instance');
67+
super(opts);
68+
debug('Creating new ProxyAgent instance: %o', opts);
6969
this.connectOpts = opts;
7070
}
7171

@@ -76,15 +76,14 @@ export class ProxyAgent extends Agent {
7676
const protocol = opts.secureEndpoint ? 'https:' : 'http:';
7777
const host = req.getHeader('host');
7878
const url = new URL(req.path, `${protocol}//${host}`).href;
79-
debug('Request URL: %o', url);
80-
8179
const proxy = getProxyForUrl(url);
8280

8381
if (!proxy) {
8482
debug('Proxy not enabled for URL: %o', url);
8583
return opts.secureEndpoint ? https.globalAgent : http.globalAgent;
8684
}
8785

86+
debug('Request URL: %o', url);
8887
debug('Proxy URL: %o', proxy);
8988

9089
// attempt to get a cached `http.Agent` instance first
@@ -105,4 +104,11 @@ export class ProxyAgent extends Agent {
105104

106105
return agent;
107106
}
107+
108+
destroy(): void {
109+
for (const agent of this.cache.values()) {
110+
agent.destroy();
111+
}
112+
super.destroy();
113+
}
108114
}

‎packages/proxy-agent/test/test.ts

+37
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ProxyServer, createProxy } from 'proxy';
88
import socks from 'socksv5';
99
import { listen } from 'async-listen';
1010
import { ProxyAgent } from '../src';
11+
import { once } from 'events';
1112

1213
const sslOptions = {
1314
key: fs.readFileSync(__dirname + '/ssl-cert-snakeoil.key'),
@@ -77,6 +78,8 @@ describe('ProxyAgent', () => {
7778
delete process.env.HTTP_PROXY;
7879
delete process.env.HTTPS_PROXY;
7980
delete process.env.NO_PROXY;
81+
httpServer.removeAllListeners('request');
82+
httpsServer.removeAllListeners('request');
8083
});
8184

8285
describe('"http" module', () => {
@@ -135,6 +138,40 @@ describe('ProxyAgent', () => {
135138
const body = await json(res);
136139
assert.equal(httpServerUrl.host, body.host);
137140
});
141+
142+
it('should work with `keepAlive: true`', async () => {
143+
httpServer.on('request', function (req, res) {
144+
res.end(JSON.stringify(req.headers));
145+
});
146+
147+
process.env.HTTP_PROXY = httpsProxyServerUrl.href;
148+
const agent = new ProxyAgent({
149+
keepAlive: true,
150+
rejectUnauthorized: false,
151+
});
152+
153+
try {
154+
const res = await req(new URL('/test', httpServerUrl), {
155+
agent,
156+
});
157+
res.resume();
158+
expect(res.headers.connection).toEqual('keep-alive');
159+
const s1 = res.socket;
160+
await once(s1, 'free');
161+
162+
const res2 = await req(new URL('/test', httpServerUrl), {
163+
agent,
164+
});
165+
res2.resume();
166+
expect(res2.headers.connection).toEqual('keep-alive');
167+
const s2 = res2.socket;
168+
assert(s1 === s2);
169+
170+
await once(s2, 'free');
171+
} finally {
172+
agent.destroy();
173+
}
174+
});
138175
});
139176

140177
describe('"https" module', () => {

‎packages/proxy/package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
],
1010
"scripts": {
1111
"build": "tsc",
12-
"test": "mocha --reporter spec",
12+
"test": "jest --env node --verbose --bail",
1313
"lint": "eslint . --ext .ts",
1414
"pack": "node ../../scripts/pack.mjs",
1515
"prepublishOnly": "npm run build"
@@ -42,9 +42,11 @@
4242
"devDependencies": {
4343
"@types/args": "^5.0.0",
4444
"@types/debug": "^4.1.7",
45-
"@types/mocha": "^5.2.7",
45+
"@types/jest": "^29.5.1",
4646
"@types/node": "^14.18.43",
47-
"mocha": "6",
47+
"async-listen": "^2.1.0",
48+
"jest": "^29.5.0",
49+
"ts-jest": "^29.1.0",
4850
"tsconfig": "workspace:*",
4951
"typescript": "^5.0.4"
5052
},

‎packages/proxy/test/test.js

-196
This file was deleted.

‎packages/proxy/test/test.ts

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import * as net from 'net';
2+
import * as http from 'http';
3+
import assert from 'assert';
4+
import { listen } from 'async-listen';
5+
import { createProxy, ProxyServer } from '../src/proxy';
6+
import { once } from 'events';
7+
8+
describe('proxy', () => {
9+
let proxy: ProxyServer;
10+
let proxyUrl: URL;
11+
12+
let server: http.Server;
13+
let serverUrl: URL;
14+
15+
beforeAll(async () => {
16+
// setup proxy server
17+
proxy = createProxy(http.createServer());
18+
proxyUrl = (await listen(proxy)) as URL;
19+
});
20+
21+
beforeAll(async () => {
22+
// setup target server
23+
server = http.createServer();
24+
serverUrl = (await listen(server)) as URL;
25+
});
26+
27+
afterAll(() => {
28+
proxy.close();
29+
server.close();
30+
});
31+
32+
beforeEach(() => {
33+
server.removeAllListeners('request');
34+
});
35+
36+
it('should proxy HTTP GET requests (keep-alive)', async () => {
37+
let requestCount = 0;
38+
39+
server.on('request', (req, res) => {
40+
requestCount++;
41+
// ensure headers are being proxied
42+
expect(req.headers['user-agent']).toEqual('curl/7.30.0');
43+
expect(req.headers.host).toEqual(serverUrl.host);
44+
expect(req.headers.accept).toEqual('*/*');
45+
res.end();
46+
});
47+
48+
const socket = net.connect({ port: +proxyUrl.port });
49+
await once(socket, 'connect');
50+
socket.write(
51+
'GET http://' +
52+
serverUrl.host +
53+
'/ HTTP/1.1\r\n' +
54+
'User-Agent: curl/7.30.0\r\n' +
55+
'Host: ' +
56+
serverUrl.host +
57+
'\r\n' +
58+
'Accept: */*\r\n' +
59+
'Proxy-Connection: Keep-Alive\r\n' +
60+
'\r\n'
61+
);
62+
63+
socket.setEncoding('utf8');
64+
const [data] = await once(socket, 'data');
65+
assert(0 == data.indexOf('HTTP/1.1 200 OK\r\n'));
66+
assert(requestCount);
67+
68+
socket.write(
69+
'GET http://' +
70+
serverUrl.host +
71+
'/ HTTP/1.1\r\n' +
72+
'User-Agent: curl/7.30.0\r\n' +
73+
'Host: ' +
74+
serverUrl.host +
75+
'\r\n' +
76+
'Accept: */*\r\n' +
77+
'Proxy-Connection: Keep-Alive\r\n' +
78+
'\r\n'
79+
);
80+
const [data2] = await once(socket, 'data');
81+
assert(0 == data2.indexOf('HTTP/1.1 200 OK\r\n'));
82+
83+
socket.destroy();
84+
});
85+
86+
it('should establish connection for CONNECT requests (keep-alive)', async () => {
87+
let requestCount = 0;
88+
89+
server.on('request', (req, res) => {
90+
requestCount++;
91+
// ensure headers are being proxied
92+
expect(req.headers.host).toEqual(serverUrl.host);
93+
expect(req.headers.foo).toEqual('bar');
94+
res.end();
95+
});
96+
97+
const socket = net.connect({ port: +proxyUrl.port });
98+
await once(socket, 'connect');
99+
100+
socket.write(
101+
'CONNECT ' +
102+
serverUrl.host +
103+
' HTTP/1.1\r\n' +
104+
'Host: ' +
105+
serverUrl.host +
106+
'\r\n' +
107+
'User-Agent: curl/7.30.0\r\n' +
108+
'Proxy-Connection: Keep-Alive\r\n' +
109+
'\r\n'
110+
);
111+
112+
socket.setEncoding('utf8');
113+
const [data] = await once(socket, 'data');
114+
assert(0 == data.indexOf('HTTP/1.1 200 Connection established\r\n'));
115+
expect(requestCount).toEqual(0);
116+
117+
socket.write(
118+
'GET / HTTP/1.1\r\n' +
119+
'Host: ' +
120+
serverUrl.host +
121+
'\r\n' +
122+
'Connection: Keep-Alive\r\n' +
123+
'Foo: bar\r\n' +
124+
'\r\n'
125+
);
126+
127+
const [data2] = await once(socket, 'data');
128+
expect(data2.includes('Connection: keep-alive')).toEqual(true);
129+
expect(requestCount).toEqual(1);
130+
131+
socket.write(
132+
'GET / HTTP/1.1\r\n' +
133+
'Host: ' +
134+
serverUrl.host +
135+
'\r\n' +
136+
'Connection: Keep-Alive\r\n' +
137+
'Foo: bar\r\n' +
138+
'\r\n'
139+
);
140+
141+
const [data3] = await once(socket, 'data');
142+
expect(data3.includes('Connection: keep-alive')).toEqual(true);
143+
expect(requestCount).toEqual(2);
144+
145+
socket.destroy();
146+
});
147+
148+
describe('authentication', () => {
149+
beforeAll(() => {
150+
delete proxy.authenticate;
151+
});
152+
153+
it('should invoke the `server.authenticate()` function when set', async () => {
154+
const auth = 'Basic Zm9vOmJhcg==';
155+
156+
const authPromise = new Promise<void>((resolve) => {
157+
proxy.authenticate = (req) => {
158+
assert(auth === req.headers['proxy-authorization']);
159+
socket.destroy();
160+
resolve();
161+
return true;
162+
};
163+
});
164+
165+
const socket = net.connect({ port: +proxyUrl.port });
166+
await once(socket, 'connect');
167+
socket.write(
168+
'GET / HTTP/1.1\r\n' +
169+
'Host: foo.com\r\n' +
170+
'Proxy-Authorization: ' +
171+
auth +
172+
'\r\n' +
173+
'\r\n'
174+
);
175+
176+
await authPromise;
177+
});
178+
179+
it('should provide the HTTP client with a 407 response status code', async () => {
180+
// reject everything
181+
proxy.authenticate = () => false;
182+
183+
const socket = net.connect({ port: +proxyUrl.port });
184+
await once(socket, 'connect');
185+
186+
socket.write('GET / HTTP/1.1\r\nHost: foo.com\r\n\r\n');
187+
188+
socket.setEncoding('utf8');
189+
const [data] = await once(socket, 'data');
190+
assert(0 == data.indexOf('HTTP/1.1 407'));
191+
socket.destroy();
192+
});
193+
194+
it("should close the socket after a CONNECT request's 407 response status code", async () => {
195+
// reject everything
196+
proxy.authenticate = () => false;
197+
198+
const socket = net.connect({ port: +proxyUrl.port });
199+
await once(socket, 'connect');
200+
socket.write('CONNECT 127.0.0.1:80 HTTP/1.1\r\n\r\n');
201+
socket.setEncoding('utf8');
202+
const [data] = await once(socket, 'data');
203+
assert(0 == data.indexOf('HTTP/1.1 407'));
204+
});
205+
});
206+
});

‎packages/proxy/test/tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"include": ["test.ts"]
4+
}

‎packages/socks-proxy-agent/src/index.ts

+14-24
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@ import * as net from 'net';
66
import * as tls from 'tls';
77
import * as http from 'http';
88

9-
interface BaseSocksProxyAgentOptions {
10-
tls?: tls.ConnectionOptions | null;
11-
}
12-
13-
interface SocksProxyAgentOptionsExtra {
14-
timeout?: number;
15-
}
16-
179
const debug = createDebug('socks-proxy-agent');
1810

1911
function parseSocksURL(url: URL): { lookup: boolean; proxy: SocksProxy } {
@@ -78,9 +70,12 @@ function parseSocksURL(url: URL): { lookup: boolean; proxy: SocksProxy } {
7870
return { lookup, proxy };
7971
}
8072

81-
export interface SocksProxyAgentOptions
82-
extends BaseSocksProxyAgentOptions,
83-
Partial<Omit<SocksProxy, keyof BaseSocksProxyAgentOptions>> {}
73+
export type SocksProxyAgentOptions = Omit<
74+
SocksProxy,
75+
// These come from the parsed URL
76+
'ipaddress' | 'host' | 'port' | 'type' | 'userId' | 'password'
77+
> &
78+
http.AgentOptions;
8479

8580
export class SocksProxyAgent extends Agent {
8681
static protocols = [
@@ -93,19 +88,16 @@ export class SocksProxyAgent extends Agent {
9388

9489
private readonly shouldLookup: boolean;
9590
private readonly proxy: SocksProxy;
96-
private readonly tlsConnectionOptions: tls.ConnectionOptions;
9791
public timeout: number | null;
9892

99-
constructor(uri: string | URL, opts?: SocksProxyAgentOptionsExtra) {
100-
super();
93+
constructor(uri: string | URL, opts?: SocksProxyAgentOptions) {
94+
super(opts);
10195

10296
const url = typeof uri === 'string' ? new URL(uri) : uri;
10397
const { proxy, lookup } = parseSocksURL(url);
10498

10599
this.shouldLookup = lookup;
106100
this.proxy = proxy;
107-
//this.tlsConnectionOptions = proxyOptions.tls != null ? proxyOptions.tls : {}
108-
this.tlsConnectionOptions = {};
109101
this.timeout = opts?.timeout ?? null;
110102
}
111103

@@ -119,13 +111,13 @@ export class SocksProxyAgent extends Agent {
119111
): Promise<net.Socket> {
120112
const { shouldLookup, proxy, timeout } = this;
121113

122-
let { host } = opts;
123-
const { port, lookup: lookupFn = dns.lookup } = opts;
124-
125-
if (!host) {
114+
if (!opts.host) {
126115
throw new Error('No `host` defined!');
127116
}
128117

118+
let { host } = opts;
119+
const { port, lookup: lookupFn = dns.lookup } = opts;
120+
129121
if (shouldLookup) {
130122
// Client-side DNS resolution for "4" and "5" socks proxy versions.
131123
host = await new Promise<string>((resolve, reject) => {
@@ -170,13 +162,11 @@ export class SocksProxyAgent extends Agent {
170162
// The proxy is connecting to a TLS server, so upgrade
171163
// this socket connection to a TLS connection.
172164
debug('Upgrading socket connection to TLS');
173-
const servername = opts.servername ?? opts.host;
174-
165+
const servername = opts.servername || opts.host;
175166
const tlsSocket = tls.connect({
176167
...omit(opts, 'host', 'path', 'port'),
177168
socket,
178-
servername,
179-
...this.tlsConnectionOptions,
169+
servername: net.isIP(servername) ? undefined : servername,
180170
});
181171

182172
tlsSocket.once('error', (error) => {

‎pnpm-lock.yaml

+12-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

1 commit comments

Comments
 (1)

vercel[bot] commented on May 3, 2023

@vercel[bot]
Please sign in to comment.