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

View File

@ -3,7 +3,6 @@ import * as plugins from '../../plugins.js';
// Importing required components
import { ConnectionManager } from './connection-manager.js';
import { SecurityManager } from './security-manager.js';
import { DomainConfigManager } from './domain-config-manager.js';
import { TlsManager } from './tls-manager.js';
import { NetworkProxyBridge } from './network-proxy-bridge.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 types and utilities
import type {
ISmartProxyOptions,
IRoutedSmartProxyOptions,
IDomainConfig
import type {
ISmartProxyOptions,
IRoutedSmartProxyOptions
} 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';
/**
* 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 {
private netServers: plugins.net.Server[] = [];
@ -38,7 +46,6 @@ export class SmartProxy extends plugins.EventEmitter {
// Component managers
private connectionManager: ConnectionManager;
private securityManager: SecurityManager;
private domainConfigManager: DomainConfigManager;
private tlsManager: TlsManager;
private networkProxyBridge: NetworkProxyBridge;
private timeoutManager: TimeoutManager;
@ -52,7 +59,35 @@ export class SmartProxy extends plugins.EventEmitter {
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) {
super();
@ -113,17 +148,10 @@ export class SmartProxy extends plugins.EventEmitter {
this.timeoutManager
);
// Create the new route manager first
// Create the route manager
this.routeManager = new RouteManager(this.settings);
// Create domain config manager and 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);
}
// Create port range manager
this.portRangeManager = new PortRangeManager(this.settings);
// Create other required components
@ -135,7 +163,6 @@ export class SmartProxy extends plugins.EventEmitter {
this.settings,
this.connectionManager,
this.securityManager,
this.domainConfigManager,
this.tlsManager,
this.networkProxyBridge,
this.timeoutManager,
@ -162,7 +189,7 @@ export class SmartProxy extends plugins.EventEmitter {
// Build and start the Port80Handler
this.port80Handler = buildPort80Handler({
...config,
httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443)
httpsRedirectPort: config.httpsRedirectPort || 443
});
// Share Port80Handler with NetworkProxyBridge before start
@ -184,17 +211,7 @@ export class SmartProxy extends plugins.EventEmitter {
return;
}
// Initialize domain config based on configuration type
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();
}
}
// Pure route-based configuration - no domain configs needed
// Initialize Port80Handler if enabled
await this.initializePort80Handler();
@ -248,35 +265,18 @@ export class SmartProxy extends plugins.EventEmitter {
}) || [];
// Create CertProvisioner with appropriate parameters
if (isLegacyOptions(this.settings)) {
this.certProvisioner = new CertProvisioner(
this.settings.domainConfigs,
this.port80Handler,
this.networkProxyBridge,
this.settings.certProvisionFunction,
acme.renewThresholdDays!,
acme.renewCheckIntervalHours!,
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
);
}
// 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!,
domainForwards
);
// Register certificate event handler
this.certProvisioner.on('certificate', (certData) => {
@ -416,60 +416,9 @@ export class SmartProxy extends plugins.EventEmitter {
/**
* 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
@ -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> {
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
// 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');
}
public async updateDomainConfigs(): Promise<void> {
console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
}
/**
* 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> {
console.log(`Updating routes (${newRoutes.length} routes)`);
// Update routes in RouteManager
this.routeManager.updateRoutes(newRoutes);
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
// Create equivalent domain configs for NetworkProxy
const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes);
// Update domain configs in DomainConfigManager for sync
this.domainConfigManager.updateDomainConfigs(domainConfigs);
// Sync with NetworkProxy
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
}
// If Port80Handler is running, provision certificates based on routes
if (this.port80Handler && this.settings.acme?.enabled) {
for (const route of newRoutes) {
// Skip routes without domains
if (!route.match.domains) continue;
// Skip non-forward routes
if (route.action.type !== 'forward') continue;
// Skip routes without TLS termination
if (!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
if (!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
!route.action.target) continue;
// Skip certificate provisioning if certificate is not auto
if (route.action.tls.certificate !== 'auto') continue;
const domains = Array.isArray(route.match.domains)
? route.match.domains
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
for (const domain of domains) {
const isWildcard = domain.includes('*');
let provision: string | plugins.tsclass.network.ICert = 'http01';
if (this.settings.certProvisionFunction) {
try {
provision = await this.settings.certProvisionFunction(domain);
@ -673,20 +562,20 @@ export class SmartProxy extends plugins.EventEmitter {
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
continue;
}
if (provision === 'http01') {
if (isWildcard) {
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
continue;
}
// Register domain with Port80Handler
this.port80Handler.addDomain({
domainName: domain,
sslRedirect: true,
acmeMaintenance: true
});
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
} else {
// 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');
}
}