fix(network-proxy, route-utils, route-manager): Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports.
This commit is contained in:
		| @@ -1,5 +1,14 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-05-14 - 16.0.3 - fix(network-proxy, route-utils, route-manager) | ||||||
|  | Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports. | ||||||
|  |  | ||||||
|  | - Updated matchIpPattern and matchIpCidr to normalize IPv6-mapped IPv4 addresses. | ||||||
|  | - Replaced legacy 'domain' field references with 'domains' in route configurations. | ||||||
|  | - Removed deprecated methods for converting legacy proxy configs and legacy route helpers. | ||||||
|  | - Adjusted test cases (event system, route utils, network proxy function targets) to use modern interfaces. | ||||||
|  | - Improved logging and error messages in route-manager and route-utils for better debugging. | ||||||
|  |  | ||||||
| ## 2025-05-10 - 16.0.2 - fix(test/certificate-provisioning) | ## 2025-05-10 - 16.0.2 - fix(test/certificate-provisioning) | ||||||
| Update certificate provisioning tests with updated port mapping and ACME options; use accountEmail instead of contactEmail, adjust auto-api route creation to use HTTPS terminate helper, and refine expectations for wildcard passthrough domains. | Update certificate provisioning tests with updated port mapping and ACME options; use accountEmail instead of contactEmail, adjust auto-api route creation to use HTTPS terminate helper, and refine expectations for wildcard passthrough domains. | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										192
									
								
								readme.plan.md
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								readme.plan.md
									
									
									
									
									
								
							| @@ -1,139 +1,103 @@ | |||||||
| # Enhanced NetworkProxy with Native Route-Based Configuration | # SmartProxy Configuration Troubleshooting | ||||||
|  |  | ||||||
| ## Project Goal | ## IPv6/IPv4 Mapping Issue | ||||||
| Transform NetworkProxy to natively use route-based configurations (`IRouteConfig`) as its primary configuration format, completely eliminating translation layers while maintaining backward compatibility through adapter methods for existing code. |  | ||||||
|  |  | ||||||
| ## Current Status | ### Problem Identified | ||||||
|  | The SmartProxy is failing to match connections for wildcard domains (like `*.lossless.digital`) when IP restrictions are in place. After extensive debugging, the root cause has been identified: | ||||||
|  |  | ||||||
| The current implementation uses: | When a connection comes in from an IPv4 address (e.g., `212.95.99.130`), the Node.js server receives it as an IPv6-mapped IPv4 address with the format `::ffff:212.95.99.130`. However, the route configuration is expecting the exact string `212.95.99.130`, causing a mismatch. | ||||||
| - SmartProxy has a rich `IRouteConfig` format with match/action pattern |  | ||||||
| - NetworkProxy uses a simpler `IReverseProxyConfig` focused on hostname and destination |  | ||||||
| - `NetworkProxyBridge` translates between these formats, losing information |  | ||||||
| - Dynamic function-based hosts and ports aren't supported in NetworkProxy |  | ||||||
| - Duplicate configuration logic exists across components |  | ||||||
|  |  | ||||||
| ## Planned Enhancements | From the debug logs: | ||||||
|  | ``` | ||||||
|  | [DEBUG] Route rejected: clientIp mismatch. Request: ::ffff:212.95.99.130, Route patterns: ["212.95.99.130"] | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### Phase 1: Convert NetworkProxy to Native Route Configuration | ### Solution | ||||||
| - [x] 1.1 Refactor NetworkProxy to use `IRouteConfig` as its primary internal format |  | ||||||
| - [x] 1.3 Update all internal processing to work directly with route configs |  | ||||||
| - [x] 1.4 Add a type-safe context object matching SmartProxy's |  | ||||||
| - [x] 1.5 Ensure backward compatibility for all existing NetworkProxy methods |  | ||||||
| - [x] 1.6 Remove `IReverseProxyConfig` usage in NetworkProxy |  | ||||||
|  |  | ||||||
| ### Phase 2: Native Route Configuration Processing | To fix this issue, update the route configurations to include both formats of the IP address. Here's how to modify the affected route: | ||||||
| - [x] 2.1 Make `updateRouteConfigs(routes: IRouteConfig[])` the primary configuration method |  | ||||||
| - [x] 2.3 Implement a full RouteManager in NetworkProxy (reusing code from SmartProxy if possible) |  | ||||||
| - [x] 2.4 Support all route matching criteria (domains, paths, headers, clientIp) |  | ||||||
| - [x] 2.5 Handle priority-based route matching and conflict resolution |  | ||||||
| - [x] 2.6 Update certificate management to work with routes directly |  | ||||||
|  |  | ||||||
| ### Phase 3: Simplify NetworkProxyBridge | ```typescript | ||||||
| - [x] 3.1 Update NetworkProxyBridge to directly pass route configs to NetworkProxy | // Wildcard domain route for *.lossless.digital | ||||||
| - [x] 3.2 Remove all translation/conversion logic in the bridge | { | ||||||
| - [x] 3.3 Simplify domain registration from routes to Port80Handler |   match: { | ||||||
| - [x] 3.4 Make the bridge a lightweight pass-through component |     ports: 443, | ||||||
| - [x] 3.5 Add comprehensive logging for route synchronization |     domains: ['*.lossless.digital'], | ||||||
| - [x] 3.6 Streamline certificate handling between components |     clientIp: ['212.95.99.130', '::ffff:212.95.99.130'],  // Include both formats | ||||||
|  |   }, | ||||||
|  |   action: { | ||||||
|  |     type: 'forward', | ||||||
|  |     target: { | ||||||
|  |       host: '212.95.99.130', | ||||||
|  |       port: 443 | ||||||
|  |     }, | ||||||
|  |     tls: { | ||||||
|  |       mode: 'passthrough' | ||||||
|  |     }, | ||||||
|  |     security: { | ||||||
|  |       allowedIps: ['212.95.99.130', '::ffff:212.95.99.130']  // Include both formats | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   name: 'Wildcard lossless.digital route (IP restricted)' | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### Phase 4: Native Function-Based Target Support | ### Alternative Long-Term Fix | ||||||
| - [x] 4.1 Implement IRouteContext creation in NetworkProxy's request handler |  | ||||||
| - [x] 4.2 Add direct support for function-based host evaluation |  | ||||||
| - [x] 4.3 Add direct support for function-based port evaluation |  | ||||||
| - [x] 4.4 Implement caching for function results to improve performance |  | ||||||
| - [x] 4.5 Add comprehensive error handling for function execution |  | ||||||
| - [x] 4.6 Share context object implementation with SmartProxy |  | ||||||
|  |  | ||||||
| ### Phase 5: Enhanced HTTP Features Using Route Logic | A more robust solution would be to modify the SmartProxy codebase to automatically handle IPv6-mapped IPv4 addresses by normalizing them before comparison. This would involve: | ||||||
| - [x] 5.1 Implement full route-based header manipulation |  | ||||||
| - [x] 5.2 Add support for URL rewriting using route context |  | ||||||
| - [x] 5.3 Support template variable resolution for strings |  | ||||||
| - [x] 5.4 Implement route security features (IP filtering, rate limiting) |  | ||||||
| - [x] 5.5 Add context-aware CORS handling |  | ||||||
| - [x] 5.6 Enable route-based WebSocket upgrades |  | ||||||
|  |  | ||||||
| ### Phase 6: Testing, Documentation and Code Sharing | 1. Modifying the `matchIpPattern` function in `route-manager.ts` to normalize IPv6-mapped IPv4 addresses: | ||||||
| - [x] 6.1 Create comprehensive tests for native route configuration |  | ||||||
| - [x] 6.2 Add specific tests for function-based targets |  | ||||||
| - [x] 6.3 Document NetworkProxy's native route capabilities |  | ||||||
| - [x] 6.4 Create shared utilities between SmartProxy and NetworkProxy |  | ||||||
| - [x] 6.5 Provide migration guide for direct NetworkProxy users |  | ||||||
| - [ ] 6.6 Benchmark performance improvements |  | ||||||
|  |  | ||||||
| ### Phase 7: Unify Component Architecture | ```typescript | ||||||
| - [x] 7.1 Implement a shared RouteManager used by both SmartProxy and NetworkProxy | private matchIpPattern(pattern: string, ip: string): boolean { | ||||||
| - [x] 7.2 Extract common route matching logic to a shared utility module |   // Normalize IPv6-mapped IPv4 addresses | ||||||
| - [x] 7.3 Consolidate duplicate security management code |   const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||||
| - [x] 7.4 Remove all legacy NetworkProxyBridge conversion code |   const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern; | ||||||
| - [x] 7.5 Make the NetworkProxyBridge a pure proxy pass-through component |    | ||||||
| - [x] 7.6 Standardize event naming and handling across components |   // Handle exact match with normalized addresses | ||||||
|  |   if (normalizedPattern === normalizedIp) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   // Rest of the existing function... | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### Phase 8: Certificate Management Consolidation | 2. Making similar modifications to other IP-related functions in the codebase. | ||||||
| - [x] 8.1 Create a unified CertificateManager component |  | ||||||
| - [x] 8.2 Centralize certificate storage and renewal logic |  | ||||||
| - [x] 8.3 Simplify ACME challenge handling across proxies |  | ||||||
| - [x] 8.4 Implement shared certificate events for all components |  | ||||||
| - [x] 8.5 Remove redundant certificate synchronization logic |  | ||||||
| - [x] 8.6 Standardize SNI handling between different proxies |  | ||||||
|  |  | ||||||
| ### Phase 9: Context and Configuration Standardization | ## Wild Card Domain Matching Issue | ||||||
| - [x] 9.1 Implement a single shared IRouteContext class |  | ||||||
| - [x] 9.2 Remove all duplicate context creation logic |  | ||||||
| - [x] 9.3 Standardize option interfaces across components |  | ||||||
| - [x] 9.4 Create shared default configurations |  | ||||||
| - [x] 9.5 Implement a unified configuration validation system |  | ||||||
| - [x] 9.6 Add runtime type checking for configurations |  | ||||||
|  |  | ||||||
| ### Phase 10: Component Consolidation | ### Explanation | ||||||
| - [x] 10.1 Merge SmartProxy and NetworkProxy functionality where appropriate |  | ||||||
| - [x] 10.2 Create a unified connection pool management system |  | ||||||
| - [x] 10.3 Standardize timeout handling across components |  | ||||||
| - [x] 10.4 Implement shared logging and monitoring |  | ||||||
| - [x] 10.5 Remove all deprecated methods and legacy compatibility |  | ||||||
| - [x] 10.6 Reduce API surface area to essentials |  | ||||||
|  |  | ||||||
| ### Phase 11: Performance Optimization & Advanced Features | The wildcard domain matching in SmartProxy works as follows: | ||||||
| - [ ] 11.1 Conduct comprehensive performance benchmarking |  | ||||||
| - [ ] 11.2 Optimize memory usage in high-connection scenarios |  | ||||||
| - [ ] 11.3 Implement connection pooling for backend targets |  | ||||||
| - [ ] 11.4 Add support for HTTP/3 and QUIC protocols |  | ||||||
| - [ ] 11.5 Enhance WebSocket support with compression and multiplexing |  | ||||||
| - [ ] 11.6 Add advanced observability through metrics and tracing integration |  | ||||||
|  |  | ||||||
| ## Benefits of Simplified Architecture | 1. When a pattern like `*.lossless.digital` is specified, it's converted to a regex: `/^.*\.lossless\.digital$/i` | ||||||
|  | 2. This correctly matches any subdomain like `my.lossless.digital`, `api.lossless.digital`, etc. | ||||||
|  | 3. However, it does NOT match the apex domain `lossless.digital` (without a subdomain) | ||||||
|  |  | ||||||
| 1. **Reduced Duplication**: | If you need to match both the apex domain and subdomains, use a list: | ||||||
|    - Shared route processing logic | ```typescript | ||||||
|    - Single certificate management system | domains: ['lossless.digital', '*.lossless.digital'] | ||||||
|    - Unified context objects | ``` | ||||||
|  |  | ||||||
| 2. **Simplified Codebase**: | ## Debugging SmartProxy | ||||||
|    - Fewer managers with cleaner responsibilities |  | ||||||
|    - Consistent APIs across components |  | ||||||
|    - Reduced complexity in bridge components |  | ||||||
|  |  | ||||||
| 3. **Improved Maintainability**: | To debug routing issues in SmartProxy: | ||||||
|    - Easier to understand component relationships |  | ||||||
|    - Consolidated logic for critical operations |  | ||||||
|    - Clearer separation of concerns |  | ||||||
|  |  | ||||||
| 4. **Enhanced Performance**: | 1. Add detailed logging to the `route-manager.js` file in the `dist_ts` directory: | ||||||
|    - Less overhead in communication between components |    - `findMatchingRoute` method - to see what criteria are being checked | ||||||
|    - Reduced memory usage through shared objects |    - `matchRouteDomain` method - to see domain matching logic | ||||||
|    - More efficient request processing |    - `matchDomain` method - to see pattern matching | ||||||
|  |    - `matchIpPattern` method - to see IP matching logic | ||||||
|  |  | ||||||
| 5. **Better Developer Experience**: | 2. Run the proxy with debugging enabled: | ||||||
|    - Consistent conceptual model across system |    ``` | ||||||
|    - More intuitive configuration interface |    pnpm run startNew | ||||||
|    - Simplified debugging and troubleshooting |    ``` | ||||||
|  |  | ||||||
| ## Implementation Approach | 3. Monitor the logs for detailed information about the routing process and identify where matches are failing. | ||||||
|  |  | ||||||
| The implementation of phases 7-10 will focus on gradually consolidating duplicate functionality: | ## Priority and Route Order | ||||||
|  |  | ||||||
| 1. First, implement shared managers and utilities to be used by both proxies | Remember that routes are evaluated in priority order (higher priority first). If multiple routes could match the same request, ensure that the more specific routes have higher priority. | ||||||
| 2. Then consolidate certificate management to simplify ACME handling |  | ||||||
| 3. Create standardized context objects and configurations |  | ||||||
| 4. Finally, merge overlapping functionality between proxy components |  | ||||||
|  |  | ||||||
| This approach will maintain compatibility with existing code while progressively simplifying the architecture to reduce complexity and improve performance. | When routes have the same priority (or none specified), they're evaluated in the order they're defined in the configuration. | ||||||
| @@ -1,202 +1,207 @@ | |||||||
| import { expect } from '@push.rocks/tapbundle'; | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
| import { | import { | ||||||
|   EventSystem, |   EventSystem, | ||||||
|   ProxyEvents, |   ProxyEvents, | ||||||
|   ComponentType |   ComponentType | ||||||
| } from '../../../ts/core/utils/event-system.js'; | } from '../../../ts/core/utils/event-system.js'; | ||||||
|  |  | ||||||
| // Test event system | // Setup function for creating a new event system | ||||||
| expect.describe('Event System', async () => { | function setupEventSystem(): { eventSystem: EventSystem, receivedEvents: any[] } { | ||||||
|   let eventSystem: EventSystem; |   const eventSystem = new EventSystem(ComponentType.SMART_PROXY, 'test-id'); | ||||||
|   let receivedEvents: any[] = []; |   const receivedEvents: any[] = []; | ||||||
|  |   return { eventSystem, receivedEvents }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | tap.test('Event System - certificate events with correct structure', async () => { | ||||||
|  |   const { eventSystem, receivedEvents } = setupEventSystem(); | ||||||
|    |    | ||||||
|   // Set up a new event system before each test |   // Set up listeners | ||||||
|   expect.beforeEach(() => { |   eventSystem.on(ProxyEvents.CERTIFICATE_ISSUED, (data) => { | ||||||
|     eventSystem = new EventSystem(ComponentType.SMART_PROXY, 'test-id'); |     receivedEvents.push({ | ||||||
|     receivedEvents = []; |       type: 'issued', | ||||||
|  |       data | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   expect.it('should emit certificate events with correct structure', async () => { |   eventSystem.on(ProxyEvents.CERTIFICATE_RENEWED, (data) => { | ||||||
|     // Set up listeners |     receivedEvents.push({ | ||||||
|     eventSystem.on(ProxyEvents.CERTIFICATE_ISSUED, (data) => { |       type: 'renewed', | ||||||
|       receivedEvents.push({ |       data | ||||||
|         type: 'issued', |  | ||||||
|         data |  | ||||||
|       }); |  | ||||||
|     }); |     }); | ||||||
|      |  | ||||||
|     eventSystem.on(ProxyEvents.CERTIFICATE_RENEWED, (data) => { |  | ||||||
|       receivedEvents.push({ |  | ||||||
|         type: 'renewed', |  | ||||||
|         data |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Emit events |  | ||||||
|     eventSystem.emitCertificateIssued({ |  | ||||||
|       domain: 'example.com', |  | ||||||
|       certificate: 'cert-content', |  | ||||||
|       privateKey: 'key-content', |  | ||||||
|       expiryDate: new Date('2025-01-01') |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     eventSystem.emitCertificateRenewed({ |  | ||||||
|       domain: 'example.com', |  | ||||||
|       certificate: 'new-cert-content', |  | ||||||
|       privateKey: 'new-key-content', |  | ||||||
|       expiryDate: new Date('2026-01-01'), |  | ||||||
|       isRenewal: true |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Verify events |  | ||||||
|     expect(receivedEvents.length).to.equal(2); |  | ||||||
|      |  | ||||||
|     // Check issuance event |  | ||||||
|     expect(receivedEvents[0].type).to.equal('issued'); |  | ||||||
|     expect(receivedEvents[0].data.domain).to.equal('example.com'); |  | ||||||
|     expect(receivedEvents[0].data.certificate).to.equal('cert-content'); |  | ||||||
|     expect(receivedEvents[0].data.componentType).to.equal(ComponentType.SMART_PROXY); |  | ||||||
|     expect(receivedEvents[0].data.componentId).to.equal('test-id'); |  | ||||||
|     expect(receivedEvents[0].data.timestamp).to.be.a('number'); |  | ||||||
|      |  | ||||||
|     // Check renewal event |  | ||||||
|     expect(receivedEvents[1].type).to.equal('renewed'); |  | ||||||
|     expect(receivedEvents[1].data.domain).to.equal('example.com'); |  | ||||||
|     expect(receivedEvents[1].data.isRenewal).to.be.true; |  | ||||||
|     expect(receivedEvents[1].data.expiryDate).to.deep.equal(new Date('2026-01-01')); |  | ||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   expect.it('should emit component lifecycle events', async () => { |   // Emit events | ||||||
|     // Set up listeners |   eventSystem.emitCertificateIssued({ | ||||||
|     eventSystem.on(ProxyEvents.COMPONENT_STARTED, (data) => { |     domain: 'example.com', | ||||||
|       receivedEvents.push({ |     certificate: 'cert-content', | ||||||
|         type: 'started', |     privateKey: 'key-content', | ||||||
|         data |     expiryDate: new Date('2025-01-01') | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     eventSystem.on(ProxyEvents.COMPONENT_STOPPED, (data) => { |  | ||||||
|       receivedEvents.push({ |  | ||||||
|         type: 'stopped', |  | ||||||
|         data |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Emit events |  | ||||||
|     eventSystem.emitComponentStarted('TestComponent', '1.0.0'); |  | ||||||
|     eventSystem.emitComponentStopped('TestComponent'); |  | ||||||
|      |  | ||||||
|     // Verify events |  | ||||||
|     expect(receivedEvents.length).to.equal(2); |  | ||||||
|      |  | ||||||
|     // Check started event |  | ||||||
|     expect(receivedEvents[0].type).to.equal('started'); |  | ||||||
|     expect(receivedEvents[0].data.name).to.equal('TestComponent'); |  | ||||||
|     expect(receivedEvents[0].data.version).to.equal('1.0.0'); |  | ||||||
|      |  | ||||||
|     // Check stopped event |  | ||||||
|     expect(receivedEvents[1].type).to.equal('stopped'); |  | ||||||
|     expect(receivedEvents[1].data.name).to.equal('TestComponent'); |  | ||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   expect.it('should emit connection events', async () => { |   eventSystem.emitCertificateRenewed({ | ||||||
|     // Set up listeners |     domain: 'example.com', | ||||||
|     eventSystem.on(ProxyEvents.CONNECTION_ESTABLISHED, (data) => { |     certificate: 'new-cert-content', | ||||||
|       receivedEvents.push({ |     privateKey: 'new-key-content', | ||||||
|         type: 'established', |     expiryDate: new Date('2026-01-01'), | ||||||
|         data |     isRenewal: true | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     eventSystem.on(ProxyEvents.CONNECTION_CLOSED, (data) => { |  | ||||||
|       receivedEvents.push({ |  | ||||||
|         type: 'closed', |  | ||||||
|         data |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Emit events |  | ||||||
|     eventSystem.emitConnectionEstablished({ |  | ||||||
|       connectionId: 'conn-123', |  | ||||||
|       clientIp: '192.168.1.1', |  | ||||||
|       port: 443, |  | ||||||
|       isTls: true, |  | ||||||
|       domain: 'example.com' |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     eventSystem.emitConnectionClosed({ |  | ||||||
|       connectionId: 'conn-123', |  | ||||||
|       clientIp: '192.168.1.1', |  | ||||||
|       port: 443 |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Verify events |  | ||||||
|     expect(receivedEvents.length).to.equal(2); |  | ||||||
|      |  | ||||||
|     // Check established event |  | ||||||
|     expect(receivedEvents[0].type).to.equal('established'); |  | ||||||
|     expect(receivedEvents[0].data.connectionId).to.equal('conn-123'); |  | ||||||
|     expect(receivedEvents[0].data.clientIp).to.equal('192.168.1.1'); |  | ||||||
|     expect(receivedEvents[0].data.port).to.equal(443); |  | ||||||
|     expect(receivedEvents[0].data.isTls).to.be.true; |  | ||||||
|      |  | ||||||
|     // Check closed event |  | ||||||
|     expect(receivedEvents[1].type).to.equal('closed'); |  | ||||||
|     expect(receivedEvents[1].data.connectionId).to.equal('conn-123'); |  | ||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   expect.it('should support once and off subscription methods', async () => { |   // Verify events | ||||||
|     // Set up a listener that should fire only once |   expect(receivedEvents.length).toEqual(2); | ||||||
|     eventSystem.once(ProxyEvents.CONNECTION_ESTABLISHED, (data) => { |    | ||||||
|       receivedEvents.push({ |   // Check issuance event | ||||||
|         type: 'once', |   expect(receivedEvents[0].type).toEqual('issued'); | ||||||
|         data |   expect(receivedEvents[0].data.domain).toEqual('example.com'); | ||||||
|       }); |   expect(receivedEvents[0].data.certificate).toEqual('cert-content'); | ||||||
|  |   expect(receivedEvents[0].data.componentType).toEqual(ComponentType.SMART_PROXY); | ||||||
|  |   expect(receivedEvents[0].data.componentId).toEqual('test-id'); | ||||||
|  |   expect(typeof receivedEvents[0].data.timestamp).toEqual('number'); | ||||||
|  |    | ||||||
|  |   // Check renewal event | ||||||
|  |   expect(receivedEvents[1].type).toEqual('renewed'); | ||||||
|  |   expect(receivedEvents[1].data.domain).toEqual('example.com'); | ||||||
|  |   expect(receivedEvents[1].data.isRenewal).toEqual(true); | ||||||
|  |   expect(receivedEvents[1].data.expiryDate).toEqual(new Date('2026-01-01')); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Event System - component lifecycle events', async () => { | ||||||
|  |   const { eventSystem, receivedEvents } = setupEventSystem(); | ||||||
|  |    | ||||||
|  |   // Set up listeners | ||||||
|  |   eventSystem.on(ProxyEvents.COMPONENT_STARTED, (data) => { | ||||||
|  |     receivedEvents.push({ | ||||||
|  |       type: 'started', | ||||||
|  |       data | ||||||
|     }); |     }); | ||||||
|      |  | ||||||
|     // Set up a persistent listener |  | ||||||
|     const persistentHandler = (data: any) => { |  | ||||||
|       receivedEvents.push({ |  | ||||||
|         type: 'persistent', |  | ||||||
|         data |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     eventSystem.on(ProxyEvents.CONNECTION_ESTABLISHED, persistentHandler); |  | ||||||
|      |  | ||||||
|     // First event should trigger both listeners |  | ||||||
|     eventSystem.emitConnectionEstablished({ |  | ||||||
|       connectionId: 'conn-1', |  | ||||||
|       clientIp: '192.168.1.1', |  | ||||||
|       port: 443 |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Second event should only trigger the persistent listener |  | ||||||
|     eventSystem.emitConnectionEstablished({ |  | ||||||
|       connectionId: 'conn-2', |  | ||||||
|       clientIp: '192.168.1.1', |  | ||||||
|       port: 443 |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Unsubscribe the persistent listener |  | ||||||
|     eventSystem.off(ProxyEvents.CONNECTION_ESTABLISHED, persistentHandler); |  | ||||||
|      |  | ||||||
|     // Third event should not trigger any listeners |  | ||||||
|     eventSystem.emitConnectionEstablished({ |  | ||||||
|       connectionId: 'conn-3', |  | ||||||
|       clientIp: '192.168.1.1', |  | ||||||
|       port: 443 |  | ||||||
|     }); |  | ||||||
|      |  | ||||||
|     // Verify events |  | ||||||
|     expect(receivedEvents.length).to.equal(3); |  | ||||||
|     expect(receivedEvents[0].type).to.equal('once'); |  | ||||||
|     expect(receivedEvents[0].data.connectionId).to.equal('conn-1'); |  | ||||||
|      |  | ||||||
|     expect(receivedEvents[1].type).to.equal('persistent'); |  | ||||||
|     expect(receivedEvents[1].data.connectionId).to.equal('conn-1'); |  | ||||||
|      |  | ||||||
|     expect(receivedEvents[2].type).to.equal('persistent'); |  | ||||||
|     expect(receivedEvents[2].data.connectionId).to.equal('conn-2'); |  | ||||||
|   }); |   }); | ||||||
| }); |    | ||||||
|  |   eventSystem.on(ProxyEvents.COMPONENT_STOPPED, (data) => { | ||||||
|  |     receivedEvents.push({ | ||||||
|  |       type: 'stopped', | ||||||
|  |       data | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Emit events | ||||||
|  |   eventSystem.emitComponentStarted('TestComponent', '1.0.0'); | ||||||
|  |   eventSystem.emitComponentStopped('TestComponent'); | ||||||
|  |    | ||||||
|  |   // Verify events | ||||||
|  |   expect(receivedEvents.length).toEqual(2); | ||||||
|  |    | ||||||
|  |   // Check started event | ||||||
|  |   expect(receivedEvents[0].type).toEqual('started'); | ||||||
|  |   expect(receivedEvents[0].data.name).toEqual('TestComponent'); | ||||||
|  |   expect(receivedEvents[0].data.version).toEqual('1.0.0'); | ||||||
|  |    | ||||||
|  |   // Check stopped event | ||||||
|  |   expect(receivedEvents[1].type).toEqual('stopped'); | ||||||
|  |   expect(receivedEvents[1].data.name).toEqual('TestComponent'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Event System - connection events', async () => { | ||||||
|  |   const { eventSystem, receivedEvents } = setupEventSystem(); | ||||||
|  |    | ||||||
|  |   // Set up listeners | ||||||
|  |   eventSystem.on(ProxyEvents.CONNECTION_ESTABLISHED, (data) => { | ||||||
|  |     receivedEvents.push({ | ||||||
|  |       type: 'established', | ||||||
|  |       data | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   eventSystem.on(ProxyEvents.CONNECTION_CLOSED, (data) => { | ||||||
|  |     receivedEvents.push({ | ||||||
|  |       type: 'closed', | ||||||
|  |       data | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Emit events | ||||||
|  |   eventSystem.emitConnectionEstablished({ | ||||||
|  |     connectionId: 'conn-123', | ||||||
|  |     clientIp: '192.168.1.1', | ||||||
|  |     port: 443, | ||||||
|  |     isTls: true, | ||||||
|  |     domain: 'example.com' | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   eventSystem.emitConnectionClosed({ | ||||||
|  |     connectionId: 'conn-123', | ||||||
|  |     clientIp: '192.168.1.1', | ||||||
|  |     port: 443 | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Verify events | ||||||
|  |   expect(receivedEvents.length).toEqual(2); | ||||||
|  |    | ||||||
|  |   // Check established event | ||||||
|  |   expect(receivedEvents[0].type).toEqual('established'); | ||||||
|  |   expect(receivedEvents[0].data.connectionId).toEqual('conn-123'); | ||||||
|  |   expect(receivedEvents[0].data.clientIp).toEqual('192.168.1.1'); | ||||||
|  |   expect(receivedEvents[0].data.port).toEqual(443); | ||||||
|  |   expect(receivedEvents[0].data.isTls).toEqual(true); | ||||||
|  |    | ||||||
|  |   // Check closed event | ||||||
|  |   expect(receivedEvents[1].type).toEqual('closed'); | ||||||
|  |   expect(receivedEvents[1].data.connectionId).toEqual('conn-123'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Event System - once and off subscription methods', async () => { | ||||||
|  |   const { eventSystem, receivedEvents } = setupEventSystem(); | ||||||
|  |    | ||||||
|  |   // Set up a listener that should fire only once | ||||||
|  |   eventSystem.once(ProxyEvents.CONNECTION_ESTABLISHED, (data) => { | ||||||
|  |     receivedEvents.push({ | ||||||
|  |       type: 'once', | ||||||
|  |       data | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Set up a persistent listener | ||||||
|  |   const persistentHandler = (data: any) => { | ||||||
|  |     receivedEvents.push({ | ||||||
|  |       type: 'persistent', | ||||||
|  |       data | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |    | ||||||
|  |   eventSystem.on(ProxyEvents.CONNECTION_ESTABLISHED, persistentHandler); | ||||||
|  |    | ||||||
|  |   // First event should trigger both listeners | ||||||
|  |   eventSystem.emitConnectionEstablished({ | ||||||
|  |     connectionId: 'conn-1', | ||||||
|  |     clientIp: '192.168.1.1', | ||||||
|  |     port: 443 | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Second event should only trigger the persistent listener | ||||||
|  |   eventSystem.emitConnectionEstablished({ | ||||||
|  |     connectionId: 'conn-2', | ||||||
|  |     clientIp: '192.168.1.1', | ||||||
|  |     port: 443 | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Unsubscribe the persistent listener | ||||||
|  |   eventSystem.off(ProxyEvents.CONNECTION_ESTABLISHED, persistentHandler); | ||||||
|  |    | ||||||
|  |   // Third event should not trigger any listeners | ||||||
|  |   eventSystem.emitConnectionEstablished({ | ||||||
|  |     connectionId: 'conn-3', | ||||||
|  |     clientIp: '192.168.1.1', | ||||||
|  |     port: 443 | ||||||
|  |   }); | ||||||
|  |    | ||||||
|  |   // Verify events | ||||||
|  |   expect(receivedEvents.length).toEqual(3); | ||||||
|  |   expect(receivedEvents[0].type).toEqual('once'); | ||||||
|  |   expect(receivedEvents[0].data.connectionId).toEqual('conn-1'); | ||||||
|  |    | ||||||
|  |   expect(receivedEvents[1].type).toEqual('persistent'); | ||||||
|  |   expect(receivedEvents[1].data.connectionId).toEqual('conn-1'); | ||||||
|  |    | ||||||
|  |   expect(receivedEvents[2].type).toEqual('persistent'); | ||||||
|  |   expect(receivedEvents[2].data.connectionId).toEqual('conn-2'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export default tap.start(); | ||||||
| @@ -1,116 +1,110 @@ | |||||||
| import { expect } from '@push.rocks/tapbundle'; | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
| import * as routeUtils from '../../../ts/core/utils/route-utils.js'; | import * as routeUtils from '../../../ts/core/utils/route-utils.js'; | ||||||
|  |  | ||||||
| // Test domain matching | // Test domain matching | ||||||
| expect.describe('Route Utils - Domain Matching', async () => { | tap.test('Route Utils - Domain Matching - exact domains', async () => { | ||||||
|   expect.it('should match exact domains', async () => { |   expect(routeUtils.matchDomain('example.com', 'example.com')).toEqual(true); | ||||||
|     expect(routeUtils.matchDomain('example.com', 'example.com')).to.be.true; | }); | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   expect.it('should match wildcard domains', async () => { | tap.test('Route Utils - Domain Matching - wildcard domains', async () => { | ||||||
|     expect(routeUtils.matchDomain('*.example.com', 'sub.example.com')).to.be.true; |   expect(routeUtils.matchDomain('*.example.com', 'sub.example.com')).toEqual(true); | ||||||
|     expect(routeUtils.matchDomain('*.example.com', 'another.sub.example.com')).to.be.true; |   expect(routeUtils.matchDomain('*.example.com', 'another.sub.example.com')).toEqual(true); | ||||||
|     expect(routeUtils.matchDomain('*.example.com', 'example.com')).to.be.false; |   expect(routeUtils.matchDomain('*.example.com', 'example.com')).toEqual(false); | ||||||
|   }); | }); | ||||||
|  |  | ||||||
|   expect.it('should match domains case-insensitively', async () => { | tap.test('Route Utils - Domain Matching - case insensitivity', async () => { | ||||||
|     expect(routeUtils.matchDomain('example.com', 'EXAMPLE.com')).to.be.true; |   expect(routeUtils.matchDomain('example.com', 'EXAMPLE.com')).toEqual(true); | ||||||
|   }); | }); | ||||||
|  |  | ||||||
|   expect.it('should match routes with multiple domain patterns', async () => { | tap.test('Route Utils - Domain Matching - multiple domain patterns', async () => { | ||||||
|     expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'example.com')).to.be.true; |   expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'example.com')).toEqual(true); | ||||||
|     expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'sub.test.com')).to.be.true; |   expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'sub.test.com')).toEqual(true); | ||||||
|     expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'something.else')).to.be.false; |   expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'something.else')).toEqual(false); | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Test path matching | // Test path matching | ||||||
| expect.describe('Route Utils - Path Matching', async () => { | tap.test('Route Utils - Path Matching - exact paths', async () => { | ||||||
|   expect.it('should match exact paths', async () => { |   expect(routeUtils.matchPath('/api/users', '/api/users')).toEqual(true); | ||||||
|     expect(routeUtils.matchPath('/api/users', '/api/users')).to.be.true; | }); | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   expect.it('should match wildcard paths', async () => { | tap.test('Route Utils - Path Matching - wildcard paths', async () => { | ||||||
|     expect(routeUtils.matchPath('/api/*', '/api/users')).to.be.true; |   expect(routeUtils.matchPath('/api/*', '/api/users')).toEqual(true); | ||||||
|     expect(routeUtils.matchPath('/api/*', '/api/products')).to.be.true; |   expect(routeUtils.matchPath('/api/*', '/api/products')).toEqual(true); | ||||||
|     expect(routeUtils.matchPath('/api/*', '/something/else')).to.be.false; |   expect(routeUtils.matchPath('/api/*', '/something/else')).toEqual(false); | ||||||
|   }); | }); | ||||||
|  |  | ||||||
|   expect.it('should match complex wildcard patterns', async () => { | tap.test('Route Utils - Path Matching - complex wildcard patterns', async () => { | ||||||
|     expect(routeUtils.matchPath('/api/*/details', '/api/users/details')).to.be.true; |   expect(routeUtils.matchPath('/api/*/details', '/api/users/details')).toEqual(true); | ||||||
|     expect(routeUtils.matchPath('/api/*/details', '/api/products/details')).to.be.true; |   expect(routeUtils.matchPath('/api/*/details', '/api/products/details')).toEqual(true); | ||||||
|     expect(routeUtils.matchPath('/api/*/details', '/api/users/other')).to.be.false; |   expect(routeUtils.matchPath('/api/*/details', '/api/users/other')).toEqual(false); | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Test IP matching | // Test IP matching | ||||||
| expect.describe('Route Utils - IP Matching', async () => { | tap.test('Route Utils - IP Matching - exact IPs', async () => { | ||||||
|   expect.it('should match exact IPs', async () => { |   expect(routeUtils.matchIpPattern('192.168.1.1', '192.168.1.1')).toEqual(true); | ||||||
|     expect(routeUtils.matchIpPattern('192.168.1.1', '192.168.1.1')).to.be.true; | }); | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   expect.it('should match wildcard IPs', async () => { | tap.test('Route Utils - IP Matching - wildcard IPs', async () => { | ||||||
|     expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.1.100')).to.be.true; |   expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.1.100')).toEqual(true); | ||||||
|     expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.2.1')).to.be.false; |   expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.2.1')).toEqual(false); | ||||||
|   }); | }); | ||||||
|  |  | ||||||
|   expect.it('should match CIDR notation', async () => { | tap.test('Route Utils - IP Matching - CIDR notation', async () => { | ||||||
|     expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.1.100')).to.be.true; |   expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.1.100')).toEqual(true); | ||||||
|     expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.2.1')).to.be.false; |   expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.2.1')).toEqual(false); | ||||||
|   }); | }); | ||||||
|  |  | ||||||
|   expect.it('should handle IPv6-mapped IPv4 addresses', async () => { | tap.test('Route Utils - IP Matching - IPv6-mapped IPv4 addresses', async () => { | ||||||
|     expect(routeUtils.matchIpPattern('192.168.1.1', '::ffff:192.168.1.1')).to.be.true; |   expect(routeUtils.matchIpPattern('192.168.1.1', '::ffff:192.168.1.1')).toEqual(true); | ||||||
|   }); | }); | ||||||
|  |  | ||||||
|   expect.it('should correctly authorize IPs based on allow/block lists', async () => { | tap.test('Route Utils - IP Matching - IP authorization with allow/block lists', async () => { | ||||||
|     // With allow and block lists |   // With allow and block lists | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'], ['192.168.1.5'])).to.be.true; |   expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'], ['192.168.1.5'])).toEqual(true); | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.1.5', ['192.168.1.*'], ['192.168.1.5'])).to.be.false; |   expect(routeUtils.isIpAuthorized('192.168.1.5', ['192.168.1.*'], ['192.168.1.5'])).toEqual(false); | ||||||
|      |    | ||||||
|     // With only allow list |   // With only allow list | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'])).to.be.true; |   expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'])).toEqual(true); | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.2.1', ['192.168.1.*'])).to.be.false; |   expect(routeUtils.isIpAuthorized('192.168.2.1', ['192.168.1.*'])).toEqual(false); | ||||||
|      |    | ||||||
|     // With only block list |   // With only block list | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.1.5', undefined, ['192.168.1.5'])).to.be.false; |   expect(routeUtils.isIpAuthorized('192.168.1.5', undefined, ['192.168.1.5'])).toEqual(false); | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.1.1', undefined, ['192.168.1.5'])).to.be.true; |   expect(routeUtils.isIpAuthorized('192.168.1.1', undefined, ['192.168.1.5'])).toEqual(true); | ||||||
|      |    | ||||||
|     // With wildcard in allow list |   // With wildcard in allow list | ||||||
|     expect(routeUtils.isIpAuthorized('192.168.1.1', ['*'], ['192.168.1.5'])).to.be.true; |   expect(routeUtils.isIpAuthorized('192.168.1.1', ['*'], ['192.168.1.5'])).toEqual(true); | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Test route specificity calculation | // Test route specificity calculation | ||||||
| expect.describe('Route Utils - Route Specificity', async () => { | tap.test('Route Utils - Route Specificity - calculating correctly', async () => { | ||||||
|   expect.it('should calculate route specificity correctly', async () => { |   const basicRoute = { domains: 'example.com' }; | ||||||
|     const basicRoute = { domains: 'example.com' }; |   const pathRoute = { domains: 'example.com', path: '/api' }; | ||||||
|     const pathRoute = { domains: 'example.com', path: '/api' }; |   const wildcardPathRoute = { domains: 'example.com', path: '/api/*' }; | ||||||
|     const wildcardPathRoute = { domains: 'example.com', path: '/api/*' }; |   const headerRoute = { domains: 'example.com', headers: { 'content-type': 'application/json' } }; | ||||||
|     const headerRoute = { domains: 'example.com', headers: { 'content-type': 'application/json' } }; |   const complexRoute = {  | ||||||
|     const complexRoute = {  |     domains: 'example.com',  | ||||||
|       domains: 'example.com',  |     path: '/api',  | ||||||
|       path: '/api',  |     headers: { 'content-type': 'application/json' }, | ||||||
|       headers: { 'content-type': 'application/json' }, |     clientIp: ['192.168.1.1']  | ||||||
|       clientIp: ['192.168.1.1']  |   }; | ||||||
|     }; |    | ||||||
|      |   // Path routes should have higher specificity than domain-only routes | ||||||
|     // Path routes should have higher specificity than domain-only routes |   expect(routeUtils.calculateRouteSpecificity(pathRoute) >  | ||||||
|     expect(routeUtils.calculateRouteSpecificity(pathRoute)) |          routeUtils.calculateRouteSpecificity(basicRoute)).toEqual(true); | ||||||
|       .to.be.greaterThan(routeUtils.calculateRouteSpecificity(basicRoute)); |    | ||||||
|      |   // Exact path routes should have higher specificity than wildcard path routes | ||||||
|     // Exact path routes should have higher specificity than wildcard path routes |   expect(routeUtils.calculateRouteSpecificity(pathRoute) >  | ||||||
|     expect(routeUtils.calculateRouteSpecificity(pathRoute)) |          routeUtils.calculateRouteSpecificity(wildcardPathRoute)).toEqual(true); | ||||||
|       .to.be.greaterThan(routeUtils.calculateRouteSpecificity(wildcardPathRoute)); |    | ||||||
|      |   // Routes with headers should have higher specificity than routes without | ||||||
|     // Routes with headers should have higher specificity than routes without |   expect(routeUtils.calculateRouteSpecificity(headerRoute) >  | ||||||
|     expect(routeUtils.calculateRouteSpecificity(headerRoute)) |          routeUtils.calculateRouteSpecificity(basicRoute)).toEqual(true); | ||||||
|       .to.be.greaterThan(routeUtils.calculateRouteSpecificity(basicRoute)); |    | ||||||
|      |   // Complex routes should have the highest specificity | ||||||
|     // Complex routes should have the highest specificity |   expect(routeUtils.calculateRouteSpecificity(complexRoute) >  | ||||||
|     expect(routeUtils.calculateRouteSpecificity(complexRoute)) |          routeUtils.calculateRouteSpecificity(pathRoute)).toEqual(true); | ||||||
|       .to.be.greaterThan(routeUtils.calculateRouteSpecificity(pathRoute)); |   expect(routeUtils.calculateRouteSpecificity(complexRoute) >  | ||||||
|     expect(routeUtils.calculateRouteSpecificity(complexRoute)) |          routeUtils.calculateRouteSpecificity(headerRoute)).toEqual(true); | ||||||
|       .to.be.greaterThan(routeUtils.calculateRouteSpecificity(headerRoute)); | }); | ||||||
|   }); |  | ||||||
| }); | export default tap.start(); | ||||||
| @@ -1,24 +1,22 @@ | |||||||
| import { expect, tap } from '@push.rocks/tapbundle'; | import { expect, tap } from '@push.rocks/tapbundle'; | ||||||
|  | import * as plugins from '../ts/plugins.js'; | ||||||
| import { NetworkProxy } from '../ts/proxies/network-proxy/index.js'; | import { NetworkProxy } from '../ts/proxies/network-proxy/index.js'; | ||||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||||
| import type { IRouteContext } from '../ts/core/models/route-context.js'; | import type { IRouteContext } from '../ts/core/models/route-context.js'; | ||||||
| import * as http from 'http'; |  | ||||||
| import * as https from 'https'; |  | ||||||
| import * as http2 from 'http2'; |  | ||||||
|  |  | ||||||
| const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); | ||||||
|  |  | ||||||
| // Declare variables for tests | // Declare variables for tests | ||||||
| let networkProxy: NetworkProxy; | let networkProxy: NetworkProxy; | ||||||
| let testServer: http.Server; | let testServer: plugins.http.Server; | ||||||
| let testServerHttp2: http2.Http2Server; | let testServerHttp2: plugins.http2.Http2Server; | ||||||
| let serverPort: number; | let serverPort: number; | ||||||
| let serverPortHttp2: number; | let serverPortHttp2: number; | ||||||
|  |  | ||||||
| // Setup test environment | // Setup test environment | ||||||
| tap.test('setup NetworkProxy function-based targets test environment', async () => { | tap.test('setup NetworkProxy function-based targets test environment', async () => { | ||||||
|   // Create simple HTTP server to respond to requests |   // Create simple HTTP server to respond to requests | ||||||
|   testServer = http.createServer((req, res) => { |   testServer = plugins.http.createServer((req, res) => { | ||||||
|     res.writeHead(200, { 'Content-Type': 'application/json' }); |     res.writeHead(200, { 'Content-Type': 'application/json' }); | ||||||
|     res.end(JSON.stringify({ |     res.end(JSON.stringify({ | ||||||
|       url: req.url, |       url: req.url, | ||||||
| @@ -29,7 +27,7 @@ tap.test('setup NetworkProxy function-based targets test environment', async () | |||||||
|   }); |   }); | ||||||
|    |    | ||||||
|   // Create simple HTTP/2 server to respond to requests |   // Create simple HTTP/2 server to respond to requests | ||||||
|   testServerHttp2 = http2.createServer(); |   testServerHttp2 = plugins.http2.createServer(); | ||||||
|   testServerHttp2.on('stream', (stream, headers) => { |   testServerHttp2.on('stream', (stream, headers) => { | ||||||
|     stream.respond({ |     stream.respond({ | ||||||
|       'content-type': 'application/json', |       'content-type': 'application/json', | ||||||
| @@ -82,10 +80,10 @@ tap.test('should support static host/port routes', async () => { | |||||||
|   const routes: IRouteConfig[] = [ |   const routes: IRouteConfig[] = [ | ||||||
|     { |     { | ||||||
|       name: 'static-route', |       name: 'static-route', | ||||||
|       domain: 'example.com', |  | ||||||
|       priority: 100, |       priority: 100, | ||||||
|       match: { |       match: { | ||||||
|         domain: 'example.com' |         domains: 'example.com', | ||||||
|  |         ports: 0 | ||||||
|       }, |       }, | ||||||
|       action: { |       action: { | ||||||
|         type: 'forward', |         type: 'forward', | ||||||
| @@ -124,10 +122,10 @@ tap.test('should support function-based host', async () => { | |||||||
|   const routes: IRouteConfig[] = [ |   const routes: IRouteConfig[] = [ | ||||||
|     { |     { | ||||||
|       name: 'function-host-route', |       name: 'function-host-route', | ||||||
|       domain: 'function.example.com', |  | ||||||
|       priority: 100, |       priority: 100, | ||||||
|       match: { |       match: { | ||||||
|         domain: 'function.example.com' |         domains: 'function.example.com', | ||||||
|  |         ports: 0 | ||||||
|       }, |       }, | ||||||
|       action: { |       action: { | ||||||
|         type: 'forward', |         type: 'forward', | ||||||
| @@ -169,10 +167,10 @@ tap.test('should support function-based port', async () => { | |||||||
|   const routes: IRouteConfig[] = [ |   const routes: IRouteConfig[] = [ | ||||||
|     { |     { | ||||||
|       name: 'function-port-route', |       name: 'function-port-route', | ||||||
|       domain: 'function-port.example.com', |  | ||||||
|       priority: 100, |       priority: 100, | ||||||
|       match: { |       match: { | ||||||
|         domain: 'function-port.example.com' |         domains: 'function-port.example.com', | ||||||
|  |         ports: 0 | ||||||
|       }, |       }, | ||||||
|       action: { |       action: { | ||||||
|         type: 'forward', |         type: 'forward', | ||||||
| @@ -214,10 +212,10 @@ tap.test('should support function-based host AND port', async () => { | |||||||
|   const routes: IRouteConfig[] = [ |   const routes: IRouteConfig[] = [ | ||||||
|     { |     { | ||||||
|       name: 'function-both-route', |       name: 'function-both-route', | ||||||
|       domain: 'function-both.example.com', |  | ||||||
|       priority: 100, |       priority: 100, | ||||||
|       match: { |       match: { | ||||||
|         domain: 'function-both.example.com' |         domains: 'function-both.example.com', | ||||||
|  |         ports: 0 | ||||||
|       }, |       }, | ||||||
|       action: { |       action: { | ||||||
|         type: 'forward', |         type: 'forward', | ||||||
| @@ -260,10 +258,10 @@ tap.test('should support context-based routing with path', async () => { | |||||||
|   const routes: IRouteConfig[] = [ |   const routes: IRouteConfig[] = [ | ||||||
|     { |     { | ||||||
|       name: 'context-path-route', |       name: 'context-path-route', | ||||||
|       domain: 'context.example.com', |  | ||||||
|       priority: 100, |       priority: 100, | ||||||
|       match: { |       match: { | ||||||
|         domain: 'context.example.com' |         domains: 'context.example.com', | ||||||
|  |         ports: 0 | ||||||
|       }, |       }, | ||||||
|       action: { |       action: { | ||||||
|         type: 'forward', |         type: 'forward', | ||||||
| @@ -338,10 +336,10 @@ tap.test('cleanup NetworkProxy function-based targets test environment', async ( | |||||||
| }); | }); | ||||||
|  |  | ||||||
| // Helper function to make HTTPS requests with self-signed certificate support | // Helper function to make HTTPS requests with self-signed certificate support | ||||||
| async function makeRequest(options: http.RequestOptions): Promise<{ statusCode: number, headers: http.IncomingHttpHeaders, body: string }> { | async function makeRequest(options: plugins.http.RequestOptions): Promise<{ statusCode: number, headers: plugins.http.IncomingHttpHeaders, body: string }> { | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     // Use HTTPS with rejectUnauthorized: false to accept self-signed certificates |     // Use HTTPS with rejectUnauthorized: false to accept self-signed certificates | ||||||
|     const req = https.request({ |     const req = plugins.https.request({ | ||||||
|       ...options, |       ...options, | ||||||
|       rejectUnauthorized: false, // Accept self-signed certificates |       rejectUnauthorized: false, // Accept self-signed certificates | ||||||
|     }, (res) => { |     }, (res) => { | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   name: '@push.rocks/smartproxy', | ||||||
|   version: '16.0.2', |   version: '16.0.3', | ||||||
|   description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' |   description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import * as plugins from '../../plugins.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Shared Route Context Interface |  * Shared Route Context Interface | ||||||
|  *  |  *  | ||||||
| @@ -42,8 +44,8 @@ export interface IRouteContext { | |||||||
|  * Used only in NetworkProxy for HTTP request handling |  * Used only in NetworkProxy for HTTP request handling | ||||||
|  */ |  */ | ||||||
| export interface IHttpRouteContext extends IRouteContext { | export interface IHttpRouteContext extends IRouteContext { | ||||||
|   req?: any; // http.IncomingMessage  |   req?: plugins.http.IncomingMessage; | ||||||
|   res?: any; // http.ServerResponse |   res?: plugins.http.ServerResponse; | ||||||
|   method?: string; // HTTP method (GET, POST, etc.) |   method?: string; // HTTP method (GET, POST, etc.) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -52,7 +54,7 @@ export interface IHttpRouteContext extends IRouteContext { | |||||||
|  * Used only in NetworkProxy for HTTP/2 request handling |  * Used only in NetworkProxy for HTTP/2 request handling | ||||||
|  */ |  */ | ||||||
| export interface IHttp2RouteContext extends IHttpRouteContext { | export interface IHttp2RouteContext extends IHttpRouteContext { | ||||||
|   stream?: any; // http2.Http2Stream |   stream?: plugins.http2.ServerHttp2Stream; | ||||||
|   headers?: Record<string, string>; // HTTP/2 pseudo-headers like :method, :path |   headers?: Record<string, string>; // HTTP/2 pseudo-headers like :method, :path | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ | |||||||
|  * @returns Whether the domain matches the pattern |  * @returns Whether the domain matches the pattern | ||||||
|  */ |  */ | ||||||
| export function matchDomain(pattern: string, domain: string): boolean { | export function matchDomain(pattern: string, domain: string): boolean { | ||||||
|   // Handle exact match |   // Handle exact match (case-insensitive) | ||||||
|   if (pattern === domain) { |   if (pattern.toLowerCase() === domain.toLowerCase()) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -139,9 +139,13 @@ export function matchIpCidr(cidr: string, ip: string): boolean { | |||||||
|   try { |   try { | ||||||
|     const { subnet, bits } = parsed; |     const { subnet, bits } = parsed; | ||||||
|      |      | ||||||
|  |     // Normalize IPv6-mapped IPv4 addresses | ||||||
|  |     const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||||
|  |     const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet; | ||||||
|  |      | ||||||
|     // Convert IP addresses to numeric values |     // Convert IP addresses to numeric values | ||||||
|     const ipNum = ipToNumber(ip); |     const ipNum = ipToNumber(normalizedIp); | ||||||
|     const subnetNum = ipToNumber(subnet); |     const subnetNum = ipToNumber(normalizedSubnet); | ||||||
|      |      | ||||||
|     // Calculate subnet mask |     // Calculate subnet mask | ||||||
|     const maskNum = ~(2 ** (32 - bits) - 1); |     const maskNum = ~(2 ** (32 - bits) - 1); | ||||||
| @@ -161,26 +165,41 @@ export function matchIpCidr(cidr: string, ip: string): boolean { | |||||||
|  * @returns Whether the IP matches the pattern |  * @returns Whether the IP matches the pattern | ||||||
|  */ |  */ | ||||||
| export function matchIpPattern(pattern: string, ip: string): boolean { | export function matchIpPattern(pattern: string, ip: string): boolean { | ||||||
|   // Handle exact match |   // Normalize IPv6-mapped IPv4 addresses | ||||||
|   if (pattern === ip) { |   const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||||
|  |   const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern; | ||||||
|  |    | ||||||
|  |   // Handle exact match with all variations | ||||||
|  |   if (pattern === ip || normalizedPattern === normalizedIp ||  | ||||||
|  |       pattern === normalizedIp || normalizedPattern === ip) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   // Handle "all" wildcard |   // Handle "all" wildcard | ||||||
|   if (pattern === '*') { |   if (pattern === '*' || normalizedPattern === '*') { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   // Handle CIDR notation (e.g., 192.168.1.0/24) |   // Handle CIDR notation (e.g., 192.168.1.0/24) | ||||||
|   if (pattern.includes('/')) { |   if (pattern.includes('/')) { | ||||||
|     return matchIpCidr(pattern, ip); |     return matchIpCidr(pattern, normalizedIp) ||  | ||||||
|  |            (normalizedPattern !== pattern && matchIpCidr(normalizedPattern, normalizedIp)); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   // Handle glob pattern (e.g., 192.168.1.*) |   // Handle glob pattern (e.g., 192.168.1.*) | ||||||
|   if (pattern.includes('*')) { |   if (pattern.includes('*')) { | ||||||
|     const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); |     const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||||
|     const regex = new RegExp(`^${regexPattern}$`); |     const regex = new RegExp(`^${regexPattern}$`); | ||||||
|     return regex.test(ip); |     if (regex.test(ip) || regex.test(normalizedIp)) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // If pattern was normalized, also test with normalized pattern | ||||||
|  |     if (normalizedPattern !== pattern) { | ||||||
|  |       const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||||
|  |       const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`); | ||||||
|  |       return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   return false; |   return false; | ||||||
|   | |||||||
| @@ -291,12 +291,15 @@ export class RouteManager { | |||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Match an IP pattern against an IP |    * Match an IP pattern against an IP | ||||||
|  |    * Supports exact matches, wildcard patterns, and CIDR notation | ||||||
|    */ |    */ | ||||||
|   private matchIp(pattern: string, ip: string): boolean { |   private matchIp(pattern: string, ip: string): boolean { | ||||||
|  |     // Exact match | ||||||
|     if (pattern === ip) { |     if (pattern === ip) { | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Wildcard matching (e.g., 192.168.0.*) | ||||||
|     if (pattern.includes('*')) { |     if (pattern.includes('*')) { | ||||||
|       const regexPattern = pattern |       const regexPattern = pattern | ||||||
|         .replace(/\./g, '\\.') |         .replace(/\./g, '\\.') | ||||||
| @@ -306,10 +309,65 @@ export class RouteManager { | |||||||
|       return regex.test(ip); |       return regex.test(ip); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO: Implement CIDR matching |     // CIDR matching (e.g., 192.168.0.0/24) | ||||||
|  |     if (pattern.includes('/')) { | ||||||
|  |       try { | ||||||
|  |         const [subnet, bits] = pattern.split('/'); | ||||||
|  |          | ||||||
|  |         // Convert IP addresses to numeric format for comparison | ||||||
|  |         const ipBinary = this.ipToBinary(ip); | ||||||
|  |         const subnetBinary = this.ipToBinary(subnet); | ||||||
|  |          | ||||||
|  |         if (!ipBinary || !subnetBinary) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Get the subnet mask from CIDR notation | ||||||
|  |         const mask = parseInt(bits, 10); | ||||||
|  |         if (isNaN(mask) || mask < 0 || mask > 32) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Check if the first 'mask' bits match between IP and subnet | ||||||
|  |         return ipBinary.slice(0, mask) === subnetBinary.slice(0, mask); | ||||||
|  |       } catch (error) { | ||||||
|  |         // If we encounter any error during CIDR matching, return false | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Convert an IP address to its binary representation | ||||||
|  |    * @param ip The IP address to convert | ||||||
|  |    * @returns Binary string representation or null if invalid | ||||||
|  |    */ | ||||||
|  |   private ipToBinary(ip: string): string | null { | ||||||
|  |     // Handle IPv4 addresses only for now | ||||||
|  |     const parts = ip.split('.'); | ||||||
|  |      | ||||||
|  |     // Validate IP format | ||||||
|  |     if (parts.length !== 4) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Convert each octet to 8-bit binary and concatenate | ||||||
|  |     try { | ||||||
|  |       return parts | ||||||
|  |         .map(part => { | ||||||
|  |           const num = parseInt(part, 10); | ||||||
|  |           if (isNaN(num) || num < 0 || num > 255) { | ||||||
|  |             throw new Error('Invalid IP octet'); | ||||||
|  |           } | ||||||
|  |           return num.toString(2).padStart(8, '0'); | ||||||
|  |         }) | ||||||
|  |         .join(''); | ||||||
|  |     } catch (error) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -500,68 +500,8 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|     this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`); |     this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   // Legacy methods have been removed. | ||||||
|    * @deprecated Use updateRouteConfigs instead |   // Please use updateRouteConfigs() directly with modern route-based configuration. | ||||||
|    * Legacy method for updating proxy configurations using IReverseProxyConfig |  | ||||||
|    * This method is maintained for backward compatibility |  | ||||||
|    */ |  | ||||||
|   public async updateProxyConfigs( |  | ||||||
|     proxyConfigsArg: IReverseProxyConfig[] |  | ||||||
|   ): Promise<void> { |  | ||||||
|     this.logger.info(`Converting ${proxyConfigsArg.length} legacy configs to route configs`); |  | ||||||
|  |  | ||||||
|     // Convert legacy configs to route configs |  | ||||||
|     const routes: IRouteConfig[] = proxyConfigsArg.map(config => |  | ||||||
|       convertLegacyConfigToRouteConfig(config, this.options.port) |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // Use the primary method |  | ||||||
|     return this.updateRouteConfigs(routes); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * @deprecated Use route-based configuration instead |  | ||||||
|    * Converts SmartProxy domain configurations to NetworkProxy configs |  | ||||||
|    * This method is maintained for backward compatibility |  | ||||||
|    */ |  | ||||||
|   public convertSmartProxyConfigs( |  | ||||||
|     domainConfigs: Array<{ |  | ||||||
|       domains: string[]; |  | ||||||
|       targetIPs?: string[]; |  | ||||||
|       allowedIPs?: string[]; |  | ||||||
|     }>, |  | ||||||
|     sslKeyPair?: { key: string; cert: string } |  | ||||||
|   ): IReverseProxyConfig[] { |  | ||||||
|     this.logger.warn('convertSmartProxyConfigs is deprecated - use route-based configuration instead'); |  | ||||||
|  |  | ||||||
|     const proxyConfigs: IReverseProxyConfig[] = []; |  | ||||||
|  |  | ||||||
|     // Use default certificates if not provided |  | ||||||
|     const defaultCerts = this.certificateManager.getDefaultCertificates(); |  | ||||||
|     const sslKey = sslKeyPair?.key || defaultCerts.key; |  | ||||||
|     const sslCert = sslKeyPair?.cert || defaultCerts.cert; |  | ||||||
|  |  | ||||||
|     for (const domainConfig of domainConfigs) { |  | ||||||
|       // Each domain in the domains array gets its own config |  | ||||||
|       for (const domain of domainConfig.domains) { |  | ||||||
|         // Skip non-hostname patterns (like IP addresses) |  | ||||||
|         if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') { |  | ||||||
|           continue; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         proxyConfigs.push({ |  | ||||||
|           hostName: domain, |  | ||||||
|           destinationIps: domainConfig.targetIPs || ['localhost'], |  | ||||||
|           destinationPorts: [this.options.port], // Use the NetworkProxy port |  | ||||||
|           privateKey: sslKey, |  | ||||||
|           publicKey: sslCert |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.logger.info(`Converted ${domainConfigs.length} SmartProxy configs to ${proxyConfigs.length} NetworkProxy configs`); |  | ||||||
|     return proxyConfigs; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Adds default headers to be included in all responses |    * Adds default headers to be included in all responses | ||||||
| @@ -650,62 +590,4 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|   public getRouteConfigs(): IRouteConfig[] { |   public getRouteConfigs(): IRouteConfig[] { | ||||||
|     return this.routeManager.getRoutes(); |     return this.routeManager.getRoutes(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * @deprecated Use getRouteConfigs instead |  | ||||||
|    * Gets all proxy configurations currently in use in the legacy format |  | ||||||
|    * This method is maintained for backward compatibility |  | ||||||
|    */ |  | ||||||
|   public getProxyConfigs(): IReverseProxyConfig[] { |  | ||||||
|     this.logger.warn('getProxyConfigs is deprecated - use getRouteConfigs instead'); |  | ||||||
|  |  | ||||||
|     // Create legacy proxy configs from our route configurations |  | ||||||
|     const legacyConfigs: IReverseProxyConfig[] = []; |  | ||||||
|     const currentRoutes = this.routeManager.getRoutes(); |  | ||||||
|  |  | ||||||
|     for (const route of currentRoutes) { |  | ||||||
|       // Skip non-forward routes or routes without domains |  | ||||||
|       if (route.action.type !== 'forward' || !route.match.domains || !route.action.target) { |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Skip routes with function-based targets |  | ||||||
|       if (typeof route.action.target.host === 'function' || typeof route.action.target.port === 'function') { |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Get domains |  | ||||||
|       const domains = Array.isArray(route.match.domains) |  | ||||||
|         ? route.match.domains.filter(d => !d.includes('*')) |  | ||||||
|         : route.match.domains.includes('*') ? [] : [route.match.domains]; |  | ||||||
|  |  | ||||||
|       // Get certificate |  | ||||||
|       let privateKey = ''; |  | ||||||
|       let publicKey = ''; |  | ||||||
|  |  | ||||||
|       if (route.action.tls?.certificate && route.action.tls.certificate !== 'auto') { |  | ||||||
|         privateKey = route.action.tls.certificate.key; |  | ||||||
|         publicKey = route.action.tls.certificate.cert; |  | ||||||
|       } else { |  | ||||||
|         const defaultCerts = this.certificateManager.getDefaultCertificates(); |  | ||||||
|         privateKey = defaultCerts.key; |  | ||||||
|         publicKey = defaultCerts.cert; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Create legacy config for each domain |  | ||||||
|       for (const domain of domains) { |  | ||||||
|         legacyConfigs.push({ |  | ||||||
|           hostName: domain, |  | ||||||
|           destinationIps: Array.isArray(route.action.target.host) |  | ||||||
|             ? route.action.target.host |  | ||||||
|             : [route.action.target.host], |  | ||||||
|           destinationPorts: [route.action.target.port], |  | ||||||
|           privateKey, |  | ||||||
|           publicKey |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return legacyConfigs; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| @@ -661,150 +661,6 @@ export class RequestHandler { | |||||||
|       }); |       }); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|      |  | ||||||
|     try { |  | ||||||
|       // Find target based on hostname |  | ||||||
|       const proxyConfig = this.router.routeReq(req); |  | ||||||
|        |  | ||||||
|       if (!proxyConfig) { |  | ||||||
|         // No matching proxy configuration |  | ||||||
|         this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); |  | ||||||
|         res.statusCode = 404; |  | ||||||
|         res.end('Not Found: No proxy configuration for this host'); |  | ||||||
|          |  | ||||||
|         // Increment failed requests counter |  | ||||||
|         if (this.metricsTracker) { |  | ||||||
|           this.metricsTracker.incrementFailedRequests(); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Get destination IP using round-robin if multiple IPs configured |  | ||||||
|       const destination = this.connectionPool.getNextTarget( |  | ||||||
|         proxyConfig.destinationIps,  |  | ||||||
|         proxyConfig.destinationPorts[0] |  | ||||||
|       ); |  | ||||||
|        |  | ||||||
|       // Create options for the proxy request |  | ||||||
|       const options: plugins.http.RequestOptions = { |  | ||||||
|         hostname: destination.host, |  | ||||||
|         port: destination.port, |  | ||||||
|         path: req.url, |  | ||||||
|         method: req.method, |  | ||||||
|         headers: { ...req.headers } |  | ||||||
|       }; |  | ||||||
|        |  | ||||||
|       // Remove host header to avoid issues with virtual hosts on target server |  | ||||||
|       // The host header should match the target server's expected hostname |  | ||||||
|       if (options.headers && options.headers.host) { |  | ||||||
|         if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) { |  | ||||||
|           options.headers.host = `${destination.host}:${destination.port}`; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       this.logger.debug( |  | ||||||
|         `Proxying request to ${destination.host}:${destination.port}${req.url}`, |  | ||||||
|         { method: req.method } |  | ||||||
|       ); |  | ||||||
|        |  | ||||||
|       // Create proxy request |  | ||||||
|       const proxyReq = plugins.http.request(options, (proxyRes) => { |  | ||||||
|         // Copy status code |  | ||||||
|         res.statusCode = proxyRes.statusCode || 500; |  | ||||||
|          |  | ||||||
|         // Copy headers from proxy response to client response |  | ||||||
|         for (const [key, value] of Object.entries(proxyRes.headers)) { |  | ||||||
|           if (value !== undefined) { |  | ||||||
|             res.setHeader(key, value); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Pipe proxy response to client response |  | ||||||
|         proxyRes.pipe(res); |  | ||||||
|          |  | ||||||
|         // Increment served requests counter when the response finishes |  | ||||||
|         res.on('finish', () => { |  | ||||||
|           if (this.metricsTracker) { |  | ||||||
|             this.metricsTracker.incrementRequestsServed(); |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // Log the completed request |  | ||||||
|           const duration = Date.now() - startTime; |  | ||||||
|           this.logger.debug( |  | ||||||
|             `Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`, |  | ||||||
|             { duration, statusCode: res.statusCode } |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // Handle proxy request errors |  | ||||||
|       proxyReq.on('error', (error) => { |  | ||||||
|         const duration = Date.now() - startTime; |  | ||||||
|         this.logger.error( |  | ||||||
|           `Proxy error for ${req.method} ${req.url}: ${error.message}`, |  | ||||||
|           { duration, error: error.message } |  | ||||||
|         ); |  | ||||||
|          |  | ||||||
|         // Increment failed requests counter |  | ||||||
|         if (this.metricsTracker) { |  | ||||||
|           this.metricsTracker.incrementFailedRequests(); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Check if headers have already been sent |  | ||||||
|         if (!res.headersSent) { |  | ||||||
|           res.statusCode = 502; |  | ||||||
|           res.end(`Bad Gateway: ${error.message}`); |  | ||||||
|         } else { |  | ||||||
|           // If headers already sent, just close the connection |  | ||||||
|           res.end(); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // Pipe request body to proxy request and handle client-side errors |  | ||||||
|       req.pipe(proxyReq); |  | ||||||
|        |  | ||||||
|       // Handle client disconnection |  | ||||||
|       req.on('error', (error) => { |  | ||||||
|         this.logger.debug(`Client connection error: ${error.message}`); |  | ||||||
|         proxyReq.destroy(); |  | ||||||
|          |  | ||||||
|         // Increment failed requests counter on client errors |  | ||||||
|         if (this.metricsTracker) { |  | ||||||
|           this.metricsTracker.incrementFailedRequests(); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       // Handle response errors |  | ||||||
|       res.on('error', (error) => { |  | ||||||
|         this.logger.debug(`Response error: ${error.message}`); |  | ||||||
|         proxyReq.destroy(); |  | ||||||
|          |  | ||||||
|         // Increment failed requests counter on response errors |  | ||||||
|         if (this.metricsTracker) { |  | ||||||
|           this.metricsTracker.incrementFailedRequests(); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|     } catch (error) { |  | ||||||
|       // Handle any unexpected errors |  | ||||||
|       this.logger.error( |  | ||||||
|         `Unexpected error handling request: ${error.message}`, |  | ||||||
|         { error: error.stack } |  | ||||||
|       ); |  | ||||||
|        |  | ||||||
|       // Increment failed requests counter |  | ||||||
|       if (this.metricsTracker) { |  | ||||||
|         this.metricsTracker.incrementFailedRequests(); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       if (!res.headersSent) { |  | ||||||
|         res.statusCode = 500; |  | ||||||
|         res.end('Internal Server Error'); |  | ||||||
|       } else { |  | ||||||
|         res.end(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ export interface IRouteContext { | |||||||
|   routeId?: string;      // The ID of the matched route |   routeId?: string;      // The ID of the matched route | ||||||
|  |  | ||||||
|   // Target information (resolved from dynamic mapping) |   // Target information (resolved from dynamic mapping) | ||||||
|   targetHost?: string;   // The resolved target host |   targetHost?: string | string[];   // The resolved target host(s) | ||||||
|   targetPort?: number;   // The resolved target port |   targetPort?: number;   // The resolved target port | ||||||
|  |  | ||||||
|   // Additional properties |   // Additional properties | ||||||
| @@ -68,8 +68,8 @@ export interface IRouteContext { | |||||||
|  * Target configuration for forwarding |  * Target configuration for forwarding | ||||||
|  */ |  */ | ||||||
| export interface IRouteTarget { | export interface IRouteTarget { | ||||||
|   host: string | string[] | ((context: any) => string | string[]);  // Support static or dynamic host selection with any compatible context |   host: string | string[] | ((context: IRouteContext) => string | string[]);  // Host or hosts with optional function for dynamic resolution | ||||||
|   port: number | ((context: any) => number);  // Support static or dynamic port mapping with any compatible context |   port: number | ((context: IRouteContext) => number);  // Port with optional function for dynamic mapping | ||||||
|   preservePort?: boolean;   // Use incoming port as target port (ignored if port is a function) |   preservePort?: boolean;   // Use incoming port as target port (ignored if port is a function) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -108,7 +108,8 @@ export interface IRouteAuthentication { | |||||||
|   oauthClientId?: string; |   oauthClientId?: string; | ||||||
|   oauthClientSecret?: string; |   oauthClientSecret?: string; | ||||||
|   oauthRedirectUri?: string; |   oauthRedirectUri?: string; | ||||||
|   [key: string]: any;  // Allow additional auth-specific options |   // Specific options for different auth types | ||||||
|  |   options?: Record<string, unknown>; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -1,498 +0,0 @@ | |||||||
| import type { |  | ||||||
|   IRouteConfig, |  | ||||||
|   IRouteMatch, |  | ||||||
|   IRouteAction, |  | ||||||
|   IRouteTarget, |  | ||||||
|   IRouteTls, |  | ||||||
|   IRouteRedirect, |  | ||||||
|   IRouteSecurity, |  | ||||||
|   IRouteAdvanced, |  | ||||||
|   TPortRange |  | ||||||
| } from './models/route-types.js'; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Basic helper function to create a route configuration |  | ||||||
|  */ |  | ||||||
| export function createRoute( |  | ||||||
|   match: IRouteMatch, |  | ||||||
|   action: IRouteAction, |  | ||||||
|   metadata?: { |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return { |  | ||||||
|     match, |  | ||||||
|     action, |  | ||||||
|     ...metadata |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a basic HTTP route configuration |  | ||||||
|  */ |  | ||||||
| export function createHttpRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 80 |  | ||||||
|     domains?: string | string[]; |  | ||||||
|     path?: string; |  | ||||||
|     target: IRouteTarget; |  | ||||||
|     headers?: Record<string, string>; |  | ||||||
|     security?: IRouteSecurity; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || 80, |  | ||||||
|       ...(options.domains ? { domains: options.domains } : {}), |  | ||||||
|       ...(options.path ? { path: options.path } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'forward', |  | ||||||
|       target: options.target, |  | ||||||
|       ...(options.headers || options.security ? { |  | ||||||
|         advanced: { |  | ||||||
|           ...(options.headers ? { headers: options.headers } : {}) |  | ||||||
|         }, |  | ||||||
|         ...(options.security ? { security: options.security } : {}) |  | ||||||
|       } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'HTTP Route', |  | ||||||
|       description: options.description, |  | ||||||
|       priority: options.priority, |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create an HTTPS route configuration with TLS termination |  | ||||||
|  */ |  | ||||||
| export function createHttpsRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 443 |  | ||||||
|     domains: string | string[]; |  | ||||||
|     path?: string; |  | ||||||
|     target: IRouteTarget; |  | ||||||
|     tlsMode?: 'terminate' | 'terminate-and-reencrypt'; |  | ||||||
|     certificate?: 'auto' | { key: string; cert: string }; |  | ||||||
|     headers?: Record<string, string>; |  | ||||||
|     security?: IRouteSecurity; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || 443, |  | ||||||
|       domains: options.domains, |  | ||||||
|       ...(options.path ? { path: options.path } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'forward', |  | ||||||
|       target: options.target, |  | ||||||
|       tls: { |  | ||||||
|         mode: options.tlsMode || 'terminate', |  | ||||||
|         certificate: options.certificate || 'auto' |  | ||||||
|       }, |  | ||||||
|       ...(options.headers || options.security ? { |  | ||||||
|         advanced: { |  | ||||||
|           ...(options.headers ? { headers: options.headers } : {}) |  | ||||||
|         }, |  | ||||||
|         ...(options.security ? { security: options.security } : {}) |  | ||||||
|       } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'HTTPS Route', |  | ||||||
|       description: options.description, |  | ||||||
|       priority: options.priority, |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create an HTTPS passthrough route configuration |  | ||||||
|  */ |  | ||||||
| export function createPassthroughRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 443 |  | ||||||
|     domains?: string | string[]; |  | ||||||
|     target: IRouteTarget; |  | ||||||
|     security?: IRouteSecurity; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || 443, |  | ||||||
|       ...(options.domains ? { domains: options.domains } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'forward', |  | ||||||
|       target: options.target, |  | ||||||
|       tls: { |  | ||||||
|         mode: 'passthrough' |  | ||||||
|       }, |  | ||||||
|       ...(options.security ? { security: options.security } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'HTTPS Passthrough Route', |  | ||||||
|       description: options.description, |  | ||||||
|       priority: options.priority, |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a redirect route configuration |  | ||||||
|  */ |  | ||||||
| export function createRedirectRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 80 |  | ||||||
|     domains?: string | string[]; |  | ||||||
|     path?: string; |  | ||||||
|     redirectTo: string; |  | ||||||
|     statusCode?: 301 | 302 | 307 | 308; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || 80, |  | ||||||
|       ...(options.domains ? { domains: options.domains } : {}), |  | ||||||
|       ...(options.path ? { path: options.path } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'redirect', |  | ||||||
|       redirect: { |  | ||||||
|         to: options.redirectTo, |  | ||||||
|         status: options.statusCode || 301 |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'Redirect Route', |  | ||||||
|       description: options.description, |  | ||||||
|       priority: options.priority, |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create an HTTP to HTTPS redirect route configuration |  | ||||||
|  */ |  | ||||||
| export function createHttpToHttpsRedirect( |  | ||||||
|   options: { |  | ||||||
|     domains: string | string[]; |  | ||||||
|     statusCode?: 301 | 302 | 307 | 308; |  | ||||||
|     name?: string; |  | ||||||
|     priority?: number; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains]; |  | ||||||
|  |  | ||||||
|   return createRedirectRoute({ |  | ||||||
|     ports: 80, |  | ||||||
|     domains: options.domains, |  | ||||||
|     redirectTo: 'https://{domain}{path}', |  | ||||||
|     statusCode: options.statusCode || 301, |  | ||||||
|     name: options.name || `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`, |  | ||||||
|     priority: options.priority || 100 // High priority for redirects |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a block route configuration |  | ||||||
|  */ |  | ||||||
| export function createBlockRoute( |  | ||||||
|   options: { |  | ||||||
|     ports: number | number[]; |  | ||||||
|     domains?: string | string[]; |  | ||||||
|     clientIp?: string[]; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports, |  | ||||||
|       ...(options.domains ? { domains: options.domains } : {}), |  | ||||||
|       ...(options.clientIp ? { clientIp: options.clientIp } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'block' |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'Block Route', |  | ||||||
|       description: options.description, |  | ||||||
|       priority: options.priority || 1000, // Very high priority for blocks |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a load balancer route configuration |  | ||||||
|  */ |  | ||||||
| export function createLoadBalancerRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 443 |  | ||||||
|     domains: string | string[]; |  | ||||||
|     path?: string; |  | ||||||
|     targets: string[]; // Array of host names/IPs for load balancing |  | ||||||
|     targetPort: number; |  | ||||||
|     tlsMode?: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; |  | ||||||
|     certificate?: 'auto' | { key: string; cert: string }; |  | ||||||
|     headers?: Record<string, string>; |  | ||||||
|     security?: IRouteSecurity; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   const useTls = options.tlsMode !== undefined; |  | ||||||
|   const defaultPort = useTls ? 443 : 80; |  | ||||||
|  |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || defaultPort, |  | ||||||
|       domains: options.domains, |  | ||||||
|       ...(options.path ? { path: options.path } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'forward', |  | ||||||
|       target: { |  | ||||||
|         host: options.targets, |  | ||||||
|         port: options.targetPort |  | ||||||
|       }, |  | ||||||
|       ...(useTls ? { |  | ||||||
|         tls: { |  | ||||||
|           mode: options.tlsMode!, |  | ||||||
|           ...(options.tlsMode !== 'passthrough' && options.certificate ? { |  | ||||||
|             certificate: options.certificate |  | ||||||
|           } : {}) |  | ||||||
|         } |  | ||||||
|       } : {}), |  | ||||||
|       ...(options.headers || options.security ? { |  | ||||||
|         advanced: { |  | ||||||
|           ...(options.headers ? { headers: options.headers } : {}) |  | ||||||
|         }, |  | ||||||
|         ...(options.security ? { security: options.security } : {}) |  | ||||||
|       } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'Load Balanced Route', |  | ||||||
|       description: options.description || `Load balancing across ${options.targets.length} backends`, |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a complete HTTPS server configuration with HTTP redirect |  | ||||||
|  */ |  | ||||||
| export function createHttpsServer( |  | ||||||
|   options: { |  | ||||||
|     domains: string | string[]; |  | ||||||
|     target: IRouteTarget; |  | ||||||
|     certificate?: 'auto' | { key: string; cert: string }; |  | ||||||
|     security?: IRouteSecurity; |  | ||||||
|     addHttpRedirect?: boolean; |  | ||||||
|     name?: string; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig[] { |  | ||||||
|   const routes: IRouteConfig[] = []; |  | ||||||
|   const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains]; |  | ||||||
|  |  | ||||||
|   // Add HTTPS route |  | ||||||
|   routes.push(createHttpsRoute({ |  | ||||||
|     domains: options.domains, |  | ||||||
|     target: options.target, |  | ||||||
|     certificate: options.certificate || 'auto', |  | ||||||
|     security: options.security, |  | ||||||
|     name: options.name || `HTTPS Server for ${domainArray.join(', ')}` |  | ||||||
|   })); |  | ||||||
|  |  | ||||||
|   // Add HTTP to HTTPS redirect if requested |  | ||||||
|   if (options.addHttpRedirect !== false) { |  | ||||||
|     routes.push(createHttpToHttpsRedirect({ |  | ||||||
|       domains: options.domains, |  | ||||||
|       name: `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`, |  | ||||||
|       priority: 100 |  | ||||||
|     })); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return routes; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a port range configuration from various input formats |  | ||||||
|  */ |  | ||||||
| export function createPortRange( |  | ||||||
|   ports: number | number[] | string | Array<{ from: number; to: number }> |  | ||||||
| ): TPortRange { |  | ||||||
|   // If it's a string like "80,443" or "8000-9000", parse it |  | ||||||
|   if (typeof ports === 'string') { |  | ||||||
|     if (ports.includes('-')) { |  | ||||||
|       // Handle range like "8000-9000" |  | ||||||
|       const [start, end] = ports.split('-').map(p => parseInt(p.trim(), 10)); |  | ||||||
|       return [{ from: start, to: end }]; |  | ||||||
|     } else if (ports.includes(',')) { |  | ||||||
|       // Handle comma-separated list like "80,443,8080" |  | ||||||
|       return ports.split(',').map(p => parseInt(p.trim(), 10)); |  | ||||||
|     } else { |  | ||||||
|       // Handle single port as string |  | ||||||
|       return parseInt(ports.trim(), 10); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Otherwise return as is |  | ||||||
|   return ports; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a security configuration object |  | ||||||
|  */ |  | ||||||
| export function createSecurityConfig( |  | ||||||
|   options: { |  | ||||||
|     allowedIps?: string[]; |  | ||||||
|     blockedIps?: string[]; |  | ||||||
|     maxConnections?: number; |  | ||||||
|     authentication?: { |  | ||||||
|       type: 'basic' | 'digest' | 'oauth'; |  | ||||||
|       // Auth-specific options |  | ||||||
|       [key: string]: any; |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| ): IRouteSecurity { |  | ||||||
|   return { |  | ||||||
|     ...(options.allowedIps ? { allowedIps: options.allowedIps } : {}), |  | ||||||
|     ...(options.blockedIps ? { blockedIps: options.blockedIps } : {}), |  | ||||||
|     ...(options.maxConnections ? { maxConnections: options.maxConnections } : {}), |  | ||||||
|     ...(options.authentication ? { authentication: options.authentication } : {}) |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a static file server route |  | ||||||
|  */ |  | ||||||
| export function createStaticFileRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 80 |  | ||||||
|     domains: string | string[]; |  | ||||||
|     path?: string; |  | ||||||
|     targetDirectory: string; |  | ||||||
|     tlsMode?: 'terminate' | 'terminate-and-reencrypt'; |  | ||||||
|     certificate?: 'auto' | { key: string; cert: string }; |  | ||||||
|     headers?: Record<string, string>; |  | ||||||
|     security?: IRouteSecurity; |  | ||||||
|     name?: string; |  | ||||||
|     description?: string; |  | ||||||
|     priority?: number; |  | ||||||
|     tags?: string[]; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   const useTls = options.tlsMode !== undefined; |  | ||||||
|   const defaultPort = useTls ? 443 : 80; |  | ||||||
|  |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || defaultPort, |  | ||||||
|       domains: options.domains, |  | ||||||
|       ...(options.path ? { path: options.path } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'forward', |  | ||||||
|       target: { |  | ||||||
|         host: 'localhost', // Static file serving is typically handled locally |  | ||||||
|         port: 0, // Special value indicating a static file server |  | ||||||
|         preservePort: false |  | ||||||
|       }, |  | ||||||
|       ...(useTls ? { |  | ||||||
|         tls: { |  | ||||||
|           mode: options.tlsMode!, |  | ||||||
|           certificate: options.certificate || 'auto' |  | ||||||
|         } |  | ||||||
|       } : {}), |  | ||||||
|       advanced: { |  | ||||||
|         ...(options.headers ? { headers: options.headers } : {}), |  | ||||||
|         staticFiles: { |  | ||||||
|           root: options.targetDirectory, |  | ||||||
|           index: ['index.html', 'index.htm'], |  | ||||||
|           directory: options.targetDirectory // For backward compatibility |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       ...(options.security ? { security: options.security } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'Static File Server', |  | ||||||
|       description: options.description || `Serving static files from ${options.targetDirectory}`, |  | ||||||
|       priority: options.priority, |  | ||||||
|       tags: options.tags |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Create a test route for debugging purposes |  | ||||||
|  */ |  | ||||||
| export function createTestRoute( |  | ||||||
|   options: { |  | ||||||
|     ports?: number | number[]; // Default: 8000 |  | ||||||
|     domains?: string | string[]; |  | ||||||
|     path?: string; |  | ||||||
|     response?: { |  | ||||||
|       status?: number; |  | ||||||
|       headers?: Record<string, string>; |  | ||||||
|       body?: string; |  | ||||||
|     }; |  | ||||||
|     name?: string; |  | ||||||
|   } |  | ||||||
| ): IRouteConfig { |  | ||||||
|   return createRoute( |  | ||||||
|     { |  | ||||||
|       ports: options.ports || 8000, |  | ||||||
|       ...(options.domains ? { domains: options.domains } : {}), |  | ||||||
|       ...(options.path ? { path: options.path } : {}) |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       type: 'forward', |  | ||||||
|       target: { |  | ||||||
|         host: 'test',  // Special value indicating a test route |  | ||||||
|         port: 0 |  | ||||||
|       }, |  | ||||||
|       advanced: { |  | ||||||
|         testResponse: { |  | ||||||
|           status: options.response?.status || 200, |  | ||||||
|           headers: options.response?.headers || { 'Content-Type': 'text/plain' }, |  | ||||||
|           body: options.response?.body || 'Test route is working!' |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       name: options.name || 'Test Route', |  | ||||||
|       description: 'Route for testing and debugging', |  | ||||||
|       priority: 500, |  | ||||||
|       tags: ['test', 'debug'] |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Route helpers for SmartProxy |  | ||||||
|  *  |  | ||||||
|  * This module provides helper functions for creating various types of route configurations |  | ||||||
|  * to be used with the SmartProxy system. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| // Re-export all functions from the route-helpers.ts file |  | ||||||
| export * from '../route-helpers.js'; |  | ||||||
| @@ -244,21 +244,36 @@ export class RouteManager extends plugins.EventEmitter { | |||||||
|    * Match an IP against a pattern |    * Match an IP against a pattern | ||||||
|    */ |    */ | ||||||
|   private matchIpPattern(pattern: string, ip: string): boolean { |   private matchIpPattern(pattern: string, ip: string): boolean { | ||||||
|     // Handle exact match |     // Normalize IPv6-mapped IPv4 addresses | ||||||
|     if (pattern === ip) { |     const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||||
|  |     const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern; | ||||||
|  |      | ||||||
|  |     // Handle exact match with normalized addresses | ||||||
|  |     if (pattern === ip || normalizedPattern === normalizedIp ||  | ||||||
|  |         pattern === normalizedIp || normalizedPattern === ip) { | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Handle CIDR notation (e.g., 192.168.1.0/24) |     // Handle CIDR notation (e.g., 192.168.1.0/24) | ||||||
|     if (pattern.includes('/')) { |     if (pattern.includes('/')) { | ||||||
|       return this.matchIpCidr(pattern, ip); |       return this.matchIpCidr(pattern, normalizedIp) ||  | ||||||
|  |              (normalizedPattern !== pattern && this.matchIpCidr(normalizedPattern, normalizedIp)); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Handle glob pattern (e.g., 192.168.1.*) |     // Handle glob pattern (e.g., 192.168.1.*) | ||||||
|     if (pattern.includes('*')) { |     if (pattern.includes('*')) { | ||||||
|       const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); |       const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||||
|       const regex = new RegExp(`^${regexPattern}$`); |       const regex = new RegExp(`^${regexPattern}$`); | ||||||
|       return regex.test(ip); |       if (regex.test(ip) || regex.test(normalizedIp)) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // If pattern was normalized, also test with normalized pattern | ||||||
|  |       if (normalizedPattern !== pattern) { | ||||||
|  |         const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*'); | ||||||
|  |         const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`); | ||||||
|  |         return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     return false; |     return false; | ||||||
| @@ -274,9 +289,13 @@ export class RouteManager extends plugins.EventEmitter { | |||||||
|       const [subnet, bits] = cidr.split('/'); |       const [subnet, bits] = cidr.split('/'); | ||||||
|       const mask = parseInt(bits, 10); |       const mask = parseInt(bits, 10); | ||||||
|        |        | ||||||
|  |       // Normalize IPv6-mapped IPv4 addresses | ||||||
|  |       const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||||
|  |       const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet; | ||||||
|  |        | ||||||
|       // Convert IP addresses to numeric values |       // Convert IP addresses to numeric values | ||||||
|       const ipNum = this.ipToNumber(ip); |       const ipNum = this.ipToNumber(normalizedIp); | ||||||
|       const subnetNum = this.ipToNumber(subnet); |       const subnetNum = this.ipToNumber(normalizedSubnet); | ||||||
|        |        | ||||||
|       // Calculate subnet mask |       // Calculate subnet mask | ||||||
|       const maskNum = ~(2 ** (32 - mask) - 1); |       const maskNum = ~(2 ** (32 - mask) - 1); | ||||||
| @@ -293,7 +312,10 @@ export class RouteManager extends plugins.EventEmitter { | |||||||
|    * Convert an IP address to a numeric value |    * Convert an IP address to a numeric value | ||||||
|    */ |    */ | ||||||
|   private ipToNumber(ip: string): number { |   private ipToNumber(ip: string): number { | ||||||
|     const parts = ip.split('.').map(part => parseInt(part, 10)); |     // Normalize IPv6-mapped IPv4 addresses | ||||||
|  |     const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; | ||||||
|  |      | ||||||
|  |     const parts = normalizedIp.split('.').map(part => parseInt(part, 10)); | ||||||
|     return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; |     return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user