344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
|
import * as plugins from './plugins.js';
|
||
|
import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js';
|
||
|
import { ConnectionManager } from './classes.pp.connectionmanager.js';
|
||
|
import { SecurityManager } from './classes.pp.securitymanager.js';
|
||
|
import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
|
||
|
import { TlsManager } from './classes.pp.tlsmanager.js';
|
||
|
import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
||
|
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
||
|
import { AcmeManager } from './classes.pp.acmemanager.js';
|
||
|
import { PortRangeManager } from './classes.pp.portrangemanager.js';
|
||
|
import { ConnectionHandler } from './classes.pp.connectionhandler.js';
|
||
|
|
||
|
/**
|
||
|
* PortProxy - Main class that coordinates all components
|
||
|
*/
|
||
|
export class PortProxy {
|
||
|
private netServers: plugins.net.Server[] = [];
|
||
|
private connectionLogger: NodeJS.Timeout | null = null;
|
||
|
private isShuttingDown: boolean = false;
|
||
|
|
||
|
// Component managers
|
||
|
private connectionManager: ConnectionManager;
|
||
|
private securityManager: SecurityManager;
|
||
|
public domainConfigManager: DomainConfigManager;
|
||
|
private tlsManager: TlsManager;
|
||
|
private networkProxyBridge: NetworkProxyBridge;
|
||
|
private timeoutManager: TimeoutManager;
|
||
|
private acmeManager: AcmeManager;
|
||
|
private portRangeManager: PortRangeManager;
|
||
|
private connectionHandler: ConnectionHandler;
|
||
|
|
||
|
constructor(settingsArg: IPortProxySettings) {
|
||
|
// Set reasonable defaults for all settings
|
||
|
this.settings = {
|
||
|
...settingsArg,
|
||
|
targetIP: settingsArg.targetIP || 'localhost',
|
||
|
initialDataTimeout: settingsArg.initialDataTimeout || 120000,
|
||
|
socketTimeout: settingsArg.socketTimeout || 3600000,
|
||
|
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000,
|
||
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000,
|
||
|
inactivityTimeout: settingsArg.inactivityTimeout || 14400000,
|
||
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
||
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
||
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
||
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000,
|
||
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024,
|
||
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
||
|
enableKeepAliveProbes:
|
||
|
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
||
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
||
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
||
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
||
|
allowSessionTicket:
|
||
|
settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
|
||
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
||
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
||
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
||
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
||
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
|
||
|
networkProxyPort: settingsArg.networkProxyPort || 8443,
|
||
|
acme: settingsArg.acme || {
|
||
|
enabled: false,
|
||
|
port: 80,
|
||
|
contactEmail: 'admin@example.com',
|
||
|
useProduction: false,
|
||
|
renewThresholdDays: 30,
|
||
|
autoRenew: true,
|
||
|
certificateStore: './certs',
|
||
|
skipConfiguredCerts: false,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
// Initialize component managers
|
||
|
this.timeoutManager = new TimeoutManager(this.settings);
|
||
|
this.securityManager = new SecurityManager(this.settings);
|
||
|
this.connectionManager = new ConnectionManager(
|
||
|
this.settings,
|
||
|
this.securityManager,
|
||
|
this.timeoutManager
|
||
|
);
|
||
|
this.domainConfigManager = new DomainConfigManager(this.settings);
|
||
|
this.tlsManager = new TlsManager(this.settings);
|
||
|
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
|
||
|
this.portRangeManager = new PortRangeManager(this.settings);
|
||
|
this.acmeManager = new AcmeManager(this.settings, this.networkProxyBridge);
|
||
|
|
||
|
// Initialize connection handler
|
||
|
this.connectionHandler = new ConnectionHandler(
|
||
|
this.settings,
|
||
|
this.connectionManager,
|
||
|
this.securityManager,
|
||
|
this.domainConfigManager,
|
||
|
this.tlsManager,
|
||
|
this.networkProxyBridge,
|
||
|
this.timeoutManager,
|
||
|
this.portRangeManager
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The settings for the port proxy
|
||
|
*/
|
||
|
public settings: IPortProxySettings;
|
||
|
|
||
|
/**
|
||
|
* Start the proxy server
|
||
|
*/
|
||
|
public async start() {
|
||
|
// Don't start if already shutting down
|
||
|
if (this.isShuttingDown) {
|
||
|
console.log("Cannot start PortProxy while it's shutting down");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Initialize and start NetworkProxy if needed
|
||
|
if (
|
||
|
this.settings.useNetworkProxy &&
|
||
|
this.settings.useNetworkProxy.length > 0
|
||
|
) {
|
||
|
await this.networkProxyBridge.initialize();
|
||
|
await this.networkProxyBridge.start();
|
||
|
}
|
||
|
|
||
|
// Validate port configuration
|
||
|
const configWarnings = this.portRangeManager.validateConfiguration();
|
||
|
if (configWarnings.length > 0) {
|
||
|
console.log("Port configuration warnings:");
|
||
|
for (const warning of configWarnings) {
|
||
|
console.log(` - ${warning}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get listening ports from PortRangeManager
|
||
|
const listeningPorts = this.portRangeManager.getListeningPorts();
|
||
|
|
||
|
// Create servers for each port
|
||
|
for (const port of listeningPorts) {
|
||
|
const server = plugins.net.createServer((socket) => {
|
||
|
// Check if shutting down
|
||
|
if (this.isShuttingDown) {
|
||
|
socket.end();
|
||
|
socket.destroy();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Delegate to connection handler
|
||
|
this.connectionHandler.handleConnection(socket);
|
||
|
}).on('error', (err: Error) => {
|
||
|
console.log(`Server Error on port ${port}: ${err.message}`);
|
||
|
});
|
||
|
|
||
|
server.listen(port, () => {
|
||
|
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
||
|
console.log(
|
||
|
`PortProxy -> OK: Now listening on port ${port}${
|
||
|
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
|
||
|
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
|
||
|
);
|
||
|
});
|
||
|
|
||
|
this.netServers.push(server);
|
||
|
}
|
||
|
|
||
|
// Set up periodic connection logging and inactivity checks
|
||
|
this.connectionLogger = setInterval(() => {
|
||
|
// Immediately return if shutting down
|
||
|
if (this.isShuttingDown) return;
|
||
|
|
||
|
// Perform inactivity check
|
||
|
this.connectionManager.performInactivityCheck();
|
||
|
|
||
|
// Log connection statistics
|
||
|
const now = Date.now();
|
||
|
let maxIncoming = 0;
|
||
|
let maxOutgoing = 0;
|
||
|
let tlsConnections = 0;
|
||
|
let nonTlsConnections = 0;
|
||
|
let completedTlsHandshakes = 0;
|
||
|
let pendingTlsHandshakes = 0;
|
||
|
let keepAliveConnections = 0;
|
||
|
let networkProxyConnections = 0;
|
||
|
|
||
|
// Get connection records for analysis
|
||
|
const connectionRecords = this.connectionManager.getConnections();
|
||
|
|
||
|
// Analyze active connections
|
||
|
for (const record of connectionRecords.values()) {
|
||
|
// Track connection stats
|
||
|
if (record.isTLS) {
|
||
|
tlsConnections++;
|
||
|
if (record.tlsHandshakeComplete) {
|
||
|
completedTlsHandshakes++;
|
||
|
} else {
|
||
|
pendingTlsHandshakes++;
|
||
|
}
|
||
|
} else {
|
||
|
nonTlsConnections++;
|
||
|
}
|
||
|
|
||
|
if (record.hasKeepAlive) {
|
||
|
keepAliveConnections++;
|
||
|
}
|
||
|
|
||
|
if (record.usingNetworkProxy) {
|
||
|
networkProxyConnections++;
|
||
|
}
|
||
|
|
||
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
||
|
if (record.outgoingStartTime) {
|
||
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get termination stats
|
||
|
const terminationStats = this.connectionManager.getTerminationStats();
|
||
|
|
||
|
// Log detailed stats
|
||
|
console.log(
|
||
|
`Active connections: ${connectionRecords.size}. ` +
|
||
|
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
||
|
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
|
||
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
|
||
|
`Termination stats: ${JSON.stringify({
|
||
|
IN: terminationStats.incoming,
|
||
|
OUT: terminationStats.outgoing,
|
||
|
})}`
|
||
|
);
|
||
|
}, this.settings.inactivityCheckInterval || 60000);
|
||
|
|
||
|
// Make sure the interval doesn't keep the process alive
|
||
|
if (this.connectionLogger.unref) {
|
||
|
this.connectionLogger.unref();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stop the proxy server
|
||
|
*/
|
||
|
public async stop() {
|
||
|
console.log('PortProxy shutting down...');
|
||
|
this.isShuttingDown = true;
|
||
|
|
||
|
// Stop accepting new connections
|
||
|
const closeServerPromises: Promise<void>[] = this.netServers.map(
|
||
|
(server) =>
|
||
|
new Promise<void>((resolve) => {
|
||
|
if (!server.listening) {
|
||
|
resolve();
|
||
|
return;
|
||
|
}
|
||
|
server.close((err) => {
|
||
|
if (err) {
|
||
|
console.log(`Error closing server: ${err.message}`);
|
||
|
}
|
||
|
resolve();
|
||
|
});
|
||
|
})
|
||
|
);
|
||
|
|
||
|
// Stop the connection logger
|
||
|
if (this.connectionLogger) {
|
||
|
clearInterval(this.connectionLogger);
|
||
|
this.connectionLogger = null;
|
||
|
}
|
||
|
|
||
|
// Wait for servers to close
|
||
|
await Promise.all(closeServerPromises);
|
||
|
console.log('All servers closed. Cleaning up active connections...');
|
||
|
|
||
|
// Clean up all active connections
|
||
|
this.connectionManager.clearConnections();
|
||
|
|
||
|
// Stop NetworkProxy
|
||
|
await this.networkProxyBridge.stop();
|
||
|
|
||
|
// Clear all servers
|
||
|
this.netServers = [];
|
||
|
|
||
|
console.log('PortProxy shutdown complete.');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates the domain configurations for the proxy
|
||
|
*/
|
||
|
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
|
||
|
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
|
||
|
|
||
|
// Update domain configs in DomainConfigManager
|
||
|
this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
|
||
|
|
||
|
// If NetworkProxy is initialized, resync the configurations
|
||
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
||
|
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates the ACME certificate settings
|
||
|
*/
|
||
|
public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
|
||
|
console.log('Updating ACME certificate settings');
|
||
|
|
||
|
// Delegate to AcmeManager
|
||
|
await this.acmeManager.updateAcmeSettings(acmeSettings);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests a certificate for a specific domain
|
||
|
*/
|
||
|
public async requestCertificate(domain: string): Promise<boolean> {
|
||
|
// Delegate to AcmeManager
|
||
|
return this.acmeManager.requestCertificate(domain);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get statistics about current connections
|
||
|
*/
|
||
|
public getStatistics(): any {
|
||
|
const connectionRecords = this.connectionManager.getConnections();
|
||
|
const terminationStats = this.connectionManager.getTerminationStats();
|
||
|
|
||
|
let tlsConnections = 0;
|
||
|
let nonTlsConnections = 0;
|
||
|
let keepAliveConnections = 0;
|
||
|
let networkProxyConnections = 0;
|
||
|
|
||
|
// Analyze active connections
|
||
|
for (const record of connectionRecords.values()) {
|
||
|
if (record.isTLS) tlsConnections++;
|
||
|
else nonTlsConnections++;
|
||
|
if (record.hasKeepAlive) keepAliveConnections++;
|
||
|
if (record.usingNetworkProxy) networkProxyConnections++;
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
activeConnections: connectionRecords.size,
|
||
|
tlsConnections,
|
||
|
nonTlsConnections,
|
||
|
keepAliveConnections,
|
||
|
networkProxyConnections,
|
||
|
terminationStats
|
||
|
};
|
||
|
}
|
||
|
}
|