Skip to content

Commit b6f3996

Browse files
committed
fix: correct target stats when sockets are reused
1 parent 6c6bb09 commit b6f3996

File tree

4 files changed

+38
-13
lines changed

4 files changed

+38
-13
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "proxy-chain",
3-
"version": "2.5.7",
3+
"version": "2.5.8",
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

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { URL } from 'url';
77

88
import type { Socket } from './socket';
99
import { badGatewayStatusCodes, createCustomStatusHttpResponse, errorCodeToStatusCode } from './statuses';
10+
import type { SocketWithPreviousStats } from './utils/count_target_bytes';
1011
import { countTargetBytes } from './utils/count_target_bytes';
1112
import { getBasicAuthorizationHeader } from './utils/get_basic';
1213

@@ -85,9 +86,15 @@ export const chain = (
8586
const fn = proxy.protocol === 'https:' ? https.request : http.request;
8687
const client = fn(proxy.origin, options as unknown as http.ClientRequestArgs);
8788

88-
client.on('connect', (response, targetSocket, clientHead) => {
89+
client.once('socket', (targetSocket: SocketWithPreviousStats) => {
90+
// Socket can be re-used by multiple requests.
91+
// That's why we need to track the previous stats.
92+
targetSocket.previousBytesRead = targetSocket.bytesRead;
93+
targetSocket.previousBytesWritten = targetSocket.bytesWritten;
8994
countTargetBytes(sourceSocket, targetSocket);
95+
});
9096

97+
client.on('connect', (response, targetSocket, clientHead) => {
9198
if (sourceSocket.readyState !== 'open') {
9299
// Sanity check, should never reach.
93100
targetSocket.destroy();

src/forward.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { URL } from 'url';
66
import util from 'util';
77

88
import { badGatewayStatusCodes, errorCodeToStatusCode } from './statuses';
9+
import type { SocketWithPreviousStats } from './utils/count_target_bytes';
910
import { countTargetBytes } from './utils/count_target_bytes';
1011
import { getBasicAuthorizationHeader } from './utils/get_basic';
1112
import { validHeadersOnly } from './utils/valid_headers_only';
@@ -114,8 +115,12 @@ export const forward = async (
114115
}
115116
});
116117

117-
client.once('socket', (socket) => {
118-
countTargetBytes(request.socket, socket);
118+
client.once('socket', (socket: SocketWithPreviousStats) => {
119+
// Socket can be re-used by multiple requests.
120+
// That's why we need to track the previous stats.
121+
socket.previousBytesRead = socket.bytesRead;
122+
socket.previousBytesWritten = socket.bytesWritten;
123+
countTargetBytes(request.socket, socket, (handler) => response.once('close', handler));
119124
});
120125

121126
// Can't use pipeline here as it automatically destroys the streams

src/utils/count_target_bytes.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,27 @@ const calculateTargetStats = Symbol('calculateTargetStats');
77

88
type Stats = { bytesWritten: number | null, bytesRead: number | null };
99

10+
/**
11+
* Socket object extended with previous read and written bytes.
12+
* Necessary due to target socket re-use.
13+
*/
14+
export type SocketWithPreviousStats = net.Socket & { previousBytesWritten?: number, previousBytesRead?: number };
15+
1016
interface Extras {
1117
[targetBytesWritten]: number;
1218
[targetBytesRead]: number;
13-
[targets]: Set<net.Socket>;
19+
[targets]: Set<SocketWithPreviousStats>;
1420
[calculateTargetStats]: () => Stats;
1521
}
1622

1723
// @ts-expect-error TS is not aware that `source` is used in the assertion.
1824
function typeSocket(source: unknown): asserts source is net.Socket & Extras {}
1925

20-
export const countTargetBytes = (source: net.Socket, target: net.Socket): void => {
26+
export const countTargetBytes = (
27+
source: net.Socket,
28+
target: SocketWithPreviousStats,
29+
registerCloseHandler?: (handler: () => void) => void,
30+
): void => {
2131
typeSocket(source);
2232

2333
source[targetBytesWritten] = source[targetBytesWritten] || 0;
@@ -26,21 +36,24 @@ export const countTargetBytes = (source: net.Socket, target: net.Socket): void =
2636

2737
source[targets].add(target);
2838

29-
target.once('close', () => {
30-
source[targetBytesWritten] += target.bytesWritten;
31-
source[targetBytesRead] += target.bytesRead;
32-
39+
const closeHandler = () => {
40+
source[targetBytesWritten] += (target.bytesWritten - (target.previousBytesWritten || 0));
41+
source[targetBytesRead] += (target.bytesRead - (target.previousBytesRead || 0));
3342
source[targets].delete(target);
34-
});
43+
};
44+
if (!registerCloseHandler) {
45+
registerCloseHandler = (handler: () => void) => target.once('close', handler);
46+
}
47+
registerCloseHandler(closeHandler);
3548

3649
if (!source[calculateTargetStats]) {
3750
source[calculateTargetStats] = () => {
3851
let bytesWritten = source[targetBytesWritten];
3952
let bytesRead = source[targetBytesRead];
4053

4154
for (const socket of source[targets]) {
42-
bytesWritten += socket.bytesWritten;
43-
bytesRead += socket.bytesRead;
55+
bytesWritten += (socket.bytesWritten - (socket.previousBytesWritten || 0));
56+
bytesRead += (socket.bytesRead - (socket.previousBytesRead || 0));
4457
}
4558

4659
return {

0 commit comments

Comments
 (0)