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'; /** * 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(); constructor(private settings: ISmartProxyOptions) {} /** * Updates the domain configurations */ public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void { this.settings.domainConfigs = 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[] { return this.settings.domainConfigs; } /** * Find domain config matching a server name */ public findDomainConfig(serverName: string): IDomainConfig | undefined { if (!serverName) return undefined; return this.settings.domainConfigs.find((config) => config.domains.some((d) => plugins.minimatch(serverName, d)) ); } /** * Find domain config for a specific port */ public findDomainConfigForPort(port: number): IDomainConfig | undefined { return this.settings.domainConfigs.find( (domain) => { const portRanges = domain.forwarding?.advanced?.portRanges; return portRanges && portRanges.length > 0 && this.isPortInRanges(port, portRanges); } ); } /** * 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; } }