Skip to content

Commit 4a45593

Browse files
authored
[proxy-agent] Use HttpsProxyAgent for WebSocket requests (#192)
Fixes #176.
1 parent a17014f commit 4a45593

File tree

6 files changed

+165
-59
lines changed

6 files changed

+165
-59
lines changed

.changeset/fluffy-bears-admire.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'proxy-agent': patch
3+
---
4+
5+
Fix WebSocket connections over "http"/"https" proxies

packages/proxy-agent/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@
4747
"@types/jest": "^29.5.1",
4848
"@types/node": "^14.18.45",
4949
"@types/proxy-from-env": "^1.0.1",
50+
"@types/ws": "^8.5.4",
5051
"async-listen": "^3.0.0",
5152
"jest": "^29.5.0",
5253
"proxy": "workspace:*",
5354
"socksv5": "github:TooTallNate/socksv5#fix/dstSock-close-event",
5455
"ts-jest": "^29.1.0",
5556
"tsconfig": "workspace:*",
56-
"typescript": "^5.0.4"
57+
"typescript": "^5.0.4",
58+
"ws": "^8.13.0"
5759
}
5860
}

packages/proxy-agent/src/index.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@ export class ProxyAgent extends Agent {
104104
opts: AgentConnectOpts
105105
): Promise<http.Agent> {
106106
const { secureEndpoint } = opts;
107-
const protocol = secureEndpoint ? 'https:' : 'http:';
107+
const isWebSocket = req.getHeader('upgrade') === 'websocket';
108+
const protocol = secureEndpoint
109+
? isWebSocket
110+
? 'wss:'
111+
: 'https:'
112+
: isWebSocket
113+
? 'ws:'
114+
: 'http:';
108115
const host = req.getHeader('host');
109116
const url = new URL(req.path, `${protocol}//${host}`).href;
110117
const proxy = this.getProxyForUrl(url);
@@ -126,7 +133,8 @@ export class ProxyAgent extends Agent {
126133
if (!isValidProtocol(proxyProto)) {
127134
throw new Error(`Unsupported protocol for proxy URL: ${proxy}`);
128135
}
129-
const ctor = proxies[proxyProto][secureEndpoint ? 1 : 0];
136+
const ctor =
137+
proxies[proxyProto][secureEndpoint || isWebSocket ? 1 : 0];
130138
// @ts-expect-error meh…
131139
agent = new ctor(proxy, this.connectOpts);
132140
this.cache.set(cacheKey, agent);

packages/proxy-agent/test/test.ts

+62
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as http from 'http';
33
import * as https from 'https';
44
import { once } from 'events';
55
import assert from 'assert';
6+
import WebSocket, { WebSocketServer } from 'ws';
67
import { json, req } from 'agent-base';
78
import { ProxyServer, createProxy } from 'proxy';
89
// @ts-expect-error no types
@@ -20,8 +21,10 @@ const sslOptions = {
2021
describe('ProxyAgent', () => {
2122
// target servers
2223
let httpServer: http.Server;
24+
let httpWebSocketServer: WebSocketServer;
2325
let httpServerUrl: URL;
2426
let httpsServer: https.Server;
27+
let httpsWebSocketServer: WebSocketServer;
2528
let httpsServerUrl: URL;
2629

2730
// proxy servers
@@ -36,12 +39,14 @@ describe('ProxyAgent', () => {
3639
beforeAll(async () => {
3740
// setup target HTTP server
3841
httpServer = http.createServer();
42+
httpWebSocketServer = new WebSocketServer({ server: httpServer });
3943
httpServerUrl = await listen(httpServer);
4044
});
4145

4246
beforeAll(async () => {
4347
// setup target SSL HTTPS server
4448
httpsServer = https.createServer(sslOptions);
49+
httpsWebSocketServer = new WebSocketServer({ server: httpsServer });
4550
httpsServerUrl = await listen(httpsServer);
4651
});
4752

@@ -79,9 +84,13 @@ describe('ProxyAgent', () => {
7984
beforeEach(() => {
8085
delete process.env.HTTP_PROXY;
8186
delete process.env.HTTPS_PROXY;
87+
delete process.env.WS_PROXY;
88+
delete process.env.WSS_PROXY;
8289
delete process.env.NO_PROXY;
8390
httpServer.removeAllListeners('request');
8491
httpsServer.removeAllListeners('request');
92+
httpWebSocketServer.removeAllListeners('connection');
93+
httpsWebSocketServer.removeAllListeners('connection');
8594
});
8695

8796
describe('"http" module', () => {
@@ -278,4 +287,57 @@ describe('ProxyAgent', () => {
278287
assert(requestUrl.href === urlParameter);
279288
});
280289
});
290+
291+
describe('"ws" module', () => {
292+
it('should work over "http" proxy to `ws:` URL', async () => {
293+
let requestCount = 0;
294+
let connectionCount = 0;
295+
httpServer.once('request', function (req, res) {
296+
requestCount++;
297+
res.end();
298+
});
299+
httpWebSocketServer.on('connection', (ws) => {
300+
connectionCount++;
301+
ws.send('OK');
302+
});
303+
304+
process.env.WS_PROXY = httpProxyServerUrl.href;
305+
const agent = new ProxyAgent();
306+
307+
const ws = new WebSocket(httpServerUrl.href.replace('http', 'ws'), {
308+
agent,
309+
});
310+
const [message] = await once(ws, 'message');
311+
expect(connectionCount).toEqual(1);
312+
expect(requestCount).toEqual(0);
313+
expect(message.toString()).toEqual('OK');
314+
ws.close();
315+
});
316+
317+
it('should work over "http" proxy to `wss:` URL', async () => {
318+
let requestCount = 0;
319+
let connectionCount = 0;
320+
httpsServer.once('request', function (req, res) {
321+
requestCount++;
322+
res.end();
323+
});
324+
httpsWebSocketServer.on('connection', (ws) => {
325+
connectionCount++;
326+
ws.send('OK');
327+
});
328+
329+
process.env.WSS_PROXY = httpProxyServerUrl.href;
330+
const agent = new ProxyAgent();
331+
332+
const ws = new WebSocket(httpsServerUrl.href.replace('https', 'wss'), {
333+
agent,
334+
rejectUnauthorized: false
335+
});
336+
const [message] = await once(ws, 'message');
337+
expect(connectionCount).toEqual(1);
338+
expect(requestCount).toEqual(0);
339+
expect(message.toString()).toEqual('OK');
340+
ws.close();
341+
});
342+
});
281343
});

0 commit comments

Comments
 (0)