@push.rocks/smartproxy 19.5.16 → 19.5.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,7 +109,8 @@ export function createSocketCleanupHandler(
109
109
  export function createIndependentSocketHandlers(
110
110
  clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
111
111
  serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
112
- onBothClosed: (reason: string) => void
112
+ onBothClosed: (reason: string) => void,
113
+ options: { enableHalfOpen?: boolean } = {}
113
114
  ): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
114
115
  let clientClosed = false;
115
116
  let serverClosed = false;
@@ -127,8 +128,13 @@ export function createIndependentSocketHandlers(
127
128
  clientClosed = true;
128
129
  clientReason = reason;
129
130
 
130
- // Allow server to continue if still active
131
- if (!serverClosed && serverSocket.writable) {
131
+ // Default behavior: close both sockets when one closes (required for proxy chains)
132
+ if (!serverClosed && !options.enableHalfOpen) {
133
+ serverSocket.destroy();
134
+ }
135
+
136
+ // Half-open support (opt-in only)
137
+ if (!serverClosed && serverSocket.writable && options.enableHalfOpen) {
132
138
  // Half-close: stop reading from client, let server finish
133
139
  clientSocket.pause();
134
140
  clientSocket.unpipe(serverSocket);
@@ -145,8 +151,13 @@ export function createIndependentSocketHandlers(
145
151
  serverClosed = true;
146
152
  serverReason = reason;
147
153
 
148
- // Allow client to continue if still active
149
- if (!clientClosed && clientSocket.writable) {
154
+ // Default behavior: close both sockets when one closes (required for proxy chains)
155
+ if (!clientClosed && !options.enableHalfOpen) {
156
+ clientSocket.destroy();
157
+ }
158
+
159
+ // Half-open support (opt-in only)
160
+ if (!clientClosed && clientSocket.writable && options.enableHalfOpen) {
150
161
  // Half-close: stop reading from server, let client finish
151
162
  serverSocket.pause();
152
163
  serverSocket.unpipe(clientSocket);
@@ -194,6 +205,79 @@ export function setupSocketHandlers(
194
205
  });
195
206
  }
196
207
 
208
+ /**
209
+ * Setup bidirectional data forwarding between two sockets with proper cleanup
210
+ * @param clientSocket The client/incoming socket
211
+ * @param serverSocket The server/outgoing socket
212
+ * @param handlers Object containing optional handlers for data and cleanup
213
+ * @returns Cleanup functions for both sockets
214
+ */
215
+ export function setupBidirectionalForwarding(
216
+ clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
217
+ serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
218
+ handlers: {
219
+ onClientData?: (chunk: Buffer) => void;
220
+ onServerData?: (chunk: Buffer) => void;
221
+ onCleanup: (reason: string) => void;
222
+ enableHalfOpen?: boolean;
223
+ }
224
+ ): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
225
+ // Set up cleanup handlers
226
+ const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
227
+ clientSocket,
228
+ serverSocket,
229
+ handlers.onCleanup,
230
+ { enableHalfOpen: handlers.enableHalfOpen }
231
+ );
232
+
233
+ // Set up error and close handlers
234
+ setupSocketHandlers(clientSocket, cleanupClient, undefined, 'client');
235
+ setupSocketHandlers(serverSocket, cleanupServer, undefined, 'server');
236
+
237
+ // Set up data forwarding with backpressure handling
238
+ clientSocket.on('data', (chunk: Buffer) => {
239
+ if (handlers.onClientData) {
240
+ handlers.onClientData(chunk);
241
+ }
242
+
243
+ if (serverSocket.writable) {
244
+ const flushed = serverSocket.write(chunk);
245
+
246
+ // Handle backpressure
247
+ if (!flushed) {
248
+ clientSocket.pause();
249
+ serverSocket.once('drain', () => {
250
+ if (!clientSocket.destroyed) {
251
+ clientSocket.resume();
252
+ }
253
+ });
254
+ }
255
+ }
256
+ });
257
+
258
+ serverSocket.on('data', (chunk: Buffer) => {
259
+ if (handlers.onServerData) {
260
+ handlers.onServerData(chunk);
261
+ }
262
+
263
+ if (clientSocket.writable) {
264
+ const flushed = clientSocket.write(chunk);
265
+
266
+ // Handle backpressure
267
+ if (!flushed) {
268
+ serverSocket.pause();
269
+ clientSocket.once('drain', () => {
270
+ if (!serverSocket.destroyed) {
271
+ serverSocket.resume();
272
+ }
273
+ });
274
+ }
275
+ }
276
+ });
277
+
278
+ return { cleanupClient, cleanupServer };
279
+ }
280
+
197
281
  /**
198
282
  * Pipe two sockets together with proper cleanup on either end
199
283
  * @param socket1 First socket
@@ -1,5 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { HttpProxy } from '../http-proxy/index.js';
3
+ import { setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
3
4
  import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
4
5
  import type { IRouteConfig } from './models/route-types.js';
5
6
 
@@ -123,36 +124,25 @@ export class HttpProxyBridge {
123
124
  proxySocket.write(initialChunk);
124
125
  }
125
126
 
126
- // Pipe the sockets together
127
- socket.pipe(proxySocket);
128
- proxySocket.pipe(socket);
129
-
130
- // Handle cleanup
131
- let cleanedUp = false;
132
- const cleanup = (reason: string) => {
133
- if (cleanedUp) return;
134
- cleanedUp = true;
135
-
136
- // Remove all event listeners to prevent memory leaks
137
- socket.removeAllListeners('end');
138
- socket.removeAllListeners('error');
139
- proxySocket.removeAllListeners('end');
140
- proxySocket.removeAllListeners('error');
141
-
142
- socket.unpipe(proxySocket);
143
- proxySocket.unpipe(socket);
144
-
145
- if (!proxySocket.destroyed) {
146
- proxySocket.destroy();
147
- }
148
-
149
- cleanupCallback(reason);
150
- };
151
-
152
- socket.on('end', () => cleanup('socket_end'));
153
- socket.on('error', () => cleanup('socket_error'));
154
- proxySocket.on('end', () => cleanup('proxy_end'));
155
- proxySocket.on('error', () => cleanup('proxy_error'));
127
+ // Use centralized bidirectional forwarding
128
+ setupBidirectionalForwarding(socket, proxySocket, {
129
+ onClientData: (chunk) => {
130
+ // Update stats if needed
131
+ if (record) {
132
+ record.bytesReceived += chunk.length;
133
+ }
134
+ },
135
+ onServerData: (chunk) => {
136
+ // Update stats if needed
137
+ if (record) {
138
+ record.bytesSent += chunk.length;
139
+ }
140
+ },
141
+ onCleanup: (reason) => {
142
+ cleanupCallback(reason);
143
+ },
144
+ enableHalfOpen: false // Close both when one closes (required for proxy chains)
145
+ });
156
146
  }
157
147
 
158
148
  /**
@@ -9,7 +9,7 @@ import { TlsManager } from './tls-manager.js';
9
9
  import { HttpProxyBridge } from './http-proxy-bridge.js';
10
10
  import { TimeoutManager } from './timeout-manager.js';
11
11
  import { RouteManager } from './route-manager.js';
12
- import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
12
+ import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
13
13
 
14
14
  /**
15
15
  * Handles new connection processing and setup logic with support for route-based configuration
@@ -176,8 +176,33 @@ export class RouteConnectionHandler {
176
176
 
177
177
  // If no routes require TLS handling and it's not port 443, route immediately
178
178
  if (!needsTlsHandling && localPort !== 443) {
179
- // Set up error handler
180
- socket.on('error', this.connectionManager.handleError('incoming', record));
179
+ // Set up proper socket handlers for immediate routing
180
+ setupSocketHandlers(
181
+ socket,
182
+ (reason) => {
183
+ // Only cleanup if connection hasn't been fully established
184
+ // Check if outgoing connection exists and is connected
185
+ if (!record.outgoing || record.outgoing.readyState !== 'open') {
186
+ logger.log('debug', `Connection ${connectionId} closed during immediate routing: ${reason}`, {
187
+ connectionId,
188
+ remoteIP: record.remoteIP,
189
+ reason,
190
+ hasOutgoing: !!record.outgoing,
191
+ outgoingState: record.outgoing?.readyState,
192
+ component: 'route-handler'
193
+ });
194
+
195
+ // If there's a pending outgoing connection, destroy it
196
+ if (record.outgoing && !record.outgoing.destroyed) {
197
+ record.outgoing.destroy();
198
+ }
199
+
200
+ this.connectionManager.cleanupConnection(record, reason);
201
+ }
202
+ },
203
+ undefined, // Use default timeout handler
204
+ 'immediate-route-client'
205
+ );
181
206
 
182
207
  // Route immediately for non-TLS connections
183
208
  this.routeConnection(socket, record, '', undefined);
@@ -221,6 +246,37 @@ export class RouteConnectionHandler {
221
246
  // Set up error handler
222
247
  socket.on('error', this.connectionManager.handleError('incoming', record));
223
248
 
249
+ // Add close/end handlers to catch immediate disconnections
250
+ socket.once('close', () => {
251
+ if (!initialDataReceived) {
252
+ logger.log('warn', `Connection ${connectionId} closed before sending initial data`, {
253
+ connectionId,
254
+ remoteIP: record.remoteIP,
255
+ component: 'route-handler'
256
+ });
257
+ if (initialTimeout) {
258
+ clearTimeout(initialTimeout);
259
+ initialTimeout = null;
260
+ }
261
+ this.connectionManager.cleanupConnection(record, 'closed_before_data');
262
+ }
263
+ });
264
+
265
+ socket.once('end', () => {
266
+ if (!initialDataReceived) {
267
+ logger.log('debug', `Connection ${connectionId} ended before sending initial data`, {
268
+ connectionId,
269
+ remoteIP: record.remoteIP,
270
+ component: 'route-handler'
271
+ });
272
+ if (initialTimeout) {
273
+ clearTimeout(initialTimeout);
274
+ initialTimeout = null;
275
+ }
276
+ // Don't cleanup on 'end' - wait for 'close'
277
+ }
278
+ });
279
+
224
280
  // First data handler to capture initial TLS handshake
225
281
  socket.once('data', (chunk: Buffer) => {
226
282
  // Clear the initial timeout since we've received data
@@ -927,107 +983,6 @@ export class RouteConnectionHandler {
927
983
  }
928
984
  }
929
985
 
930
- /**
931
- * Setup improved error handling for the outgoing connection
932
- * @deprecated This method is no longer used - error handling is done in createSocketWithErrorHandler
933
- */
934
- private setupOutgoingErrorHandler(
935
- connectionId: string,
936
- targetSocket: plugins.net.Socket,
937
- record: IConnectionRecord,
938
- socket: plugins.net.Socket,
939
- finalTargetHost: string,
940
- finalTargetPort: number
941
- ): void {
942
- targetSocket.once('error', (err) => {
943
- // This handler runs only once during the initial connection phase
944
- const code = (err as any).code;
945
- logger.log('error',
946
- `Connection setup error for ${connectionId} to ${finalTargetHost}:${finalTargetPort}: ${err.message} (${code})`,
947
- {
948
- connectionId,
949
- targetHost: finalTargetHost,
950
- targetPort: finalTargetPort,
951
- errorMessage: err.message,
952
- errorCode: code,
953
- component: 'route-handler'
954
- }
955
- );
956
-
957
- // Resume the incoming socket to prevent it from hanging
958
- socket.resume();
959
-
960
- // Log specific error types for easier debugging
961
- if (code === 'ECONNREFUSED') {
962
- logger.log('error',
963
- `Connection ${connectionId}: Target ${finalTargetHost}:${finalTargetPort} refused connection. Check if the target service is running and listening on that port.`,
964
- {
965
- connectionId,
966
- targetHost: finalTargetHost,
967
- targetPort: finalTargetPort,
968
- recommendation: 'Check if the target service is running and listening on that port.',
969
- component: 'route-handler'
970
- }
971
- );
972
- } else if (code === 'ETIMEDOUT') {
973
- logger.log('error',
974
- `Connection ${connectionId} to ${finalTargetHost}:${finalTargetPort} timed out. Check network conditions, firewall rules, or if the target is too far away.`,
975
- {
976
- connectionId,
977
- targetHost: finalTargetHost,
978
- targetPort: finalTargetPort,
979
- recommendation: 'Check network conditions, firewall rules, or if the target is too far away.',
980
- component: 'route-handler'
981
- }
982
- );
983
- } else if (code === 'ECONNRESET') {
984
- logger.log('error',
985
- `Connection ${connectionId} to ${finalTargetHost}:${finalTargetPort} was reset. The target might have closed the connection abruptly.`,
986
- {
987
- connectionId,
988
- targetHost: finalTargetHost,
989
- targetPort: finalTargetPort,
990
- recommendation: 'The target might have closed the connection abruptly.',
991
- component: 'route-handler'
992
- }
993
- );
994
- } else if (code === 'EHOSTUNREACH') {
995
- logger.log('error',
996
- `Connection ${connectionId}: Host ${finalTargetHost} is unreachable. Check DNS settings, network routing, or firewall rules.`,
997
- {
998
- connectionId,
999
- targetHost: finalTargetHost,
1000
- recommendation: 'Check DNS settings, network routing, or firewall rules.',
1001
- component: 'route-handler'
1002
- }
1003
- );
1004
- } else if (code === 'ENOTFOUND') {
1005
- logger.log('error',
1006
- `Connection ${connectionId}: DNS lookup failed for ${finalTargetHost}. Check your DNS settings or if the hostname is correct.`,
1007
- {
1008
- connectionId,
1009
- targetHost: finalTargetHost,
1010
- recommendation: 'Check your DNS settings or if the hostname is correct.',
1011
- component: 'route-handler'
1012
- }
1013
- );
1014
- }
1015
-
1016
- // Clear any existing error handler after connection phase
1017
- targetSocket.removeAllListeners('error');
1018
-
1019
- // Re-add the normal error handler for established connections
1020
- targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
1021
-
1022
- if (record.outgoingTerminationReason === null) {
1023
- record.outgoingTerminationReason = 'connection_failed';
1024
- this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
1025
- }
1026
-
1027
- // Clean up the connection
1028
- this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
1029
- });
1030
- }
1031
986
 
1032
987
  /**
1033
988
  * Sets up a direct connection to the target
@@ -1090,6 +1045,16 @@ export class RouteConnectionHandler {
1090
1045
  host: finalTargetHost,
1091
1046
  onError: (error) => {
1092
1047
  // Connection failed - clean up everything immediately
1048
+ // Check if connection record is still valid (client might have disconnected)
1049
+ if (record.connectionClosed) {
1050
+ logger.log('debug', `Backend connection failed but client already disconnected for ${connectionId}`, {
1051
+ connectionId,
1052
+ errorCode: (error as any).code,
1053
+ component: 'route-handler'
1054
+ });
1055
+ return;
1056
+ }
1057
+
1093
1058
  logger.log('error',
1094
1059
  `Connection setup error for ${connectionId} to ${finalTargetHost}:${finalTargetPort}: ${error.message} (${(error as any).code})`,
1095
1060
  {
@@ -1117,10 +1082,12 @@ export class RouteConnectionHandler {
1117
1082
  }
1118
1083
 
1119
1084
  // Resume the incoming socket to prevent it from hanging
1120
- socket.resume();
1085
+ if (socket && !socket.destroyed) {
1086
+ socket.resume();
1087
+ }
1121
1088
 
1122
1089
  // Clean up the incoming socket
1123
- if (!socket.destroyed) {
1090
+ if (socket && !socket.destroyed) {
1124
1091
  socket.destroy();
1125
1092
  }
1126
1093
 
@@ -1170,65 +1137,27 @@ export class RouteConnectionHandler {
1170
1137
  record.pendingDataSize = 0;
1171
1138
  }
1172
1139
 
1173
- // Set up independent socket handlers for half-open connection support
1174
- const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
1175
- socket,
1176
- targetSocket,
1177
- (reason) => {
1140
+ // Use centralized bidirectional forwarding setup
1141
+ setupBidirectionalForwarding(socket, targetSocket, {
1142
+ onClientData: (chunk) => {
1143
+ record.bytesReceived += chunk.length;
1144
+ this.timeoutManager.updateActivity(record);
1145
+ },
1146
+ onServerData: (chunk) => {
1147
+ record.bytesSent += chunk.length;
1148
+ this.timeoutManager.updateActivity(record);
1149
+ },
1150
+ onCleanup: (reason) => {
1178
1151
  this.connectionManager.cleanupConnection(record, reason);
1179
- }
1180
- );
1181
-
1182
- // Setup socket handlers with custom timeout handling
1183
- setupSocketHandlers(socket, cleanupClient, (sock) => {
1184
- // Don't close on timeout for keep-alive connections
1185
- if (record.hasKeepAlive) {
1186
- sock.setTimeout(this.settings.socketTimeout || 3600000);
1187
- }
1188
- }, 'client');
1189
-
1190
- setupSocketHandlers(targetSocket, cleanupServer, (sock) => {
1191
- // Don't close on timeout for keep-alive connections
1192
- if (record.hasKeepAlive) {
1193
- sock.setTimeout(this.settings.socketTimeout || 3600000);
1194
- }
1195
- }, 'server');
1196
-
1197
- // Forward data from client to target with backpressure handling
1198
- socket.on('data', (chunk: Buffer) => {
1199
- record.bytesReceived += chunk.length;
1200
- this.timeoutManager.updateActivity(record);
1201
-
1202
- if (targetSocket.writable) {
1203
- const flushed = targetSocket.write(chunk);
1204
-
1205
- // Handle backpressure
1206
- if (!flushed) {
1207
- socket.pause();
1208
- targetSocket.once('drain', () => {
1209
- socket.resume();
1210
- });
1211
- }
1212
- }
1213
- });
1214
-
1215
- // Forward data from target to client with backpressure handling
1216
- targetSocket.on('data', (chunk: Buffer) => {
1217
- record.bytesSent += chunk.length;
1218
- this.timeoutManager.updateActivity(record);
1219
-
1220
- if (socket.writable) {
1221
- const flushed = socket.write(chunk);
1222
-
1223
- // Handle backpressure
1224
- if (!flushed) {
1225
- targetSocket.pause();
1226
- socket.once('drain', () => {
1227
- targetSocket.resume();
1228
- });
1229
- }
1230
- }
1152
+ },
1153
+ enableHalfOpen: false // Default: close both when one closes (required for proxy chains)
1231
1154
  });
1155
+
1156
+ // Apply timeouts if keep-alive is enabled
1157
+ if (record.hasKeepAlive) {
1158
+ socket.setTimeout(this.settings.socketTimeout || 3600000);
1159
+ targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
1160
+ }
1232
1161
 
1233
1162
  // Log successful connection
1234
1163
  logger.log('info',
@@ -1294,7 +1223,7 @@ export class RouteConnectionHandler {
1294
1223
  }
1295
1224
  });
1296
1225
 
1297
- // Only set up basic properties - everything else happens in onConnect
1226
+ // Set outgoing socket immediately so it can be cleaned up if client disconnects
1298
1227
  record.outgoing = targetSocket;
1299
1228
  record.outgoingStartTime = Date.now();
1300
1229
 
@@ -1387,11 +1316,5 @@ export class RouteConnectionHandler {
1387
1316
 
1388
1317
  // Apply socket timeouts
1389
1318
  this.timeoutManager.applySocketTimeouts(record);
1390
-
1391
- // Track outgoing data for bytes counting (moved from the duplicate connect handler)
1392
- targetSocket.on('data', (chunk: Buffer) => {
1393
- record.bytesSent += chunk.length;
1394
- this.timeoutManager.updateActivity(record);
1395
- });
1396
1319
  }
1397
1320
  }