This commit is contained in:
Philipp Kunz 2025-05-10 00:49:39 +00:00
parent 3596d35f45
commit 529857220d
4 changed files with 270 additions and 501 deletions

View File

@ -13,64 +13,17 @@ export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'ht
*/ */
export type IRoutedSmartProxyOptions = ISmartProxyOptions; export type IRoutedSmartProxyOptions = ISmartProxyOptions;
/**
* Legacy domain configuration interface for backward compatibility
*/
export interface IDomainConfig {
domains: string[];
forwarding: {
type: TForwardingType;
target: {
host: string | string[];
port: number;
};
acme?: {
enabled?: boolean;
maintenance?: boolean;
production?: boolean;
forwardChallenges?: {
host: string;
port: number;
useTls?: boolean;
};
};
http?: {
enabled?: boolean;
redirectToHttps?: boolean;
headers?: Record<string, string>;
};
https?: {
customCert?: {
key: string;
cert: string;
};
forwardSni?: boolean;
};
security?: {
allowedIps?: string[];
blockedIps?: string[];
maxConnections?: number;
};
advanced?: {
portRanges?: Array<{ from: number; to: number }>;
networkProxyPort?: number;
keepAlive?: boolean;
timeout?: number;
headers?: Record<string, string>;
};
};
}
/** /**
* Helper functions for type checking configuration types * Helper functions for type checking configuration types
*/ */
export function isLegacyOptions(options: any): boolean { export function isLegacyOptions(options: any): boolean {
return !!(options.domainConfigs && options.domainConfigs.length > 0 && // Legacy options are no longer supported
(!options.routes || options.routes.length === 0)); return false;
} }
export function isRoutedOptions(options: any): boolean { export function isRoutedOptions(options: any): boolean {
return !!(options.routes && options.routes.length > 0); // All configurations are now route-based
return true;
} }
/** /**
@ -80,14 +33,7 @@ export interface ISmartProxyOptions {
// The unified configuration array (required) // The unified configuration array (required)
routes: IRouteConfig[]; routes: IRouteConfig[];
// Legacy options for backward compatibility // Port range configuration
fromPort?: number;
toPort?: number;
sniEnabled?: boolean;
domainConfigs?: IDomainConfig[];
targetIP?: string;
defaultAllowedIPs?: string[];
defaultBlockedIPs?: string[];
globalPortRanges?: Array<{ from: number; to: number }>; globalPortRanges?: Array<{ from: number; to: number }>;
forwardAllGlobalRanges?: boolean; forwardAllGlobalRanges?: boolean;
preserveSourceIP?: boolean; preserveSourceIP?: boolean;
@ -99,8 +45,8 @@ export interface ISmartProxyOptions {
port: number; // Default port to use when not specified in routes port: number; // Default port to use when not specified in routes
}; };
security?: { security?: {
allowedIPs?: string[]; // Default allowed IPs allowedIps?: string[]; // Default allowed IPs
blockedIPs?: string[]; // Default blocked IPs blockedIps?: string[]; // Default blocked IPs
maxConnections?: number; // Default max connections maxConnections?: number; // Default max connections
}; };
preserveSourceIP?: boolean; // Default source IP preservation preserveSourceIP?: boolean; // Default source IP preservation
@ -184,9 +130,6 @@ export interface IConnectionRecord {
pendingData: Buffer[]; // Buffer to hold data during connection setup pendingData: Buffer[]; // Buffer to hold data during connection setup
pendingDataSize: number; // Track total size of pending data pendingDataSize: number; // Track total size of pending data
// Legacy property for backward compatibility
domainConfig?: IDomainConfig;
// Enhanced tracking fields // Enhanced tracking fields
bytesReceived: number; // Total bytes received bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent bytesSent: number; // Total bytes sent

View File

@ -4,10 +4,19 @@ import { Port80Handler } from '../../http/port80/port80-handler.js';
import { Port80HandlerEvents } from '../../core/models/common-types.js'; import { Port80HandlerEvents } from '../../core/models/common-types.js';
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js'; import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
import type { ICertificateData } from '../../certificate/models/certificate-types.js'; import type { ICertificateData } from '../../certificate/models/certificate-types.js';
import type { IConnectionRecord, ISmartProxyOptions, IDomainConfig } from './models/interfaces.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
import type { IRouteConfig } from './models/route-types.js';
/** /**
* Manages NetworkProxy integration for TLS termination * Manages NetworkProxy integration for TLS termination
*
* NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination.
* It converts route configurations to NetworkProxy configuration format and manages
* certificate provisioning through Port80Handler when ACME is enabled.
*
* It is used by SmartProxy for routes that have:
* - TLS mode of 'terminate' or 'terminate-and-reencrypt'
* - Certificate set to 'auto' or custom certificate
*/ */
export class NetworkProxyBridge { export class NetworkProxyBridge {
private networkProxy: NetworkProxy | null = null; private networkProxy: NetworkProxy | null = null;
@ -58,8 +67,8 @@ export class NetworkProxyBridge {
this.networkProxy.setExternalPort80Handler(this.port80Handler); this.networkProxy.setExternalPort80Handler(this.port80Handler);
} }
// Convert and apply domain configurations to NetworkProxy // Apply route configurations to NetworkProxy
await this.syncDomainConfigsToNetworkProxy(); await this.syncRoutesToNetworkProxy(this.settings.routes || []);
} }
} }
@ -249,9 +258,19 @@ export class NetworkProxyBridge {
} }
/** /**
* Synchronizes domain configurations to NetworkProxy * Synchronizes routes to NetworkProxy
*
* This method converts route configurations to NetworkProxy format and updates
* the NetworkProxy with the converted configurations. It handles:
*
* - Extracting domain, target, and certificate information from routes
* - Converting TLS mode settings to NetworkProxy configuration
* - Applying security and advanced settings
* - Registering domains for ACME certificate provisioning when needed
*
* @param routes The route configurations to sync to NetworkProxy
*/ */
public async syncDomainConfigsToNetworkProxy(): Promise<void> { public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
if (!this.networkProxy) { if (!this.networkProxy) {
console.log('Cannot sync configurations - NetworkProxy not initialized'); console.log('Cannot sync configurations - NetworkProxy not initialized');
return; return;
@ -282,38 +301,112 @@ export class NetworkProxyBridge {
}; };
} }
// Convert domain configs to NetworkProxy configs // Convert routes to NetworkProxy configs
const proxyConfigs = this.networkProxy.convertSmartProxyConfigs( const proxyConfigs = this.convertRoutesToNetworkProxyConfigs(routes, certPair);
this.settings.domainConfigs,
certPair
);
// Log ACME-eligible domains // Update the proxy configs
const acmeEnabled = !!this.settings.acme?.enabled;
if (acmeEnabled) {
const acmeEligibleDomains = proxyConfigs
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
.map((config) => config.hostName);
if (acmeEligibleDomains.length > 0) {
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
// Register these domains with Port80Handler if available
if (this.port80Handler) {
this.registerDomainsWithPort80Handler(acmeEligibleDomains);
}
} else {
console.log('No domains eligible for ACME certificates found in configuration');
}
}
// Update NetworkProxy with the converted configs
await this.networkProxy.updateProxyConfigs(proxyConfigs); await this.networkProxy.updateProxyConfigs(proxyConfigs);
console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`); console.log(`Synced ${proxyConfigs.length} configurations to NetworkProxy`);
} catch (err) { } catch (err) {
console.log(`Failed to sync configurations: ${err}`); console.log(`Error syncing routes to NetworkProxy: ${err}`);
} }
} }
/**
* Convert routes to NetworkProxy configuration format
*
* This method transforms route-based configuration to NetworkProxy's configuration format.
* It processes each route and creates appropriate NetworkProxy configs for domains
* that require TLS termination.
*
* @param routes Array of route configurations to convert
* @param defaultCertPair Default certificate to use if no custom certificate is specified
* @returns Array of NetworkProxy configurations
*/
public convertRoutesToNetworkProxyConfigs(
routes: IRouteConfig[],
defaultCertPair: { key: string; cert: string }
): plugins.tsclass.network.IReverseProxyConfig[] {
const configs: plugins.tsclass.network.IReverseProxyConfig[] = [];
for (const route of routes) {
// Skip routes without domains
if (!route.match.domains) continue;
// Skip non-forward routes
if (route.action.type !== 'forward') continue;
// Skip routes without TLS configuration
if (!route.action.tls || !route.action.target) continue;
// Get domains from route
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// Create a config for each domain
for (const domain of domains) {
// Determine if this route requires TLS termination
const needsTermination = route.action.tls.mode === 'terminate' ||
route.action.tls.mode === 'terminate-and-reencrypt';
// Skip passthrough domains for NetworkProxy
if (route.action.tls.mode === 'passthrough') continue;
// Get certificate
let certKey = defaultCertPair.key;
let certCert = defaultCertPair.cert;
// Use custom certificate if specified
if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate === 'object') {
certKey = route.action.tls.certificate.key;
certCert = route.action.tls.certificate.cert;
}
// Determine target hosts and ports
const targetHosts = Array.isArray(route.action.target.host)
? route.action.target.host
: [route.action.target.host];
const targetPort = route.action.target.port;
// Create NetworkProxy config
const config: plugins.tsclass.network.IReverseProxyConfig = {
hostName: domain,
privateKey: certKey,
publicKey: certCert,
destinationIps: targetHosts,
destinationPorts: [targetPort],
proxyConfig: {
targetIsTls: route.action.tls.mode === 'terminate-and-reencrypt',
allowHTTP1: true,
// Apply any other NetworkProxy-specific settings
...(route.action.advanced ? {
preserveHost: true,
timeout: route.action.advanced.timeout,
headers: route.action.advanced.headers
} : {})
}
};
configs.push(config);
}
}
return configs;
}
/**
* @deprecated This method is deprecated and will be removed in a future version.
* Use syncRoutesToNetworkProxy() instead.
*
* This legacy method exists only for backward compatibility and
* simply forwards to syncRoutesToNetworkProxy().
*/
public async syncDomainConfigsToNetworkProxy(): Promise<void> {
console.log('Method syncDomainConfigsToNetworkProxy is deprecated. Use syncRoutesToNetworkProxy instead.');
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
}
/** /**
* Request a certificate for a specific domain * Request a certificate for a specific domain

View File

@ -1,12 +1,10 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import type { import type {
IConnectionRecord, IConnectionRecord,
IDomainConfig,
ISmartProxyOptions ISmartProxyOptions
} from './models/interfaces.js'; } from './models/interfaces.js';
import { import {
isRoutedOptions, isRoutedOptions
isLegacyOptions
} from './models/interfaces.js'; } from './models/interfaces.js';
import type { import type {
IRouteConfig, IRouteConfig,
@ -14,13 +12,11 @@ import type {
} from './models/route-types.js'; } from './models/route-types.js';
import { ConnectionManager } from './connection-manager.js'; import { ConnectionManager } from './connection-manager.js';
import { SecurityManager } from './security-manager.js'; import { SecurityManager } from './security-manager.js';
import { DomainConfigManager } from './domain-config-manager.js';
import { TlsManager } from './tls-manager.js'; import { TlsManager } from './tls-manager.js';
import { NetworkProxyBridge } from './network-proxy-bridge.js'; import { NetworkProxyBridge } from './network-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 type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
import type { TForwardingType } from '../../forwarding/config/forwarding-types.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
@ -32,7 +28,6 @@ export class RouteConnectionHandler {
settings: ISmartProxyOptions, settings: ISmartProxyOptions,
private connectionManager: ConnectionManager, private connectionManager: ConnectionManager,
private securityManager: SecurityManager, private securityManager: SecurityManager,
private domainConfigManager: DomainConfigManager,
private tlsManager: TlsManager, private tlsManager: TlsManager,
private networkProxyBridge: NetworkProxyBridge, private networkProxyBridge: NetworkProxyBridge,
private timeoutManager: TimeoutManager, private timeoutManager: TimeoutManager,
@ -244,37 +239,20 @@ export class RouteConnectionHandler {
if (!routeMatch) { if (!routeMatch) {
console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`); console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`);
// Fall back to legacy matching if we're using a hybrid configuration
const domainConfig = serverName
? this.domainConfigManager.findDomainConfig(serverName)
: this.domainConfigManager.findDomainConfigForPort(localPort);
if (domainConfig) { // No matching route, use default/fallback handling
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using legacy domain configuration for ${serverName || 'port ' + localPort}`);
}
// Associate this domain config with the connection
record.domainConfig = domainConfig;
// Handle the connection using the legacy setup
return this.handleLegacyConnection(socket, record, serverName, domainConfig, initialChunk);
}
// No matching route or domain config, use default/fallback handling
console.log(`[${connectionId}] Using default route handling for connection`); console.log(`[${connectionId}] Using default route handling for connection`);
// Check default security settings // Check default security settings
const defaultSecuritySettings = this.settings.defaults?.security; const defaultSecuritySettings = this.settings.defaults?.security;
if (defaultSecuritySettings) { if (defaultSecuritySettings) {
if (defaultSecuritySettings.allowedIPs && defaultSecuritySettings.allowedIPs.length > 0) { if (defaultSecuritySettings.allowedIps && defaultSecuritySettings.allowedIps.length > 0) {
const isAllowed = this.securityManager.isIPAuthorized( const isAllowed = this.securityManager.isIPAuthorized(
remoteIP, remoteIP,
defaultSecuritySettings.allowedIPs, defaultSecuritySettings.allowedIps,
defaultSecuritySettings.blockedIPs || [] defaultSecuritySettings.blockedIps || []
); );
if (!isAllowed) { if (!isAllowed) {
console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`); console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
socket.end(); socket.end();
@ -282,34 +260,24 @@ export class RouteConnectionHandler {
return; return;
} }
} }
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
// Legacy default IP restrictions
const isAllowed = this.securityManager.isIPAuthorized(
remoteIP,
this.settings.defaultAllowedIPs,
this.settings.defaultBlockedIPs || []
);
if (!isAllowed) {
console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
socket.end();
this.connectionManager.cleanupConnection(record, 'ip_blocked');
return;
}
} }
// Setup direct connection with default settings // Setup direct connection with default settings
let targetHost: string; if (this.settings.defaults?.target) {
let targetPort: number; // Use defaults from configuration
const targetHost = this.settings.defaults.target.host;
const targetPort = this.settings.defaults.target.port;
if (isRoutedOptions(this.settings) && this.settings.defaults?.target) { return this.setupDirectConnection(
// Use defaults from routed configuration socket,
targetHost = this.settings.defaults.target.host; record,
targetPort = this.settings.defaults.target.port; undefined,
} else if (this.settings.targetIP && this.settings.toPort) { serverName,
// Fall back to legacy settings initialChunk,
targetHost = this.settings.targetIP; undefined,
targetPort = this.settings.toPort; targetHost,
targetPort
);
} else { } else {
// No default target available, terminate the connection // No default target available, terminate the connection
console.log(`[${connectionId}] No default target configured. Closing connection.`); console.log(`[${connectionId}] No default target configured. Closing connection.`);
@ -317,17 +285,6 @@ export class RouteConnectionHandler {
this.connectionManager.cleanupConnection(record, 'no_default_target'); this.connectionManager.cleanupConnection(record, 'no_default_target');
return; return;
} }
return this.setupDirectConnection(
socket,
record,
undefined,
serverName,
initialChunk,
undefined,
targetHost,
targetPort
);
} }
// A matching route was found // A matching route was found
@ -575,114 +532,8 @@ export class RouteConnectionHandler {
} }
/** /**
* Handle a connection using legacy domain configuration * Legacy connection handling has been removed in favor of pure route-based approach
*/ */
private handleLegacyConnection(
socket: plugins.net.Socket,
record: IConnectionRecord,
serverName: string,
domainConfig: IDomainConfig,
initialChunk?: Buffer
): void {
const connectionId = record.id;
// Get the forwarding type for this domain
const forwardingType = this.domainConfigManager.getForwardingType(domainConfig);
// IP validation
const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
console.log(
`[${connectionId}] Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`
);
socket.end();
this.connectionManager.initiateCleanupOnce(record, 'ip_blocked');
return;
}
// Handle based on forwarding type
switch (forwardingType) {
case 'http-only':
// For HTTP-only configs with TLS traffic
if (record.isTLS) {
console.log(`[${connectionId}] Received TLS connection for HTTP-only domain ${serverName}`);
socket.end();
this.connectionManager.initiateCleanupOnce(record, 'wrong_protocol');
return;
}
break;
case 'https-passthrough':
// For TLS passthrough with TLS traffic
if (record.isTLS) {
try {
const handler = this.domainConfigManager.getForwardingHandler(domainConfig);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using forwarding handler for SNI passthrough to ${serverName}`);
}
// Handle the connection using the handler
return handler.handleConnection(socket);
} catch (err) {
console.log(`[${connectionId}] Error using forwarding handler: ${err}`);
}
}
break;
case 'https-terminate-to-http':
case 'https-terminate-to-https':
// For TLS termination with TLS traffic
if (record.isTLS) {
const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using TLS termination (${forwardingType}) for ${serverName} on port ${networkProxyPort}`);
}
// Forward to NetworkProxy with domain-specific port
return this.networkProxyBridge.forwardToNetworkProxy(
connectionId,
socket,
record,
initialChunk!,
networkProxyPort,
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
}
break;
}
// If we're still here, use the forwarding handler if available
try {
const handler = this.domainConfigManager.getForwardingHandler(domainConfig);
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using general forwarding handler for domain ${serverName || 'unknown'}`);
}
// Handle the connection using the handler
return handler.handleConnection(socket);
} catch (err) {
console.log(`[${connectionId}] Error using forwarding handler: ${err}`);
}
// Fallback: set up direct connection
const targetIp = this.domainConfigManager.getTargetIP(domainConfig);
const targetPort = this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort);
return this.setupDirectConnection(
socket,
record,
domainConfig,
serverName,
initialChunk,
undefined,
targetIp,
targetPort
);
}
/** /**
* Sets up a direct connection to the target * Sets up a direct connection to the target
@ -690,7 +541,7 @@ export class RouteConnectionHandler {
private setupDirectConnection( private setupDirectConnection(
socket: plugins.net.Socket, socket: plugins.net.Socket,
record: IConnectionRecord, record: IConnectionRecord,
domainConfig?: IDomainConfig, _unused?: any, // kept for backward compatibility
serverName?: string, serverName?: string,
initialChunk?: Buffer, initialChunk?: Buffer,
overridePort?: number, overridePort?: number,
@ -698,22 +549,15 @@ export class RouteConnectionHandler {
targetPort?: number targetPort?: number
): void { ): void {
const connectionId = record.id; const connectionId = record.id;
// Determine target host and port if not provided
const finalTargetHost = targetHost || (domainConfig
? this.domainConfigManager.getTargetIP(domainConfig)
: this.settings.defaults?.target?.host
? this.settings.defaults.target.host
: this.settings.targetIP!);
// Determine target port - first try explicit port, then forwarding config, then fallback // Determine target host and port if not provided
const finalTargetPort = targetPort || (overridePort !== undefined const finalTargetHost = targetHost ||
? overridePort (this.settings.defaults?.target?.host || 'localhost');
: domainConfig
? this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort) // Determine target port
: this.settings.defaults?.target?.port const finalTargetPort = targetPort ||
? this.settings.defaults.target.port (overridePort !== undefined ? overridePort :
: this.settings.toPort); (this.settings.defaults?.target?.port || 443));
// Setup connection options // Setup connection options
const connectionOptions: plugins.net.NetConnectOpts = { const connectionOptions: plugins.net.NetConnectOpts = {

View File

@ -3,7 +3,6 @@ import * as plugins from '../../plugins.js';
// Importing required components // Importing required components
import { ConnectionManager } from './connection-manager.js'; import { ConnectionManager } from './connection-manager.js';
import { SecurityManager } from './security-manager.js'; import { SecurityManager } from './security-manager.js';
import { DomainConfigManager } from './domain-config-manager.js';
import { TlsManager } from './tls-manager.js'; import { TlsManager } from './tls-manager.js';
import { NetworkProxyBridge } from './network-proxy-bridge.js'; import { NetworkProxyBridge } from './network-proxy-bridge.js';
import { TimeoutManager } from './timeout-manager.js'; import { TimeoutManager } from './timeout-manager.js';
@ -19,16 +18,25 @@ import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
// Import types and utilities // Import types and utilities
import type { import type {
ISmartProxyOptions, ISmartProxyOptions,
IRoutedSmartProxyOptions, IRoutedSmartProxyOptions
IDomainConfig
} from './models/interfaces.js'; } from './models/interfaces.js';
import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js'; import { isRoutedOptions } from './models/interfaces.js';
import type { IRouteConfig } from './models/route-types.js'; import type { IRouteConfig } from './models/route-types.js';
/** /**
* SmartProxy - Unified route-based API * 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.)
*/ */
export class SmartProxy extends plugins.EventEmitter { export class SmartProxy extends plugins.EventEmitter {
private netServers: plugins.net.Server[] = []; private netServers: plugins.net.Server[] = [];
@ -38,7 +46,6 @@ export class SmartProxy extends plugins.EventEmitter {
// Component managers // Component managers
private connectionManager: ConnectionManager; private connectionManager: ConnectionManager;
private securityManager: SecurityManager; private securityManager: SecurityManager;
private domainConfigManager: DomainConfigManager;
private tlsManager: TlsManager; private tlsManager: TlsManager;
private networkProxyBridge: NetworkProxyBridge; private networkProxyBridge: NetworkProxyBridge;
private timeoutManager: TimeoutManager; private timeoutManager: TimeoutManager;
@ -52,7 +59,35 @@ export class SmartProxy extends plugins.EventEmitter {
private certProvisioner?: CertProvisioner; private certProvisioner?: CertProvisioner;
/** /**
* Constructor that supports both legacy and route-based configuration * 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: ['*'] }
* }
* });
* ```
*/ */
constructor(settingsArg: ISmartProxyOptions) { constructor(settingsArg: ISmartProxyOptions) {
super(); super();
@ -113,17 +148,10 @@ export class SmartProxy extends plugins.EventEmitter {
this.timeoutManager this.timeoutManager
); );
// Create the new route manager first // Create the route manager
this.routeManager = new RouteManager(this.settings); this.routeManager = new RouteManager(this.settings);
// Create domain config manager and port range manager // Create port range manager
this.domainConfigManager = new DomainConfigManager(this.settings);
// Share the route manager with the domain config manager
if (typeof this.domainConfigManager.setRouteManager === 'function') {
this.domainConfigManager.setRouteManager(this.routeManager);
}
this.portRangeManager = new PortRangeManager(this.settings); this.portRangeManager = new PortRangeManager(this.settings);
// Create other required components // Create other required components
@ -135,7 +163,6 @@ export class SmartProxy extends plugins.EventEmitter {
this.settings, this.settings,
this.connectionManager, this.connectionManager,
this.securityManager, this.securityManager,
this.domainConfigManager,
this.tlsManager, this.tlsManager,
this.networkProxyBridge, this.networkProxyBridge,
this.timeoutManager, this.timeoutManager,
@ -162,7 +189,7 @@ export class SmartProxy extends plugins.EventEmitter {
// Build and start the Port80Handler // Build and start the Port80Handler
this.port80Handler = buildPort80Handler({ this.port80Handler = buildPort80Handler({
...config, ...config,
httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443) httpsRedirectPort: config.httpsRedirectPort || 443
}); });
// Share Port80Handler with NetworkProxyBridge before start // Share Port80Handler with NetworkProxyBridge before start
@ -184,17 +211,7 @@ export class SmartProxy extends plugins.EventEmitter {
return; return;
} }
// Initialize domain config based on configuration type // Pure route-based configuration - no domain configs needed
if (isLegacyOptions(this.settings)) {
// Initialize domain config manager with the legacy domain configs
this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs || []);
} else if (isRoutedOptions(this.settings)) {
// For pure route-based configuration, the domain config is already initialized
// in the constructor, but we might need to regenerate it
if (typeof this.domainConfigManager.generateDomainConfigsFromRoutes === 'function') {
this.domainConfigManager.generateDomainConfigsFromRoutes();
}
}
// Initialize Port80Handler if enabled // Initialize Port80Handler if enabled
await this.initializePort80Handler(); await this.initializePort80Handler();
@ -248,35 +265,18 @@ export class SmartProxy extends plugins.EventEmitter {
}) || []; }) || [];
// Create CertProvisioner with appropriate parameters // Create CertProvisioner with appropriate parameters
if (isLegacyOptions(this.settings)) { // No longer need to support multiple configuration types
this.certProvisioner = new CertProvisioner( // Just pass the routes directly
this.settings.domainConfigs, this.certProvisioner = new CertProvisioner(
this.port80Handler, this.settings.routes,
this.networkProxyBridge, this.port80Handler,
this.settings.certProvisionFunction, this.networkProxyBridge,
acme.renewThresholdDays!, this.settings.certProvisionFunction,
acme.renewCheckIntervalHours!, acme.renewThresholdDays!,
acme.autoRenew!, acme.renewCheckIntervalHours!,
domainForwards acme.autoRenew!,
); domainForwards
} else { );
// For route-based configuration, we need to adapt the interface
// Convert routes to domain configs for CertProvisioner
const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes(
(this.settings as IRoutedSmartProxyOptions).routes
);
this.certProvisioner = new CertProvisioner(
domainConfigs,
this.port80Handler,
this.networkProxyBridge,
this.settings.certProvisionFunction,
acme.renewThresholdDays!,
acme.renewCheckIntervalHours!,
acme.autoRenew!,
domainForwards
);
}
// Register certificate event handler // Register certificate event handler
this.certProvisioner.on('certificate', (certData) => { this.certProvisioner.on('certificate', (certData) => {
@ -416,60 +416,9 @@ export class SmartProxy extends plugins.EventEmitter {
/** /**
* Extract domain configurations from routes for certificate provisioning * Extract domain configurations from routes for certificate provisioning
*
* Note: This method has been removed as we now work directly with routes
*/ */
private extractDomainConfigsFromRoutes(routes: IRouteConfig[]): IDomainConfig[] {
const domainConfigs: IDomainConfig[] = [];
for (const route of routes) {
// Skip routes without domain specs
if (!route.match.domains) continue;
// Skip non-forward routes
if (route.action.type !== 'forward') continue;
// Only process routes that need TLS termination (those with certificates)
if (!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
!route.action.target) continue;
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// Determine forwarding type based on TLS mode
const forwardingType = route.action.tls.mode === 'terminate'
? 'https-terminate-to-http'
: 'https-terminate-to-https';
// Create a forwarding config
const forwarding = {
type: forwardingType as any,
target: {
host: Array.isArray(route.action.target.host)
? route.action.target.host[0]
: route.action.target.host,
port: route.action.target.port
},
// Add TLS settings
https: {
customCert: route.action.tls.certificate !== 'auto'
? route.action.tls.certificate
: undefined
},
// Add security settings if present
security: route.action.security,
// Add advanced settings if present
advanced: route.action.advanced
};
domainConfigs.push({
domains,
forwarding
});
}
return domainConfigs;
}
/** /**
* Stop the proxy server * Stop the proxy server
@ -535,134 +484,74 @@ export class SmartProxy extends plugins.EventEmitter {
} }
/** /**
* Updates the domain configurations for the proxy (legacy support) * Updates the domain configurations for the proxy
*
* Note: This legacy method has been removed. Use updateRoutes instead.
*/ */
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { public async updateDomainConfigs(): Promise<void> {
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
// Update domain configs in DomainConfigManager (legacy)
this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
// Also update the RouteManager with these domain configs
this.routeManager.updateFromDomainConfigs(newDomainConfigs);
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
}
// If Port80Handler is running, provision certificates based on forwarding type
if (this.port80Handler && this.settings.acme?.enabled) {
for (const domainConfig of newDomainConfigs) {
// Skip certificate provisioning for http-only or passthrough configs that don't need certs
const forwardingType = this.domainConfigManager.getForwardingType(domainConfig);
const needsCertificate =
forwardingType === 'https-terminate-to-http' ||
forwardingType === 'https-terminate-to-https';
// Skip certificate provisioning if ACME is explicitly disabled for this domain
const acmeDisabled = domainConfig.forwarding.acme?.enabled === false;
if (!needsCertificate || acmeDisabled) {
if (this.settings.enableDetailedLogging) {
console.log(`Skipping certificate provisioning for ${domainConfig.domains.join(', ')} (${forwardingType})`);
}
continue;
}
for (const domain of domainConfig.domains) {
const isWildcard = domain.includes('*');
let provision: string | plugins.tsclass.network.ICert = 'http01';
// Check for ACME forwarding configuration in the domain
const forwardAcmeChallenges = domainConfig.forwarding.acme?.forwardChallenges;
if (this.settings.certProvisionFunction) {
try {
provision = await this.settings.certProvisionFunction(domain);
} catch (err) {
console.log(`certProvider error for ${domain}: ${err}`);
}
} else if (isWildcard) {
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
continue;
}
if (provision === 'http01') {
if (isWildcard) {
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
continue;
}
// Create Port80Handler options from the forwarding configuration
const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding);
this.port80Handler.addDomain(port80Config);
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
} else {
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
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)
};
this.networkProxyBridge.applyExternalCertificate(certData);
console.log(`Applied static certificate for ${domain} from certProvider`);
}
}
}
console.log('Provisioned certificates for new domains');
}
} }
/** /**
* Update routes with new configuration (new API) * 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' }
* }
* }
* ]);
* ```
*/ */
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> { public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
console.log(`Updating routes (${newRoutes.length} routes)`); console.log(`Updating routes (${newRoutes.length} routes)`);
// Update routes in RouteManager // Update routes in RouteManager
this.routeManager.updateRoutes(newRoutes); this.routeManager.updateRoutes(newRoutes);
// If NetworkProxy is initialized, resync the configurations // If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) { if (this.networkProxyBridge.getNetworkProxy()) {
// Create equivalent domain configs for NetworkProxy await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes);
// Update domain configs in DomainConfigManager for sync
this.domainConfigManager.updateDomainConfigs(domainConfigs);
// Sync with NetworkProxy
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
} }
// If Port80Handler is running, provision certificates based on routes // If Port80Handler is running, provision certificates based on routes
if (this.port80Handler && this.settings.acme?.enabled) { if (this.port80Handler && this.settings.acme?.enabled) {
for (const route of newRoutes) { for (const route of newRoutes) {
// Skip routes without domains // Skip routes without domains
if (!route.match.domains) continue; if (!route.match.domains) continue;
// Skip non-forward routes // Skip non-forward routes
if (route.action.type !== 'forward') continue; if (route.action.type !== 'forward') continue;
// Skip routes without TLS termination // Skip routes without TLS termination
if (!route.action.tls || if (!route.action.tls ||
route.action.tls.mode === 'passthrough' || route.action.tls.mode === 'passthrough' ||
!route.action.target) continue; !route.action.target) continue;
// Skip certificate provisioning if certificate is not auto // Skip certificate provisioning if certificate is not auto
if (route.action.tls.certificate !== 'auto') continue; if (route.action.tls.certificate !== 'auto') continue;
const domains = Array.isArray(route.match.domains) const domains = Array.isArray(route.match.domains)
? route.match.domains ? route.match.domains
: [route.match.domains]; : [route.match.domains];
for (const domain of domains) { for (const domain of domains) {
const isWildcard = domain.includes('*'); const isWildcard = domain.includes('*');
let provision: string | plugins.tsclass.network.ICert = 'http01'; let provision: string | plugins.tsclass.network.ICert = 'http01';
if (this.settings.certProvisionFunction) { if (this.settings.certProvisionFunction) {
try { try {
provision = await this.settings.certProvisionFunction(domain); provision = await this.settings.certProvisionFunction(domain);
@ -673,20 +562,20 @@ export class SmartProxy extends plugins.EventEmitter {
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
continue; continue;
} }
if (provision === 'http01') { if (provision === 'http01') {
if (isWildcard) { if (isWildcard) {
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
continue; continue;
} }
// Register domain with Port80Handler // Register domain with Port80Handler
this.port80Handler.addDomain({ this.port80Handler.addDomain({
domainName: domain, domainName: domain,
sslRedirect: true, sslRedirect: true,
acmeMaintenance: true acmeMaintenance: true
}); });
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
} else { } else {
// Handle static certificate (e.g., DNS-01 provisioned) // Handle static certificate (e.g., DNS-01 provisioned)
@ -702,7 +591,7 @@ export class SmartProxy extends plugins.EventEmitter {
} }
} }
} }
console.log('Provisioned certificates for new routes'); console.log('Provisioned certificates for new routes');
} }
} }