Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
265b80ee04 | |||
726d40b9a5 | |||
cacc88797a | |||
bed1a76537 | |||
eb2e67fecc | |||
c7c325a7d8 |
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiryDate": "2025-08-29T18:29:48.329Z",
|
"expiryDate": "2025-08-30T08:04:36.897Z",
|
||||||
"issueDate": "2025-05-31T18:29:48.329Z",
|
"issueDate": "2025-06-01T08:04:36.897Z",
|
||||||
"savedAt": "2025-05-31T18:29:48.330Z"
|
"savedAt": "2025-06-01T08:04:36.897Z"
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "19.5.5",
|
"version": "19.5.7",
|
||||||
"private": false,
|
"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.",
|
"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",
|
"main": "dist_ts/index.js",
|
||||||
|
@ -249,4 +249,4 @@ tap.test('should not create timers when shutting down', async () => {
|
|||||||
expect(intervalFired).toBeFalse();
|
expect(intervalFired).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
@ -13,8 +13,11 @@ tap.test('AcmeStateManager should track challenge routes correctly', async (tool
|
|||||||
path: '/.well-known/acme-challenge/*'
|
path: '/.well-known/acme-challenge/*'
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static',
|
type: 'socket-handler',
|
||||||
handler: async () => ({ status: 200, body: 'challenge' })
|
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/*'
|
path: '/.well-known/acme-challenge/*'
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,7 +61,7 @@ tap.test('AcmeStateManager should track port allocations', async (tools) => {
|
|||||||
path: '/.well-known/acme-challenge/*'
|
path: '/.well-known/acme-challenge/*'
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,7 +100,7 @@ tap.test('AcmeStateManager should select primary route by priority', async (tool
|
|||||||
ports: 80
|
ports: 80
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,7 +111,7 @@ tap.test('AcmeStateManager should select primary route by priority', async (tool
|
|||||||
ports: 80
|
ports: 80
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +122,7 @@ tap.test('AcmeStateManager should select primary route by priority', async (tool
|
|||||||
ports: 80
|
ports: 80
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -149,7 +152,7 @@ tap.test('AcmeStateManager should handle clear operation', async (tools) => {
|
|||||||
ports: [80, 443]
|
ports: [80, 443]
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,7 +162,7 @@ tap.test('AcmeStateManager should handle clear operation', async (tools) => {
|
|||||||
ports: 8080
|
ports: 8080
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'static'
|
type: 'socket-handler'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,7 +57,14 @@ tap.test('should detect and forward non-TLS connections on useHttpProxy ports',
|
|||||||
getAllRoutes: () => mockSettings.routes,
|
getAllRoutes: () => mockSettings.routes,
|
||||||
getRoutesForPort: (port: number) => mockSettings.routes.filter(r => {
|
getRoutesForPort: (port: number) => mockSettings.routes.filter(r => {
|
||||||
const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports];
|
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: () => {},
|
resume: () => {},
|
||||||
removeListener: function() { return this; },
|
removeListener: function() { return this; },
|
||||||
emit: () => {},
|
emit: () => {},
|
||||||
|
setNoDelay: () => {},
|
||||||
|
setKeepAlive: () => {},
|
||||||
_dataHandler: null as any
|
_dataHandler: null as any
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
@ -176,7 +185,14 @@ tap.test('should handle TLS connections normally', async (tapTest) => {
|
|||||||
getAllRoutes: () => mockSettings.routes,
|
getAllRoutes: () => mockSettings.routes,
|
||||||
getRoutesForPort: (port: number) => mockSettings.routes.filter(r => {
|
getRoutesForPort: (port: number) => mockSettings.routes.filter(r => {
|
||||||
const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports];
|
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: () => {},
|
resume: () => {},
|
||||||
removeListener: function() { return this; },
|
removeListener: function() { return this; },
|
||||||
emit: () => {},
|
emit: () => {},
|
||||||
|
setNoDelay: () => {},
|
||||||
|
setKeepAlive: () => {},
|
||||||
_dataHandler: null as any
|
_dataHandler: null as any
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy
|
|||||||
proxy.settings.enableDetailedLogging = true;
|
proxy.settings.enableDetailedLogging = true;
|
||||||
|
|
||||||
// Override the HttpProxy initialization to avoid actual HttpProxy setup
|
// Override the HttpProxy initialization to avoid actual HttpProxy setup
|
||||||
const mockHttpProxy = { available: true };
|
|
||||||
proxy['httpProxyBridge'].initialize = async () => {
|
proxy['httpProxyBridge'].initialize = async () => {
|
||||||
console.log('Mock: HttpProxyBridge initialized');
|
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()
|
args[1].end(); // socket.end()
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalGetHttpProxy = proxy['httpProxyBridge'].getHttpProxy;
|
// No need to mock getHttpProxy - the bridge already handles HttpProxy availability
|
||||||
proxy['httpProxyBridge'].getHttpProxy = () => {
|
|
||||||
console.log('Mock: getHttpProxy called, returning:', mockHttpProxy);
|
|
||||||
return mockHttpProxy;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Make a connection to port 8080
|
// Make a connection to port 8080
|
||||||
const client = new net.Socket();
|
const client = new net.Socket();
|
||||||
|
@ -591,13 +591,6 @@ tap.test('cleanup', async () => {
|
|||||||
|
|
||||||
// Exit handler removed to prevent interference with test cleanup
|
// Exit handler removed to prevent interference with test cleanup
|
||||||
|
|
||||||
// Add a post-hook to force exit after tap completion
|
// Teardown test removed - let tap handle proper cleanup
|
||||||
tap.test('teardown', async () => {
|
|
||||||
// Force exit after all tests complete
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[TEST] Force exit after tap completion');
|
|
||||||
process.exit(0);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
@ -403,7 +403,12 @@ export class EnhancedConnectionPool<T> extends LifecycleComponent {
|
|||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
while (this.activeConnections.size > 0 && Date.now() - startTime < timeout) {
|
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
|
// Destroy all connections
|
||||||
|
@ -16,3 +16,4 @@ export * from './fs-utils.js';
|
|||||||
export * from './lifecycle-component.js';
|
export * from './lifecycle-component.js';
|
||||||
export * from './binary-heap.js';
|
export * from './binary-heap.js';
|
||||||
export * from './enhanced-connection-pool.js';
|
export * from './enhanced-connection-pool.js';
|
||||||
|
export * from './socket-utils.js';
|
||||||
|
@ -9,6 +9,7 @@ export abstract class LifecycleComponent {
|
|||||||
target: any;
|
target: any;
|
||||||
event: string;
|
event: string;
|
||||||
handler: Function;
|
handler: Function;
|
||||||
|
actualHandler?: Function; // The actual handler registered (may be wrapped)
|
||||||
once?: boolean;
|
once?: boolean;
|
||||||
}> = [];
|
}> = [];
|
||||||
private childComponents: Set<LifecycleComponent> = new Set();
|
private childComponents: Set<LifecycleComponent> = new Set();
|
||||||
@ -21,7 +22,11 @@ export abstract class LifecycleComponent {
|
|||||||
protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout {
|
protected setTimeout(handler: Function, timeout: number): NodeJS.Timeout {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
// Return a dummy timer if shutting down
|
// 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 = () => {
|
const wrappedHandler = () => {
|
||||||
@ -33,6 +38,12 @@ export abstract class LifecycleComponent {
|
|||||||
|
|
||||||
const timer = setTimeout(wrappedHandler, timeout);
|
const timer = setTimeout(wrappedHandler, timeout);
|
||||||
this.timers.add(timer);
|
this.timers.add(timer);
|
||||||
|
|
||||||
|
// Allow process to exit even with timer
|
||||||
|
if (typeof timer.unref === 'function') {
|
||||||
|
timer.unref();
|
||||||
|
}
|
||||||
|
|
||||||
return timer;
|
return timer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +53,12 @@ export abstract class LifecycleComponent {
|
|||||||
protected setInterval(handler: Function, interval: number): NodeJS.Timeout {
|
protected setInterval(handler: Function, interval: number): NodeJS.Timeout {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
// Return a dummy timer if shutting down
|
// 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 = () => {
|
const wrappedHandler = () => {
|
||||||
@ -121,11 +137,12 @@ export abstract class LifecycleComponent {
|
|||||||
throw new Error('Target must support on() or addEventListener()');
|
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({
|
this.listeners.push({
|
||||||
target,
|
target,
|
||||||
event,
|
event,
|
||||||
handler,
|
handler,
|
||||||
|
actualHandler, // The handler that was actually registered (may be wrapped)
|
||||||
once: options?.once
|
once: options?.once
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -208,12 +225,15 @@ export abstract class LifecycleComponent {
|
|||||||
this.intervals.clear();
|
this.intervals.clear();
|
||||||
|
|
||||||
// Remove all event listeners
|
// 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
|
// All listeners need to be removed, including 'once' listeners that might not have fired
|
||||||
if (typeof target.removeListener === 'function') {
|
if (typeof target.removeListener === 'function') {
|
||||||
target.removeListener(event, handler);
|
target.removeListener(event, handlerToRemove);
|
||||||
} else if (typeof target.removeEventListener === 'function') {
|
} else if (typeof target.removeEventListener === 'function') {
|
||||||
target.removeEventListener(event, handler);
|
target.removeEventListener(event, handlerToRemove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.listeners = [];
|
this.listeners = [];
|
||||||
|
96
ts/core/utils/socket-utils.ts
Normal file
96
ts/core/utils/socket-utils.ts
Normal 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);
|
||||||
|
}
|
@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } 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
|
* Handler for HTTP-only forwarding
|
||||||
@ -40,12 +41,15 @@ export class HttpForwardingHandler extends ForwardingHandler {
|
|||||||
const remoteAddress = socket.remoteAddress || 'unknown';
|
const remoteAddress = socket.remoteAddress || 'unknown';
|
||||||
const localPort = socket.localPort || 80;
|
const localPort = socket.localPort || 80;
|
||||||
|
|
||||||
socket.on('close', (hadError) => {
|
// Set up socket handlers with proper cleanup
|
||||||
|
const handleClose = (reason: string) => {
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
hadError
|
reason
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setupSocketHandlers(socket, handleClose, 'http');
|
||||||
|
|
||||||
socket.on('error', (error) => {
|
socket.on('error', (error) => {
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||||
|
@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } 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)
|
* Handler for HTTPS passthrough (SNI forwarding without termination)
|
||||||
@ -50,36 +51,24 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
|
|||||||
// Create a connection to the target server
|
// Create a connection to the target server
|
||||||
const serverSocket = plugins.net.connect(target.port, target.host);
|
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
|
// Track data transfer for logging
|
||||||
let bytesSent = 0;
|
let bytesSent = 0;
|
||||||
let bytesReceived = 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
|
// Forward data from client to server
|
||||||
clientSocket.on('data', (data) => {
|
clientSocket.on('data', (data) => {
|
||||||
bytesSent += data.length;
|
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
|
// Set timeouts
|
||||||
const timeout = this.getTimeout();
|
const timeout = this.getTimeout();
|
||||||
clientSocket.setTimeout(timeout);
|
clientSocket.setTimeout(timeout);
|
||||||
serverSocket.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();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } 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
|
* Handler for HTTPS termination with HTTP backend
|
||||||
@ -95,62 +96,24 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
|||||||
tls: true
|
tls: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle TLS errors
|
// Variables to track connections
|
||||||
tlsSocket.on('error', (error) => {
|
let backendSocket: plugins.net.Socket | null = null;
|
||||||
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
|
|
||||||
|
|
||||||
let dataBuffer = Buffer.alloc(0);
|
let dataBuffer = Buffer.alloc(0);
|
||||||
|
let connectionEstablished = false;
|
||||||
|
|
||||||
tlsSocket.on('data', (data) => {
|
// Create cleanup handler for all sockets
|
||||||
// Append to buffer
|
const handleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
|
||||||
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', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
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
|
// Set timeout
|
||||||
const timeout = this.getTimeout();
|
const timeout = this.getTimeout();
|
||||||
tlsSocket.setTimeout(timeout);
|
tlsSocket.setTimeout(timeout);
|
||||||
@ -160,9 +123,58 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
|||||||
remoteAddress,
|
remoteAddress,
|
||||||
error: 'TLS connection timeout'
|
error: 'TLS connection timeout'
|
||||||
});
|
});
|
||||||
|
handleClose('timeout');
|
||||||
|
});
|
||||||
|
|
||||||
if (!tlsSocket.destroyed) {
|
// Handle TLS data
|
||||||
tlsSocket.destroy();
|
tlsSocket.on('data', (data) => {
|
||||||
|
// If backend connection already established, just forward the data
|
||||||
|
if (connectionEstablished && backendSocket && !backendSocket.destroyed) {
|
||||||
|
backendSocket.write(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}`
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } 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
|
* Handler for HTTPS termination with HTTPS backend
|
||||||
@ -93,28 +94,38 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|||||||
tls: true
|
tls: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle TLS errors
|
// Variable to track backend socket
|
||||||
tlsSocket.on('error', (error) => {
|
let backendSocket: plugins.tls.TLSSocket | null = null;
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: `TLS error: ${error.message}`
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tlsSocket.destroyed) {
|
// Create cleanup handler for both sockets
|
||||||
tlsSocket.destroy();
|
const handleClose = createSocketCleanupHandler(tlsSocket, backendSocket, (reason) => {
|
||||||
}
|
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
||||||
|
remoteAddress,
|
||||||
|
reason
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// The TLS socket will now emit HTTP traffic that can be processed
|
// Set up error handling with our cleanup utility
|
||||||
// In a real implementation, we would create an HTTP parser and handle
|
setupSocketHandlers(tlsSocket, handleClose, 'tls');
|
||||||
// the requests here, but for simplicity, we'll just forward the data
|
|
||||||
|
// 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
|
// Get the target from configuration
|
||||||
const target = this.getTargetFromConfig();
|
const target = this.getTargetFromConfig();
|
||||||
|
|
||||||
// Set up the connection to the HTTPS backend
|
// Set up the connection to the HTTPS backend
|
||||||
const connectToBackend = () => {
|
const connectToBackend = () => {
|
||||||
const backendSocket = plugins.tls.connect({
|
backendSocket = plugins.tls.connect({
|
||||||
host: target.host,
|
host: target.host,
|
||||||
port: target.port,
|
port: target.port,
|
||||||
// In a real implementation, we would configure TLS options
|
// In a real implementation, we would configure TLS options
|
||||||
@ -127,30 +138,29 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set up bidirectional data flow
|
// Set up bidirectional data flow
|
||||||
tlsSocket.pipe(backendSocket);
|
tlsSocket.pipe(backendSocket!);
|
||||||
backendSocket.pipe(tlsSocket);
|
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) => {
|
backendSocket.on('error', (error) => {
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
this.emit(ForwardingHandlerEvents.ERROR, {
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
error: `Backend connection error: ${error.message}`
|
error: `Backend connection error: ${error.message}`
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!tlsSocket.destroyed) {
|
|
||||||
tlsSocket.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle close
|
// Set timeout for backend socket
|
||||||
backendSocket.on('close', () => {
|
|
||||||
if (!tlsSocket.destroyed) {
|
|
||||||
tlsSocket.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set timeout
|
|
||||||
const timeout = this.getTimeout();
|
|
||||||
backendSocket.setTimeout(timeout);
|
backendSocket.setTimeout(timeout);
|
||||||
|
|
||||||
backendSocket.on('timeout', () => {
|
backendSocket.on('timeout', () => {
|
||||||
@ -158,10 +168,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|||||||
remoteAddress,
|
remoteAddress,
|
||||||
error: 'Backend connection timeout'
|
error: 'Backend connection timeout'
|
||||||
});
|
});
|
||||||
|
newHandleClose('backend_timeout');
|
||||||
if (!backendSocket.destroyed) {
|
|
||||||
backendSocket.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -169,28 +176,6 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|||||||
tlsSocket.on('secure', () => {
|
tlsSocket.on('secure', () => {
|
||||||
connectToBackend();
|
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { type IHttpProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './models/types.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
|
* Manages a pool of backend connections for efficient reuse
|
||||||
@ -133,14 +134,7 @@ export class ConnectionPool {
|
|||||||
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
||||||
connections.length > (this.options.connectionPoolSize || 50)) {
|
connections.length > (this.options.connectionPoolSize || 50)) {
|
||||||
|
|
||||||
try {
|
cleanupSocket(connection.socket, `pool-${host}-idle`);
|
||||||
if (!connection.socket.destroyed) {
|
|
||||||
connection.socket.end();
|
|
||||||
connection.socket.destroy();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`Error destroying pooled connection to ${host}`, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
connections.shift(); // Remove from pool
|
connections.shift(); // Remove from pool
|
||||||
removed++;
|
removed++;
|
||||||
@ -170,14 +164,7 @@ export class ConnectionPool {
|
|||||||
this.logger.debug(`Closing ${connections.length} connections to ${host}`);
|
this.logger.debug(`Closing ${connections.length} connections to ${host}`);
|
||||||
|
|
||||||
for (const connection of connections) {
|
for (const connection of connections) {
|
||||||
try {
|
cleanupSocket(connection.socket, `pool-${host}-close`);
|
||||||
if (!connection.socket.destroyed) {
|
|
||||||
connection.socket.end();
|
|
||||||
connection.socket.destroy();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error closing connection to ${host}:`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import { RequestHandler, type IMetricsTracker } from './request-handler.js';
|
|||||||
import { WebSocketHandler } from './websocket-handler.js';
|
import { WebSocketHandler } from './websocket-handler.js';
|
||||||
import { ProxyRouter } from '../../routing/router/index.js';
|
import { ProxyRouter } from '../../routing/router/index.js';
|
||||||
import { RouteRouter } from '../../routing/router/route-router.js';
|
import { RouteRouter } from '../../routing/router/route-router.js';
|
||||||
|
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
||||||
import { FunctionCache } from './function-cache.js';
|
import { FunctionCache } from './function-cache.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -520,11 +521,7 @@ export class HttpProxy implements IMetricsTracker {
|
|||||||
|
|
||||||
// Close all tracked sockets
|
// Close all tracked sockets
|
||||||
for (const socket of this.socketMap.getArray()) {
|
for (const socket of this.socketMap.getArray()) {
|
||||||
try {
|
cleanupSocket(socket, 'http-proxy-stop');
|
||||||
socket.destroy();
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Error destroying socket', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close all connection pool connections
|
// Close all connection pool connections
|
||||||
|
@ -4,6 +4,7 @@ import { SecurityManager } from './security-manager.js';
|
|||||||
import { TimeoutManager } from './timeout-manager.js';
|
import { TimeoutManager } from './timeout-manager.js';
|
||||||
import { logger } from '../../core/utils/logger.js';
|
import { logger } from '../../core/utils/logger.js';
|
||||||
import { LifecycleComponent } from '../../core/utils/lifecycle-component.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
|
* Manages connection lifecycle, tracking, and cleanup with performance optimizations
|
||||||
@ -278,10 +279,10 @@ export class ConnectionManager extends LifecycleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle socket cleanup without delay
|
// Handle socket cleanup without delay
|
||||||
this.cleanupSocketImmediate(record, 'incoming', record.incoming);
|
cleanupSocket(record.incoming, `${record.id}-incoming`);
|
||||||
|
|
||||||
if (record.outgoing) {
|
if (record.outgoing) {
|
||||||
this.cleanupSocketImmediate(record, 'outgoing', record.outgoing);
|
cleanupSocket(record.outgoing, `${record.id}-outgoing`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear pendingData to avoid memory leaks
|
// 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
|
* Creates a generic error handler for incoming or outgoing sockets
|
||||||
@ -552,19 +536,13 @@ export class ConnectionManager extends LifecycleComponent {
|
|||||||
record.cleanupTimer = undefined;
|
record.cleanupTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immediate destruction
|
// Immediate destruction using socket-utils
|
||||||
if (record.incoming) {
|
if (record.incoming) {
|
||||||
record.incoming.removeAllListeners();
|
cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`);
|
||||||
if (!record.incoming.destroyed) {
|
|
||||||
record.incoming.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record.outgoing) {
|
if (record.outgoing) {
|
||||||
record.outgoing.removeAllListeners();
|
cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`);
|
||||||
if (!record.outgoing.destroyed) {
|
|
||||||
record.outgoing.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log('error', `Error during connection cleanup: ${err}`, {
|
logger.log('error', `Error during connection cleanup: ${err}`, {
|
||||||
|
@ -128,10 +128,24 @@ export class HttpProxyBridge {
|
|||||||
proxySocket.pipe(socket);
|
proxySocket.pipe(socket);
|
||||||
|
|
||||||
// Handle cleanup
|
// Handle cleanup
|
||||||
|
let cleanedUp = false;
|
||||||
const cleanup = (reason: string) => {
|
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);
|
socket.unpipe(proxySocket);
|
||||||
proxySocket.unpipe(socket);
|
proxySocket.unpipe(socket);
|
||||||
proxySocket.destroy();
|
|
||||||
|
if (!proxySocket.destroyed) {
|
||||||
|
proxySocket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
cleanupCallback(reason);
|
cleanupCallback(reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import type { ISmartProxyOptions } from './models/interfaces.js';
|
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||||
import { RouteConnectionHandler } from './route-connection-handler.js';
|
import { RouteConnectionHandler } from './route-connection-handler.js';
|
||||||
import { logger } from '../../core/utils/logger.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
|
* PortManager handles the dynamic creation and removal of port listeners
|
||||||
@ -64,8 +65,7 @@ export class PortManager {
|
|||||||
const server = plugins.net.createServer((socket) => {
|
const server = plugins.net.createServer((socket) => {
|
||||||
// Check if shutting down
|
// Check if shutting down
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
socket.end();
|
cleanupSocket(socket, 'port-manager-shutdown');
|
||||||
socket.destroy();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { TlsManager } from './tls-manager.js';
|
|||||||
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
import { HttpProxyBridge } from './http-proxy-bridge.js';
|
||||||
import { TimeoutManager } from './timeout-manager.js';
|
import { TimeoutManager } from './timeout-manager.js';
|
||||||
import { RouteManager } from './route-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
|
* 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);
|
const ipValidation = this.securityManager.validateIP(remoteIP);
|
||||||
if (!ipValidation.allowed) {
|
if (!ipValidation.allowed) {
|
||||||
logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
|
logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
|
||||||
socket.end();
|
cleanupSocket(socket, `rejected-${ipValidation.reason}`);
|
||||||
socket.destroy();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -822,6 +821,38 @@ export class RouteConnectionHandler {
|
|||||||
return;
|
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
|
// Create route context for the handler
|
||||||
const routeContext = this.createRouteContext({
|
const routeContext = this.createRouteContext({
|
||||||
connectionId: record.id,
|
connectionId: record.id,
|
||||||
@ -855,6 +886,8 @@ export class RouteConnectionHandler {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
component: 'route-handler'
|
component: 'route-handler'
|
||||||
});
|
});
|
||||||
|
// Remove all event listeners before destroying to prevent memory leaks
|
||||||
|
socket.removeAllListeners();
|
||||||
if (!socket.destroyed) {
|
if (!socket.destroyed) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
@ -875,6 +908,8 @@ export class RouteConnectionHandler {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
component: 'route-handler'
|
component: 'route-handler'
|
||||||
});
|
});
|
||||||
|
// Remove all event listeners before destroying to prevent memory leaks
|
||||||
|
socket.removeAllListeners();
|
||||||
if (!socket.destroyed) {
|
if (!socket.destroyed) {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
@ -1229,7 +1264,7 @@ export class RouteConnectionHandler {
|
|||||||
connectionId,
|
connectionId,
|
||||||
serverName,
|
serverName,
|
||||||
connInfo,
|
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
|
// Store the handler in the connection record so we can remove it during cleanup
|
||||||
|
Reference in New Issue
Block a user