Skip to content

Commit e4367ce

Browse files
authored
fix: accept pre-response data on CONNECT (#284)
1 parent 4fe3e2b commit e4367ce

File tree

4 files changed

+70
-5
lines changed

4 files changed

+70
-5
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "proxy-chain",
3-
"version": "2.0.6",
3+
"version": "2.0.7",
44
"description": "Node.js implementation of a proxy server (think Squid) with support for SSL, authentication, upstream proxy chaining, and protocol tunneling.",
55
"main": "dist/index.js",
66
"keywords": [

src/chain.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ export const chain = (
5959
}: ChainOpts,
6060
): void => {
6161
if (head && head.length > 0) {
62-
throw new Error(`Unexpected data on CONNECT: ${head.length} bytes`);
62+
// HTTP/1.1 has no defined semantics when sending payload along with CONNECT and servers can reject the request.
63+
// HTTP/2 only says that subsequent DATA frames must be transferred after HEADERS has been sent.
64+
// HTTP/3 says that all DATA frames should be transferred (implies pre-HEADERS data).
65+
//
66+
// Let's go with the HTTP/3 behavior.
67+
// There are also clients that send payload along with CONNECT to save milliseconds apparently.
68+
// Beware of upstream proxy servers that send out valid CONNECT responses with diagnostic data such as IPs!
69+
sourceSocket.unshift(head);
6370
}
6471

6572
const { proxyChainId } = sourceSocket;
@@ -118,8 +125,8 @@ export const chain = (
118125
}
119126

120127
if (clientHead.length > 0) {
121-
targetSocket.destroy(new Error(`Unexpected data on CONNECT: ${clientHead.length} bytes`));
122-
return;
128+
// See comment above
129+
targetSocket.unshift(clientHead);
123130
}
124131

125132
server.emit('tunnelConnectResponded', {

src/direct.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export const direct = (
4545
}
4646

4747
if (head.length > 0) {
48-
throw new Error(`Unexpected data on CONNECT: ${head.length} bytes`);
48+
// See comment in chain.ts
49+
sourceSocket.unshift(head);
4950
}
5051

5152
const options = {

test/server.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,6 +1331,63 @@ it('supports localAddress', async () => {
13311331
}
13321332
});
13331333

1334+
it('supports pre-response CONNECT payload', (done) => {
1335+
const plain = net.createServer((socket) => {
1336+
socket.pipe(socket);
1337+
});
1338+
1339+
plain.once('error', done);
1340+
1341+
plain.listen(0, async () => {
1342+
const server = new Server({
1343+
port: 0,
1344+
});
1345+
1346+
try {
1347+
await server.listen();
1348+
} catch (error) {
1349+
done(error);
1350+
return;
1351+
}
1352+
1353+
const socket = net.connect({
1354+
host: '127.0.0.1',
1355+
port: server.port,
1356+
});
1357+
1358+
socket.write([
1359+
`CONNECT 127.0.0.1:${plain.address().port} HTTP/1.1`,
1360+
`Host: 127.0.0.1:${plain.address().port}`,
1361+
``,
1362+
`foobar`,
1363+
].join('\r\n'));
1364+
1365+
let success = false;
1366+
1367+
socket.once('error', done);
1368+
socket.on('data', (data) => {
1369+
success = data.includes('foobar');
1370+
socket.end();
1371+
});
1372+
1373+
socket.setTimeout(1000, () => {
1374+
socket.destroy(new Error('Socket timed out'));
1375+
});
1376+
1377+
socket.once('close', () => {
1378+
plain.close(async () => {
1379+
await server.close();
1380+
1381+
if (success) {
1382+
done();
1383+
} else {
1384+
done(new Error('failure'));
1385+
}
1386+
});
1387+
});
1388+
});
1389+
});
1390+
13341391
// Run all combinations of test parameters
13351392
const useSslVariants = [
13361393
false,

0 commit comments

Comments
 (0)