fix
This commit is contained in:
		| @@ -13,64 +13,17 @@ export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'ht | ||||
|  */ | ||||
| export type IRoutedSmartProxyOptions = ISmartProxyOptions; | ||||
|  | ||||
| /** | ||||
|  * Legacy domain configuration interface for backward compatibility | ||||
|  */ | ||||
| export interface IDomainConfig { | ||||
|   domains: string[]; | ||||
|   forwarding: { | ||||
|     type: TForwardingType; | ||||
|     target: { | ||||
|       host: string | string[]; | ||||
|       port: number; | ||||
|     }; | ||||
|     acme?: { | ||||
|       enabled?: boolean; | ||||
|       maintenance?: boolean; | ||||
|       production?: boolean; | ||||
|       forwardChallenges?: { | ||||
|         host: string; | ||||
|         port: number; | ||||
|         useTls?: boolean; | ||||
|       }; | ||||
|     }; | ||||
|     http?: { | ||||
|       enabled?: boolean; | ||||
|       redirectToHttps?: boolean; | ||||
|       headers?: Record<string, string>; | ||||
|     }; | ||||
|     https?: { | ||||
|       customCert?: { | ||||
|         key: string; | ||||
|         cert: string; | ||||
|       }; | ||||
|       forwardSni?: boolean; | ||||
|     }; | ||||
|     security?: { | ||||
|       allowedIps?: string[]; | ||||
|       blockedIps?: string[]; | ||||
|       maxConnections?: number; | ||||
|     }; | ||||
|     advanced?: { | ||||
|       portRanges?: Array<{ from: number; to: number }>; | ||||
|       networkProxyPort?: number; | ||||
|       keepAlive?: boolean; | ||||
|       timeout?: number; | ||||
|       headers?: Record<string, string>; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Helper functions for type checking configuration types | ||||
|  */ | ||||
| export function isLegacyOptions(options: any): boolean { | ||||
|   return !!(options.domainConfigs && options.domainConfigs.length > 0 && | ||||
|            (!options.routes || options.routes.length === 0)); | ||||
|   // Legacy options are no longer supported | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| export function isRoutedOptions(options: any): boolean { | ||||
|   return !!(options.routes && options.routes.length > 0); | ||||
|   // All configurations are now route-based | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -80,14 +33,7 @@ export interface ISmartProxyOptions { | ||||
|   // The unified configuration array (required) | ||||
|   routes: IRouteConfig[]; | ||||
|  | ||||
|   // Legacy options for backward compatibility | ||||
|   fromPort?: number; | ||||
|   toPort?: number; | ||||
|   sniEnabled?: boolean; | ||||
|   domainConfigs?: IDomainConfig[]; | ||||
|   targetIP?: string; | ||||
|   defaultAllowedIPs?: string[]; | ||||
|   defaultBlockedIPs?: string[]; | ||||
|   // Port range configuration | ||||
|   globalPortRanges?: Array<{ from: number; to: number }>; | ||||
|   forwardAllGlobalRanges?: boolean; | ||||
|   preserveSourceIP?: boolean; | ||||
| @@ -99,8 +45,8 @@ export interface ISmartProxyOptions { | ||||
|       port: number; // Default port to use when not specified in routes | ||||
|     }; | ||||
|     security?: { | ||||
|       allowedIPs?: string[]; // Default allowed IPs | ||||
|       blockedIPs?: string[]; // Default blocked IPs | ||||
|       allowedIps?: string[]; // Default allowed IPs | ||||
|       blockedIps?: string[]; // Default blocked IPs | ||||
|       maxConnections?: number; // Default max connections | ||||
|     }; | ||||
|     preserveSourceIP?: boolean; // Default source IP preservation | ||||
| @@ -184,9 +130,6 @@ export interface IConnectionRecord { | ||||
|   pendingData: Buffer[]; // Buffer to hold data during connection setup | ||||
|   pendingDataSize: number; // Track total size of pending data | ||||
|  | ||||
|   // Legacy property for backward compatibility | ||||
|   domainConfig?: IDomainConfig; | ||||
|  | ||||
|   // Enhanced tracking fields | ||||
|   bytesReceived: number; // Total bytes received | ||||
|   bytesSent: number; // Total bytes sent | ||||
|   | ||||
| @@ -4,10 +4,19 @@ import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||
| import { Port80HandlerEvents } from '../../core/models/common-types.js'; | ||||
| import { subscribeToPort80Handler } from '../../core/utils/event-utils.js'; | ||||
| import type { ICertificateData } from '../../certificate/models/certificate-types.js'; | ||||
| import type { IConnectionRecord, ISmartProxyOptions, IDomainConfig } from './models/interfaces.js'; | ||||
| import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; | ||||
| import type { IRouteConfig } from './models/route-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Manages NetworkProxy integration for TLS termination | ||||
|  * | ||||
|  * NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination. | ||||
|  * It converts route configurations to NetworkProxy configuration format and manages | ||||
|  * certificate provisioning through Port80Handler when ACME is enabled. | ||||
|  * | ||||
|  * It is used by SmartProxy for routes that have: | ||||
|  * - TLS mode of 'terminate' or 'terminate-and-reencrypt' | ||||
|  * - Certificate set to 'auto' or custom certificate | ||||
|  */ | ||||
| export class NetworkProxyBridge { | ||||
|   private networkProxy: NetworkProxy | null = null; | ||||
| @@ -58,8 +67,8 @@ export class NetworkProxyBridge { | ||||
|         this.networkProxy.setExternalPort80Handler(this.port80Handler); | ||||
|       } | ||||
|  | ||||
|       // Convert and apply domain configurations to NetworkProxy | ||||
|       await this.syncDomainConfigsToNetworkProxy(); | ||||
|       // Apply route configurations to NetworkProxy | ||||
|       await this.syncRoutesToNetworkProxy(this.settings.routes || []); | ||||
|     } | ||||
|   } | ||||
|    | ||||
| @@ -249,9 +258,19 @@ export class NetworkProxyBridge { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Synchronizes domain configurations to NetworkProxy | ||||
|    * Synchronizes routes to NetworkProxy | ||||
|    * | ||||
|    * This method converts route configurations to NetworkProxy format and updates | ||||
|    * the NetworkProxy with the converted configurations. It handles: | ||||
|    * | ||||
|    * - Extracting domain, target, and certificate information from routes | ||||
|    * - Converting TLS mode settings to NetworkProxy configuration | ||||
|    * - Applying security and advanced settings | ||||
|    * - Registering domains for ACME certificate provisioning when needed | ||||
|    * | ||||
|    * @param routes The route configurations to sync to NetworkProxy | ||||
|    */ | ||||
|   public async syncDomainConfigsToNetworkProxy(): Promise<void> { | ||||
|   public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> { | ||||
|     if (!this.networkProxy) { | ||||
|       console.log('Cannot sync configurations - NetworkProxy not initialized'); | ||||
|       return; | ||||
| @@ -282,38 +301,112 @@ export class NetworkProxyBridge { | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       // Convert domain configs to NetworkProxy configs | ||||
|       const proxyConfigs = this.networkProxy.convertSmartProxyConfigs( | ||||
|         this.settings.domainConfigs, | ||||
|         certPair | ||||
|       ); | ||||
|       // Convert routes to NetworkProxy configs | ||||
|       const proxyConfigs = this.convertRoutesToNetworkProxyConfigs(routes, certPair); | ||||
|  | ||||
|       // Log ACME-eligible domains | ||||
|       const acmeEnabled = !!this.settings.acme?.enabled; | ||||
|       if (acmeEnabled) { | ||||
|         const acmeEligibleDomains = proxyConfigs | ||||
|           .filter((config) => !config.hostName.includes('*')) // Exclude wildcards | ||||
|           .map((config) => config.hostName); | ||||
|  | ||||
|         if (acmeEligibleDomains.length > 0) { | ||||
|           console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`); | ||||
|            | ||||
|           // Register these domains with Port80Handler if available | ||||
|           if (this.port80Handler) { | ||||
|             this.registerDomainsWithPort80Handler(acmeEligibleDomains); | ||||
|           } | ||||
|         } else { | ||||
|           console.log('No domains eligible for ACME certificates found in configuration'); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Update NetworkProxy with the converted configs | ||||
|       // Update the proxy configs | ||||
|       await this.networkProxy.updateProxyConfigs(proxyConfigs); | ||||
|       console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`); | ||||
|       console.log(`Synced ${proxyConfigs.length} configurations to NetworkProxy`); | ||||
|     } catch (err) { | ||||
|       console.log(`Failed to sync configurations: ${err}`); | ||||
|       console.log(`Error syncing routes to NetworkProxy: ${err}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Convert routes to NetworkProxy configuration format | ||||
|    * | ||||
|    * This method transforms route-based configuration to NetworkProxy's configuration format. | ||||
|    * It processes each route and creates appropriate NetworkProxy configs for domains | ||||
|    * that require TLS termination. | ||||
|    * | ||||
|    * @param routes Array of route configurations to convert | ||||
|    * @param defaultCertPair Default certificate to use if no custom certificate is specified | ||||
|    * @returns Array of NetworkProxy configurations | ||||
|    */ | ||||
|   public convertRoutesToNetworkProxyConfigs( | ||||
|     routes: IRouteConfig[], | ||||
|     defaultCertPair: { key: string; cert: string } | ||||
|   ): plugins.tsclass.network.IReverseProxyConfig[] { | ||||
|     const configs: plugins.tsclass.network.IReverseProxyConfig[] = []; | ||||
|  | ||||
|     for (const route of routes) { | ||||
|       // Skip routes without domains | ||||
|       if (!route.match.domains) continue; | ||||
|  | ||||
|       // Skip non-forward routes | ||||
|       if (route.action.type !== 'forward') continue; | ||||
|  | ||||
|       // Skip routes without TLS configuration | ||||
|       if (!route.action.tls || !route.action.target) continue; | ||||
|  | ||||
|       // Get domains from route | ||||
|       const domains = Array.isArray(route.match.domains) | ||||
|         ? route.match.domains | ||||
|         : [route.match.domains]; | ||||
|  | ||||
|       // Create a config for each domain | ||||
|       for (const domain of domains) { | ||||
|         // Determine if this route requires TLS termination | ||||
|         const needsTermination = route.action.tls.mode === 'terminate' || | ||||
|                                 route.action.tls.mode === 'terminate-and-reencrypt'; | ||||
|  | ||||
|         // Skip passthrough domains for NetworkProxy | ||||
|         if (route.action.tls.mode === 'passthrough') continue; | ||||
|  | ||||
|         // Get certificate | ||||
|         let certKey = defaultCertPair.key; | ||||
|         let certCert = defaultCertPair.cert; | ||||
|  | ||||
|         // Use custom certificate if specified | ||||
|         if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate === 'object') { | ||||
|           certKey = route.action.tls.certificate.key; | ||||
|           certCert = route.action.tls.certificate.cert; | ||||
|         } | ||||
|  | ||||
|         // Determine target hosts and ports | ||||
|         const targetHosts = Array.isArray(route.action.target.host) | ||||
|           ? route.action.target.host | ||||
|           : [route.action.target.host]; | ||||
|  | ||||
|         const targetPort = route.action.target.port; | ||||
|  | ||||
|         // Create NetworkProxy config | ||||
|         const config: plugins.tsclass.network.IReverseProxyConfig = { | ||||
|           hostName: domain, | ||||
|           privateKey: certKey, | ||||
|           publicKey: certCert, | ||||
|           destinationIps: targetHosts, | ||||
|           destinationPorts: [targetPort], | ||||
|           proxyConfig: { | ||||
|             targetIsTls: route.action.tls.mode === 'terminate-and-reencrypt', | ||||
|             allowHTTP1: true, | ||||
|             // Apply any other NetworkProxy-specific settings | ||||
|             ...(route.action.advanced ? { | ||||
|               preserveHost: true, | ||||
|               timeout: route.action.advanced.timeout, | ||||
|               headers: route.action.advanced.headers | ||||
|             } : {}) | ||||
|           } | ||||
|         }; | ||||
|  | ||||
|         configs.push(config); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return configs; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @deprecated This method is deprecated and will be removed in a future version. | ||||
|    * Use syncRoutesToNetworkProxy() instead. | ||||
|    * | ||||
|    * This legacy method exists only for backward compatibility and | ||||
|    * simply forwards to syncRoutesToNetworkProxy(). | ||||
|    */ | ||||
|   public async syncDomainConfigsToNetworkProxy(): Promise<void> { | ||||
|     console.log('Method syncDomainConfigsToNetworkProxy is deprecated. Use syncRoutesToNetworkProxy instead.'); | ||||
|     await this.syncRoutesToNetworkProxy(this.settings.routes || []); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Request a certificate for a specific domain | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { | ||||
|   IConnectionRecord, | ||||
|   IDomainConfig, | ||||
|   ISmartProxyOptions | ||||
| } from './models/interfaces.js'; | ||||
| import { | ||||
|   isRoutedOptions, | ||||
|   isLegacyOptions | ||||
|   isRoutedOptions | ||||
| } from './models/interfaces.js'; | ||||
| import type { | ||||
|   IRouteConfig, | ||||
| @@ -14,13 +12,11 @@ import type { | ||||
| } from './models/route-types.js'; | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { DomainConfigManager } from './domain-config-manager.js'; | ||||
| import { TlsManager } from './tls-manager.js'; | ||||
| import { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| import { TimeoutManager } from './timeout-manager.js'; | ||||
| import { RouteManager } from './route-manager.js'; | ||||
| import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; | ||||
| import type { TForwardingType } from '../../forwarding/config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Handles new connection processing and setup logic with support for route-based configuration | ||||
| @@ -32,7 +28,6 @@ export class RouteConnectionHandler { | ||||
|     settings: ISmartProxyOptions, | ||||
|     private connectionManager: ConnectionManager, | ||||
|     private securityManager: SecurityManager, | ||||
|     private domainConfigManager: DomainConfigManager, | ||||
|     private tlsManager: TlsManager, | ||||
|     private networkProxyBridge: NetworkProxyBridge, | ||||
|     private timeoutManager: TimeoutManager, | ||||
| @@ -244,37 +239,20 @@ export class RouteConnectionHandler { | ||||
|  | ||||
|     if (!routeMatch) { | ||||
|       console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`); | ||||
|        | ||||
|       // Fall back to legacy matching if we're using a hybrid configuration | ||||
|       const domainConfig = serverName  | ||||
|         ? this.domainConfigManager.findDomainConfig(serverName) | ||||
|         : this.domainConfigManager.findDomainConfigForPort(localPort); | ||||
|  | ||||
|       if (domainConfig) { | ||||
|         if (this.settings.enableDetailedLogging) { | ||||
|           console.log(`[${connectionId}] Using legacy domain configuration for ${serverName || 'port ' + localPort}`); | ||||
|         } | ||||
|          | ||||
|         // Associate this domain config with the connection | ||||
|         record.domainConfig = domainConfig; | ||||
|          | ||||
|         // Handle the connection using the legacy setup | ||||
|         return this.handleLegacyConnection(socket, record, serverName, domainConfig, initialChunk); | ||||
|       } | ||||
|        | ||||
|       // No matching route or domain config, use default/fallback handling | ||||
|       // No matching route, use default/fallback handling | ||||
|       console.log(`[${connectionId}] Using default route handling for connection`); | ||||
|        | ||||
|  | ||||
|       // Check default security settings | ||||
|       const defaultSecuritySettings = this.settings.defaults?.security; | ||||
|       if (defaultSecuritySettings) { | ||||
|         if (defaultSecuritySettings.allowedIPs && defaultSecuritySettings.allowedIPs.length > 0) { | ||||
|         if (defaultSecuritySettings.allowedIps && defaultSecuritySettings.allowedIps.length > 0) { | ||||
|           const isAllowed = this.securityManager.isIPAuthorized( | ||||
|             remoteIP, | ||||
|             defaultSecuritySettings.allowedIPs, | ||||
|             defaultSecuritySettings.blockedIPs || [] | ||||
|             defaultSecuritySettings.allowedIps, | ||||
|             defaultSecuritySettings.blockedIps || [] | ||||
|           ); | ||||
|            | ||||
|  | ||||
|           if (!isAllowed) { | ||||
|             console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`); | ||||
|             socket.end(); | ||||
| @@ -282,34 +260,24 @@ export class RouteConnectionHandler { | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|       } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { | ||||
|         // Legacy default IP restrictions | ||||
|         const isAllowed = this.securityManager.isIPAuthorized( | ||||
|           remoteIP, | ||||
|           this.settings.defaultAllowedIPs, | ||||
|           this.settings.defaultBlockedIPs || [] | ||||
|         ); | ||||
|          | ||||
|         if (!isAllowed) { | ||||
|           console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`); | ||||
|           socket.end(); | ||||
|           this.connectionManager.cleanupConnection(record, 'ip_blocked'); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Setup direct connection with default settings | ||||
|       let targetHost: string; | ||||
|       let targetPort: number; | ||||
|       if (this.settings.defaults?.target) { | ||||
|         // Use defaults from configuration | ||||
|         const targetHost = this.settings.defaults.target.host; | ||||
|         const targetPort = this.settings.defaults.target.port; | ||||
|  | ||||
|       if (isRoutedOptions(this.settings) && this.settings.defaults?.target) { | ||||
|         // Use defaults from routed configuration | ||||
|         targetHost = this.settings.defaults.target.host; | ||||
|         targetPort = this.settings.defaults.target.port; | ||||
|       } else if (this.settings.targetIP && this.settings.toPort) { | ||||
|         // Fall back to legacy settings | ||||
|         targetHost = this.settings.targetIP; | ||||
|         targetPort = this.settings.toPort; | ||||
|         return this.setupDirectConnection( | ||||
|           socket, | ||||
|           record, | ||||
|           undefined, | ||||
|           serverName, | ||||
|           initialChunk, | ||||
|           undefined, | ||||
|           targetHost, | ||||
|           targetPort | ||||
|         ); | ||||
|       } else { | ||||
|         // No default target available, terminate the connection | ||||
|         console.log(`[${connectionId}] No default target configured. Closing connection.`); | ||||
| @@ -317,17 +285,6 @@ export class RouteConnectionHandler { | ||||
|         this.connectionManager.cleanupConnection(record, 'no_default_target'); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       return this.setupDirectConnection( | ||||
|         socket, | ||||
|         record, | ||||
|         undefined, | ||||
|         serverName, | ||||
|         initialChunk, | ||||
|         undefined, | ||||
|         targetHost, | ||||
|         targetPort | ||||
|       ); | ||||
|     } | ||||
|      | ||||
|     // A matching route was found | ||||
| @@ -575,114 +532,8 @@ export class RouteConnectionHandler { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle a connection using legacy domain configuration | ||||
|    * Legacy connection handling has been removed in favor of pure route-based approach | ||||
|    */ | ||||
|   private handleLegacyConnection( | ||||
|     socket: plugins.net.Socket, | ||||
|     record: IConnectionRecord, | ||||
|     serverName: string, | ||||
|     domainConfig: IDomainConfig, | ||||
|     initialChunk?: Buffer | ||||
|   ): void { | ||||
|     const connectionId = record.id; | ||||
|      | ||||
|     // Get the forwarding type for this domain | ||||
|     const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); | ||||
|      | ||||
|     // IP validation | ||||
|     const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig); | ||||
|      | ||||
|     if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) { | ||||
|       console.log( | ||||
|         `[${connectionId}] Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}` | ||||
|       ); | ||||
|       socket.end(); | ||||
|       this.connectionManager.initiateCleanupOnce(record, 'ip_blocked'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Handle based on forwarding type | ||||
|     switch (forwardingType) { | ||||
|       case 'http-only': | ||||
|         // For HTTP-only configs with TLS traffic | ||||
|         if (record.isTLS) { | ||||
|           console.log(`[${connectionId}] Received TLS connection for HTTP-only domain ${serverName}`); | ||||
|           socket.end(); | ||||
|           this.connectionManager.initiateCleanupOnce(record, 'wrong_protocol'); | ||||
|           return; | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-passthrough': | ||||
|         // For TLS passthrough with TLS traffic | ||||
|         if (record.isTLS) { | ||||
|           try { | ||||
|             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 | ||||
|             return handler.handleConnection(socket); | ||||
|           } catch (err) { | ||||
|             console.log(`[${connectionId}] Error using forwarding handler: ${err}`); | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
|       case 'https-terminate-to-https': | ||||
|         // For TLS termination with TLS traffic | ||||
|         if (record.isTLS) { | ||||
|           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 | ||||
|           return this.networkProxyBridge.forwardToNetworkProxy( | ||||
|             connectionId, | ||||
|             socket, | ||||
|             record, | ||||
|             initialChunk!, | ||||
|             networkProxyPort, | ||||
|             (reason) => this.connectionManager.initiateCleanupOnce(record, reason) | ||||
|           ); | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|     // If we're still here, use the forwarding handler if available | ||||
|     try { | ||||
|       const handler = this.domainConfigManager.getForwardingHandler(domainConfig); | ||||
|        | ||||
|       if (this.settings.enableDetailedLogging) { | ||||
|         console.log(`[${connectionId}] Using general forwarding handler for domain ${serverName || 'unknown'}`); | ||||
|       } | ||||
|        | ||||
|       // Handle the connection using the handler | ||||
|       return handler.handleConnection(socket); | ||||
|     } catch (err) { | ||||
|       console.log(`[${connectionId}] Error using forwarding handler: ${err}`); | ||||
|     } | ||||
|      | ||||
|     // Fallback: set up direct connection | ||||
|     const targetIp = this.domainConfigManager.getTargetIP(domainConfig); | ||||
|     const targetPort = this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort); | ||||
|      | ||||
|     return this.setupDirectConnection( | ||||
|       socket, | ||||
|       record, | ||||
|       domainConfig, | ||||
|       serverName, | ||||
|       initialChunk, | ||||
|       undefined, | ||||
|       targetIp, | ||||
|       targetPort | ||||
|     ); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Sets up a direct connection to the target | ||||
| @@ -690,7 +541,7 @@ export class RouteConnectionHandler { | ||||
|   private setupDirectConnection( | ||||
|     socket: plugins.net.Socket, | ||||
|     record: IConnectionRecord, | ||||
|     domainConfig?: IDomainConfig, | ||||
|     _unused?: any, // kept for backward compatibility | ||||
|     serverName?: string, | ||||
|     initialChunk?: Buffer, | ||||
|     overridePort?: number, | ||||
| @@ -698,22 +549,15 @@ export class RouteConnectionHandler { | ||||
|     targetPort?: number | ||||
|   ): void { | ||||
|     const connectionId = record.id; | ||||
|      | ||||
|     // Determine target host and port if not provided | ||||
|     const finalTargetHost = targetHost || (domainConfig | ||||
|       ? this.domainConfigManager.getTargetIP(domainConfig) | ||||
|       : this.settings.defaults?.target?.host | ||||
|         ? this.settings.defaults.target.host | ||||
|         : this.settings.targetIP!); | ||||
|  | ||||
|     // Determine target port - first try explicit port, then forwarding config, then fallback | ||||
|     const finalTargetPort = targetPort || (overridePort !== undefined | ||||
|       ? overridePort | ||||
|       : domainConfig | ||||
|         ? this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort) | ||||
|         : this.settings.defaults?.target?.port | ||||
|           ? this.settings.defaults.target.port | ||||
|           : this.settings.toPort); | ||||
|     // Determine target host and port if not provided | ||||
|     const finalTargetHost = targetHost || | ||||
|       (this.settings.defaults?.target?.host || 'localhost'); | ||||
|  | ||||
|     // Determine target port | ||||
|     const finalTargetPort = targetPort || | ||||
|       (overridePort !== undefined ? overridePort : | ||||
|        (this.settings.defaults?.target?.port || 443)); | ||||
|  | ||||
|     // Setup connection options | ||||
|     const connectionOptions: plugins.net.NetConnectOpts = { | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import * as plugins from '../../plugins.js'; | ||||
| // Importing required components | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { DomainConfigManager } from './domain-config-manager.js'; | ||||
| import { TlsManager } from './tls-manager.js'; | ||||
| import { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| import { TimeoutManager } from './timeout-manager.js'; | ||||
| @@ -19,16 +18,25 @@ import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; | ||||
| import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; | ||||
|  | ||||
| // Import types and utilities | ||||
| import type {  | ||||
|   ISmartProxyOptions,  | ||||
|   IRoutedSmartProxyOptions, | ||||
|   IDomainConfig | ||||
| import type { | ||||
|   ISmartProxyOptions, | ||||
|   IRoutedSmartProxyOptions | ||||
| } from './models/interfaces.js'; | ||||
| import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js'; | ||||
| import { isRoutedOptions } from './models/interfaces.js'; | ||||
| import type { IRouteConfig } from './models/route-types.js'; | ||||
|  | ||||
| /** | ||||
|  * SmartProxy - Unified route-based API | ||||
|  * SmartProxy - Pure route-based API | ||||
|  * | ||||
|  * SmartProxy is a unified proxy system that works with routes to define connection handling behavior. | ||||
|  * Each route contains matching criteria (ports, domains, etc.) and an action to take (forward, redirect, block). | ||||
|  * | ||||
|  * Configuration is provided through a set of routes, with each route defining: | ||||
|  * - What to match (ports, domains, paths, client IPs) | ||||
|  * - What to do with matching traffic (forward, redirect, block) | ||||
|  * - How to handle TLS (passthrough, terminate, terminate-and-reencrypt) | ||||
|  * - Security settings (IP restrictions, connection limits) | ||||
|  * - Advanced options (timeout, headers, etc.) | ||||
|  */ | ||||
| export class SmartProxy extends plugins.EventEmitter { | ||||
|   private netServers: plugins.net.Server[] = []; | ||||
| @@ -38,7 +46,6 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   // Component managers | ||||
|   private connectionManager: ConnectionManager; | ||||
|   private securityManager: SecurityManager; | ||||
|   private domainConfigManager: DomainConfigManager; | ||||
|   private tlsManager: TlsManager; | ||||
|   private networkProxyBridge: NetworkProxyBridge; | ||||
|   private timeoutManager: TimeoutManager; | ||||
| @@ -52,7 +59,35 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   private certProvisioner?: CertProvisioner; | ||||
|    | ||||
|   /** | ||||
|    * Constructor that supports both legacy and route-based configuration | ||||
|    * Constructor for SmartProxy | ||||
|    * | ||||
|    * @param settingsArg Configuration options containing routes and other settings | ||||
|    * Routes define how traffic is matched and handled, with each route having: | ||||
|    * - match: criteria for matching traffic (ports, domains, paths, IPs) | ||||
|    * - action: what to do with matched traffic (forward, redirect, block) | ||||
|    * | ||||
|    * Example: | ||||
|    * ```ts | ||||
|    * const proxy = new SmartProxy({ | ||||
|    *   routes: [ | ||||
|    *     { | ||||
|    *       match: { | ||||
|    *         ports: 443, | ||||
|    *         domains: ['example.com', '*.example.com'] | ||||
|    *       }, | ||||
|    *       action: { | ||||
|    *         type: 'forward', | ||||
|    *         target: { host: '10.0.0.1', port: 8443 }, | ||||
|    *         tls: { mode: 'passthrough' } | ||||
|    *       } | ||||
|    *     } | ||||
|    *   ], | ||||
|    *   defaults: { | ||||
|    *     target: { host: 'localhost', port: 8080 }, | ||||
|    *     security: { allowedIps: ['*'] } | ||||
|    *   } | ||||
|    * }); | ||||
|    * ``` | ||||
|    */ | ||||
|   constructor(settingsArg: ISmartProxyOptions) { | ||||
|     super(); | ||||
| @@ -113,17 +148,10 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       this.timeoutManager | ||||
|     ); | ||||
|      | ||||
|     // Create the new route manager first | ||||
|     // Create the route manager | ||||
|     this.routeManager = new RouteManager(this.settings); | ||||
|  | ||||
|     // Create domain config manager and port range manager | ||||
|     this.domainConfigManager = new DomainConfigManager(this.settings); | ||||
|  | ||||
|     // Share the route manager with the domain config manager | ||||
|     if (typeof this.domainConfigManager.setRouteManager === 'function') { | ||||
|       this.domainConfigManager.setRouteManager(this.routeManager); | ||||
|     } | ||||
|  | ||||
|     // Create port range manager | ||||
|     this.portRangeManager = new PortRangeManager(this.settings); | ||||
|      | ||||
|     // Create other required components | ||||
| @@ -135,7 +163,6 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       this.settings, | ||||
|       this.connectionManager, | ||||
|       this.securityManager, | ||||
|       this.domainConfigManager, | ||||
|       this.tlsManager, | ||||
|       this.networkProxyBridge, | ||||
|       this.timeoutManager, | ||||
| @@ -162,7 +189,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       // Build and start the Port80Handler | ||||
|       this.port80Handler = buildPort80Handler({ | ||||
|         ...config, | ||||
|         httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443) | ||||
|         httpsRedirectPort: config.httpsRedirectPort || 443 | ||||
|       }); | ||||
|        | ||||
|       // Share Port80Handler with NetworkProxyBridge before start | ||||
| @@ -184,17 +211,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Initialize domain config based on configuration type | ||||
|     if (isLegacyOptions(this.settings)) { | ||||
|       // Initialize domain config manager with the legacy domain configs | ||||
|       this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs || []); | ||||
|     } else if (isRoutedOptions(this.settings)) { | ||||
|       // For pure route-based configuration, the domain config is already initialized | ||||
|       // in the constructor, but we might need to regenerate it | ||||
|       if (typeof this.domainConfigManager.generateDomainConfigsFromRoutes === 'function') { | ||||
|         this.domainConfigManager.generateDomainConfigsFromRoutes(); | ||||
|       } | ||||
|     } | ||||
|     // Pure route-based configuration - no domain configs needed | ||||
|  | ||||
|     // Initialize Port80Handler if enabled | ||||
|     await this.initializePort80Handler(); | ||||
| @@ -248,35 +265,18 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       }) || []; | ||||
|  | ||||
|       // Create CertProvisioner with appropriate parameters | ||||
|       if (isLegacyOptions(this.settings)) { | ||||
|         this.certProvisioner = new CertProvisioner( | ||||
|           this.settings.domainConfigs, | ||||
|           this.port80Handler, | ||||
|           this.networkProxyBridge, | ||||
|           this.settings.certProvisionFunction, | ||||
|           acme.renewThresholdDays!, | ||||
|           acme.renewCheckIntervalHours!, | ||||
|           acme.autoRenew!, | ||||
|           domainForwards | ||||
|         ); | ||||
|       } else { | ||||
|         // For route-based configuration, we need to adapt the interface | ||||
|         // Convert routes to domain configs for CertProvisioner | ||||
|         const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes( | ||||
|           (this.settings as IRoutedSmartProxyOptions).routes | ||||
|         ); | ||||
|  | ||||
|         this.certProvisioner = new CertProvisioner( | ||||
|           domainConfigs, | ||||
|           this.port80Handler, | ||||
|           this.networkProxyBridge, | ||||
|           this.settings.certProvisionFunction, | ||||
|           acme.renewThresholdDays!, | ||||
|           acme.renewCheckIntervalHours!, | ||||
|           acme.autoRenew!, | ||||
|           domainForwards | ||||
|         ); | ||||
|       } | ||||
|       // No longer need to support multiple configuration types | ||||
|       // Just pass the routes directly | ||||
|       this.certProvisioner = new CertProvisioner( | ||||
|         this.settings.routes, | ||||
|         this.port80Handler, | ||||
|         this.networkProxyBridge, | ||||
|         this.settings.certProvisionFunction, | ||||
|         acme.renewThresholdDays!, | ||||
|         acme.renewCheckIntervalHours!, | ||||
|         acme.autoRenew!, | ||||
|         domainForwards | ||||
|       ); | ||||
|  | ||||
|       // Register certificate event handler | ||||
|       this.certProvisioner.on('certificate', (certData) => { | ||||
| @@ -416,60 +416,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|    | ||||
|   /** | ||||
|    * Extract domain configurations from routes for certificate provisioning | ||||
|    * | ||||
|    * Note: This method has been removed as we now work directly with routes | ||||
|    */ | ||||
|   private extractDomainConfigsFromRoutes(routes: IRouteConfig[]): IDomainConfig[] { | ||||
|     const domainConfigs: IDomainConfig[] = []; | ||||
|      | ||||
|     for (const route of routes) { | ||||
|       // Skip routes without domain specs | ||||
|       if (!route.match.domains) continue; | ||||
|        | ||||
|       // Skip non-forward routes | ||||
|       if (route.action.type !== 'forward') continue; | ||||
|        | ||||
|       // Only process routes that need TLS termination (those with certificates) | ||||
|       if (!route.action.tls ||  | ||||
|           route.action.tls.mode === 'passthrough' ||  | ||||
|           !route.action.target) continue; | ||||
|        | ||||
|       const domains = Array.isArray(route.match.domains)  | ||||
|         ? route.match.domains  | ||||
|         : [route.match.domains]; | ||||
|        | ||||
|       // Determine forwarding type based on TLS mode | ||||
|       const forwardingType = route.action.tls.mode === 'terminate'  | ||||
|         ? 'https-terminate-to-http'  | ||||
|         : 'https-terminate-to-https'; | ||||
|        | ||||
|       // Create a forwarding config | ||||
|       const forwarding = { | ||||
|         type: forwardingType as any, | ||||
|         target: { | ||||
|           host: Array.isArray(route.action.target.host)  | ||||
|             ? route.action.target.host[0]  | ||||
|             : route.action.target.host, | ||||
|           port: route.action.target.port | ||||
|         }, | ||||
|         // Add TLS settings | ||||
|         https: { | ||||
|           customCert: route.action.tls.certificate !== 'auto'  | ||||
|             ? route.action.tls.certificate  | ||||
|             : undefined | ||||
|         }, | ||||
|         // Add security settings if present | ||||
|         security: route.action.security, | ||||
|         // Add advanced settings if present | ||||
|         advanced: route.action.advanced | ||||
|       }; | ||||
|        | ||||
|       domainConfigs.push({ | ||||
|         domains, | ||||
|         forwarding | ||||
|       }); | ||||
|     } | ||||
|      | ||||
|     return domainConfigs; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop the proxy server | ||||
| @@ -535,134 +484,74 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Updates the domain configurations for the proxy (legacy support) | ||||
|    * Updates the domain configurations for the proxy | ||||
|    * | ||||
|    * Note: This legacy method has been removed. Use updateRoutes instead. | ||||
|    */ | ||||
|   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { | ||||
|     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); | ||||
|  | ||||
|     // Update domain configs in DomainConfigManager (legacy) | ||||
|     this.domainConfigManager.updateDomainConfigs(newDomainConfigs); | ||||
|  | ||||
|     // Also update the RouteManager with these domain configs | ||||
|     this.routeManager.updateFromDomainConfigs(newDomainConfigs); | ||||
|  | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); | ||||
|     } | ||||
|  | ||||
|     // If Port80Handler is running, provision certificates based on forwarding type | ||||
|     if (this.port80Handler && this.settings.acme?.enabled) { | ||||
|       for (const domainConfig of newDomainConfigs) { | ||||
|         // Skip certificate provisioning for http-only or passthrough configs that don't need certs | ||||
|         const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); | ||||
|         const needsCertificate = | ||||
|           forwardingType === 'https-terminate-to-http' || | ||||
|           forwardingType === 'https-terminate-to-https'; | ||||
|  | ||||
|         // Skip certificate provisioning if ACME is explicitly disabled for this domain | ||||
|         const acmeDisabled = domainConfig.forwarding.acme?.enabled === false; | ||||
|  | ||||
|         if (!needsCertificate || acmeDisabled) { | ||||
|           if (this.settings.enableDetailedLogging) { | ||||
|             console.log(`Skipping certificate provisioning for ${domainConfig.domains.join(', ')} (${forwardingType})`); | ||||
|           } | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         for (const domain of domainConfig.domains) { | ||||
|           const isWildcard = domain.includes('*'); | ||||
|           let provision: string | plugins.tsclass.network.ICert = 'http01'; | ||||
|  | ||||
|           // Check for ACME forwarding configuration in the domain | ||||
|           const forwardAcmeChallenges = domainConfig.forwarding.acme?.forwardChallenges; | ||||
|  | ||||
|           if (this.settings.certProvisionFunction) { | ||||
|             try { | ||||
|               provision = await this.settings.certProvisionFunction(domain); | ||||
|             } catch (err) { | ||||
|               console.log(`certProvider error for ${domain}: ${err}`); | ||||
|             } | ||||
|           } else if (isWildcard) { | ||||
|             console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); | ||||
|             continue; | ||||
|           } | ||||
|  | ||||
|           if (provision === 'http01') { | ||||
|             if (isWildcard) { | ||||
|               console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); | ||||
|               continue; | ||||
|             } | ||||
|  | ||||
|             // Create Port80Handler options from the forwarding configuration | ||||
|             const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding); | ||||
|  | ||||
|             this.port80Handler.addDomain(port80Config); | ||||
|             console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); | ||||
|           } else { | ||||
|             // Static certificate (e.g., DNS-01 provisioned) supports wildcards | ||||
|             const certObj = provision as plugins.tsclass.network.ICert; | ||||
|             const certData: ICertificateData = { | ||||
|               domain: certObj.domainName, | ||||
|               certificate: certObj.publicKey, | ||||
|               privateKey: certObj.privateKey, | ||||
|               expiryDate: new Date(certObj.validUntil) | ||||
|             }; | ||||
|             this.networkProxyBridge.applyExternalCertificate(certData); | ||||
|             console.log(`Applied static certificate for ${domain} from certProvider`); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       console.log('Provisioned certificates for new domains'); | ||||
|     } | ||||
|   public async updateDomainConfigs(): Promise<void> { | ||||
|     console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.'); | ||||
|     throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead'); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Update routes with new configuration (new API) | ||||
|    * Update routes with new configuration | ||||
|    * | ||||
|    * This method replaces the current route configuration with the provided routes. | ||||
|    * It also provisions certificates for routes that require TLS termination and have | ||||
|    * `certificate: 'auto'` set in their TLS configuration. | ||||
|    * | ||||
|    * @param newRoutes Array of route configurations to use | ||||
|    * | ||||
|    * Example: | ||||
|    * ```ts | ||||
|    * proxy.updateRoutes([ | ||||
|    *   { | ||||
|    *     match: { ports: 443, domains: 'secure.example.com' }, | ||||
|    *     action: { | ||||
|    *       type: 'forward', | ||||
|    *       target: { host: '10.0.0.1', port: 8443 }, | ||||
|    *       tls: { mode: 'terminate', certificate: 'auto' } | ||||
|    *     } | ||||
|    *   } | ||||
|    * ]); | ||||
|    * ``` | ||||
|    */ | ||||
|   public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> { | ||||
|     console.log(`Updating routes (${newRoutes.length} routes)`); | ||||
|      | ||||
|  | ||||
|     // Update routes in RouteManager | ||||
|     this.routeManager.updateRoutes(newRoutes); | ||||
|      | ||||
|  | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|       // Create equivalent domain configs for NetworkProxy | ||||
|       const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes); | ||||
|        | ||||
|       // Update domain configs in DomainConfigManager for sync | ||||
|       this.domainConfigManager.updateDomainConfigs(domainConfigs); | ||||
|        | ||||
|       // Sync with NetworkProxy | ||||
|       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); | ||||
|       await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     // If Port80Handler is running, provision certificates based on routes | ||||
|     if (this.port80Handler && this.settings.acme?.enabled) { | ||||
|       for (const route of newRoutes) { | ||||
|         // Skip routes without domains | ||||
|         if (!route.match.domains) continue; | ||||
|          | ||||
|  | ||||
|         // Skip non-forward routes | ||||
|         if (route.action.type !== 'forward') continue; | ||||
|          | ||||
|  | ||||
|         // Skip routes without TLS termination | ||||
|         if (!route.action.tls ||  | ||||
|             route.action.tls.mode === 'passthrough' ||  | ||||
|         if (!route.action.tls || | ||||
|             route.action.tls.mode === 'passthrough' || | ||||
|             !route.action.target) continue; | ||||
|          | ||||
|  | ||||
|         // Skip certificate provisioning if certificate is not auto | ||||
|         if (route.action.tls.certificate !== 'auto') continue; | ||||
|          | ||||
|         const domains = Array.isArray(route.match.domains)  | ||||
|           ? route.match.domains  | ||||
|  | ||||
|         const domains = Array.isArray(route.match.domains) | ||||
|           ? route.match.domains | ||||
|           : [route.match.domains]; | ||||
|          | ||||
|  | ||||
|         for (const domain of domains) { | ||||
|           const isWildcard = domain.includes('*'); | ||||
|           let provision: string | plugins.tsclass.network.ICert = 'http01'; | ||||
|            | ||||
|  | ||||
|           if (this.settings.certProvisionFunction) { | ||||
|             try { | ||||
|               provision = await this.settings.certProvisionFunction(domain); | ||||
| @@ -673,20 +562,20 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|             console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); | ||||
|             continue; | ||||
|           } | ||||
|            | ||||
|  | ||||
|           if (provision === 'http01') { | ||||
|             if (isWildcard) { | ||||
|               console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); | ||||
|               continue; | ||||
|             } | ||||
|              | ||||
|  | ||||
|             // Register domain with Port80Handler | ||||
|             this.port80Handler.addDomain({ | ||||
|               domainName: domain, | ||||
|               sslRedirect: true, | ||||
|               acmeMaintenance: true | ||||
|             }); | ||||
|              | ||||
|  | ||||
|             console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); | ||||
|           } else { | ||||
|             // Handle static certificate (e.g., DNS-01 provisioned) | ||||
| @@ -702,7 +591,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|        | ||||
|  | ||||
|       console.log('Provisioned certificates for new routes'); | ||||
|     } | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user