2025-05-09 21:21:28 +00:00
import * as plugins from '../../plugins.js' ;
2025-05-19 23:37:11 +00:00
import { logger } from '../../core/utils/logger.js' ;
2025-05-04 12:44:35 +00:00
2026-02-09 10:55:46 +00:00
// Rust bridge and helpers
import { RustProxyBridge } from './rust-proxy-bridge.js' ;
import { RoutePreprocessor } from './route-preprocessor.js' ;
import { SocketHandlerServer } from './socket-handler-server.js' ;
import { RustMetricsAdapter } from './rust-metrics-adapter.js' ;
2025-05-19 03:40:58 +00:00
2026-02-09 10:55:46 +00:00
// Route management
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js' ;
2025-08-14 14:30:54 +00:00
import { RouteValidator } from './utils/route-validator.js' ;
2026-02-09 10:55:46 +00:00
import { Mutex } from './utils/mutex.js' ;
2025-08-14 14:30:54 +00:00
2026-02-09 10:55:46 +00:00
// Types
2026-02-13 13:08:30 +00:00
import type { ISmartProxyOptions , TSmartProxyCertProvisionObject , IAcmeOptions } from './models/interfaces.js' ;
2026-02-09 10:55:46 +00:00
import type { IRouteConfig } from './models/route-types.js' ;
2025-06-22 22:28:37 +00:00
import type { IMetrics } from './models/metrics-types.js' ;
2025-06-09 15:02:36 +00:00
2025-03-25 22:30:57 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* SmartProxy - Rust - backed proxy engine with TypeScript configuration API .
2025-05-10 00:49:39 +00:00
*
2026-02-09 10:55:46 +00:00
* All networking ( TCP , TLS , HTTP reverse proxy , connection management , security ,
* NFTables ) is handled by the Rust binary . TypeScript is only :
* - The npm module interface ( types , route helpers )
* - The thin IPC wrapper ( this class )
* - Socket - handler callback relay ( for JS - defined handlers )
* - Certificate provisioning callbacks ( certProvisionFunction )
2025-03-25 22:30:57 +00:00
* /
2025-05-01 12:13:18 +00:00
export class SmartProxy extends plugins . EventEmitter {
2026-02-09 10:55:46 +00:00
public settings : ISmartProxyOptions ;
2025-06-22 22:28:37 +00:00
public routeManager : RouteManager ;
2026-02-09 10:55:46 +00:00
private bridge : RustProxyBridge ;
private preprocessor : RoutePreprocessor ;
private socketHandlerServer : SocketHandlerServer | null = null ;
private metricsAdapter : RustMetricsAdapter ;
2025-08-14 14:30:54 +00:00
private routeUpdateLock : Mutex ;
2026-02-09 16:25:33 +00:00
private stopping = false ;
2026-02-09 10:55:46 +00:00
2025-05-09 22:46:53 +00:00
constructor ( settingsArg : ISmartProxyOptions ) {
2025-05-01 12:13:18 +00:00
super ( ) ;
2026-02-09 10:55:46 +00:00
// Apply defaults
2025-03-25 22:30:57 +00:00
this . settings = {
. . . settingsArg ,
initialDataTimeout : settingsArg.initialDataTimeout || 120000 ,
socketTimeout : settingsArg.socketTimeout || 3600000 ,
maxConnectionLifetime : settingsArg.maxConnectionLifetime || 86400000 ,
inactivityTimeout : settingsArg.inactivityTimeout || 14400000 ,
gracefulShutdownTimeout : settingsArg.gracefulShutdownTimeout || 30000 ,
maxConnectionsPerIP : settingsArg.maxConnectionsPerIP || 100 ,
connectionRateLimitPerMinute : settingsArg.connectionRateLimitPerMinute || 300 ,
keepAliveTreatment : settingsArg.keepAliveTreatment || 'extended' ,
keepAliveInactivityMultiplier : settingsArg.keepAliveInactivityMultiplier || 6 ,
extendedKeepAliveLifetime : settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000 ,
} ;
2026-02-09 10:55:46 +00:00
// Normalize ACME options
2025-05-18 18:29:59 +00:00
if ( this . settings . acme ) {
if ( this . settings . acme . accountEmail && ! this . settings . acme . email ) {
this . settings . acme . email = this . settings . acme . accountEmail ;
}
2025-05-02 14:58:33 +00:00
this . settings . acme = {
2026-02-09 10:55:46 +00:00
enabled : this.settings.acme.enabled !== false ,
2025-05-18 18:29:59 +00:00
port : this.settings.acme.port || 80 ,
email : this.settings.acme.email ,
useProduction : this.settings.acme.useProduction || false ,
renewThresholdDays : this.settings.acme.renewThresholdDays || 30 ,
2026-02-09 10:55:46 +00:00
autoRenew : this.settings.acme.autoRenew !== false ,
2025-05-18 18:29:59 +00:00
certificateStore : this.settings.acme.certificateStore || './certs' ,
skipConfiguredCerts : this.settings.acme.skipConfiguredCerts || false ,
renewCheckIntervalHours : this.settings.acme.renewCheckIntervalHours || 24 ,
routeForwards : this.settings.acme.routeForwards || [ ] ,
2026-02-09 10:55:46 +00:00
. . . this . settings . acme ,
2025-05-02 11:19:14 +00:00
} ;
2025-03-25 22:30:57 +00:00
}
2026-02-09 10:55:46 +00:00
// Validate routes
if ( this . settings . routes ? . length ) {
2025-08-14 14:30:54 +00:00
const validation = RouteValidator . validateRoutes ( this . settings . routes ) ;
if ( ! validation . valid ) {
RouteValidator . logValidationErrors ( validation . errors ) ;
throw new Error ( ` Initial route validation failed: ${ validation . errors . size } route(s) have errors ` ) ;
}
}
2026-02-09 10:55:46 +00:00
// Create logger adapter
const loggerAdapter = {
debug : ( message : string , data? : any ) = > logger . log ( 'debug' , message , data ) ,
info : ( message : string , data? : any ) = > logger . log ( 'info' , message , data ) ,
warn : ( message : string , data? : any ) = > logger . log ( 'warn' , message , data ) ,
error : ( message : string , data? : any ) = > logger . log ( 'error' , message , data ) ,
} ;
// Initialize components
2025-06-02 03:57:52 +00:00
this . routeManager = new RouteManager ( {
logger : loggerAdapter ,
enableDetailedLogging : this.settings.enableDetailedLogging ,
2026-02-09 10:55:46 +00:00
routes : this.settings.routes ,
2025-06-02 03:57:52 +00:00
} ) ;
2025-05-10 00:26:03 +00:00
2026-02-09 10:55:46 +00:00
this . bridge = new RustProxyBridge ( ) ;
this . preprocessor = new RoutePreprocessor ( ) ;
2026-02-09 16:25:33 +00:00
this . metricsAdapter = new RustMetricsAdapter (
this . bridge ,
this . settings . metrics ? . sampleIntervalMs ? ? 1000
) ;
2025-05-19 03:40:58 +00:00
this . routeUpdateLock = new Mutex ( ) ;
2025-05-18 23:07:31 +00:00
}
2025-03-25 22:30:57 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Start the proxy .
* Spawns the Rust binary , configures socket relay if needed , sends routes , handles cert provisioning .
2025-03-25 22:30:57 +00:00
* /
2026-02-09 10:55:46 +00:00
public async start ( ) : Promise < void > {
// Spawn Rust binary
const spawned = await this . bridge . spawn ( ) ;
if ( ! spawned ) {
2025-05-18 18:29:59 +00:00
throw new Error (
2026-02-09 10:55:46 +00:00
'RustProxy binary not found. Set SMARTPROXY_RUST_BINARY env var, install the platform package, ' +
2026-02-10 09:43:40 +00:00
'or build locally with: pnpm build'
2025-05-18 18:29:59 +00:00
) ;
}
2025-03-25 22:30:57 +00:00
2026-02-09 16:25:33 +00:00
// Handle unexpected exit (only emits error if not intentionally stopping)
2026-02-09 10:55:46 +00:00
this . bridge . on ( 'exit' , ( code : number | null , signal : string | null ) = > {
2026-02-09 16:25:33 +00:00
if ( this . stopping ) return ;
2026-02-09 10:55:46 +00:00
logger . log ( 'error' , ` RustProxy exited unexpectedly (code= ${ code } , signal= ${ signal } ) ` , { component : 'smart-proxy' } ) ;
this . emit ( 'error' , new Error ( ` RustProxy exited (code= ${ code } , signal= ${ signal } ) ` ) ) ;
2025-05-20 15:32:19 +00:00
} ) ;
2026-02-09 16:25:33 +00:00
// Check if any routes need TS-side handling (socket handlers, dynamic functions)
2026-02-09 10:55:46 +00:00
const hasHandlerRoutes = this . settings . routes . some (
( r ) = >
( r . action . type === 'socket-handler' && r . action . socketHandler ) ||
r . action . targets ? . some ( ( t ) = > typeof t . host === 'function' || typeof t . port === 'function' )
) ;
2025-05-15 14:35:01 +00:00
2026-02-09 16:25:33 +00:00
// Start socket handler relay server (but don't tell Rust yet - proxy not started)
2026-02-09 10:55:46 +00:00
if ( hasHandlerRoutes ) {
this . socketHandlerServer = new SocketHandlerServer ( this . preprocessor ) ;
await this . socketHandlerServer . start ( ) ;
2025-05-20 16:01:32 +00:00
}
2026-02-09 10:55:46 +00:00
// Preprocess routes (strip JS functions, convert socket-handler routes)
const rustRoutes = this . preprocessor . preprocessForRust ( this . settings . routes ) ;
2025-05-20 16:01:32 +00:00
2026-02-13 13:08:30 +00:00
// When certProvisionFunction handles cert provisioning,
// disable Rust's built-in ACME to prevent race condition.
let acmeForRust = this . settings . acme ;
if ( this . settings . certProvisionFunction && acmeForRust ? . enabled ) {
acmeForRust = { . . . acmeForRust , enabled : false } ;
logger . log ( 'info' , 'Rust ACME disabled — certProvisionFunction will handle certificate provisioning' , { component : 'smart-proxy' } ) ;
}
2026-02-09 10:55:46 +00:00
// Build Rust config
2026-02-13 13:08:30 +00:00
const config = this . buildRustConfig ( rustRoutes , acmeForRust ) ;
2025-03-25 22:30:57 +00:00
2026-02-09 10:55:46 +00:00
// Start the Rust proxy
await this . bridge . startProxy ( config ) ;
2025-03-25 22:30:57 +00:00
2026-02-09 16:25:33 +00:00
// Now that Rust proxy is running, configure socket handler relay
if ( this . socketHandlerServer ) {
await this . bridge . setSocketHandlerRelay ( this . socketHandlerServer . getSocketPath ( ) ) ;
}
2026-02-09 10:55:46 +00:00
// Handle certProvisionFunction
await this . provisionCertificatesViaCallback ( ) ;
2025-03-25 22:30:57 +00:00
2026-02-09 10:55:46 +00:00
// Start metrics polling
this . metricsAdapter . startPolling ( ) ;
2025-03-25 22:30:57 +00:00
2026-02-09 10:55:46 +00:00
logger . log ( 'info' , 'SmartProxy started (Rust engine)' , { component : 'smart-proxy' } ) ;
2025-03-25 22:30:57 +00:00
}
2025-05-19 03:40:58 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Stop the proxy .
2025-05-19 03:40:58 +00:00
* /
2026-02-09 10:55:46 +00:00
public async stop ( ) : Promise < void > {
logger . log ( 'info' , 'SmartProxy shutting down...' , { component : 'smart-proxy' } ) ;
2026-02-09 16:25:33 +00:00
this . stopping = true ;
2026-02-09 10:55:46 +00:00
// Stop metrics polling
this . metricsAdapter . stopPolling ( ) ;
2026-02-09 16:25:33 +00:00
// Remove exit listener before killing to avoid spurious error events
this . bridge . removeAllListeners ( 'exit' ) ;
2026-02-09 10:55:46 +00:00
// Stop Rust proxy
2025-05-20 15:44:48 +00:00
try {
2026-02-09 10:55:46 +00:00
await this . bridge . stopProxy ( ) ;
} catch {
// Ignore if already stopped
2025-05-20 15:44:48 +00:00
}
2026-02-09 10:55:46 +00:00
this . bridge . kill ( ) ;
// Stop socket handler relay
if ( this . socketHandlerServer ) {
await this . socketHandlerServer . stop ( ) ;
this . socketHandlerServer = null ;
}
logger . log ( 'info' , 'SmartProxy shutdown complete.' , { component : 'smart-proxy' } ) ;
2025-05-19 03:40:58 +00:00
}
2026-02-09 10:55:46 +00:00
2025-05-10 00:01:02 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Update routes atomically .
2025-05-10 00:01:02 +00:00
* /
public async updateRoutes ( newRoutes : IRouteConfig [ ] ) : Promise < void > {
2025-05-19 03:40:58 +00:00
return this . routeUpdateLock . runExclusive ( async ( ) = > {
2026-02-09 10:55:46 +00:00
// Validate
const validation = RouteValidator . validateRoutes ( newRoutes ) ;
if ( ! validation . valid ) {
RouteValidator . logValidationErrors ( validation . errors ) ;
throw new Error ( ` Route validation failed: ${ validation . errors . size } route(s) have errors ` ) ;
2025-05-20 15:44:48 +00:00
}
2025-05-10 00:49:39 +00:00
2026-02-09 10:55:46 +00:00
// Preprocess for Rust
const rustRoutes = this . preprocessor . preprocessForRust ( newRoutes ) ;
// Send to Rust
await this . bridge . updateRoutes ( rustRoutes ) ;
// Update local route manager
this . routeManager . updateRoutes ( newRoutes ) ;
// Update socket handler relay if handler routes changed
const hasHandlerRoutes = newRoutes . some (
( r ) = >
( r . action . type === 'socket-handler' && r . action . socketHandler ) ||
r . action . targets ? . some ( ( t ) = > typeof t . host === 'function' || typeof t . port === 'function' )
2025-05-19 03:40:58 +00:00
) ;
2026-02-09 10:55:46 +00:00
if ( hasHandlerRoutes && ! this . socketHandlerServer ) {
this . socketHandlerServer = new SocketHandlerServer ( this . preprocessor ) ;
await this . socketHandlerServer . start ( ) ;
await this . bridge . setSocketHandlerRelay ( this . socketHandlerServer . getSocketPath ( ) ) ;
} else if ( ! hasHandlerRoutes && this . socketHandlerServer ) {
await this . socketHandlerServer . stop ( ) ;
this . socketHandlerServer = null ;
2025-05-19 03:40:58 +00:00
}
2026-02-09 10:55:46 +00:00
// Update stored routes
this . settings . routes = newRoutes ;
// Handle cert provisioning for new routes
await this . provisionCertificatesViaCallback ( ) ;
logger . log ( 'info' , ` Routes updated ( ${ newRoutes . length } routes) ` , { component : 'smart-proxy' } ) ;
2025-05-19 03:40:58 +00:00
} ) ;
2025-05-10 00:01:02 +00:00
}
2026-02-09 10:55:46 +00:00
2025-03-25 22:30:57 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Provision a certificate for a named route .
2025-03-25 22:30:57 +00:00
* /
2025-05-18 15:38:07 +00:00
public async provisionCertificate ( routeName : string ) : Promise < void > {
2026-02-09 10:55:46 +00:00
await this . bridge . provisionCertificate ( routeName ) ;
2025-05-18 15:38:07 +00:00
}
2025-05-20 15:32:19 +00:00
2025-05-18 15:38:07 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Force renewal of a certificate .
2025-05-18 15:38:07 +00:00
* /
public async renewCertificate ( routeName : string ) : Promise < void > {
2026-02-09 10:55:46 +00:00
await this . bridge . renewCertificate ( routeName ) ;
2025-05-18 15:38:07 +00:00
}
2026-02-09 10:55:46 +00:00
2025-05-18 15:38:07 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Get certificate status for a route ( async - calls Rust ) .
2025-05-18 15:38:07 +00:00
* /
2026-02-09 10:55:46 +00:00
public async getCertificateStatus ( routeName : string ) : Promise < any > {
return this . bridge . getCertificateStatus ( routeName ) ;
2025-03-25 22:30:57 +00:00
}
2026-02-09 10:55:46 +00:00
2025-06-09 15:02:36 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Get the metrics interface .
2025-06-09 15:02:36 +00:00
* /
2025-06-22 22:28:37 +00:00
public getMetrics ( ) : IMetrics {
2026-02-09 10:55:46 +00:00
return this . metricsAdapter ;
2025-06-09 15:02:36 +00:00
}
2026-02-09 10:55:46 +00:00
2025-03-25 22:30:57 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Get statistics ( async - calls Rust ) .
2025-03-25 22:30:57 +00:00
* /
2026-02-09 10:55:46 +00:00
public async getStatistics ( ) : Promise < any > {
return this . bridge . getStatistics ( ) ;
2025-03-25 22:30:57 +00:00
}
2026-02-09 10:55:46 +00:00
2025-05-13 12:48:41 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Add a listening port at runtime .
2025-05-13 12:48:41 +00:00
* /
public async addListeningPort ( port : number ) : Promise < void > {
2026-02-09 10:55:46 +00:00
await this . bridge . addListeningPort ( port ) ;
2025-05-13 12:48:41 +00:00
}
/ * *
2026-02-09 10:55:46 +00:00
* Remove a listening port at runtime .
2025-05-13 12:48:41 +00:00
* /
public async removeListeningPort ( port : number ) : Promise < void > {
2026-02-09 10:55:46 +00:00
await this . bridge . removeListeningPort ( port ) ;
2025-05-13 12:48:41 +00:00
}
/ * *
2026-02-09 10:55:46 +00:00
* Get all currently listening ports ( async - calls Rust ) .
2025-05-13 12:48:41 +00:00
* /
2026-02-09 10:55:46 +00:00
public async getListeningPorts ( ) : Promise < number [ ] > {
2026-02-09 16:25:33 +00:00
if ( ! this . bridge . running ) return [ ] ;
2026-02-09 10:55:46 +00:00
return this . bridge . getListeningPorts ( ) ;
2025-05-13 12:48:41 +00:00
}
2025-03-25 22:30:57 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Get eligible domains for ACME certificates ( sync - reads local routes ) .
2025-03-25 22:30:57 +00:00
* /
public getEligibleDomainsForCertificates ( ) : string [ ] {
const domains : string [ ] = [ ] ;
2026-02-09 10:55:46 +00:00
for ( const route of this . settings . routes || [ ] ) {
2025-05-10 00:01:02 +00:00
if ( ! route . match . domains ) continue ;
2026-02-09 10:55:46 +00:00
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 ] ;
const eligible = routeDomains . filter ( ( d ) = > ! d . includes ( '*' ) && this . isValidDomain ( d ) ) ;
domains . push ( . . . eligible ) ;
2025-03-25 22:30:57 +00:00
}
return domains ;
}
2026-02-09 10:55:46 +00:00
2025-05-15 14:35:01 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Get NFTables status ( async - calls Rust ) .
2025-05-15 14:35:01 +00:00
* /
public async getNfTablesStatus ( ) : Promise < Record < string , any > > {
2026-02-09 10:55:46 +00:00
return this . bridge . getNftablesStatus ( ) ;
2025-05-15 14:35:01 +00:00
}
2026-02-09 10:55:46 +00:00
// --- Private helpers ---
2025-05-18 18:29:59 +00:00
/ * *
2026-02-09 10:55:46 +00:00
* Build the Rust configuration object from TS settings .
2025-05-18 18:29:59 +00:00
* /
2026-02-13 13:08:30 +00:00
private buildRustConfig ( routes : IRouteConfig [ ] , acmeOverride? : IAcmeOptions ) : any {
const acme = acmeOverride !== undefined ? acmeOverride : this.settings.acme ;
2026-02-09 10:55:46 +00:00
return {
routes ,
defaults : this.settings.defaults ,
2026-02-13 13:08:30 +00:00
acme : acme
2026-02-09 10:55:46 +00:00
? {
2026-02-13 13:08:30 +00:00
enabled : acme.enabled ,
email : acme.email ,
useProduction : acme.useProduction ,
port : acme.port ,
renewThresholdDays : acme.renewThresholdDays ,
autoRenew : acme.autoRenew ,
certificateStore : acme.certificateStore ,
renewCheckIntervalHours : acme.renewCheckIntervalHours ,
2026-02-09 10:55:46 +00:00
}
: undefined ,
connectionTimeout : this.settings.connectionTimeout ,
initialDataTimeout : this.settings.initialDataTimeout ,
socketTimeout : this.settings.socketTimeout ,
maxConnectionLifetime : this.settings.maxConnectionLifetime ,
gracefulShutdownTimeout : this.settings.gracefulShutdownTimeout ,
maxConnectionsPerIp : this.settings.maxConnectionsPerIP ,
connectionRateLimitPerMinute : this.settings.connectionRateLimitPerMinute ,
keepAliveTreatment : this.settings.keepAliveTreatment ,
keepAliveInactivityMultiplier : this.settings.keepAliveInactivityMultiplier ,
extendedKeepAliveLifetime : this.settings.extendedKeepAliveLifetime ,
acceptProxyProtocol : this.settings.acceptProxyProtocol ,
sendProxyProtocol : this.settings.sendProxyProtocol ,
} ;
}
/ * *
* For routes with certificate : 'auto' , call certProvisionFunction if set .
* If the callback returns a cert object , load it into Rust .
* If it returns 'http01' , let Rust handle ACME .
* /
private async provisionCertificatesViaCallback ( ) : Promise < void > {
const provisionFn = this . settings . certProvisionFunction ;
if ( ! provisionFn ) return ;
2026-02-13 13:08:30 +00:00
const provisionedDomains = new Set < string > ( ) ;
2026-02-09 10:55:46 +00:00
for ( const route of this . settings . routes ) {
if ( route . action . tls ? . certificate !== 'auto' ) continue ;
if ( ! route . match . domains ) continue ;
2026-02-13 13:08:30 +00:00
const rawDomains = Array . isArray ( route . match . domains ) ? route . match . domains : [ route . match . domains ] ;
const certDomains = this . normalizeDomainsForCertProvisioning ( rawDomains ) ;
2026-02-09 10:55:46 +00:00
2026-02-13 13:08:30 +00:00
for ( const domain of certDomains ) {
if ( provisionedDomains . has ( domain ) ) continue ;
provisionedDomains . add ( domain ) ;
2026-02-09 10:55:46 +00:00
try {
const result : TSmartProxyCertProvisionObject = await provisionFn ( domain ) ;
if ( result === 'http01' ) {
2026-02-13 13:08:30 +00:00
// Callback wants HTTP-01 for this domain — trigger Rust ACME explicitly
if ( route . name ) {
try {
await this . bridge . provisionCertificate ( route . name ) ;
logger . log ( 'info' , ` Triggered Rust ACME for ${ domain } (route: ${ route . name } ) ` , { component : 'smart-proxy' } ) ;
} catch ( provisionErr : any ) {
logger . log ( 'warn' , ` Cannot provision cert for ${ domain } — callback returned 'http01' but Rust ACME failed: ${ provisionErr . message } ` , { component : 'smart-proxy' } ) ;
}
}
2026-02-09 10:55:46 +00:00
continue ;
}
// Got a static cert object - load it into Rust
if ( result && typeof result === 'object' ) {
const certObj = result as plugins . tsclass . network . ICert ;
await this . bridge . loadCertificate (
domain ,
certObj . publicKey ,
certObj . privateKey ,
) ;
logger . log ( 'info' , ` Certificate loaded via provision function for ${ domain } ` , { component : 'smart-proxy' } ) ;
}
} catch ( err : any ) {
logger . log ( 'warn' , ` certProvisionFunction failed for ${ domain } : ${ err . message } ` , { component : 'smart-proxy' } ) ;
// Fallback to ACME if enabled
if ( this . settings . certProvisionFallbackToAcme !== false ) {
logger . log ( 'info' , ` Falling back to ACME for ${ domain } ` , { component : 'smart-proxy' } ) ;
}
}
2025-05-18 18:29:59 +00:00
}
}
}
2025-05-15 14:35:01 +00:00
2026-02-13 13:08:30 +00:00
/ * *
* Normalize routing glob patterns into valid domain identifiers for cert provisioning .
* - ` *nevermind.cloud ` → ` ['nevermind.cloud', '*.nevermind.cloud'] `
* - ` *.lossless.digital ` → ` ['*.lossless.digital'] ` ( already valid wildcard )
* - ` code.foss.global ` → ` ['code.foss.global'] ` ( plain domain )
* - ` *mid*.example.com ` → skipped with warning ( unsupported glob )
* /
private normalizeDomainsForCertProvisioning ( rawDomains : string [ ] ) : string [ ] {
const result : string [ ] = [ ] ;
for ( const raw of rawDomains ) {
// Plain domain — no glob characters
if ( ! raw . includes ( '*' ) ) {
result . push ( raw ) ;
continue ;
}
// Valid wildcard: *.example.com
if ( raw . startsWith ( '*.' ) && ! raw . slice ( 2 ) . includes ( '*' ) ) {
result . push ( raw ) ;
continue ;
}
// Routing glob like *example.com (leading star, no dot after it)
// Convert to bare domain + wildcard pair
if ( raw . startsWith ( '*' ) && ! raw . startsWith ( '*.' ) && ! raw . slice ( 1 ) . includes ( '*' ) ) {
const baseDomain = raw . slice ( 1 ) ; // Remove leading *
result . push ( baseDomain ) ;
result . push ( ` *. ${ baseDomain } ` ) ;
continue ;
}
// Unsupported glob pattern (e.g. *mid*.example.com)
logger . log ( 'warn' , ` Skipping unsupported glob pattern for cert provisioning: ${ raw } ` , { component : 'smart-proxy' } ) ;
}
return result ;
}
2026-02-09 10:55:46 +00:00
private isValidDomain ( domain : string ) : boolean {
if ( ! domain || domain . length === 0 ) return false ;
if ( domain . includes ( '*' ) ) return false ;
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])?)*$/ ;
return validDomainRegex . test ( domain ) ;
}
}