@push.rocks/smartproxy 19.5.17 → 19.5.19

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.
@@ -67,37 +67,6 @@ export function cleanupSocket(
67
67
  });
68
68
  }
69
69
 
70
- /**
71
- * Create a cleanup handler for paired sockets (client and server)
72
- * @param clientSocket The client socket
73
- * @param serverSocket The server socket (optional)
74
- * @param onCleanup Optional callback when cleanup is done
75
- * @returns A cleanup function that can be called multiple times safely
76
- * @deprecated Use createIndependentSocketHandlers for better half-open support
77
- */
78
- export function createSocketCleanupHandler(
79
- clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
80
- serverSocket?: plugins.net.Socket | plugins.tls.TLSSocket | null,
81
- onCleanup?: (reason: string) => void
82
- ): (reason: string) => void {
83
- let cleanedUp = false;
84
-
85
- return (reason: string) => {
86
- if (cleanedUp) return;
87
- cleanedUp = true;
88
-
89
- // Cleanup both sockets (old behavior - too aggressive)
90
- cleanupSocket(clientSocket, 'client', { immediate: true });
91
- if (serverSocket) {
92
- cleanupSocket(serverSocket, 'server', { immediate: true });
93
- }
94
-
95
- // Call cleanup callback if provided
96
- if (onCleanup) {
97
- onCleanup(reason);
98
- }
99
- };
100
- }
101
70
 
102
71
  /**
103
72
  * Create independent cleanup handlers for paired sockets that support half-open connections
@@ -109,7 +78,8 @@ export function createSocketCleanupHandler(
109
78
  export function createIndependentSocketHandlers(
110
79
  clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
111
80
  serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
112
- onBothClosed: (reason: string) => void
81
+ onBothClosed: (reason: string) => void,
82
+ options: { enableHalfOpen?: boolean } = {}
113
83
  ): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
114
84
  let clientClosed = false;
115
85
  let serverClosed = false;
@@ -127,8 +97,13 @@ export function createIndependentSocketHandlers(
127
97
  clientClosed = true;
128
98
  clientReason = reason;
129
99
 
130
- // Allow server to continue if still active
131
- if (!serverClosed && serverSocket.writable) {
100
+ // Default behavior: close both sockets when one closes (required for proxy chains)
101
+ if (!serverClosed && !options.enableHalfOpen) {
102
+ serverSocket.destroy();
103
+ }
104
+
105
+ // Half-open support (opt-in only)
106
+ if (!serverClosed && serverSocket.writable && options.enableHalfOpen) {
132
107
  // Half-close: stop reading from client, let server finish
133
108
  clientSocket.pause();
134
109
  clientSocket.unpipe(serverSocket);
@@ -145,8 +120,13 @@ export function createIndependentSocketHandlers(
145
120
  serverClosed = true;
146
121
  serverReason = reason;
147
122
 
148
- // Allow client to continue if still active
149
- if (!clientClosed && clientSocket.writable) {
123
+ // Default behavior: close both sockets when one closes (required for proxy chains)
124
+ if (!clientClosed && !options.enableHalfOpen) {
125
+ clientSocket.destroy();
126
+ }
127
+
128
+ // Half-open support (opt-in only)
129
+ if (!clientClosed && clientSocket.writable && options.enableHalfOpen) {
150
130
  // Half-close: stop reading from server, let client finish
151
131
  serverSocket.pause();
152
132
  serverSocket.unpipe(clientSocket);
@@ -195,16 +175,76 @@ export function setupSocketHandlers(
195
175
  }
196
176
 
197
177
  /**
198
- * Pipe two sockets together with proper cleanup on either end
199
- * @param socket1 First socket
200
- * @param socket2 Second socket
178
+ * Setup bidirectional data forwarding between two sockets with proper cleanup
179
+ * @param clientSocket The client/incoming socket
180
+ * @param serverSocket The server/outgoing socket
181
+ * @param handlers Object containing optional handlers for data and cleanup
182
+ * @returns Cleanup functions for both sockets
201
183
  */
202
- export function pipeSockets(
203
- socket1: plugins.net.Socket | plugins.tls.TLSSocket,
204
- socket2: plugins.net.Socket | plugins.tls.TLSSocket
205
- ): void {
206
- socket1.pipe(socket2);
207
- socket2.pipe(socket1);
184
+ export function setupBidirectionalForwarding(
185
+ clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
186
+ serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
187
+ handlers: {
188
+ onClientData?: (chunk: Buffer) => void;
189
+ onServerData?: (chunk: Buffer) => void;
190
+ onCleanup: (reason: string) => void;
191
+ enableHalfOpen?: boolean;
192
+ }
193
+ ): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
194
+ // Set up cleanup handlers
195
+ const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
196
+ clientSocket,
197
+ serverSocket,
198
+ handlers.onCleanup,
199
+ { enableHalfOpen: handlers.enableHalfOpen }
200
+ );
201
+
202
+ // Set up error and close handlers
203
+ setupSocketHandlers(clientSocket, cleanupClient, undefined, 'client');
204
+ setupSocketHandlers(serverSocket, cleanupServer, undefined, 'server');
205
+
206
+ // Set up data forwarding with backpressure handling
207
+ clientSocket.on('data', (chunk: Buffer) => {
208
+ if (handlers.onClientData) {
209
+ handlers.onClientData(chunk);
210
+ }
211
+
212
+ if (serverSocket.writable) {
213
+ const flushed = serverSocket.write(chunk);
214
+
215
+ // Handle backpressure
216
+ if (!flushed) {
217
+ clientSocket.pause();
218
+ serverSocket.once('drain', () => {
219
+ if (!clientSocket.destroyed) {
220
+ clientSocket.resume();
221
+ }
222
+ });
223
+ }
224
+ }
225
+ });
226
+
227
+ serverSocket.on('data', (chunk: Buffer) => {
228
+ if (handlers.onServerData) {
229
+ handlers.onServerData(chunk);
230
+ }
231
+
232
+ if (clientSocket.writable) {
233
+ const flushed = clientSocket.write(chunk);
234
+
235
+ // Handle backpressure
236
+ if (!flushed) {
237
+ serverSocket.pause();
238
+ clientSocket.once('drain', () => {
239
+ if (!serverSocket.destroyed) {
240
+ serverSocket.resume();
241
+ }
242
+ });
243
+ }
244
+ }
245
+ });
246
+
247
+ return { cleanupClient, cleanupServer };
208
248
  }
209
249
 
210
250
  /**
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.js';
2
2
  import { ForwardingHandler } from './base-handler.js';
3
3
  import type { IForwardConfig } from '../config/forwarding-types.js';
4
4
  import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
5
- import { createSocketCleanupHandler, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
5
+ import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
6
6
 
7
7
  /**
8
8
  * Handler for HTTPS termination with HTTP backend
@@ -100,19 +100,30 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
100
100
  let backendSocket: plugins.net.Socket | null = null;
101
101
  let dataBuffer = Buffer.alloc(0);
102
102
  let connectionEstablished = false;
103
+ let forwardingSetup = false;
103
104
 
104
- // Create cleanup handler for all sockets
105
- const handleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
106
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
107
- remoteAddress,
108
- reason
109
- });
110
- dataBuffer = Buffer.alloc(0);
111
- connectionEstablished = false;
112
- });
105
+ // Set up initial error handling for TLS socket
106
+ const tlsCleanupHandler = (reason: string) => {
107
+ if (!forwardingSetup) {
108
+ // If forwarding not set up yet, emit disconnected and cleanup
109
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
110
+ remoteAddress,
111
+ reason
112
+ });
113
+ dataBuffer = Buffer.alloc(0);
114
+ connectionEstablished = false;
115
+
116
+ if (!tlsSocket.destroyed) {
117
+ tlsSocket.destroy();
118
+ }
119
+ if (backendSocket && !backendSocket.destroyed) {
120
+ backendSocket.destroy();
121
+ }
122
+ }
123
+ // If forwarding is setup, setupBidirectionalForwarding will handle cleanup
124
+ };
113
125
 
114
- // Set up error handling with our cleanup utility
115
- setupSocketHandlers(tlsSocket, handleClose, undefined, 'tls');
126
+ setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls');
116
127
 
117
128
  // Set timeout
118
129
  const timeout = this.getTimeout();
@@ -123,7 +134,7 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
123
134
  remoteAddress,
124
135
  error: 'TLS connection timeout'
125
136
  });
126
- handleClose('timeout');
137
+ tlsCleanupHandler('timeout');
127
138
  });
128
139
 
129
140
  // Handle TLS data
@@ -172,30 +183,33 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
172
183
  dataBuffer = Buffer.alloc(0);
173
184
  }
174
185
 
175
- // Set up bidirectional data flow
176
- tlsSocket.pipe(backendSocket!);
177
- backendSocket!.pipe(tlsSocket);
186
+ // Now set up bidirectional forwarding with proper cleanup
187
+ forwardingSetup = true;
188
+ setupBidirectionalForwarding(tlsSocket, backendSocket!, {
189
+ onCleanup: (reason) => {
190
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
191
+ remoteAddress,
192
+ reason
193
+ });
194
+ dataBuffer = Buffer.alloc(0);
195
+ connectionEstablished = false;
196
+ forwardingSetup = false;
197
+ },
198
+ enableHalfOpen: false // Close both when one closes
199
+ });
178
200
  }
179
201
  });
180
202
 
181
- // Update the cleanup handler with the backend socket
182
- const newHandleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
183
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
184
- remoteAddress,
185
- reason
186
- });
187
- dataBuffer = Buffer.alloc(0);
188
- connectionEstablished = false;
189
- });
190
-
191
- // Set up handlers for backend socket
192
- setupSocketHandlers(backendSocket, newHandleClose, undefined, 'backend');
193
-
203
+ // Additional error logging for backend socket
194
204
  backendSocket.on('error', (error) => {
195
- this.emit(ForwardingHandlerEvents.ERROR, {
196
- remoteAddress,
197
- error: `Target connection error: ${error.message}`
198
- });
205
+ if (!connectionEstablished) {
206
+ // Connection failed during setup
207
+ this.emit(ForwardingHandlerEvents.ERROR, {
208
+ remoteAddress,
209
+ error: `Target connection error: ${error.message}`
210
+ });
211
+ }
212
+ // If connected, setupBidirectionalForwarding handles cleanup
199
213
  });
200
214
  }
201
215
  });
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.js';
2
2
  import { ForwardingHandler } from './base-handler.js';
3
3
  import type { IForwardConfig } from '../config/forwarding-types.js';
4
4
  import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
5
- import { createSocketCleanupHandler, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
5
+ import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
6
6
 
7
7
  /**
8
8
  * Handler for HTTPS termination with HTTPS backend
@@ -96,17 +96,26 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
96
96
 
97
97
  // Variable to track backend socket
98
98
  let backendSocket: plugins.tls.TLSSocket | null = null;
99
+ let isConnectedToBackend = false;
99
100
 
100
- // Create cleanup handler for both sockets
101
- const handleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
102
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
103
- remoteAddress,
104
- reason
105
- });
106
- });
101
+ // Set up initial error handling for TLS socket
102
+ const tlsCleanupHandler = (reason: string) => {
103
+ if (!isConnectedToBackend) {
104
+ // If backend not connected yet, just emit disconnected event
105
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
106
+ remoteAddress,
107
+ reason
108
+ });
109
+
110
+ // Cleanup TLS socket if needed
111
+ if (!tlsSocket.destroyed) {
112
+ tlsSocket.destroy();
113
+ }
114
+ }
115
+ // If connected to backend, setupBidirectionalForwarding will handle cleanup
116
+ };
107
117
 
108
- // Set up error handling with our cleanup utility
109
- setupSocketHandlers(tlsSocket, handleClose, undefined, 'tls');
118
+ setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls');
110
119
 
111
120
  // Set timeout
112
121
  const timeout = this.getTimeout();
@@ -117,7 +126,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
117
126
  remoteAddress,
118
127
  error: 'TLS connection timeout'
119
128
  });
120
- handleClose('timeout');
129
+ tlsCleanupHandler('timeout');
121
130
  });
122
131
 
123
132
  // Get the target from configuration
@@ -131,44 +140,55 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
131
140
  // In a real implementation, we would configure TLS options
132
141
  rejectUnauthorized: false // For testing only, never use in production
133
142
  }, () => {
143
+ isConnectedToBackend = true;
144
+
134
145
  this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
135
146
  direction: 'outbound',
136
147
  target: `${target.host}:${target.port}`,
137
148
  tls: true
138
149
  });
139
150
 
140
- // Set up bidirectional data flow
141
- tlsSocket.pipe(backendSocket!);
142
- backendSocket!.pipe(tlsSocket);
143
- });
144
-
145
- // Update the cleanup handler with the backend socket
146
- const newHandleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
147
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
148
- remoteAddress,
149
- reason
151
+ // Set up bidirectional forwarding with proper cleanup
152
+ setupBidirectionalForwarding(tlsSocket, backendSocket!, {
153
+ onCleanup: (reason) => {
154
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
155
+ remoteAddress,
156
+ reason
157
+ });
158
+ },
159
+ enableHalfOpen: false // Close both when one closes
160
+ });
161
+
162
+ // Set timeout for backend socket
163
+ backendSocket!.setTimeout(timeout);
164
+
165
+ backendSocket!.on('timeout', () => {
166
+ this.emit(ForwardingHandlerEvents.ERROR, {
167
+ remoteAddress,
168
+ error: 'Backend connection timeout'
169
+ });
170
+ // Let setupBidirectionalForwarding handle the cleanup
150
171
  });
151
172
  });
152
173
 
153
- // Set up handlers for backend socket
154
- setupSocketHandlers(backendSocket, newHandleClose, undefined, 'backend');
155
-
174
+ // Handle backend connection errors
156
175
  backendSocket.on('error', (error) => {
157
176
  this.emit(ForwardingHandlerEvents.ERROR, {
158
177
  remoteAddress,
159
178
  error: `Backend connection error: ${error.message}`
160
179
  });
161
- });
162
-
163
- // Set timeout for backend socket
164
- backendSocket.setTimeout(timeout);
165
-
166
- backendSocket.on('timeout', () => {
167
- this.emit(ForwardingHandlerEvents.ERROR, {
168
- remoteAddress,
169
- error: 'Backend connection timeout'
170
- });
171
- newHandleClose('backend_timeout');
180
+
181
+ if (!isConnectedToBackend) {
182
+ // Connection failed, clean up TLS socket
183
+ if (!tlsSocket.destroyed) {
184
+ tlsSocket.destroy();
185
+ }
186
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
187
+ remoteAddress,
188
+ reason: `backend_connection_failed: ${error.message}`
189
+ });
190
+ }
191
+ // If connected, let setupBidirectionalForwarding handle cleanup
172
192
  });
173
193
  };
174
194
 
@@ -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
@@ -1137,65 +1137,27 @@ export class RouteConnectionHandler {
1137
1137
  record.pendingDataSize = 0;
1138
1138
  }
1139
1139
 
1140
- // Set up independent socket handlers for half-open connection support
1141
- const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
1142
- socket,
1143
- targetSocket,
1144
- (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) => {
1145
1151
  this.connectionManager.cleanupConnection(record, reason);
1146
- }
1147
- );
1148
-
1149
- // Setup socket handlers with custom timeout handling
1150
- setupSocketHandlers(socket, cleanupClient, (sock) => {
1151
- // Don't close on timeout for keep-alive connections
1152
- if (record.hasKeepAlive) {
1153
- sock.setTimeout(this.settings.socketTimeout || 3600000);
1154
- }
1155
- }, 'client');
1156
-
1157
- setupSocketHandlers(targetSocket, cleanupServer, (sock) => {
1158
- // Don't close on timeout for keep-alive connections
1159
- if (record.hasKeepAlive) {
1160
- sock.setTimeout(this.settings.socketTimeout || 3600000);
1161
- }
1162
- }, 'server');
1163
-
1164
- // Forward data from client to target with backpressure handling
1165
- socket.on('data', (chunk: Buffer) => {
1166
- record.bytesReceived += chunk.length;
1167
- this.timeoutManager.updateActivity(record);
1168
-
1169
- if (targetSocket.writable) {
1170
- const flushed = targetSocket.write(chunk);
1171
-
1172
- // Handle backpressure
1173
- if (!flushed) {
1174
- socket.pause();
1175
- targetSocket.once('drain', () => {
1176
- socket.resume();
1177
- });
1178
- }
1179
- }
1180
- });
1181
-
1182
- // Forward data from target to client with backpressure handling
1183
- targetSocket.on('data', (chunk: Buffer) => {
1184
- record.bytesSent += chunk.length;
1185
- this.timeoutManager.updateActivity(record);
1186
-
1187
- if (socket.writable) {
1188
- const flushed = socket.write(chunk);
1189
-
1190
- // Handle backpressure
1191
- if (!flushed) {
1192
- targetSocket.pause();
1193
- socket.once('drain', () => {
1194
- targetSocket.resume();
1195
- });
1196
- }
1197
- }
1152
+ },
1153
+ enableHalfOpen: false // Default: close both when one closes (required for proxy chains)
1198
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
+ }
1199
1161
 
1200
1162
  // Log successful connection
1201
1163
  logger.log('info',
@@ -1354,11 +1316,5 @@ export class RouteConnectionHandler {
1354
1316
 
1355
1317
  // Apply socket timeouts
1356
1318
  this.timeoutManager.applySocketTimeouts(record);
1357
-
1358
- // Track outgoing data for bytes counting (moved from the duplicate connect handler)
1359
- targetSocket.on('data', (chunk: Buffer) => {
1360
- record.bytesSent += chunk.length;
1361
- this.timeoutManager.updateActivity(record);
1362
- });
1363
1319
  }
1364
1320
  }