import * as plugins from '../../plugins.js'; import type { IDomainConfig, ISmartProxyOptions } from './models/interfaces.js'; import type { TForwardingType, IForwardConfig } from '../../forwarding/config/forwarding-types.js'; import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; import { ForwardingHandlerFactory } from '../../forwarding/factory/forwarding-factory.js'; import type { IRouteConfig } from './models/route-types.js'; import { RouteManager } from './route-manager.js'; /** * Manages domain configurations and target selection */ export class DomainConfigManager { // Track round-robin indices for domain configs private domainTargetIndices: Map = new Map(); // Cache forwarding handlers for each domain config private forwardingHandlers: Map = new Map(); // Store derived domain configs from routes private derivedDomainConfigs: IDomainConfig[] = []; // Reference to RouteManager for route-based configuration private routeManager?: RouteManager; constructor(private settings: ISmartProxyOptions) { // Initialize with derived domain configs if using route-based configuration if (settings.routes && !settings.domainConfigs) { this.generateDomainConfigsFromRoutes(); } } /** * Set the route manager reference for route-based queries */ public setRouteManager(routeManager: RouteManager): void { this.routeManager = routeManager; // Regenerate domain configs from routes if needed if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) { this.generateDomainConfigsFromRoutes(); } } /** * Generate domain configs from routes */ public generateDomainConfigsFromRoutes(): void { this.derivedDomainConfigs = []; if (!this.settings.routes) return; for (const route of this.settings.routes) { if (route.action.type !== 'forward' || !route.match.domains) continue; // Convert route to domain config const domainConfig = this.routeToDomainConfig(route); if (domainConfig) { this.derivedDomainConfigs.push(domainConfig); } } } /** * Convert a route to a domain config */ private routeToDomainConfig(route: IRouteConfig): IDomainConfig | null { if (route.action.type !== 'forward' || !route.action.target) return null; // Get domains from route const domains = Array.isArray(route.match.domains) ? route.match.domains : (route.match.domains ? [route.match.domains] : []); if (domains.length === 0) return null; // Determine forwarding type based on TLS mode let forwardingType: TForwardingType = 'http-only'; if (route.action.tls) { switch (route.action.tls.mode) { case 'passthrough': forwardingType = 'https-passthrough'; break; case 'terminate': forwardingType = 'https-terminate-to-http'; break; case 'terminate-and-reencrypt': forwardingType = 'https-terminate-to-https'; break; } } // Create domain config return { domains, forwarding: { type: forwardingType, target: { host: route.action.target.host, port: route.action.target.port }, security: route.action.security ? { allowedIps: route.action.security.allowedIps, blockedIps: route.action.security.blockedIps, maxConnections: route.action.security.maxConnections } : undefined, https: route.action.tls && route.action.tls.certificate !== 'auto' ? { customCert: route.action.tls.certificate } : undefined, advanced: route.action.advanced } }; } /** * Updates the domain configurations */ public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void { // If we're using domainConfigs property, update it if (this.settings.domainConfigs) { this.settings.domainConfigs = newDomainConfigs; } else { // Otherwise update our derived configs this.derivedDomainConfigs = newDomainConfigs; } // Reset target indices for removed configs const currentConfigSet = new Set(newDomainConfigs); for (const [config] of this.domainTargetIndices) { if (!currentConfigSet.has(config)) { this.domainTargetIndices.delete(config); } } // Clear handlers for removed configs and create handlers for new configs const handlersToRemove: IDomainConfig[] = []; for (const [config] of this.forwardingHandlers) { if (!currentConfigSet.has(config)) { handlersToRemove.push(config); } } // Remove handlers that are no longer needed for (const config of handlersToRemove) { this.forwardingHandlers.delete(config); } // Create handlers for new configs for (const config of newDomainConfigs) { if (!this.forwardingHandlers.has(config)) { try { const handler = this.createForwardingHandler(config); this.forwardingHandlers.set(config, handler); } catch (err) { console.log(`Error creating forwarding handler for domain ${config.domains.join(', ')}: ${err}`); } } } } /** * Get all domain configurations */ public getDomainConfigs(): IDomainConfig[] { // Use domainConfigs from settings if available, otherwise use derived configs return this.settings.domainConfigs || this.derivedDomainConfigs; } /** * Find domain config matching a server name */ public findDomainConfig(serverName: string): IDomainConfig | undefined { if (!serverName) return undefined; // Get domain configs from the appropriate source const domainConfigs = this.getDomainConfigs(); // Check for direct match for (const config of domainConfigs) { if (config.domains.some(d => plugins.minimatch(serverName, d))) { return config; } } // No match found return undefined; } /** * Find domain config for a specific port */ public findDomainConfigForPort(port: number): IDomainConfig | undefined { // Get domain configs from the appropriate source const domainConfigs = this.getDomainConfigs(); // Check if any domain config has a matching port range for (const domain of domainConfigs) { const portRanges = domain.forwarding?.advanced?.portRanges; if (portRanges && portRanges.length > 0 && this.isPortInRanges(port, portRanges)) { return domain; } } // If we're in route-based mode, also check routes for this port if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) { const routesForPort = this.settings.routes.filter(route => { // Check if this port is in the route's ports if (typeof route.match.ports === 'number') { return route.match.ports === port; } else if (Array.isArray(route.match.ports)) { return route.match.ports.some(p => { if (typeof p === 'number') { return p === port; } else if (p.from && p.to) { return port >= p.from && port <= p.to; } return false; }); } return false; }); // If we found any routes for this port, convert the first one to a domain config if (routesForPort.length > 0 && routesForPort[0].action.type === 'forward') { const domainConfig = this.routeToDomainConfig(routesForPort[0]); if (domainConfig) { return domainConfig; } } } return undefined; } /** * Check if a port is within any of the given ranges */ public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean { return ranges.some((range) => port >= range.from && port <= range.to); } /** * Get target IP with round-robin support */ public getTargetIP(domainConfig: IDomainConfig): string { const targetHosts = Array.isArray(domainConfig.forwarding.target.host) ? domainConfig.forwarding.target.host : [domainConfig.forwarding.target.host]; if (targetHosts.length > 0) { const currentIndex = this.domainTargetIndices.get(domainConfig) || 0; const ip = targetHosts[currentIndex % targetHosts.length]; this.domainTargetIndices.set(domainConfig, currentIndex + 1); return ip; } return this.settings.targetIP || 'localhost'; } /** * Get target host with round-robin support (for tests) * This is just an alias for getTargetIP for easier test compatibility */ public getTargetHost(domainConfig: IDomainConfig): string { return this.getTargetIP(domainConfig); } /** * Get target port from domain config */ public getTargetPort(domainConfig: IDomainConfig, defaultPort: number): number { return domainConfig.forwarding.target.port || defaultPort; } /** * Checks if a domain should use NetworkProxy */ public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean { const forwardingType = this.getForwardingType(domainConfig); return forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-https'; } /** * Gets the NetworkProxy port for a domain */ public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined { // First check if we should use NetworkProxy at all if (!this.shouldUseNetworkProxy(domainConfig)) { return undefined; } return domainConfig.forwarding.advanced?.networkProxyPort || this.settings.networkProxyPort; } /** * Get effective allowed and blocked IPs for a domain * * This method combines domain-specific security rules from the forwarding configuration * with global security defaults when necessary. */ public getEffectiveIPRules(domainConfig: IDomainConfig): { allowedIPs: string[], blockedIPs: string[] } { // Start with empty arrays const allowedIPs: string[] = []; const blockedIPs: string[] = []; // Add IPs from forwarding security settings if available if (domainConfig.forwarding?.security?.allowedIps) { allowedIPs.push(...domainConfig.forwarding.security.allowedIps); } else { // If no allowed IPs are specified in forwarding config and global defaults exist, use them if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { allowedIPs.push(...this.settings.defaultAllowedIPs); } else { // Default to allow all if no specific rules allowedIPs.push('*'); } } // Add blocked IPs from forwarding security settings if available if (domainConfig.forwarding?.security?.blockedIps) { blockedIPs.push(...domainConfig.forwarding.security.blockedIps); } // Always add global blocked IPs, even if domain has its own rules // This ensures that global blocks take precedence if (this.settings.defaultBlockedIPs && this.settings.defaultBlockedIPs.length > 0) { // Add only unique IPs that aren't already in the list for (const ip of this.settings.defaultBlockedIPs) { if (!blockedIPs.includes(ip)) { blockedIPs.push(ip); } } } return { allowedIPs, blockedIPs }; } /** * Get connection timeout for a domain */ public getConnectionTimeout(domainConfig?: IDomainConfig): number { if (domainConfig?.forwarding.advanced?.timeout) { return domainConfig.forwarding.advanced.timeout; } return this.settings.maxConnectionLifetime || 86400000; // 24 hours default } /** * Creates a forwarding handler for a domain configuration */ private createForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler { // Create a new handler using the factory const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding); // Initialize the handler handler.initialize().catch(err => { console.log(`Error initializing forwarding handler for ${domainConfig.domains.join(', ')}: ${err}`); }); return handler; } /** * Gets a forwarding handler for a domain config * If no handler exists, creates one */ public getForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler { // If we already have a handler, return it if (this.forwardingHandlers.has(domainConfig)) { return this.forwardingHandlers.get(domainConfig)!; } // Otherwise create a new handler const handler = this.createForwardingHandler(domainConfig); this.forwardingHandlers.set(domainConfig, handler); return handler; } /** * Gets the forwarding type for a domain config */ public getForwardingType(domainConfig?: IDomainConfig): TForwardingType | undefined { if (!domainConfig?.forwarding) return undefined; return domainConfig.forwarding.type; } /** * Checks if the forwarding type requires TLS termination */ public requiresTlsTermination(domainConfig?: IDomainConfig): boolean { if (!domainConfig) return false; const forwardingType = this.getForwardingType(domainConfig); return forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-https'; } /** * Checks if the forwarding type supports HTTP */ public supportsHttp(domainConfig?: IDomainConfig): boolean { if (!domainConfig) return false; const forwardingType = this.getForwardingType(domainConfig); // HTTP-only always supports HTTP if (forwardingType === 'http-only') return true; // For termination types, check the HTTP settings if (forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-https') { // HTTP is supported by default for termination types return domainConfig.forwarding?.http?.enabled !== false; } // HTTPS-passthrough doesn't support HTTP return false; } /** * Checks if HTTP requests should be redirected to HTTPS */ public shouldRedirectToHttps(domainConfig?: IDomainConfig): boolean { if (!domainConfig?.forwarding) return false; // Only check for redirect if HTTP is enabled if (this.supportsHttp(domainConfig)) { return !!domainConfig.forwarding.http?.redirectToHttps; } return false; } }