change to route based approach
This commit is contained in:
		| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|   "name": "@push.rocks/smartproxy", |   "name": "@push.rocks/smartproxy", | ||||||
|   "version": "13.1.3", |   "version": "14.0.0", | ||||||
|   "private": false, |   "private": false, | ||||||
|   "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.", |   "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.", | ||||||
|   "main": "dist_ts/index.js", |   "main": "dist_ts/index.js", | ||||||
|   "typings": "dist_ts/index.d.ts", |   "typings": "dist_ts/index.d.ts", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   | |||||||
							
								
								
									
										665
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										665
									
								
								readme.plan.md
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| # SmartProxy Fully Unified Configuration Plan | # SmartProxy Fully Unified Configuration Plan (Updated) | ||||||
|  |  | ||||||
| ## Project Goal | ## Project Goal | ||||||
| Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by: | Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by: | ||||||
| @@ -6,6 +6,7 @@ Redesign SmartProxy's configuration for a more elegant, unified, and comprehensi | |||||||
| 2. Eliminating the confusion between domain configs and port forwarding | 2. Eliminating the confusion between domain configs and port forwarding | ||||||
| 3. Providing a clear, declarative API that makes the intent obvious | 3. Providing a clear, declarative API that makes the intent obvious | ||||||
| 4. Enhancing maintainability and extensibility for future features | 4. Enhancing maintainability and extensibility for future features | ||||||
|  | 5. Completely removing legacy code to eliminate technical debt | ||||||
|  |  | ||||||
| ## Current Issues | ## Current Issues | ||||||
|  |  | ||||||
| @@ -132,177 +133,14 @@ interface ISmartProxyOptions { | |||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Example Configuration | ## Revised Implementation Plan | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| const proxy = new SmartProxy({ |  | ||||||
|   // All routing is configured in a single array |  | ||||||
|   routes: [ |  | ||||||
|     // Basic HTTPS server with automatic certificates |  | ||||||
|     { |  | ||||||
|       match: { |  | ||||||
|         ports: 443, |  | ||||||
|         domains: ['example.com', '*.example.com'] |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { |  | ||||||
|           host: 'localhost', |  | ||||||
|           port: 8080 |  | ||||||
|         }, |  | ||||||
|         tls: { |  | ||||||
|           mode: 'terminate', |  | ||||||
|           certificate: 'auto' // Use ACME |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       name: 'Main HTTPS Server' |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // HTTP to HTTPS redirect |  | ||||||
|     { |  | ||||||
|       match: { |  | ||||||
|         ports: 80, |  | ||||||
|         domains: ['example.com', '*.example.com'] |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'redirect', |  | ||||||
|         redirect: { |  | ||||||
|           to: 'https://{domain}{path}', |  | ||||||
|           status: 301 |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       name: 'HTTP to HTTPS Redirect' |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // Admin portal with IP restriction |  | ||||||
|     { |  | ||||||
|       match: { |  | ||||||
|         ports: 8443, |  | ||||||
|         domains: 'admin.example.com' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { |  | ||||||
|           host: 'admin-backend', |  | ||||||
|           port: 3000 |  | ||||||
|         }, |  | ||||||
|         tls: { |  | ||||||
|           mode: 'terminate', |  | ||||||
|           certificate: 'auto' |  | ||||||
|         }, |  | ||||||
|         security: { |  | ||||||
|           allowedIps: ['192.168.1.*', '10.0.0.*'], |  | ||||||
|           maxConnections: 10 |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       priority: 100, // Higher priority than default rules |  | ||||||
|       name: 'Admin Portal' |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // Port range for direct forwarding |  | ||||||
|     { |  | ||||||
|       match: { |  | ||||||
|         ports: [{ from: 10000, to: 10010 }], |  | ||||||
|         // No domains = all domains or direct IP |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { |  | ||||||
|           host: 'backend-server', |  | ||||||
|           port: 10000, |  | ||||||
|           preservePort: true // Use same port number as incoming |  | ||||||
|         }, |  | ||||||
|         tls: { |  | ||||||
|           mode: 'passthrough' // Direct TCP forwarding |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       name: 'Dynamic Port Range' |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // Path-based routing |  | ||||||
|     { |  | ||||||
|       match: { |  | ||||||
|         ports: 443, |  | ||||||
|         domains: 'api.example.com', |  | ||||||
|         path: '/v1/*' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { |  | ||||||
|           host: 'api-v1-service', |  | ||||||
|           port: 8001 |  | ||||||
|         }, |  | ||||||
|         tls: { |  | ||||||
|           mode: 'terminate' |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       name: 'API v1 Endpoints' |  | ||||||
|     }, |  | ||||||
|      |  | ||||||
|     // Load balanced backend |  | ||||||
|     { |  | ||||||
|       match: { |  | ||||||
|         ports: 443, |  | ||||||
|         domains: 'app.example.com' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { |  | ||||||
|           // Round-robin load balancing |  | ||||||
|           host: [ |  | ||||||
|             'app-server-1', |  | ||||||
|             'app-server-2', |  | ||||||
|             'app-server-3' |  | ||||||
|           ], |  | ||||||
|           port: 8080 |  | ||||||
|         }, |  | ||||||
|         tls: { |  | ||||||
|           mode: 'terminate' |  | ||||||
|         }, |  | ||||||
|         advanced: { |  | ||||||
|           headers: { |  | ||||||
|             'X-Served-By': '{server}', |  | ||||||
|             'X-Client-IP': '{clientIp}' |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       name: 'Load Balanced App Servers' |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|    |  | ||||||
|   // Global defaults |  | ||||||
|   defaults: { |  | ||||||
|     target: { |  | ||||||
|       host: 'localhost', |  | ||||||
|       port: 8080 |  | ||||||
|     }, |  | ||||||
|     security: { |  | ||||||
|       maxConnections: 1000, |  | ||||||
|       // Global security defaults |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|    |  | ||||||
|   // ACME configuration for auto certificates |  | ||||||
|   acme: { |  | ||||||
|     enabled: true, |  | ||||||
|     email: 'admin@example.com', |  | ||||||
|     production: true, |  | ||||||
|     renewThresholdDays: 30 |  | ||||||
|   }, |  | ||||||
|    |  | ||||||
|   // Other global settings |  | ||||||
|   // ... |  | ||||||
| }); |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Implementation Plan |  | ||||||
|  |  | ||||||
| ### Phase 1: Core Design & Interface Definition | ### Phase 1: Core Design & Interface Definition | ||||||
|  |  | ||||||
| 1. **Define New Core Interfaces**: | 1. **Define New Core Interfaces**: | ||||||
|    - Create `IRouteConfig` interface with `match` and `action` branches |    - Create `IRouteConfig` interface with `match` and `action` branches | ||||||
|    - Define all sub-interfaces for matching and actions |    - Define all sub-interfaces for matching and actions | ||||||
|    - Update `ISmartProxyOptions` to use `routes` array |    - Create new `ISmartProxyOptions` to use `routes` array exclusively | ||||||
|    - Define template variable system for dynamic values |    - Define template variable system for dynamic values | ||||||
|  |  | ||||||
| 2. **Create Helper Functions**: | 2. **Create Helper Functions**: | ||||||
| @@ -319,78 +157,56 @@ const proxy = new SmartProxy({ | |||||||
| ### Phase 2: Core Implementation | ### Phase 2: Core Implementation | ||||||
|  |  | ||||||
| 1. **Create RouteManager**: | 1. **Create RouteManager**: | ||||||
|    - Replaces both PortRangeManager and DomainConfigManager |    - Build a new RouteManager to replace both PortRangeManager and DomainConfigManager | ||||||
|    - Handles all routing decisions in one place |    - Implement port and domain matching in one unified system | ||||||
|    - Provides fast lookup from port+domain+path to action |    - Create efficient route lookup algorithms | ||||||
|    - Manages server instances for different ports |  | ||||||
|  |  | ||||||
| 2. **Update ConnectionHandler**: | 2. **Implement New ConnectionHandler**: | ||||||
|    - Simplify to work with the unified route system |    - Create a new ConnectionHandler built from scratch for routes | ||||||
|    - Implement templating system for dynamic values |    - Implement the routing logic with the new match/action pattern | ||||||
|    - Support more sophisticated routing rules |    - Support template processing for headers and other dynamic values | ||||||
|  |  | ||||||
| 3. **Implement New SmartProxy Core**: | 3. **Implement New SmartProxy Core**: | ||||||
|    - Rewrite initialization to use route-based configuration |    - Create new SmartProxy implementation using routes exclusively | ||||||
|    - Create network servers based on port specifications |    - Build network servers based on port specifications | ||||||
|    - Manage TLS contexts and certificates |    - Manage TLS contexts and certificates | ||||||
|  |  | ||||||
| ### Phase 3: Feature Implementation | ### Phase 3: Legacy Code Removal | ||||||
|  |  | ||||||
| 1. **Path-Based Routing**: | 1. **Identify Legacy Components**: | ||||||
|    - Implement HTTP path matching |    - Create an inventory of all files and components to be removed | ||||||
|    - Add wildcard and regex support for paths |    - Document dependencies between legacy components | ||||||
|    - Create route differentiation based on HTTP method |    - Create a removal plan that minimizes disruption | ||||||
|  |  | ||||||
| 2. **Enhanced Security**: | 2. **Remove Legacy Components**: | ||||||
|    - Implement per-route security rules |    - Remove PortRangeManager and related code | ||||||
|    - Add authentication methods (basic, digest, etc.) |    - Remove DomainConfigManager and related code | ||||||
|    - Support for IP-based access control |    - Remove old ConnectionHandler implementation | ||||||
|  |    - Remove other legacy components | ||||||
|  |  | ||||||
| 3. **TLS Management**: | 3. **Clean Interface Adaptations**: | ||||||
|    - Support multiple certificate types (auto, manual, wildcard) |    - Remove all legacy interfaces and types | ||||||
|    - Implement certificate selection based on SNI |    - Update type exports to only expose route-based interfaces | ||||||
|    - Support different TLS modes per route |    - Remove any adapter or backward compatibility code | ||||||
|  |  | ||||||
| 4. **Metrics & Monitoring**: | ### Phase 4: Updated Documentation & Examples | ||||||
|    - Per-route statistics |  | ||||||
|    - Named route tracking for better visibility |  | ||||||
|    - Tag-based grouping of metrics |  | ||||||
|  |  | ||||||
| ### Phase 4: Backward Compatibility |  | ||||||
|  |  | ||||||
| 1. **Legacy Adapter Layer**: |  | ||||||
|    - Convert old configuration format to route-based format |  | ||||||
|    - Map fromPort/toPort/domainConfigs to unified routes |  | ||||||
|    - Preserve all functionality during migration |  | ||||||
|  |  | ||||||
| 2. **API Compatibility**: |  | ||||||
|    - Support both old and new configuration methods |  | ||||||
|    - Add deprecation warnings when using legacy properties |  | ||||||
|    - Provide migration utilities |  | ||||||
|  |  | ||||||
| 3. **Documentation**: |  | ||||||
|    - Clear migration guide for existing users |  | ||||||
|    - Examples mapping old config to new config |  | ||||||
|    - Timeline for deprecation |  | ||||||
|  |  | ||||||
| ### Phase 5: Documentation & Examples |  | ||||||
|  |  | ||||||
| 1. **Update Core Documentation**: | 1. **Update Core Documentation**: | ||||||
|    - Rewrite README.md with a focus on route-based configuration |    - Rewrite README.md with a focus on route-based configuration exclusively | ||||||
|    - Create interface reference documentation |    - Create interface reference documentation | ||||||
|    - Document all template variables |    - Document all template variables | ||||||
|  |  | ||||||
| 2. **Create Example Library**: | 2. **Create Example Library**: | ||||||
|    - Common configuration patterns |    - Common configuration patterns using the new API | ||||||
|    - Complex use cases for advanced features |    - Complex use cases for advanced features | ||||||
|    - Infrastructure-as-code examples |    - Infrastructure-as-code examples | ||||||
|  |  | ||||||
| 3. **Add Validation Tooling**: | 3. **Add Validation Tools**: | ||||||
|    - Configuration validator to check for issues |    - Configuration validator to check for issues | ||||||
|    - Schema definitions for IDE autocomplete |    - Schema definitions for IDE autocomplete | ||||||
|    - Runtime validation helpers |    - Runtime validation helpers | ||||||
|  |  | ||||||
| ### Phase 6: Testing | ### Phase 5: Testing | ||||||
|  |  | ||||||
| 1. **Unit Tests**: | 1. **Unit Tests**: | ||||||
|    - Test route matching logic |    - Test route matching logic | ||||||
| @@ -398,378 +214,103 @@ const proxy = new SmartProxy({ | |||||||
|    - Test template processing |    - Test template processing | ||||||
|  |  | ||||||
| 2. **Integration Tests**: | 2. **Integration Tests**: | ||||||
|    - Verify full proxy flows |    - Verify full proxy flows with the new system | ||||||
|    - Test complex routing scenarios |    - Test complex routing scenarios | ||||||
|    - Check backward compatibility |    - Ensure all features work as expected | ||||||
|  |  | ||||||
| 3. **Performance Testing**: | 3. **Performance Testing**: | ||||||
|    - Benchmark routing performance |    - Benchmark routing performance | ||||||
|    - Evaluate memory usage |    - Evaluate memory usage | ||||||
|    - Test with large numbers of routes |    - Test with large numbers of routes | ||||||
|  |  | ||||||
| ## Benefits of the New Approach |  | ||||||
|  |  | ||||||
| 1. **Truly Unified Configuration**: |  | ||||||
|    - One "source of truth" for all routing |  | ||||||
|    - Entire routing flow visible in a single configuration |  | ||||||
|    - No overlapping or conflicting configuration systems |  | ||||||
|  |  | ||||||
| 2. **Declarative Intent**: |  | ||||||
|    - Configuration clearly states what to match and what action to take |  | ||||||
|    - Metadata provides context and documentation inline |  | ||||||
|    - Easy to understand the purpose of each route |  | ||||||
|  |  | ||||||
| 3. **Advanced Routing Capabilities**: |  | ||||||
|    - Path-based routing with pattern matching |  | ||||||
|    - Client IP-based conditional routing |  | ||||||
|    - Fine-grained security controls |  | ||||||
|  |  | ||||||
| 4. **Composable and Extensible**: |  | ||||||
|    - Each route is a self-contained unit of configuration |  | ||||||
|    - Routes can be grouped by tags or priority |  | ||||||
|    - New match criteria or actions can be added without breaking changes |  | ||||||
|  |  | ||||||
| 5. **Better Developer Experience**: |  | ||||||
|    - Clear, consistent configuration pattern |  | ||||||
|    - Helper functions for common scenarios |  | ||||||
|    - Better error messages and validation |  | ||||||
|  |  | ||||||
| ## Example Use Cases |  | ||||||
|  |  | ||||||
| ### 1. Complete Reverse Proxy with Auto SSL |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| const proxy = new SmartProxy({ |  | ||||||
|   routes: [ |  | ||||||
|     // HTTPS server for all domains |  | ||||||
|     { |  | ||||||
|       match: { ports: 443 }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { host: 'localhost', port: 8080 }, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     // HTTP to HTTPS redirect |  | ||||||
|     { |  | ||||||
|       match: { ports: 80 }, |  | ||||||
|       action: { |  | ||||||
|         type: 'redirect', |  | ||||||
|         redirect: { to: 'https://{domain}{path}', status: 301 } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   acme: { |  | ||||||
|     enabled: true, |  | ||||||
|     email: 'admin@example.com', |  | ||||||
|     production: true |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 2. Microservices API Gateway |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| const apiGateway = new SmartProxy({ |  | ||||||
|   routes: [ |  | ||||||
|     // Users API |  | ||||||
|     { |  | ||||||
|       match: {  |  | ||||||
|         ports: 443, |  | ||||||
|         domains: 'api.example.com', |  | ||||||
|         path: '/users/*' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { host: 'users-service', port: 8001 }, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' } |  | ||||||
|       }, |  | ||||||
|       name: 'Users Service' |  | ||||||
|     }, |  | ||||||
|     // Products API |  | ||||||
|     { |  | ||||||
|       match: {  |  | ||||||
|         ports: 443, |  | ||||||
|         domains: 'api.example.com', |  | ||||||
|         path: '/products/*' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { host: 'products-service', port: 8002 }, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' } |  | ||||||
|       }, |  | ||||||
|       name: 'Products Service' |  | ||||||
|     }, |  | ||||||
|     // Orders API with authentication |  | ||||||
|     { |  | ||||||
|       match: {  |  | ||||||
|         ports: 443, |  | ||||||
|         domains: 'api.example.com', |  | ||||||
|         path: '/orders/*' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { host: 'orders-service', port: 8003 }, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' }, |  | ||||||
|         security: { |  | ||||||
|           authentication: { |  | ||||||
|             type: 'basic', |  | ||||||
|             users: { 'admin': 'password' } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       name: 'Orders Service (Protected)' |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   acme: { |  | ||||||
|     enabled: true, |  | ||||||
|     email: 'admin@example.com' |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### 3. Multi-Environment Setup |  | ||||||
|  |  | ||||||
| ```typescript |  | ||||||
| const environments = { |  | ||||||
|   production: { |  | ||||||
|     target: { host: 'prod-backend', port: 8080 }, |  | ||||||
|     security: { maxConnections: 1000 } |  | ||||||
|   }, |  | ||||||
|   staging: { |  | ||||||
|     target: { host: 'staging-backend', port: 8080 }, |  | ||||||
|     security: {  |  | ||||||
|       allowedIps: ['10.0.0.*', '192.168.1.*'], |  | ||||||
|       maxConnections: 100 |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   development: { |  | ||||||
|     target: { host: 'localhost', port: 3000 }, |  | ||||||
|     security: {  |  | ||||||
|       allowedIps: ['127.0.0.1'], |  | ||||||
|       maxConnections: 10 |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Select environment based on hostname |  | ||||||
| const proxy = new SmartProxy({ |  | ||||||
|   routes: [ |  | ||||||
|     // Production environment |  | ||||||
|     { |  | ||||||
|       match: {  |  | ||||||
|         ports: [80, 443], |  | ||||||
|         domains: ['app.example.com', 'www.example.com'] |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: environments.production.target, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' }, |  | ||||||
|         security: environments.production.security |  | ||||||
|       }, |  | ||||||
|       name: 'Production Environment' |  | ||||||
|     }, |  | ||||||
|     // Staging environment |  | ||||||
|     { |  | ||||||
|       match: {  |  | ||||||
|         ports: [80, 443], |  | ||||||
|         domains: 'staging.example.com' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: environments.staging.target, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' }, |  | ||||||
|         security: environments.staging.security |  | ||||||
|       }, |  | ||||||
|       name: 'Staging Environment' |  | ||||||
|     }, |  | ||||||
|     // Development environment |  | ||||||
|     { |  | ||||||
|       match: {  |  | ||||||
|         ports: [80, 443], |  | ||||||
|         domains: 'dev.example.com' |  | ||||||
|       }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: environments.development.target, |  | ||||||
|         tls: { mode: 'terminate', certificate: 'auto' }, |  | ||||||
|         security: environments.development.security |  | ||||||
|       }, |  | ||||||
|       name: 'Development Environment' |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   acme: { enabled: true, email: 'admin@example.com' } |  | ||||||
| }); |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Implementation Strategy | ## Implementation Strategy | ||||||
|  |  | ||||||
| ### Code Organization | ### Code Organization | ||||||
|  |  | ||||||
| 1. **New Files**: | 1. **New Files**: | ||||||
|    - `route-manager.ts` (core routing engine) |    - `route-config.ts` - Core route interfaces | ||||||
|    - `route-types.ts` (interface definitions) |    - `route-manager.ts` - Route matching and management | ||||||
|    - `route-helpers.ts` (helper functions) |    - `route-connection-handler.ts` - Connection handling with routes | ||||||
|    - `route-matcher.ts` (matching logic) |    - `route-smart-proxy.ts` - Main SmartProxy implementation | ||||||
|    - `template-engine.ts` (for variable substitution) |    - `template-engine.ts` - For variable substitution | ||||||
|  |  | ||||||
| 2. **Modified Files**: | 2. **File Removal**: | ||||||
|    - `smart-proxy.ts` (update to use route-based configuration) |    - Remove `port-range-manager.ts` | ||||||
|    - `connection-handler.ts` (simplify using route-based approach) |    - Remove `domain-config-manager.ts` | ||||||
|    - Replace `port-range-manager.ts` and `domain-config-manager.ts` |    - Remove legacy interfaces and adapter code | ||||||
|  |    - Remove backward compatibility shims | ||||||
|  |  | ||||||
| ### Backward Compatibility | ### Transition Strategy | ||||||
|  |  | ||||||
| The backward compatibility layer will convert the legacy configuration to the new format: | 1. **Breaking Change Approach**: | ||||||
|  |    - This will be a major version update with breaking changes | ||||||
|  |    - No backward compatibility will be maintained | ||||||
|  |    - Clear migration documentation will guide users to the new API | ||||||
|  |  | ||||||
| ```typescript | 2. **Package Structure**: | ||||||
| function convertLegacyConfig(legacy: ILegacySmartProxyOptions): ISmartProxyOptions { |    - `@push.rocks/smartproxy` package will be updated to v14.0.0 | ||||||
|   const routes: IRouteConfig[] = []; |    - Legacy code fully removed, only route-based API exposed | ||||||
|  |    - Support documentation provided for migration | ||||||
|  |  | ||||||
|   // Convert main port configuration | 3. **Migration Documentation**: | ||||||
|   if (legacy.fromPort) { |    - Provide a migration guide with examples | ||||||
|     // Add main listener for fromPort |    - Show equivalent route configurations for common legacy patterns | ||||||
|     routes.push({ |    - Offer code transformation helpers for complex setups | ||||||
|       match: { ports: legacy.fromPort }, |  | ||||||
|       action: { |  | ||||||
|         type: 'forward', |  | ||||||
|         target: { host: legacy.targetIP || 'localhost', port: legacy.toPort }, |  | ||||||
|         tls: { mode: legacy.sniEnabled ? 'passthrough' : 'terminate' } |  | ||||||
|       }, |  | ||||||
|       name: 'Main Listener (Legacy)' |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // If ACME is enabled, add HTTP listener for challenges | ## Benefits of the Clean Approach | ||||||
|     if (legacy.acme?.enabled) { |  | ||||||
|       routes.push({ |  | ||||||
|         match: { ports: 80 }, |  | ||||||
|         action: { |  | ||||||
|           type: 'forward', |  | ||||||
|           target: { host: 'localhost', port: 80 }, |  | ||||||
|           // Special flag for ACME handler |  | ||||||
|           acmeEnabled: true |  | ||||||
|         }, |  | ||||||
|         name: 'ACME Challenge Handler (Legacy)' |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Convert domain configs | 1. **Reduced Complexity**: | ||||||
|   if (legacy.domainConfigs) { |    - No overlapping or conflicting configuration systems | ||||||
|     for (const domainConfig of legacy.domainConfigs) { |    - No dual maintenance of backward compatibility code | ||||||
|       const { domains, forwarding } = domainConfig; |    - Simplified internal architecture | ||||||
|  |  | ||||||
|       // Determine action based on forwarding type | 2. **Cleaner Code Base**: | ||||||
|       let action: Partial<IRouteAction> = { |    - Removal of technical debt | ||||||
|         type: 'forward', |    - Better separation of concerns | ||||||
|         target: { |    - More maintainable codebase | ||||||
|           host: forwarding.target.host, |  | ||||||
|           port: forwarding.target.port |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
|  |  | ||||||
|       // Set TLS mode based on forwarding type | 3. **Better User Experience**: | ||||||
|       switch (forwarding.type) { |    - Consistent, predictable API | ||||||
|         case 'http-only': |    - No confusing overlapping options | ||||||
|           // No TLS |    - Clear documentation of one approach, not two | ||||||
|           break; |  | ||||||
|         case 'https-passthrough': |  | ||||||
|           action.tls = { mode: 'passthrough' }; |  | ||||||
|           break; |  | ||||||
|         case 'https-terminate-to-http': |  | ||||||
|           action.tls = {  |  | ||||||
|             mode: 'terminate', |  | ||||||
|             certificate: forwarding.https?.customCert || 'auto' |  | ||||||
|           }; |  | ||||||
|           break; |  | ||||||
|         case 'https-terminate-to-https': |  | ||||||
|           action.tls = {  |  | ||||||
|             mode: 'terminate-and-reencrypt', |  | ||||||
|             certificate: forwarding.https?.customCert || 'auto' |  | ||||||
|           }; |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Security settings | 4. **Future-Proof Design**: | ||||||
|       if (forwarding.security) { |    - Easier to extend with new features | ||||||
|         action.security = forwarding.security; |    - Better performance without legacy overhead | ||||||
|       } |    - Cleaner foundation for future enhancements | ||||||
|  |  | ||||||
|       // Add HTTP redirect if needed | ## Migration Support | ||||||
|       if (forwarding.http?.redirectToHttps) { |  | ||||||
|         routes.push({ |  | ||||||
|           match: { ports: 80, domains }, |  | ||||||
|           action: { |  | ||||||
|             type: 'redirect', |  | ||||||
|             redirect: { to: 'https://{domain}{path}', status: 301 } |  | ||||||
|           }, |  | ||||||
|           name: `HTTP Redirect for ${domains.join(', ')} (Legacy)` |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Add main route | While we're removing backward compatibility from the codebase, we'll provide extensive migration support: | ||||||
|       routes.push({ |  | ||||||
|         match: {  |  | ||||||
|           ports: forwarding.type.startsWith('https') ? 443 : 80, |  | ||||||
|           domains |  | ||||||
|         }, |  | ||||||
|         action: action as IRouteAction, |  | ||||||
|         name: `Route for ${domains.join(', ')} (Legacy)` |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       // Add port ranges if specified | 1. **Migration Guide**: | ||||||
|       if (forwarding.advanced?.portRanges) { |    - Detailed documentation on moving from legacy to route-based config | ||||||
|         for (const range of forwarding.advanced.portRanges) { |    - Pattern-matching examples for all common use cases | ||||||
|           routes.push({ |    - Troubleshooting guide for common migration issues | ||||||
|             match: { |  | ||||||
|               ports: { from: range.from, to: range.to }, |  | ||||||
|               domains |  | ||||||
|             }, |  | ||||||
|             action: action as IRouteAction, |  | ||||||
|             name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')} (Legacy)` |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Global port ranges | 2. **Conversion Tool**: | ||||||
|   if (legacy.globalPortRanges) { |    - Provide a standalone one-time conversion tool | ||||||
|     for (const range of legacy.globalPortRanges) { |    - Takes legacy configuration and outputs route-based equivalents | ||||||
|       routes.push({ |    - Will not be included in the main package to avoid bloat | ||||||
|         match: { ports: { from: range.from, to: range.to } }, |  | ||||||
|         action: { |  | ||||||
|           type: 'forward', |  | ||||||
|           target: {  |  | ||||||
|             host: legacy.targetIP || 'localhost', |  | ||||||
|             port: legacy.forwardAllGlobalRanges ? 0 : legacy.toPort, |  | ||||||
|             preservePort: !!legacy.forwardAllGlobalRanges |  | ||||||
|           }, |  | ||||||
|           tls: { mode: 'passthrough' } |  | ||||||
|         }, |  | ||||||
|         name: `Global Port Range ${range.from}-${range.to} (Legacy)` |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return { | 3. **Version Policy**: | ||||||
|     routes, |    - Maintain the legacy version (13.x) for security updates | ||||||
|     defaults: { |    - Make the route-based version a clear major version change (14.0.0) | ||||||
|       target: { |    - Clearly communicate the breaking changes | ||||||
|         host: legacy.targetIP || 'localhost', |  | ||||||
|         port: legacy.toPort |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     acme: legacy.acme |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ## Success Criteria | ## Timeline and Versioning | ||||||
|  |  | ||||||
| - All existing functionality works with the new route-based configuration | 1. **Development**: | ||||||
| - Performance is equal or better than the current implementation |    - Develop route-based implementation in a separate branch | ||||||
| - Configuration is more intuitive and easier to understand |    - Complete full test coverage of new implementation | ||||||
| - New features can be added without breaking existing code |    - Ensure documentation is complete | ||||||
| - Code is more maintainable with clear separation of concerns |  | ||||||
| - Migration from old configuration to new is straightforward | 2. **Release**: | ||||||
|  |    - Release as version 14.0.0 | ||||||
|  |    - Clearly mark as breaking change | ||||||
|  |    - Provide migration guide at release time | ||||||
|  |  | ||||||
|  | 3. **Support**: | ||||||
|  |    - Offer extended support for migration questions | ||||||
|  |    - Consider maintaining security updates for v13.x for 6 months | ||||||
|  |    - Focus active development on route-based version only | ||||||
							
								
								
									
										181
									
								
								test/test.route-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								test/test.route-config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | /** | ||||||
|  |  * Tests for the new route-based configuration system | ||||||
|  |  */ | ||||||
|  | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
|  |  | ||||||
|  | // Import from core modules | ||||||
|  | import { | ||||||
|  |   SmartProxy, | ||||||
|  |   createHttpRoute, | ||||||
|  |   createHttpsRoute, | ||||||
|  |   createPassthroughRoute, | ||||||
|  |   createRedirectRoute, | ||||||
|  |   createHttpToHttpsRedirect, | ||||||
|  |   createHttpsServer, | ||||||
|  |   createLoadBalancerRoute | ||||||
|  | } from '../ts/proxies/smart-proxy/index.js'; | ||||||
|  |  | ||||||
|  | // Import test helpers | ||||||
|  | import { getCertificate } from './helpers/certificates.js'; | ||||||
|  |  | ||||||
|  | tap.test('Routes: Should create basic HTTP route', async () => { | ||||||
|  |   // Create a simple HTTP route | ||||||
|  |   const httpRoute = createHttpRoute({ | ||||||
|  |     ports: 8080, | ||||||
|  |     domains: 'example.com', | ||||||
|  |     target: { | ||||||
|  |       host: 'localhost', | ||||||
|  |       port: 3000 | ||||||
|  |     }, | ||||||
|  |     name: 'Basic HTTP Route' | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Validate the route configuration | ||||||
|  |   expect(httpRoute.match.ports).to.equal(8080); | ||||||
|  |   expect(httpRoute.match.domains).to.equal('example.com'); | ||||||
|  |   expect(httpRoute.action.type).to.equal('forward'); | ||||||
|  |   expect(httpRoute.action.target?.host).to.equal('localhost'); | ||||||
|  |   expect(httpRoute.action.target?.port).to.equal(3000); | ||||||
|  |   expect(httpRoute.name).to.equal('Basic HTTP Route'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Routes: Should create HTTPS route with TLS termination', async () => { | ||||||
|  |   // Create an HTTPS route with TLS termination | ||||||
|  |   const httpsRoute = createHttpsRoute({ | ||||||
|  |     domains: 'secure.example.com', | ||||||
|  |     target: { | ||||||
|  |       host: 'localhost', | ||||||
|  |       port: 8080 | ||||||
|  |     }, | ||||||
|  |     certificate: 'auto', | ||||||
|  |     name: 'HTTPS Route' | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Validate the route configuration | ||||||
|  |   expect(httpsRoute.match.ports).to.equal(443); // Default HTTPS port | ||||||
|  |   expect(httpsRoute.match.domains).to.equal('secure.example.com'); | ||||||
|  |   expect(httpsRoute.action.type).to.equal('forward'); | ||||||
|  |   expect(httpsRoute.action.tls?.mode).to.equal('terminate'); | ||||||
|  |   expect(httpsRoute.action.tls?.certificate).to.equal('auto'); | ||||||
|  |   expect(httpsRoute.action.target?.host).to.equal('localhost'); | ||||||
|  |   expect(httpsRoute.action.target?.port).to.equal(8080); | ||||||
|  |   expect(httpsRoute.name).to.equal('HTTPS Route'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Routes: Should create HTTP to HTTPS redirect', async () => { | ||||||
|  |   // Create an HTTP to HTTPS redirect | ||||||
|  |   const redirectRoute = createHttpToHttpsRedirect({ | ||||||
|  |     domains: 'example.com', | ||||||
|  |     statusCode: 301 | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Validate the route configuration | ||||||
|  |   expect(redirectRoute.match.ports).to.equal(80); | ||||||
|  |   expect(redirectRoute.match.domains).to.equal('example.com'); | ||||||
|  |   expect(redirectRoute.action.type).to.equal('redirect'); | ||||||
|  |   expect(redirectRoute.action.redirect?.to).to.equal('https://{domain}{path}'); | ||||||
|  |   expect(redirectRoute.action.redirect?.status).to.equal(301); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Routes: Should create complete HTTPS server with redirects', async () => { | ||||||
|  |   // Create a complete HTTPS server setup | ||||||
|  |   const routes = createHttpsServer({ | ||||||
|  |     domains: 'example.com', | ||||||
|  |     target: { | ||||||
|  |       host: 'localhost', | ||||||
|  |       port: 8080 | ||||||
|  |     }, | ||||||
|  |     certificate: 'auto', | ||||||
|  |     addHttpRedirect: true | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Validate that we got two routes (HTTPS route and HTTP redirect) | ||||||
|  |   expect(routes.length).to.equal(2); | ||||||
|  |  | ||||||
|  |   // Validate HTTPS route | ||||||
|  |   const httpsRoute = routes[0]; | ||||||
|  |   expect(httpsRoute.match.ports).to.equal(443); | ||||||
|  |   expect(httpsRoute.match.domains).to.equal('example.com'); | ||||||
|  |   expect(httpsRoute.action.type).to.equal('forward'); | ||||||
|  |   expect(httpsRoute.action.tls?.mode).to.equal('terminate'); | ||||||
|  |  | ||||||
|  |   // Validate HTTP redirect route | ||||||
|  |   const redirectRoute = routes[1]; | ||||||
|  |   expect(redirectRoute.match.ports).to.equal(80); | ||||||
|  |   expect(redirectRoute.action.type).to.equal('redirect'); | ||||||
|  |   expect(redirectRoute.action.redirect?.to).to.equal('https://{domain}{path}'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Routes: Should create load balancer route', async () => { | ||||||
|  |   // Create a load balancer route | ||||||
|  |   const lbRoute = createLoadBalancerRoute({ | ||||||
|  |     domains: 'app.example.com', | ||||||
|  |     targets: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], | ||||||
|  |     targetPort: 8080, | ||||||
|  |     tlsMode: 'terminate', | ||||||
|  |     certificate: 'auto', | ||||||
|  |     name: 'Load Balanced Route' | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Validate the route configuration | ||||||
|  |   expect(lbRoute.match.domains).to.equal('app.example.com'); | ||||||
|  |   expect(lbRoute.action.type).to.equal('forward'); | ||||||
|  |   expect(Array.isArray(lbRoute.action.target?.host)).to.equal(true); | ||||||
|  |   expect((lbRoute.action.target?.host as string[]).length).to.equal(3); | ||||||
|  |   expect((lbRoute.action.target?.host as string[])[0]).to.equal('10.0.0.1'); | ||||||
|  |   expect(lbRoute.action.target?.port).to.equal(8080); | ||||||
|  |   expect(lbRoute.action.tls?.mode).to.equal('terminate'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('SmartProxy: Should create instance with route-based config', async () => { | ||||||
|  |   // Create TLS certificates for testing | ||||||
|  |   const cert = await getCertificate(); | ||||||
|  |  | ||||||
|  |   // Create a SmartProxy instance with route-based configuration | ||||||
|  |   const proxy = new SmartProxy({ | ||||||
|  |     routes: [ | ||||||
|  |       createHttpRoute({ | ||||||
|  |         ports: 8080, | ||||||
|  |         domains: 'example.com', | ||||||
|  |         target: { | ||||||
|  |           host: 'localhost', | ||||||
|  |           port: 3000 | ||||||
|  |         }, | ||||||
|  |         name: 'HTTP Route' | ||||||
|  |       }), | ||||||
|  |       createHttpsRoute({ | ||||||
|  |         domains: 'secure.example.com', | ||||||
|  |         target: { | ||||||
|  |           host: 'localhost', | ||||||
|  |           port: 8443 | ||||||
|  |         }, | ||||||
|  |         certificate: { | ||||||
|  |           key: cert.key, | ||||||
|  |           cert: cert.cert | ||||||
|  |         }, | ||||||
|  |         name: 'HTTPS Route' | ||||||
|  |       }) | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       target: { | ||||||
|  |         host: 'localhost', | ||||||
|  |         port: 8080 | ||||||
|  |       }, | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1', '192.168.0.*'], | ||||||
|  |         maxConnections: 100 | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     // Additional settings | ||||||
|  |     initialDataTimeout: 10000, | ||||||
|  |     inactivityTimeout: 300000, | ||||||
|  |     enableDetailedLogging: true | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Simply verify the instance was created successfully | ||||||
|  |   expect(proxy).to.be.an('object'); | ||||||
|  |   expect(proxy.start).to.be.a('function'); | ||||||
|  |   expect(proxy.stop).to.be.a('function'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
| @@ -66,13 +66,25 @@ function createTestClient(port: number, data: string): Promise<string> { | |||||||
| tap.test('setup port proxy test environment', async () => { | tap.test('setup port proxy test environment', async () => { | ||||||
|   testServer = await createTestServer(TEST_SERVER_PORT); |   testServer = await createTestServer(TEST_SERVER_PORT); | ||||||
|   smartProxy = new SmartProxy({ |   smartProxy = new SmartProxy({ | ||||||
|     fromPort: PROXY_PORT, |     routes: [ | ||||||
|     toPort: TEST_SERVER_PORT, |       { | ||||||
|     targetIP: 'localhost', |         match: { | ||||||
|     domainConfigs: [], |           ports: PROXY_PORT | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1'], |         action: { | ||||||
|     globalPortRanges: [] |           type: 'forward', | ||||||
|  |           target: { | ||||||
|  |             host: 'localhost', | ||||||
|  |             port: TEST_SERVER_PORT | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1'] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|   allProxies.push(smartProxy); // Track this proxy |   allProxies.push(smartProxy); // Track this proxy | ||||||
| }); | }); | ||||||
| @@ -92,13 +104,25 @@ tap.test('should forward TCP connections and data to localhost', async () => { | |||||||
| // Test proxy with a custom target host. | // Test proxy with a custom target host. | ||||||
| tap.test('should forward TCP connections to custom host', async () => { | tap.test('should forward TCP connections to custom host', async () => { | ||||||
|   const customHostProxy = new SmartProxy({ |   const customHostProxy = new SmartProxy({ | ||||||
|     fromPort: PROXY_PORT + 1, |     routes: [ | ||||||
|     toPort: TEST_SERVER_PORT, |       { | ||||||
|     targetIP: '127.0.0.1', |         match: { | ||||||
|     domainConfigs: [], |           ports: PROXY_PORT + 1 | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1'], |         action: { | ||||||
|     globalPortRanges: [] |           type: 'forward', | ||||||
|  |           target: { | ||||||
|  |             host: '127.0.0.1', | ||||||
|  |             port: TEST_SERVER_PORT | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1'] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|   allProxies.push(customHostProxy); // Track this proxy |   allProxies.push(customHostProxy); // Track this proxy | ||||||
|    |    | ||||||
| @@ -125,14 +149,25 @@ tap.test('should forward connections to custom IP', async () => { | |||||||
|   // We're simulating routing to a different IP by using a different port |   // We're simulating routing to a different IP by using a different port | ||||||
|   // This tests the core functionality without requiring multiple IPs |   // This tests the core functionality without requiring multiple IPs | ||||||
|   const domainProxy = new SmartProxy({ |   const domainProxy = new SmartProxy({ | ||||||
|     fromPort: forcedProxyPort,  // 4003 - Listen on this port |     routes: [ | ||||||
|     toPort: targetServerPort,   // 4200 - Forward to this port |       { | ||||||
|     targetIP: '127.0.0.1',      // Always use localhost (works in Docker) |         match: { | ||||||
|     domainConfigs: [],          // No domain configs to confuse things |           ports: forcedProxyPort | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost |         action: { | ||||||
|     // We'll test the functionality WITHOUT port ranges this time |           type: 'forward', | ||||||
|     globalPortRanges: [] |           target: { | ||||||
|  |             host: '127.0.0.1', | ||||||
|  |             port: targetServerPort | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|   allProxies.push(domainProxy); // Track this proxy |   allProxies.push(domainProxy); // Track this proxy | ||||||
|  |  | ||||||
| @@ -208,22 +243,46 @@ tap.test('should stop port proxy', async () => { | |||||||
| tap.test('should support optional source IP preservation in chained proxies', async () => { | tap.test('should support optional source IP preservation in chained proxies', async () => { | ||||||
|   // Chained proxies without IP preservation. |   // Chained proxies without IP preservation. | ||||||
|   const firstProxyDefault = new SmartProxy({ |   const firstProxyDefault = new SmartProxy({ | ||||||
|     fromPort: PROXY_PORT + 4, |     routes: [ | ||||||
|     toPort: PROXY_PORT + 5, |       { | ||||||
|     targetIP: 'localhost', |         match: { | ||||||
|     domainConfigs: [], |           ports: PROXY_PORT + 4 | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], |         action: { | ||||||
|     globalPortRanges: [] |           type: 'forward', | ||||||
|  |           target: { | ||||||
|  |             host: 'localhost', | ||||||
|  |             port: PROXY_PORT + 5 | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|   const secondProxyDefault = new SmartProxy({ |   const secondProxyDefault = new SmartProxy({ | ||||||
|     fromPort: PROXY_PORT + 5, |     routes: [ | ||||||
|     toPort: TEST_SERVER_PORT, |       { | ||||||
|     targetIP: 'localhost', |         match: { | ||||||
|     domainConfigs: [], |           ports: PROXY_PORT + 5 | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], |         action: { | ||||||
|     globalPortRanges: [] |           type: 'forward', | ||||||
|  |           target: { | ||||||
|  |             host: 'localhost', | ||||||
|  |             port: TEST_SERVER_PORT | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies |   allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies | ||||||
| @@ -243,24 +302,50 @@ tap.test('should support optional source IP preservation in chained proxies', as | |||||||
|  |  | ||||||
|   // Chained proxies with IP preservation. |   // Chained proxies with IP preservation. | ||||||
|   const firstProxyPreserved = new SmartProxy({ |   const firstProxyPreserved = new SmartProxy({ | ||||||
|     fromPort: PROXY_PORT + 6, |     routes: [ | ||||||
|     toPort: PROXY_PORT + 7, |       { | ||||||
|     targetIP: 'localhost', |         match: { | ||||||
|     domainConfigs: [], |           ports: PROXY_PORT + 6 | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1'], |         action: { | ||||||
|     preserveSourceIP: true, |           type: 'forward', | ||||||
|     globalPortRanges: [] |           target: { | ||||||
|  |             host: 'localhost', | ||||||
|  |             port: PROXY_PORT + 7 | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1'] | ||||||
|  |       }, | ||||||
|  |       preserveSourceIP: true | ||||||
|  |     }, | ||||||
|  |     preserveSourceIP: true | ||||||
|   }); |   }); | ||||||
|   const secondProxyPreserved = new SmartProxy({ |   const secondProxyPreserved = new SmartProxy({ | ||||||
|     fromPort: PROXY_PORT + 7, |     routes: [ | ||||||
|     toPort: TEST_SERVER_PORT, |       { | ||||||
|     targetIP: 'localhost', |         match: { | ||||||
|     domainConfigs: [], |           ports: PROXY_PORT + 7 | ||||||
|     sniEnabled: false, |         }, | ||||||
|     defaultAllowedIPs: ['127.0.0.1'], |         action: { | ||||||
|     preserveSourceIP: true, |           type: 'forward', | ||||||
|     globalPortRanges: [] |           target: { | ||||||
|  |             host: 'localhost', | ||||||
|  |             port: TEST_SERVER_PORT | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     defaults: { | ||||||
|  |       security: { | ||||||
|  |         allowedIPs: ['127.0.0.1'] | ||||||
|  |       }, | ||||||
|  |       preserveSourceIP: true | ||||||
|  |     }, | ||||||
|  |     preserveSourceIP: true | ||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies |   allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * SmartProxy implementation |  * SmartProxy implementation | ||||||
|  |  * | ||||||
|  |  * Version 14.0.0: Unified Route-Based Configuration API | ||||||
|  */ |  */ | ||||||
| // Re-export models | // Re-export models | ||||||
| export * from './models/index.js'; | export * from './models/index.js'; | ||||||
| @@ -7,12 +9,26 @@ export * from './models/index.js'; | |||||||
| // Export the main SmartProxy class | // Export the main SmartProxy class | ||||||
| export { SmartProxy } from './smart-proxy.js'; | export { SmartProxy } from './smart-proxy.js'; | ||||||
|  |  | ||||||
| // Export supporting classes | // Export core supporting classes | ||||||
| export { ConnectionManager } from './connection-manager.js'; | export { ConnectionManager } from './connection-manager.js'; | ||||||
| export { SecurityManager } from './security-manager.js'; | export { SecurityManager } from './security-manager.js'; | ||||||
| export { DomainConfigManager } from './domain-config-manager.js'; |  | ||||||
| export { TimeoutManager } from './timeout-manager.js'; | export { TimeoutManager } from './timeout-manager.js'; | ||||||
| export { TlsManager } from './tls-manager.js'; | export { TlsManager } from './tls-manager.js'; | ||||||
| export { NetworkProxyBridge } from './network-proxy-bridge.js'; | export { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||||
| export { PortRangeManager } from './port-range-manager.js'; |  | ||||||
| export { ConnectionHandler } from './connection-handler.js'; | // Export route-based components | ||||||
|  | export { RouteManager } from './route-manager.js'; | ||||||
|  | export { RouteConnectionHandler } from './route-connection-handler.js'; | ||||||
|  |  | ||||||
|  | // Export route helpers for configuration | ||||||
|  | export { | ||||||
|  |   createRoute, | ||||||
|  |   createHttpRoute, | ||||||
|  |   createHttpsRoute, | ||||||
|  |   createPassthroughRoute, | ||||||
|  |   createRedirectRoute, | ||||||
|  |   createHttpToHttpsRedirect, | ||||||
|  |   createBlockRoute, | ||||||
|  |   createLoadBalancerRoute, | ||||||
|  |   createHttpsServer | ||||||
|  | } from './route-helpers.js'; | ||||||
|   | |||||||
| @@ -2,3 +2,7 @@ | |||||||
|  * SmartProxy models |  * SmartProxy models | ||||||
|  */ |  */ | ||||||
| export * from './interfaces.js'; | export * from './interfaces.js'; | ||||||
|  | export * from './route-types.js'; | ||||||
|  |  | ||||||
|  | // Re-export IRoutedSmartProxyOptions explicitly to avoid ambiguity | ||||||
|  | export type { ISmartProxyOptions as IRoutedSmartProxyOptions } from './interfaces.js'; | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| import * as plugins from '../../../plugins.js'; | import * as plugins from '../../../plugins.js'; | ||||||
| import type { IForwardConfig } from '../../../forwarding/config/forwarding-types.js'; | import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||||
|  | import type { IRouteConfig } from './route-types.js'; | ||||||
|  | import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Provision object for static or HTTP-01 certificate |  * Provision object for static or HTTP-01 certificate | ||||||
| @@ -7,27 +9,102 @@ import type { IForwardConfig } from '../../../forwarding/config/forwarding-types | |||||||
| export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Domain configuration with forwarding configuration |  * Alias for backward compatibility with code that uses IRoutedSmartProxyOptions | ||||||
|  |  */ | ||||||
|  | export type IRoutedSmartProxyOptions = ISmartProxyOptions; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Legacy domain configuration interface for backward compatibility | ||||||
|  */ |  */ | ||||||
| export interface IDomainConfig { | export interface IDomainConfig { | ||||||
|   domains: string[]; // Glob patterns for domain(s) |   domains: string[]; | ||||||
|   forwarding: IForwardConfig; // Unified forwarding configuration |   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>; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Configuration options for the SmartProxy |  * Helper functions for type checking - now always assume route-based config | ||||||
|  |  */ | ||||||
|  | export function isLegacyOptions(options: any): boolean { | ||||||
|  |   return false; // No longer supporting legacy options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function isRoutedOptions(options: any): boolean { | ||||||
|  |   return true; // Always assume routed options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * SmartProxy configuration options | ||||||
|  */ |  */ | ||||||
| import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; |  | ||||||
| export interface ISmartProxyOptions { | export interface ISmartProxyOptions { | ||||||
|   fromPort: number; |   // The unified configuration array (required) | ||||||
|   toPort: number; |   routes: IRouteConfig[]; | ||||||
|   targetIP?: string; // Global target host to proxy to, defaults to 'localhost' |  | ||||||
|   domainConfigs: IDomainConfig[]; |   // Legacy options for backward compatibility | ||||||
|  |   fromPort?: number; | ||||||
|  |   toPort?: number; | ||||||
|   sniEnabled?: boolean; |   sniEnabled?: boolean; | ||||||
|  |   domainConfigs?: IDomainConfig[]; | ||||||
|  |   targetIP?: string; | ||||||
|   defaultAllowedIPs?: string[]; |   defaultAllowedIPs?: string[]; | ||||||
|   defaultBlockedIPs?: string[]; |   defaultBlockedIPs?: string[]; | ||||||
|  |   globalPortRanges?: Array<{ from: number; to: number }>; | ||||||
|  |   forwardAllGlobalRanges?: boolean; | ||||||
|   preserveSourceIP?: boolean; |   preserveSourceIP?: boolean; | ||||||
|  |  | ||||||
|  |   // Global/default settings | ||||||
|  |   defaults?: { | ||||||
|  |     target?: { | ||||||
|  |       host: string; // Default host to use when not specified in routes | ||||||
|  |       port: number; // Default port to use when not specified in routes | ||||||
|  |     }; | ||||||
|  |     security?: { | ||||||
|  |       allowedIPs?: string[]; // Default allowed IPs | ||||||
|  |       blockedIPs?: string[]; // Default blocked IPs | ||||||
|  |       maxConnections?: number; // Default max connections | ||||||
|  |     }; | ||||||
|  |     preserveSourceIP?: boolean; // Default source IP preservation | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   // TLS options |   // TLS options | ||||||
|   pfx?: Buffer; |   pfx?: Buffer; | ||||||
|   key?: string | Buffer | Array<Buffer | string>; |   key?: string | Buffer | Array<Buffer | string>; | ||||||
| @@ -50,8 +127,6 @@ export interface ISmartProxyOptions { | |||||||
|   inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) |   inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) | ||||||
|  |  | ||||||
|   gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown |   gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown | ||||||
|   globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges |  | ||||||
|   forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP |  | ||||||
|  |  | ||||||
|   // Socket optimization settings |   // Socket optimization settings | ||||||
|   noDelay?: boolean; // Disable Nagle's algorithm (default: true) |   noDelay?: boolean; // Disable Nagle's algorithm (default: true) | ||||||
| @@ -108,6 +183,9 @@ export interface IConnectionRecord { | |||||||
|   pendingData: Buffer[]; // Buffer to hold data during connection setup |   pendingData: Buffer[]; // Buffer to hold data during connection setup | ||||||
|   pendingDataSize: number; // Track total size of pending data |   pendingDataSize: number; // Track total size of pending data | ||||||
|  |  | ||||||
|  |   // Legacy property for backward compatibility | ||||||
|  |   domainConfig?: IDomainConfig; | ||||||
|  |  | ||||||
|   // Enhanced tracking fields |   // Enhanced tracking fields | ||||||
|   bytesReceived: number; // Total bytes received |   bytesReceived: number; // Total bytes received | ||||||
|   bytesSent: number; // Total bytes sent |   bytesSent: number; // Total bytes sent | ||||||
| @@ -116,7 +194,7 @@ export interface IConnectionRecord { | |||||||
|   isTLS: boolean; // Whether this connection is a TLS connection |   isTLS: boolean; // Whether this connection is a TLS connection | ||||||
|   tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete |   tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete | ||||||
|   hasReceivedInitialData: boolean; // Whether initial data has been received |   hasReceivedInitialData: boolean; // Whether initial data has been received | ||||||
|   domainConfig?: IDomainConfig; // Associated domain config for this connection |   routeConfig?: IRouteConfig; // Associated route config for this connection | ||||||
|  |  | ||||||
|   // Keep-alive tracking |   // Keep-alive tracking | ||||||
|   hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection |   hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection | ||||||
|   | |||||||
							
								
								
									
										184
									
								
								ts/proxies/smart-proxy/models/route-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								ts/proxies/smart-proxy/models/route-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | |||||||
|  | import * as plugins from '../../../plugins.js'; | ||||||
|  | import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||||
|  | import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Supported action types for route configurations | ||||||
|  |  */ | ||||||
|  | export type TRouteActionType = 'forward' | 'redirect' | 'block'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * TLS handling modes for route configurations | ||||||
|  |  */ | ||||||
|  | export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Port range specification format | ||||||
|  |  */ | ||||||
|  | export type TPortRange = number | number[] | Array<{ from: number; to: number }>; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Route match criteria for incoming requests | ||||||
|  |  */ | ||||||
|  | export interface IRouteMatch { | ||||||
|  |   // Listen on these ports (required) | ||||||
|  |   ports: TPortRange; | ||||||
|  |    | ||||||
|  |   // Optional domain patterns to match (default: all domains) | ||||||
|  |   domains?: string | string[]; | ||||||
|  |    | ||||||
|  |   // Advanced matching criteria | ||||||
|  |   path?: string;           // Match specific paths | ||||||
|  |   clientIp?: string[];     // Match specific client IPs | ||||||
|  |   tlsVersion?: string[];   // Match specific TLS versions | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Target configuration for forwarding | ||||||
|  |  */ | ||||||
|  | export interface IRouteTarget { | ||||||
|  |   host: string | string[];  // Support single host or round-robin | ||||||
|  |   port: number; | ||||||
|  |   preservePort?: boolean;   // Use incoming port as target port | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * TLS configuration for route actions | ||||||
|  |  */ | ||||||
|  | export interface IRouteTls { | ||||||
|  |   mode: TTlsMode; | ||||||
|  |   certificate?: 'auto' | {   // Auto = use ACME | ||||||
|  |     key: string; | ||||||
|  |     cert: string; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Redirect configuration for route actions | ||||||
|  |  */ | ||||||
|  | export interface IRouteRedirect { | ||||||
|  |   to: string;            // URL or template with {domain}, {port}, etc. | ||||||
|  |   status: 301 | 302 | 307 | 308; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Security options for route actions | ||||||
|  |  */ | ||||||
|  | export interface IRouteSecurity { | ||||||
|  |   allowedIps?: string[]; | ||||||
|  |   blockedIps?: string[]; | ||||||
|  |   maxConnections?: number; | ||||||
|  |   authentication?: { | ||||||
|  |     type: 'basic' | 'digest' | 'oauth'; | ||||||
|  |     // Auth-specific options would go here | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Advanced options for route actions | ||||||
|  |  */ | ||||||
|  | export interface IRouteAdvanced { | ||||||
|  |   timeout?: number; | ||||||
|  |   headers?: Record<string, string>; | ||||||
|  |   keepAlive?: boolean; | ||||||
|  |   // Additional advanced options would go here | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Action configuration for route handling | ||||||
|  |  */ | ||||||
|  | export interface IRouteAction { | ||||||
|  |   // Basic routing | ||||||
|  |   type: TRouteActionType; | ||||||
|  |    | ||||||
|  |   // Target for forwarding | ||||||
|  |   target?: IRouteTarget; | ||||||
|  |    | ||||||
|  |   // TLS handling | ||||||
|  |   tls?: IRouteTls; | ||||||
|  |    | ||||||
|  |   // For redirects | ||||||
|  |   redirect?: IRouteRedirect; | ||||||
|  |    | ||||||
|  |   // Security options | ||||||
|  |   security?: IRouteSecurity; | ||||||
|  |    | ||||||
|  |   // Advanced options | ||||||
|  |   advanced?: IRouteAdvanced; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The core unified configuration interface | ||||||
|  |  */ | ||||||
|  | export interface IRouteConfig { | ||||||
|  |   // What to match | ||||||
|  |   match: IRouteMatch; | ||||||
|  |    | ||||||
|  |   // What to do with matched traffic | ||||||
|  |   action: IRouteAction; | ||||||
|  |    | ||||||
|  |   // Optional metadata | ||||||
|  |   name?: string;             // Human-readable name for this route | ||||||
|  |   description?: string;      // Description of the route's purpose | ||||||
|  |   priority?: number;         // Controls matching order (higher = matched first) | ||||||
|  |   tags?: string[];           // Arbitrary tags for categorization | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Unified SmartProxy options with routes-based configuration | ||||||
|  |  */ | ||||||
|  | export interface IRoutedSmartProxyOptions { | ||||||
|  |   // The unified configuration array (required) | ||||||
|  |   routes: IRouteConfig[]; | ||||||
|  |    | ||||||
|  |   // Global/default settings | ||||||
|  |   defaults?: { | ||||||
|  |     target?: { | ||||||
|  |       host: string; | ||||||
|  |       port: number; | ||||||
|  |     }; | ||||||
|  |     security?: IRouteSecurity; | ||||||
|  |     tls?: IRouteTls; | ||||||
|  |     // ...other defaults | ||||||
|  |   }; | ||||||
|  |    | ||||||
|  |   // Other global settings remain (acme, etc.) | ||||||
|  |   acme?: IAcmeOptions; | ||||||
|  |    | ||||||
|  |   // Connection timeouts and other global settings | ||||||
|  |   initialDataTimeout?: number; | ||||||
|  |   socketTimeout?: number; | ||||||
|  |   inactivityCheckInterval?: number; | ||||||
|  |   maxConnectionLifetime?: number; | ||||||
|  |   inactivityTimeout?: number; | ||||||
|  |   gracefulShutdownTimeout?: number; | ||||||
|  |    | ||||||
|  |   // Socket optimization settings | ||||||
|  |   noDelay?: boolean; | ||||||
|  |   keepAlive?: boolean; | ||||||
|  |   keepAliveInitialDelay?: number; | ||||||
|  |   maxPendingDataSize?: number; | ||||||
|  |    | ||||||
|  |   // Enhanced features | ||||||
|  |   disableInactivityCheck?: boolean; | ||||||
|  |   enableKeepAliveProbes?: boolean; | ||||||
|  |   enableDetailedLogging?: boolean; | ||||||
|  |   enableTlsDebugLogging?: boolean; | ||||||
|  |   enableRandomizedTimeouts?: boolean; | ||||||
|  |   allowSessionTicket?: boolean; | ||||||
|  |    | ||||||
|  |   // Rate limiting and security | ||||||
|  |   maxConnectionsPerIP?: number; | ||||||
|  |   connectionRateLimitPerMinute?: number; | ||||||
|  |    | ||||||
|  |   // Enhanced keep-alive settings | ||||||
|  |   keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; | ||||||
|  |   keepAliveInactivityMultiplier?: number; | ||||||
|  |   extendedKeepAliveLifetime?: number; | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges, | ||||||
|  |    * or a static certificate object for immediate provisioning. | ||||||
|  |    */ | ||||||
|  |   certProvisionFunction?: (domain: string) => Promise<any>; | ||||||
|  | } | ||||||
							
								
								
									
										1117
									
								
								ts/proxies/smart-proxy/route-connection-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1117
									
								
								ts/proxies/smart-proxy/route-connection-handler.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										344
									
								
								ts/proxies/smart-proxy/route-helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								ts/proxies/smart-proxy/route-helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,344 @@ | |||||||
|  | import type {  | ||||||
|  |   IRouteConfig,  | ||||||
|  |   IRouteMatch,  | ||||||
|  |   IRouteAction,  | ||||||
|  |   IRouteTarget, | ||||||
|  |   IRouteTls, | ||||||
|  |   IRouteRedirect, | ||||||
|  |   IRouteSecurity, | ||||||
|  |   IRouteAdvanced | ||||||
|  | } 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; | ||||||
|  | } | ||||||
							
								
								
									
										587
									
								
								ts/proxies/smart-proxy/route-manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										587
									
								
								ts/proxies/smart-proxy/route-manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,587 @@ | |||||||
|  | import * as plugins from '../../plugins.js'; | ||||||
|  | import type { | ||||||
|  |   IRouteConfig, | ||||||
|  |   IRouteMatch, | ||||||
|  |   IRouteAction, | ||||||
|  |   TPortRange | ||||||
|  | } from './models/route-types.js'; | ||||||
|  | import type { | ||||||
|  |   ISmartProxyOptions, | ||||||
|  |   IRoutedSmartProxyOptions, | ||||||
|  |   IDomainConfig | ||||||
|  | } from './models/interfaces.js'; | ||||||
|  | import { | ||||||
|  |   isRoutedOptions, | ||||||
|  |   isLegacyOptions | ||||||
|  | } from './models/interfaces.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Result of route matching | ||||||
|  |  */ | ||||||
|  | export interface IRouteMatchResult { | ||||||
|  |   route: IRouteConfig; | ||||||
|  |   // Additional match parameters (path, query, etc.) | ||||||
|  |   params?: Record<string, string>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The RouteManager handles all routing decisions based on connections and attributes | ||||||
|  |  */ | ||||||
|  | export class RouteManager extends plugins.EventEmitter { | ||||||
|  |   private routes: IRouteConfig[] = []; | ||||||
|  |   private portMap: Map<number, IRouteConfig[]> = new Map(); | ||||||
|  |   private options: IRoutedSmartProxyOptions; | ||||||
|  |    | ||||||
|  |   constructor(options: ISmartProxyOptions) { | ||||||
|  |     super(); | ||||||
|  |      | ||||||
|  |     // We no longer support legacy options, always use provided options | ||||||
|  |     this.options = options; | ||||||
|  |      | ||||||
|  |     // Initialize routes from either source | ||||||
|  |     this.updateRoutes(this.options.routes); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Update routes with new configuration | ||||||
|  |    */ | ||||||
|  |   public updateRoutes(routes: IRouteConfig[] = []): void { | ||||||
|  |     // Sort routes by priority (higher first) | ||||||
|  |     this.routes = [...(routes || [])].sort((a, b) => { | ||||||
|  |       const priorityA = a.priority ?? 0; | ||||||
|  |       const priorityB = b.priority ?? 0; | ||||||
|  |       return priorityB - priorityA; | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // Rebuild port mapping for fast lookups | ||||||
|  |     this.rebuildPortMap(); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Rebuild the port mapping for fast lookups | ||||||
|  |    */ | ||||||
|  |   private rebuildPortMap(): void { | ||||||
|  |     this.portMap.clear(); | ||||||
|  |      | ||||||
|  |     for (const route of this.routes) { | ||||||
|  |       const ports = this.expandPortRange(route.match.ports); | ||||||
|  |        | ||||||
|  |       for (const port of ports) { | ||||||
|  |         if (!this.portMap.has(port)) { | ||||||
|  |           this.portMap.set(port, []); | ||||||
|  |         } | ||||||
|  |         this.portMap.get(port)!.push(route); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Expand a port range specification into an array of individual ports | ||||||
|  |    */ | ||||||
|  |   private expandPortRange(portRange: TPortRange): number[] { | ||||||
|  |     if (typeof portRange === 'number') { | ||||||
|  |       return [portRange]; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (Array.isArray(portRange)) { | ||||||
|  |       // Handle array of port objects or numbers | ||||||
|  |       return portRange.flatMap(item => { | ||||||
|  |         if (typeof item === 'number') { | ||||||
|  |           return [item]; | ||||||
|  |         } else if (typeof item === 'object' && 'from' in item && 'to' in item) { | ||||||
|  |           // Handle port range object | ||||||
|  |           const ports: number[] = []; | ||||||
|  |           for (let p = item.from; p <= item.to; p++) { | ||||||
|  |             ports.push(p); | ||||||
|  |           } | ||||||
|  |           return ports; | ||||||
|  |         } | ||||||
|  |         return []; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Get all ports that should be listened on | ||||||
|  |    */ | ||||||
|  |   public getListeningPorts(): number[] { | ||||||
|  |     return Array.from(this.portMap.keys()); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Get all routes for a given port | ||||||
|  |    */ | ||||||
|  |   public getRoutesForPort(port: number): IRouteConfig[] { | ||||||
|  |     return this.portMap.get(port) || []; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Test if a pattern matches a domain using glob matching | ||||||
|  |    */ | ||||||
|  |   private matchDomain(pattern: string, domain: string): boolean { | ||||||
|  |     // Convert glob pattern to regex | ||||||
|  |     const regexPattern = pattern | ||||||
|  |       .replace(/\./g, '\\.')    // Escape dots | ||||||
|  |       .replace(/\*/g, '.*');    // Convert * to .* | ||||||
|  |      | ||||||
|  |     const regex = new RegExp(`^${regexPattern}$`, 'i'); | ||||||
|  |     return regex.test(domain); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Match a domain against all patterns in a route | ||||||
|  |    */ | ||||||
|  |   private matchRouteDomain(route: IRouteConfig, domain: string): boolean { | ||||||
|  |     if (!route.match.domains) { | ||||||
|  |       // If no domains specified, match all domains | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     const patterns = Array.isArray(route.match.domains)  | ||||||
|  |       ? route.match.domains  | ||||||
|  |       : [route.match.domains]; | ||||||
|  |      | ||||||
|  |     return patterns.some(pattern => this.matchDomain(pattern, domain)); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Check if a client IP is allowed by a route's security settings | ||||||
|  |    */ | ||||||
|  |   private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean { | ||||||
|  |     const security = route.action.security; | ||||||
|  |      | ||||||
|  |     if (!security) { | ||||||
|  |       return true; // No security settings means allowed | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Check blocked IPs first | ||||||
|  |     if (security.blockedIps && security.blockedIps.length > 0) { | ||||||
|  |       for (const pattern of security.blockedIps) { | ||||||
|  |         if (this.matchIpPattern(pattern, clientIp)) { | ||||||
|  |           return false; // IP is blocked | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // If there are allowed IPs, check them | ||||||
|  |     if (security.allowedIps && security.allowedIps.length > 0) { | ||||||
|  |       for (const pattern of security.allowedIps) { | ||||||
|  |         if (this.matchIpPattern(pattern, clientIp)) { | ||||||
|  |           return true; // IP is allowed | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return false; // IP not in allowed list | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // No allowed IPs specified, so IP is allowed | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Match an IP against a pattern | ||||||
|  |    */ | ||||||
|  |   private matchIpPattern(pattern: string, ip: string): boolean { | ||||||
|  |     // Handle exact match | ||||||
|  |     if (pattern === ip) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Handle CIDR notation (e.g., 192.168.1.0/24) | ||||||
|  |     if (pattern.includes('/')) { | ||||||
|  |       return this.matchIpCidr(pattern, ip); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 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); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Match an IP against a CIDR pattern | ||||||
|  |    */ | ||||||
|  |   private matchIpCidr(cidr: string, ip: string): boolean { | ||||||
|  |     try { | ||||||
|  |       // In a real implementation, you'd use a proper IP library | ||||||
|  |       // This is a simplified implementation | ||||||
|  |       const [subnet, bits] = cidr.split('/'); | ||||||
|  |       const mask = parseInt(bits, 10); | ||||||
|  |        | ||||||
|  |       // Convert IP addresses to numeric values | ||||||
|  |       const ipNum = this.ipToNumber(ip); | ||||||
|  |       const subnetNum = this.ipToNumber(subnet); | ||||||
|  |        | ||||||
|  |       // Calculate subnet mask | ||||||
|  |       const maskNum = ~(2 ** (32 - mask) - 1); | ||||||
|  |        | ||||||
|  |       // Check if IP is in subnet | ||||||
|  |       return (ipNum & maskNum) === (subnetNum & maskNum); | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(`Error matching IP ${ip} against CIDR ${cidr}:`, e); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Convert an IP address to a numeric value | ||||||
|  |    */ | ||||||
|  |   private ipToNumber(ip: string): number { | ||||||
|  |     const parts = ip.split('.').map(part => parseInt(part, 10)); | ||||||
|  |     return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Find the matching route for a connection | ||||||
|  |    */ | ||||||
|  |   public findMatchingRoute(options: { | ||||||
|  |     port: number; | ||||||
|  |     domain?: string; | ||||||
|  |     clientIp: string; | ||||||
|  |     path?: string; | ||||||
|  |     tlsVersion?: string; | ||||||
|  |   }): IRouteMatchResult | null { | ||||||
|  |     const { port, domain, clientIp, path, tlsVersion } = options; | ||||||
|  |      | ||||||
|  |     // Get all routes for this port | ||||||
|  |     const routesForPort = this.getRoutesForPort(port); | ||||||
|  |      | ||||||
|  |     // Find the first matching route based on priority order | ||||||
|  |     for (const route of routesForPort) { | ||||||
|  |       // Check domain match if specified | ||||||
|  |       if (domain && !this.matchRouteDomain(route, domain)) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Check path match if specified in both route and request | ||||||
|  |       if (path && route.match.path) { | ||||||
|  |         if (!this.matchPath(route.match.path, path)) { | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Check client IP match | ||||||
|  |       if (route.match.clientIp && !route.match.clientIp.some(pattern =>  | ||||||
|  |         this.matchIpPattern(pattern, clientIp))) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Check TLS version match | ||||||
|  |       if (tlsVersion && route.match.tlsVersion &&  | ||||||
|  |           !route.match.tlsVersion.includes(tlsVersion)) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Check security settings | ||||||
|  |       if (!this.isClientIpAllowed(route, clientIp)) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // All checks passed, this route matches | ||||||
|  |       return { route }; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Match a path against a pattern | ||||||
|  |    */ | ||||||
|  |   private matchPath(pattern: string, path: string): boolean { | ||||||
|  |     // Convert the glob pattern to a regex | ||||||
|  |     const regexPattern = pattern | ||||||
|  |       .replace(/\./g, '\\.')    // Escape dots | ||||||
|  |       .replace(/\*/g, '.*')     // Convert * to .* | ||||||
|  |       .replace(/\//g, '\\/');   // Escape slashes | ||||||
|  |      | ||||||
|  |     const regex = new RegExp(`^${regexPattern}$`); | ||||||
|  |     return regex.test(path); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Convert a domain config to routes | ||||||
|  |    * (For backward compatibility with code that still uses domainConfigs) | ||||||
|  |    */ | ||||||
|  |   public domainConfigToRoutes(domainConfig: IDomainConfig): IRouteConfig[] { | ||||||
|  |     const routes: IRouteConfig[] = []; | ||||||
|  |     const { domains, forwarding } = domainConfig; | ||||||
|  |      | ||||||
|  |     // Determine the action based on forwarding type | ||||||
|  |     let action: IRouteAction = { | ||||||
|  |       type: 'forward', | ||||||
|  |       target: { | ||||||
|  |         host: forwarding.target.host, | ||||||
|  |         port: forwarding.target.port | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     // Set TLS mode based on forwarding type | ||||||
|  |     switch (forwarding.type) { | ||||||
|  |       case 'http-only': | ||||||
|  |         // No TLS settings needed | ||||||
|  |         break; | ||||||
|  |       case 'https-passthrough': | ||||||
|  |         action.tls = { mode: 'passthrough' }; | ||||||
|  |         break; | ||||||
|  |       case 'https-terminate-to-http': | ||||||
|  |         action.tls = {  | ||||||
|  |           mode: 'terminate', | ||||||
|  |           certificate: forwarding.https?.customCert ? { | ||||||
|  |             key: forwarding.https.customCert.key, | ||||||
|  |             cert: forwarding.https.customCert.cert | ||||||
|  |           } : 'auto' | ||||||
|  |         }; | ||||||
|  |         break; | ||||||
|  |       case 'https-terminate-to-https': | ||||||
|  |         action.tls = {  | ||||||
|  |           mode: 'terminate-and-reencrypt', | ||||||
|  |           certificate: forwarding.https?.customCert ? { | ||||||
|  |             key: forwarding.https.customCert.key, | ||||||
|  |             cert: forwarding.https.customCert.cert | ||||||
|  |           } : 'auto' | ||||||
|  |         }; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Add security settings if present | ||||||
|  |     if (forwarding.security) { | ||||||
|  |       action.security = { | ||||||
|  |         allowedIps: forwarding.security.allowedIps, | ||||||
|  |         blockedIps: forwarding.security.blockedIps, | ||||||
|  |         maxConnections: forwarding.security.maxConnections | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Add advanced settings if present | ||||||
|  |     if (forwarding.advanced) { | ||||||
|  |       action.advanced = { | ||||||
|  |         timeout: forwarding.advanced.timeout, | ||||||
|  |         headers: forwarding.advanced.headers, | ||||||
|  |         keepAlive: forwarding.advanced.keepAlive | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Determine which port to use based on forwarding type | ||||||
|  |     const defaultPort = forwarding.type.startsWith('https') ? 443 : 80; | ||||||
|  |      | ||||||
|  |     // Add the main route | ||||||
|  |     routes.push({ | ||||||
|  |       match: { | ||||||
|  |         ports: defaultPort, | ||||||
|  |         domains | ||||||
|  |       }, | ||||||
|  |       action, | ||||||
|  |       name: `Route for ${domains.join(', ')}` | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // Add HTTP redirect if needed | ||||||
|  |     if (forwarding.http?.redirectToHttps) { | ||||||
|  |       routes.push({ | ||||||
|  |         match: { | ||||||
|  |           ports: 80, | ||||||
|  |           domains | ||||||
|  |         }, | ||||||
|  |         action: { | ||||||
|  |           type: 'redirect', | ||||||
|  |           redirect: { | ||||||
|  |             to: 'https://{domain}{path}', | ||||||
|  |             status: 301 | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         name: `HTTP Redirect for ${domains.join(', ')}`, | ||||||
|  |         priority: 100 // Higher priority for redirects | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Add port ranges if specified | ||||||
|  |     if (forwarding.advanced?.portRanges) { | ||||||
|  |       for (const range of forwarding.advanced.portRanges) { | ||||||
|  |         routes.push({ | ||||||
|  |           match: { | ||||||
|  |             ports: [{ from: range.from, to: range.to }], | ||||||
|  |             domains | ||||||
|  |           }, | ||||||
|  |           action, | ||||||
|  |           name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')}` | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return routes; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Update routes based on domain configs | ||||||
|  |    * (For backward compatibility with code that still uses domainConfigs) | ||||||
|  |    */ | ||||||
|  |   public updateFromDomainConfigs(domainConfigs: IDomainConfig[]): void { | ||||||
|  |     const routes: IRouteConfig[] = []; | ||||||
|  |      | ||||||
|  |     // Convert each domain config to routes | ||||||
|  |     for (const config of domainConfigs) { | ||||||
|  |       routes.push(...this.domainConfigToRoutes(config)); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Merge with existing routes that aren't derived from domain configs | ||||||
|  |     const nonDomainRoutes = this.routes.filter(r =>  | ||||||
|  |       !r.name || !r.name.includes('for ')); | ||||||
|  |      | ||||||
|  |     this.updateRoutes([...nonDomainRoutes, ...routes]); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Validate the route configuration and return any warnings | ||||||
|  |    */ | ||||||
|  |   public validateConfiguration(): string[] { | ||||||
|  |     const warnings: string[] = []; | ||||||
|  |     const duplicatePorts = new Map<number, number>(); | ||||||
|  |      | ||||||
|  |     // Check for routes with the same exact match criteria | ||||||
|  |     for (let i = 0; i < this.routes.length; i++) { | ||||||
|  |       for (let j = i + 1; j < this.routes.length; j++) { | ||||||
|  |         const route1 = this.routes[i]; | ||||||
|  |         const route2 = this.routes[j]; | ||||||
|  |          | ||||||
|  |         // Check if route match criteria are the same | ||||||
|  |         if (this.areMatchesSimilar(route1.match, route2.match)) { | ||||||
|  |           warnings.push( | ||||||
|  |             `Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` + | ||||||
|  |             `The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.` | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Check for routes that may never be matched due to priority | ||||||
|  |     for (let i = 0; i < this.routes.length; i++) { | ||||||
|  |       const route = this.routes[i]; | ||||||
|  |       const higherPriorityRoutes = this.routes.filter(r =>  | ||||||
|  |         (r.priority || 0) > (route.priority || 0)); | ||||||
|  |        | ||||||
|  |       for (const higherRoute of higherPriorityRoutes) { | ||||||
|  |         if (this.isRouteShadowed(route, higherRoute)) { | ||||||
|  |           warnings.push( | ||||||
|  |             `Route "${route.name || i}" may never be matched because it is shadowed by ` + | ||||||
|  |             `higher priority route "${higherRoute.name || 'unnamed'}"` | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return warnings; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Check if two route matches are similar (potential conflict) | ||||||
|  |    */ | ||||||
|  |   private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean { | ||||||
|  |     // Check port overlap | ||||||
|  |     const ports1 = new Set(this.expandPortRange(match1.ports)); | ||||||
|  |     const ports2 = new Set(this.expandPortRange(match2.ports)); | ||||||
|  |      | ||||||
|  |     let havePortOverlap = false; | ||||||
|  |     for (const port of ports1) { | ||||||
|  |       if (ports2.has(port)) { | ||||||
|  |         havePortOverlap = true; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     if (!havePortOverlap) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Check domain overlap | ||||||
|  |     if (match1.domains && match2.domains) { | ||||||
|  |       const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains]; | ||||||
|  |       const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains]; | ||||||
|  |        | ||||||
|  |       // Check if any domain pattern from match1 could match any from match2 | ||||||
|  |       let haveDomainOverlap = false; | ||||||
|  |       for (const domain1 of domains1) { | ||||||
|  |         for (const domain2 of domains2) { | ||||||
|  |           if (domain1 === domain2 ||  | ||||||
|  |               (domain1.includes('*') || domain2.includes('*'))) { | ||||||
|  |             haveDomainOverlap = true; | ||||||
|  |             break; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         if (haveDomainOverlap) break; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       if (!haveDomainOverlap) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } else if (match1.domains || match2.domains) { | ||||||
|  |       // One has domains, the other doesn't - they could overlap | ||||||
|  |       // The one with domains is more specific, so it's not exactly a conflict | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Check path overlap | ||||||
|  |     if (match1.path && match2.path) { | ||||||
|  |       // This is a simplified check - in a real implementation, | ||||||
|  |       // you'd need to check if the path patterns could match the same paths | ||||||
|  |       return match1.path === match2.path ||  | ||||||
|  |              match1.path.includes('*') ||  | ||||||
|  |              match2.path.includes('*'); | ||||||
|  |     } else if (match1.path || match2.path) { | ||||||
|  |       // One has a path, the other doesn't | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // If we get here, the matches have significant overlap | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Check if a route is completely shadowed by a higher priority route | ||||||
|  |    */ | ||||||
|  |   private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean { | ||||||
|  |     // If they don't have similar match criteria, no shadowing occurs | ||||||
|  |     if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // If higher priority route has more specific criteria, no shadowing | ||||||
|  |     if (this.isRouteMoreSpecific(higherPriorityRoute.match, route.match)) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // If higher priority route is equally or less specific but has higher priority, | ||||||
|  |     // it shadows the lower priority route | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Check if route1 is more specific than route2 | ||||||
|  |    */ | ||||||
|  |   private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean { | ||||||
|  |     // Check if match1 has more specific criteria | ||||||
|  |     let match1Points = 0; | ||||||
|  |     let match2Points = 0; | ||||||
|  |      | ||||||
|  |     // Path is the most specific | ||||||
|  |     if (match1.path) match1Points += 3; | ||||||
|  |     if (match2.path) match2Points += 3; | ||||||
|  |      | ||||||
|  |     // Domain is next most specific | ||||||
|  |     if (match1.domains) match1Points += 2; | ||||||
|  |     if (match2.domains) match2Points += 2; | ||||||
|  |      | ||||||
|  |     // Client IP and TLS version are least specific | ||||||
|  |     if (match1.clientIp) match1Points += 1; | ||||||
|  |     if (match2.clientIp) match2Points += 1; | ||||||
|  |      | ||||||
|  |     if (match1.tlsVersion) match1Points += 1; | ||||||
|  |     if (match2.tlsVersion) match2Points += 1; | ||||||
|  |      | ||||||
|  |     return match1Points > match2Points; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| import * as plugins from '../../plugins.js'; | import * as plugins from '../../plugins.js'; | ||||||
|  |  | ||||||
| // Importing from the new structure | // Importing required components | ||||||
| import { ConnectionManager } from './connection-manager.js'; | import { ConnectionManager } from './connection-manager.js'; | ||||||
| import { SecurityManager } from './security-manager.js'; | import { SecurityManager } from './security-manager.js'; | ||||||
| import { DomainConfigManager } from './domain-config-manager.js'; | import { DomainConfigManager } from './domain-config-manager.js'; | ||||||
| @@ -8,23 +8,27 @@ import { TlsManager } from './tls-manager.js'; | |||||||
| import { NetworkProxyBridge } from './network-proxy-bridge.js'; | import { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||||
| import { TimeoutManager } from './timeout-manager.js'; | import { TimeoutManager } from './timeout-manager.js'; | ||||||
| import { PortRangeManager } from './port-range-manager.js'; | import { PortRangeManager } from './port-range-manager.js'; | ||||||
| import { ConnectionHandler } from './connection-handler.js'; | import { RouteManager } from './route-manager.js'; | ||||||
|  | import { RouteConnectionHandler } from './route-connection-handler.js'; | ||||||
|  |  | ||||||
| // External dependencies from migrated modules | // External dependencies | ||||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; | import { Port80Handler } from '../../http/port80/port80-handler.js'; | ||||||
| import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; | import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; | ||||||
| import type { ICertificateData } from '../../certificate/models/certificate-types.js'; | import type { ICertificateData } from '../../certificate/models/certificate-types.js'; | ||||||
| import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; | import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; | ||||||
| import type { TForwardingType } from '../../forwarding/config/forwarding-types.js'; |  | ||||||
| import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; | import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; | ||||||
|  |  | ||||||
| // Import types from models | // Import types and utilities | ||||||
| import type { ISmartProxyOptions, IDomainConfig } from './models/interfaces.js'; | import type {  | ||||||
| // Provide backward compatibility types |   ISmartProxyOptions,  | ||||||
| export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig }; |   IRoutedSmartProxyOptions, | ||||||
|  |   IDomainConfig | ||||||
|  | } from './models/interfaces.js'; | ||||||
|  | import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js'; | ||||||
|  | import type { IRouteConfig } from './models/route-types.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * SmartProxy - Main class that coordinates all components |  * SmartProxy - Unified route-based API | ||||||
|  */ |  */ | ||||||
| export class SmartProxy extends plugins.EventEmitter { | export class SmartProxy extends plugins.EventEmitter { | ||||||
|   private netServers: plugins.net.Server[] = []; |   private netServers: plugins.net.Server[] = []; | ||||||
| @@ -34,24 +38,28 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|   // Component managers |   // Component managers | ||||||
|   private connectionManager: ConnectionManager; |   private connectionManager: ConnectionManager; | ||||||
|   private securityManager: SecurityManager; |   private securityManager: SecurityManager; | ||||||
|   public domainConfigManager: DomainConfigManager; |   private domainConfigManager: DomainConfigManager; | ||||||
|   private tlsManager: TlsManager; |   private tlsManager: TlsManager; | ||||||
|   private networkProxyBridge: NetworkProxyBridge; |   private networkProxyBridge: NetworkProxyBridge; | ||||||
|   private timeoutManager: TimeoutManager; |   private timeoutManager: TimeoutManager; | ||||||
|   private portRangeManager: PortRangeManager; |   private portRangeManager: PortRangeManager; | ||||||
|   private connectionHandler: ConnectionHandler; |   private routeManager: RouteManager; | ||||||
|  |   private routeConnectionHandler: RouteConnectionHandler; | ||||||
|    |    | ||||||
|   // Port80Handler for ACME certificate management |   // Port80Handler for ACME certificate management | ||||||
|   private port80Handler: Port80Handler | null = null; |   private port80Handler: Port80Handler | null = null; | ||||||
|   // CertProvisioner for unified certificate workflows |   // CertProvisioner for unified certificate workflows | ||||||
|   private certProvisioner?: CertProvisioner; |   private certProvisioner?: CertProvisioner; | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Constructor that supports both legacy and route-based configuration | ||||||
|  |    */ | ||||||
|   constructor(settingsArg: ISmartProxyOptions) { |   constructor(settingsArg: ISmartProxyOptions) { | ||||||
|     super(); |     super(); | ||||||
|  |      | ||||||
|     // Set reasonable defaults for all settings |     // Set reasonable defaults for all settings | ||||||
|     this.settings = { |     this.settings = { | ||||||
|       ...settingsArg, |       ...settingsArg, | ||||||
|       targetIP: settingsArg.targetIP || 'localhost', |  | ||||||
|       initialDataTimeout: settingsArg.initialDataTimeout || 120000, |       initialDataTimeout: settingsArg.initialDataTimeout || 120000, | ||||||
|       socketTimeout: settingsArg.socketTimeout || 3600000, |       socketTimeout: settingsArg.socketTimeout || 3600000, | ||||||
|       inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, |       inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, | ||||||
| @@ -76,12 +84,11 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, |       keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, | ||||||
|       extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, |       extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, | ||||||
|       networkProxyPort: settingsArg.networkProxyPort || 8443, |       networkProxyPort: settingsArg.networkProxyPort || 8443, | ||||||
|       acme: settingsArg.acme || {}, |  | ||||||
|       globalPortRanges: settingsArg.globalPortRanges || [], |  | ||||||
|     }; |     }; | ||||||
|      |      | ||||||
|     // Set default ACME options if not provided |     // Set default ACME options if not provided | ||||||
|     if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) { |     this.settings.acme = this.settings.acme || {}; | ||||||
|  |     if (Object.keys(this.settings.acme).length === 0) { | ||||||
|       this.settings.acme = { |       this.settings.acme = { | ||||||
|         enabled: false, |         enabled: false, | ||||||
|         port: 80, |         port: 80, | ||||||
| @@ -91,7 +98,7 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|         autoRenew: true, |         autoRenew: true, | ||||||
|         certificateStore: './certs', |         certificateStore: './certs', | ||||||
|         skipConfiguredCerts: false, |         skipConfiguredCerts: false, | ||||||
|         httpsRedirectPort: this.settings.fromPort, |         httpsRedirectPort: this.settings.fromPort || 443, | ||||||
|         renewCheckIntervalHours: 24, |         renewCheckIntervalHours: 24, | ||||||
|         domainForwards: [] |         domainForwards: [] | ||||||
|       }; |       }; | ||||||
| @@ -105,13 +112,20 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       this.securityManager,  |       this.securityManager,  | ||||||
|       this.timeoutManager |       this.timeoutManager | ||||||
|     ); |     ); | ||||||
|  |      | ||||||
|  |     // Create domain config manager and port range manager (for backward compatibility) | ||||||
|     this.domainConfigManager = new DomainConfigManager(this.settings); |     this.domainConfigManager = new DomainConfigManager(this.settings); | ||||||
|     this.tlsManager = new TlsManager(this.settings); |  | ||||||
|     this.networkProxyBridge = new NetworkProxyBridge(this.settings); |  | ||||||
|     this.portRangeManager = new PortRangeManager(this.settings); |     this.portRangeManager = new PortRangeManager(this.settings); | ||||||
|      |      | ||||||
|     // Initialize connection handler |     // Create the new route manager | ||||||
|     this.connectionHandler = new ConnectionHandler( |     this.routeManager = new RouteManager(this.settings); | ||||||
|  |      | ||||||
|  |     // Create other required components | ||||||
|  |     this.tlsManager = new TlsManager(this.settings); | ||||||
|  |     this.networkProxyBridge = new NetworkProxyBridge(this.settings); | ||||||
|  |      | ||||||
|  |     // Initialize connection handler with route support | ||||||
|  |     this.routeConnectionHandler = new RouteConnectionHandler( | ||||||
|       this.settings, |       this.settings, | ||||||
|       this.connectionManager, |       this.connectionManager, | ||||||
|       this.securityManager, |       this.securityManager, | ||||||
| @@ -119,12 +133,12 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       this.tlsManager, |       this.tlsManager, | ||||||
|       this.networkProxyBridge, |       this.networkProxyBridge, | ||||||
|       this.timeoutManager, |       this.timeoutManager, | ||||||
|       this.portRangeManager |       this.routeManager | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * The settings for the port proxy |    * The settings for the SmartProxy | ||||||
|    */ |    */ | ||||||
|   public settings: ISmartProxyOptions; |   public settings: ISmartProxyOptions; | ||||||
|    |    | ||||||
| @@ -142,8 +156,9 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       // Build and start the Port80Handler |       // Build and start the Port80Handler | ||||||
|       this.port80Handler = buildPort80Handler({ |       this.port80Handler = buildPort80Handler({ | ||||||
|         ...config, |         ...config, | ||||||
|         httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort |         httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443) | ||||||
|       }); |       }); | ||||||
|  |        | ||||||
|       // Share Port80Handler with NetworkProxyBridge before start |       // Share Port80Handler with NetworkProxyBridge before start | ||||||
|       this.networkProxyBridge.setPort80Handler(this.port80Handler); |       this.networkProxyBridge.setPort80Handler(this.port80Handler); | ||||||
|       await this.port80Handler.start(); |       await this.port80Handler.start(); | ||||||
| @@ -154,7 +169,7 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Start the proxy server |    * Start the proxy server with support for both configuration types | ||||||
|    */ |    */ | ||||||
|   public async start() { |   public async start() { | ||||||
|     // Don't start if already shutting down |     // Don't start if already shutting down | ||||||
| @@ -163,11 +178,11 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Process domain configs |     // If using legacy format, make sure domainConfigs are initialized | ||||||
|     // Note: ensureForwardingConfig is no longer needed since forwarding is now required |     if (isLegacyOptions(this.settings)) { | ||||||
|  |       // Initialize domain config manager with the processed configs | ||||||
|     // Initialize domain config manager with the processed configs |       this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); | ||||||
|     this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); |     } | ||||||
|  |  | ||||||
|     // Initialize Port80Handler if enabled |     // Initialize Port80Handler if enabled | ||||||
|     await this.initializePort80Handler(); |     await this.initializePort80Handler(); | ||||||
| @@ -176,20 +191,39 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|     if (this.port80Handler) { |     if (this.port80Handler) { | ||||||
|       const acme = this.settings.acme!; |       const acme = this.settings.acme!; | ||||||
|  |  | ||||||
|       // Convert domain forwards to use the new forwarding system if possible |       // Setup domain forwards based on configuration type | ||||||
|       const domainForwards = acme.domainForwards?.map(f => { |       const domainForwards = acme.domainForwards?.map(f => { | ||||||
|         // If the domain has a forwarding config in domainConfigs, use that |         if (isLegacyOptions(this.settings)) { | ||||||
|         const domainConfig = this.settings.domainConfigs.find( |           // If using legacy mode, check if domain config exists | ||||||
|           dc => dc.domains.some(d => d === f.domain) |           const domainConfig = this.settings.domainConfigs.find( | ||||||
|         ); |             dc => dc.domains.some(d => d === f.domain) | ||||||
|  |           ); | ||||||
|  |  | ||||||
|         if (domainConfig?.forwarding) { |           if (domainConfig?.forwarding) { | ||||||
|           return { |             return { | ||||||
|  |               domain: f.domain, | ||||||
|  |               forwardConfig: f.forwardConfig, | ||||||
|  |               acmeForwardConfig: f.acmeForwardConfig, | ||||||
|  |               sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           // In route mode, look for matching route | ||||||
|  |           const route = this.routeManager.findMatchingRoute({ | ||||||
|  |             port: 443, | ||||||
|             domain: f.domain, |             domain: f.domain, | ||||||
|             forwardConfig: f.forwardConfig, |             clientIp: '127.0.0.1' // Dummy IP for finding routes | ||||||
|             acmeForwardConfig: f.acmeForwardConfig, |           })?.route; | ||||||
|             sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false |  | ||||||
|           }; |           if (route && route.action.type === 'forward' && route.action.tls) { | ||||||
|  |             // If we found a matching route with TLS settings | ||||||
|  |             return { | ||||||
|  |               domain: f.domain, | ||||||
|  |               forwardConfig: f.forwardConfig, | ||||||
|  |               acmeForwardConfig: f.acmeForwardConfig, | ||||||
|  |               sslRedirect: f.sslRedirect || false | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // Otherwise use the existing configuration |         // Otherwise use the existing configuration | ||||||
| @@ -201,17 +235,38 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|         }; |         }; | ||||||
|       }) || []; |       }) || []; | ||||||
|  |  | ||||||
|       this.certProvisioner = new CertProvisioner( |       // Create CertProvisioner with appropriate parameters | ||||||
|         this.settings.domainConfigs, |       if (isLegacyOptions(this.settings)) { | ||||||
|         this.port80Handler, |         this.certProvisioner = new CertProvisioner( | ||||||
|         this.networkProxyBridge, |           this.settings.domainConfigs, | ||||||
|         this.settings.certProvisionFunction, |           this.port80Handler, | ||||||
|         acme.renewThresholdDays!, |           this.networkProxyBridge, | ||||||
|         acme.renewCheckIntervalHours!, |           this.settings.certProvisionFunction, | ||||||
|         acme.autoRenew!, |           acme.renewThresholdDays!, | ||||||
|         domainForwards |           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 | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Register certificate event handler | ||||||
|       this.certProvisioner.on('certificate', (certData) => { |       this.certProvisioner.on('certificate', (certData) => { | ||||||
|         this.emit('certificate', { |         this.emit('certificate', { | ||||||
|           domain: certData.domain, |           domain: certData.domain, | ||||||
| @@ -228,25 +283,22 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Initialize and start NetworkProxy if needed |     // Initialize and start NetworkProxy if needed | ||||||
|     if ( |     if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { | ||||||
|       this.settings.useNetworkProxy && |  | ||||||
|       this.settings.useNetworkProxy.length > 0 |  | ||||||
|     ) { |  | ||||||
|       await this.networkProxyBridge.initialize(); |       await this.networkProxyBridge.initialize(); | ||||||
|       await this.networkProxyBridge.start(); |       await this.networkProxyBridge.start(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Validate port configuration |     // Validate the route configuration | ||||||
|     const configWarnings = this.portRangeManager.validateConfiguration(); |     const configWarnings = this.routeManager.validateConfiguration(); | ||||||
|     if (configWarnings.length > 0) { |     if (configWarnings.length > 0) { | ||||||
|       console.log("Port configuration warnings:"); |       console.log("Route configuration warnings:"); | ||||||
|       for (const warning of configWarnings) { |       for (const warning of configWarnings) { | ||||||
|         console.log(` - ${warning}`); |         console.log(` - ${warning}`); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Get listening ports from PortRangeManager |     // Get listening ports from RouteManager | ||||||
|     const listeningPorts = this.portRangeManager.getListeningPorts(); |     const listeningPorts = this.routeManager.getListeningPorts(); | ||||||
|  |  | ||||||
|     // Create servers for each port |     // Create servers for each port | ||||||
|     for (const port of listeningPorts) { |     for (const port of listeningPorts) { | ||||||
| @@ -258,8 +310,8 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Delegate to connection handler |         // Delegate to route connection handler | ||||||
|         this.connectionHandler.handleConnection(socket); |         this.routeConnectionHandler.handleConnection(socket); | ||||||
|       }).on('error', (err: Error) => { |       }).on('error', (err: Error) => { | ||||||
|         console.log(`Server Error on port ${port}: ${err.message}`); |         console.log(`Server Error on port ${port}: ${err.message}`); | ||||||
|       }); |       }); | ||||||
| @@ -268,7 +320,9 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|         const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); |         const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); | ||||||
|         console.log( |         console.log( | ||||||
|           `SmartProxy -> OK: Now listening on port ${port}${ |           `SmartProxy -> OK: Now listening on port ${port}${ | ||||||
|             this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' |             isLegacyOptions(this.settings) && this.settings.sniEnabled && !isNetworkProxyPort ?  | ||||||
|  |               ' (SNI passthrough enabled)' :  | ||||||
|  |               '' | ||||||
|           }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` |           }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` | ||||||
|         ); |         ); | ||||||
|       }); |       }); | ||||||
| @@ -348,12 +402,70 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Extract domain configurations from routes for certificate provisioning | ||||||
|  |    */ | ||||||
|  |   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 |    * Stop the proxy server | ||||||
|    */ |    */ | ||||||
|   public async stop() { |   public async stop() { | ||||||
|     console.log('SmartProxy shutting down...'); |     console.log('SmartProxy shutting down...'); | ||||||
|     this.isShuttingDown = true; |     this.isShuttingDown = true; | ||||||
|  |      | ||||||
|     // Stop CertProvisioner if active |     // Stop CertProvisioner if active | ||||||
|     if (this.certProvisioner) { |     if (this.certProvisioner) { | ||||||
|       await this.certProvisioner.stop(); |       await this.certProvisioner.stop(); | ||||||
| @@ -411,14 +523,17 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Updates the domain configurations for the proxy |    * Updates the domain configurations for the proxy (legacy support) | ||||||
|    */ |    */ | ||||||
|   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { |   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { | ||||||
|     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); |     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); | ||||||
|  |  | ||||||
|     // Update domain configs in DomainConfigManager |     // Update domain configs in DomainConfigManager (legacy) | ||||||
|     this.domainConfigManager.updateDomainConfigs(newDomainConfigs); |     this.domainConfigManager.updateDomainConfigs(newDomainConfigs); | ||||||
|  |  | ||||||
|  |     // Also update the RouteManager with these domain configs | ||||||
|  |     this.routeManager.updateFromDomainConfigs(newDomainConfigs); | ||||||
|  |  | ||||||
|     // If NetworkProxy is initialized, resync the configurations |     // If NetworkProxy is initialized, resync the configurations | ||||||
|     if (this.networkProxyBridge.getNetworkProxy()) { |     if (this.networkProxyBridge.getNetworkProxy()) { | ||||||
|       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); |       await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); | ||||||
| @@ -428,7 +543,7 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|     if (this.port80Handler && this.settings.acme?.enabled) { |     if (this.port80Handler && this.settings.acme?.enabled) { | ||||||
|       for (const domainConfig of newDomainConfigs) { |       for (const domainConfig of newDomainConfigs) { | ||||||
|         // Skip certificate provisioning for http-only or passthrough configs that don't need certs |         // Skip certificate provisioning for http-only or passthrough configs that don't need certs | ||||||
|         const forwardingType = domainConfig.forwarding.type; |         const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); | ||||||
|         const needsCertificate = |         const needsCertificate = | ||||||
|           forwardingType === 'https-terminate-to-http' || |           forwardingType === 'https-terminate-to-http' || | ||||||
|           forwardingType === 'https-terminate-to-https'; |           forwardingType === 'https-terminate-to-https'; | ||||||
| @@ -490,6 +605,95 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Update routes with new configuration (new API) | ||||||
|  |    */ | ||||||
|  |   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(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 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' ||  | ||||||
|  |             !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  | ||||||
|  |           : [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); | ||||||
|  |             } 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; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 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) | ||||||
|  |             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 routes'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Request a certificate for a specific domain |    * Request a certificate for a specific domain | ||||||
| @@ -583,7 +787,8 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       networkProxyConnections, |       networkProxyConnections, | ||||||
|       terminationStats, |       terminationStats, | ||||||
|       acmeEnabled: !!this.port80Handler, |       acmeEnabled: !!this.port80Handler, | ||||||
|       port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null |       port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null, | ||||||
|  |       routes: this.routeManager.getListeningPorts().length | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|    |    | ||||||
| @@ -591,18 +796,44 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|    * Get a list of eligible domains for ACME certificates |    * Get a list of eligible domains for ACME certificates | ||||||
|    */ |    */ | ||||||
|   public getEligibleDomainsForCertificates(): string[] { |   public getEligibleDomainsForCertificates(): string[] { | ||||||
|     // Collect all non-wildcard domains from domain configs |  | ||||||
|     const domains: string[] = []; |     const domains: string[] = []; | ||||||
|      |      | ||||||
|     for (const config of this.settings.domainConfigs) { |     // Get domains from routes | ||||||
|  |     const routes = isRoutedOptions(this.settings) ? this.settings.routes : []; | ||||||
|  |      | ||||||
|  |     for (const route of routes) { | ||||||
|  |       if (!route.match.domains) continue; | ||||||
|  |        | ||||||
|  |       // Skip routes without TLS termination or auto certificates | ||||||
|  |       if (route.action.type !== 'forward' ||  | ||||||
|  |           !route.action.tls ||  | ||||||
|  |           route.action.tls.mode === 'passthrough' ||  | ||||||
|  |           route.action.tls.certificate !== 'auto') continue; | ||||||
|  |        | ||||||
|  |       const routeDomains = Array.isArray(route.match.domains)  | ||||||
|  |         ? route.match.domains  | ||||||
|  |         : [route.match.domains]; | ||||||
|  |        | ||||||
|       // Skip domains that can't be used with ACME |       // Skip domains that can't be used with ACME | ||||||
|       const eligibleDomains = config.domains.filter(domain =>  |       const eligibleDomains = routeDomains.filter(domain =>  | ||||||
|         !domain.includes('*') && this.isValidDomain(domain) |         !domain.includes('*') && this.isValidDomain(domain) | ||||||
|       ); |       ); | ||||||
|        |        | ||||||
|       domains.push(...eligibleDomains); |       domains.push(...eligibleDomains); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     // For legacy mode, also get domains from domain configs | ||||||
|  |     if (isLegacyOptions(this.settings)) { | ||||||
|  |       for (const config of this.settings.domainConfigs) { | ||||||
|  |         // Skip domains that can't be used with ACME | ||||||
|  |         const eligibleDomains = config.domains.filter(domain =>  | ||||||
|  |           !domain.includes('*') && this.isValidDomain(domain) | ||||||
|  |         ); | ||||||
|  |          | ||||||
|  |         domains.push(...eligibleDomains); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|     return domains; |     return domains; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user