feat(forwarding): Add unified forwarding system docs and tests; update build script and .gitignore
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '10.2.0', | ||||
|   version: '10.3.0', | ||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
							
								
								
									
										87
									
								
								ts/common/port80-adapter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								ts/common/port80-adapter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| import type {  | ||||
|   IForwardConfig as ILegacyForwardConfig,  | ||||
|   IDomainOptions  | ||||
| } from './types.js'; | ||||
|  | ||||
| import type {  | ||||
|   IForwardConfig as INewForwardConfig  | ||||
| } from '../smartproxy/types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Converts a new forwarding configuration target to the legacy format | ||||
|  * for Port80Handler | ||||
|  */ | ||||
| export function convertToLegacyForwardConfig( | ||||
|   forwardConfig: INewForwardConfig | ||||
| ): ILegacyForwardConfig { | ||||
|   // Determine host from the target configuration | ||||
|   const host = Array.isArray(forwardConfig.target.host) | ||||
|     ? forwardConfig.target.host[0]  // Use the first host in the array | ||||
|     : forwardConfig.target.host; | ||||
|    | ||||
|   return { | ||||
|     ip: host, | ||||
|     port: forwardConfig.target.port | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates Port80Handler domain options from a domain name and forwarding config | ||||
|  */ | ||||
| export function createPort80HandlerOptions( | ||||
|   domain: string, | ||||
|   forwardConfig: INewForwardConfig | ||||
| ): IDomainOptions { | ||||
|   // Determine if we should redirect HTTP to HTTPS | ||||
|   let sslRedirect = false; | ||||
|   if (forwardConfig.http?.redirectToHttps) { | ||||
|     sslRedirect = true; | ||||
|   } | ||||
|    | ||||
|   // Determine if ACME maintenance should be enabled | ||||
|   // Enable by default for termination types, unless explicitly disabled | ||||
|   const requiresTls =  | ||||
|     forwardConfig.type === 'https-terminate-to-http' ||  | ||||
|     forwardConfig.type === 'https-terminate-to-https'; | ||||
|    | ||||
|   const acmeMaintenance =  | ||||
|     requiresTls &&  | ||||
|     forwardConfig.acme?.enabled !== false; | ||||
|    | ||||
|   // Set up forwarding configuration | ||||
|   const options: IDomainOptions = { | ||||
|     domainName: domain, | ||||
|     sslRedirect, | ||||
|     acmeMaintenance | ||||
|   }; | ||||
|    | ||||
|   // Add ACME challenge forwarding if configured | ||||
|   if (forwardConfig.acme?.forwardChallenges) { | ||||
|     options.acmeForward = { | ||||
|       ip: Array.isArray(forwardConfig.acme.forwardChallenges.host)  | ||||
|         ? forwardConfig.acme.forwardChallenges.host[0] | ||||
|         : forwardConfig.acme.forwardChallenges.host, | ||||
|       port: forwardConfig.acme.forwardChallenges.port | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   // Add HTTP forwarding if this is an HTTP-only config or if HTTP is enabled | ||||
|   const supportsHttp =  | ||||
|     forwardConfig.type === 'http-only' ||  | ||||
|     (forwardConfig.http?.enabled !== false && | ||||
|      (forwardConfig.type === 'https-terminate-to-http' ||  | ||||
|       forwardConfig.type === 'https-terminate-to-https')); | ||||
|    | ||||
|   if (supportsHttp) { | ||||
|     options.forward = { | ||||
|       ip: Array.isArray(forwardConfig.target.host)  | ||||
|         ? forwardConfig.target.host[0] | ||||
|         : forwardConfig.target.host, | ||||
|       port: forwardConfig.target.port | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   return options; | ||||
| } | ||||
							
								
								
									
										120
									
								
								ts/examples/forwarding-example.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								ts/examples/forwarding-example.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { createServer } from 'http'; | ||||
| import { Socket } from 'net'; | ||||
| import { | ||||
|   DomainManager, | ||||
|   DomainManagerEvents, | ||||
|   createDomainConfig, | ||||
|   helpers | ||||
| } from '../smartproxy/forwarding/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Example showing how to use the unified forwarding system | ||||
|  */ | ||||
| async function main() { | ||||
|   console.log('Initializing forwarding example...'); | ||||
|    | ||||
|   // Create the domain manager | ||||
|   const domainManager = new DomainManager(); | ||||
|    | ||||
|   // Set up event listeners | ||||
|   domainManager.on(DomainManagerEvents.DOMAIN_ADDED, (data) => { | ||||
|     console.log(`Domain added: ${data.domains.join(', ')} (${data.forwardingType})`); | ||||
|   }); | ||||
|    | ||||
|   domainManager.on(DomainManagerEvents.DOMAIN_MATCHED, (data) => { | ||||
|     console.log(`Domain matched: ${data.domain} (${data.handlerType})`); | ||||
|   }); | ||||
|    | ||||
|   domainManager.on(DomainManagerEvents.DOMAIN_MATCH_FAILED, (data) => { | ||||
|     console.log(`Domain match failed: ${data.domain}`); | ||||
|   }); | ||||
|    | ||||
|   domainManager.on(DomainManagerEvents.ERROR, (data) => { | ||||
|     console.error(`Error:`, data); | ||||
|   }); | ||||
|    | ||||
|   // Add example domains with different forwarding types | ||||
|    | ||||
|   // Example 1: HTTP-only forwarding | ||||
|   await domainManager.addDomainConfig( | ||||
|     createDomainConfig('example.com', helpers.httpOnly('localhost', 3000)) | ||||
|   ); | ||||
|    | ||||
|   // Example 2: HTTPS termination with HTTP backend | ||||
|   await domainManager.addDomainConfig( | ||||
|     createDomainConfig('secure.example.com', helpers.tlsTerminateToHttp('localhost', 3000)) | ||||
|   ); | ||||
|    | ||||
|   // Example 3: HTTPS termination with HTTPS backend | ||||
|   await domainManager.addDomainConfig( | ||||
|     createDomainConfig('api.example.com', helpers.tlsTerminateToHttps('localhost', 8443)) | ||||
|   ); | ||||
|    | ||||
|   // Example 4: SNI passthrough | ||||
|   await domainManager.addDomainConfig( | ||||
|     createDomainConfig('passthrough.example.com', helpers.sniPassthrough('10.0.0.5', 443)) | ||||
|   ); | ||||
|    | ||||
|   // Example 5: Custom configuration for a more complex setup | ||||
|   await domainManager.addDomainConfig( | ||||
|     createDomainConfig(['*.example.com', '*.example.org'], { | ||||
|       type: 'https-terminate-to-http', | ||||
|       target: { | ||||
|         host: ['10.0.0.10', '10.0.0.11'], // Round-robin load balancing | ||||
|         port: 8080 | ||||
|       }, | ||||
|       http: { | ||||
|         enabled: true, | ||||
|         redirectToHttps: false // Allow both HTTP and HTTPS | ||||
|       }, | ||||
|       acme: { | ||||
|         enabled: true, | ||||
|         maintenance: true, | ||||
|         production: false,  // Use staging for testing | ||||
|         forwardChallenges: { | ||||
|           host: '192.168.1.100', | ||||
|           port: 8080 | ||||
|         } | ||||
|       }, | ||||
|       security: { | ||||
|         allowedIps: ['10.0.0.*', '192.168.1.*'], | ||||
|         maxConnections: 100 | ||||
|       }, | ||||
|       advanced: { | ||||
|         headers: { | ||||
|           'X-Forwarded-For': '{clientIp}', | ||||
|           'X-Forwarded-Host': '{sni}' | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   ); | ||||
|    | ||||
|   // Create a simple HTTP server to demonstrate HTTP handler | ||||
|   const httpServer = createServer((req, res) => { | ||||
|     // Extract the domain from the Host header | ||||
|     const domain = req.headers.host?.split(':')[0] || 'unknown'; | ||||
|      | ||||
|     // Forward the request to the appropriate handler | ||||
|     if (!domainManager.handleHttpRequest(domain, req, res)) { | ||||
|       // No handler found, send a default response | ||||
|       res.statusCode = 404; | ||||
|       res.end(`No handler found for domain: ${domain}`); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|   // Listen on HTTP port | ||||
|   httpServer.listen(80, () => { | ||||
|     console.log('HTTP server listening on port 80'); | ||||
|   }); | ||||
|    | ||||
|   // For HTTPS and SNI, we would need to set up a TLS server | ||||
|   // This is a simplified example that just shows how the domain manager works | ||||
|    | ||||
|   console.log('Forwarding example initialized successfully'); | ||||
| } | ||||
|  | ||||
| // Run the example | ||||
| main().catch(error => { | ||||
|   console.error('Error running example:', error); | ||||
| }); | ||||
| @@ -7,3 +7,6 @@ export * from './smartproxy/classes.pp.snihandler.js'; | ||||
| export * from './smartproxy/classes.pp.interfaces.js'; | ||||
|  | ||||
| export * from './common/types.js'; | ||||
|  | ||||
| // Export forwarding system | ||||
| export * as forwarding from './smartproxy/forwarding/index.js'; | ||||
| @@ -11,6 +11,8 @@ import { TlsManager } from './classes.pp.tlsmanager.js'; | ||||
| import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; | ||||
| import { TimeoutManager } from './classes.pp.timeoutmanager.js'; | ||||
| import { PortRangeManager } from './classes.pp.portrangemanager.js'; | ||||
| import type { IForwardingHandler } from './forwarding/forwarding.handler.js'; | ||||
| import type { ForwardingType } from './types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handles new connection processing and setup logic | ||||
| @@ -176,37 +178,73 @@ export class ConnectionHandler { | ||||
|             destPort: socket.localPort || 0, | ||||
|           }; | ||||
|  | ||||
|           // Extract SNI for domain-specific NetworkProxy handling if available | ||||
|           // Extract SNI for domain-specific forwarding if available | ||||
|           const serverName = this.tlsManager.extractSNI(chunk, connInfo); | ||||
|  | ||||
|           // For NetworkProxy connections, we'll allow session tickets even without SNI | ||||
|           // We'll only use the serverName if available to determine the specific NetworkProxy port | ||||
|           // We'll only use the serverName if available to determine the specific forwarding | ||||
|           if (serverName) { | ||||
|             // Save domain config and SNI in connection record | ||||
|             const domainConfig = this.domainConfigManager.findDomainConfig(serverName); | ||||
|             record.domainConfig = domainConfig; | ||||
|             record.lockedDomain = serverName; | ||||
|  | ||||
|             // Use domain-specific NetworkProxy port if configured | ||||
|             if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) { | ||||
|               const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig); | ||||
|             // If we have a domain config and it has a forwarding config | ||||
|             if (domainConfig) { | ||||
|               try { | ||||
|                 // Get the forwarding type for this domain | ||||
|                 const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); | ||||
|  | ||||
|               if (this.settings.enableDetailedLogging) { | ||||
|                 console.log( | ||||
|                   `[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}` | ||||
|                 ); | ||||
|                 // For TLS termination types, use NetworkProxy | ||||
|                 if (forwardingType === 'https-terminate-to-http' || | ||||
|                     forwardingType === 'https-terminate-to-https') { | ||||
|                   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 | ||||
|                   this.networkProxyBridge.forwardToNetworkProxy( | ||||
|                     connectionId, | ||||
|                     socket, | ||||
|                     record, | ||||
|                     chunk, | ||||
|                     networkProxyPort, | ||||
|                     (reason) => this.connectionManager.initiateCleanupOnce(record, reason) | ||||
|                   ); | ||||
|                   return; | ||||
|                 } | ||||
|  | ||||
|                 // For HTTPS passthrough, use the forwarding handler directly | ||||
|                 if (forwardingType === 'https-passthrough') { | ||||
|                   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 | ||||
|                   handler.handleConnection(socket); | ||||
|  | ||||
|                   return; | ||||
|                 } | ||||
|  | ||||
|                 // For HTTP-only, we shouldn't get TLS connections | ||||
|                 if (forwardingType === 'http-only') { | ||||
|                   console.log(`[${connectionId}] Received TLS connection for HTTP-only domain ${serverName}`); | ||||
|                   socket.end(); | ||||
|                   this.connectionManager.cleanupConnection(record, 'wrong_protocol'); | ||||
|                   return; | ||||
|                 } | ||||
|               } catch (err) { | ||||
|                 console.log(`[${connectionId}] Error using forwarding handler: ${err}`); | ||||
|                 // Fall through to default NetworkProxy handling | ||||
|               } | ||||
|  | ||||
|               // Forward to NetworkProxy with domain-specific port | ||||
|               this.networkProxyBridge.forwardToNetworkProxy( | ||||
|                 connectionId, | ||||
|                 socket, | ||||
|                 record, | ||||
|                 chunk, | ||||
|                 networkProxyPort, | ||||
|                 (reason) => this.connectionManager.initiateCleanupOnce(record, reason) | ||||
|               ); | ||||
|               return; | ||||
|             } | ||||
|           } else if ( | ||||
|             this.settings.allowSessionTicket === false && | ||||
| @@ -229,10 +267,38 @@ export class ConnectionHandler { | ||||
|           (reason) => this.connectionManager.initiateCleanupOnce(record, reason) | ||||
|         ); | ||||
|       } else { | ||||
|         // If not TLS, use normal direct connection | ||||
|         // If not TLS, handle as plain HTTP | ||||
|         console.log( | ||||
|           `[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}` | ||||
|         ); | ||||
|  | ||||
|         // Check if we have a domain config based on port | ||||
|         const portBasedDomainConfig = this.domainConfigManager.findDomainConfigForPort(record.localPort); | ||||
|  | ||||
|         if (portBasedDomainConfig) { | ||||
|           try { | ||||
|             // If this domain supports HTTP via a forwarding handler, use it | ||||
|             if (this.domainConfigManager.supportsHttp(portBasedDomainConfig)) { | ||||
|               const handler = this.domainConfigManager.getForwardingHandler(portBasedDomainConfig); | ||||
|  | ||||
|               if (this.settings.enableDetailedLogging) { | ||||
|                 console.log( | ||||
|                   `[${connectionId}] Using forwarding handler for non-TLS connection to port ${record.localPort}` | ||||
|                 ); | ||||
|               } | ||||
|  | ||||
|               // Handle the connection using the handler | ||||
|               handler.handleConnection(socket); | ||||
|  | ||||
|               return; | ||||
|             } | ||||
|           } catch (err) { | ||||
|             console.log(`[${connectionId}] Error using forwarding handler for HTTP: ${err}`); | ||||
|             // Fall through to direct connection | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         // Use legacy direct connection as fallback | ||||
|         this.setupDirectConnection(socket, record, undefined, undefined, chunk); | ||||
|       } | ||||
|     }); | ||||
| @@ -652,13 +718,99 @@ export class ConnectionHandler { | ||||
|   ): void { | ||||
|     const connectionId = record.id; | ||||
|  | ||||
|     // If we have a domain config, try to use a forwarding handler | ||||
|     if (domainConfig) { | ||||
|       try { | ||||
|         // Get the forwarding handler for this domain | ||||
|         const forwardingHandler = this.domainConfigManager.getForwardingHandler(domainConfig); | ||||
|  | ||||
|         // Check the forwarding type to determine how to handle the connection | ||||
|         const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); | ||||
|  | ||||
|         // For TLS connections, handle differently based on forwarding type | ||||
|         if (record.isTLS) { | ||||
|           // For HTTP-only, we shouldn't get TLS connections | ||||
|           if (forwardingType === 'http-only') { | ||||
|             console.log(`[${connectionId}] Received TLS connection for HTTP-only domain ${serverName || 'unknown'}`); | ||||
|             socket.end(); | ||||
|             this.connectionManager.initiateCleanupOnce(record, 'wrong_protocol'); | ||||
|             return; | ||||
|           } | ||||
|  | ||||
|           // For HTTPS passthrough, use the handler's connection handling | ||||
|           if (forwardingType === 'https-passthrough') { | ||||
|             // If there's initial data, process it first | ||||
|             if (initialChunk) { | ||||
|               record.bytesReceived += initialChunk.length; | ||||
|             } | ||||
|  | ||||
|             // Let the handler take over | ||||
|             if (this.settings.enableDetailedLogging) { | ||||
|               console.log(`[${connectionId}] Using forwarding handler for ${forwardingType} connection to ${serverName || 'unknown'}`); | ||||
|             } | ||||
|  | ||||
|             // Pass the connection to the handler | ||||
|             forwardingHandler.handleConnection(socket); | ||||
|  | ||||
|             // Set metadata fields | ||||
|             record.usingNetworkProxy = false; | ||||
|  | ||||
|             // Add connection information to record | ||||
|             if (serverName) { | ||||
|               record.lockedDomain = serverName; | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|           } | ||||
|  | ||||
|           // For TLS termination types, we'll fall through to the legacy connection setup | ||||
|           // because NetworkProxy is used for termination | ||||
|         } | ||||
|         // For non-TLS connections, check if we support HTTP | ||||
|         else if (!record.isTLS && this.domainConfigManager.supportsHttp(domainConfig)) { | ||||
|           // For HTTP handling that the handler supports natively | ||||
|           if (forwardingType === 'http-only' || | ||||
|               (forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-https')) { | ||||
|  | ||||
|             // If there's redirect to HTTPS configured and this is a normal HTTP connection | ||||
|             if (this.domainConfigManager.shouldRedirectToHttps(domainConfig)) { | ||||
|               // We'll let the handler deal with the HTTP request and potential redirect | ||||
|               // Once an HTTP request arrives, it can redirect as needed | ||||
|             } | ||||
|  | ||||
|             // Let the handler take over for HTTP handling | ||||
|             if (this.settings.enableDetailedLogging) { | ||||
|               console.log(`[${connectionId}] Using forwarding handler for HTTP connection to ${serverName || 'unknown'}`); | ||||
|             } | ||||
|  | ||||
|             // Pass the connection to the handler | ||||
|             forwardingHandler.handleConnection(socket); | ||||
|  | ||||
|             // Add connection information to record | ||||
|             if (serverName) { | ||||
|               record.lockedDomain = serverName; | ||||
|             } | ||||
|  | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.log(`[${connectionId}] Error using forwarding handler: ${err}`); | ||||
|         // Fall through to legacy connection handling | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // If we get here, we'll use legacy connection handling | ||||
|  | ||||
|     // Determine target host | ||||
|     const targetHost = domainConfig | ||||
|       ? this.domainConfigManager.getTargetIP(domainConfig) | ||||
|       : this.settings.targetIP!; | ||||
|  | ||||
|     // Determine target port | ||||
|     const targetPort = overridePort !== undefined ? overridePort : this.settings.toPort; | ||||
|     // Determine target port - first try forwarding config, then fallback | ||||
|     const targetPort = domainConfig | ||||
|       ? this.domainConfigManager.getTargetPort(domainConfig, overridePort !== undefined ? overridePort : this.settings.toPort) | ||||
|       : (overridePort !== undefined ? overridePort : this.settings.toPort); | ||||
|  | ||||
|     // Setup connection options | ||||
|     const connectionOptions: plugins.net.NetConnectOpts = { | ||||
| @@ -842,6 +994,21 @@ export class ConnectionHandler { | ||||
|         this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed'); | ||||
|       } | ||||
|  | ||||
|       // If we have a forwarding handler for this domain, let it handle the error | ||||
|       if (domainConfig) { | ||||
|         try { | ||||
|           const forwardingHandler = this.domainConfigManager.getForwardingHandler(domainConfig); | ||||
|           forwardingHandler.emit('connection_error', { | ||||
|             socket, | ||||
|             error: err, | ||||
|             connectionId | ||||
|           }); | ||||
|         } catch (handlerErr) { | ||||
|           // If getting the handler fails, just log and continue with normal cleanup | ||||
|           console.log(`Error getting forwarding handler for error handling: ${handlerErr}`); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Clean up the connection | ||||
|       this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`); | ||||
|     }); | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces.js'; | ||||
| import type { ForwardingType, IForwardConfig } from './types/forwarding.types.js'; | ||||
| import { ForwardingHandlerFactory } from './forwarding/forwarding.factory.js'; | ||||
| import type { IForwardingHandler } from './forwarding/forwarding.handler.js'; | ||||
|  | ||||
| /** | ||||
|  * Manages domain configurations and target selection | ||||
| @@ -7,7 +10,10 @@ import type { IDomainConfig, ISmartProxyOptions } from './classes.pp.interfaces. | ||||
| export class DomainConfigManager { | ||||
|   // Track round-robin indices for domain configs | ||||
|   private domainTargetIndices: Map<IDomainConfig, number> = new Map(); | ||||
|    | ||||
|  | ||||
|   // Cache forwarding handlers for each domain config | ||||
|   private forwardingHandlers: Map<IDomainConfig, IForwardingHandler> = new Map(); | ||||
|  | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
| @@ -15,7 +21,7 @@ export class DomainConfigManager { | ||||
|    */ | ||||
|   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) { | ||||
| @@ -23,6 +29,31 @@ export class DomainConfigManager { | ||||
|         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}`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
| @@ -66,30 +97,59 @@ export class DomainConfigManager { | ||||
|    * Get target IP with round-robin support | ||||
|    */ | ||||
|   public getTargetIP(domainConfig: IDomainConfig): string { | ||||
|     if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) { | ||||
|     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 = domainConfig.targetIPs[currentIndex % domainConfig.targetIPs.length]; | ||||
|       const ip = targetHosts[currentIndex % targetHosts.length]; | ||||
|       this.domainTargetIndices.set(domainConfig, currentIndex + 1); | ||||
|       return ip; | ||||
|     } | ||||
|      | ||||
|  | ||||
|     return this.settings.targetIP || 'localhost'; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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 { | ||||
|     // Check forwarding type first | ||||
|     const forwardingType = this.getForwardingType(domainConfig); | ||||
|  | ||||
|     if (forwardingType === 'https-terminate-to-http' || | ||||
|         forwardingType === 'https-terminate-to-https') { | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     // Fall back to legacy setting | ||||
|     return !!domainConfig.useNetworkProxy; | ||||
|   } | ||||
|    | ||||
|  | ||||
|   /** | ||||
|    * Gets the NetworkProxy port for a domain | ||||
|    */ | ||||
|   public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined { | ||||
|     return domainConfig.useNetworkProxy  | ||||
|       ? (domainConfig.networkProxyPort || this.settings.networkProxyPort) | ||||
|       : undefined; | ||||
|     // First check if we should use NetworkProxy at all | ||||
|     if (!this.shouldUseNetworkProxy(domainConfig)) { | ||||
|       return undefined; | ||||
|     } | ||||
|  | ||||
|     // Check forwarding config first | ||||
|     if (domainConfig.forwarding?.advanced?.networkProxyPort) { | ||||
|       return domainConfig.forwarding.advanced.networkProxyPort; | ||||
|     } | ||||
|  | ||||
|     // Fall back to legacy setting | ||||
|     return domainConfig.networkProxyPort || this.settings.networkProxyPort; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
| @@ -99,15 +159,40 @@ export class DomainConfigManager { | ||||
|     allowedIPs: string[], | ||||
|     blockedIPs: string[] | ||||
|   } { | ||||
|     // Start with empty arrays | ||||
|     const allowedIPs: string[] = []; | ||||
|     const blockedIPs: string[] = []; | ||||
|  | ||||
|     // Add IPs from forwarding security settings | ||||
|     if (domainConfig.forwarding?.security?.allowedIps) { | ||||
|       allowedIPs.push(...domainConfig.forwarding.security.allowedIps); | ||||
|     } | ||||
|  | ||||
|     if (domainConfig.forwarding?.security?.blockedIps) { | ||||
|       blockedIPs.push(...domainConfig.forwarding.security.blockedIps); | ||||
|     } | ||||
|  | ||||
|     // Add legacy settings | ||||
|     if (domainConfig.allowedIPs.length > 0) { | ||||
|       allowedIPs.push(...domainConfig.allowedIPs); | ||||
|     } | ||||
|  | ||||
|     if (domainConfig.blockedIPs && domainConfig.blockedIPs.length > 0) { | ||||
|       blockedIPs.push(...domainConfig.blockedIPs); | ||||
|     } | ||||
|  | ||||
|     // Add global defaults | ||||
|     if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { | ||||
|       allowedIPs.push(...this.settings.defaultAllowedIPs); | ||||
|     } | ||||
|  | ||||
|     if (this.settings.defaultBlockedIPs && this.settings.defaultBlockedIPs.length > 0) { | ||||
|       blockedIPs.push(...this.settings.defaultBlockedIPs); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|       allowedIPs: [ | ||||
|         ...domainConfig.allowedIPs, | ||||
|         ...(this.settings.defaultAllowedIPs || []) | ||||
|       ], | ||||
|       blockedIPs: [ | ||||
|         ...(domainConfig.blockedIPs || []), | ||||
|         ...(this.settings.defaultBlockedIPs || []) | ||||
|       ] | ||||
|       allowedIPs, | ||||
|       blockedIPs | ||||
|     }; | ||||
|   } | ||||
|    | ||||
| @@ -115,9 +200,107 @@ export class DomainConfigManager { | ||||
|    * Get connection timeout for a domain | ||||
|    */ | ||||
|   public getConnectionTimeout(domainConfig?: IDomainConfig): number { | ||||
|     // First check forwarding configuration for timeout | ||||
|     if (domainConfig?.forwarding?.advanced?.timeout) { | ||||
|       return domainConfig.forwarding.advanced.timeout; | ||||
|     } | ||||
|  | ||||
|     // Fall back to legacy connectionTimeout | ||||
|     if (domainConfig?.connectionTimeout) { | ||||
|       return domainConfig.connectionTimeout; | ||||
|     } | ||||
|  | ||||
|     return this.settings.maxConnectionLifetime || 86400000; // 24 hours default | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Creates a forwarding handler for a domain configuration | ||||
|    */ | ||||
|   private createForwardingHandler(domainConfig: IDomainConfig): IForwardingHandler { | ||||
|     if (!domainConfig.forwarding) { | ||||
|       throw new Error(`Domain config for ${domainConfig.domains.join(', ')} has no forwarding configuration`); | ||||
|     } | ||||
|  | ||||
|     // 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): IForwardingHandler { | ||||
|     // 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): ForwardingType | 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; | ||||
|   } | ||||
| } | ||||
| @@ -1,23 +1,15 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IForwardConfig } from './forwarding/index.js'; | ||||
|  | ||||
| /** | ||||
|  * Provision object for static or HTTP-01 certificate | ||||
|  */ | ||||
| export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | ||||
|  | ||||
| /** Domain configuration with per-domain allowed port ranges */ | ||||
| /** Domain configuration with forwarding configuration */ | ||||
| export interface IDomainConfig { | ||||
|   domains: string[]; // Glob patterns for domain(s) | ||||
|   allowedIPs: string[]; // Glob patterns for allowed IPs | ||||
|   blockedIPs?: string[]; // Glob patterns for blocked IPs | ||||
|   targetIPs?: string[]; // If multiple targetIPs are given, use round robin. | ||||
|   portRanges?: Array<{ from: number; to: number }>; // Optional port ranges | ||||
|   // Allow domain-specific timeout override | ||||
|   connectionTimeout?: number; // Connection timeout override (ms) | ||||
|  | ||||
|   // NetworkProxy integration options for this specific domain | ||||
|   useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain | ||||
|   networkProxyPort?: number; // Override default NetworkProxy port for this domain | ||||
|   forwarding: IForwardConfig; // Unified forwarding configuration | ||||
| } | ||||
|  | ||||
| /** Port proxy settings including global allowed port ranges */ | ||||
|   | ||||
| @@ -12,6 +12,9 @@ import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { CertProvisioner } from './classes.pp.certprovisioner.js'; | ||||
| import type { ICertificateData } from '../common/types.js'; | ||||
| import { buildPort80Handler } from '../common/acmeFactory.js'; | ||||
| import { ensureForwardingConfig } from './forwarding/legacy-converter.js'; | ||||
| import type { ForwardingType } from './types/forwarding.types.js'; | ||||
| import { createPort80HandlerOptions } from '../common/port80-adapter.js'; | ||||
|  | ||||
| import type { ISmartProxyOptions, IDomainConfig } from './classes.pp.interfaces.js'; | ||||
| export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig }; | ||||
| @@ -156,11 +159,44 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Pre-process domain configs to ensure they all have forwarding configurations | ||||
|     this.settings.domainConfigs = this.settings.domainConfigs.map(config => ensureForwardingConfig(config)); | ||||
|  | ||||
|     // Initialize domain config manager with the processed configs | ||||
|     this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); | ||||
|  | ||||
|     // Initialize Port80Handler if enabled | ||||
|     await this.initializePort80Handler(); | ||||
|  | ||||
|     // Initialize CertProvisioner for unified certificate workflows | ||||
|     if (this.port80Handler) { | ||||
|       const acme = this.settings.acme!; | ||||
|  | ||||
|       // Convert domain forwards to use the new forwarding system if possible | ||||
|       const domainForwards = acme.domainForwards?.map(f => { | ||||
|         // If the domain has a forwarding config in domainConfigs, use that | ||||
|         const domainConfig = this.settings.domainConfigs.find( | ||||
|           dc => dc.domains.some(d => d === f.domain) | ||||
|         ); | ||||
|  | ||||
|         if (domainConfig?.forwarding) { | ||||
|           return { | ||||
|             domain: f.domain, | ||||
|             forwardConfig: f.forwardConfig, | ||||
|             acmeForwardConfig: f.acmeForwardConfig, | ||||
|             sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false | ||||
|           }; | ||||
|         } | ||||
|  | ||||
|         // Otherwise use the existing configuration | ||||
|         return { | ||||
|           domain: f.domain, | ||||
|           forwardConfig: f.forwardConfig, | ||||
|           acmeForwardConfig: f.acmeForwardConfig, | ||||
|           sslRedirect: f.sslRedirect || false | ||||
|         }; | ||||
|       }) || []; | ||||
|  | ||||
|       this.certProvisioner = new CertProvisioner( | ||||
|         this.settings.domainConfigs, | ||||
|         this.port80Handler, | ||||
| @@ -169,13 +205,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|         acme.renewThresholdDays!, | ||||
|         acme.renewCheckIntervalHours!, | ||||
|         acme.autoRenew!, | ||||
|         acme.domainForwards?.map(f => ({ | ||||
|           domain: f.domain, | ||||
|           forwardConfig: f.forwardConfig, | ||||
|           acmeForwardConfig: f.acmeForwardConfig, | ||||
|           sslRedirect: f.sslRedirect || false | ||||
|         })) || [] | ||||
|         domainForwards | ||||
|       ); | ||||
|  | ||||
|       this.certProvisioner.on('certificate', (certData) => { | ||||
|         this.emit('certificate', { | ||||
|           domain: certData.domain, | ||||
| @@ -186,6 +218,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|           isRenewal: certData.isRenewal | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       await this.certProvisioner.start(); | ||||
|       console.log('CertProvisioner started'); | ||||
|     } | ||||
| @@ -378,21 +411,44 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { | ||||
|     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); | ||||
|      | ||||
|  | ||||
|     // Ensure each domain config has a valid forwarding configuration | ||||
|     const processedConfigs = newDomainConfigs.map(config => ensureForwardingConfig(config)); | ||||
|  | ||||
|     // Update domain configs in DomainConfigManager | ||||
|     this.domainConfigManager.updateDomainConfigs(newDomainConfigs); | ||||
|      | ||||
|     this.domainConfigManager.updateDomainConfigs(processedConfigs); | ||||
|  | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); | ||||
|     } | ||||
|      | ||||
|     // If Port80Handler is running, provision certificates per new domain | ||||
|  | ||||
|     // If Port80Handler is running, provision certificates based on forwarding type | ||||
|     if (this.port80Handler && this.settings.acme?.enabled) { | ||||
|       for (const domainConfig of newDomainConfigs) { | ||||
|       for (const domainConfig of processedConfigs) { | ||||
|         // Skip certificate provisioning for http-only or passthrough configs that don't need certs | ||||
|         const forwardingType = domainConfig.forwarding?.type as ForwardingType; | ||||
|         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); | ||||
| @@ -403,16 +459,17 @@ 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; | ||||
|             } | ||||
|             this.port80Handler.addDomain({ | ||||
|               domainName: domain, | ||||
|               sslRedirect: true, | ||||
|               acmeMaintenance: true | ||||
|             }); | ||||
|  | ||||
|             // 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 | ||||
|   | ||||
							
								
								
									
										45
									
								
								ts/smartproxy/forwarding/domain-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								ts/smartproxy/forwarding/domain-config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Updated domain configuration with unified forwarding configuration | ||||
|  */ | ||||
| export interface IDomainConfig { | ||||
|   // Core properties - domain patterns | ||||
|   domains: string[]; | ||||
|    | ||||
|   // Unified forwarding configuration | ||||
|   forwarding: IForwardConfig; | ||||
|    | ||||
|   // Legacy security properties that will be migrated to forwarding.security | ||||
|   allowedIPs?: string[]; | ||||
|   blockedIPs?: string[]; | ||||
|    | ||||
|   // Legacy NetworkProxy properties | ||||
|   useNetworkProxy?: boolean; | ||||
|   networkProxyPort?: number; | ||||
|    | ||||
|   // Legacy timeout property | ||||
|   connectionTimeout?: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Helper function to create a domain configuration | ||||
|  */ | ||||
| export function createDomainConfig( | ||||
|   domains: string | string[], | ||||
|   forwarding: IForwardConfig, | ||||
|   security?: { | ||||
|     allowedIPs?: string[]; | ||||
|     blockedIPs?: string[]; | ||||
|   } | ||||
| ): IDomainConfig { | ||||
|   // Normalize domains to an array | ||||
|   const domainArray = Array.isArray(domains) ? domains : [domains]; | ||||
|    | ||||
|   return { | ||||
|     domains: domainArray, | ||||
|     forwarding, | ||||
|     allowedIPs: security?.allowedIPs || ['*'], | ||||
|     blockedIPs: security?.blockedIPs | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										283
									
								
								ts/smartproxy/forwarding/domain-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								ts/smartproxy/forwarding/domain-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,283 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { IDomainConfig } from './domain-config.js'; | ||||
| import type { IForwardingHandler } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerFactory } from './forwarding.factory.js'; | ||||
|  | ||||
| /** | ||||
|  * Events emitted by the DomainManager | ||||
|  */ | ||||
| export enum DomainManagerEvents { | ||||
|   DOMAIN_ADDED = 'domain-added', | ||||
|   DOMAIN_REMOVED = 'domain-removed', | ||||
|   DOMAIN_MATCHED = 'domain-matched', | ||||
|   DOMAIN_MATCH_FAILED = 'domain-match-failed', | ||||
|   CERTIFICATE_NEEDED = 'certificate-needed', | ||||
|   CERTIFICATE_LOADED = 'certificate-loaded', | ||||
|   ERROR = 'error' | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Manages domains and their forwarding handlers | ||||
|  */ | ||||
| export class DomainManager extends plugins.EventEmitter { | ||||
|   private domainConfigs: IDomainConfig[] = []; | ||||
|   private domainHandlers: Map<string, IForwardingHandler> = new Map(); | ||||
|    | ||||
|   /** | ||||
|    * Create a new DomainManager | ||||
|    * @param initialDomains Optional initial domain configurations | ||||
|    */ | ||||
|   constructor(initialDomains?: IDomainConfig[]) { | ||||
|     super(); | ||||
|      | ||||
|     if (initialDomains) { | ||||
|       this.setDomainConfigs(initialDomains); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set or replace all domain configurations | ||||
|    * @param configs Array of domain configurations | ||||
|    */ | ||||
|   public async setDomainConfigs(configs: IDomainConfig[]): Promise<void> { | ||||
|     // Clear existing handlers | ||||
|     this.domainHandlers.clear(); | ||||
|      | ||||
|     // Store new configurations | ||||
|     this.domainConfigs = [...configs]; | ||||
|      | ||||
|     // Initialize handlers for each domain | ||||
|     for (const config of this.domainConfigs) { | ||||
|       await this.createHandlersForDomain(config); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add a new domain configuration | ||||
|    * @param config The domain configuration to add | ||||
|    */ | ||||
|   public async addDomainConfig(config: IDomainConfig): Promise<void> { | ||||
|     // Check if any of these domains already exist | ||||
|     for (const domain of config.domains) { | ||||
|       if (this.domainHandlers.has(domain)) { | ||||
|         // Remove existing handler for this domain | ||||
|         this.domainHandlers.delete(domain); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Add the new configuration | ||||
|     this.domainConfigs.push(config); | ||||
|      | ||||
|     // Create handlers for the new domain | ||||
|     await this.createHandlersForDomain(config); | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_ADDED, { | ||||
|       domains: config.domains, | ||||
|       forwardingType: config.forwarding.type | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Remove a domain configuration | ||||
|    * @param domain The domain to remove | ||||
|    * @returns True if the domain was found and removed | ||||
|    */ | ||||
|   public removeDomainConfig(domain: string): boolean { | ||||
|     // Find the config that includes this domain | ||||
|     const index = this.domainConfigs.findIndex(config =>  | ||||
|       config.domains.includes(domain) | ||||
|     ); | ||||
|      | ||||
|     if (index === -1) { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     // Get the config | ||||
|     const config = this.domainConfigs[index]; | ||||
|      | ||||
|     // Remove all handlers for this config | ||||
|     for (const domainName of config.domains) { | ||||
|       this.domainHandlers.delete(domainName); | ||||
|     } | ||||
|      | ||||
|     // Remove the config | ||||
|     this.domainConfigs.splice(index, 1); | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_REMOVED, { | ||||
|       domains: config.domains | ||||
|     }); | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Find the handler for a domain | ||||
|    * @param domain The domain to find a handler for | ||||
|    * @returns The handler or undefined if no match | ||||
|    */ | ||||
|   public findHandlerForDomain(domain: string): IForwardingHandler | undefined { | ||||
|     // Try exact match | ||||
|     if (this.domainHandlers.has(domain)) { | ||||
|       return this.domainHandlers.get(domain); | ||||
|     } | ||||
|      | ||||
|     // Try wildcard matches | ||||
|     const wildcardHandler = this.findWildcardHandler(domain); | ||||
|     if (wildcardHandler) { | ||||
|       return wildcardHandler; | ||||
|     } | ||||
|      | ||||
|     // No match found | ||||
|     return undefined; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a connection for a domain | ||||
|    * @param domain The domain | ||||
|    * @param socket The client socket | ||||
|    * @returns True if the connection was handled | ||||
|    */ | ||||
|   public handleConnection(domain: string, socket: plugins.net.Socket): boolean { | ||||
|     const handler = this.findHandlerForDomain(domain); | ||||
|      | ||||
|     if (!handler) { | ||||
|       this.emit(DomainManagerEvents.DOMAIN_MATCH_FAILED, { | ||||
|         domain, | ||||
|         remoteAddress: socket.remoteAddress | ||||
|       }); | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_MATCHED, { | ||||
|       domain, | ||||
|       handlerType: handler.constructor.name, | ||||
|       remoteAddress: socket.remoteAddress | ||||
|     }); | ||||
|      | ||||
|     // Handle the connection | ||||
|     handler.handleConnection(socket); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request for a domain | ||||
|    * @param domain The domain | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    * @returns True if the request was handled | ||||
|    */ | ||||
|   public handleHttpRequest(domain: string, req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): boolean { | ||||
|     const handler = this.findHandlerForDomain(domain); | ||||
|      | ||||
|     if (!handler) { | ||||
|       this.emit(DomainManagerEvents.DOMAIN_MATCH_FAILED, { | ||||
|         domain, | ||||
|         remoteAddress: req.socket.remoteAddress | ||||
|       }); | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     this.emit(DomainManagerEvents.DOMAIN_MATCHED, { | ||||
|       domain, | ||||
|       handlerType: handler.constructor.name, | ||||
|       remoteAddress: req.socket.remoteAddress | ||||
|     }); | ||||
|      | ||||
|     // Handle the request | ||||
|     handler.handleHttpRequest(req, res); | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create handlers for a domain configuration | ||||
|    * @param config The domain configuration | ||||
|    */ | ||||
|   private async createHandlersForDomain(config: IDomainConfig): Promise<void> { | ||||
|     try { | ||||
|       // Create a handler for this forwarding configuration | ||||
|       const handler = ForwardingHandlerFactory.createHandler(config.forwarding); | ||||
|        | ||||
|       // Initialize the handler | ||||
|       await handler.initialize(); | ||||
|        | ||||
|       // Set up event forwarding | ||||
|       this.setupHandlerEvents(handler, config); | ||||
|        | ||||
|       // Store the handler for each domain in the config | ||||
|       for (const domain of config.domains) { | ||||
|         this.domainHandlers.set(domain, handler); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       this.emit(DomainManagerEvents.ERROR, { | ||||
|         domains: config.domains, | ||||
|         error: error instanceof Error ? error.message : String(error) | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set up event forwarding from a handler | ||||
|    * @param handler The handler | ||||
|    * @param config The domain configuration for this handler | ||||
|    */ | ||||
|   private setupHandlerEvents(handler: IForwardingHandler, config: IDomainConfig): void { | ||||
|     // Forward relevant events | ||||
|     handler.on(ForwardingHandlerEvents.CERTIFICATE_NEEDED, (data) => { | ||||
|       this.emit(DomainManagerEvents.CERTIFICATE_NEEDED, { | ||||
|         ...data, | ||||
|         domains: config.domains | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     handler.on(ForwardingHandlerEvents.CERTIFICATE_LOADED, (data) => { | ||||
|       this.emit(DomainManagerEvents.CERTIFICATE_LOADED, { | ||||
|         ...data, | ||||
|         domains: config.domains | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     handler.on(ForwardingHandlerEvents.ERROR, (data) => { | ||||
|       this.emit(DomainManagerEvents.ERROR, { | ||||
|         ...data, | ||||
|         domains: config.domains | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Find a handler for a domain using wildcard matching | ||||
|    * @param domain The domain to find a handler for | ||||
|    * @returns The handler or undefined if no match | ||||
|    */ | ||||
|   private findWildcardHandler(domain: string): IForwardingHandler | undefined { | ||||
|     // Exact match already checked in findHandlerForDomain | ||||
|      | ||||
|     // Try subdomain wildcard (*.example.com) | ||||
|     if (domain.includes('.')) { | ||||
|       const parts = domain.split('.'); | ||||
|       if (parts.length > 2) { | ||||
|         const wildcardDomain = `*.${parts.slice(1).join('.')}`; | ||||
|         if (this.domainHandlers.has(wildcardDomain)) { | ||||
|           return this.domainHandlers.get(wildcardDomain); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Try full wildcard | ||||
|     if (this.domainHandlers.has('*')) { | ||||
|       return this.domainHandlers.get('*'); | ||||
|     } | ||||
|      | ||||
|     // No match found | ||||
|     return undefined; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get all domain configurations | ||||
|    * @returns Array of domain configurations | ||||
|    */ | ||||
|   public getDomainConfigs(): IDomainConfig[] { | ||||
|     return [...this.domainConfigs]; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										155
									
								
								ts/smartproxy/forwarding/forwarding.factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								ts/smartproxy/forwarding/forwarding.factory.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| import type { IForwardConfig, IForwardingHandler } from '../types/forwarding.types.js'; | ||||
| import { HttpForwardingHandler } from './http.handler.js'; | ||||
| import { HttpsPassthroughHandler } from './https-passthrough.handler.js'; | ||||
| import { HttpsTerminateToHttpHandler } from './https-terminate-to-http.handler.js'; | ||||
| import { HttpsTerminateToHttpsHandler } from './https-terminate-to-https.handler.js'; | ||||
|  | ||||
| /** | ||||
|  * Factory for creating forwarding handlers based on the configuration type | ||||
|  */ | ||||
| export class ForwardingHandlerFactory { | ||||
|   /** | ||||
|    * Create a forwarding handler based on the configuration | ||||
|    * @param config The forwarding configuration | ||||
|    * @returns The appropriate forwarding handler | ||||
|    */ | ||||
|   public static createHandler(config: IForwardConfig): IForwardingHandler { | ||||
|     // Create the appropriate handler based on the forwarding type | ||||
|     switch (config.type) { | ||||
|       case 'http-only': | ||||
|         return new HttpForwardingHandler(config); | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         return new HttpsPassthroughHandler(config); | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|         return new HttpsTerminateToHttpHandler(config); | ||||
|          | ||||
|       case 'https-terminate-to-https': | ||||
|         return new HttpsTerminateToHttpsHandler(config); | ||||
|          | ||||
|       default: | ||||
|         // Type system should prevent this, but just in case: | ||||
|         throw new Error(`Unknown forwarding type: ${(config as any).type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply default values to a forwarding configuration based on its type | ||||
|    * @param config The original forwarding configuration | ||||
|    * @returns A configuration with defaults applied | ||||
|    */ | ||||
|   public static applyDefaults(config: IForwardConfig): IForwardConfig { | ||||
|     // Create a deep copy of the configuration | ||||
|     const result: IForwardConfig = JSON.parse(JSON.stringify(config)); | ||||
|      | ||||
|     // Apply defaults based on forwarding type | ||||
|     switch (config.type) { | ||||
|       case 'http-only': | ||||
|         // Set defaults for HTTP-only mode | ||||
|         result.http = { | ||||
|           enabled: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         break; | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         // Set defaults for HTTPS passthrough | ||||
|         result.https = { | ||||
|           forwardSni: true, | ||||
|           ...config.https | ||||
|         }; | ||||
|         // SNI forwarding doesn't do HTTP | ||||
|         result.http = { | ||||
|           enabled: false, | ||||
|           ...config.http | ||||
|         }; | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|         // Set defaults for HTTPS termination to HTTP | ||||
|         result.https = { | ||||
|           ...config.https | ||||
|         }; | ||||
|         // Support HTTP access by default in this mode | ||||
|         result.http = { | ||||
|           enabled: true, | ||||
|           redirectToHttps: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         // Enable ACME by default | ||||
|         result.acme = { | ||||
|           enabled: true, | ||||
|           maintenance: true, | ||||
|           ...config.acme | ||||
|         }; | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-https': | ||||
|         // Similar to terminate-to-http but with different target handling | ||||
|         result.https = { | ||||
|           ...config.https | ||||
|         }; | ||||
|         result.http = { | ||||
|           enabled: true, | ||||
|           redirectToHttps: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         result.acme = { | ||||
|           enabled: true, | ||||
|           maintenance: true, | ||||
|           ...config.acme | ||||
|         }; | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate a forwarding configuration | ||||
|    * @param config The configuration to validate | ||||
|    * @throws Error if the configuration is invalid | ||||
|    */ | ||||
|   public static validateConfig(config: IForwardConfig): void { | ||||
|     // Validate common properties | ||||
|     if (!config.target) { | ||||
|       throw new Error('Forwarding configuration must include a target'); | ||||
|     } | ||||
|      | ||||
|     if (!config.target.host || (Array.isArray(config.target.host) && config.target.host.length === 0)) { | ||||
|       throw new Error('Target must include a host or array of hosts'); | ||||
|     } | ||||
|      | ||||
|     if (!config.target.port || config.target.port <= 0 || config.target.port > 65535) { | ||||
|       throw new Error('Target must include a valid port (1-65535)'); | ||||
|     } | ||||
|      | ||||
|     // Type-specific validation | ||||
|     switch (config.type) { | ||||
|       case 'http-only': | ||||
|         // HTTP-only needs http.enabled to be true | ||||
|         if (config.http?.enabled === false) { | ||||
|           throw new Error('HTTP-only forwarding must have HTTP enabled'); | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         // HTTPS passthrough doesn't support HTTP | ||||
|         if (config.http?.enabled === true) { | ||||
|           throw new Error('HTTPS passthrough does not support HTTP'); | ||||
|         } | ||||
|          | ||||
|         // HTTPS passthrough doesn't work with ACME | ||||
|         if (config.acme?.enabled === true) { | ||||
|           throw new Error('HTTPS passthrough does not support ACME'); | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|       case 'https-terminate-to-https': | ||||
|         // These modes support all options, nothing specific to validate | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										127
									
								
								ts/smartproxy/forwarding/forwarding.handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								ts/smartproxy/forwarding/forwarding.handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler | ||||
| } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Base class for all forwarding handlers | ||||
|  */ | ||||
| export abstract class ForwardingHandler extends plugins.EventEmitter implements IForwardingHandler { | ||||
|   /** | ||||
|    * Create a new ForwardingHandler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(protected config: IForwardConfig) { | ||||
|     super(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler | ||||
|    * Base implementation does nothing, subclasses should override as needed | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // Base implementation - no initialization needed | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle a new socket connection | ||||
|    * @param socket The incoming socket connection | ||||
|    */ | ||||
|   public abstract handleConnection(socket: plugins.net.Socket): void; | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void; | ||||
|    | ||||
|   /** | ||||
|    * Get a target from the configuration, supporting round-robin selection | ||||
|    * @returns A resolved target object with host and port | ||||
|    */ | ||||
|   protected getTargetFromConfig(): { host: string, port: number } { | ||||
|     const { target } = this.config; | ||||
|      | ||||
|     // Handle round-robin host selection | ||||
|     if (Array.isArray(target.host)) { | ||||
|       if (target.host.length === 0) { | ||||
|         throw new Error('No target hosts specified'); | ||||
|       } | ||||
|        | ||||
|       // Simple round-robin selection | ||||
|       const randomIndex = Math.floor(Math.random() * target.host.length); | ||||
|       return { | ||||
|         host: target.host[randomIndex], | ||||
|         port: target.port | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     // Single host | ||||
|     return { | ||||
|       host: target.host, | ||||
|       port: target.port | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Redirect an HTTP request to HTTPS | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     const host = req.headers.host || ''; | ||||
|     const path = req.url || '/'; | ||||
|     const redirectUrl = `https://${host}${path}`; | ||||
|      | ||||
|     res.writeHead(301, { | ||||
|       'Location': redirectUrl, | ||||
|       'Cache-Control': 'no-cache' | ||||
|     }); | ||||
|     res.end(`Redirecting to ${redirectUrl}`); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|       statusCode: 301, | ||||
|       headers: { 'Location': redirectUrl }, | ||||
|       size: 0 | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply custom headers from configuration | ||||
|    * @param headers The original headers | ||||
|    * @param variables Variables to replace in the headers | ||||
|    * @returns The headers with custom values applied | ||||
|    */ | ||||
|   protected applyCustomHeaders( | ||||
|     headers: Record<string, string | string[] | undefined>, | ||||
|     variables: Record<string, string> | ||||
|   ): Record<string, string | string[] | undefined> { | ||||
|     const customHeaders = this.config.advanced?.headers || {}; | ||||
|     const result = { ...headers }; | ||||
|      | ||||
|     // Apply custom headers with variable substitution | ||||
|     for (const [key, value] of Object.entries(customHeaders)) { | ||||
|       let processedValue = value; | ||||
|        | ||||
|       // Replace variables in the header value | ||||
|       for (const [varName, varValue] of Object.entries(variables)) { | ||||
|         processedValue = processedValue.replace(`{${varName}}`, varValue); | ||||
|       } | ||||
|        | ||||
|       result[key] = processedValue; | ||||
|     } | ||||
|      | ||||
|     return result; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the timeout for this connection from configuration | ||||
|    * @returns Timeout in milliseconds | ||||
|    */ | ||||
|   protected getTimeout(): number { | ||||
|     return this.config.advanced?.timeout || 60000; // Default: 60 seconds | ||||
|   } | ||||
| } | ||||
							
								
								
									
										140
									
								
								ts/smartproxy/forwarding/http.handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								ts/smartproxy/forwarding/http.handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTP-only forwarding | ||||
|  */ | ||||
| export class HttpForwardingHandler extends ForwardingHandler { | ||||
|   /** | ||||
|    * Create a new HTTP forwarding handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTP-only configuration | ||||
|     if (config.type !== 'http-only') { | ||||
|       throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a raw socket connection | ||||
|    * HTTP handler doesn't do much with raw sockets as it mainly processes | ||||
|    * parsed HTTP requests | ||||
|    */ | ||||
|   public handleConnection(socket: plugins.net.Socket): void { | ||||
|     // For HTTP, we mainly handle parsed requests, but we can still set up | ||||
|     // some basic connection tracking | ||||
|     const remoteAddress = socket.remoteAddress || 'unknown'; | ||||
|      | ||||
|     socket.on('close', (hadError) => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         hadError | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     socket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: error.message | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create a custom headers object with variables for substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track bytes for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										182
									
								
								ts/smartproxy/forwarding/https-passthrough.handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								ts/smartproxy/forwarding/https-passthrough.handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS passthrough (SNI forwarding without termination) | ||||
|  */ | ||||
| export class HttpsPassthroughHandler extends ForwardingHandler { | ||||
|   /** | ||||
|    * Create a new HTTPS passthrough handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS passthrough configuration | ||||
|     if (config.type !== 'https-passthrough') { | ||||
|       throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by forwarding it without termination | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Log the connection | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Create a connection to the target server | ||||
|     const serverSocket = plugins.net.connect(target.port, target.host); | ||||
|      | ||||
|     // Handle errors on the server socket | ||||
|     serverSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `Target connection error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Close the client socket if it's still open | ||||
|       if (!clientSocket.destroyed) { | ||||
|         clientSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Handle errors on the client socket | ||||
|     clientSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `Client connection error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Close the server socket if it's still open | ||||
|       if (!serverSocket.destroyed) { | ||||
|         serverSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track data transfer for logging | ||||
|     let bytesSent = 0; | ||||
|     let bytesReceived = 0; | ||||
|      | ||||
|     // Forward data from client to server | ||||
|     clientSocket.on('data', (data) => { | ||||
|       bytesSent += data.length; | ||||
|        | ||||
|       // Check if server socket is writable | ||||
|       if (serverSocket.writable) { | ||||
|         const flushed = serverSocket.write(data); | ||||
|          | ||||
|         // Handle backpressure | ||||
|         if (!flushed) { | ||||
|           clientSocket.pause(); | ||||
|           serverSocket.once('drain', () => { | ||||
|             clientSocket.resume(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|         direction: 'outbound', | ||||
|         bytes: data.length, | ||||
|         total: bytesSent | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Forward data from server to client | ||||
|     serverSocket.on('data', (data) => { | ||||
|       bytesReceived += data.length; | ||||
|        | ||||
|       // Check if client socket is writable | ||||
|       if (clientSocket.writable) { | ||||
|         const flushed = clientSocket.write(data); | ||||
|          | ||||
|         // Handle backpressure | ||||
|         if (!flushed) { | ||||
|           serverSocket.pause(); | ||||
|           clientSocket.once('drain', () => { | ||||
|             serverSocket.resume(); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|         direction: 'inbound', | ||||
|         bytes: data.length, | ||||
|         total: bytesReceived | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle connection close | ||||
|     const handleClose = () => { | ||||
|       if (!clientSocket.destroyed) { | ||||
|         clientSocket.destroy(); | ||||
|       } | ||||
|        | ||||
|       if (!serverSocket.destroyed) { | ||||
|         serverSocket.destroy(); | ||||
|       } | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress, | ||||
|         bytesSent, | ||||
|         bytesReceived | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Set up close handlers | ||||
|     clientSocket.on('close', handleClose); | ||||
|     serverSocket.on('close', handleClose); | ||||
|      | ||||
|     // Set timeouts | ||||
|     const timeout = this.getTimeout(); | ||||
|     clientSocket.setTimeout(timeout); | ||||
|     serverSocket.setTimeout(timeout); | ||||
|      | ||||
|     // Handle timeouts | ||||
|     clientSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'Client connection timeout' | ||||
|       }); | ||||
|       handleClose(); | ||||
|     }); | ||||
|      | ||||
|     serverSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'Server connection timeout' | ||||
|       }); | ||||
|       handleClose(); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request - HTTPS passthrough doesn't support HTTP | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // HTTPS passthrough doesn't support HTTP requests | ||||
|     res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||||
|     res.end('HTTP not supported for this domain'); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|       statusCode: 404, | ||||
|       headers: { 'Content-Type': 'text/plain' }, | ||||
|       size: 'HTTP not supported for this domain'.length | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										264
									
								
								ts/smartproxy/forwarding/https-terminate-to-http.handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								ts/smartproxy/forwarding/https-terminate-to-http.handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS termination with HTTP backend | ||||
|  */ | ||||
| export class HttpsTerminateToHttpHandler extends ForwardingHandler { | ||||
|   private tlsServer: plugins.tls.Server | null = null; | ||||
|   private secureContext: plugins.tls.SecureContext | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new HTTPS termination with HTTP backend handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS terminate to HTTP configuration | ||||
|     if (config.type !== 'https-terminate-to-http') { | ||||
|       throw new Error(`Invalid configuration type for HttpsTerminateToHttpHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler, setting up TLS context | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // We need to load or create TLS certificates | ||||
|     if (this.config.https?.customCert) { | ||||
|       // Use custom certificate from configuration | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: this.config.https.customCert.key, | ||||
|         cert: this.config.https.customCert.cert | ||||
|       }); | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { | ||||
|         source: 'config', | ||||
|         domain: this.config.target.host | ||||
|       }); | ||||
|     } else if (this.config.acme?.enabled) { | ||||
|       // Request certificate through ACME if needed | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { | ||||
|         domain: Array.isArray(this.config.target.host)  | ||||
|           ? this.config.target.host[0]  | ||||
|           : this.config.target.host, | ||||
|         useProduction: this.config.acme.production || false | ||||
|       }); | ||||
|        | ||||
|       // In a real implementation, we would wait for the certificate to be issued | ||||
|       // For now, we'll use a dummy context | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', | ||||
|         cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set the secure context for TLS termination | ||||
|    * Called when a certificate is available | ||||
|    * @param context The secure context | ||||
|    */ | ||||
|   public setSecureContext(context: plugins.tls.SecureContext): void { | ||||
|     this.secureContext = context; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by terminating TLS and forwarding to HTTP backend | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Make sure we have a secure context | ||||
|     if (!this.secureContext) { | ||||
|       clientSocket.destroy(new Error('TLS secure context not initialized')); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     // Create a TLS socket using our secure context | ||||
|     const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { | ||||
|       secureContext: this.secureContext, | ||||
|       isServer: true, | ||||
|       server: this.tlsServer || undefined | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       tls: true | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS errors | ||||
|     tlsSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `TLS error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // The TLS socket will now emit HTTP traffic that can be processed | ||||
|     // In a real implementation, we would create an HTTP parser and handle | ||||
|     // the requests here, but for simplicity, we'll just log the data | ||||
|      | ||||
|     let dataBuffer = Buffer.alloc(0); | ||||
|      | ||||
|     tlsSocket.on('data', (data) => { | ||||
|       // Append to buffer | ||||
|       dataBuffer = Buffer.concat([dataBuffer, data]); | ||||
|        | ||||
|       // Very basic HTTP parsing - in a real implementation, use http-parser | ||||
|       if (dataBuffer.includes(Buffer.from('\r\n\r\n'))) { | ||||
|         const target = this.getTargetFromConfig(); | ||||
|          | ||||
|         // Simple example: forward the data to an HTTP server | ||||
|         const socket = plugins.net.connect(target.port, target.host, () => { | ||||
|           socket.write(dataBuffer); | ||||
|           dataBuffer = Buffer.alloc(0); | ||||
|            | ||||
|           // Set up bidirectional data flow | ||||
|           tlsSocket.pipe(socket); | ||||
|           socket.pipe(tlsSocket); | ||||
|         }); | ||||
|          | ||||
|         socket.on('error', (error) => { | ||||
|           this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|             remoteAddress, | ||||
|             error: `Target connection error: ${error.message}` | ||||
|           }); | ||||
|            | ||||
|           if (!tlsSocket.destroyed) { | ||||
|             tlsSocket.destroy(); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Handle close | ||||
|     tlsSocket.on('close', () => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request by forwarding to the HTTP backend | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Check if we should redirect to HTTPS | ||||
|     if (this.config.http?.redirectToHttps) { | ||||
|       this.redirectToHttps(req, res); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create custom headers with variable substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track response size for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										292
									
								
								ts/smartproxy/forwarding/https-terminate-to-https.handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								ts/smartproxy/forwarding/https-terminate-to-https.handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,292 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { ForwardingHandler } from './forwarding.handler.js'; | ||||
| import type { IForwardConfig } from '../types/forwarding.types.js'; | ||||
| import { ForwardingHandlerEvents } from '../types/forwarding.types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handler for HTTPS termination with HTTPS backend | ||||
|  */ | ||||
| export class HttpsTerminateToHttpsHandler extends ForwardingHandler { | ||||
|   private secureContext: plugins.tls.SecureContext | null = null; | ||||
|    | ||||
|   /** | ||||
|    * Create a new HTTPS termination with HTTPS backend handler | ||||
|    * @param config The forwarding configuration | ||||
|    */ | ||||
|   constructor(config: IForwardConfig) { | ||||
|     super(config); | ||||
|      | ||||
|     // Validate that this is an HTTPS terminate to HTTPS configuration | ||||
|     if (config.type !== 'https-terminate-to-https') { | ||||
|       throw new Error(`Invalid configuration type for HttpsTerminateToHttpsHandler: ${config.type}`); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize the handler, setting up TLS context | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     // We need to load or create TLS certificates for termination | ||||
|     if (this.config.https?.customCert) { | ||||
|       // Use custom certificate from configuration | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: this.config.https.customCert.key, | ||||
|         cert: this.config.https.customCert.cert | ||||
|       }); | ||||
|        | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, { | ||||
|         source: 'config', | ||||
|         domain: this.config.target.host | ||||
|       }); | ||||
|     } else if (this.config.acme?.enabled) { | ||||
|       // Request certificate through ACME if needed | ||||
|       this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, { | ||||
|         domain: Array.isArray(this.config.target.host)  | ||||
|           ? this.config.target.host[0]  | ||||
|           : this.config.target.host, | ||||
|         useProduction: this.config.acme.production || false | ||||
|       }); | ||||
|        | ||||
|       // In a real implementation, we would wait for the certificate to be issued | ||||
|       // For now, we'll use a dummy context | ||||
|       this.secureContext = plugins.tls.createSecureContext({ | ||||
|         key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----', | ||||
|         cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----' | ||||
|       }); | ||||
|     } else { | ||||
|       throw new Error('HTTPS termination requires either a custom certificate or ACME enabled'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Set the secure context for TLS termination | ||||
|    * Called when a certificate is available | ||||
|    * @param context The secure context | ||||
|    */ | ||||
|   public setSecureContext(context: plugins.tls.SecureContext): void { | ||||
|     this.secureContext = context; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a TLS/SSL socket connection by terminating TLS and creating a new TLS connection to backend | ||||
|    * @param clientSocket The incoming socket from the client | ||||
|    */ | ||||
|   public handleConnection(clientSocket: plugins.net.Socket): void { | ||||
|     // Make sure we have a secure context | ||||
|     if (!this.secureContext) { | ||||
|       clientSocket.destroy(new Error('TLS secure context not initialized')); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     const remoteAddress = clientSocket.remoteAddress || 'unknown'; | ||||
|     const remotePort = clientSocket.remotePort || 0; | ||||
|      | ||||
|     // Create a TLS socket using our secure context | ||||
|     const tlsSocket = new plugins.tls.TLSSocket(clientSocket, { | ||||
|       secureContext: this.secureContext, | ||||
|       isServer: true | ||||
|     }); | ||||
|      | ||||
|     this.emit(ForwardingHandlerEvents.CONNECTED, { | ||||
|       remoteAddress, | ||||
|       remotePort, | ||||
|       tls: true | ||||
|     }); | ||||
|      | ||||
|     // Handle TLS errors | ||||
|     tlsSocket.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: `TLS error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // The TLS socket will now emit HTTP traffic that can be processed | ||||
|     // In a real implementation, we would create an HTTP parser and handle | ||||
|     // the requests here, but for simplicity, we'll just forward the data | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Set up the connection to the HTTPS backend | ||||
|     const connectToBackend = () => { | ||||
|       const backendSocket = plugins.tls.connect({ | ||||
|         host: target.host, | ||||
|         port: target.port, | ||||
|         // In a real implementation, we would configure TLS options | ||||
|         rejectUnauthorized: false // For testing only, never use in production | ||||
|       }, () => { | ||||
|         this.emit(ForwardingHandlerEvents.DATA_FORWARDED, { | ||||
|           direction: 'outbound', | ||||
|           target: `${target.host}:${target.port}`, | ||||
|           tls: true | ||||
|         }); | ||||
|          | ||||
|         // Set up bidirectional data flow | ||||
|         tlsSocket.pipe(backendSocket); | ||||
|         backendSocket.pipe(tlsSocket); | ||||
|       }); | ||||
|        | ||||
|       backendSocket.on('error', (error) => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: `Backend connection error: ${error.message}` | ||||
|         }); | ||||
|          | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Handle close | ||||
|       backendSocket.on('close', () => { | ||||
|         if (!tlsSocket.destroyed) { | ||||
|           tlsSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Set timeout | ||||
|       const timeout = this.getTimeout(); | ||||
|       backendSocket.setTimeout(timeout); | ||||
|        | ||||
|       backendSocket.on('timeout', () => { | ||||
|         this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|           remoteAddress, | ||||
|           error: 'Backend connection timeout' | ||||
|         }); | ||||
|          | ||||
|         if (!backendSocket.destroyed) { | ||||
|           backendSocket.destroy(); | ||||
|         } | ||||
|       }); | ||||
|     }; | ||||
|      | ||||
|     // Wait for the TLS handshake to complete before connecting to backend | ||||
|     tlsSocket.on('secure', () => { | ||||
|       connectToBackend(); | ||||
|     }); | ||||
|      | ||||
|     // Handle close | ||||
|     tlsSocket.on('close', () => { | ||||
|       this.emit(ForwardingHandlerEvents.DISCONNECTED, { | ||||
|         remoteAddress | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Set timeout | ||||
|     const timeout = this.getTimeout(); | ||||
|     tlsSocket.setTimeout(timeout); | ||||
|      | ||||
|     tlsSocket.on('timeout', () => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress, | ||||
|         error: 'TLS connection timeout' | ||||
|       }); | ||||
|        | ||||
|       if (!tlsSocket.destroyed) { | ||||
|         tlsSocket.destroy(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle an HTTP request by forwarding to the HTTPS backend | ||||
|    * @param req The HTTP request | ||||
|    * @param res The HTTP response | ||||
|    */ | ||||
|   public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { | ||||
|     // Check if we should redirect to HTTPS | ||||
|     if (this.config.http?.redirectToHttps) { | ||||
|       this.redirectToHttps(req, res); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Get the target from configuration | ||||
|     const target = this.getTargetFromConfig(); | ||||
|      | ||||
|     // Create custom headers with variable substitution | ||||
|     const variables = { | ||||
|       clientIp: req.socket.remoteAddress || 'unknown' | ||||
|     }; | ||||
|      | ||||
|     // Prepare headers, merging with any custom headers from config | ||||
|     const headers = this.applyCustomHeaders(req.headers, variables); | ||||
|      | ||||
|     // Create the proxy request options | ||||
|     const options = { | ||||
|       hostname: target.host, | ||||
|       port: target.port, | ||||
|       path: req.url, | ||||
|       method: req.method, | ||||
|       headers, | ||||
|       // In a real implementation, we would configure TLS options | ||||
|       rejectUnauthorized: false // For testing only, never use in production | ||||
|     }; | ||||
|      | ||||
|     // Create the proxy request using HTTPS | ||||
|     const proxyReq = plugins.https.request(options, (proxyRes) => { | ||||
|       // Copy status code and headers from the proxied response | ||||
|       res.writeHead(proxyRes.statusCode || 500, proxyRes.headers); | ||||
|        | ||||
|       // Pipe the proxy response to the client response | ||||
|       proxyRes.pipe(res); | ||||
|        | ||||
|       // Track response size for logging | ||||
|       let responseSize = 0; | ||||
|       proxyRes.on('data', (chunk) => { | ||||
|         responseSize += chunk.length; | ||||
|       }); | ||||
|        | ||||
|       proxyRes.on('end', () => { | ||||
|         this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, { | ||||
|           statusCode: proxyRes.statusCode, | ||||
|           headers: proxyRes.headers, | ||||
|           size: responseSize | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|      | ||||
|     // Handle errors in the proxy request | ||||
|     proxyReq.on('error', (error) => { | ||||
|       this.emit(ForwardingHandlerEvents.ERROR, { | ||||
|         remoteAddress: req.socket.remoteAddress, | ||||
|         error: `Proxy request error: ${error.message}` | ||||
|       }); | ||||
|        | ||||
|       // Send an error response if headers haven't been sent yet | ||||
|       if (!res.headersSent) { | ||||
|         res.writeHead(502, { 'Content-Type': 'text/plain' }); | ||||
|         res.end(`Error forwarding request: ${error.message}`); | ||||
|       } else { | ||||
|         // Just end the response if headers have already been sent | ||||
|         res.end(); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Track request details for logging | ||||
|     let requestSize = 0; | ||||
|     req.on('data', (chunk) => { | ||||
|       requestSize += chunk.length; | ||||
|     }); | ||||
|      | ||||
|     // Log the request | ||||
|     this.emit(ForwardingHandlerEvents.HTTP_REQUEST, { | ||||
|       method: req.method, | ||||
|       url: req.url, | ||||
|       headers: req.headers, | ||||
|       remoteAddress: req.socket.remoteAddress, | ||||
|       target: `${target.host}:${target.port}` | ||||
|     }); | ||||
|      | ||||
|     // Pipe the client request to the proxy request | ||||
|     if (req.readable) { | ||||
|       req.pipe(proxyReq); | ||||
|     } else { | ||||
|       proxyReq.end(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										52
									
								
								ts/smartproxy/forwarding/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								ts/smartproxy/forwarding/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // Export types | ||||
| export type { | ||||
|   ForwardingType, | ||||
|   IForwardConfig, | ||||
|   IForwardingHandler, | ||||
|   ITargetConfig, | ||||
|   IHttpOptions, | ||||
|   IHttpsOptions, | ||||
|   IAcmeForwardingOptions, | ||||
|   ISecurityOptions, | ||||
|   IAdvancedOptions | ||||
| } from '../types/forwarding.types.js'; | ||||
|  | ||||
| // Export values | ||||
| export { | ||||
|   ForwardingHandlerEvents, | ||||
|   httpOnly, | ||||
|   tlsTerminateToHttp, | ||||
|   tlsTerminateToHttps, | ||||
|   sniPassthrough | ||||
| } from '../types/forwarding.types.js'; | ||||
|  | ||||
| // Export domain configuration | ||||
| export * from './domain-config.js'; | ||||
|  | ||||
| // Export handlers | ||||
| export { ForwardingHandler } from './forwarding.handler.js'; | ||||
| export { HttpForwardingHandler } from './http.handler.js'; | ||||
| export { HttpsPassthroughHandler } from './https-passthrough.handler.js'; | ||||
| export { HttpsTerminateToHttpHandler } from './https-terminate-to-http.handler.js'; | ||||
| export { HttpsTerminateToHttpsHandler } from './https-terminate-to-https.handler.js'; | ||||
|  | ||||
| // Export factory | ||||
| export { ForwardingHandlerFactory } from './forwarding.factory.js'; | ||||
|  | ||||
| // Export manager | ||||
| export { DomainManager, DomainManagerEvents } from './domain-manager.js'; | ||||
|  | ||||
| // Helper functions as a convenience object | ||||
| import { | ||||
|   httpOnly, | ||||
|   tlsTerminateToHttp, | ||||
|   tlsTerminateToHttps, | ||||
|   sniPassthrough | ||||
| } from '../types/forwarding.types.js'; | ||||
|  | ||||
| export const helpers = { | ||||
|   httpOnly, | ||||
|   tlsTerminateToHttp, | ||||
|   tlsTerminateToHttps, | ||||
|   sniPassthrough | ||||
| }; | ||||
							
								
								
									
										162
									
								
								ts/smartproxy/types/forwarding.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								ts/smartproxy/types/forwarding.types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,162 @@ | ||||
| import type * as plugins from '../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * The primary forwarding types supported by SmartProxy | ||||
|  */ | ||||
| export type ForwardingType = | ||||
|   | 'http-only'                // HTTP forwarding only (no HTTPS) | ||||
|   | 'https-passthrough'        // Pass-through TLS traffic (SNI forwarding) | ||||
|   | 'https-terminate-to-http'  // Terminate TLS and forward to HTTP backend | ||||
|   | 'https-terminate-to-https'; // Terminate TLS and forward to HTTPS backend | ||||
|  | ||||
| /** | ||||
|  * Target configuration for forwarding | ||||
|  */ | ||||
| export interface ITargetConfig { | ||||
|   host: string | string[];  // Support single host or round-robin | ||||
|   port: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP-specific options for forwarding | ||||
|  */ | ||||
| export interface IHttpOptions { | ||||
|   enabled?: boolean;                 // Whether HTTP is enabled | ||||
|   redirectToHttps?: boolean;         // Redirect HTTP to HTTPS | ||||
|   headers?: Record<string, string>;  // Custom headers for HTTP responses | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTPS-specific options for forwarding | ||||
|  */ | ||||
| export interface IHttpsOptions { | ||||
|   customCert?: {                    // Use custom cert instead of auto-provisioned | ||||
|     key: string; | ||||
|     cert: string; | ||||
|   }; | ||||
|   forwardSni?: boolean;             // Forward SNI info in passthrough mode | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * ACME certificate handling options | ||||
|  */ | ||||
| export interface IAcmeForwardingOptions { | ||||
|   enabled?: boolean;                // Enable ACME certificate provisioning   | ||||
|   maintenance?: boolean;            // Auto-renew certificates | ||||
|   production?: boolean;             // Use production ACME servers | ||||
|   forwardChallenges?: {             // Forward ACME challenges | ||||
|     host: string; | ||||
|     port: number; | ||||
|     useTls?: boolean; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Security options for forwarding | ||||
|  */ | ||||
| export interface ISecurityOptions { | ||||
|   allowedIps?: string[];            // IPs allowed to connect | ||||
|   blockedIps?: string[];            // IPs blocked from connecting | ||||
|   maxConnections?: number;          // Max simultaneous connections | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Advanced options for forwarding | ||||
|  */ | ||||
| export interface IAdvancedOptions { | ||||
|   portRanges?: Array<{ from: number; to: number }>; // Allowed port ranges | ||||
|   networkProxyPort?: number;        // Custom NetworkProxy port if using terminate mode | ||||
|   keepAlive?: boolean;              // Enable TCP keepalive | ||||
|   timeout?: number;                 // Connection timeout in ms | ||||
|   headers?: Record<string, string>; // Custom headers with support for variables like {sni} | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Unified forwarding configuration interface | ||||
|  */ | ||||
| export interface IForwardConfig { | ||||
|   // Define the primary forwarding type - use-case driven approach | ||||
|   type: ForwardingType; | ||||
|    | ||||
|   // Target configuration | ||||
|   target: ITargetConfig; | ||||
|    | ||||
|   // Protocol options | ||||
|   http?: IHttpOptions; | ||||
|   https?: IHttpsOptions; | ||||
|   acme?: IAcmeForwardingOptions; | ||||
|    | ||||
|   // Security and advanced options | ||||
|   security?: ISecurityOptions; | ||||
|   advanced?: IAdvancedOptions; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Event types emitted by forwarding handlers | ||||
|  */ | ||||
| export enum ForwardingHandlerEvents { | ||||
|   CONNECTED = 'connected', | ||||
|   DISCONNECTED = 'disconnected', | ||||
|   ERROR = 'error', | ||||
|   DATA_FORWARDED = 'data-forwarded', | ||||
|   HTTP_REQUEST = 'http-request', | ||||
|   HTTP_RESPONSE = 'http-response', | ||||
|   CERTIFICATE_NEEDED = 'certificate-needed', | ||||
|   CERTIFICATE_LOADED = 'certificate-loaded' | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Base interface for forwarding handlers | ||||
|  */ | ||||
| export interface IForwardingHandler extends plugins.EventEmitter { | ||||
|   initialize(): Promise<void>; | ||||
|   handleConnection(socket: plugins.net.Socket): void; | ||||
|   handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Helper function types for common forwarding patterns | ||||
|  */ | ||||
| export const httpOnly = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'http-only', | ||||
|   target: partialConfig.target, | ||||
|   http: { enabled: true, ...(partialConfig.http || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
|  | ||||
| export const tlsTerminateToHttp = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'https-terminate-to-http', | ||||
|   target: partialConfig.target, | ||||
|   https: { ...(partialConfig.https || {}) }, | ||||
|   acme: { enabled: true, maintenance: true, ...(partialConfig.acme || {}) }, | ||||
|   http: { enabled: true, redirectToHttps: true, ...(partialConfig.http || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
|  | ||||
| export const tlsTerminateToHttps = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'https-terminate-to-https', | ||||
|   target: partialConfig.target, | ||||
|   https: { ...(partialConfig.https || {}) }, | ||||
|   acme: { enabled: true, maintenance: true, ...(partialConfig.acme || {}) }, | ||||
|   http: { enabled: true, redirectToHttps: true, ...(partialConfig.http || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
|  | ||||
| export const httpsPassthrough = ( | ||||
|   partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'> | ||||
| ): IForwardConfig => ({ | ||||
|   type: 'https-passthrough', | ||||
|   target: partialConfig.target, | ||||
|   https: { forwardSni: true, ...(partialConfig.https || {}) }, | ||||
|   ...(partialConfig.security ? { security: partialConfig.security } : {}), | ||||
|   ...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {}) | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user