TooTallNate / proxy-agents

Node.js HTTP Proxy Agents Monorepo
https://proxy-agents.n8.io
919 stars 238 forks source link

`proxy-agent` is not working with http proxy & web sockets #176

Closed OpportunityLiu closed 1 year ago

OpportunityLiu commented 1 year ago

Since proxies will drop hop-by-hop headers (Connect and Upgrade), we cannot use http-proxy-agent to connect to a web socket. But when I use proxy-agent, it will send ws: requests via http-proxy-agent.

TooTallNate commented 1 year ago

Can you share your code?

OpportunityLiu commented 1 year ago

Here is a simple repro:

proxy.js

import * as http from 'http';
import { createProxy } from 'proxy';

const server = createProxy(http.createServer());
server.listen(3128, () => {
    var port = server.address().port;
    console.log('HTTP(s) proxy server listening on port %d', port);
});

server.js

import { createServer } from 'node:http';
import { WebSocketServer } from 'ws';

const s = createServer((req, res) => {
    console.log('request', req.url, req.headers);
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('okay');
});
s.listen(8080);
const wss = new WebSocketServer({ server: s });
wss.on('listening', () => {
    console.log('Ws server listening');
});
wss.on('connection', (ws, req) => {
    console.log('connected', req.url, req.headers);
});

client.js

import { WebSocket } from 'ws';
import { ProxyAgent } from 'proxy-agent';

const ws1 = new WebSocket('ws://localhost:8080/ws1');

process.env['http_proxy'] = 'http://localhost:3128';
process.env['https_proxy'] = 'http://localhost:3128';
process.env['no_proxy'] = '';
const agent = new ProxyAgent();
const ws2 = new WebSocket('ws://localhost:8080/ws2', { agent });

Start server.js and proxy.js, then run client.js.

In server.js, you will get:

Ws server listening
connected /ws1 {
  'sec-websocket-version': '13',
  'sec-websocket-key': 'b3x5PQrzBDcfjKK7T9ndXQ==',
  connection: 'Upgrade',
  upgrade: 'websocket',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits',
  host: 'localhost:8080'
}
request /ws2 {
  'sec-websocket-version': '13',
  'sec-websocket-key': '2dCSx8sD4wUmNCXAENRoRA==',
  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits',
  host: 'localhost:8080',
  'proxy-connection': 'close',
  'x-forwarded-for': '::ffff:127.0.0.1',
  via: '1.1 xxx (proxy/2.1.1)',
  connection: 'close'
}

indicates that connection without proxy-agent (/ws1) is handled successfully by server, while /ws2 is handled as a plain http request.

In client.js, you will get:

node:events:491
      throw er; // Unhandled 'error' event
      ^

Error: Unexpected server response: 200
    at ClientRequest.<anonymous> (...\node_modules\ws\lib\websocket.js:888:7)
    at ClientRequest.emit (node:events:513:28)
    at HTTPParser.parserOnIncomingClient (node:_http_client:693:27)
    at HTTPParser.parserOnHeadersComplete (node:_http_common:128:17)
    at Socket.socketOnData (node:_http_client:534:22)
    at Socket.emit (node:events:513:28)
    at addChunk (node:internal/streams/readable:315:12)
    at readableAddChunk (node:internal/streams/readable:289:9)
    at Socket.Readable.push (node:internal/streams/readable:228:10)
    at TCP.onStreamRead (node:internal/stream_base_commons:190:23)
Emitted 'error' event on WebSocket instance at:
    at emitErrorAndClose (...\node_modules\ws\lib\websocket.js:1008:13)
    at processTicksAndRejections (node:internal/process/task_queues:83:21)
OpportunityLiu commented 1 year ago

Currently a workaround is always to use https-proxy-agent for http proxies.

import { proxies } from 'proxy-agent';

proxies['http'][0] = proxies['http'][1];
proxies['https'][0] = proxies['https'][1];
TooTallNate commented 1 year ago

Fixed in proxy-agent@6.2.1.

ttodua commented 1 year ago

a simple example can be found here too - https://gist.github.com/ttodua/7a66e5ca28e55deebc58b0dd8e0c39a2