Compare commits

...

6 Commits

Author SHA1 Message Date
265b80ee04 19.5.7
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 14m26s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-01 08:09:39 +00:00
726d40b9a5 feat(lifecycle-component): enhance lifecycle management with unref support for timers and event listeners
fix(lifecycle-component): store actual event handler for proper cleanup
chore(meta): update certificate dates in meta.json
2025-06-01 08:09:29 +00:00
cacc88797a 19.5.6
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 17m22s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-01 08:03:39 +00:00
bed1a76537 refactor(socket-utils): replace direct socket cleanup with centralized cleanupSocket utility across connection management 2025-06-01 08:02:32 +00:00
eb2e67fecc feat(socket-utils): implement socket cleanup utilities and enhance socket handling in forwarding handlers 2025-06-01 07:51:20 +00:00
c7c325a7d8 fix(tests): update AcmeStateManager tests to use socket-handler for challenge routes
fix(tests): enhance non-TLS connection detection with range support in HttpProxy tests
2025-06-01 07:06:11 +00:00
21 changed files with 359 additions and 265 deletions

View File

@ -1,5 +1,5 @@
{
"expiryDate": "2025-08-29T18:29:48.329Z",
"issueDate": "2025-05-31T18:29:48.329Z",
"savedAt": "2025-05-31T18:29:48.330Z"
"expiryDate": "2025-08-30T08:04:36.897Z",
"issueDate": "2025-06-01T08:04:36.897Z",
"savedAt": "2025-06-01T08:04:36.897Z"
}

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "19.5.5",
"version": "19.5.7",
"private": false,
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
"main": "dist_ts/index.js",

View File

@ -249,4 +249,4 @@ tap.test('should not create timers when shutting down', async () => {
expect(intervalFired).toBeFalse();
});
tap.start();
export default tap.start();

View File

@ -13,8 +13,11 @@ tap.test('AcmeStateManager should track challenge routes correctly', async (tool
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static',
handler: async () => ({ status: 200, body: 'challenge' })
type: 'socket-handler',
socketHandler: async (socket, context) => {
// Mock handler that would write the challenge response
socket.end('challenge response');
}
}
};
@ -46,7 +49,7 @@ tap.test('AcmeStateManager should track port allocations', async (tools) => {
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static'
type: 'socket-handler'
}
};
@ -58,7 +61,7 @@ tap.test('AcmeStateManager should track port allocations', async (tools) => {
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static'
type: 'socket-handler'
}
};
@ -97,7 +100,7 @@ tap.test('AcmeStateManager should select primary route by priority', async (tool
ports: 80
},
action: {
type: 'static'
type: 'socket-handler'
}
};
@ -108,7 +111,7 @@ tap.test('AcmeStateManager should select primary route by priority', async (tool
ports: 80
},
action: {
type: 'static'
type: 'socket-handler'
}
};
@ -119,7 +122,7 @@ tap.test('AcmeStateManager should select primary route by priority', async (tool
ports: 80
},
action: {
type: 'static'
type: 'socket-handler'
}
};
@ -149,7 +152,7 @@ tap.test('AcmeStateManager should handle clear operation', async (tools) => {
ports: [80, 443]
},
action: {
type: 'static'
type: 'socket-handler'
}
};
@ -159,7 +162,7 @@ tap.test('AcmeStateManager should handle clear operation', async (tools) => {
ports: 8080
},
action: {
type: 'static'
type: 'socket-handler'
}
};

View File

@ -57,7 +57,14 @@ tap.test('should detect and forward non-TLS connections on useHttpProxy ports',
getAllRoutes: () => mockSettings.routes,
getRoutesForPort: (port: number) => mockSettings.routes.filter(r => {
const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports];
return ports.includes(port);
return ports.some(p => {
if (typeof p === 'number') {
return p === port;
} else if (p && typeof p === 'object' && 'from' in p && 'to' in p) {
return port >= p.from && port <= p.to;
}
return false;
});
})
};
@ -101,6 +108,8 @@ tap.test('should detect and forward non-TLS connections on useHttpProxy ports',
resume: () => {},
removeListener: function() { return this; },
emit: () => {},
setNoDelay: () => {},
setKeepAlive: () => {},
_dataHandler: null as any
} as any;
@ -176,7 +185,14 @@ tap.test('should handle TLS connections normally', async (tapTest) => {
getAllRoutes: () => mockSettings.routes,
getRoutesForPort: (port: number) => mockSettings.routes.filter(r => {
const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports];
return ports.includes(port);
return ports.some(p => {
if (typeof p === 'number') {
return p === port;
} else if (p && typeof p === 'object' && 'from' in p && 'to' in p) {
return port >= p.from && port <= p.to;
}
return false;
});
})
};
@ -211,6 +227,8 @@ tap.test('should handle TLS connections normally', async (tapTest) => {
resume: () => {},
removeListener: function() { return this; },
emit: () => {},
setNoDelay: () => {},
setKeepAlive: () => {},
_dataHandler: null as any
} as any;

View File

@ -26,7 +26,6 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy
proxy.settings.enableDetailedLogging = true;
// Override the HttpProxy initialization to avoid actual HttpProxy setup
const mockHttpProxy = { available: true };
proxy['httpProxyBridge'].initialize = async () => {
console.log('Mock: HttpProxyBridge initialized');
};
@ -49,11 +48,7 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy
args[1].end(); // socket.end()
};
const originalGetHttpProxy = proxy['httpProxyBridge'].getHttpProxy;
proxy['httpProxyBridge'].getHttpProxy = () => {
console.log('Mock: getHttpProxy called, returning:', mockHttpProxy);
return mockHttpProxy;
};
// No need to mock getHttpProxy - the bridge already handles HttpProxy availability
// Make a connection to port 8080
const client = new net.Socket();

View File

@ -591,13 +591,6 @@ tap.test('cleanup', async () => {
// Exit handler removed to prevent interference with test cleanup
// Add a post-hook to force exit after tap completion
tap.test('teardown', async () => {
// Force exit after all tests complete
setTimeout(() => {
console.log('[TEST] Force exit after tap completion');
process.exit(0);
}, 1000);
});
// Teardown test removed - let tap handle proper cleanup
export default tap.start();

View File

@ -403,7 +403,12 @@ export class EnhancedConnectionPool<T> extends LifecycleComponent {
const startTime = Date.now();
while (this.activeConnections.size > 0 && Date.now() - startTime < timeout) {
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => {
const timer = setTimeout(resolve, 100);
if (typeof timer.unref === 'function') {
timer.unref();
}
});
}
// Destroy all connections

View File

@ -16,3 +16,4 @@ export * from './fs-utils.js';
export * from './lifecycle-component.js';
export * from './binary-heap.js';
export * from './enhanced-connection-pool.js';
export * from './socket-utils.js';

View File

@ -9,6 +9,7 @@ export abstract class LifecycleComponent {
target: any;
event: string;
handler: Function;
actualHandler?: Function; // The actual handler registered (may be wrapped)
once?: boolean;
}> = [];
private childComponents: Set<LifecycleComponent> = new Set();
@ -21,7 +22,11 @@ export abstract class LifecycleComponent {
protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout {
if (this.isShuttingDown) {
// Return a dummy timer if shutting down
return setTimeout(() => {}, 0);
const dummyTimer = setTimeout(() => {}, 0);
if (typeof dummyTimer.unref === 'function') {
dummyTimer.unref();
}
return dummyTimer;
}
const wrappedHandler = () => {
@ -33,6 +38,12 @@ export abstract class LifecycleComponent {
const timer = setTimeout(wrappedHandler, timeout);
this.timers.add(timer);
// Allow process to exit even with timer
if (typeof timer.unref === 'function') {
timer.unref();
}
return timer;
}
@ -42,7 +53,12 @@ export abstract class LifecycleComponent {
protected setInterval(handler: Function, interval: number): NodeJS.Timeout {
if (this.isShuttingDown) {
// Return a dummy timer if shutting down
return setInterval(() => {}, interval);
const dummyTimer = setInterval(() => {}, interval);
if (typeof dummyTimer.unref === 'function') {
dummyTimer.unref();
}
clearInterval(dummyTimer); // Clear immediately since we don't need it
return dummyTimer;
}
const wrappedHandler = () => {
@ -121,11 +137,12 @@ export abstract class LifecycleComponent {
throw new Error('Target must support on() or addEventListener()');
}
// Store the original handler in our tracking (not the wrapped one)
// Store both the original handler and the actual handler registered
this.listeners.push({
target,
event,
handler,
actualHandler, // The handler that was actually registered (may be wrapped)
once: options?.once
});
}
@ -208,12 +225,15 @@ export abstract class LifecycleComponent {
this.intervals.clear();
// Remove all event listeners
for (const { target, event, handler } of this.listeners) {
for (const { target, event, handler, actualHandler } of this.listeners) {
// Use actualHandler if available (for wrapped handlers), otherwise use the original handler
const handlerToRemove = actualHandler || handler;
// All listeners need to be removed, including 'once' listeners that might not have fired
if (typeof target.removeListener === 'function') {
target.removeListener(event, handler);
target.removeListener(event, handlerToRemove);
} else if (typeof target.removeEventListener === 'function') {
target.removeEventListener(event, handler);
target.removeEventListener(event, handlerToRemove);
}
}
this.listeners = [];

View File

@ -0,0 +1,96 @@
import * as plugins from '../../plugins.js';
/**
* Safely cleanup a socket by removing all listeners and destroying it
* @param socket The socket to cleanup
* @param socketName Optional name for logging
*/
export function cleanupSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket | null, socketName?: string): void {
if (!socket) return;
try {
// Remove all event listeners
socket.removeAllListeners();
// Unpipe any streams
socket.unpipe();
// Destroy if not already destroyed
if (!socket.destroyed) {
socket.destroy();
}
} catch (err) {
console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
}
}
/**
* Create a cleanup handler for paired sockets (client and server)
* @param clientSocket The client socket
* @param serverSocket The server socket (optional)
* @param onCleanup Optional callback when cleanup is done
* @returns A cleanup function that can be called multiple times safely
*/
export function createSocketCleanupHandler(
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
serverSocket?: plugins.net.Socket | plugins.tls.TLSSocket | null,
onCleanup?: (reason: string) => void
): (reason: string) => void {
let cleanedUp = false;
return (reason: string) => {
if (cleanedUp) return;
cleanedUp = true;
// Cleanup both sockets
cleanupSocket(clientSocket, 'client');
if (serverSocket) {
cleanupSocket(serverSocket, 'server');
}
// Call cleanup callback if provided
if (onCleanup) {
onCleanup(reason);
}
};
}
/**
* Setup socket error and close handlers with proper cleanup
* @param socket The socket to setup handlers for
* @param handleClose The cleanup function to call
* @param errorPrefix Optional prefix for error messages
*/
export function setupSocketHandlers(
socket: plugins.net.Socket | plugins.tls.TLSSocket,
handleClose: (reason: string) => void,
errorPrefix?: string
): void {
socket.on('error', (error) => {
const prefix = errorPrefix || 'Socket';
handleClose(`${prefix}_error: ${error.message}`);
});
socket.on('close', () => {
const prefix = errorPrefix || 'socket';
handleClose(`${prefix}_closed`);
});
socket.on('timeout', () => {
const prefix = errorPrefix || 'socket';
handleClose(`${prefix}_timeout`);
});
}
/**
* Pipe two sockets together with proper cleanup on either end
* @param socket1 First socket
* @param socket2 Second socket
*/
export function pipeSockets(
socket1: plugins.net.Socket | plugins.tls.TLSSocket,
socket2: plugins.net.Socket | plugins.tls.TLSSocket
): void {
socket1.pipe(socket2);
socket2.pipe(socket1);
}

View File

@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { setupSocketHandlers } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTP-only forwarding
@ -40,12 +41,15 @@ export class HttpForwardingHandler extends ForwardingHandler {
const remoteAddress = socket.remoteAddress || 'unknown';
const localPort = socket.localPort || 80;
socket.on('close', (hadError) => {
// Set up socket handlers with proper cleanup
const handleClose = (reason: string) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
hadError
reason
});
});
};
setupSocketHandlers(socket, handleClose, 'http');
socket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {

View File

@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { createSocketCleanupHandler, setupSocketHandlers, pipeSockets } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTPS passthrough (SNI forwarding without termination)
@ -50,36 +51,24 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
// Create a connection to the target server
const serverSocket = plugins.net.connect(target.port, target.host);
// Handle errors on the server socket
serverSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Target connection error: ${error.message}`
});
// Close the client socket if it's still open
if (!clientSocket.destroyed) {
clientSocket.destroy();
}
});
// Handle errors on the client socket
clientSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Client connection error: ${error.message}`
});
// Close the server socket if it's still open
if (!serverSocket.destroyed) {
serverSocket.destroy();
}
});
// Track data transfer for logging
let bytesSent = 0;
let bytesReceived = 0;
// Create cleanup handler with our utility
const handleClose = createSocketCleanupHandler(clientSocket, serverSocket, (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
bytesSent,
bytesReceived,
reason
});
});
// Setup error and close handlers for both sockets
setupSocketHandlers(serverSocket, handleClose, 'server');
setupSocketHandlers(clientSocket, handleClose, 'client');
// Forward data from client to server
clientSocket.on('data', (data) => {
bytesSent += data.length;
@ -128,48 +117,10 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
});
});
// Handle connection close
const handleClose = () => {
if (!clientSocket.destroyed) {
clientSocket.destroy();
}
if (!serverSocket.destroyed) {
serverSocket.destroy();
}
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
bytesSent,
bytesReceived
});
};
// Set up close handlers
clientSocket.on('close', handleClose);
serverSocket.on('close', handleClose);
// Set timeouts
const timeout = this.getTimeout();
clientSocket.setTimeout(timeout);
serverSocket.setTimeout(timeout);
// Handle timeouts
clientSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'Client connection timeout'
});
handleClose();
});
serverSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'Server connection timeout'
});
handleClose();
});
}
/**

View File

@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { createSocketCleanupHandler, setupSocketHandlers } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTPS termination with HTTP backend
@ -95,62 +96,24 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
tls: true
});
// Handle TLS errors
tlsSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `TLS error: ${error.message}`
});
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
});
// The TLS socket will now emit HTTP traffic that can be processed
// In a real implementation, we would create an HTTP parser and handle
// the requests here, but for simplicity, we'll just log the data
// Variables to track connections
let backendSocket: plugins.net.Socket | null = null;
let dataBuffer = Buffer.alloc(0);
let connectionEstablished = false;
tlsSocket.on('data', (data) => {
// Append to buffer
dataBuffer = Buffer.concat([dataBuffer, data]);
// Very basic HTTP parsing - in a real implementation, use http-parser
if (dataBuffer.includes(Buffer.from('\r\n\r\n'))) {
const target = this.getTargetFromConfig();
// Simple example: forward the data to an HTTP server
const socket = plugins.net.connect(target.port, target.host, () => {
socket.write(dataBuffer);
dataBuffer = Buffer.alloc(0);
// Set up bidirectional data flow
tlsSocket.pipe(socket);
socket.pipe(tlsSocket);
});
socket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Target connection error: ${error.message}`
});
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
});
}
});
// Handle close
tlsSocket.on('close', () => {
// Create cleanup handler for all sockets
const handleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress
remoteAddress,
reason
});
dataBuffer = Buffer.alloc(0);
connectionEstablished = false;
});
// Set up error handling with our cleanup utility
setupSocketHandlers(tlsSocket, handleClose, 'tls');
// Set timeout
const timeout = this.getTimeout();
tlsSocket.setTimeout(timeout);
@ -160,9 +123,58 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
remoteAddress,
error: 'TLS connection timeout'
});
handleClose('timeout');
});
// Handle TLS data
tlsSocket.on('data', (data) => {
// If backend connection already established, just forward the data
if (connectionEstablished && backendSocket && !backendSocket.destroyed) {
backendSocket.write(data);
return;
}
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
// Append to buffer
dataBuffer = Buffer.concat([dataBuffer, data]);
// Very basic HTTP parsing - in a real implementation, use http-parser
if (dataBuffer.includes(Buffer.from('\r\n\r\n')) && !connectionEstablished) {
const target = this.getTargetFromConfig();
// Create backend connection
backendSocket = plugins.net.connect(target.port, target.host, () => {
connectionEstablished = true;
// Send buffered data
if (dataBuffer.length > 0) {
backendSocket!.write(dataBuffer);
dataBuffer = Buffer.alloc(0);
}
// Set up bidirectional data flow
tlsSocket.pipe(backendSocket!);
backendSocket!.pipe(tlsSocket);
});
// Update the cleanup handler with the backend socket
const newHandleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
dataBuffer = Buffer.alloc(0);
connectionEstablished = false;
});
// Set up handlers for backend socket
setupSocketHandlers(backendSocket, newHandleClose, 'backend');
backendSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Target connection error: ${error.message}`
});
});
}
});
}

View File

@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import { ForwardingHandler } from './base-handler.js';
import type { IForwardConfig } from '../config/forwarding-types.js';
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
import { createSocketCleanupHandler, setupSocketHandlers } from '../../core/utils/socket-utils.js';
/**
* Handler for HTTPS termination with HTTPS backend
@ -93,28 +94,38 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
tls: true
});
// Handle TLS errors
tlsSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
// Variable to track backend socket
let backendSocket: plugins.tls.TLSSocket | null = null;
// Create cleanup handler for both sockets
const handleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
error: `TLS error: ${error.message}`
reason
});
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
});
// The TLS socket will now emit HTTP traffic that can be processed
// In a real implementation, we would create an HTTP parser and handle
// the requests here, but for simplicity, we'll just forward the data
// Set up error handling with our cleanup utility
setupSocketHandlers(tlsSocket, handleClose, 'tls');
// Set timeout
const timeout = this.getTimeout();
tlsSocket.setTimeout(timeout);
tlsSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'TLS connection timeout'
});
handleClose('timeout');
});
// Get the target from configuration
const target = this.getTargetFromConfig();
// Set up the connection to the HTTPS backend
const connectToBackend = () => {
const backendSocket = plugins.tls.connect({
backendSocket = plugins.tls.connect({
host: target.host,
port: target.port,
// In a real implementation, we would configure TLS options
@ -127,30 +138,29 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
});
// Set up bidirectional data flow
tlsSocket.pipe(backendSocket);
backendSocket.pipe(tlsSocket);
tlsSocket.pipe(backendSocket!);
backendSocket!.pipe(tlsSocket);
});
// Update the cleanup handler with the backend socket
const newHandleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress,
reason
});
});
// Set up handlers for backend socket
setupSocketHandlers(backendSocket, newHandleClose, 'backend');
backendSocket.on('error', (error) => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: `Backend connection error: ${error.message}`
});
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
});
// Handle close
backendSocket.on('close', () => {
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
});
// Set timeout
const timeout = this.getTimeout();
// Set timeout for backend socket
backendSocket.setTimeout(timeout);
backendSocket.on('timeout', () => {
@ -158,10 +168,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
remoteAddress,
error: 'Backend connection timeout'
});
if (!backendSocket.destroyed) {
backendSocket.destroy();
}
newHandleClose('backend_timeout');
});
};
@ -169,28 +176,6 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
tlsSocket.on('secure', () => {
connectToBackend();
});
// Handle close
tlsSocket.on('close', () => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
remoteAddress
});
});
// Set timeout
const timeout = this.getTimeout();
tlsSocket.setTimeout(timeout);
tlsSocket.on('timeout', () => {
this.emit(ForwardingHandlerEvents.ERROR, {
remoteAddress,
error: 'TLS connection timeout'
});
if (!tlsSocket.destroyed) {
tlsSocket.destroy();
}
});
}
/**

View File

@ -1,5 +1,6 @@
import * as plugins from '../../plugins.js';
import { type IHttpProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './models/types.js';
import { cleanupSocket } from '../../core/utils/socket-utils.js';
/**
* Manages a pool of backend connections for efficient reuse
@ -133,14 +134,7 @@ export class ConnectionPool {
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
connections.length > (this.options.connectionPoolSize || 50)) {
try {
if (!connection.socket.destroyed) {
connection.socket.end();
connection.socket.destroy();
}
} catch (err) {
this.logger.error(`Error destroying pooled connection to ${host}`, err);
}
cleanupSocket(connection.socket, `pool-${host}-idle`);
connections.shift(); // Remove from pool
removed++;
@ -170,14 +164,7 @@ export class ConnectionPool {
this.logger.debug(`Closing ${connections.length} connections to ${host}`);
for (const connection of connections) {
try {
if (!connection.socket.destroyed) {
connection.socket.end();
connection.socket.destroy();
}
} catch (error) {
this.logger.error(`Error closing connection to ${host}:`, error);
}
cleanupSocket(connection.socket, `pool-${host}-close`);
}
}

View File

@ -18,6 +18,7 @@ import { RequestHandler, type IMetricsTracker } from './request-handler.js';
import { WebSocketHandler } from './websocket-handler.js';
import { ProxyRouter } from '../../routing/router/index.js';
import { RouteRouter } from '../../routing/router/route-router.js';
import { cleanupSocket } from '../../core/utils/socket-utils.js';
import { FunctionCache } from './function-cache.js';
/**
@ -520,11 +521,7 @@ export class HttpProxy implements IMetricsTracker {
// Close all tracked sockets
for (const socket of this.socketMap.getArray()) {
try {
socket.destroy();
} catch (error) {
this.logger.error('Error destroying socket', error);
}
cleanupSocket(socket, 'http-proxy-stop');
}
// Close all connection pool connections

View File

@ -4,6 +4,7 @@ import { SecurityManager } from './security-manager.js';
import { TimeoutManager } from './timeout-manager.js';
import { logger } from '../../core/utils/logger.js';
import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
import { cleanupSocket } from '../../core/utils/socket-utils.js';
/**
* Manages connection lifecycle, tracking, and cleanup with performance optimizations
@ -278,10 +279,10 @@ export class ConnectionManager extends LifecycleComponent {
}
// Handle socket cleanup without delay
this.cleanupSocketImmediate(record, 'incoming', record.incoming);
cleanupSocket(record.incoming, `${record.id}-incoming`);
if (record.outgoing) {
this.cleanupSocketImmediate(record, 'outgoing', record.outgoing);
cleanupSocket(record.outgoing, `${record.id}-outgoing`);
}
// Clear pendingData to avoid memory leaks
@ -313,23 +314,6 @@ export class ConnectionManager extends LifecycleComponent {
}
}
/**
* Helper method to clean up a socket immediately
*/
private cleanupSocketImmediate(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
try {
if (!socket.destroyed) {
socket.destroy();
}
} catch (err) {
logger.log('error', `Error destroying ${side} socket: ${err}`, {
connectionId: record.id,
side,
error: err,
component: 'connection-manager'
});
}
}
/**
* Creates a generic error handler for incoming or outgoing sockets
@ -552,19 +536,13 @@ export class ConnectionManager extends LifecycleComponent {
record.cleanupTimer = undefined;
}
// Immediate destruction
// Immediate destruction using socket-utils
if (record.incoming) {
record.incoming.removeAllListeners();
if (!record.incoming.destroyed) {
record.incoming.destroy();
}
cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`);
}
if (record.outgoing) {
record.outgoing.removeAllListeners();
if (!record.outgoing.destroyed) {
record.outgoing.destroy();
}
cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`);
}
} catch (err) {
logger.log('error', `Error during connection cleanup: ${err}`, {

View File

@ -128,10 +128,24 @@ export class HttpProxyBridge {
proxySocket.pipe(socket);
// Handle cleanup
let cleanedUp = false;
const cleanup = (reason: string) => {
if (cleanedUp) return;
cleanedUp = true;
// Remove all event listeners to prevent memory leaks
socket.removeAllListeners('end');
socket.removeAllListeners('error');
proxySocket.removeAllListeners('end');
proxySocket.removeAllListeners('error');
socket.unpipe(proxySocket);
proxySocket.unpipe(socket);
proxySocket.destroy();
if (!proxySocket.destroyed) {
proxySocket.destroy();
}
cleanupCallback(reason);
};

View File

@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
import type { ISmartProxyOptions } from './models/interfaces.js';
import { RouteConnectionHandler } from './route-connection-handler.js';
import { logger } from '../../core/utils/logger.js';
import { cleanupSocket } from '../../core/utils/socket-utils.js';
/**
* PortManager handles the dynamic creation and removal of port listeners
@ -64,8 +65,7 @@ export class PortManager {
const server = plugins.net.createServer((socket) => {
// Check if shutting down
if (this.isShuttingDown) {
socket.end();
socket.destroy();
cleanupSocket(socket, 'port-manager-shutdown');
return;
}

View File

@ -9,7 +9,7 @@ import { TlsManager } from './tls-manager.js';
import { HttpProxyBridge } from './http-proxy-bridge.js';
import { TimeoutManager } from './timeout-manager.js';
import { RouteManager } from './route-manager.js';
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
import { cleanupSocket } from '../../core/utils/socket-utils.js';
/**
* Handles new connection processing and setup logic with support for route-based configuration
@ -84,8 +84,7 @@ export class RouteConnectionHandler {
const ipValidation = this.securityManager.validateIP(remoteIP);
if (!ipValidation.allowed) {
logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
socket.end();
socket.destroy();
cleanupSocket(socket, `rejected-${ipValidation.reason}`);
return;
}
@ -822,6 +821,38 @@ export class RouteConnectionHandler {
return;
}
// Track event listeners added by the handler so we can clean them up
const originalOn = socket.on.bind(socket);
const originalOnce = socket.once.bind(socket);
const trackedListeners: Array<{event: string; listener: (...args: any[]) => void}> = [];
// Override socket.on to track listeners
socket.on = function(event: string, listener: (...args: any[]) => void) {
trackedListeners.push({event, listener});
return originalOn(event, listener);
} as any;
// Override socket.once to track listeners
socket.once = function(event: string, listener: (...args: any[]) => void) {
trackedListeners.push({event, listener});
return originalOnce(event, listener);
} as any;
// Set up automatic cleanup when socket closes
const cleanupHandler = () => {
// Remove all tracked listeners
for (const {event, listener} of trackedListeners) {
socket.removeListener(event, listener);
}
// Restore original methods
socket.on = originalOn;
socket.once = originalOnce;
};
// Listen for socket close to trigger cleanup
originalOnce('close', cleanupHandler);
originalOnce('error', cleanupHandler);
// Create route context for the handler
const routeContext = this.createRouteContext({
connectionId: record.id,
@ -855,6 +886,8 @@ export class RouteConnectionHandler {
error: error.message,
component: 'route-handler'
});
// Remove all event listeners before destroying to prevent memory leaks
socket.removeAllListeners();
if (!socket.destroyed) {
socket.destroy();
}
@ -875,6 +908,8 @@ export class RouteConnectionHandler {
error: error.message,
component: 'route-handler'
});
// Remove all event listeners before destroying to prevent memory leaks
socket.removeAllListeners();
if (!socket.destroyed) {
socket.destroy();
}
@ -1229,7 +1264,7 @@ export class RouteConnectionHandler {
connectionId,
serverName,
connInfo,
(connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
(_connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
// Store the handler in the connection record so we can remove it during cleanup