fix(port-manager, certificate-manager): Improve port binding and ACME challenge route integration in SmartProxy
This commit is contained in:
@ -1,6 +1,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';
|
||||
|
||||
/**
|
||||
* PortManager handles the dynamic creation and removal of port listeners
|
||||
@ -8,12 +9,17 @@ import { RouteConnectionHandler } from './route-connection-handler.js';
|
||||
* This class provides methods to add and remove listening ports at runtime,
|
||||
* allowing SmartProxy to adapt to configuration changes without requiring
|
||||
* a full restart.
|
||||
*
|
||||
* It includes a reference counting system to track how many routes are using
|
||||
* each port, so ports can be automatically released when they are no longer needed.
|
||||
*/
|
||||
export class PortManager {
|
||||
private servers: Map<number, plugins.net.Server> = new Map();
|
||||
private settings: ISmartProxyOptions;
|
||||
private routeConnectionHandler: RouteConnectionHandler;
|
||||
private isShuttingDown: boolean = false;
|
||||
// Track how many routes are using each port
|
||||
private portRefCounts: Map<number, number> = new Map();
|
||||
|
||||
/**
|
||||
* Create a new PortManager
|
||||
@ -38,10 +44,18 @@ export class PortManager {
|
||||
public async addPort(port: number): Promise<void> {
|
||||
// Check if we're already listening on this port
|
||||
if (this.servers.has(port)) {
|
||||
console.log(`PortManager: Already listening on port ${port}`);
|
||||
// Port is already bound, just increment the reference count
|
||||
this.incrementPortRefCount(port);
|
||||
logger.log('debug', `PortManager: Port ${port} is already bound by SmartProxy, reusing binding`, {
|
||||
port,
|
||||
component: 'port-manager'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize reference count for new port
|
||||
this.portRefCounts.set(port, 1);
|
||||
|
||||
// Create a server for this port
|
||||
const server = plugins.net.createServer((socket) => {
|
||||
// Check if shutting down
|
||||
@ -54,24 +68,56 @@ export class PortManager {
|
||||
// Delegate to route connection handler
|
||||
this.routeConnectionHandler.handleConnection(socket);
|
||||
}).on('error', (err: Error) => {
|
||||
console.log(`Server Error on port ${port}: ${err.message}`);
|
||||
logger.log('error', `Server Error on port ${port}: ${err.message}`, {
|
||||
port,
|
||||
error: err.message,
|
||||
component: 'port-manager'
|
||||
});
|
||||
});
|
||||
|
||||
// Start listening on the port
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
server.listen(port, () => {
|
||||
const isHttpProxyPort = this.settings.useHttpProxy?.includes(port);
|
||||
console.log(
|
||||
`SmartProxy -> OK: Now listening on port ${port}${
|
||||
isHttpProxyPort ? ' (HttpProxy forwarding enabled)' : ''
|
||||
}`
|
||||
);
|
||||
logger.log('info', `SmartProxy -> OK: Now listening on port ${port}${
|
||||
isHttpProxyPort ? ' (HttpProxy forwarding enabled)' : ''
|
||||
}`, {
|
||||
port,
|
||||
isHttpProxyPort: !!isHttpProxyPort,
|
||||
component: 'port-manager'
|
||||
});
|
||||
|
||||
// Store the server reference
|
||||
this.servers.set(port, server);
|
||||
resolve();
|
||||
}).on('error', (err) => {
|
||||
console.log(`Failed to listen on port ${port}: ${err.message}`);
|
||||
// Check if this is an external conflict
|
||||
const { isConflict, isExternal } = this.isPortConflict(err);
|
||||
|
||||
if (isConflict && !isExternal) {
|
||||
// This is an internal conflict (port already bound by SmartProxy)
|
||||
// This shouldn't normally happen because we check servers.has(port) above
|
||||
logger.log('warn', `Port ${port} binding conflict: already in use by SmartProxy`, {
|
||||
port,
|
||||
component: 'port-manager'
|
||||
});
|
||||
// Still increment reference count to maintain tracking
|
||||
this.incrementPortRefCount(port);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the error and propagate it
|
||||
logger.log('error', `Failed to listen on port ${port}: ${err.message}`, {
|
||||
port,
|
||||
error: err.message,
|
||||
code: (err as any).code,
|
||||
component: 'port-manager'
|
||||
});
|
||||
|
||||
// Clean up reference count since binding failed
|
||||
this.portRefCounts.delete(port);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
@ -84,10 +130,28 @@ export class PortManager {
|
||||
* @returns Promise that resolves when the server is closed
|
||||
*/
|
||||
public async removePort(port: number): Promise<void> {
|
||||
// Decrement the reference count first
|
||||
const newRefCount = this.decrementPortRefCount(port);
|
||||
|
||||
// If there are still references to this port, keep it open
|
||||
if (newRefCount > 0) {
|
||||
logger.log('debug', `PortManager: Port ${port} still has ${newRefCount} references, keeping open`, {
|
||||
port,
|
||||
refCount: newRefCount,
|
||||
component: 'port-manager'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the server for this port
|
||||
const server = this.servers.get(port);
|
||||
if (!server) {
|
||||
console.log(`PortManager: Not listening on port ${port}`);
|
||||
logger.log('warn', `PortManager: Not listening on port ${port}`, {
|
||||
port,
|
||||
component: 'port-manager'
|
||||
});
|
||||
// Ensure reference count is reset
|
||||
this.portRefCounts.delete(port);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -95,13 +159,21 @@ export class PortManager {
|
||||
return new Promise<void>((resolve) => {
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
console.log(`Error closing server on port ${port}: ${err.message}`);
|
||||
logger.log('error', `Error closing server on port ${port}: ${err.message}`, {
|
||||
port,
|
||||
error: err.message,
|
||||
component: 'port-manager'
|
||||
});
|
||||
} else {
|
||||
console.log(`SmartProxy -> Stopped listening on port ${port}`);
|
||||
logger.log('info', `SmartProxy -> Stopped listening on port ${port}`, {
|
||||
port,
|
||||
component: 'port-manager'
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the server reference
|
||||
// Remove the server reference and clean up reference counting
|
||||
this.servers.delete(port);
|
||||
this.portRefCounts.delete(port);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@ -192,4 +264,89 @@ export class PortManager {
|
||||
public getServers(): Map<number, plugins.net.Server> {
|
||||
return new Map(this.servers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is bound by this SmartProxy instance
|
||||
*
|
||||
* @param port The port number to check
|
||||
* @returns True if the port is currently bound by SmartProxy
|
||||
*/
|
||||
public isPortBoundBySmartProxy(port: number): boolean {
|
||||
return this.servers.has(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current reference count for a port
|
||||
*
|
||||
* @param port The port number to check
|
||||
* @returns The number of routes using this port, 0 if none
|
||||
*/
|
||||
public getPortRefCount(port: number): number {
|
||||
return this.portRefCounts.get(port) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the reference count for a port
|
||||
*
|
||||
* @param port The port number to increment
|
||||
* @returns The new reference count
|
||||
*/
|
||||
public incrementPortRefCount(port: number): number {
|
||||
const currentCount = this.portRefCounts.get(port) || 0;
|
||||
const newCount = currentCount + 1;
|
||||
this.portRefCounts.set(port, newCount);
|
||||
|
||||
logger.log('debug', `Port ${port} reference count increased to ${newCount}`, {
|
||||
port,
|
||||
refCount: newCount,
|
||||
component: 'port-manager'
|
||||
});
|
||||
|
||||
return newCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the reference count for a port
|
||||
*
|
||||
* @param port The port number to decrement
|
||||
* @returns The new reference count
|
||||
*/
|
||||
public decrementPortRefCount(port: number): number {
|
||||
const currentCount = this.portRefCounts.get(port) || 0;
|
||||
|
||||
if (currentCount <= 0) {
|
||||
logger.log('warn', `Attempted to decrement reference count for port ${port} below zero`, {
|
||||
port,
|
||||
component: 'port-manager'
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
const newCount = currentCount - 1;
|
||||
this.portRefCounts.set(port, newCount);
|
||||
|
||||
logger.log('debug', `Port ${port} reference count decreased to ${newCount}`, {
|
||||
port,
|
||||
refCount: newCount,
|
||||
component: 'port-manager'
|
||||
});
|
||||
|
||||
return newCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a port binding error is due to an external or internal conflict
|
||||
*
|
||||
* @param error The error object from a failed port binding
|
||||
* @returns Object indicating if this is a conflict and if it's external
|
||||
*/
|
||||
private isPortConflict(error: any): { isConflict: boolean; isExternal: boolean } {
|
||||
if (error.code !== 'EADDRINUSE') {
|
||||
return { isConflict: false, isExternal: false };
|
||||
}
|
||||
|
||||
// Check if we already have this port
|
||||
const isBoundInternally = this.servers.has(Number(error.port));
|
||||
return { isConflict: true, isExternal: !isBoundInternally };
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user