2025-05-09 21:21:28 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Importing required components
|
2025-05-09 21:21:28 +00:00
|
|
|
import { ConnectionManager } from './connection-manager.js';
|
|
|
|
import { SecurityManager } from './security-manager.js';
|
|
|
|
import { TlsManager } from './tls-manager.js';
|
|
|
|
import { NetworkProxyBridge } from './network-proxy-bridge.js';
|
|
|
|
import { TimeoutManager } from './timeout-manager.js';
|
2025-05-10 07:34:35 +00:00
|
|
|
// import { PortRangeManager } from './port-range-manager.js';
|
2025-05-10 00:01:02 +00:00
|
|
|
import { RouteManager } from './route-manager.js';
|
|
|
|
import { RouteConnectionHandler } from './route-connection-handler.js';
|
2025-05-09 21:21:28 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// External dependencies
|
2025-05-09 21:21:28 +00:00
|
|
|
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
|
|
|
import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js';
|
2025-05-09 22:46:53 +00:00
|
|
|
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
|
2025-05-09 21:21:28 +00:00
|
|
|
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
|
|
|
|
import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Import types and utilities
|
2025-05-10 00:49:39 +00:00
|
|
|
import type {
|
|
|
|
ISmartProxyOptions,
|
|
|
|
IRoutedSmartProxyOptions
|
2025-05-10 00:01:02 +00:00
|
|
|
} from './models/interfaces.js';
|
2025-05-10 07:34:35 +00:00
|
|
|
import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js';
|
2025-05-10 00:01:02 +00:00
|
|
|
import type { IRouteConfig } from './models/route-types.js';
|
2025-05-04 12:44:35 +00:00
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
/**
|
2025-05-10 00:49:39 +00:00
|
|
|
* SmartProxy - Pure route-based API
|
|
|
|
*
|
|
|
|
* SmartProxy is a unified proxy system that works with routes to define connection handling behavior.
|
|
|
|
* Each route contains matching criteria (ports, domains, etc.) and an action to take (forward, redirect, block).
|
|
|
|
*
|
|
|
|
* Configuration is provided through a set of routes, with each route defining:
|
|
|
|
* - What to match (ports, domains, paths, client IPs)
|
|
|
|
* - What to do with matching traffic (forward, redirect, block)
|
|
|
|
* - How to handle TLS (passthrough, terminate, terminate-and-reencrypt)
|
|
|
|
* - Security settings (IP restrictions, connection limits)
|
|
|
|
* - Advanced options (timeout, headers, etc.)
|
2025-03-25 22:30:57 +00:00
|
|
|
*/
|
2025-05-01 12:13:18 +00:00
|
|
|
export class SmartProxy extends plugins.EventEmitter {
|
2025-03-25 22:30:57 +00:00
|
|
|
private netServers: plugins.net.Server[] = [];
|
|
|
|
private connectionLogger: NodeJS.Timeout | null = null;
|
|
|
|
private isShuttingDown: boolean = false;
|
|
|
|
|
|
|
|
// Component managers
|
|
|
|
private connectionManager: ConnectionManager;
|
|
|
|
private securityManager: SecurityManager;
|
|
|
|
private tlsManager: TlsManager;
|
|
|
|
private networkProxyBridge: NetworkProxyBridge;
|
|
|
|
private timeoutManager: TimeoutManager;
|
2025-05-10 07:34:35 +00:00
|
|
|
// private portRangeManager: PortRangeManager;
|
2025-05-10 00:01:02 +00:00
|
|
|
private routeManager: RouteManager;
|
|
|
|
private routeConnectionHandler: RouteConnectionHandler;
|
2025-03-25 22:30:57 +00:00
|
|
|
|
|
|
|
// Port80Handler for ACME certificate management
|
|
|
|
private port80Handler: Port80Handler | null = null;
|
2025-05-02 11:19:14 +00:00
|
|
|
// CertProvisioner for unified certificate workflows
|
|
|
|
private certProvisioner?: CertProvisioner;
|
2025-03-25 22:30:57 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
/**
|
2025-05-10 00:49:39 +00:00
|
|
|
* Constructor for SmartProxy
|
|
|
|
*
|
|
|
|
* @param settingsArg Configuration options containing routes and other settings
|
|
|
|
* Routes define how traffic is matched and handled, with each route having:
|
|
|
|
* - match: criteria for matching traffic (ports, domains, paths, IPs)
|
|
|
|
* - action: what to do with matched traffic (forward, redirect, block)
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
* ```ts
|
|
|
|
* const proxy = new SmartProxy({
|
|
|
|
* routes: [
|
|
|
|
* {
|
|
|
|
* match: {
|
|
|
|
* ports: 443,
|
|
|
|
* domains: ['example.com', '*.example.com']
|
|
|
|
* },
|
|
|
|
* action: {
|
|
|
|
* type: 'forward',
|
|
|
|
* target: { host: '10.0.0.1', port: 8443 },
|
|
|
|
* tls: { mode: 'passthrough' }
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ],
|
|
|
|
* defaults: {
|
|
|
|
* target: { host: 'localhost', port: 8080 },
|
|
|
|
* security: { allowedIps: ['*'] }
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
* ```
|
2025-05-10 00:01:02 +00:00
|
|
|
*/
|
2025-05-09 22:46:53 +00:00
|
|
|
constructor(settingsArg: ISmartProxyOptions) {
|
2025-05-01 12:13:18 +00:00
|
|
|
super();
|
2025-05-10 00:01:02 +00:00
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
// Set reasonable defaults for all settings
|
|
|
|
this.settings = {
|
|
|
|
...settingsArg,
|
|
|
|
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,
|
2025-05-09 22:46:53 +00:00
|
|
|
enableKeepAliveProbes:
|
2025-03-25 22:30:57 +00:00
|
|
|
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
|
|
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
|
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
|
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
2025-05-09 22:46:53 +00:00
|
|
|
allowSessionTicket:
|
2025-03-25 22:30:57 +00:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2025-05-02 14:58:33 +00:00
|
|
|
// Set default ACME options if not provided
|
2025-05-10 00:01:02 +00:00
|
|
|
this.settings.acme = this.settings.acme || {};
|
|
|
|
if (Object.keys(this.settings.acme).length === 0) {
|
2025-05-02 14:58:33 +00:00
|
|
|
this.settings.acme = {
|
2025-05-02 11:19:14 +00:00
|
|
|
enabled: false,
|
|
|
|
port: 80,
|
2025-05-05 10:46:05 +00:00
|
|
|
accountEmail: 'admin@example.com',
|
2025-05-02 11:19:14 +00:00
|
|
|
useProduction: false,
|
|
|
|
renewThresholdDays: 30,
|
|
|
|
autoRenew: true,
|
|
|
|
certificateStore: './certs',
|
|
|
|
skipConfiguredCerts: false,
|
2025-05-10 07:34:35 +00:00
|
|
|
httpsRedirectPort: 443,
|
2025-05-02 14:58:33 +00:00
|
|
|
renewCheckIntervalHours: 24,
|
2025-05-10 13:59:34 +00:00
|
|
|
routeForwards: []
|
2025-05-02 11:19:14 +00:00
|
|
|
};
|
2025-03-25 22:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
);
|
2025-05-10 00:01:02 +00:00
|
|
|
|
2025-05-10 00:49:39 +00:00
|
|
|
// Create the route manager
|
2025-05-10 00:26:03 +00:00
|
|
|
this.routeManager = new RouteManager(this.settings);
|
|
|
|
|
2025-05-10 00:49:39 +00:00
|
|
|
// Create port range manager
|
2025-05-10 07:34:35 +00:00
|
|
|
// this.portRangeManager = new PortRangeManager(this.settings);
|
2025-05-10 00:01:02 +00:00
|
|
|
|
|
|
|
// Create other required components
|
2025-03-25 22:30:57 +00:00
|
|
|
this.tlsManager = new TlsManager(this.settings);
|
|
|
|
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Initialize connection handler with route support
|
|
|
|
this.routeConnectionHandler = new RouteConnectionHandler(
|
2025-03-25 22:30:57 +00:00
|
|
|
this.settings,
|
|
|
|
this.connectionManager,
|
|
|
|
this.securityManager,
|
|
|
|
this.tlsManager,
|
|
|
|
this.networkProxyBridge,
|
|
|
|
this.timeoutManager,
|
2025-05-10 00:01:02 +00:00
|
|
|
this.routeManager
|
2025-03-25 22:30:57 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-05-10 00:01:02 +00:00
|
|
|
* The settings for the SmartProxy
|
2025-03-25 22:30:57 +00:00
|
|
|
*/
|
2025-05-09 22:46:53 +00:00
|
|
|
public settings: ISmartProxyOptions;
|
2025-03-25 22:30:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the Port80Handler for ACME certificate management
|
|
|
|
*/
|
|
|
|
private async initializePort80Handler(): Promise<void> {
|
2025-05-02 14:58:33 +00:00
|
|
|
const config = this.settings.acme!;
|
|
|
|
if (!config.enabled) {
|
|
|
|
console.log('ACME is disabled in configuration');
|
2025-03-25 22:30:57 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2025-05-02 14:58:33 +00:00
|
|
|
// Build and start the Port80Handler
|
|
|
|
this.port80Handler = buildPort80Handler({
|
|
|
|
...config,
|
2025-05-10 00:49:39 +00:00
|
|
|
httpsRedirectPort: config.httpsRedirectPort || 443
|
2025-03-25 22:30:57 +00:00
|
|
|
});
|
2025-05-10 00:01:02 +00:00
|
|
|
|
2025-05-02 14:58:33 +00:00
|
|
|
// Share Port80Handler with NetworkProxyBridge before start
|
2025-03-25 22:30:57 +00:00
|
|
|
this.networkProxyBridge.setPort80Handler(this.port80Handler);
|
|
|
|
await this.port80Handler.start();
|
|
|
|
console.log(`Port80Handler started on port ${config.port}`);
|
|
|
|
} catch (err) {
|
|
|
|
console.log(`Error initializing Port80Handler: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-05-10 00:01:02 +00:00
|
|
|
* Start the proxy server with support for both configuration types
|
2025-03-25 22:30:57 +00:00
|
|
|
*/
|
|
|
|
public async start() {
|
|
|
|
// Don't start if already shutting down
|
|
|
|
if (this.isShuttingDown) {
|
2025-05-09 21:21:28 +00:00
|
|
|
console.log("Cannot start SmartProxy while it's shutting down");
|
2025-03-25 22:30:57 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-05-10 00:49:39 +00:00
|
|
|
// Pure route-based configuration - no domain configs needed
|
2025-05-09 14:15:45 +00:00
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
// Initialize Port80Handler if enabled
|
|
|
|
await this.initializePort80Handler();
|
2025-05-09 14:15:45 +00:00
|
|
|
|
2025-05-02 11:19:14 +00:00
|
|
|
// Initialize CertProvisioner for unified certificate workflows
|
|
|
|
if (this.port80Handler) {
|
2025-05-02 14:58:33 +00:00
|
|
|
const acme = this.settings.acme!;
|
2025-05-09 14:15:45 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Setup route forwards
|
|
|
|
const routeForwards = acme.routeForwards?.map(f => f) || [];
|
2025-05-09 14:15:45 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Create CertProvisioner with appropriate parameters
|
2025-05-10 00:49:39 +00:00
|
|
|
// No longer need to support multiple configuration types
|
|
|
|
// Just pass the routes directly
|
|
|
|
this.certProvisioner = new CertProvisioner(
|
|
|
|
this.settings.routes,
|
|
|
|
this.port80Handler,
|
|
|
|
this.networkProxyBridge,
|
|
|
|
this.settings.certProvisionFunction,
|
|
|
|
acme.renewThresholdDays!,
|
|
|
|
acme.renewCheckIntervalHours!,
|
|
|
|
acme.autoRenew!,
|
2025-05-10 13:59:34 +00:00
|
|
|
routeForwards
|
2025-05-10 00:49:39 +00:00
|
|
|
);
|
2025-05-09 14:15:45 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Register certificate event handler
|
2025-05-02 11:19:14 +00:00
|
|
|
this.certProvisioner.on('certificate', (certData) => {
|
|
|
|
this.emit('certificate', {
|
|
|
|
domain: certData.domain,
|
|
|
|
publicKey: certData.certificate,
|
|
|
|
privateKey: certData.privateKey,
|
|
|
|
expiryDate: certData.expiryDate,
|
|
|
|
source: certData.source,
|
|
|
|
isRenewal: certData.isRenewal
|
|
|
|
});
|
|
|
|
});
|
2025-05-09 14:15:45 +00:00
|
|
|
|
2025-05-02 11:19:14 +00:00
|
|
|
await this.certProvisioner.start();
|
|
|
|
console.log('CertProvisioner started');
|
|
|
|
}
|
2025-03-25 22:30:57 +00:00
|
|
|
|
|
|
|
// Initialize and start NetworkProxy if needed
|
2025-05-10 00:01:02 +00:00
|
|
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
2025-03-25 22:30:57 +00:00
|
|
|
await this.networkProxyBridge.initialize();
|
|
|
|
await this.networkProxyBridge.start();
|
|
|
|
}
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Validate the route configuration
|
|
|
|
const configWarnings = this.routeManager.validateConfiguration();
|
2025-03-25 22:30:57 +00:00
|
|
|
if (configWarnings.length > 0) {
|
2025-05-10 00:01:02 +00:00
|
|
|
console.log("Route configuration warnings:");
|
2025-03-25 22:30:57 +00:00
|
|
|
for (const warning of configWarnings) {
|
|
|
|
console.log(` - ${warning}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Get listening ports from RouteManager
|
|
|
|
const listeningPorts = this.routeManager.getListeningPorts();
|
2025-03-25 22:30:57 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Delegate to route connection handler
|
|
|
|
this.routeConnectionHandler.handleConnection(socket);
|
2025-03-25 22:30:57 +00:00
|
|
|
}).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(
|
2025-05-09 21:21:28 +00:00
|
|
|
`SmartProxy -> OK: Now listening on port ${port}${
|
2025-05-10 07:34:35 +00:00
|
|
|
isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''
|
|
|
|
}`
|
2025-03-25 22:30:57 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
/**
|
|
|
|
* Extract domain configurations from routes for certificate provisioning
|
2025-05-10 00:49:39 +00:00
|
|
|
*
|
|
|
|
* Note: This method has been removed as we now work directly with routes
|
2025-05-10 00:01:02 +00:00
|
|
|
*/
|
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
/**
|
|
|
|
* Stop the proxy server
|
|
|
|
*/
|
|
|
|
public async stop() {
|
2025-05-09 21:21:28 +00:00
|
|
|
console.log('SmartProxy shutting down...');
|
2025-03-25 22:30:57 +00:00
|
|
|
this.isShuttingDown = true;
|
2025-05-10 00:01:02 +00:00
|
|
|
|
2025-05-02 11:19:14 +00:00
|
|
|
// Stop CertProvisioner if active
|
|
|
|
if (this.certProvisioner) {
|
|
|
|
await this.certProvisioner.stop();
|
|
|
|
console.log('CertProvisioner stopped');
|
2025-05-01 15:39:20 +00:00
|
|
|
}
|
2025-03-25 22:30:57 +00:00
|
|
|
|
|
|
|
// Stop the Port80Handler if running
|
|
|
|
if (this.port80Handler) {
|
|
|
|
try {
|
|
|
|
await this.port80Handler.stop();
|
|
|
|
console.log('Port80Handler stopped');
|
|
|
|
this.port80Handler = null;
|
|
|
|
} catch (err) {
|
|
|
|
console.log(`Error stopping Port80Handler: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 = [];
|
|
|
|
|
2025-05-09 21:21:28 +00:00
|
|
|
console.log('SmartProxy shutdown complete.');
|
2025-03-25 22:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-05-10 00:49:39 +00:00
|
|
|
* Updates the domain configurations for the proxy
|
|
|
|
*
|
|
|
|
* Note: This legacy method has been removed. Use updateRoutes instead.
|
2025-03-25 22:30:57 +00:00
|
|
|
*/
|
2025-05-10 00:49:39 +00:00
|
|
|
public async updateDomainConfigs(): Promise<void> {
|
|
|
|
console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
|
|
|
|
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
|
2025-03-25 22:30:57 +00:00
|
|
|
}
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
/**
|
2025-05-10 00:49:39 +00:00
|
|
|
* Update routes with new configuration
|
|
|
|
*
|
|
|
|
* This method replaces the current route configuration with the provided routes.
|
|
|
|
* It also provisions certificates for routes that require TLS termination and have
|
|
|
|
* `certificate: 'auto'` set in their TLS configuration.
|
|
|
|
*
|
|
|
|
* @param newRoutes Array of route configurations to use
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
* ```ts
|
|
|
|
* proxy.updateRoutes([
|
|
|
|
* {
|
|
|
|
* match: { ports: 443, domains: 'secure.example.com' },
|
|
|
|
* action: {
|
|
|
|
* type: 'forward',
|
|
|
|
* target: { host: '10.0.0.1', port: 8443 },
|
|
|
|
* tls: { mode: 'terminate', certificate: 'auto' }
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
* ]);
|
|
|
|
* ```
|
2025-05-10 00:01:02 +00:00
|
|
|
*/
|
|
|
|
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
|
|
|
|
console.log(`Updating routes (${newRoutes.length} routes)`);
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Update routes in RouteManager
|
|
|
|
this.routeManager.updateRoutes(newRoutes);
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// If NetworkProxy is initialized, resync the configurations
|
|
|
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
2025-05-10 00:49:39 +00:00
|
|
|
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
|
2025-05-10 00:01:02 +00:00
|
|
|
}
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// If Port80Handler is running, provision certificates based on routes
|
|
|
|
if (this.port80Handler && this.settings.acme?.enabled) {
|
2025-05-10 13:59:34 +00:00
|
|
|
// Register all eligible domains from routes
|
|
|
|
this.port80Handler.addDomainsFromRoutes(newRoutes);
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Handle static certificates from certProvisionFunction if available
|
|
|
|
if (this.settings.certProvisionFunction) {
|
|
|
|
for (const route of newRoutes) {
|
|
|
|
// Skip routes without domains
|
|
|
|
if (!route.match.domains) continue;
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Skip non-forward routes
|
|
|
|
if (route.action.type !== 'forward') continue;
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Skip routes without TLS termination
|
|
|
|
if (!route.action.tls ||
|
|
|
|
route.action.tls.mode === 'passthrough' ||
|
|
|
|
!route.action.target) continue;
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
// Skip certificate provisioning if certificate is not auto
|
|
|
|
if (route.action.tls.certificate !== 'auto') continue;
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
const domains = Array.isArray(route.match.domains)
|
|
|
|
? route.match.domains
|
|
|
|
: [route.match.domains];
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 13:59:34 +00:00
|
|
|
for (const domain of domains) {
|
2025-05-10 00:01:02 +00:00
|
|
|
try {
|
2025-05-10 13:59:34 +00:00
|
|
|
const provision = await this.settings.certProvisionFunction(domain);
|
|
|
|
|
|
|
|
// Skip http01 as those are handled by Port80Handler
|
|
|
|
if (provision !== 'http01') {
|
|
|
|
// Handle static certificate (e.g., DNS-01 provisioned)
|
|
|
|
const certObj = provision as plugins.tsclass.network.ICert;
|
|
|
|
const certData: ICertificateData = {
|
|
|
|
domain: certObj.domainName,
|
|
|
|
certificate: certObj.publicKey,
|
|
|
|
privateKey: certObj.privateKey,
|
|
|
|
expiryDate: new Date(certObj.validUntil),
|
|
|
|
routeReference: {
|
|
|
|
routeName: route.name
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
|
|
console.log(`Applied static certificate for ${domain} from certProvider`);
|
|
|
|
}
|
2025-05-10 00:01:02 +00:00
|
|
|
} catch (err) {
|
|
|
|
console.log(`certProvider error for ${domain}: ${err}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-05-10 00:49:39 +00:00
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
console.log('Provisioned certificates for new routes');
|
|
|
|
}
|
|
|
|
}
|
2025-03-25 22:30:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Request a certificate for a specific domain
|
2025-05-10 13:59:34 +00:00
|
|
|
*
|
|
|
|
* @param domain The domain to request a certificate for
|
|
|
|
* @param routeName Optional route name to associate with the certificate
|
2025-03-25 22:30:57 +00:00
|
|
|
*/
|
2025-05-10 13:59:34 +00:00
|
|
|
public async requestCertificate(domain: string, routeName?: string): Promise<boolean> {
|
2025-03-25 22:30:57 +00:00
|
|
|
// Validate domain format
|
|
|
|
if (!this.isValidDomain(domain)) {
|
|
|
|
console.log(`Invalid domain format: ${domain}`);
|
|
|
|
return false;
|
|
|
|
}
|
2025-05-10 13:59:34 +00:00
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
// Use Port80Handler if available
|
|
|
|
if (this.port80Handler) {
|
|
|
|
try {
|
|
|
|
// Check if we already have a certificate
|
|
|
|
const cert = this.port80Handler.getCertificate(domain);
|
|
|
|
if (cert) {
|
|
|
|
console.log(`Certificate already exists for ${domain}, valid until ${cert.expiryDate.toISOString()}`);
|
|
|
|
return true;
|
|
|
|
}
|
2025-05-10 13:59:34 +00:00
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
// Register domain for certificate issuance
|
|
|
|
this.port80Handler.addDomain({
|
2025-05-10 13:59:34 +00:00
|
|
|
domain,
|
2025-03-25 22:30:57 +00:00
|
|
|
sslRedirect: true,
|
2025-05-10 13:59:34 +00:00
|
|
|
acmeMaintenance: true,
|
|
|
|
routeReference: routeName ? { routeName } : undefined
|
2025-03-25 22:30:57 +00:00
|
|
|
});
|
2025-05-10 13:59:34 +00:00
|
|
|
|
|
|
|
console.log(`Domain ${domain} registered for certificate issuance` + (routeName ? ` for route '${routeName}'` : ''));
|
2025-03-25 22:30:57 +00:00
|
|
|
return true;
|
|
|
|
} catch (err) {
|
|
|
|
console.log(`Error registering domain with Port80Handler: ${err}`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fall back to NetworkProxyBridge
|
|
|
|
return this.networkProxyBridge.requestCertificate(domain);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validates if a domain name is valid for certificate issuance
|
|
|
|
*/
|
|
|
|
private isValidDomain(domain: string): boolean {
|
|
|
|
// Very basic domain validation
|
|
|
|
if (!domain || domain.length === 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for wildcard domains (they can't get ACME certs)
|
|
|
|
if (domain.includes('*')) {
|
|
|
|
console.log(`Wildcard domains like "${domain}" are not supported for ACME certificates`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if domain has at least one dot and no invalid characters
|
|
|
|
const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
|
|
if (!validDomainRegex.test(domain)) {
|
|
|
|
console.log(`Domain "${domain}" has invalid format`);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
|
|
|
acmeEnabled: !!this.port80Handler,
|
2025-05-10 00:01:02 +00:00
|
|
|
port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null,
|
|
|
|
routes: this.routeManager.getListeningPorts().length
|
2025-03-25 22:30:57 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a list of eligible domains for ACME certificates
|
|
|
|
*/
|
|
|
|
public getEligibleDomainsForCertificates(): string[] {
|
|
|
|
const domains: string[] = [];
|
|
|
|
|
2025-05-10 00:01:02 +00:00
|
|
|
// Get domains from routes
|
|
|
|
const routes = isRoutedOptions(this.settings) ? this.settings.routes : [];
|
|
|
|
|
|
|
|
for (const route of routes) {
|
|
|
|
if (!route.match.domains) continue;
|
|
|
|
|
|
|
|
// Skip routes without TLS termination or auto certificates
|
|
|
|
if (route.action.type !== 'forward' ||
|
|
|
|
!route.action.tls ||
|
|
|
|
route.action.tls.mode === 'passthrough' ||
|
|
|
|
route.action.tls.certificate !== 'auto') continue;
|
|
|
|
|
|
|
|
const routeDomains = Array.isArray(route.match.domains)
|
|
|
|
? route.match.domains
|
|
|
|
: [route.match.domains];
|
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
// Skip domains that can't be used with ACME
|
2025-05-10 00:01:02 +00:00
|
|
|
const eligibleDomains = routeDomains.filter(domain =>
|
2025-03-25 22:30:57 +00:00
|
|
|
!domain.includes('*') && this.isValidDomain(domain)
|
|
|
|
);
|
|
|
|
|
|
|
|
domains.push(...eligibleDomains);
|
|
|
|
}
|
|
|
|
|
2025-05-10 07:34:35 +00:00
|
|
|
// Legacy mode is no longer supported
|
2025-05-10 00:01:02 +00:00
|
|
|
|
2025-03-25 22:30:57 +00:00
|
|
|
return domains;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get status of certificates managed by Port80Handler
|
|
|
|
*/
|
|
|
|
public getCertificateStatus(): any {
|
|
|
|
if (!this.port80Handler) {
|
|
|
|
return {
|
|
|
|
enabled: false,
|
|
|
|
message: 'Port80Handler is not enabled'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get eligible domains
|
|
|
|
const eligibleDomains = this.getEligibleDomainsForCertificates();
|
|
|
|
const certificateStatus: Record<string, any> = {};
|
|
|
|
|
|
|
|
// Check each domain
|
|
|
|
for (const domain of eligibleDomains) {
|
|
|
|
const cert = this.port80Handler.getCertificate(domain);
|
|
|
|
|
|
|
|
if (cert) {
|
|
|
|
const now = new Date();
|
|
|
|
const expiryDate = cert.expiryDate;
|
|
|
|
const daysRemaining = Math.floor((expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000));
|
|
|
|
|
|
|
|
certificateStatus[domain] = {
|
|
|
|
status: 'valid',
|
|
|
|
expiryDate: expiryDate.toISOString(),
|
|
|
|
daysRemaining,
|
2025-05-02 14:58:33 +00:00
|
|
|
renewalNeeded: daysRemaining <= (this.settings.acme?.renewThresholdDays ?? 0)
|
2025-03-25 22:30:57 +00:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
certificateStatus[domain] = {
|
|
|
|
status: 'missing',
|
|
|
|
message: 'No certificate found'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-02 14:58:33 +00:00
|
|
|
const acme = this.settings.acme!;
|
2025-03-25 22:30:57 +00:00
|
|
|
return {
|
|
|
|
enabled: true,
|
2025-05-02 14:58:33 +00:00
|
|
|
port: acme.port!,
|
|
|
|
useProduction: acme.useProduction!,
|
|
|
|
autoRenew: acme.autoRenew!,
|
2025-03-25 22:30:57 +00:00
|
|
|
certificates: certificateStatus
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|