fix(network-proxy, route-utils, route-manager): Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports.
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '16.0.2', | ||||
|   version: '16.0.3', | ||||
|   description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Shared Route Context Interface | ||||
|  *  | ||||
| @@ -42,8 +44,8 @@ export interface IRouteContext { | ||||
|  * Used only in NetworkProxy for HTTP request handling | ||||
|  */ | ||||
| export interface IHttpRouteContext extends IRouteContext { | ||||
|   req?: any; // http.IncomingMessage  | ||||
|   res?: any; // http.ServerResponse | ||||
|   req?: plugins.http.IncomingMessage; | ||||
|   res?: plugins.http.ServerResponse; | ||||
|   method?: string; // HTTP method (GET, POST, etc.) | ||||
| } | ||||
|  | ||||
| @@ -52,7 +54,7 @@ export interface IHttpRouteContext extends IRouteContext { | ||||
|  * Used only in NetworkProxy for HTTP/2 request handling | ||||
|  */ | ||||
| export interface IHttp2RouteContext extends IHttpRouteContext { | ||||
|   stream?: any; // http2.Http2Stream | ||||
|   stream?: plugins.http2.ServerHttp2Stream; | ||||
|   headers?: Record<string, string>; // HTTP/2 pseudo-headers like :method, :path | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -13,8 +13,8 @@ | ||||
|  * @returns Whether the domain matches the pattern | ||||
|  */ | ||||
| export function matchDomain(pattern: string, domain: string): boolean { | ||||
|   // Handle exact match | ||||
|   if (pattern === domain) { | ||||
|   // Handle exact match (case-insensitive) | ||||
|   if (pattern.toLowerCase() === domain.toLowerCase()) { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
| @@ -139,9 +139,13 @@ export function matchIpCidr(cidr: string, ip: string): boolean { | ||||
|   try { | ||||
|     const { subnet, bits } = parsed; | ||||
|      | ||||
|     // Normalize IPv6-mapped IPv4 addresses | ||||
|     const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||
|     const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet; | ||||
|      | ||||
|     // Convert IP addresses to numeric values | ||||
|     const ipNum = ipToNumber(ip); | ||||
|     const subnetNum = ipToNumber(subnet); | ||||
|     const ipNum = ipToNumber(normalizedIp); | ||||
|     const subnetNum = ipToNumber(normalizedSubnet); | ||||
|      | ||||
|     // Calculate subnet mask | ||||
|     const maskNum = ~(2 ** (32 - bits) - 1); | ||||
| @@ -161,26 +165,41 @@ export function matchIpCidr(cidr: string, ip: string): boolean { | ||||
|  * @returns Whether the IP matches the pattern | ||||
|  */ | ||||
| export function matchIpPattern(pattern: string, ip: string): boolean { | ||||
|   // Handle exact match | ||||
|   if (pattern === ip) { | ||||
|   // Normalize IPv6-mapped IPv4 addresses | ||||
|   const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||
|   const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern; | ||||
|    | ||||
|   // Handle exact match with all variations | ||||
|   if (pattern === ip || normalizedPattern === normalizedIp ||  | ||||
|       pattern === normalizedIp || normalizedPattern === ip) { | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   // Handle "all" wildcard | ||||
|   if (pattern === '*') { | ||||
|   if (pattern === '*' || normalizedPattern === '*') { | ||||
|     return true; | ||||
|   } | ||||
|    | ||||
|   // Handle CIDR notation (e.g., 192.168.1.0/24) | ||||
|   if (pattern.includes('/')) { | ||||
|     return matchIpCidr(pattern, ip); | ||||
|     return matchIpCidr(pattern, normalizedIp) ||  | ||||
|            (normalizedPattern !== pattern && matchIpCidr(normalizedPattern, normalizedIp)); | ||||
|   } | ||||
|    | ||||
|   // Handle glob pattern (e.g., 192.168.1.*) | ||||
|   if (pattern.includes('*')) { | ||||
|     const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||
|     const regex = new RegExp(`^${regexPattern}$`); | ||||
|     return regex.test(ip); | ||||
|     if (regex.test(ip) || regex.test(normalizedIp)) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // If pattern was normalized, also test with normalized pattern | ||||
|     if (normalizedPattern !== pattern) { | ||||
|       const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||
|       const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`); | ||||
|       return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   return false; | ||||
|   | ||||
| @@ -291,12 +291,15 @@ export class RouteManager { | ||||
|  | ||||
|   /** | ||||
|    * Match an IP pattern against an IP | ||||
|    * Supports exact matches, wildcard patterns, and CIDR notation | ||||
|    */ | ||||
|   private matchIp(pattern: string, ip: string): boolean { | ||||
|     // Exact match | ||||
|     if (pattern === ip) { | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|     // Wildcard matching (e.g., 192.168.0.*) | ||||
|     if (pattern.includes('*')) { | ||||
|       const regexPattern = pattern | ||||
|         .replace(/\./g, '\\.') | ||||
| @@ -306,10 +309,65 @@ export class RouteManager { | ||||
|       return regex.test(ip); | ||||
|     } | ||||
|  | ||||
|     // TODO: Implement CIDR matching | ||||
|     // CIDR matching (e.g., 192.168.0.0/24) | ||||
|     if (pattern.includes('/')) { | ||||
|       try { | ||||
|         const [subnet, bits] = pattern.split('/'); | ||||
|          | ||||
|         // Convert IP addresses to numeric format for comparison | ||||
|         const ipBinary = this.ipToBinary(ip); | ||||
|         const subnetBinary = this.ipToBinary(subnet); | ||||
|          | ||||
|         if (!ipBinary || !subnetBinary) { | ||||
|           return false; | ||||
|         } | ||||
|          | ||||
|         // Get the subnet mask from CIDR notation | ||||
|         const mask = parseInt(bits, 10); | ||||
|         if (isNaN(mask) || mask < 0 || mask > 32) { | ||||
|           return false; | ||||
|         } | ||||
|          | ||||
|         // Check if the first 'mask' bits match between IP and subnet | ||||
|         return ipBinary.slice(0, mask) === subnetBinary.slice(0, mask); | ||||
|       } catch (error) { | ||||
|         // If we encounter any error during CIDR matching, return false | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Convert an IP address to its binary representation | ||||
|    * @param ip The IP address to convert | ||||
|    * @returns Binary string representation or null if invalid | ||||
|    */ | ||||
|   private ipToBinary(ip: string): string | null { | ||||
|     // Handle IPv4 addresses only for now | ||||
|     const parts = ip.split('.'); | ||||
|      | ||||
|     // Validate IP format | ||||
|     if (parts.length !== 4) { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     // Convert each octet to 8-bit binary and concatenate | ||||
|     try { | ||||
|       return parts | ||||
|         .map(part => { | ||||
|           const num = parseInt(part, 10); | ||||
|           if (isNaN(num) || num < 0 || num > 255) { | ||||
|             throw new Error('Invalid IP octet'); | ||||
|           } | ||||
|           return num.toString(2).padStart(8, '0'); | ||||
|         }) | ||||
|         .join(''); | ||||
|     } catch (error) { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -500,68 +500,8 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|     this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @deprecated Use updateRouteConfigs instead | ||||
|    * Legacy method for updating proxy configurations using IReverseProxyConfig | ||||
|    * This method is maintained for backward compatibility | ||||
|    */ | ||||
|   public async updateProxyConfigs( | ||||
|     proxyConfigsArg: IReverseProxyConfig[] | ||||
|   ): Promise<void> { | ||||
|     this.logger.info(`Converting ${proxyConfigsArg.length} legacy configs to route configs`); | ||||
|  | ||||
|     // Convert legacy configs to route configs | ||||
|     const routes: IRouteConfig[] = proxyConfigsArg.map(config => | ||||
|       convertLegacyConfigToRouteConfig(config, this.options.port) | ||||
|     ); | ||||
|  | ||||
|     // Use the primary method | ||||
|     return this.updateRouteConfigs(routes); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @deprecated Use route-based configuration instead | ||||
|    * Converts SmartProxy domain configurations to NetworkProxy configs | ||||
|    * This method is maintained for backward compatibility | ||||
|    */ | ||||
|   public convertSmartProxyConfigs( | ||||
|     domainConfigs: Array<{ | ||||
|       domains: string[]; | ||||
|       targetIPs?: string[]; | ||||
|       allowedIPs?: string[]; | ||||
|     }>, | ||||
|     sslKeyPair?: { key: string; cert: string } | ||||
|   ): IReverseProxyConfig[] { | ||||
|     this.logger.warn('convertSmartProxyConfigs is deprecated - use route-based configuration instead'); | ||||
|  | ||||
|     const proxyConfigs: IReverseProxyConfig[] = []; | ||||
|  | ||||
|     // Use default certificates if not provided | ||||
|     const defaultCerts = this.certificateManager.getDefaultCertificates(); | ||||
|     const sslKey = sslKeyPair?.key || defaultCerts.key; | ||||
|     const sslCert = sslKeyPair?.cert || defaultCerts.cert; | ||||
|  | ||||
|     for (const domainConfig of domainConfigs) { | ||||
|       // Each domain in the domains array gets its own config | ||||
|       for (const domain of domainConfig.domains) { | ||||
|         // Skip non-hostname patterns (like IP addresses) | ||||
|         if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         proxyConfigs.push({ | ||||
|           hostName: domain, | ||||
|           destinationIps: domainConfig.targetIPs || ['localhost'], | ||||
|           destinationPorts: [this.options.port], // Use the NetworkProxy port | ||||
|           privateKey: sslKey, | ||||
|           publicKey: sslCert | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this.logger.info(`Converted ${domainConfigs.length} SmartProxy configs to ${proxyConfigs.length} NetworkProxy configs`); | ||||
|     return proxyConfigs; | ||||
|   } | ||||
|   // Legacy methods have been removed. | ||||
|   // Please use updateRouteConfigs() directly with modern route-based configuration. | ||||
|  | ||||
|   /** | ||||
|    * Adds default headers to be included in all responses | ||||
| @@ -650,62 +590,4 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   public getRouteConfigs(): IRouteConfig[] { | ||||
|     return this.routeManager.getRoutes(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @deprecated Use getRouteConfigs instead | ||||
|    * Gets all proxy configurations currently in use in the legacy format | ||||
|    * This method is maintained for backward compatibility | ||||
|    */ | ||||
|   public getProxyConfigs(): IReverseProxyConfig[] { | ||||
|     this.logger.warn('getProxyConfigs is deprecated - use getRouteConfigs instead'); | ||||
|  | ||||
|     // Create legacy proxy configs from our route configurations | ||||
|     const legacyConfigs: IReverseProxyConfig[] = []; | ||||
|     const currentRoutes = this.routeManager.getRoutes(); | ||||
|  | ||||
|     for (const route of currentRoutes) { | ||||
|       // Skip non-forward routes or routes without domains | ||||
|       if (route.action.type !== 'forward' || !route.match.domains || !route.action.target) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       // Skip routes with function-based targets | ||||
|       if (typeof route.action.target.host === 'function' || typeof route.action.target.port === 'function') { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       // Get domains | ||||
|       const domains = Array.isArray(route.match.domains) | ||||
|         ? route.match.domains.filter(d => !d.includes('*')) | ||||
|         : route.match.domains.includes('*') ? [] : [route.match.domains]; | ||||
|  | ||||
|       // Get certificate | ||||
|       let privateKey = ''; | ||||
|       let publicKey = ''; | ||||
|  | ||||
|       if (route.action.tls?.certificate && route.action.tls.certificate !== 'auto') { | ||||
|         privateKey = route.action.tls.certificate.key; | ||||
|         publicKey = route.action.tls.certificate.cert; | ||||
|       } else { | ||||
|         const defaultCerts = this.certificateManager.getDefaultCertificates(); | ||||
|         privateKey = defaultCerts.key; | ||||
|         publicKey = defaultCerts.cert; | ||||
|       } | ||||
|  | ||||
|       // Create legacy config for each domain | ||||
|       for (const domain of domains) { | ||||
|         legacyConfigs.push({ | ||||
|           hostName: domain, | ||||
|           destinationIps: Array.isArray(route.action.target.host) | ||||
|             ? route.action.target.host | ||||
|             : [route.action.target.host], | ||||
|           destinationPorts: [route.action.target.port], | ||||
|           privateKey, | ||||
|           publicKey | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return legacyConfigs; | ||||
|   } | ||||
| } | ||||
| @@ -661,150 +661,6 @@ export class RequestHandler { | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Find target based on hostname | ||||
|       const proxyConfig = this.router.routeReq(req); | ||||
|        | ||||
|       if (!proxyConfig) { | ||||
|         // No matching proxy configuration | ||||
|         this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); | ||||
|         res.statusCode = 404; | ||||
|         res.end('Not Found: No proxy configuration for this host'); | ||||
|          | ||||
|         // Increment failed requests counter | ||||
|         if (this.metricsTracker) { | ||||
|           this.metricsTracker.incrementFailedRequests(); | ||||
|         } | ||||
|          | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       // Get destination IP using round-robin if multiple IPs configured | ||||
|       const destination = this.connectionPool.getNextTarget( | ||||
|         proxyConfig.destinationIps,  | ||||
|         proxyConfig.destinationPorts[0] | ||||
|       ); | ||||
|        | ||||
|       // Create options for the proxy request | ||||
|       const options: plugins.http.RequestOptions = { | ||||
|         hostname: destination.host, | ||||
|         port: destination.port, | ||||
|         path: req.url, | ||||
|         method: req.method, | ||||
|         headers: { ...req.headers } | ||||
|       }; | ||||
|        | ||||
|       // Remove host header to avoid issues with virtual hosts on target server | ||||
|       // The host header should match the target server's expected hostname | ||||
|       if (options.headers && options.headers.host) { | ||||
|         if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) { | ||||
|           options.headers.host = `${destination.host}:${destination.port}`; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       this.logger.debug( | ||||
|         `Proxying request to ${destination.host}:${destination.port}${req.url}`, | ||||
|         { method: req.method } | ||||
|       ); | ||||
|        | ||||
|       // Create proxy request | ||||
|       const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|         // Copy status code | ||||
|         res.statusCode = proxyRes.statusCode || 500; | ||||
|          | ||||
|         // Copy headers from proxy response to client response | ||||
|         for (const [key, value] of Object.entries(proxyRes.headers)) { | ||||
|           if (value !== undefined) { | ||||
|             res.setHeader(key, value); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Pipe proxy response to client response | ||||
|         proxyRes.pipe(res); | ||||
|          | ||||
|         // Increment served requests counter when the response finishes | ||||
|         res.on('finish', () => { | ||||
|           if (this.metricsTracker) { | ||||
|             this.metricsTracker.incrementRequestsServed(); | ||||
|           } | ||||
|            | ||||
|           // Log the completed request | ||||
|           const duration = Date.now() - startTime; | ||||
|           this.logger.debug( | ||||
|             `Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`, | ||||
|             { duration, statusCode: res.statusCode } | ||||
|           ); | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       // Handle proxy request errors | ||||
|       proxyReq.on('error', (error) => { | ||||
|         const duration = Date.now() - startTime; | ||||
|         this.logger.error( | ||||
|           `Proxy error for ${req.method} ${req.url}: ${error.message}`, | ||||
|           { duration, error: error.message } | ||||
|         ); | ||||
|          | ||||
|         // Increment failed requests counter | ||||
|         if (this.metricsTracker) { | ||||
|           this.metricsTracker.incrementFailedRequests(); | ||||
|         } | ||||
|          | ||||
|         // Check if headers have already been sent | ||||
|         if (!res.headersSent) { | ||||
|           res.statusCode = 502; | ||||
|           res.end(`Bad Gateway: ${error.message}`); | ||||
|         } else { | ||||
|           // If headers already sent, just close the connection | ||||
|           res.end(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Pipe request body to proxy request and handle client-side errors | ||||
|       req.pipe(proxyReq); | ||||
|        | ||||
|       // Handle client disconnection | ||||
|       req.on('error', (error) => { | ||||
|         this.logger.debug(`Client connection error: ${error.message}`); | ||||
|         proxyReq.destroy(); | ||||
|          | ||||
|         // Increment failed requests counter on client errors | ||||
|         if (this.metricsTracker) { | ||||
|           this.metricsTracker.incrementFailedRequests(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|       // Handle response errors | ||||
|       res.on('error', (error) => { | ||||
|         this.logger.debug(`Response error: ${error.message}`); | ||||
|         proxyReq.destroy(); | ||||
|          | ||||
|         // Increment failed requests counter on response errors | ||||
|         if (this.metricsTracker) { | ||||
|           this.metricsTracker.incrementFailedRequests(); | ||||
|         } | ||||
|       }); | ||||
|        | ||||
|     } catch (error) { | ||||
|       // Handle any unexpected errors | ||||
|       this.logger.error( | ||||
|         `Unexpected error handling request: ${error.message}`, | ||||
|         { error: error.stack } | ||||
|       ); | ||||
|        | ||||
|       // Increment failed requests counter | ||||
|       if (this.metricsTracker) { | ||||
|         this.metricsTracker.incrementFailedRequests(); | ||||
|       } | ||||
|        | ||||
|       if (!res.headersSent) { | ||||
|         res.statusCode = 500; | ||||
|         res.end('Internal Server Error'); | ||||
|       } else { | ||||
|         res.end(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -56,7 +56,7 @@ export interface IRouteContext { | ||||
|   routeId?: string;      // The ID of the matched route | ||||
|  | ||||
|   // Target information (resolved from dynamic mapping) | ||||
|   targetHost?: string;   // The resolved target host | ||||
|   targetHost?: string | string[];   // The resolved target host(s) | ||||
|   targetPort?: number;   // The resolved target port | ||||
|  | ||||
|   // Additional properties | ||||
| @@ -68,8 +68,8 @@ export interface IRouteContext { | ||||
|  * Target configuration for forwarding | ||||
|  */ | ||||
| export interface IRouteTarget { | ||||
|   host: string | string[] | ((context: any) => string | string[]);  // Support static or dynamic host selection with any compatible context | ||||
|   port: number | ((context: any) => number);  // Support static or dynamic port mapping with any compatible context | ||||
|   host: string | string[] | ((context: IRouteContext) => string | string[]);  // Host or hosts with optional function for dynamic resolution | ||||
|   port: number | ((context: IRouteContext) => number);  // Port with optional function for dynamic mapping | ||||
|   preservePort?: boolean;   // Use incoming port as target port (ignored if port is a function) | ||||
| } | ||||
|  | ||||
| @@ -108,7 +108,8 @@ export interface IRouteAuthentication { | ||||
|   oauthClientId?: string; | ||||
|   oauthClientSecret?: string; | ||||
|   oauthRedirectUri?: string; | ||||
|   [key: string]: any;  // Allow additional auth-specific options | ||||
|   // Specific options for different auth types | ||||
|   options?: Record<string, unknown>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,498 +0,0 @@ | ||||
| import type { | ||||
|   IRouteConfig, | ||||
|   IRouteMatch, | ||||
|   IRouteAction, | ||||
|   IRouteTarget, | ||||
|   IRouteTls, | ||||
|   IRouteRedirect, | ||||
|   IRouteSecurity, | ||||
|   IRouteAdvanced, | ||||
|   TPortRange | ||||
| } from './models/route-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Basic helper function to create a route configuration | ||||
|  */ | ||||
| export function createRoute( | ||||
|   match: IRouteMatch, | ||||
|   action: IRouteAction, | ||||
|   metadata?: { | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return { | ||||
|     match, | ||||
|     action, | ||||
|     ...metadata | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a basic HTTP route configuration | ||||
|  */ | ||||
| export function createHttpRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 80 | ||||
|     domains?: string | string[]; | ||||
|     path?: string; | ||||
|     target: IRouteTarget; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 80, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: options.target, | ||||
|       ...(options.headers || options.security ? { | ||||
|         advanced: { | ||||
|           ...(options.headers ? { headers: options.headers } : {}) | ||||
|         }, | ||||
|         ...(options.security ? { security: options.security } : {}) | ||||
|       } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'HTTP Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTPS route configuration with TLS termination | ||||
|  */ | ||||
| export function createHttpsRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 443 | ||||
|     domains: string | string[]; | ||||
|     path?: string; | ||||
|     target: IRouteTarget; | ||||
|     tlsMode?: 'terminate' | 'terminate-and-reencrypt'; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 443, | ||||
|       domains: options.domains, | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: options.target, | ||||
|       tls: { | ||||
|         mode: options.tlsMode || 'terminate', | ||||
|         certificate: options.certificate || 'auto' | ||||
|       }, | ||||
|       ...(options.headers || options.security ? { | ||||
|         advanced: { | ||||
|           ...(options.headers ? { headers: options.headers } : {}) | ||||
|         }, | ||||
|         ...(options.security ? { security: options.security } : {}) | ||||
|       } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'HTTPS Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTPS passthrough route configuration | ||||
|  */ | ||||
| export function createPassthroughRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 443 | ||||
|     domains?: string | string[]; | ||||
|     target: IRouteTarget; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 443, | ||||
|       ...(options.domains ? { domains: options.domains } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: options.target, | ||||
|       tls: { | ||||
|         mode: 'passthrough' | ||||
|       }, | ||||
|       ...(options.security ? { security: options.security } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'HTTPS Passthrough Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a redirect route configuration | ||||
|  */ | ||||
| export function createRedirectRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 80 | ||||
|     domains?: string | string[]; | ||||
|     path?: string; | ||||
|     redirectTo: string; | ||||
|     statusCode?: 301 | 302 | 307 | 308; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 80, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'redirect', | ||||
|       redirect: { | ||||
|         to: options.redirectTo, | ||||
|         status: options.statusCode || 301 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Redirect Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create an HTTP to HTTPS redirect route configuration | ||||
|  */ | ||||
| export function createHttpToHttpsRedirect( | ||||
|   options: { | ||||
|     domains: string | string[]; | ||||
|     statusCode?: 301 | 302 | 307 | 308; | ||||
|     name?: string; | ||||
|     priority?: number; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains]; | ||||
|  | ||||
|   return createRedirectRoute({ | ||||
|     ports: 80, | ||||
|     domains: options.domains, | ||||
|     redirectTo: 'https://{domain}{path}', | ||||
|     statusCode: options.statusCode || 301, | ||||
|     name: options.name || `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`, | ||||
|     priority: options.priority || 100 // High priority for redirects | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a block route configuration | ||||
|  */ | ||||
| export function createBlockRoute( | ||||
|   options: { | ||||
|     ports: number | number[]; | ||||
|     domains?: string | string[]; | ||||
|     clientIp?: string[]; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.clientIp ? { clientIp: options.clientIp } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'block' | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Block Route', | ||||
|       description: options.description, | ||||
|       priority: options.priority || 1000, // Very high priority for blocks | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a load balancer route configuration | ||||
|  */ | ||||
| export function createLoadBalancerRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 443 | ||||
|     domains: string | string[]; | ||||
|     path?: string; | ||||
|     targets: string[]; // Array of host names/IPs for load balancing | ||||
|     targetPort: number; | ||||
|     tlsMode?: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   const useTls = options.tlsMode !== undefined; | ||||
|   const defaultPort = useTls ? 443 : 80; | ||||
|  | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || defaultPort, | ||||
|       domains: options.domains, | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: options.targets, | ||||
|         port: options.targetPort | ||||
|       }, | ||||
|       ...(useTls ? { | ||||
|         tls: { | ||||
|           mode: options.tlsMode!, | ||||
|           ...(options.tlsMode !== 'passthrough' && options.certificate ? { | ||||
|             certificate: options.certificate | ||||
|           } : {}) | ||||
|         } | ||||
|       } : {}), | ||||
|       ...(options.headers || options.security ? { | ||||
|         advanced: { | ||||
|           ...(options.headers ? { headers: options.headers } : {}) | ||||
|         }, | ||||
|         ...(options.security ? { security: options.security } : {}) | ||||
|       } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Load Balanced Route', | ||||
|       description: options.description || `Load balancing across ${options.targets.length} backends`, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a complete HTTPS server configuration with HTTP redirect | ||||
|  */ | ||||
| export function createHttpsServer( | ||||
|   options: { | ||||
|     domains: string | string[]; | ||||
|     target: IRouteTarget; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     security?: IRouteSecurity; | ||||
|     addHttpRedirect?: boolean; | ||||
|     name?: string; | ||||
|   } | ||||
| ): IRouteConfig[] { | ||||
|   const routes: IRouteConfig[] = []; | ||||
|   const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains]; | ||||
|  | ||||
|   // Add HTTPS route | ||||
|   routes.push(createHttpsRoute({ | ||||
|     domains: options.domains, | ||||
|     target: options.target, | ||||
|     certificate: options.certificate || 'auto', | ||||
|     security: options.security, | ||||
|     name: options.name || `HTTPS Server for ${domainArray.join(', ')}` | ||||
|   })); | ||||
|  | ||||
|   // Add HTTP to HTTPS redirect if requested | ||||
|   if (options.addHttpRedirect !== false) { | ||||
|     routes.push(createHttpToHttpsRedirect({ | ||||
|       domains: options.domains, | ||||
|       name: `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`, | ||||
|       priority: 100 | ||||
|     })); | ||||
|   } | ||||
|  | ||||
|   return routes; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a port range configuration from various input formats | ||||
|  */ | ||||
| export function createPortRange( | ||||
|   ports: number | number[] | string | Array<{ from: number; to: number }> | ||||
| ): TPortRange { | ||||
|   // If it's a string like "80,443" or "8000-9000", parse it | ||||
|   if (typeof ports === 'string') { | ||||
|     if (ports.includes('-')) { | ||||
|       // Handle range like "8000-9000" | ||||
|       const [start, end] = ports.split('-').map(p => parseInt(p.trim(), 10)); | ||||
|       return [{ from: start, to: end }]; | ||||
|     } else if (ports.includes(',')) { | ||||
|       // Handle comma-separated list like "80,443,8080" | ||||
|       return ports.split(',').map(p => parseInt(p.trim(), 10)); | ||||
|     } else { | ||||
|       // Handle single port as string | ||||
|       return parseInt(ports.trim(), 10); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Otherwise return as is | ||||
|   return ports; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a security configuration object | ||||
|  */ | ||||
| export function createSecurityConfig( | ||||
|   options: { | ||||
|     allowedIps?: string[]; | ||||
|     blockedIps?: string[]; | ||||
|     maxConnections?: number; | ||||
|     authentication?: { | ||||
|       type: 'basic' | 'digest' | 'oauth'; | ||||
|       // Auth-specific options | ||||
|       [key: string]: any; | ||||
|     }; | ||||
|   } | ||||
| ): IRouteSecurity { | ||||
|   return { | ||||
|     ...(options.allowedIps ? { allowedIps: options.allowedIps } : {}), | ||||
|     ...(options.blockedIps ? { blockedIps: options.blockedIps } : {}), | ||||
|     ...(options.maxConnections ? { maxConnections: options.maxConnections } : {}), | ||||
|     ...(options.authentication ? { authentication: options.authentication } : {}) | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a static file server route | ||||
|  */ | ||||
| export function createStaticFileRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 80 | ||||
|     domains: string | string[]; | ||||
|     path?: string; | ||||
|     targetDirectory: string; | ||||
|     tlsMode?: 'terminate' | 'terminate-and-reencrypt'; | ||||
|     certificate?: 'auto' | { key: string; cert: string }; | ||||
|     headers?: Record<string, string>; | ||||
|     security?: IRouteSecurity; | ||||
|     name?: string; | ||||
|     description?: string; | ||||
|     priority?: number; | ||||
|     tags?: string[]; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   const useTls = options.tlsMode !== undefined; | ||||
|   const defaultPort = useTls ? 443 : 80; | ||||
|  | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || defaultPort, | ||||
|       domains: options.domains, | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: 'localhost', // Static file serving is typically handled locally | ||||
|         port: 0, // Special value indicating a static file server | ||||
|         preservePort: false | ||||
|       }, | ||||
|       ...(useTls ? { | ||||
|         tls: { | ||||
|           mode: options.tlsMode!, | ||||
|           certificate: options.certificate || 'auto' | ||||
|         } | ||||
|       } : {}), | ||||
|       advanced: { | ||||
|         ...(options.headers ? { headers: options.headers } : {}), | ||||
|         staticFiles: { | ||||
|           root: options.targetDirectory, | ||||
|           index: ['index.html', 'index.htm'], | ||||
|           directory: options.targetDirectory // For backward compatibility | ||||
|         } | ||||
|       }, | ||||
|       ...(options.security ? { security: options.security } : {}) | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Static File Server', | ||||
|       description: options.description || `Serving static files from ${options.targetDirectory}`, | ||||
|       priority: options.priority, | ||||
|       tags: options.tags | ||||
|     } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Create a test route for debugging purposes | ||||
|  */ | ||||
| export function createTestRoute( | ||||
|   options: { | ||||
|     ports?: number | number[]; // Default: 8000 | ||||
|     domains?: string | string[]; | ||||
|     path?: string; | ||||
|     response?: { | ||||
|       status?: number; | ||||
|       headers?: Record<string, string>; | ||||
|       body?: string; | ||||
|     }; | ||||
|     name?: string; | ||||
|   } | ||||
| ): IRouteConfig { | ||||
|   return createRoute( | ||||
|     { | ||||
|       ports: options.ports || 8000, | ||||
|       ...(options.domains ? { domains: options.domains } : {}), | ||||
|       ...(options.path ? { path: options.path } : {}) | ||||
|     }, | ||||
|     { | ||||
|       type: 'forward', | ||||
|       target: { | ||||
|         host: 'test',  // Special value indicating a test route | ||||
|         port: 0 | ||||
|       }, | ||||
|       advanced: { | ||||
|         testResponse: { | ||||
|           status: options.response?.status || 200, | ||||
|           headers: options.response?.headers || { 'Content-Type': 'text/plain' }, | ||||
|           body: options.response?.body || 'Test route is working!' | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: options.name || 'Test Route', | ||||
|       description: 'Route for testing and debugging', | ||||
|       priority: 500, | ||||
|       tags: ['test', 'debug'] | ||||
|     } | ||||
|   ); | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| /** | ||||
|  * Route helpers for SmartProxy | ||||
|  *  | ||||
|  * This module provides helper functions for creating various types of route configurations | ||||
|  * to be used with the SmartProxy system. | ||||
|  */ | ||||
|  | ||||
| // Re-export all functions from the route-helpers.ts file | ||||
| export * from '../route-helpers.js'; | ||||
| @@ -244,21 +244,36 @@ export class RouteManager extends plugins.EventEmitter { | ||||
|    * Match an IP against a pattern | ||||
|    */ | ||||
|   private matchIpPattern(pattern: string, ip: string): boolean { | ||||
|     // Handle exact match | ||||
|     if (pattern === ip) { | ||||
|     // Normalize IPv6-mapped IPv4 addresses | ||||
|     const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||
|     const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern; | ||||
|      | ||||
|     // Handle exact match with normalized addresses | ||||
|     if (pattern === ip || normalizedPattern === normalizedIp ||  | ||||
|         pattern === normalizedIp || normalizedPattern === ip) { | ||||
|       return true; | ||||
|     } | ||||
|      | ||||
|     // Handle CIDR notation (e.g., 192.168.1.0/24) | ||||
|     if (pattern.includes('/')) { | ||||
|       return this.matchIpCidr(pattern, ip); | ||||
|       return this.matchIpCidr(pattern, normalizedIp) ||  | ||||
|              (normalizedPattern !== pattern && this.matchIpCidr(normalizedPattern, normalizedIp)); | ||||
|     } | ||||
|      | ||||
|     // Handle glob pattern (e.g., 192.168.1.*) | ||||
|     if (pattern.includes('*')) { | ||||
|       const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||
|       const regex = new RegExp(`^${regexPattern}$`); | ||||
|       return regex.test(ip); | ||||
|       if (regex.test(ip) || regex.test(normalizedIp)) { | ||||
|         return true; | ||||
|       } | ||||
|        | ||||
|       // If pattern was normalized, also test with normalized pattern | ||||
|       if (normalizedPattern !== pattern) { | ||||
|         const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||
|         const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`); | ||||
|         return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return false; | ||||
| @@ -274,9 +289,13 @@ export class RouteManager extends plugins.EventEmitter { | ||||
|       const [subnet, bits] = cidr.split('/'); | ||||
|       const mask = parseInt(bits, 10); | ||||
|        | ||||
|       // Normalize IPv6-mapped IPv4 addresses | ||||
|       const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||
|       const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet; | ||||
|        | ||||
|       // Convert IP addresses to numeric values | ||||
|       const ipNum = this.ipToNumber(ip); | ||||
|       const subnetNum = this.ipToNumber(subnet); | ||||
|       const ipNum = this.ipToNumber(normalizedIp); | ||||
|       const subnetNum = this.ipToNumber(normalizedSubnet); | ||||
|        | ||||
|       // Calculate subnet mask | ||||
|       const maskNum = ~(2 ** (32 - mask) - 1); | ||||
| @@ -293,7 +312,10 @@ export class RouteManager extends plugins.EventEmitter { | ||||
|    * Convert an IP address to a numeric value | ||||
|    */ | ||||
|   private ipToNumber(ip: string): number { | ||||
|     const parts = ip.split('.').map(part => parseInt(part, 10)); | ||||
|     // Normalize IPv6-mapped IPv4 addresses | ||||
|     const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||
|      | ||||
|     const parts = normalizedIp.split('.').map(part => parseInt(part, 10)); | ||||
|     return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; | ||||
|   } | ||||
|    | ||||
|   | ||||
		Reference in New Issue
	
	Block a user