@push.rocks/smartproxy 19.5.6 → 19.5.9
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.
- package/dist_ts/core/utils/enhanced-connection-pool.js +7 -2
- package/dist_ts/core/utils/lifecycle-component.js +23 -7
- package/dist_ts/core/utils/socket-utils.d.ts +21 -2
- package/dist_ts/core/utils/socket-utils.js +108 -22
- package/dist_ts/forwarding/handlers/http-handler.js +7 -2
- package/dist_ts/forwarding/handlers/https-passthrough-handler.d.ts +1 -1
- package/dist_ts/forwarding/handlers/https-passthrough-handler.js +16 -10
- package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +3 -3
- package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +3 -3
- package/dist_ts/proxies/http-proxy/connection-pool.js +3 -3
- package/dist_ts/proxies/http-proxy/http-proxy.js +3 -4
- package/dist_ts/proxies/smart-proxy/connection-manager.js +48 -14
- package/dist_ts/proxies/smart-proxy/port-manager.js +2 -2
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +48 -10
- package/package.json +1 -1
- package/readme.plan.md +246 -1139
- package/ts/core/utils/enhanced-connection-pool.ts +6 -1
- package/ts/core/utils/lifecycle-component.ts +26 -6
- package/ts/core/utils/socket-utils.ts +123 -19
- package/ts/forwarding/handlers/http-handler.ts +6 -1
- package/ts/forwarding/handlers/https-passthrough-handler.ts +28 -16
- package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +2 -2
- package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +2 -2
- package/ts/proxies/http-proxy/connection-pool.ts +2 -2
- package/ts/proxies/http-proxy/http-proxy.ts +4 -3
- package/ts/proxies/smart-proxy/connection-manager.ts +48 -13
- package/ts/proxies/smart-proxy/port-manager.ts +1 -1
- package/ts/proxies/smart-proxy/route-connection-handler.ts +58 -9
|
@@ -403,7 +403,12 @@ export class EnhancedConnectionPool<T> extends LifecycleComponent {
|
|
|
403
403
|
const startTime = Date.now();
|
|
404
404
|
|
|
405
405
|
while (this.activeConnections.size > 0 && Date.now() - startTime < timeout) {
|
|
406
|
-
await new Promise(resolve =>
|
|
406
|
+
await new Promise(resolve => {
|
|
407
|
+
const timer = setTimeout(resolve, 100);
|
|
408
|
+
if (typeof timer.unref === 'function') {
|
|
409
|
+
timer.unref();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
407
412
|
}
|
|
408
413
|
|
|
409
414
|
// Destroy all connections
|
|
@@ -9,6 +9,7 @@ export abstract class LifecycleComponent {
|
|
|
9
9
|
target: any;
|
|
10
10
|
event: string;
|
|
11
11
|
handler: Function;
|
|
12
|
+
actualHandler?: Function; // The actual handler registered (may be wrapped)
|
|
12
13
|
once?: boolean;
|
|
13
14
|
}> = [];
|
|
14
15
|
private childComponents: Set<LifecycleComponent> = new Set();
|
|
@@ -21,7 +22,11 @@ export abstract class LifecycleComponent {
|
|
|
21
22
|
protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout {
|
|
22
23
|
if (this.isShuttingDown) {
|
|
23
24
|
// Return a dummy timer if shutting down
|
|
24
|
-
|
|
25
|
+
const dummyTimer = setTimeout(() => {}, 0);
|
|
26
|
+
if (typeof dummyTimer.unref === 'function') {
|
|
27
|
+
dummyTimer.unref();
|
|
28
|
+
}
|
|
29
|
+
return dummyTimer;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
const wrappedHandler = () => {
|
|
@@ -33,6 +38,12 @@ export abstract class LifecycleComponent {
|
|
|
33
38
|
|
|
34
39
|
const timer = setTimeout(wrappedHandler, timeout);
|
|
35
40
|
this.timers.add(timer);
|
|
41
|
+
|
|
42
|
+
// Allow process to exit even with timer
|
|
43
|
+
if (typeof timer.unref === 'function') {
|
|
44
|
+
timer.unref();
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
return timer;
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -42,7 +53,12 @@ export abstract class LifecycleComponent {
|
|
|
42
53
|
protected setInterval(handler: Function, interval: number): NodeJS.Timeout {
|
|
43
54
|
if (this.isShuttingDown) {
|
|
44
55
|
// Return a dummy timer if shutting down
|
|
45
|
-
|
|
56
|
+
const dummyTimer = setInterval(() => {}, interval);
|
|
57
|
+
if (typeof dummyTimer.unref === 'function') {
|
|
58
|
+
dummyTimer.unref();
|
|
59
|
+
}
|
|
60
|
+
clearInterval(dummyTimer); // Clear immediately since we don't need it
|
|
61
|
+
return dummyTimer;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
const wrappedHandler = () => {
|
|
@@ -121,11 +137,12 @@ export abstract class LifecycleComponent {
|
|
|
121
137
|
throw new Error('Target must support on() or addEventListener()');
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
// Store the original handler
|
|
140
|
+
// Store both the original handler and the actual handler registered
|
|
125
141
|
this.listeners.push({
|
|
126
142
|
target,
|
|
127
143
|
event,
|
|
128
144
|
handler,
|
|
145
|
+
actualHandler, // The handler that was actually registered (may be wrapped)
|
|
129
146
|
once: options?.once
|
|
130
147
|
});
|
|
131
148
|
}
|
|
@@ -208,12 +225,15 @@ export abstract class LifecycleComponent {
|
|
|
208
225
|
this.intervals.clear();
|
|
209
226
|
|
|
210
227
|
// Remove all event listeners
|
|
211
|
-
for (const { target, event, handler } of this.listeners) {
|
|
228
|
+
for (const { target, event, handler, actualHandler } of this.listeners) {
|
|
229
|
+
// Use actualHandler if available (for wrapped handlers), otherwise use the original handler
|
|
230
|
+
const handlerToRemove = actualHandler || handler;
|
|
231
|
+
|
|
212
232
|
// All listeners need to be removed, including 'once' listeners that might not have fired
|
|
213
233
|
if (typeof target.removeListener === 'function') {
|
|
214
|
-
target.removeListener(event,
|
|
234
|
+
target.removeListener(event, handlerToRemove);
|
|
215
235
|
} else if (typeof target.removeEventListener === 'function') {
|
|
216
|
-
target.removeEventListener(event,
|
|
236
|
+
target.removeEventListener(event, handlerToRemove);
|
|
217
237
|
}
|
|
218
238
|
}
|
|
219
239
|
this.listeners = [];
|
|
@@ -1,27 +1,62 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
|
|
3
|
+
export interface CleanupOptions {
|
|
4
|
+
immediate?: boolean; // Force immediate destruction
|
|
5
|
+
allowDrain?: boolean; // Allow write buffer to drain
|
|
6
|
+
gracePeriod?: number; // Ms to wait before force close
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
/**
|
|
4
10
|
* Safely cleanup a socket by removing all listeners and destroying it
|
|
5
11
|
* @param socket The socket to cleanup
|
|
6
12
|
* @param socketName Optional name for logging
|
|
13
|
+
* @param options Cleanup options
|
|
7
14
|
*/
|
|
8
|
-
export function cleanupSocket(
|
|
9
|
-
|
|
15
|
+
export function cleanupSocket(
|
|
16
|
+
socket: plugins.net.Socket | plugins.tls.TLSSocket | null,
|
|
17
|
+
socketName?: string,
|
|
18
|
+
options: CleanupOptions = {}
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
if (!socket || socket.destroyed) return Promise.resolve();
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
return new Promise<void>((resolve) => {
|
|
23
|
+
const cleanup = () => {
|
|
24
|
+
try {
|
|
25
|
+
// Remove all event listeners
|
|
26
|
+
socket.removeAllListeners();
|
|
27
|
+
|
|
28
|
+
// Destroy if not already destroyed
|
|
29
|
+
if (!socket.destroyed) {
|
|
30
|
+
socket.destroy();
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
|
|
34
|
+
}
|
|
35
|
+
resolve();
|
|
36
|
+
};
|
|
14
37
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
38
|
+
if (options.immediate) {
|
|
39
|
+
// Immediate cleanup (old behavior)
|
|
40
|
+
socket.unpipe();
|
|
41
|
+
cleanup();
|
|
42
|
+
} else if (options.allowDrain && socket.writable) {
|
|
43
|
+
// Allow pending writes to complete
|
|
44
|
+
socket.end(() => cleanup());
|
|
45
|
+
|
|
46
|
+
// Force cleanup after grace period
|
|
47
|
+
if (options.gracePeriod) {
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
if (!socket.destroyed) {
|
|
50
|
+
cleanup();
|
|
51
|
+
}
|
|
52
|
+
}, options.gracePeriod);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
// Default: immediate cleanup
|
|
56
|
+
socket.unpipe();
|
|
57
|
+
cleanup();
|
|
21
58
|
}
|
|
22
|
-
}
|
|
23
|
-
console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
|
|
24
|
-
}
|
|
59
|
+
});
|
|
25
60
|
}
|
|
26
61
|
|
|
27
62
|
/**
|
|
@@ -30,6 +65,7 @@ export function cleanupSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket
|
|
|
30
65
|
* @param serverSocket The server socket (optional)
|
|
31
66
|
* @param onCleanup Optional callback when cleanup is done
|
|
32
67
|
* @returns A cleanup function that can be called multiple times safely
|
|
68
|
+
* @deprecated Use createIndependentSocketHandlers for better half-open support
|
|
33
69
|
*/
|
|
34
70
|
export function createSocketCleanupHandler(
|
|
35
71
|
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
@@ -42,10 +78,10 @@ export function createSocketCleanupHandler(
|
|
|
42
78
|
if (cleanedUp) return;
|
|
43
79
|
cleanedUp = true;
|
|
44
80
|
|
|
45
|
-
// Cleanup both sockets
|
|
46
|
-
cleanupSocket(clientSocket, 'client');
|
|
81
|
+
// Cleanup both sockets (old behavior - too aggressive)
|
|
82
|
+
cleanupSocket(clientSocket, 'client', { immediate: true });
|
|
47
83
|
if (serverSocket) {
|
|
48
|
-
cleanupSocket(serverSocket, 'server');
|
|
84
|
+
cleanupSocket(serverSocket, 'server', { immediate: true });
|
|
49
85
|
}
|
|
50
86
|
|
|
51
87
|
// Call cleanup callback if provided
|
|
@@ -55,15 +91,79 @@ export function createSocketCleanupHandler(
|
|
|
55
91
|
};
|
|
56
92
|
}
|
|
57
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Create independent cleanup handlers for paired sockets that support half-open connections
|
|
96
|
+
* @param clientSocket The client socket
|
|
97
|
+
* @param serverSocket The server socket
|
|
98
|
+
* @param onBothClosed Callback when both sockets are closed
|
|
99
|
+
* @returns Independent cleanup functions for each socket
|
|
100
|
+
*/
|
|
101
|
+
export function createIndependentSocketHandlers(
|
|
102
|
+
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
103
|
+
serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
104
|
+
onBothClosed: (reason: string) => void
|
|
105
|
+
): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
|
|
106
|
+
let clientClosed = false;
|
|
107
|
+
let serverClosed = false;
|
|
108
|
+
let clientReason = '';
|
|
109
|
+
let serverReason = '';
|
|
110
|
+
|
|
111
|
+
const checkBothClosed = () => {
|
|
112
|
+
if (clientClosed && serverClosed) {
|
|
113
|
+
onBothClosed(`client: ${clientReason}, server: ${serverReason}`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const cleanupClient = async (reason: string) => {
|
|
118
|
+
if (clientClosed) return;
|
|
119
|
+
clientClosed = true;
|
|
120
|
+
clientReason = reason;
|
|
121
|
+
|
|
122
|
+
// Allow server to continue if still active
|
|
123
|
+
if (!serverClosed && serverSocket.writable) {
|
|
124
|
+
// Half-close: stop reading from client, let server finish
|
|
125
|
+
clientSocket.pause();
|
|
126
|
+
clientSocket.unpipe(serverSocket);
|
|
127
|
+
await cleanupSocket(clientSocket, 'client', { allowDrain: true, gracePeriod: 5000 });
|
|
128
|
+
} else {
|
|
129
|
+
await cleanupSocket(clientSocket, 'client', { immediate: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
checkBothClosed();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const cleanupServer = async (reason: string) => {
|
|
136
|
+
if (serverClosed) return;
|
|
137
|
+
serverClosed = true;
|
|
138
|
+
serverReason = reason;
|
|
139
|
+
|
|
140
|
+
// Allow client to continue if still active
|
|
141
|
+
if (!clientClosed && clientSocket.writable) {
|
|
142
|
+
// Half-close: stop reading from server, let client finish
|
|
143
|
+
serverSocket.pause();
|
|
144
|
+
serverSocket.unpipe(clientSocket);
|
|
145
|
+
await cleanupSocket(serverSocket, 'server', { allowDrain: true, gracePeriod: 5000 });
|
|
146
|
+
} else {
|
|
147
|
+
await cleanupSocket(serverSocket, 'server', { immediate: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
checkBothClosed();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return { cleanupClient, cleanupServer };
|
|
154
|
+
}
|
|
155
|
+
|
|
58
156
|
/**
|
|
59
157
|
* Setup socket error and close handlers with proper cleanup
|
|
60
158
|
* @param socket The socket to setup handlers for
|
|
61
159
|
* @param handleClose The cleanup function to call
|
|
160
|
+
* @param handleTimeout Optional custom timeout handler
|
|
62
161
|
* @param errorPrefix Optional prefix for error messages
|
|
63
162
|
*/
|
|
64
163
|
export function setupSocketHandlers(
|
|
65
164
|
socket: plugins.net.Socket | plugins.tls.TLSSocket,
|
|
66
165
|
handleClose: (reason: string) => void,
|
|
166
|
+
handleTimeout?: (socket: plugins.net.Socket | plugins.tls.TLSSocket) => void,
|
|
67
167
|
errorPrefix?: string
|
|
68
168
|
): void {
|
|
69
169
|
socket.on('error', (error) => {
|
|
@@ -77,8 +177,12 @@ export function setupSocketHandlers(
|
|
|
77
177
|
});
|
|
78
178
|
|
|
79
179
|
socket.on('timeout', () => {
|
|
80
|
-
|
|
81
|
-
|
|
180
|
+
if (handleTimeout) {
|
|
181
|
+
handleTimeout(socket); // Custom timeout handling
|
|
182
|
+
} else {
|
|
183
|
+
// Default: just log, don't close
|
|
184
|
+
console.warn(`Socket timeout: ${errorPrefix || 'socket'}`);
|
|
185
|
+
}
|
|
82
186
|
});
|
|
83
187
|
}
|
|
84
188
|
|
|
@@ -49,7 +49,12 @@ export class HttpForwardingHandler extends ForwardingHandler {
|
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
// Use custom timeout handler that doesn't close the socket
|
|
53
|
+
setupSocketHandlers(socket, handleClose, () => {
|
|
54
|
+
// For HTTP, we can be more aggressive with timeouts since connections are shorter
|
|
55
|
+
// But still don't close immediately - let the connection finish naturally
|
|
56
|
+
console.warn(`HTTP socket timeout from ${remoteAddress}`);
|
|
57
|
+
}, 'http');
|
|
53
58
|
|
|
54
59
|
socket.on('error', (error) => {
|
|
55
60
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
@@ -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 {
|
|
5
|
+
import { createIndependentSocketHandlers, setupSocketHandlers } from '../../core/utils/socket-utils.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Handler for HTTPS passthrough (SNI forwarding without termination)
|
|
@@ -55,19 +55,32 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
|
|
|
55
55
|
let bytesSent = 0;
|
|
56
56
|
let bytesReceived = 0;
|
|
57
57
|
|
|
58
|
-
// Create
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
// Create independent handlers for half-open connection support
|
|
59
|
+
const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
|
|
60
|
+
clientSocket,
|
|
61
|
+
serverSocket,
|
|
62
|
+
(reason) => {
|
|
63
|
+
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
64
|
+
remoteAddress,
|
|
65
|
+
bytesSent,
|
|
66
|
+
bytesReceived,
|
|
67
|
+
reason
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
);
|
|
67
71
|
|
|
68
|
-
// Setup
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// Setup handlers with custom timeout handling that doesn't close connections
|
|
73
|
+
const timeout = this.getTimeout();
|
|
74
|
+
|
|
75
|
+
setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
|
|
76
|
+
// Just reset timeout, don't close
|
|
77
|
+
socket.setTimeout(timeout);
|
|
78
|
+
}, 'client');
|
|
79
|
+
|
|
80
|
+
setupSocketHandlers(serverSocket, cleanupServer, (socket) => {
|
|
81
|
+
// Just reset timeout, don't close
|
|
82
|
+
socket.setTimeout(timeout);
|
|
83
|
+
}, 'server');
|
|
71
84
|
|
|
72
85
|
// Forward data from client to server
|
|
73
86
|
clientSocket.on('data', (data) => {
|
|
@@ -117,8 +130,7 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
|
|
|
117
130
|
});
|
|
118
131
|
});
|
|
119
132
|
|
|
120
|
-
// Set timeouts
|
|
121
|
-
const timeout = this.getTimeout();
|
|
133
|
+
// Set initial timeouts - they will be reset on each timeout event
|
|
122
134
|
clientSocket.setTimeout(timeout);
|
|
123
135
|
serverSocket.setTimeout(timeout);
|
|
124
136
|
}
|
|
@@ -128,7 +140,7 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
|
|
|
128
140
|
* @param req The HTTP request
|
|
129
141
|
* @param res The HTTP response
|
|
130
142
|
*/
|
|
131
|
-
public handleHttpRequest(
|
|
143
|
+
public handleHttpRequest(_req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
132
144
|
// HTTPS passthrough doesn't support HTTP requests
|
|
133
145
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
134
146
|
res.end('HTTP not supported for this domain');
|
|
@@ -112,7 +112,7 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
// Set up error handling with our cleanup utility
|
|
115
|
-
setupSocketHandlers(tlsSocket, handleClose, 'tls');
|
|
115
|
+
setupSocketHandlers(tlsSocket, handleClose, undefined, 'tls');
|
|
116
116
|
|
|
117
117
|
// Set timeout
|
|
118
118
|
const timeout = this.getTimeout();
|
|
@@ -167,7 +167,7 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
// Set up handlers for backend socket
|
|
170
|
-
setupSocketHandlers(backendSocket, newHandleClose, 'backend');
|
|
170
|
+
setupSocketHandlers(backendSocket, newHandleClose, undefined, 'backend');
|
|
171
171
|
|
|
172
172
|
backendSocket.on('error', (error) => {
|
|
173
173
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
@@ -106,7 +106,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
// Set up error handling with our cleanup utility
|
|
109
|
-
setupSocketHandlers(tlsSocket, handleClose, 'tls');
|
|
109
|
+
setupSocketHandlers(tlsSocket, handleClose, undefined, 'tls');
|
|
110
110
|
|
|
111
111
|
// Set timeout
|
|
112
112
|
const timeout = this.getTimeout();
|
|
@@ -151,7 +151,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|
|
151
151
|
});
|
|
152
152
|
|
|
153
153
|
// Set up handlers for backend socket
|
|
154
|
-
setupSocketHandlers(backendSocket, newHandleClose, 'backend');
|
|
154
|
+
setupSocketHandlers(backendSocket, newHandleClose, undefined, 'backend');
|
|
155
155
|
|
|
156
156
|
backendSocket.on('error', (error) => {
|
|
157
157
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
@@ -134,7 +134,7 @@ export class ConnectionPool {
|
|
|
134
134
|
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
|
135
135
|
connections.length > (this.options.connectionPoolSize || 50)) {
|
|
136
136
|
|
|
137
|
-
cleanupSocket(connection.socket, `pool-${host}-idle
|
|
137
|
+
cleanupSocket(connection.socket, `pool-${host}-idle`, { immediate: true }).catch(() => {});
|
|
138
138
|
|
|
139
139
|
connections.shift(); // Remove from pool
|
|
140
140
|
removed++;
|
|
@@ -164,7 +164,7 @@ export class ConnectionPool {
|
|
|
164
164
|
this.logger.debug(`Closing ${connections.length} connections to ${host}`);
|
|
165
165
|
|
|
166
166
|
for (const connection of connections) {
|
|
167
|
-
cleanupSocket(connection.socket, `pool-${host}-close
|
|
167
|
+
cleanupSocket(connection.socket, `pool-${host}-close`, { immediate: true }).catch(() => {});
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -520,9 +520,10 @@ export class HttpProxy implements IMetricsTracker {
|
|
|
520
520
|
this.webSocketHandler.shutdown();
|
|
521
521
|
|
|
522
522
|
// Close all tracked sockets
|
|
523
|
-
|
|
524
|
-
cleanupSocket(socket, 'http-proxy-stop')
|
|
525
|
-
|
|
523
|
+
const socketCleanupPromises = this.socketMap.getArray().map(socket =>
|
|
524
|
+
cleanupSocket(socket, 'http-proxy-stop', { immediate: true })
|
|
525
|
+
);
|
|
526
|
+
await Promise.all(socketCleanupPromises);
|
|
526
527
|
|
|
527
528
|
// Close all connection pool connections
|
|
528
529
|
this.connectionPool.closeAllConnections();
|
|
@@ -278,12 +278,37 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
// Handle socket cleanup
|
|
282
|
-
|
|
281
|
+
// Handle socket cleanup - check if sockets are still active
|
|
282
|
+
const cleanupPromises: Promise<void>[] = [];
|
|
283
|
+
|
|
284
|
+
if (record.incoming) {
|
|
285
|
+
if (!record.incoming.writable || record.incoming.destroyed) {
|
|
286
|
+
// Socket is not active, clean up immediately
|
|
287
|
+
cleanupPromises.push(cleanupSocket(record.incoming, `${record.id}-incoming`, { immediate: true }));
|
|
288
|
+
} else {
|
|
289
|
+
// Socket is still active, allow graceful cleanup
|
|
290
|
+
cleanupPromises.push(cleanupSocket(record.incoming, `${record.id}-incoming`, { allowDrain: true, gracePeriod: 5000 }));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
283
293
|
|
|
284
294
|
if (record.outgoing) {
|
|
285
|
-
|
|
295
|
+
if (!record.outgoing.writable || record.outgoing.destroyed) {
|
|
296
|
+
// Socket is not active, clean up immediately
|
|
297
|
+
cleanupPromises.push(cleanupSocket(record.outgoing, `${record.id}-outgoing`, { immediate: true }));
|
|
298
|
+
} else {
|
|
299
|
+
// Socket is still active, allow graceful cleanup
|
|
300
|
+
cleanupPromises.push(cleanupSocket(record.outgoing, `${record.id}-outgoing`, { allowDrain: true, gracePeriod: 5000 }));
|
|
301
|
+
}
|
|
286
302
|
}
|
|
303
|
+
|
|
304
|
+
// Wait for cleanup to complete
|
|
305
|
+
Promise.all(cleanupPromises).catch(err => {
|
|
306
|
+
logger.log('error', `Error during socket cleanup: ${err}`, {
|
|
307
|
+
connectionId: record.id,
|
|
308
|
+
error: err,
|
|
309
|
+
component: 'connection-manager'
|
|
310
|
+
});
|
|
311
|
+
});
|
|
287
312
|
|
|
288
313
|
// Clear pendingData to avoid memory leaks
|
|
289
314
|
record.pendingData = [];
|
|
@@ -484,19 +509,24 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
484
509
|
}
|
|
485
510
|
|
|
486
511
|
// Parity check: if outgoing socket closed and incoming remains active
|
|
512
|
+
// Increased from 2 minutes to 30 minutes for long-lived connections
|
|
487
513
|
if (
|
|
488
514
|
record.outgoingClosedTime &&
|
|
489
515
|
!record.incoming.destroyed &&
|
|
490
516
|
!record.connectionClosed &&
|
|
491
|
-
now - record.outgoingClosedTime >
|
|
517
|
+
now - record.outgoingClosedTime > 1800000 // 30 minutes
|
|
492
518
|
) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
519
|
+
// Only close if no data activity for 10 minutes
|
|
520
|
+
if (now - record.lastActivity > 600000) {
|
|
521
|
+
logger.log('warn', `Parity check failed after extended timeout: ${record.remoteIP}`, {
|
|
522
|
+
connectionId,
|
|
523
|
+
remoteIP: record.remoteIP,
|
|
524
|
+
timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
|
|
525
|
+
inactiveFor: plugins.prettyMs(now - record.lastActivity),
|
|
526
|
+
component: 'connection-manager'
|
|
527
|
+
});
|
|
528
|
+
this.cleanupConnection(record, 'parity_check');
|
|
529
|
+
}
|
|
500
530
|
}
|
|
501
531
|
}
|
|
502
532
|
}
|
|
@@ -537,13 +567,18 @@ export class ConnectionManager extends LifecycleComponent {
|
|
|
537
567
|
}
|
|
538
568
|
|
|
539
569
|
// Immediate destruction using socket-utils
|
|
570
|
+
const shutdownPromises: Promise<void>[] = [];
|
|
571
|
+
|
|
540
572
|
if (record.incoming) {
|
|
541
|
-
cleanupSocket(record.incoming, `${record.id}-incoming-shutdown
|
|
573
|
+
shutdownPromises.push(cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`, { immediate: true }));
|
|
542
574
|
}
|
|
543
575
|
|
|
544
576
|
if (record.outgoing) {
|
|
545
|
-
cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown
|
|
577
|
+
shutdownPromises.push(cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`, { immediate: true }));
|
|
546
578
|
}
|
|
579
|
+
|
|
580
|
+
// Don't wait for shutdown cleanup in this batch processing
|
|
581
|
+
Promise.all(shutdownPromises).catch(() => {});
|
|
547
582
|
} catch (err) {
|
|
548
583
|
logger.log('error', `Error during connection cleanup: ${err}`, {
|
|
549
584
|
connectionId: record.id,
|
|
@@ -65,7 +65,7 @@ export class PortManager {
|
|
|
65
65
|
const server = plugins.net.createServer((socket) => {
|
|
66
66
|
// Check if shutting down
|
|
67
67
|
if (this.isShuttingDown) {
|
|
68
|
-
cleanupSocket(socket, 'port-manager-shutdown');
|
|
68
|
+
cleanupSocket(socket, 'port-manager-shutdown', { immediate: true });
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -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 } from '../../core/utils/socket-utils.js';
|
|
12
|
+
import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers } 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
|
|
@@ -84,7 +84,7 @@ export class RouteConnectionHandler {
|
|
|
84
84
|
const ipValidation = this.securityManager.validateIP(remoteIP);
|
|
85
85
|
if (!ipValidation.allowed) {
|
|
86
86
|
logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
|
|
87
|
-
cleanupSocket(socket, `rejected-${ipValidation.reason}
|
|
87
|
+
cleanupSocket(socket, `rejected-${ipValidation.reason}`, { immediate: true });
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -1110,9 +1110,8 @@ export class RouteConnectionHandler {
|
|
|
1110
1110
|
// Setup improved error handling for outgoing connection
|
|
1111
1111
|
this.setupOutgoingErrorHandler(connectionId, targetSocket, record, socket, finalTargetHost, finalTargetPort);
|
|
1112
1112
|
|
|
1113
|
-
//
|
|
1114
|
-
|
|
1115
|
-
socket.on('close', this.connectionManager.handleClose('incoming', record));
|
|
1113
|
+
// Note: Close handlers are managed by independent socket handlers above
|
|
1114
|
+
// We don't register handleClose here to avoid bilateral cleanup
|
|
1116
1115
|
|
|
1117
1116
|
// Setup error handlers for incoming socket
|
|
1118
1117
|
socket.on('error', this.connectionManager.handleError('incoming', record));
|
|
@@ -1225,14 +1224,64 @@ export class RouteConnectionHandler {
|
|
|
1225
1224
|
record.pendingDataSize = 0;
|
|
1226
1225
|
}
|
|
1227
1226
|
|
|
1228
|
-
//
|
|
1229
|
-
|
|
1230
|
-
|
|
1227
|
+
// Set up independent socket handlers for half-open connection support
|
|
1228
|
+
const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
|
|
1229
|
+
socket,
|
|
1230
|
+
targetSocket,
|
|
1231
|
+
(reason) => {
|
|
1232
|
+
this.connectionManager.initiateCleanupOnce(record, reason);
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1231
1235
|
|
|
1232
|
-
//
|
|
1236
|
+
// Setup socket handlers with custom timeout handling
|
|
1237
|
+
setupSocketHandlers(socket, cleanupClient, (sock) => {
|
|
1238
|
+
// Don't close on timeout for keep-alive connections
|
|
1239
|
+
if (record.hasKeepAlive) {
|
|
1240
|
+
sock.setTimeout(this.settings.socketTimeout || 3600000);
|
|
1241
|
+
}
|
|
1242
|
+
}, 'client');
|
|
1243
|
+
|
|
1244
|
+
setupSocketHandlers(targetSocket, cleanupServer, (sock) => {
|
|
1245
|
+
// Don't close on timeout for keep-alive connections
|
|
1246
|
+
if (record.hasKeepAlive) {
|
|
1247
|
+
sock.setTimeout(this.settings.socketTimeout || 3600000);
|
|
1248
|
+
}
|
|
1249
|
+
}, 'server');
|
|
1250
|
+
|
|
1251
|
+
// Forward data from client to target with backpressure handling
|
|
1233
1252
|
socket.on('data', (chunk: Buffer) => {
|
|
1234
1253
|
record.bytesReceived += chunk.length;
|
|
1235
1254
|
this.timeoutManager.updateActivity(record);
|
|
1255
|
+
|
|
1256
|
+
if (targetSocket.writable) {
|
|
1257
|
+
const flushed = targetSocket.write(chunk);
|
|
1258
|
+
|
|
1259
|
+
// Handle backpressure
|
|
1260
|
+
if (!flushed) {
|
|
1261
|
+
socket.pause();
|
|
1262
|
+
targetSocket.once('drain', () => {
|
|
1263
|
+
socket.resume();
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// Forward data from target to client with backpressure handling
|
|
1270
|
+
targetSocket.on('data', (chunk: Buffer) => {
|
|
1271
|
+
record.bytesSent += chunk.length;
|
|
1272
|
+
this.timeoutManager.updateActivity(record);
|
|
1273
|
+
|
|
1274
|
+
if (socket.writable) {
|
|
1275
|
+
const flushed = socket.write(chunk);
|
|
1276
|
+
|
|
1277
|
+
// Handle backpressure
|
|
1278
|
+
if (!flushed) {
|
|
1279
|
+
targetSocket.pause();
|
|
1280
|
+
socket.once('drain', () => {
|
|
1281
|
+
targetSocket.resume();
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1236
1285
|
});
|
|
1237
1286
|
|
|
1238
1287
|
// Log successful connection
|