From d36c4b97ec5649d083ba3b9112e3469387567eb4 Mon Sep 17 00:00:00 2001 From: Gokhan Kurt Date: Mon, 31 Aug 2026 19:04:38 +0300 Subject: [PATCH] Register the connection listener explicitly, not via the constructor The two forms are equivalent in Node, where net.Server's constructor argument is registered as a 'connection' listener. Only the explicit form is portable: runtimes that handle the constructor callback internally never dispatch to it from server.emit('connection', socket), which is how a proxy re-injects a tunnelled socket after CONNECT. Adds a test covering that re-injection path, which mockttp depends on and which was previously only implicit. --- src/index.ts | 5 +++- test/reinjected-connection.spec.ts | 44 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test/reinjected-connection.spec.ts diff --git a/src/index.ts b/src/index.ts index a4a5ea3..4b3fb05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,7 +103,10 @@ class Server extends net.Server { ) { // We just act as a plain TCP server, accepting and examing // each connection, then passing it to the right subserver. - super((socket) => this.connectionListener(socket)); + // Registered explicitly rather than via super(), so that re-emitted sockets + // (server.emit('connection', socket), as proxies do after CONNECT) reach it too. + super(); + this.on('connection', (socket) => this.connectionListener(socket)); let config: HttpolyglotOptions = {}; let requestListener: http.RequestListener; diff --git a/test/reinjected-connection.spec.ts b/test/reinjected-connection.spec.ts new file mode 100644 index 0000000..7f58d66 --- /dev/null +++ b/test/reinjected-connection.spec.ts @@ -0,0 +1,44 @@ +import * as net from 'net'; +import * as http from 'http'; +import { expect } from 'chai'; + +import * as httpolyglot from '..'; +import { Deferred, getDeferred } from './test-util'; + +describe("A re-injected connection", () => { + + let server: httpolyglot.Server; + let tunnelServer: net.Server; + let serverReqRes: Deferred<[http.IncomingMessage, http.ServerResponse]>; + + beforeEach(async () => { + serverReqRes = getDeferred(); + server = httpolyglot.createServer((req, res) => serverReqRes.resolve([req, res])); + server.listen(); + + // Stands in for a proxy's CONNECT handler: it takes the raw socket itself, and + // then hands it back to the polyglot server to be sniffed and routed as usual. + tunnelServer = net.createServer((socket) => server.emit('connection', socket)); + await new Promise((resolve) => { tunnelServer.listen(() => resolve()); }); + }); + + afterEach(() => { + server.close(); + tunnelServer.close(); + }); + + it("should be sniffed and routed like any other connection", async () => { + const client = new net.Socket(); + await new Promise((resolve) => { + client.connect((tunnelServer.address() as net.AddressInfo).port, '127.0.0.1', () => resolve()); + }); + client.write('GET /re-injected HTTP/1.1\r\nHost: localhost\r\n\r\n'); + + const [req, res] = await serverReqRes; + expect(req.url).to.equal('/re-injected'); + + res.writeHead(200); + res.end(); + client.destroy(); + }); +});