From 788ccea81e8e1d65b8e707e63691f3c20d355c20 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 26 Mar 2026 20:45:41 +0000 Subject: [PATCH] BREAKING CHANGE(smart-proxy): remove route helper APIs and standardize route configuration on plain route objects --- changelog.md | 7 + readme.hints.md | 62 +- readme.md | 286 ++++---- rust/crates/rustproxy-config/src/helpers.rs | 339 --------- rust/crates/rustproxy-config/src/lib.rs | 2 - .../rustproxy-config/src/proxy_options.rs | 50 +- .../rustproxy-config/src/route_types.rs | 25 + .../crates/rustproxy-config/src/validation.rs | 44 +- rust/crates/rustproxy/src/lib.rs | 34 +- test/test.bun.ts | 69 +- test/test.deno.ts | 72 +- test/test.forwarding.examples.ts | 181 ++--- test/test.forwarding.ts | 66 +- test/test.nftables-integration.simple.ts | 90 +-- test/test.nftables-integration.ts | 191 +++-- test/test.port-mapping.ts | 186 ++--- test/test.route-config.ts | 489 +++++++------ test/test.route-utils.ts | 656 +++++------------- ts/00_commitinfo_data.ts | 2 +- ts/proxies/smart-proxy/utils/index.ts | 14 +- ts/proxies/smart-proxy/utils/route-helpers.ts | 11 - .../utils/route-helpers/api-helpers.ts | 144 ---- .../utils/route-helpers/dynamic-helpers.ts | 125 ---- .../utils/route-helpers/http-helpers.ts | 40 -- .../utils/route-helpers/https-helpers.ts | 163 ----- .../smart-proxy/utils/route-helpers/index.ts | 62 -- .../route-helpers/load-balancer-helpers.ts | 154 ---- .../utils/route-helpers/nftables-helpers.ts | 202 ------ .../utils/route-helpers/security-helpers.ts | 96 --- .../utils/route-helpers/websocket-helpers.ts | 98 --- .../{route-helpers => }/socket-handlers.ts | 34 +- tsconfig.json | 5 +- 32 files changed, 1159 insertions(+), 2840 deletions(-) delete mode 100644 rust/crates/rustproxy-config/src/helpers.rs delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/index.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts delete mode 100644 ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts rename ts/proxies/smart-proxy/utils/{route-helpers => }/socket-handlers.ts (91%) diff --git a/changelog.md b/changelog.md index aba880a..22eaf95 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-26 - 27.0.0 - BREAKING CHANGE(smart-proxy) +remove route helper APIs and standardize route configuration on plain route objects + +- Removes TypeScript route helper exports and related Rust config helpers in favor of defining routes directly with match and action properties. +- Updates documentation and tests to use plain IRouteConfig objects and SocketHandlers imports instead of helper factory functions. +- Moves socket handlers to a top-level utils export and keeps direct socket-handler route configuration as the supported pattern. + ## 2026-03-26 - 26.3.0 - feat(nftables) move NFTables forwarding management from the Rust engine to @push.rocks/smartnftables diff --git a/readme.hints.md b/readme.hints.md index 75fbb1d..e027c59 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -462,35 +462,57 @@ For TLS termination modes (`terminate` and `terminate-and-reencrypt`), SmartProx **HTTP to HTTPS Redirect**: ```typescript -import { createHttpToHttpsRedirect } from '@push.rocks/smartproxy'; +import { SocketHandlers } from '@push.rocks/smartproxy'; -const redirectRoute = createHttpToHttpsRedirect(['example.com', 'www.example.com']); +const redirectRoute = { + name: 'http-to-https', + match: { ports: 80, domains: ['example.com', 'www.example.com'] }, + action: { + type: 'socket-handler' as const, + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + } +}; ``` **Complete HTTPS Server (with redirect)**: ```typescript -import { createCompleteHttpsServer } from '@push.rocks/smartproxy'; - -const routes = createCompleteHttpsServer( - 'example.com', - { host: 'localhost', port: 8080 }, - { certificate: 'auto' } -); +const routes = [ + { + name: 'https-server', + match: { ports: 443, domains: 'example.com' }, + action: { + type: 'forward' as const, + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate' as const, certificate: 'auto' as const } + } + }, + { + name: 'http-redirect', + match: { ports: 80, domains: 'example.com' }, + action: { + type: 'socket-handler' as const, + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + } + } +]; ``` **Load Balancer with Health Checks**: ```typescript -import { createLoadBalancerRoute } from '@push.rocks/smartproxy'; - -const lbRoute = createLoadBalancerRoute( - 'api.example.com', - [ - { host: 'backend1', port: 8080 }, - { host: 'backend2', port: 8080 }, - { host: 'backend3', port: 8080 } - ], - { tls: { mode: 'terminate', certificate: 'auto' } } -); +const lbRoute = { + name: 'load-balancer', + match: { ports: 443, domains: 'api.example.com' }, + action: { + type: 'forward' as const, + targets: [ + { host: 'backend1', port: 8080 }, + { host: 'backend2', port: 8080 }, + { host: 'backend3', port: 8080 } + ], + tls: { mode: 'terminate' as const, certificate: 'auto' as const }, + loadBalancing: { algorithm: 'round-robin' as const } + } +}; ``` ### Smart SNI Requirement (v22.3+) diff --git a/readme.md b/readme.md index e503456..c2ea1c2 100644 --- a/readme.md +++ b/readme.md @@ -44,7 +44,7 @@ Whether you're building microservices, deploying edge infrastructure, proxying U Get up and running in 30 seconds: ```typescript -import { SmartProxy, createCompleteHttpsServer } from '@push.rocks/smartproxy'; +import { SmartProxy, SocketHandlers } from '@push.rocks/smartproxy'; // Create a proxy with automatic HTTPS const proxy = new SmartProxy({ @@ -53,13 +53,25 @@ const proxy = new SmartProxy({ useProduction: true }, routes: [ - // Complete HTTPS setup in one call! ✨ - ...createCompleteHttpsServer('app.example.com', { - host: 'localhost', - port: 3000 - }, { - certificate: 'auto' // Automatic Let's Encrypt cert 🎩 - }) + // HTTPS route with automatic Let's Encrypt cert + { + name: 'https-app', + match: { ports: 443, domains: 'app.example.com' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 3000 }], + tls: { mode: 'terminate', certificate: 'auto' } + } + }, + // HTTP → HTTPS redirect + { + name: 'http-redirect', + match: { ports: 80, domains: 'app.example.com' }, + action: { + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + } + } ] }); @@ -111,31 +123,38 @@ SmartProxy supports three TLS handling modes: ### 🌐 HTTP to HTTPS Redirect ```typescript -import { SmartProxy, createHttpToHttpsRedirect } from '@push.rocks/smartproxy'; +import { SmartProxy, SocketHandlers } from '@push.rocks/smartproxy'; const proxy = new SmartProxy({ - routes: [ - createHttpToHttpsRedirect(['example.com', '*.example.com']) - ] + routes: [{ + name: 'http-to-https', + match: { ports: 80, domains: ['example.com', '*.example.com'] }, + action: { + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + } + }] }); ``` ### ⚖️ Load Balancer with Health Checks ```typescript -import { SmartProxy, createLoadBalancerRoute } from '@push.rocks/smartproxy'; +import { SmartProxy } from '@push.rocks/smartproxy'; const proxy = new SmartProxy({ - routes: [ - createLoadBalancerRoute( - 'app.example.com', - [ + routes: [{ + name: 'load-balancer', + match: { ports: 443, domains: 'app.example.com' }, + action: { + type: 'forward', + targets: [ { host: 'server1.internal', port: 8080 }, { host: 'server2.internal', port: 8080 }, { host: 'server3.internal', port: 8080 } ], - { - tls: { mode: 'terminate', certificate: 'auto' }, + tls: { mode: 'terminate', certificate: 'auto' }, + loadBalancing: { algorithm: 'round-robin', healthCheck: { path: '/health', @@ -145,57 +164,68 @@ const proxy = new SmartProxy({ healthyThreshold: 2 } } - ) - ] + } + }] }); ``` ### 🔌 WebSocket Proxy ```typescript -import { SmartProxy, createWebSocketRoute } from '@push.rocks/smartproxy'; +import { SmartProxy } from '@push.rocks/smartproxy'; const proxy = new SmartProxy({ - routes: [ - createWebSocketRoute( - 'ws.example.com', - { host: 'websocket-server', port: 8080 }, - { - path: '/socket', - useTls: true, - certificate: 'auto', + routes: [{ + name: 'websocket', + match: { ports: 443, domains: 'ws.example.com', path: '/socket' }, + priority: 100, + action: { + type: 'forward', + targets: [{ host: 'websocket-server', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + websocket: { + enabled: true, pingInterval: 30000, pingTimeout: 10000 } - ) - ] + } + }] }); ``` ### 🚦 API Gateway with Rate Limiting ```typescript -import { SmartProxy, createApiGatewayRoute, addRateLimiting } from '@push.rocks/smartproxy'; +import { SmartProxy } from '@push.rocks/smartproxy'; -let apiRoute = createApiGatewayRoute( - 'api.example.com', - '/api', - { host: 'api-backend', port: 8080 }, - { - useTls: true, - certificate: 'auto', - addCorsHeaders: true - } -); - -// Add rate limiting — 100 requests per minute per IP -apiRoute = addRateLimiting(apiRoute, { - maxRequests: 100, - window: 60, - keyBy: 'ip' +const proxy = new SmartProxy({ + routes: [{ + name: 'api-gateway', + match: { ports: 443, domains: 'api.example.com', path: '/api/*' }, + priority: 100, + action: { + type: 'forward', + targets: [{ host: 'api-backend', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + headers: { + response: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400' + } + }, + security: { + rateLimit: { + enabled: true, + maxRequests: 100, + window: 60, + keyBy: 'ip' + } + } + }] }); - -const proxy = new SmartProxy({ routes: [apiRoute] }); ``` ### 🎮 Custom Protocol Handler (TCP) @@ -203,36 +233,40 @@ const proxy = new SmartProxy({ routes: [apiRoute] }); SmartProxy lets you implement any protocol with full socket control. Routes with JavaScript socket handlers are automatically relayed from the Rust engine back to your TypeScript code: ```typescript -import { SmartProxy, createSocketHandlerRoute, SocketHandlers } from '@push.rocks/smartproxy'; +import { SmartProxy, SocketHandlers } from '@push.rocks/smartproxy'; -// Use pre-built handlers -const echoRoute = createSocketHandlerRoute( - 'echo.example.com', - 7777, - SocketHandlers.echo -); +const proxy = new SmartProxy({ + routes: [ + // Use pre-built handlers + { + name: 'echo-server', + match: { ports: 7777, domains: 'echo.example.com' }, + action: { type: 'socket-handler', socketHandler: SocketHandlers.echo } + }, + // Or create your own custom protocol + { + name: 'custom-protocol', + match: { ports: 9999, domains: 'custom.example.com' }, + action: { + type: 'socket-handler', + socketHandler: async (socket) => { + console.log(`New connection on custom protocol`); + socket.write('Welcome to my custom protocol!\n'); -// Or create your own custom protocol -const customRoute = createSocketHandlerRoute( - 'custom.example.com', - 9999, - async (socket) => { - console.log(`New connection on custom protocol`); - socket.write('Welcome to my custom protocol!\n'); - - socket.on('data', (data) => { - const command = data.toString().trim(); - switch (command) { - case 'PING': socket.write('PONG\n'); break; - case 'TIME': socket.write(`${new Date().toISOString()}\n`); break; - case 'QUIT': socket.end('Goodbye!\n'); break; - default: socket.write(`Unknown: ${command}\n`); + socket.on('data', (data) => { + const command = data.toString().trim(); + switch (command) { + case 'PING': socket.write('PONG\n'); break; + case 'TIME': socket.write(`${new Date().toISOString()}\n`); break; + case 'QUIT': socket.end('Goodbye!\n'); break; + default: socket.write(`Unknown: ${command}\n`); + } + }); + } } - }); - } -); - -const proxy = new SmartProxy({ routes: [echoRoute, customRoute] }); + } + ] +}); ``` **Pre-built Socket Handlers:** @@ -387,20 +421,23 @@ const dualStackRoute: IRouteConfig = { For ultra-low latency on Linux, use kernel-level forwarding via [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables) (requires root): ```typescript -import { SmartProxy, createNfTablesTerminateRoute } from '@push.rocks/smartproxy'; +import { SmartProxy } from '@push.rocks/smartproxy'; const proxy = new SmartProxy({ - routes: [ - createNfTablesTerminateRoute( - 'fast.example.com', - { host: 'backend', port: 8080 }, - { - ports: 443, - certificate: 'auto', + routes: [{ + name: 'nftables-fast', + match: { ports: 443, domains: 'fast.example.com' }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'backend', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + nftables: { + protocol: 'tcp', preserveSourceIP: true // Backend sees real client IP } - ) - ] + } + }] }); ``` @@ -409,15 +446,18 @@ const proxy = new SmartProxy({ Forward encrypted traffic to backends without terminating TLS — the proxy routes based on the SNI hostname alone: ```typescript -import { SmartProxy, createHttpsPassthroughRoute } from '@push.rocks/smartproxy'; +import { SmartProxy } from '@push.rocks/smartproxy'; const proxy = new SmartProxy({ - routes: [ - createHttpsPassthroughRoute('secure.example.com', { - host: 'backend-that-handles-tls', - port: 8443 - }) - ] + routes: [{ + name: 'sni-passthrough', + match: { ports: 443, domains: 'secure.example.com' }, + action: { + type: 'forward', + targets: [{ host: 'backend-that-handles-tls', port: 8443 }], + tls: { mode: 'passthrough' } + } + }] }); ``` @@ -524,15 +564,7 @@ Comprehensive per-route security options: } ``` -**Security modifier helpers** let you add security to any existing route: - -```typescript -import { addRateLimiting, addBasicAuth, addJwtAuth } from '@push.rocks/smartproxy'; - -let route = createHttpsTerminateRoute('api.example.com', { host: 'backend', port: 8080 }); -route = addRateLimiting(route, { maxRequests: 100, window: 60, keyBy: 'ip' }); -route = addBasicAuth(route, { users: [{ username: 'admin', password: 'secret' }] }); -``` +Security options are configured directly on each route's `security` property — no separate helpers needed. ### 📊 Runtime Management @@ -712,7 +744,7 @@ SmartProxy uses a hybrid **Rust + TypeScript** architecture: ``` - **Rust Engine** handles all networking: TCP, UDP, TLS, QUIC, HTTP proxying, connection management, security, and metrics -- **TypeScript** provides the npm API, configuration types, route helpers, validation, and handler callbacks +- **TypeScript** provides the npm API, configuration types, validation, and handler callbacks - **NFTables** managed by [`@push.rocks/smartnftables`](https://code.foss.global/push.rocks/smartnftables) — kernel-level DNAT/SNAT forwarding, firewall rules, and rate limiting via the `nft` CLI - **IPC** — The TypeScript wrapper uses JSON commands/events over stdin/stdout to communicate with the Rust binary - **Socket/Datagram Relay** — Unix domain socket servers for routes requiring TypeScript-side handling (socket handlers, datagram handlers, dynamic host/port functions) @@ -858,47 +890,13 @@ interface IRouteQuic { } ``` -## 🛠️ Helper Functions Reference - -All helpers are fully typed and return `IRouteConfig` or `IRouteConfig[]`: +## 🛠️ Exports Reference ```typescript import { - // HTTP/HTTPS - createHttpRoute, // Plain HTTP route - createHttpsTerminateRoute, // HTTPS with TLS termination - createHttpsPassthroughRoute, // SNI passthrough (no termination) - createHttpToHttpsRedirect, // HTTP → HTTPS redirect - createCompleteHttpsServer, // HTTPS + redirect combo (returns IRouteConfig[]) - - // Load Balancing - createLoadBalancerRoute, // Multi-backend with health checks - createSmartLoadBalancer, // Dynamic domain-based backend selection - - // API & WebSocket - createApiRoute, // API route with path matching - createApiGatewayRoute, // API gateway with CORS - createWebSocketRoute, // WebSocket-enabled route - - // Custom Protocols - createSocketHandlerRoute, // Custom TCP socket handler - SocketHandlers, // Pre-built handlers (echo, proxy, block, etc.) - - // NFTables (Linux, requires root) - createNfTablesRoute, // Kernel-level packet forwarding - createNfTablesTerminateRoute, // NFTables + TLS termination - createCompleteNfTablesHttpsServer, // NFTables HTTPS + redirect combo - - // Dynamic Routing - createPortMappingRoute, // Port mapping with context - createOffsetPortMappingRoute, // Simple port offset - createDynamicRoute, // Dynamic host/port via functions - createPortOffset, // Port offset factory - - // Security Modifiers - addRateLimiting, // Add rate limiting to any route - addBasicAuth, // Add basic auth to any route - addJwtAuth, // Add JWT auth to any route + // Core + SmartProxy, // Main proxy class + SocketHandlers, // Pre-built socket handlers (echo, proxy, block, httpRedirect, httpServer, etc.) // Route Utilities mergeRouteConfigs, // Deep-merge two route configs @@ -910,7 +908,7 @@ import { } from '@push.rocks/smartproxy'; ``` -> **Tip:** For UDP datagram handler routes or QUIC/HTTP3 routes, construct `IRouteConfig` objects directly — there are no helper functions for these yet. See the [UDP Datagram Handler](#-udp-datagram-handler) and [QUIC / HTTP3 Forwarding](#-quic--http3-forwarding) examples above. +All routes are configured as plain `IRouteConfig` objects with `match` and `action` properties — see the examples throughout this document. ## 📖 API Documentation diff --git a/rust/crates/rustproxy-config/src/helpers.rs b/rust/crates/rustproxy-config/src/helpers.rs deleted file mode 100644 index 024b848..0000000 --- a/rust/crates/rustproxy-config/src/helpers.rs +++ /dev/null @@ -1,339 +0,0 @@ -use crate::route_types::*; -use crate::tls_types::*; - -/// Create a simple HTTP forwarding route. -/// Equivalent to SmartProxy's `createHttpRoute()`. -pub fn create_http_route( - domains: impl Into, - target_host: impl Into, - target_port: u16, -) -> RouteConfig { - RouteConfig { - id: None, - route_match: RouteMatch { - ports: PortRange::Single(80), - domains: Some(domains.into()), - path: None, - client_ip: None, - transport: None, - tls_version: None, - headers: None, - protocol: None, - }, - action: RouteAction { - action_type: RouteActionType::Forward, - targets: Some(vec![RouteTarget { - target_match: None, - host: HostSpec::Single(target_host.into()), - port: PortSpec::Fixed(target_port), - tls: None, - websocket: None, - load_balancing: None, - send_proxy_protocol: None, - headers: None, - advanced: None, - backend_transport: None, - priority: None, - }]), - tls: None, - websocket: None, - load_balancing: None, - advanced: None, - options: None, - send_proxy_protocol: None, - udp: None, - }, - headers: None, - security: None, - name: None, - description: None, - priority: None, - tags: None, - enabled: None, - } -} - -/// Create an HTTPS termination route. -/// Equivalent to SmartProxy's `createHttpsTerminateRoute()`. -pub fn create_https_terminate_route( - domains: impl Into, - target_host: impl Into, - target_port: u16, -) -> RouteConfig { - let mut route = create_http_route(domains, target_host, target_port); - route.route_match.ports = PortRange::Single(443); - route.action.tls = Some(RouteTls { - mode: TlsMode::Terminate, - certificate: Some(CertificateSpec::Auto("auto".to_string())), - acme: None, - versions: None, - ciphers: None, - honor_cipher_order: None, - session_timeout: None, - }); - route -} - -/// Create a TLS passthrough route. -/// Equivalent to SmartProxy's `createHttpsPassthroughRoute()`. -pub fn create_https_passthrough_route( - domains: impl Into, - target_host: impl Into, - target_port: u16, -) -> RouteConfig { - let mut route = create_http_route(domains, target_host, target_port); - route.route_match.ports = PortRange::Single(443); - route.action.tls = Some(RouteTls { - mode: TlsMode::Passthrough, - certificate: None, - acme: None, - versions: None, - ciphers: None, - honor_cipher_order: None, - session_timeout: None, - }); - route -} - -/// Create an HTTP-to-HTTPS redirect route. -/// Equivalent to SmartProxy's `createHttpToHttpsRedirect()`. -pub fn create_http_to_https_redirect( - domains: impl Into, -) -> RouteConfig { - let domains = domains.into(); - RouteConfig { - id: None, - route_match: RouteMatch { - ports: PortRange::Single(80), - domains: Some(domains), - path: None, - client_ip: None, - transport: None, - tls_version: None, - headers: None, - protocol: None, - }, - action: RouteAction { - action_type: RouteActionType::Forward, - targets: None, - tls: None, - websocket: None, - load_balancing: None, - advanced: Some(RouteAdvanced { - timeout: None, - headers: None, - keep_alive: None, - static_files: None, - test_response: Some(RouteTestResponse { - status: 301, - headers: { - let mut h = std::collections::HashMap::new(); - h.insert("Location".to_string(), "https://{domain}{path}".to_string()); - h - }, - body: String::new(), - }), - url_rewrite: None, - }), - options: None, - send_proxy_protocol: None, - udp: None, - }, - headers: None, - security: None, - name: Some("HTTP to HTTPS Redirect".to_string()), - description: None, - priority: None, - tags: None, - enabled: None, - } -} - -/// Create a complete HTTPS server with HTTP redirect. -/// Equivalent to SmartProxy's `createCompleteHttpsServer()`. -pub fn create_complete_https_server( - domain: impl Into, - target_host: impl Into, - target_port: u16, -) -> Vec { - let domain = domain.into(); - let target_host = target_host.into(); - - vec![ - create_http_to_https_redirect(DomainSpec::Single(domain.clone())), - create_https_terminate_route( - DomainSpec::Single(domain), - target_host, - target_port, - ), - ] -} - -/// Create a load balancer route. -/// Equivalent to SmartProxy's `createLoadBalancerRoute()`. -pub fn create_load_balancer_route( - domains: impl Into, - targets: Vec<(String, u16)>, - tls: Option, -) -> RouteConfig { - let route_targets: Vec = targets - .into_iter() - .map(|(host, port)| RouteTarget { - target_match: None, - host: HostSpec::Single(host), - port: PortSpec::Fixed(port), - tls: None, - websocket: None, - load_balancing: None, - send_proxy_protocol: None, - headers: None, - advanced: None, - backend_transport: None, - priority: None, - }) - .collect(); - - let port = if tls.is_some() { 443 } else { 80 }; - - RouteConfig { - id: None, - route_match: RouteMatch { - ports: PortRange::Single(port), - domains: Some(domains.into()), - path: None, - client_ip: None, - transport: None, - tls_version: None, - headers: None, - protocol: None, - }, - action: RouteAction { - action_type: RouteActionType::Forward, - targets: Some(route_targets), - tls, - websocket: None, - load_balancing: Some(RouteLoadBalancing { - algorithm: LoadBalancingAlgorithm::RoundRobin, - health_check: None, - }), - advanced: None, - options: None, - send_proxy_protocol: None, - udp: None, - }, - headers: None, - security: None, - name: Some("Load Balancer".to_string()), - description: None, - priority: None, - tags: None, - enabled: None, - } -} - -// Convenience conversions for DomainSpec -impl From<&str> for DomainSpec { - fn from(s: &str) -> Self { - DomainSpec::Single(s.to_string()) - } -} - -impl From for DomainSpec { - fn from(s: String) -> Self { - DomainSpec::Single(s) - } -} - -impl From> for DomainSpec { - fn from(v: Vec) -> Self { - DomainSpec::List(v) - } -} - -impl From> for DomainSpec { - fn from(v: Vec<&str>) -> Self { - DomainSpec::List(v.into_iter().map(|s| s.to_string()).collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::tls_types::TlsMode; - - #[test] - fn test_create_http_route() { - let route = create_http_route("example.com", "localhost", 8080); - assert_eq!(route.route_match.ports.to_ports(), vec![80]); - let domains = route.route_match.domains.as_ref().unwrap().to_vec(); - assert_eq!(domains, vec!["example.com"]); - let target = &route.action.targets.as_ref().unwrap()[0]; - assert_eq!(target.host.first(), "localhost"); - assert_eq!(target.port.resolve(80), 8080); - assert!(route.action.tls.is_none()); - } - - #[test] - fn test_create_https_terminate_route() { - let route = create_https_terminate_route("api.example.com", "backend", 3000); - assert_eq!(route.route_match.ports.to_ports(), vec![443]); - let tls = route.action.tls.as_ref().unwrap(); - assert_eq!(tls.mode, TlsMode::Terminate); - assert!(tls.certificate.as_ref().unwrap().is_auto()); - } - - #[test] - fn test_create_https_passthrough_route() { - let route = create_https_passthrough_route("secure.example.com", "backend", 443); - assert_eq!(route.route_match.ports.to_ports(), vec![443]); - let tls = route.action.tls.as_ref().unwrap(); - assert_eq!(tls.mode, TlsMode::Passthrough); - assert!(tls.certificate.is_none()); - } - - #[test] - fn test_create_http_to_https_redirect() { - let route = create_http_to_https_redirect("example.com"); - assert_eq!(route.route_match.ports.to_ports(), vec![80]); - assert!(route.action.targets.is_none()); - let test_response = route.action.advanced.as_ref().unwrap().test_response.as_ref().unwrap(); - assert_eq!(test_response.status, 301); - assert!(test_response.headers.contains_key("Location")); - } - - #[test] - fn test_create_complete_https_server() { - let routes = create_complete_https_server("example.com", "backend", 8080); - assert_eq!(routes.len(), 2); - // First route is HTTP redirect - assert_eq!(routes[0].route_match.ports.to_ports(), vec![80]); - // Second route is HTTPS terminate - assert_eq!(routes[1].route_match.ports.to_ports(), vec![443]); - } - - #[test] - fn test_create_load_balancer_route() { - let targets = vec![ - ("backend1".to_string(), 8080), - ("backend2".to_string(), 8080), - ("backend3".to_string(), 8080), - ]; - let route = create_load_balancer_route("*.example.com", targets, None); - assert_eq!(route.route_match.ports.to_ports(), vec![80]); - assert_eq!(route.action.targets.as_ref().unwrap().len(), 3); - let lb = route.action.load_balancing.as_ref().unwrap(); - assert_eq!(lb.algorithm, LoadBalancingAlgorithm::RoundRobin); - } - - #[test] - fn test_domain_spec_from_str() { - let spec: DomainSpec = "example.com".into(); - assert_eq!(spec.to_vec(), vec!["example.com"]); - } - - #[test] - fn test_domain_spec_from_vec() { - let spec: DomainSpec = vec!["a.com", "b.com"].into(); - assert_eq!(spec.to_vec(), vec!["a.com", "b.com"]); - } -} diff --git a/rust/crates/rustproxy-config/src/lib.rs b/rust/crates/rustproxy-config/src/lib.rs index e62471c..0f90ae9 100644 --- a/rust/crates/rustproxy-config/src/lib.rs +++ b/rust/crates/rustproxy-config/src/lib.rs @@ -8,7 +8,6 @@ pub mod proxy_options; pub mod tls_types; pub mod security_types; pub mod validation; -pub mod helpers; // Re-export all primary types pub use route_types::*; @@ -16,4 +15,3 @@ pub use proxy_options::*; pub use tls_types::*; pub use security_types::*; pub use validation::*; -pub use helpers::*; diff --git a/rust/crates/rustproxy-config/src/proxy_options.rs b/rust/crates/rustproxy-config/src/proxy_options.rs index 6ec5fc0..c4fa57f 100644 --- a/rust/crates/rustproxy-config/src/proxy_options.rs +++ b/rust/crates/rustproxy-config/src/proxy_options.rs @@ -331,12 +331,48 @@ impl RustProxyOptions { #[cfg(test)] mod tests { use super::*; - use crate::helpers::*; + use crate::route_types::*; + use crate::tls_types::*; + + fn make_route(domain: &str, host: &str, port: u16, listen_port: u16) -> RouteConfig { + RouteConfig { + id: None, + route_match: RouteMatch { + ports: PortRange::Single(listen_port), + domains: Some(DomainSpec::Single(domain.to_string())), + path: None, client_ip: None, transport: None, tls_version: None, headers: None, protocol: None, + }, + action: RouteAction { + action_type: RouteActionType::Forward, + targets: Some(vec![RouteTarget { + target_match: None, + host: HostSpec::Single(host.to_string()), + port: PortSpec::Fixed(port), + tls: None, websocket: None, load_balancing: None, send_proxy_protocol: None, + headers: None, advanced: None, backend_transport: None, priority: None, + }]), + tls: None, websocket: None, load_balancing: None, advanced: None, + options: None, send_proxy_protocol: None, udp: None, + }, + headers: None, security: None, name: None, description: None, + priority: None, tags: None, enabled: None, + } + } + + fn make_passthrough_route(domain: &str, host: &str, port: u16) -> RouteConfig { + let mut route = make_route(domain, host, port, 443); + route.action.tls = Some(RouteTls { + mode: TlsMode::Passthrough, + certificate: None, acme: None, versions: None, ciphers: None, + honor_cipher_order: None, session_timeout: None, + }); + route + } #[test] fn test_serde_roundtrip_minimal() { let options = RustProxyOptions { - routes: vec![create_http_route("example.com", "localhost", 8080)], + routes: vec![make_route("example.com", "localhost", 8080, 80)], ..Default::default() }; let json = serde_json::to_string(&options).unwrap(); @@ -348,8 +384,8 @@ mod tests { fn test_serde_roundtrip_full() { let options = RustProxyOptions { routes: vec![ - create_http_route("a.com", "backend1", 8080), - create_https_passthrough_route("b.com", "backend2", 443), + make_route("a.com", "backend1", 8080, 80), + make_passthrough_route("b.com", "backend2", 443), ], connection_timeout: Some(5000), socket_timeout: Some(60000), @@ -402,9 +438,9 @@ mod tests { fn test_all_listening_ports() { let options = RustProxyOptions { routes: vec![ - create_http_route("a.com", "backend", 8080), // port 80 - create_https_passthrough_route("b.com", "backend", 443), // port 443 - create_http_route("c.com", "backend", 9090), // port 80 (duplicate) + make_route("a.com", "backend", 8080, 80), // port 80 + make_passthrough_route("b.com", "backend", 443), // port 443 + make_route("c.com", "backend", 9090, 80), // port 80 (duplicate) ], ..Default::default() }; diff --git a/rust/crates/rustproxy-config/src/route_types.rs b/rust/crates/rustproxy-config/src/route_types.rs index d140b59..83d9611 100644 --- a/rust/crates/rustproxy-config/src/route_types.rs +++ b/rust/crates/rustproxy-config/src/route_types.rs @@ -79,6 +79,31 @@ impl DomainSpec { } } +// Convenience conversions for DomainSpec +impl From<&str> for DomainSpec { + fn from(s: &str) -> Self { + DomainSpec::Single(s.to_string()) + } +} + +impl From for DomainSpec { + fn from(s: String) -> Self { + DomainSpec::Single(s) + } +} + +impl From> for DomainSpec { + fn from(v: Vec) -> Self { + DomainSpec::List(v) + } +} + +impl From> for DomainSpec { + fn from(v: Vec<&str>) -> Self { + DomainSpec::List(v.into_iter().map(|s| s.to_string()).collect()) + } +} + /// Header match value: either exact string or regex pattern. /// In JSON, all values come as strings. Regex patterns are prefixed with `/` and suffixed with `/`. #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/rust/crates/rustproxy-config/src/validation.rs b/rust/crates/rustproxy-config/src/validation.rs index 4998f57..5d61214 100644 --- a/rust/crates/rustproxy-config/src/validation.rs +++ b/rust/crates/rustproxy-config/src/validation.rs @@ -104,7 +104,49 @@ mod tests { use crate::route_types::*; fn make_valid_route() -> RouteConfig { - crate::helpers::create_http_route("example.com", "localhost", 8080) + RouteConfig { + id: None, + route_match: RouteMatch { + ports: PortRange::Single(80), + domains: Some(DomainSpec::Single("example.com".to_string())), + path: None, + client_ip: None, + transport: None, + tls_version: None, + headers: None, + protocol: None, + }, + action: RouteAction { + action_type: RouteActionType::Forward, + targets: Some(vec![RouteTarget { + target_match: None, + host: HostSpec::Single("localhost".to_string()), + port: PortSpec::Fixed(8080), + tls: None, + websocket: None, + load_balancing: None, + send_proxy_protocol: None, + headers: None, + advanced: None, + backend_transport: None, + priority: None, + }]), + tls: None, + websocket: None, + load_balancing: None, + advanced: None, + options: None, + send_proxy_protocol: None, + udp: None, + }, + headers: None, + security: None, + name: None, + description: None, + priority: None, + tags: None, + enabled: None, + } } #[test] diff --git a/rust/crates/rustproxy/src/lib.rs b/rust/crates/rustproxy/src/lib.rs index 13bdf27..c2e1111 100644 --- a/rust/crates/rustproxy/src/lib.rs +++ b/rust/crates/rustproxy/src/lib.rs @@ -7,14 +7,40 @@ //! //! ```rust,no_run //! use rustproxy::RustProxy; -//! use rustproxy_config::{RustProxyOptions, create_https_passthrough_route}; +//! use rustproxy_config::*; //! //! #[tokio::main] //! async fn main() -> anyhow::Result<()> { //! let options = RustProxyOptions { -//! routes: vec![ -//! create_https_passthrough_route("example.com", "backend", 443), -//! ], +//! routes: vec![RouteConfig { +//! id: None, +//! route_match: RouteMatch { +//! ports: PortRange::Single(443), +//! domains: Some(DomainSpec::Single("example.com".to_string())), +//! path: None, client_ip: None, transport: None, +//! tls_version: None, headers: None, protocol: None, +//! }, +//! action: RouteAction { +//! action_type: RouteActionType::Forward, +//! targets: Some(vec![RouteTarget { +//! target_match: None, +//! host: HostSpec::Single("backend".to_string()), +//! port: PortSpec::Fixed(443), +//! tls: None, websocket: None, load_balancing: None, +//! send_proxy_protocol: None, headers: None, advanced: None, +//! backend_transport: None, priority: None, +//! }]), +//! tls: Some(RouteTls { +//! mode: TlsMode::Passthrough, +//! certificate: None, acme: None, versions: None, +//! ciphers: None, honor_cipher_order: None, session_timeout: None, +//! }), +//! websocket: None, load_balancing: None, advanced: None, +//! options: None, send_proxy_protocol: None, udp: None, +//! }, +//! headers: None, security: None, name: None, description: None, +//! priority: None, tags: None, enabled: None, +//! }], //! ..Default::default() //! }; //! diff --git a/test/test.bun.ts b/test/test.bun.ts index 5a7c8c0..38411ab 100644 --- a/test/test.bun.ts +++ b/test/test.bun.ts @@ -1,11 +1,5 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { - createHttpsTerminateRoute, - createCompleteHttpsServer, - createHttpRoute, -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; - import { mergeRouteConfigs, cloneRoute, @@ -19,8 +13,11 @@ import { import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; -tap.test('route creation - createHttpsTerminateRoute produces correct structure', async () => { - const route = createHttpsTerminateRoute('secure.example.com', { host: '127.0.0.1', port: 8443 }); +tap.test('route creation - HTTPS terminate route has correct structure', async () => { + const route: IRouteConfig = { + match: { ports: 443, domains: 'secure.example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 8443 }], tls: { mode: 'terminate', certificate: 'auto' } } + }; expect(route).toHaveProperty('match'); expect(route).toHaveProperty('action'); expect(route.action.type).toEqual('forward'); @@ -29,20 +26,10 @@ tap.test('route creation - createHttpsTerminateRoute produces correct structure' expect(route.match.domains).toEqual('secure.example.com'); }); -tap.test('route creation - createCompleteHttpsServer returns redirect and main route', async () => { - const routes = createCompleteHttpsServer('app.example.com', { host: '127.0.0.1', port: 3000 }); - expect(routes).toBeArray(); - expect(routes.length).toBeGreaterThanOrEqual(2); - // Should have an HTTP→HTTPS redirect and an HTTPS route - const hasRedirect = routes.some((r) => r.action.type === 'forward' && r.action.redirect !== undefined); - const hasHttps = routes.some((r) => r.action.tls?.mode === 'terminate'); - expect(hasRedirect || hasHttps).toBeTrue(); -}); - tap.test('route validation - validateRoutes on a set of routes', async () => { const routes: IRouteConfig[] = [ - createHttpRoute('a.com', { host: '127.0.0.1', port: 3000 }), - createHttpRoute('b.com', { host: '127.0.0.1', port: 4000 }), + { match: { ports: 80, domains: 'a.com' }, action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } }, + { match: { ports: 80, domains: 'b.com' }, action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 4000 }] } }, ]; const result = validateRoutes(routes); expect(result.valid).toBeTrue(); @@ -51,7 +38,7 @@ tap.test('route validation - validateRoutes on a set of routes', async () => { tap.test('route validation - validateRoutes catches invalid route in set', async () => { const routes: any[] = [ - createHttpRoute('valid.com', { host: '127.0.0.1', port: 3000 }), + { match: { ports: 80, domains: 'valid.com' }, action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } }, { match: { ports: 80 } }, // missing action ]; const result = validateRoutes(routes); @@ -60,23 +47,30 @@ tap.test('route validation - validateRoutes catches invalid route in set', async }); tap.test('path matching - routeMatchesPath with exact path', async () => { - const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - route.match.path = '/api'; + const route: IRouteConfig = { + match: { ports: 80, domains: 'example.com', path: '/api' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } + }; expect(routeMatchesPath(route, '/api')).toBeTrue(); expect(routeMatchesPath(route, '/other')).toBeFalse(); }); tap.test('path matching - route without path matches everything', async () => { - const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - // No path set, should match any path + const route: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } + }; expect(routeMatchesPath(route, '/anything')).toBeTrue(); expect(routeMatchesPath(route, '/')).toBeTrue(); }); tap.test('route merging - mergeRouteConfigs combines routes', async () => { - const base = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - base.priority = 10; - base.name = 'base-route'; + const base: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] }, + priority: 10, + name: 'base-route' + }; const merged = mergeRouteConfigs(base, { priority: 50, @@ -85,14 +79,16 @@ tap.test('route merging - mergeRouteConfigs combines routes', async () => { expect(merged.priority).toEqual(50); expect(merged.name).toEqual('merged-route'); - // Original route fields should be preserved expect(merged.match.domains).toEqual('example.com'); expect(merged.action.targets![0].host).toEqual('127.0.0.1'); }); tap.test('route merging - mergeRouteConfigs does not mutate original', async () => { - const base = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - base.name = 'original'; + const base: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] }, + name: 'original' + }; const merged = mergeRouteConfigs(base, { name: 'changed' }); expect(base.name).toEqual('original'); @@ -100,20 +96,21 @@ tap.test('route merging - mergeRouteConfigs does not mutate original', async () }); tap.test('route cloning - cloneRoute produces independent copy', async () => { - const original = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - original.priority = 42; - original.name = 'original-route'; + const original: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] }, + priority: 42, + name: 'original-route' + }; const cloned = cloneRoute(original); - // Should be equal in value expect(cloned.match.domains).toEqual('example.com'); expect(cloned.priority).toEqual(42); expect(cloned.name).toEqual('original-route'); expect(cloned.action.targets![0].host).toEqual('127.0.0.1'); expect(cloned.action.targets![0].port).toEqual(3000); - // Should be independent - modifying clone shouldn't affect original cloned.name = 'cloned-route'; cloned.priority = 99; expect(original.name).toEqual('original-route'); diff --git a/test/test.deno.ts b/test/test.deno.ts index e4c7bfc..aab2b7f 100644 --- a/test/test.deno.ts +++ b/test/test.deno.ts @@ -1,11 +1,5 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { - createHttpRoute, - createHttpsTerminateRoute, - createLoadBalancerRoute, -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; - import { findMatchingRoutes, findBestMatchingRoute, @@ -22,24 +16,11 @@ import { import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; -tap.test('route creation - createHttpRoute produces correct structure', async () => { - const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - expect(route).toHaveProperty('match'); - expect(route).toHaveProperty('action'); - expect(route.match.domains).toEqual('example.com'); - expect(route.action.type).toEqual('forward'); - expect(route.action.targets).toBeArray(); - expect(route.action.targets![0].host).toEqual('127.0.0.1'); - expect(route.action.targets![0].port).toEqual(3000); -}); - -tap.test('route creation - createHttpRoute with array of domains', async () => { - const route = createHttpRoute(['a.com', 'b.com'], { host: 'localhost', port: 8080 }); - expect(route.match.domains).toEqual(['a.com', 'b.com']); -}); - tap.test('route validation - validateRouteConfig accepts valid route', async () => { - const route = createHttpRoute('valid.example.com', { host: '10.0.0.1', port: 8080 }); + const route: IRouteConfig = { + match: { ports: 80, domains: 'valid.example.com' }, + action: { type: 'forward', targets: [{ host: '10.0.0.1', port: 8080 }] } + }; const result = validateRouteConfig(route); expect(result.valid).toBeTrue(); expect(result.errors).toHaveLength(0); @@ -67,30 +48,44 @@ tap.test('route validation - isValidPort checks correctly', async () => { }); tap.test('domain matching - exact domain', async () => { - const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); + const route: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } + }; expect(routeMatchesDomain(route, 'example.com')).toBeTrue(); expect(routeMatchesDomain(route, 'other.com')).toBeFalse(); }); tap.test('domain matching - wildcard domain', async () => { - const route = createHttpRoute('*.example.com', { host: '127.0.0.1', port: 3000 }); + const route: IRouteConfig = { + match: { ports: 80, domains: '*.example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } + }; expect(routeMatchesDomain(route, 'sub.example.com')).toBeTrue(); expect(routeMatchesDomain(route, 'example.com')).toBeFalse(); }); tap.test('port matching - single port', async () => { - const route = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - // createHttpRoute defaults to port 80 + const route: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } + }; expect(routeMatchesPort(route, 80)).toBeTrue(); expect(routeMatchesPort(route, 443)).toBeFalse(); }); tap.test('route finding - findBestMatchingRoute selects by priority', async () => { - const lowPriority = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - lowPriority.priority = 10; + const lowPriority: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] }, + priority: 10 + }; - const highPriority = createHttpRoute('example.com', { host: '127.0.0.1', port: 4000 }); - highPriority.priority = 100; + const highPriority: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 4000 }] }, + priority: 100 + }; const routes: IRouteConfig[] = [lowPriority, highPriority]; const best = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 }); @@ -100,9 +95,18 @@ tap.test('route finding - findBestMatchingRoute selects by priority', async () = }); tap.test('route finding - findMatchingRoutes returns all matches', async () => { - const route1 = createHttpRoute('example.com', { host: '127.0.0.1', port: 3000 }); - const route2 = createHttpRoute('example.com', { host: '127.0.0.1', port: 4000 }); - const route3 = createHttpRoute('other.com', { host: '127.0.0.1', port: 5000 }); + const route1: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 3000 }] } + }; + const route2: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 4000 }] } + }; + const route3: IRouteConfig = { + match: { ports: 80, domains: 'other.com' }, + action: { type: 'forward', targets: [{ host: '127.0.0.1', port: 5000 }] } + }; const matches = findMatchingRoutes([route1, route2, route3], { domain: 'example.com', port: 80 }); expect(matches).toHaveLength(2); diff --git a/test/test.forwarding.examples.ts b/test/test.forwarding.examples.ts index d56e2da..b8eaba4 100644 --- a/test/test.forwarding.examples.ts +++ b/test/test.forwarding.examples.ts @@ -2,146 +2,101 @@ import * as path from 'path'; import { tap, expect } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -import { - createHttpRoute, - createHttpsTerminateRoute, - createHttpsPassthroughRoute, - createHttpToHttpsRedirect, - createCompleteHttpsServer, - createLoadBalancerRoute, - createApiRoute, - createWebSocketRoute -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; +import { SocketHandlers } from '../ts/proxies/smart-proxy/utils/socket-handlers.js'; import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; -// Test to demonstrate various route configurations using the new helpers tap.test('Route-based configuration examples', async (tools) => { - // Example 1: HTTP-only configuration - const httpOnlyRoute = createHttpRoute( - 'http.example.com', - { - host: 'localhost', - port: 3000 - }, - { - name: 'Basic HTTP Route' - } - ); + const httpOnlyRoute: IRouteConfig = { + match: { ports: 80, domains: 'http.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'Basic HTTP Route' + }; console.log('HTTP-only route created successfully:', httpOnlyRoute.name); expect(httpOnlyRoute.action.type).toEqual('forward'); expect(httpOnlyRoute.match.domains).toEqual('http.example.com'); - // Example 2: HTTPS Passthrough (SNI) configuration - const httpsPassthroughRoute = createHttpsPassthroughRoute( - 'pass.example.com', - { - host: ['10.0.0.1', '10.0.0.2'], // Round-robin target IPs - port: 443 - }, - { - name: 'HTTPS Passthrough Route' - } - ); + const httpsPassthroughRoute: IRouteConfig = { + match: { ports: 443, domains: 'pass.example.com' }, + action: { type: 'forward', targets: [{ host: '10.0.0.1', port: 443 }, { host: '10.0.0.2', port: 443 }], tls: { mode: 'passthrough' } }, + name: 'HTTPS Passthrough Route' + }; expect(httpsPassthroughRoute).toBeTruthy(); expect(httpsPassthroughRoute.action.tls?.mode).toEqual('passthrough'); expect(Array.isArray(httpsPassthroughRoute.action.targets)).toBeTrue(); - // Example 3: HTTPS Termination to HTTP Backend - const terminateToHttpRoute = createHttpsTerminateRoute( - 'secure.example.com', - { - host: 'localhost', - port: 8080 - }, - { - certificate: 'auto', - name: 'HTTPS Termination to HTTP Backend' - } - ); + const terminateToHttpRoute: IRouteConfig = { + match: { ports: 443, domains: 'secure.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'HTTPS Termination to HTTP Backend' + }; - // Create the HTTP to HTTPS redirect for this domain - const httpToHttpsRedirect = createHttpToHttpsRedirect( - 'secure.example.com', - 443, - { - name: 'HTTP to HTTPS Redirect for secure.example.com' - } - ); + const httpToHttpsRedirect: IRouteConfig = { + match: { ports: 80, domains: 'secure.example.com' }, + action: { type: 'socket-handler', socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) }, + name: 'HTTP to HTTPS Redirect for secure.example.com' + }; expect(terminateToHttpRoute).toBeTruthy(); expect(terminateToHttpRoute.action.tls?.mode).toEqual('terminate'); - expect(httpToHttpsRedirect.action.type).toEqual('socket-handler'); - // Example 4: Load Balancer with HTTPS - const loadBalancerRoute = createLoadBalancerRoute( - 'proxy.example.com', - ['internal-api-1.local', 'internal-api-2.local'], - 8443, - { - tls: { - mode: 'terminate-and-reencrypt', - certificate: 'auto' - }, - name: 'Load Balanced HTTPS Route' - } - ); + const loadBalancerRoute: IRouteConfig = { + match: { ports: 443, domains: 'proxy.example.com' }, + action: { + type: 'forward', + targets: [ + { host: 'internal-api-1.local', port: 8443 }, + { host: 'internal-api-2.local', port: 8443 } + ], + tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' } + }, + name: 'Load Balanced HTTPS Route' + }; expect(loadBalancerRoute).toBeTruthy(); expect(loadBalancerRoute.action.tls?.mode).toEqual('terminate-and-reencrypt'); expect(Array.isArray(loadBalancerRoute.action.targets)).toBeTrue(); - // Example 5: API Route - const apiRoute = createApiRoute( - 'api.example.com', - '/api', - { host: 'localhost', port: 8081 }, - { - name: 'API Route', - useTls: true, - addCorsHeaders: true - } - ); + const apiRoute: IRouteConfig = { + match: { ports: 443, domains: 'api.example.com', path: '/api' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8081 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + name: 'API Route' + }; expect(apiRoute.action.type).toEqual('forward'); expect(apiRoute.match.path).toBeTruthy(); - // Example 6: Complete HTTPS Server with HTTP Redirect - const httpsServerRoutes = createCompleteHttpsServer( - 'complete.example.com', - { - host: 'localhost', - port: 8080 + const httpsRoute: IRouteConfig = { + match: { ports: 443, domains: 'complete.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'Complete HTTPS Server' + }; + + const httpsRedirectRoute: IRouteConfig = { + match: { ports: 80, domains: 'complete.example.com' }, + action: { type: 'socket-handler', socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) }, + name: 'Complete HTTPS Server - Redirect' + }; + + const webSocketRoute: IRouteConfig = { + match: { ports: 443, domains: 'ws.example.com', path: '/ws' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8082 }], + tls: { mode: 'terminate', certificate: 'auto' }, + websocket: { enabled: true } }, - { - certificate: 'auto', - name: 'Complete HTTPS Server' - } - ); - - expect(Array.isArray(httpsServerRoutes)).toBeTrue(); - expect(httpsServerRoutes.length).toEqual(2); // HTTPS route and HTTP redirect - expect(httpsServerRoutes[0].action.tls?.mode).toEqual('terminate'); - expect(httpsServerRoutes[1].action.type).toEqual('socket-handler'); - - // Example 7: Static File Server - removed (use nginx/apache behind proxy) - - // Example 8: WebSocket Route - const webSocketRoute = createWebSocketRoute( - 'ws.example.com', - '/ws', - { host: 'localhost', port: 8082 }, - { - useTls: true, - name: 'WebSocket Route' - } - ); + name: 'WebSocket Route' + }; expect(webSocketRoute.action.type).toEqual('forward'); expect(webSocketRoute.action.websocket?.enabled).toBeTrue(); - // Create a SmartProxy instance with all routes const allRoutes: IRouteConfig[] = [ httpOnlyRoute, httpsPassthroughRoute, @@ -149,19 +104,17 @@ tap.test('Route-based configuration examples', async (tools) => { httpToHttpsRedirect, loadBalancerRoute, apiRoute, - ...httpsServerRoutes, + httpsRoute, + httpsRedirectRoute, webSocketRoute ]; - // We're not actually starting the SmartProxy in this test, - // just verifying that the configuration is valid const smartProxy = new SmartProxy({ routes: allRoutes }); - // Just verify that all routes are configured correctly console.log(`Created ${allRoutes.length} example routes`); - expect(allRoutes.length).toEqual(9); // One less without static file route + expect(allRoutes.length).toEqual(9); }); -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/test/test.forwarding.ts b/test/test.forwarding.ts index e19eaf5..9ac1d52 100644 --- a/test/test.forwarding.ts +++ b/test/test.forwarding.ts @@ -1,27 +1,8 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; -// Import route-based helpers -import { - createHttpRoute, - createHttpsTerminateRoute, - createHttpsPassthroughRoute, - createHttpToHttpsRedirect, - createCompleteHttpsServer -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; +import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; -// Create helper functions for backward compatibility -const helpers = { - httpOnly: (domains: string | string[], target: any) => createHttpRoute(domains, target), - tlsTerminateToHttp: (domains: string | string[], target: any) => - createHttpsTerminateRoute(domains, target), - tlsTerminateToHttps: (domains: string | string[], target: any) => - createHttpsTerminateRoute(domains, target, { reencrypt: true }), - httpsPassthrough: (domains: string | string[], target: any) => - createHttpsPassthroughRoute(domains, target) -}; - -// Route-based utility functions for testing function findRouteForDomain(routes: any[], domain: string): any { return routes.find(route => { const domains = Array.isArray(route.match.domains) @@ -31,55 +12,44 @@ function findRouteForDomain(routes: any[], domain: string): any { }); } -// Replace the old test with route-based tests tap.test('Route Helpers - Create HTTP routes', async () => { - const route = helpers.httpOnly('example.com', { host: 'localhost', port: 3000 }); + const route: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] } + }; expect(route.action.type).toEqual('forward'); expect(route.match.domains).toEqual('example.com'); expect(route.action.targets?.[0]).toEqual({ host: 'localhost', port: 3000 }); }); tap.test('Route Helpers - Create HTTPS terminate to HTTP routes', async () => { - const route = helpers.tlsTerminateToHttp('secure.example.com', { host: 'localhost', port: 3000 }); + const route: IRouteConfig = { + match: { ports: 443, domains: 'secure.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }], tls: { mode: 'terminate', certificate: 'auto' } } + }; expect(route.action.type).toEqual('forward'); expect(route.match.domains).toEqual('secure.example.com'); expect(route.action.tls?.mode).toEqual('terminate'); }); tap.test('Route Helpers - Create HTTPS passthrough routes', async () => { - const route = helpers.httpsPassthrough('passthrough.example.com', { host: 'backend', port: 443 }); + const route: IRouteConfig = { + match: { ports: 443, domains: 'passthrough.example.com' }, + action: { type: 'forward', targets: [{ host: 'backend', port: 443 }], tls: { mode: 'passthrough' } } + }; expect(route.action.type).toEqual('forward'); expect(route.match.domains).toEqual('passthrough.example.com'); expect(route.action.tls?.mode).toEqual('passthrough'); }); tap.test('Route Helpers - Create HTTPS to HTTPS routes', async () => { - const route = helpers.tlsTerminateToHttps('reencrypt.example.com', { host: 'backend', port: 443 }); + const route: IRouteConfig = { + match: { ports: 443, domains: 'reencrypt.example.com' }, + action: { type: 'forward', targets: [{ host: 'backend', port: 443 }], tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' } } + }; expect(route.action.type).toEqual('forward'); expect(route.match.domains).toEqual('reencrypt.example.com'); expect(route.action.tls?.mode).toEqual('terminate-and-reencrypt'); }); -tap.test('Route Helpers - Create complete HTTPS server with redirect', async () => { - const routes = createCompleteHttpsServer( - 'full.example.com', - { host: 'localhost', port: 3000 }, - { certificate: 'auto' } - ); - - expect(routes.length).toEqual(2); - - // Check HTTP to HTTPS redirect - find route by port - const redirectRoute = routes.find(r => r.match.ports === 80); - expect(redirectRoute.action.type).toEqual('socket-handler'); - expect(redirectRoute.action.socketHandler).toBeDefined(); - expect(redirectRoute.match.ports).toEqual(80); - - // Check HTTPS route - const httpsRoute = routes.find(r => r.action.type === 'forward'); - expect(httpsRoute.match.ports).toEqual(443); - expect(httpsRoute.action.tls?.mode).toEqual('terminate'); -}); - -// Export test runner -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/test/test.nftables-integration.simple.ts b/test/test.nftables-integration.simple.ts index 8282cdb..2973baf 100644 --- a/test/test.nftables-integration.simple.ts +++ b/test/test.nftables-integration.simple.ts @@ -1,15 +1,14 @@ import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as child_process from 'child_process'; import { promisify } from 'util'; +import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; + const exec = promisify(child_process.exec); -// Check if we have root privileges to run NFTables tests async function checkRootPrivileges(): Promise { try { - // Check if we're running as root const { stdout } = await exec('id -u'); return stdout.trim() === '0'; } catch (err) { @@ -17,7 +16,6 @@ async function checkRootPrivileges(): Promise { } } -// Check if tests should run const isRoot = await checkRootPrivileges(); if (!isRoot) { @@ -29,68 +27,70 @@ if (!isRoot) { console.log(''); } -// Define the test with proper skip condition const testFn = isRoot ? tap.test : tap.skip.test; testFn('NFTables integration tests', async () => { - + console.log('Running NFTables tests with root privileges'); - - // Create test routes - const routes = [ - createNfTablesRoute('tcp-forward', { - host: 'localhost', - port: 8080 - }, { - ports: 9080, - protocol: 'tcp' - }), - - createNfTablesRoute('udp-forward', { - host: 'localhost', - port: 5353 - }, { - ports: 5354, - protocol: 'udp' - }), - - createNfTablesRoute('port-range', { - host: 'localhost', - port: 8080 - }, { - ports: [{ from: 9000, to: 9100 }], - protocol: 'tcp' - }) + + const routes: IRouteConfig[] = [ + { + match: { ports: 9080 }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: 8080 }], + nftables: { protocol: 'tcp' } + }, + name: 'tcp-forward' + }, + + { + match: { ports: 5354 }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: 5353 }], + nftables: { protocol: 'udp' } + }, + name: 'udp-forward' + }, + + { + match: { ports: [{ from: 9000, to: 9100 }] }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: 8080 }], + nftables: { protocol: 'tcp' } + }, + name: 'port-range' + } ]; - + const smartProxy = new SmartProxy({ enableDetailedLogging: true, routes }); - - // Start the proxy + await smartProxy.start(); console.log('SmartProxy started with NFTables routes'); - - // Get NFTables status + const status = await smartProxy.getNfTablesStatus(); console.log('NFTables status:', JSON.stringify(status, null, 2)); - - // Verify all routes are provisioned + expect(Object.keys(status).length).toEqual(routes.length); - + for (const routeStatus of Object.values(status)) { expect(routeStatus.active).toBeTrue(); expect(routeStatus.ruleCount.total).toBeGreaterThan(0); } - - // Stop the proxy + await smartProxy.stop(); console.log('SmartProxy stopped'); - - // Verify all rules are cleaned up + const finalStatus = await smartProxy.getNfTablesStatus(); expect(Object.keys(finalStatus).length).toEqual(0); }); -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/test/test.nftables-integration.ts b/test/test.nftables-integration.ts index 31ed206..06d3650 100644 --- a/test/test.nftables-integration.ts +++ b/test/test.nftables-integration.ts @@ -1,5 +1,4 @@ import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as http from 'http'; @@ -10,13 +9,13 @@ import { fileURLToPath } from 'url'; import * as child_process from 'child_process'; import { promisify } from 'util'; +import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; + const exec = promisify(child_process.exec); -// Get __dirname equivalent for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Check if we have root privileges async function checkRootPrivileges(): Promise { try { const { stdout } = await exec('id -u'); @@ -26,7 +25,6 @@ async function checkRootPrivileges(): Promise { } } -// Check if tests should run const runTests = await checkRootPrivileges(); if (!runTests) { @@ -36,10 +34,8 @@ if (!runTests) { console.log('Skipping NFTables integration tests'); console.log('========================================'); console.log(''); - // Skip tests when not running as root - tests are marked with tap.skip.test } -// Test server and client utilities let testTcpServer: net.Server; let testHttpServer: http.Server; let testHttpsServer: https.Server; @@ -53,10 +49,8 @@ const PROXY_HTTP_PORT = 5001; const PROXY_HTTPS_PORT = 5002; const TEST_DATA = 'Hello through NFTables!'; -// Helper to create test certificates async function createTestCertificates() { try { - // Import the certificate helper const certsModule = await import('./helpers/certificates.js'); const certificates = certsModule.loadTestCertificates(); return { @@ -65,7 +59,6 @@ async function createTestCertificates() { }; } catch (err) { console.error('Failed to load test certificates:', err); - // Use dummy certificates for testing return { cert: fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'cert.pem'), 'utf8'), key: fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'key.pem'), 'utf8') @@ -75,111 +68,112 @@ async function createTestCertificates() { tap.skip.test('setup NFTables integration test environment', async () => { console.log('Running NFTables integration tests with root privileges'); - - // Create a basic TCP test server + testTcpServer = net.createServer((socket) => { socket.on('data', (data) => { socket.write(`Server says: ${data.toString()}`); }); }); - + await new Promise((resolve) => { testTcpServer.listen(TEST_TCP_PORT, () => { console.log(`TCP test server listening on port ${TEST_TCP_PORT}`); resolve(); }); }); - - // Create an HTTP test server + testHttpServer = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(`HTTP Server says: ${TEST_DATA}`); }); - + await new Promise((resolve) => { testHttpServer.listen(TEST_HTTP_PORT, () => { console.log(`HTTP test server listening on port ${TEST_HTTP_PORT}`); resolve(); }); }); - - // Create an HTTPS test server + const certs = await createTestCertificates(); testHttpsServer = https.createServer({ key: certs.key, cert: certs.cert }, (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(`HTTPS Server says: ${TEST_DATA}`); }); - + await new Promise((resolve) => { testHttpsServer.listen(TEST_HTTPS_PORT, () => { console.log(`HTTPS test server listening on port ${TEST_HTTPS_PORT}`); resolve(); }); }); - - // Create SmartProxy with various NFTables routes + smartProxy = new SmartProxy({ enableDetailedLogging: true, routes: [ - // TCP forwarding route - createNfTablesRoute('tcp-nftables', { - host: 'localhost', - port: TEST_TCP_PORT - }, { - ports: PROXY_TCP_PORT, - protocol: 'tcp' - }), - - // HTTP forwarding route - createNfTablesRoute('http-nftables', { - host: 'localhost', - port: TEST_HTTP_PORT - }, { - ports: PROXY_HTTP_PORT, - protocol: 'tcp' - }), - - // HTTPS termination route - createNfTablesTerminateRoute('https-nftables.example.com', { - host: 'localhost', - port: TEST_HTTPS_PORT - }, { - ports: PROXY_HTTPS_PORT, - protocol: 'tcp', - certificate: certs - }), - - // Route with IP allow list - createNfTablesRoute('secure-tcp', { - host: 'localhost', - port: TEST_TCP_PORT - }, { - ports: 5003, - protocol: 'tcp', - ipAllowList: ['127.0.0.1', '::1'] - }), - - // Route with QoS settings - createNfTablesRoute('qos-tcp', { - host: 'localhost', - port: TEST_TCP_PORT - }, { - ports: 5004, - protocol: 'tcp', - maxRate: '10mbps', - priority: 1 - }) + { + match: { ports: PROXY_TCP_PORT }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: TEST_TCP_PORT }], + nftables: { protocol: 'tcp' } + }, + name: 'tcp-nftables' + }, + + { + match: { ports: PROXY_HTTP_PORT }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: TEST_HTTP_PORT }], + nftables: { protocol: 'tcp' } + }, + name: 'http-nftables' + }, + + { + match: { ports: PROXY_HTTPS_PORT, domains: 'https-nftables.example.com' }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: TEST_HTTPS_PORT }], + tls: { mode: 'terminate', certificate: 'auto' }, + nftables: { protocol: 'tcp' } + }, + name: 'https-nftables' + }, + + { + match: { ports: 5003 }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: TEST_TCP_PORT }], + nftables: { protocol: 'tcp', ipAllowList: ['127.0.0.1', '::1'] } + }, + name: 'secure-tcp' + }, + + { + match: { ports: 5004 }, + action: { + type: 'forward', + forwardingEngine: 'nftables', + targets: [{ host: 'localhost', port: TEST_TCP_PORT }], + nftables: { protocol: 'tcp', maxRate: '10mbps', priority: 1 } + }, + name: 'qos-tcp' + } ] }); - + console.log('SmartProxy created, now starting...'); - - // Start the proxy + try { await smartProxy.start(); console.log('SmartProxy started successfully'); - - // Verify proxy is listening on expected ports + const listeningPorts = smartProxy.getListeningPorts(); console.log(`SmartProxy is listening on ports: ${listeningPorts.join(', ')}`); } catch (err) { @@ -190,8 +184,7 @@ tap.skip.test('setup NFTables integration test environment', async () => { tap.skip.test('should forward TCP connections through NFTables', async () => { console.log(`Attempting to connect to proxy TCP port ${PROXY_TCP_PORT}...`); - - // First verify our test server is running + try { const testClient = new net.Socket(); await new Promise((resolve, reject) => { @@ -205,40 +198,39 @@ tap.skip.test('should forward TCP connections through NFTables', async () => { } catch (err) { console.error(`Test server on port ${TEST_TCP_PORT} is not accessible: ${err}`); } - - // Connect to the proxy port + const client = new net.Socket(); - + const response = await new Promise((resolve, reject) => { let responseData = ''; const timeout = setTimeout(() => { client.destroy(); reject(new Error(`Connection timeout after 5 seconds to proxy port ${PROXY_TCP_PORT}`)); }, 5000); - + client.connect(PROXY_TCP_PORT, 'localhost', () => { console.log(`Connected to proxy port ${PROXY_TCP_PORT}, sending data...`); client.write(TEST_DATA); }); - + client.on('data', (data) => { console.log(`Received data from proxy: ${data.toString()}`); responseData += data.toString(); client.end(); }); - + client.on('end', () => { clearTimeout(timeout); resolve(responseData); }); - + client.on('error', (err) => { clearTimeout(timeout); console.error(`Connection error on proxy port ${PROXY_TCP_PORT}: ${err.message}`); reject(err); }); }); - + expect(response).toEqual(`Server says: ${TEST_DATA}`); }); @@ -254,21 +246,20 @@ tap.skip.test('should forward HTTP connections through NFTables', async () => { }); }).on('error', reject); }); - + expect(response).toEqual(`HTTP Server says: ${TEST_DATA}`); }); tap.skip.test('should handle HTTPS termination with NFTables', async () => { - // Skip this test if running without proper certificates const response = await new Promise((resolve, reject) => { const options = { hostname: 'localhost', port: PROXY_HTTPS_PORT, path: '/', method: 'GET', - rejectUnauthorized: false // For self-signed cert + rejectUnauthorized: false }; - + https.get(options, (res) => { let data = ''; res.on('data', (chunk) => { @@ -279,43 +270,40 @@ tap.skip.test('should handle HTTPS termination with NFTables', async () => { }); }).on('error', reject); }); - + expect(response).toEqual(`HTTPS Server says: ${TEST_DATA}`); }); tap.skip.test('should respect IP allow lists in NFTables', async () => { - // This test should pass since we're connecting from localhost const client = new net.Socket(); - + const connected = await new Promise((resolve) => { const timeout = setTimeout(() => { client.destroy(); resolve(false); }, 2000); - + client.connect(5003, 'localhost', () => { clearTimeout(timeout); client.end(); resolve(true); }); - + client.on('error', () => { clearTimeout(timeout); resolve(false); }); }); - + expect(connected).toBeTrue(); }); tap.skip.test('should get NFTables status', async () => { const status = await smartProxy.getNfTablesStatus(); - - // Check that we have status for our routes + const statusKeys = Object.keys(status); expect(statusKeys.length).toBeGreaterThan(0); - - // Check status structure for one of the routes + const firstStatus = status[statusKeys[0]]; expect(firstStatus).toHaveProperty('active'); expect(firstStatus).toHaveProperty('ruleCount'); @@ -324,21 +312,20 @@ tap.skip.test('should get NFTables status', async () => { }); tap.skip.test('cleanup NFTables integration test environment', async () => { - // Stop the proxy and test servers await smartProxy.stop(); - + await new Promise((resolve) => { testTcpServer.close(() => { resolve(); }); }); - + await new Promise((resolve) => { testHttpServer.close(() => { resolve(); }); }); - + await new Promise((resolve) => { testHttpsServer.close(() => { resolve(); @@ -346,4 +333,4 @@ tap.skip.test('cleanup NFTables integration test environment', async () => { }); }); -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/test/test.port-mapping.ts b/test/test.port-mapping.ts index 3dbc106..ece26d0 100644 --- a/test/test.port-mapping.ts +++ b/test/test.port-mapping.ts @@ -1,30 +1,20 @@ import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -import { - createPortMappingRoute, - createOffsetPortMappingRoute, - createDynamicRoute, - createSmartLoadBalancer, - createPortOffset -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; import type { IRouteConfig, IRouteContext } from '../ts/proxies/smart-proxy/models/route-types.js'; import { findFreePorts, assertPortsFree } from './helpers/port-allocator.js'; -// Test server and client utilities let testServers: Array<{ server: net.Server; port: number }> = []; let smartProxy: SmartProxy; -let TEST_PORTS: number[]; // 3 test server ports -let PROXY_PORTS: number[]; // 6 proxy ports +let TEST_PORTS: number[]; +let PROXY_PORTS: number[]; const TEST_DATA = 'Hello through dynamic port mapper!'; -// Cleanup function to close all servers and proxies function cleanup() { console.log('Starting cleanup...'); const promises = []; - - // Close test servers + for (const { server, port } of testServers) { promises.push(new Promise(resolve => { console.log(`Closing test server on port ${port}`); @@ -34,31 +24,28 @@ function cleanup() { }); })); } - - // Stop SmartProxy + if (smartProxy) { console.log('Stopping SmartProxy...'); promises.push(smartProxy.stop().then(() => { console.log('SmartProxy stopped'); })); } - + return Promise.all(promises); } -// Helper: Creates a test TCP server that listens on a given port function createTestServer(port: number): Promise { return new Promise((resolve) => { const server = net.createServer((socket) => { socket.on('data', (data) => { - // Echo the received data back with a server identifier socket.write(`Server ${port} says: ${data.toString()}`); }); socket.on('error', (error) => { console.error(`[Test Server] Socket error on port ${port}:`, error); }); }); - + server.listen(port, () => { console.log(`[Test Server] Listening on port ${port}`); testServers.push({ server, port }); @@ -67,32 +54,31 @@ function createTestServer(port: number): Promise { }); } -// Helper: Creates a test client connection with timeout function createTestClient(port: number, data: string): Promise { return new Promise((resolve, reject) => { const client = new net.Socket(); let response = ''; - + const timeout = setTimeout(() => { client.destroy(); reject(new Error(`Client connection timeout to port ${port}`)); }, 5000); - + client.connect(port, 'localhost', () => { console.log(`[Test Client] Connected to server on port ${port}`); client.write(data); }); - + client.on('data', (chunk) => { response += chunk.toString(); client.end(); }); - + client.on('end', () => { clearTimeout(timeout); resolve(response); }); - + client.on('error', (error) => { clearTimeout(timeout); reject(error); @@ -100,123 +86,108 @@ function createTestClient(port: number, data: string): Promise { }); } -// Set up test environment tap.test('setup port mapping test environment', async () => { const allPorts = await findFreePorts(9); TEST_PORTS = allPorts.slice(0, 3); PROXY_PORTS = allPorts.slice(3, 9); - // Create multiple test servers on different ports await Promise.all([ createTestServer(TEST_PORTS[0]), createTestServer(TEST_PORTS[1]), createTestServer(TEST_PORTS[2]), ]); - // Compute dynamic offset between proxy and test ports const portOffset = TEST_PORTS[1] - PROXY_PORTS[1]; - // Create a SmartProxy with dynamic port mapping routes smartProxy = new SmartProxy({ routes: [ - // Simple function that returns the same port (identity mapping) - createPortMappingRoute({ - sourcePortRange: PROXY_PORTS[0], - targetHost: 'localhost', - portMapper: (context) => TEST_PORTS[0], - name: 'Identity Port Mapping' - }), - - // Offset port mapping using dynamic offset - createOffsetPortMappingRoute({ - ports: PROXY_PORTS[1], - targetHost: 'localhost', - offset: portOffset, - name: `Offset Port Mapping (${portOffset})` - }), - - // Dynamic route with conditional port mapping - createDynamicRoute({ - ports: [PROXY_PORTS[2], PROXY_PORTS[3]], - targetHost: (context) => { - // Dynamic host selection based on port - return context.port === PROXY_PORTS[2] ? 'localhost' : '127.0.0.1'; + { + match: { ports: PROXY_PORTS[0] }, + action: { + type: 'forward', + targets: [{ + host: 'localhost', + port: (context: IRouteContext) => TEST_PORTS[0] + }] }, - portMapper: (context) => { - // Port mapping logic based on incoming port - if (context.port === PROXY_PORTS[2]) { - return TEST_PORTS[0]; - } else { - return TEST_PORTS[2]; - } + name: 'Identity Port Mapping' + }, + + { + match: { ports: PROXY_PORTS[1] }, + action: { + type: 'forward', + targets: [{ + host: 'localhost', + port: (context: IRouteContext) => context.port + portOffset + }] + }, + name: `Offset Port Mapping (${portOffset})` + }, + + { + match: { ports: [PROXY_PORTS[2], PROXY_PORTS[3]] }, + action: { + type: 'forward', + targets: [{ + host: (context: IRouteContext) => { + return context.port === PROXY_PORTS[2] ? 'localhost' : '127.0.0.1'; + }, + port: (context: IRouteContext) => { + if (context.port === PROXY_PORTS[2]) { + return TEST_PORTS[0]; + } else { + return TEST_PORTS[2]; + } + } + }] }, name: 'Dynamic Host and Port Mapping' - }), + }, - // Smart load balancer for domain-based routing - createSmartLoadBalancer({ - ports: PROXY_PORTS[4], - domainTargets: { - 'test1.example.com': 'localhost', - 'test2.example.com': '127.0.0.1' + { + match: { ports: PROXY_PORTS[4] }, + action: { + type: 'forward', + targets: [{ + host: (context: IRouteContext) => { + if (context.domain === 'test1.example.com') return 'localhost'; + if (context.domain === 'test2.example.com') return '127.0.0.1'; + return 'localhost'; + }, + port: (context: IRouteContext) => { + if (context.domain === 'test1.example.com') { + return TEST_PORTS[0]; + } else { + return TEST_PORTS[1]; + } + } + }] }, - portMapper: (context) => { - // Use different backend ports based on domain - if (context.domain === 'test1.example.com') { - return TEST_PORTS[0]; - } else { - return TEST_PORTS[1]; - } - }, - defaultTarget: 'localhost', name: 'Smart Domain Load Balancer' - }) + } ] }); - // Start the SmartProxy await smartProxy.start(); }); -// Test 1: Simple identity port mapping tap.test('should map port using identity function', async () => { const response = await createTestClient(PROXY_PORTS[0], TEST_DATA); expect(response).toEqual(`Server ${TEST_PORTS[0]} says: ${TEST_DATA}`); }); -// Test 2: Offset port mapping tap.test('should map port using offset function', async () => { const response = await createTestClient(PROXY_PORTS[1], TEST_DATA); expect(response).toEqual(`Server ${TEST_PORTS[1]} says: ${TEST_DATA}`); }); -// Test 3: Dynamic port and host mapping (conditional logic) tap.test('should map port using dynamic logic', async () => { const response = await createTestClient(PROXY_PORTS[2], TEST_DATA); expect(response).toEqual(`Server ${TEST_PORTS[0]} says: ${TEST_DATA}`); }); -// Test 4: Test reuse of createPortOffset helper -tap.test('should use createPortOffset helper for port mapping', async () => { - // Test the createPortOffset helper with dynamic offset - const portOffset = TEST_PORTS[1] - PROXY_PORTS[1]; - const offsetFn = createPortOffset(portOffset); - const context = { - port: PROXY_PORTS[1], - clientIp: '127.0.0.1', - serverIp: '127.0.0.1', - isTls: false, - timestamp: Date.now(), - connectionId: 'test-connection' - } as IRouteContext; - - const mappedPort = offsetFn(context); - expect(mappedPort).toEqual(TEST_PORTS[1]); -}); - -// Test 5: Test error handling for invalid port mapping functions tap.test('should handle errors in port mapping functions', async () => { - // Create a route with a function that throws an error const errorRoute: IRouteConfig = { match: { ports: PROXY_PORTS[5] @@ -232,34 +203,27 @@ tap.test('should handle errors in port mapping functions', async () => { }, name: 'Error Route' }; - - // Add the route to SmartProxy + await smartProxy.updateRoutes([...smartProxy.settings.routes, errorRoute]); - - // The connection should fail or timeout + try { await createTestClient(PROXY_PORTS[5], TEST_DATA); - // Connection should not succeed expect(false).toBeTrue(); } catch (error) { - // Connection failed as expected expect(true).toBeTrue(); } }); -// Cleanup tap.test('cleanup port mapping test environment', async () => { - // Add timeout to prevent hanging if SmartProxy shutdown has issues const cleanupPromise = cleanup(); - const timeoutPromise = new Promise((_, reject) => + const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Cleanup timeout after 5 seconds')), 5000) ); - + try { await Promise.race([cleanupPromise, timeoutPromise]); } catch (error) { console.error('Cleanup error:', error); - // Force cleanup even if there's an error testServers = []; smartProxy = null as any; } @@ -267,4 +231,4 @@ tap.test('cleanup port mapping test environment', async () => { await assertPortsFree([...TEST_PORTS, ...PROXY_PORTS]); }); -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/test/test.route-config.ts b/test/test.route-config.ts index c35ac0f..567fc3b 100644 --- a/test/test.route-config.ts +++ b/test/test.route-config.ts @@ -6,7 +6,7 @@ import { expect, tap } from '@git.zone/tstest/tapbundle'; // Import from core modules import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -// Import route utilities and helpers +// Import route utilities import { findMatchingRoutes, findBestMatchingRoute, @@ -28,16 +28,7 @@ import { assertValidRoute } from '../ts/proxies/smart-proxy/utils/route-validator.js'; -import { - createHttpRoute, - createHttpsTerminateRoute, - createHttpsPassthroughRoute, - createHttpToHttpsRedirect, - createCompleteHttpsServer, - createLoadBalancerRoute, - createApiRoute, - createWebSocketRoute -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; +import { SocketHandlers } from '../ts/proxies/smart-proxy/utils/socket-handlers.js'; // Import test helpers import { loadTestCertificates } from './helpers/certificates.js'; @@ -47,12 +38,12 @@ import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types. // --------------------------------- Route Creation Tests --------------------------------- tap.test('Routes: Should create basic HTTP route', async () => { - // Create a simple HTTP route - const httpRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }, { + const httpRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'Basic HTTP Route' - }); + }; - // Validate the route configuration expect(httpRoute.match.ports).toEqual(80); expect(httpRoute.match.domains).toEqual('example.com'); expect(httpRoute.action.type).toEqual('forward'); @@ -62,14 +53,17 @@ tap.test('Routes: Should create basic HTTP route', async () => { }); tap.test('Routes: Should create HTTPS route with TLS termination', async () => { - // Create an HTTPS route with TLS termination - const httpsRoute = createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 8080 }, { - certificate: 'auto', + const httpsRoute: IRouteConfig = { + match: { ports: 443, domains: 'secure.example.com' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, name: 'HTTPS Route' - }); + }; - // Validate the route configuration - expect(httpsRoute.match.ports).toEqual(443); // Default HTTPS port + expect(httpsRoute.match.ports).toEqual(443); expect(httpsRoute.match.domains).toEqual('secure.example.com'); expect(httpsRoute.action.type).toEqual('forward'); expect(httpsRoute.action.tls?.mode).toEqual('terminate'); @@ -80,10 +74,15 @@ tap.test('Routes: Should create HTTPS route with TLS termination', async () => { }); tap.test('Routes: Should create HTTP to HTTPS redirect', async () => { - // Create an HTTP to HTTPS redirect - const redirectRoute = createHttpToHttpsRedirect('example.com', 443); + const redirectRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + }, + name: 'HTTP to HTTPS Redirect for example.com' + }; - // Validate the route configuration expect(redirectRoute.match.ports).toEqual(80); expect(redirectRoute.match.domains).toEqual('example.com'); expect(redirectRoute.action.type).toEqual('socket-handler'); @@ -91,22 +90,34 @@ tap.test('Routes: Should create HTTP to HTTPS redirect', async () => { }); tap.test('Routes: Should create complete HTTPS server with redirects', async () => { - // Create a complete HTTPS server setup - const routes = createCompleteHttpsServer('example.com', { host: 'localhost', port: 8080 }, { - certificate: 'auto' - }); + const routes: IRouteConfig[] = [ + { + match: { ports: 443, domains: 'example.com' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + name: 'HTTPS Terminate Route for example.com' + }, + { + match: { ports: 80, domains: 'example.com' }, + action: { + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + }, + name: 'HTTP to HTTPS Redirect for example.com' + } + ]; - // Validate that we got two routes (HTTPS route and HTTP redirect) expect(routes.length).toEqual(2); - // Validate HTTPS route const httpsRoute = routes[0]; expect(httpsRoute.match.ports).toEqual(443); expect(httpsRoute.match.domains).toEqual('example.com'); expect(httpsRoute.action.type).toEqual('forward'); expect(httpsRoute.action.tls?.mode).toEqual('terminate'); - // Validate HTTP redirect route const redirectRoute = routes[1]; expect(redirectRoute.match.ports).toEqual(80); expect(redirectRoute.action.type).toEqual('socket-handler'); @@ -114,21 +125,17 @@ tap.test('Routes: Should create complete HTTPS server with redirects', async () }); tap.test('Routes: Should create load balancer route', async () => { - // Create a load balancer route - const lbRoute = createLoadBalancerRoute( - 'app.example.com', - ['10.0.0.1', '10.0.0.2', '10.0.0.3'], - 8080, - { - tls: { - mode: 'terminate', - certificate: 'auto' - }, - name: 'Load Balanced Route' - } - ); + const lbRoute: IRouteConfig = { + match: { ports: 443, domains: 'app.example.com' }, + action: { + type: 'forward', + targets: [{ host: ['10.0.0.1', '10.0.0.2', '10.0.0.3'], port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' }, + loadBalancing: { algorithm: 'round-robin' } + }, + name: 'Load Balanced Route' + }; - // Validate the route configuration expect(lbRoute.match.domains).toEqual('app.example.com'); expect(lbRoute.action.type).toEqual('forward'); expect(Array.isArray(lbRoute.action.targets?.[0]?.host)).toBeTrue(); @@ -139,23 +146,32 @@ tap.test('Routes: Should create load balancer route', async () => { }); tap.test('Routes: Should create API route with CORS', async () => { - // Create an API route with CORS headers - const apiRoute = createApiRoute('api.example.com', '/v1', { host: 'localhost', port: 3000 }, { - useTls: true, - certificate: 'auto', - addCorsHeaders: true, + const apiRoute: IRouteConfig = { + match: { ports: 443, domains: 'api.example.com', path: '/v1/*' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 3000 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + headers: { + response: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400' + } + }, + priority: 100, name: 'API Route' - }); + }; - // Validate the route configuration expect(apiRoute.match.domains).toEqual('api.example.com'); expect(apiRoute.match.path).toEqual('/v1/*'); expect(apiRoute.action.type).toEqual('forward'); expect(apiRoute.action.tls?.mode).toEqual('terminate'); expect(apiRoute.action.targets?.[0]?.host).toEqual('localhost'); expect(apiRoute.action.targets?.[0]?.port).toEqual(3000); - - // Check CORS headers + expect(apiRoute.headers).toBeDefined(); if (apiRoute.headers?.response) { expect(apiRoute.headers.response['Access-Control-Allow-Origin']).toEqual('*'); @@ -164,23 +180,25 @@ tap.test('Routes: Should create API route with CORS', async () => { }); tap.test('Routes: Should create WebSocket route', async () => { - // Create a WebSocket route - const wsRoute = createWebSocketRoute('ws.example.com', '/socket', { host: 'localhost', port: 5000 }, { - useTls: true, - certificate: 'auto', - pingInterval: 15000, + const wsRoute: IRouteConfig = { + match: { ports: 443, domains: 'ws.example.com', path: '/socket' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 5000 }], + tls: { mode: 'terminate', certificate: 'auto' }, + websocket: { enabled: true, pingInterval: 15000 } + }, + priority: 100, name: 'WebSocket Route' - }); + }; - // Validate the route configuration expect(wsRoute.match.domains).toEqual('ws.example.com'); expect(wsRoute.match.path).toEqual('/socket'); expect(wsRoute.action.type).toEqual('forward'); expect(wsRoute.action.tls?.mode).toEqual('terminate'); expect(wsRoute.action.targets?.[0]?.host).toEqual('localhost'); expect(wsRoute.action.targets?.[0]?.port).toEqual(5000); - - // Check WebSocket configuration + expect(wsRoute.action.websocket).toBeDefined(); if (wsRoute.action.websocket) { expect(wsRoute.action.websocket.enabled).toBeTrue(); @@ -191,22 +209,27 @@ tap.test('Routes: Should create WebSocket route', async () => { // Static file serving has been removed - should be handled by external servers tap.test('SmartProxy: Should create instance with route-based config', async () => { - // Create TLS certificates for testing const certs = loadTestCertificates(); - // Create a SmartProxy instance with route-based configuration const proxy = new SmartProxy({ routes: [ - createHttpRoute('example.com', { host: 'localhost', port: 3000 }, { + { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, name: 'HTTP Route' - }), - createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 8443 }, { - certificate: { - key: certs.privateKey, - cert: certs.publicKey + }, + { + match: { ports: 443, domains: 'secure.example.com' }, + action: { + type: 'forward', + targets: [{ host: 'localhost', port: 8443 }], + tls: { + mode: 'terminate', + certificate: { key: certs.privateKey, cert: certs.publicKey } + } }, name: 'HTTPS Route' - }) + } ], defaults: { target: { @@ -218,13 +241,11 @@ tap.test('SmartProxy: Should create instance with route-based config', async () maxConnections: 100 } }, - // Additional settings initialDataTimeout: 10000, inactivityTimeout: 300000, enableDetailedLogging: true }); - // Simply verify the instance was created successfully expect(typeof proxy).toEqual('object'); expect(typeof proxy.start).toEqual('function'); expect(typeof proxy.stop).toEqual('function'); @@ -233,94 +254,109 @@ tap.test('SmartProxy: Should create instance with route-based config', async () // --------------------------------- Edge Case Tests --------------------------------- tap.test('Edge Case - Empty Routes Array', async () => { - // Attempting to find routes in an empty array const emptyRoutes: IRouteConfig[] = []; const matches = findMatchingRoutes(emptyRoutes, { domain: 'example.com', port: 80 }); - + expect(matches).toBeInstanceOf(Array); expect(matches.length).toEqual(0); - + const bestMatch = findBestMatchingRoute(emptyRoutes, { domain: 'example.com', port: 80 }); expect(bestMatch).toBeUndefined(); }); tap.test('Edge Case - Multiple Matching Routes with Same Priority', async () => { - // Create multiple routes with identical priority but different targets - const route1 = createHttpRoute('example.com', { host: 'server1', port: 3000 }); - const route2 = createHttpRoute('example.com', { host: 'server2', port: 3000 }); - const route3 = createHttpRoute('example.com', { host: 'server3', port: 3000 }); - - // Set all to the same priority + const route1: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'server1', port: 3000 }] }, + name: 'HTTP Route for example.com' + }; + const route2: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'server2', port: 3000 }] }, + name: 'HTTP Route for example.com' + }; + const route3: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'server3', port: 3000 }] }, + name: 'HTTP Route for example.com' + }; + route1.priority = 100; route2.priority = 100; route3.priority = 100; - + const routes = [route1, route2, route3]; - - // Find matching routes + const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 }); - - // Should find all three routes + expect(matches.length).toEqual(3); - - // First match could be any of the routes since they have the same priority - // But the implementation should be consistent (likely keep the original order) + const bestMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 }); expect(bestMatch).not.toBeUndefined(); }); tap.test('Edge Case - Wildcard Domains and Path Matching', async () => { - // Create routes with wildcard domains and path patterns - const wildcardApiRoute = createApiRoute('*.example.com', '/api', { host: 'api-server', port: 3000 }, { - useTls: true, - certificate: 'auto' - }); - - const exactApiRoute = createApiRoute('api.example.com', '/api', { host: 'specific-api-server', port: 3001 }, { - useTls: true, - certificate: 'auto', - priority: 200 // Higher priority - }); - + const wildcardApiRoute: IRouteConfig = { + match: { ports: 443, domains: '*.example.com', path: '/api/*' }, + action: { + type: 'forward', + targets: [{ host: 'api-server', port: 3000 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + priority: 100, + name: 'API Route for *.example.com' + }; + + const exactApiRoute: IRouteConfig = { + match: { ports: 443, domains: 'api.example.com', path: '/api/*' }, + action: { + type: 'forward', + targets: [{ host: 'specific-api-server', port: 3001 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + priority: 200, + name: 'API Route for api.example.com' + }; + const routes = [wildcardApiRoute, exactApiRoute]; - - // Test with a specific subdomain that matches both routes + const matches = findMatchingRoutes(routes, { domain: 'api.example.com', path: '/api/users', port: 443 }); - - // Should match both routes + expect(matches.length).toEqual(2); - - // The exact domain match should have higher priority + const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 }); expect(bestMatch).not.toBeUndefined(); if (bestMatch) { - expect(bestMatch.action.targets[0].port).toEqual(3001); // Should match the exact domain route + expect(bestMatch.action.targets[0].port).toEqual(3001); } - - // Test with a different subdomain - should only match the wildcard route + const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 }); expect(otherMatches.length).toEqual(1); - expect(otherMatches[0].action.targets[0].port).toEqual(3000); // Should match the wildcard domain route + expect(otherMatches[0].action.targets[0].port).toEqual(3000); }); tap.test('Edge Case - Disabled Routes', async () => { - // Create enabled and disabled routes - const enabledRoute = createHttpRoute('example.com', { host: 'server1', port: 3000 }); - const disabledRoute = createHttpRoute('example.com', { host: 'server2', port: 3001 }); + const enabledRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'server1', port: 3000 }] }, + name: 'HTTP Route for example.com' + }; + const disabledRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'server2', port: 3001 }] }, + name: 'HTTP Route for example.com' + }; disabledRoute.enabled = false; - + const routes = [enabledRoute, disabledRoute]; - - // Find matching routes + const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 }); - - // Should only find the enabled route + expect(matches.length).toEqual(1); expect(matches[0].action.targets[0].port).toEqual(3000); }); tap.test('Edge Case - Complex Path and Headers Matching', async () => { - // Create route with complex path and headers matching const complexRoute: IRouteConfig = { match: { domains: 'api.example.com', @@ -344,22 +380,20 @@ tap.test('Edge Case - Complex Path and Headers Matching', async () => { }, name: 'Complex API Route' }; - - // Test with matching criteria + const matchingPath = routeMatchesPath(complexRoute, '/api/v2/users'); expect(matchingPath).toBeTrue(); - + const matchingHeaders = routeMatchesHeaders(complexRoute, { 'Content-Type': 'application/json', 'X-API-Key': 'valid-key', 'Accept': 'application/json' }); expect(matchingHeaders).toBeTrue(); - - // Test with non-matching criteria + const nonMatchingPath = routeMatchesPath(complexRoute, '/api/v1/users'); expect(nonMatchingPath).toBeFalse(); - + const nonMatchingHeaders = routeMatchesHeaders(complexRoute, { 'Content-Type': 'application/json', 'X-API-Key': 'invalid-key' @@ -368,7 +402,6 @@ tap.test('Edge Case - Complex Path and Headers Matching', async () => { }); tap.test('Edge Case - Port Range Matching', async () => { - // Create route with port range matching const portRangeRoute: IRouteConfig = { match: { domains: 'example.com', @@ -383,17 +416,14 @@ tap.test('Edge Case - Port Range Matching', async () => { }, name: 'Port Range Route' }; - - // Test with ports in the range - expect(routeMatchesPort(portRangeRoute, 8000)).toBeTrue(); // Lower bound - expect(routeMatchesPort(portRangeRoute, 8500)).toBeTrue(); // Middle - expect(routeMatchesPort(portRangeRoute, 9000)).toBeTrue(); // Upper bound - - // Test with ports outside the range - expect(routeMatchesPort(portRangeRoute, 7999)).toBeFalse(); // Just below - expect(routeMatchesPort(portRangeRoute, 9001)).toBeFalse(); // Just above - - // Test with multiple port ranges + + expect(routeMatchesPort(portRangeRoute, 8000)).toBeTrue(); + expect(routeMatchesPort(portRangeRoute, 8500)).toBeTrue(); + expect(routeMatchesPort(portRangeRoute, 9000)).toBeTrue(); + + expect(routeMatchesPort(portRangeRoute, 7999)).toBeFalse(); + expect(routeMatchesPort(portRangeRoute, 9001)).toBeFalse(); + const multiRangeRoute: IRouteConfig = { match: { domains: 'example.com', @@ -411,7 +441,7 @@ tap.test('Edge Case - Port Range Matching', async () => { }, name: 'Multi Range Route' }; - + expect(routeMatchesPort(multiRangeRoute, 85)).toBeTrue(); expect(routeMatchesPort(multiRangeRoute, 8500)).toBeTrue(); expect(routeMatchesPort(multiRangeRoute, 100)).toBeFalse(); @@ -420,55 +450,56 @@ tap.test('Edge Case - Port Range Matching', async () => { // --------------------------------- Wildcard Domain Tests --------------------------------- tap.test('Wildcard Domain Handling', async () => { - // Create routes with different wildcard patterns - const simpleDomainRoute = createHttpRoute('example.com', { host: 'server1', port: 3000 }); - const wildcardSubdomainRoute = createHttpRoute('*.example.com', { host: 'server2', port: 3001 }); - const specificSubdomainRoute = createHttpRoute('api.example.com', { host: 'server3', port: 3002 }); + const simpleDomainRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'server1', port: 3000 }] }, + name: 'HTTP Route for example.com' + }; + const wildcardSubdomainRoute: IRouteConfig = { + match: { ports: 80, domains: '*.example.com' }, + action: { type: 'forward', targets: [{ host: 'server2', port: 3001 }] }, + name: 'HTTP Route for *.example.com' + }; + const specificSubdomainRoute: IRouteConfig = { + match: { ports: 80, domains: 'api.example.com' }, + action: { type: 'forward', targets: [{ host: 'server3', port: 3002 }] }, + name: 'HTTP Route for api.example.com' + }; - // Set explicit priorities to ensure deterministic matching - specificSubdomainRoute.priority = 200; // Highest priority for specific domain - wildcardSubdomainRoute.priority = 100; // Medium priority for wildcard - simpleDomainRoute.priority = 50; // Lowest priority for generic domain + specificSubdomainRoute.priority = 200; + wildcardSubdomainRoute.priority = 100; + simpleDomainRoute.priority = 50; const routes = [simpleDomainRoute, wildcardSubdomainRoute, specificSubdomainRoute]; - // Test exact domain match expect(routeMatchesDomain(simpleDomainRoute, 'example.com')).toBeTrue(); expect(routeMatchesDomain(simpleDomainRoute, 'sub.example.com')).toBeFalse(); - // Test wildcard subdomain match expect(routeMatchesDomain(wildcardSubdomainRoute, 'any.example.com')).toBeTrue(); expect(routeMatchesDomain(wildcardSubdomainRoute, 'nested.sub.example.com')).toBeTrue(); expect(routeMatchesDomain(wildcardSubdomainRoute, 'example.com')).toBeFalse(); - // Test specific subdomain match expect(routeMatchesDomain(specificSubdomainRoute, 'api.example.com')).toBeTrue(); expect(routeMatchesDomain(specificSubdomainRoute, 'other.example.com')).toBeFalse(); expect(routeMatchesDomain(specificSubdomainRoute, 'sub.api.example.com')).toBeFalse(); - // Test finding best match when multiple domains match const specificSubdomainRequest = { domain: 'api.example.com', port: 80 }; const bestSpecificMatch = findBestMatchingRoute(routes, specificSubdomainRequest); expect(bestSpecificMatch).not.toBeUndefined(); if (bestSpecificMatch) { - // Find which route was matched const matchedPort = bestSpecificMatch.action.targets[0].port; console.log(`Matched route with port: ${matchedPort}`); - // Verify it's the specific subdomain route (with highest priority) expect(bestSpecificMatch.priority).toEqual(200); } - // Test with a subdomain that matches wildcard but not specific const otherSubdomainRequest = { domain: 'other.example.com', port: 80 }; const bestWildcardMatch = findBestMatchingRoute(routes, otherSubdomainRequest); expect(bestWildcardMatch).not.toBeUndefined(); if (bestWildcardMatch) { - // Find which route was matched const matchedPort = bestWildcardMatch.action.targets[0].port; console.log(`Matched route with port: ${matchedPort}`); - // Verify it's the wildcard subdomain route (with medium priority) expect(bestWildcardMatch.priority).toEqual(100); } }); @@ -476,56 +507,83 @@ tap.test('Wildcard Domain Handling', async () => { // --------------------------------- Integration Tests --------------------------------- tap.test('Route Integration - Combining Multiple Route Types', async () => { - // Create a comprehensive set of routes for a full application const routes: IRouteConfig[] = [ - // Main website with HTTPS and HTTP redirect - ...createCompleteHttpsServer('example.com', { host: 'web-server', port: 8080 }, { - certificate: 'auto' - }), - - // API endpoints - createApiRoute('api.example.com', '/v1', { host: 'api-server', port: 3000 }, { - useTls: true, - certificate: 'auto', - addCorsHeaders: true - }), - - // WebSocket for real-time updates - createWebSocketRoute('ws.example.com', '/live', { host: 'websocket-server', port: 5000 }, { - useTls: true, - certificate: 'auto' - }), - - - // Legacy system with passthrough - createHttpsPassthroughRoute('legacy.example.com', { host: 'legacy-server', port: 443 }) + { + match: { ports: 443, domains: 'example.com' }, + action: { + type: 'forward', + targets: [{ host: 'web-server', port: 8080 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + name: 'HTTPS Terminate Route for example.com' + }, + { + match: { ports: 80, domains: 'example.com' }, + action: { + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect('https://{domain}:443{path}', 301) + }, + name: 'HTTP to HTTPS Redirect for example.com' + }, + { + match: { ports: 443, domains: 'api.example.com', path: '/v1/*' }, + action: { + type: 'forward', + targets: [{ host: 'api-server', port: 3000 }], + tls: { mode: 'terminate', certificate: 'auto' } + }, + headers: { + response: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400' + } + }, + priority: 100, + name: 'API Route for api.example.com' + }, + { + match: { ports: 443, domains: 'ws.example.com', path: '/live' }, + action: { + type: 'forward', + targets: [{ host: 'websocket-server', port: 5000 }], + tls: { mode: 'terminate', certificate: 'auto' }, + websocket: { enabled: true } + }, + priority: 100, + name: 'WebSocket Route for ws.example.com' + }, + { + match: { ports: 443, domains: 'legacy.example.com' }, + action: { + type: 'forward', + targets: [{ host: 'legacy-server', port: 443 }], + tls: { mode: 'passthrough' } + }, + name: 'HTTPS Passthrough Route for legacy.example.com' + } ]; - - // Validate all routes + const validationResult = validateRoutes(routes); expect(validationResult.valid).toBeTrue(); expect(validationResult.errors.length).toEqual(0); - - // Test route matching for different endpoints - - // Web server (HTTPS) + const webServerMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 443 }); expect(webServerMatch).not.toBeUndefined(); if (webServerMatch) { expect(webServerMatch.action.type).toEqual('forward'); expect(webServerMatch.action.targets[0].host).toEqual('web-server'); } - - // Web server (HTTP redirect via socket handler) + const webRedirectMatch = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 }); expect(webRedirectMatch).not.toBeUndefined(); if (webRedirectMatch) { expect(webRedirectMatch.action.type).toEqual('socket-handler'); } - - // API server - const apiMatch = findBestMatchingRoute(routes, { - domain: 'api.example.com', + + const apiMatch = findBestMatchingRoute(routes, { + domain: 'api.example.com', port: 443, path: '/v1/users' }); @@ -534,10 +592,9 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(apiMatch.action.type).toEqual('forward'); expect(apiMatch.action.targets[0].host).toEqual('api-server'); } - - // WebSocket server - const wsMatch = findBestMatchingRoute(routes, { - domain: 'ws.example.com', + + const wsMatch = findBestMatchingRoute(routes, { + domain: 'ws.example.com', port: 443, path: '/live' }); @@ -547,12 +604,9 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(wsMatch.action.targets[0].host).toEqual('websocket-server'); expect(wsMatch.action.websocket?.enabled).toBeTrue(); } - - // Static assets route was removed - static file serving should be handled externally - - // Legacy system - const legacyMatch = findBestMatchingRoute(routes, { - domain: 'legacy.example.com', + + const legacyMatch = findBestMatchingRoute(routes, { + domain: 'legacy.example.com', port: 443 }); expect(legacyMatch).not.toBeUndefined(); @@ -565,7 +619,6 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { // --------------------------------- Protocol Match Field Tests --------------------------------- tap.test('Routes: Should accept protocol field on route match', async () => { - // Create a route with protocol: 'http' const httpOnlyRoute: IRouteConfig = { match: { ports: 443, @@ -583,16 +636,13 @@ tap.test('Routes: Should accept protocol field on route match', async () => { name: 'HTTP-only Route', }; - // Validate the route - protocol field should not cause errors const validation = validateRouteConfig(httpOnlyRoute); expect(validation.valid).toBeTrue(); - // Verify the protocol field is preserved expect(httpOnlyRoute.match.protocol).toEqual('http'); }); tap.test('Routes: Should accept protocol tcp on route match', async () => { - // Create a route with protocol: 'tcp' const tcpOnlyRoute: IRouteConfig = { match: { ports: 443, @@ -616,28 +666,26 @@ tap.test('Routes: Should accept protocol tcp on route match', async () => { }); tap.test('Routes: Protocol field should work with terminate-and-reencrypt', async () => { - // Create a terminate-and-reencrypt route that only accepts HTTP - const reencryptRoute = createHttpsTerminateRoute( - 'secure.example.com', - { host: 'backend', port: 443 }, - { reencrypt: true, certificate: 'auto', name: 'Reencrypt HTTP Route' } - ); + const reencryptRoute: IRouteConfig = { + match: { ports: 443, domains: 'secure.example.com' }, + action: { + type: 'forward', + targets: [{ host: 'backend', port: 443 }], + tls: { mode: 'terminate-and-reencrypt', certificate: 'auto' } + }, + name: 'Reencrypt HTTP Route' + }; - // Set protocol restriction to http reencryptRoute.match.protocol = 'http'; - // Validate the route const validation = validateRouteConfig(reencryptRoute); expect(validation.valid).toBeTrue(); - // Verify TLS mode expect(reencryptRoute.action.tls?.mode).toEqual('terminate-and-reencrypt'); - // Verify protocol field is preserved expect(reencryptRoute.match.protocol).toEqual('http'); }); tap.test('Routes: Protocol field should not affect domain/port matching', async () => { - // Routes with and without protocol field should both match the same domain/port const routeWithProtocol: IRouteConfig = { match: { ports: 443, @@ -669,11 +717,9 @@ tap.test('Routes: Protocol field should not affect domain/port matching', async const routes = [routeWithProtocol, routeWithoutProtocol]; - // Both routes should match the domain/port (protocol is a hint for Rust-side matching) const matches = findMatchingRoutes(routes, { domain: 'example.com', port: 443 }); expect(matches.length).toEqual(2); - // The one with higher priority should be first const best = findBestMatchingRoute(routes, { domain: 'example.com', port: 443 }); expect(best).not.toBeUndefined(); expect(best!.name).toEqual('With Protocol'); @@ -696,11 +742,9 @@ tap.test('Routes: Protocol field preserved through route cloning', async () => { const cloned = cloneRoute(original); - // Verify protocol is preserved in clone expect(cloned.match.protocol).toEqual('http'); expect(cloned.action.tls?.mode).toEqual('terminate-and-reencrypt'); - // Modify clone should not affect original cloned.match.protocol = 'tcp'; expect(original.match.protocol).toEqual('http'); }); @@ -720,10 +764,9 @@ tap.test('Routes: Protocol field preserved through route merging', async () => { name: 'Merge Base', }; - // Merge with override that changes name but not protocol const merged = mergeRouteConfigs(base, { name: 'Merged Route' }); expect(merged.match.protocol).toEqual('http'); expect(merged.name).toEqual('Merged Route'); }); -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/test/test.route-utils.ts b/test/test.route-utils.ts index c3a0d31..6eefebf 100644 --- a/test/test.route-utils.ts +++ b/test/test.route-utils.ts @@ -1,21 +1,7 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; -// Import from individual modules to avoid naming conflicts import { - // Route helpers - createHttpRoute, - createHttpsTerminateRoute, - createApiRoute, - createWebSocketRoute, - createHttpToHttpsRedirect, - createHttpsPassthroughRoute, - createCompleteHttpsServer, - createLoadBalancerRoute -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; - -import { - // Route validators validateRouteConfig, validateRoutes, isValidDomain, @@ -27,7 +13,6 @@ import { } from '../ts/proxies/smart-proxy/utils/route-validator.js'; import { - // Route utilities mergeRouteConfigs, findMatchingRoutes, findBestMatchingRoute, @@ -39,16 +24,6 @@ import { cloneRoute } from '../ts/proxies/smart-proxy/utils/route-utils.js'; -import { - // Route patterns - createApiGatewayRoute, - createWebSocketRoute as createWebSocketPattern, - createLoadBalancerRoute as createLbPattern, - addRateLimiting, - addBasicAuth, - addJwtAuth -} from '../ts/proxies/smart-proxy/utils/route-helpers.js'; - import type { IRouteConfig, IRouteMatch, @@ -84,7 +59,7 @@ tap.test('Route Validation - isValidPort', async () => { expect(isValidPort(443)).toBeTrue(); expect(isValidPort(8080)).toBeTrue(); expect(isValidPort([80, 443])).toBeTrue(); - + // Invalid ports expect(isValidPort(0)).toBeFalse(); expect(isValidPort(65536)).toBeFalse(); @@ -101,7 +76,7 @@ tap.test('Route Validation - validateRouteMatch', async () => { const validResult = validateRouteMatch(validMatch); expect(validResult.valid).toBeTrue(); expect(validResult.errors.length).toEqual(0); - + // Invalid match configuration (invalid domain) const invalidMatch: IRouteMatch = { ports: 80, @@ -111,7 +86,7 @@ tap.test('Route Validation - validateRouteMatch', async () => { expect(invalidResult.valid).toBeFalse(); expect(invalidResult.errors.length).toBeGreaterThan(0); expect(invalidResult.errors[0]).toInclude('Invalid domain'); - + // Invalid match configuration (invalid port) const invalidPortMatch: IRouteMatch = { ports: 0, @@ -121,7 +96,7 @@ tap.test('Route Validation - validateRouteMatch', async () => { expect(invalidPortResult.valid).toBeFalse(); expect(invalidPortResult.errors.length).toBeGreaterThan(0); expect(invalidPortResult.errors[0]).toInclude('Invalid port'); - + // Test path validation const invalidPathMatch: IRouteMatch = { ports: 80, @@ -146,7 +121,7 @@ tap.test('Route Validation - validateRouteAction', async () => { const validForwardResult = validateRouteAction(validForwardAction); expect(validForwardResult.valid).toBeTrue(); expect(validForwardResult.errors.length).toEqual(0); - + // Valid socket-handler action const validSocketAction: IRouteAction = { type: 'socket-handler', @@ -157,7 +132,7 @@ tap.test('Route Validation - validateRouteAction', async () => { const validSocketResult = validateRouteAction(validSocketAction); expect(validSocketResult.valid).toBeTrue(); expect(validSocketResult.errors.length).toEqual(0); - + // Invalid action (missing targets) const invalidAction: IRouteAction = { type: 'forward' @@ -166,7 +141,7 @@ tap.test('Route Validation - validateRouteAction', async () => { expect(invalidResult.valid).toBeFalse(); expect(invalidResult.errors.length).toBeGreaterThan(0); expect(invalidResult.errors[0]).toInclude('Targets array is required'); - + // Invalid action (missing socket handler) const invalidSocketAction: IRouteAction = { type: 'socket-handler' @@ -179,11 +154,15 @@ tap.test('Route Validation - validateRouteAction', async () => { tap.test('Route Validation - validateRouteConfig', async () => { // Valid route config - const validRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const validRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; const validResult = validateRouteConfig(validRoute); expect(validResult.valid).toBeTrue(); expect(validResult.errors.length).toEqual(0); - + // Invalid route config (missing targets) const invalidRoute: IRouteConfig = { match: { @@ -203,7 +182,11 @@ tap.test('Route Validation - validateRouteConfig', async () => { tap.test('Route Validation - validateRoutes', async () => { // Create valid and invalid routes const routes = [ - createHttpRoute('example.com', { host: 'localhost', port: 3000 }), + { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + } as IRouteConfig, { match: { domains: 'invalid..domain', @@ -217,9 +200,13 @@ tap.test('Route Validation - validateRoutes', async () => { } } } as IRouteConfig, - createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 3001 }) + { + match: { ports: 443, domains: 'secure.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'HTTPS Terminate Route for secure.example.com', + } as IRouteConfig ]; - + const result = validateRoutes(routes); expect(result.valid).toBeFalse(); expect(result.errors.length).toEqual(1); @@ -230,13 +217,13 @@ tap.test('Route Validation - validateRoutes', async () => { tap.test('Route Validation - hasRequiredPropertiesForAction', async () => { // Forward action - const forwardRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const forwardRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; expect(hasRequiredPropertiesForAction(forwardRoute, 'forward')).toBeTrue(); - - // Socket handler action (redirect functionality) - const redirectRoute = createHttpToHttpsRedirect('example.com'); - expect(hasRequiredPropertiesForAction(redirectRoute, 'socket-handler')).toBeTrue(); - + // Socket handler action const socketRoute: IRouteConfig = { match: { @@ -252,7 +239,7 @@ tap.test('Route Validation - hasRequiredPropertiesForAction', async () => { name: 'Socket Handler Route' }; expect(hasRequiredPropertiesForAction(socketRoute, 'socket-handler')).toBeTrue(); - + // Missing required properties const invalidForwardRoute: IRouteConfig = { match: { @@ -269,9 +256,13 @@ tap.test('Route Validation - hasRequiredPropertiesForAction', async () => { tap.test('Route Validation - assertValidRoute', async () => { // Valid route - const validRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const validRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; expect(() => assertValidRoute(validRoute)).not.toThrow(); - + // Invalid route const invalidRoute: IRouteConfig = { match: { @@ -290,8 +281,12 @@ tap.test('Route Validation - assertValidRoute', async () => { tap.test('Route Utilities - mergeRouteConfigs', async () => { // Base route - const baseRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - + const baseRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; + // Override with different name and port const overrideRoute: Partial = { name: 'Merged Route', @@ -299,16 +294,16 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { ports: 8080 } }; - + // Merge configs const mergedRoute = mergeRouteConfigs(baseRoute, overrideRoute); - + // Check merged properties expect(mergedRoute.name).toEqual('Merged Route'); expect(mergedRoute.match.ports).toEqual(8080); expect(mergedRoute.match.domains).toEqual('example.com'); expect(mergedRoute.action.type).toEqual('forward'); - + // Test merging action properties const actionOverride: Partial = { action: { @@ -319,11 +314,11 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { }] } }; - + const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride); expect(actionMergedRoute.action.targets?.[0]?.host).toEqual('new-host.local'); expect(actionMergedRoute.action.targets?.[0]?.port).toEqual(5000); - + // Test replacing action with socket handler const typeChangeOverride: Partial = { action: { @@ -336,7 +331,7 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { } } }; - + const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride); expect(typeChangedRoute.action.type).toEqual('socket-handler'); expect(typeChangedRoute.action.socketHandler).toBeDefined(); @@ -345,37 +340,53 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { tap.test('Route Matching - routeMatchesDomain', async () => { // Create route with wildcard domain - const wildcardRoute = createHttpRoute('*.example.com', { host: 'localhost', port: 3000 }); - + const wildcardRoute: IRouteConfig = { + match: { ports: 80, domains: '*.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for *.example.com', + }; + // Create route with exact domain - const exactRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - + const exactRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; + // Create route with multiple domains - const multiDomainRoute = createHttpRoute(['example.com', 'example.org'], { host: 'localhost', port: 3000 }); - + const multiDomainRoute: IRouteConfig = { + match: { ports: 80, domains: ['example.com', 'example.org'] }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com,example.org', + }; + // Test wildcard domain matching expect(routeMatchesDomain(wildcardRoute, 'sub.example.com')).toBeTrue(); expect(routeMatchesDomain(wildcardRoute, 'another.example.com')).toBeTrue(); expect(routeMatchesDomain(wildcardRoute, 'example.com')).toBeFalse(); expect(routeMatchesDomain(wildcardRoute, 'example.org')).toBeFalse(); - + // Test exact domain matching expect(routeMatchesDomain(exactRoute, 'example.com')).toBeTrue(); expect(routeMatchesDomain(exactRoute, 'sub.example.com')).toBeFalse(); - + // Test multiple domains matching expect(routeMatchesDomain(multiDomainRoute, 'example.com')).toBeTrue(); expect(routeMatchesDomain(multiDomainRoute, 'example.org')).toBeTrue(); expect(routeMatchesDomain(multiDomainRoute, 'example.net')).toBeFalse(); - + // Test case insensitivity expect(routeMatchesDomain(exactRoute, 'Example.Com')).toBeTrue(); }); tap.test('Route Matching - routeMatchesPort', async () => { // Create routes with different port configurations - const singlePortRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - + const singlePortRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; + const multiPortRoute: IRouteConfig = { match: { domains: 'example.com', @@ -389,7 +400,7 @@ tap.test('Route Matching - routeMatchesPort', async () => { }] } }; - + const portRangeRoute: IRouteConfig = { match: { domains: 'example.com', @@ -403,16 +414,16 @@ tap.test('Route Matching - routeMatchesPort', async () => { }] } }; - + // Test single port matching expect(routeMatchesPort(singlePortRoute, 80)).toBeTrue(); expect(routeMatchesPort(singlePortRoute, 443)).toBeFalse(); - + // Test multi-port matching expect(routeMatchesPort(multiPortRoute, 80)).toBeTrue(); expect(routeMatchesPort(multiPortRoute, 8080)).toBeTrue(); expect(routeMatchesPort(multiPortRoute, 3000)).toBeFalse(); - + // Test port range matching expect(routeMatchesPort(portRangeRoute, 8000)).toBeTrue(); expect(routeMatchesPort(portRangeRoute, 8500)).toBeTrue(); @@ -437,11 +448,11 @@ tap.test('Route Matching - routeMatchesPath', async () => { }] } }; - + // Test prefix matching with wildcard (not trailing slash) const prefixPathRoute: IRouteConfig = { match: { - domains: 'example.com', + domains: 'example.com', ports: 80, path: '/api/*' }, @@ -453,7 +464,7 @@ tap.test('Route Matching - routeMatchesPath', async () => { }] } }; - + const wildcardPathRoute: IRouteConfig = { match: { domains: 'example.com', @@ -468,17 +479,17 @@ tap.test('Route Matching - routeMatchesPath', async () => { }] } }; - + // Test exact path matching expect(routeMatchesPath(exactPathRoute, '/api')).toBeTrue(); expect(routeMatchesPath(exactPathRoute, '/api/users')).toBeFalse(); expect(routeMatchesPath(exactPathRoute, '/app')).toBeFalse(); - + // Test prefix path matching with wildcard expect(routeMatchesPath(prefixPathRoute, '/api/')).toBeFalse(); // Wildcard requires content after /api/ expect(routeMatchesPath(prefixPathRoute, '/api/users')).toBeTrue(); expect(routeMatchesPath(prefixPathRoute, '/app/')).toBeFalse(); - + // Test wildcard path matching expect(routeMatchesPath(wildcardPathRoute, '/api/users')).toBeTrue(); expect(routeMatchesPath(wildcardPathRoute, '/api/products')).toBeTrue(); @@ -504,30 +515,34 @@ tap.test('Route Matching - routeMatchesHeaders', async () => { }] } }; - + // Test header matching expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'application/json', 'X-Custom-Header': 'value' })).toBeTrue(); - + expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'application/json', 'X-Custom-Header': 'value', 'Extra-Header': 'something' })).toBeTrue(); - + expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'application/json' })).toBeFalse(); - + expect(routeMatchesHeaders(headerRoute, { 'Content-Type': 'text/html', 'X-Custom-Header': 'value' })).toBeFalse(); - + // Route without header matching should match any headers - const noHeaderRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const noHeaderRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; expect(routeMatchesHeaders(noHeaderRoute, { 'Content-Type': 'application/json' })).toBeTrue(); @@ -536,78 +551,118 @@ tap.test('Route Matching - routeMatchesHeaders', async () => { tap.test('Route Finding - findMatchingRoutes', async () => { // Create multiple routes const routes: IRouteConfig[] = [ - createHttpRoute('example.com', { host: 'localhost', port: 3000 }), - createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 3001 }), - createApiRoute('api.example.com', '/v1', { host: 'localhost', port: 3002 }), - createWebSocketRoute('ws.example.com', '/socket', { host: 'localhost', port: 3003 }) + { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }, + { + match: { ports: 443, domains: 'secure.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'HTTPS Route for secure.example.com', + }, + { + match: { ports: 443, domains: 'api.example.com', path: '/v1/*' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3002 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'API Route for api.example.com', + }, + { + match: { ports: 443, domains: 'ws.example.com', path: '/socket' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3003 }], tls: { mode: 'terminate', certificate: 'auto' }, websocket: { enabled: true } }, + name: 'WebSocket Route for ws.example.com', + }, ]; - + // Set priorities routes[0].priority = 10; routes[1].priority = 20; routes[2].priority = 30; routes[3].priority = 40; - + // Find routes for different criteria const httpMatches = findMatchingRoutes(routes, { domain: 'example.com', port: 80 }); expect(httpMatches.length).toEqual(1); expect(httpMatches[0].name).toInclude('HTTP Route'); - + const httpsMatches = findMatchingRoutes(routes, { domain: 'secure.example.com', port: 443 }); expect(httpsMatches.length).toEqual(1); expect(httpsMatches[0].name).toInclude('HTTPS Route'); - + const apiMatches = findMatchingRoutes(routes, { domain: 'api.example.com', path: '/v1/users' }); expect(apiMatches.length).toEqual(1); expect(apiMatches[0].name).toInclude('API Route'); - + const wsMatches = findMatchingRoutes(routes, { domain: 'ws.example.com', path: '/socket' }); expect(wsMatches.length).toEqual(1); expect(wsMatches[0].name).toInclude('WebSocket Route'); - + // Test finding multiple routes that match same criteria - const route1 = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const route1: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; route1.priority = 10; - - const route2 = createHttpRoute('example.com', { host: 'localhost', port: 3001 }); + + const route2: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }] }, + name: 'HTTP Route for example.com', + }; route2.priority = 20; route2.match.path = '/api'; - + const multiMatchRoutes = [route1, route2]; - + const multiMatches = findMatchingRoutes(multiMatchRoutes, { domain: 'example.com', port: 80 }); expect(multiMatches.length).toEqual(2); expect(multiMatches[0].priority).toEqual(20); // Higher priority should be first expect(multiMatches[1].priority).toEqual(10); - + // Test disabled routes - const disabledRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const disabledRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; disabledRoute.enabled = false; - + const enabledRoutes = findMatchingRoutes([disabledRoute], { domain: 'example.com', port: 80 }); expect(enabledRoutes.length).toEqual(0); }); tap.test('Route Finding - findBestMatchingRoute', async () => { // Create multiple routes with different priorities - const route1 = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const route1: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; route1.priority = 10; - - const route2 = createHttpRoute('example.com', { host: 'localhost', port: 3001 }); + + const route2: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }] }, + name: 'HTTP Route for example.com', + }; route2.priority = 20; route2.match.path = '/api'; - - const route3 = createHttpRoute('example.com', { host: 'localhost', port: 3002 }); + + const route3: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3002 }] }, + name: 'HTTP Route for example.com', + }; route3.priority = 30; route3.match.path = '/api/users'; - + const routes = [route1, route2, route3]; - + // Find best route for different criteria const bestGeneral = findBestMatchingRoute(routes, { domain: 'example.com', port: 80 }); expect(bestGeneral).not.toBeUndefined(); expect(bestGeneral?.priority).toEqual(30); - + // Test when no routes match const noMatch = findBestMatchingRoute(routes, { domain: 'unknown.com', port: 80 }); expect(noMatch).toBeUndefined(); @@ -615,389 +670,54 @@ tap.test('Route Finding - findBestMatchingRoute', async () => { tap.test('Route Utilities - generateRouteId', async () => { // Test ID generation for different route types - const httpRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); + const httpRoute: IRouteConfig = { + match: { ports: 80, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com', + }; const httpId = generateRouteId(httpRoute); expect(httpId).toInclude('example-com'); expect(httpId).toInclude('80'); expect(httpId).toInclude('forward'); - - const httpsRoute = createHttpsTerminateRoute('secure.example.com', { host: 'localhost', port: 3001 }); + + const httpsRoute: IRouteConfig = { + match: { ports: 443, domains: 'secure.example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3001 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'HTTPS Terminate Route for secure.example.com', + }; const httpsId = generateRouteId(httpsRoute); expect(httpsId).toInclude('secure-example-com'); expect(httpsId).toInclude('443'); expect(httpsId).toInclude('forward'); - - const multiDomainRoute = createHttpRoute(['example.com', 'example.org'], { host: 'localhost', port: 3000 }); + + const multiDomainRoute: IRouteConfig = { + match: { ports: 80, domains: ['example.com', 'example.org'] }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }] }, + name: 'HTTP Route for example.com,example.org', + }; const multiDomainId = generateRouteId(multiDomainRoute); expect(multiDomainId).toInclude('example-com-example-org'); }); tap.test('Route Utilities - cloneRoute', async () => { // Create a route and clone it - const originalRoute = createHttpsTerminateRoute('example.com', { host: 'localhost', port: 3000 }, { - certificate: 'auto', - name: 'Original Route' - }); - + const originalRoute: IRouteConfig = { + match: { ports: 443, domains: 'example.com' }, + action: { type: 'forward', targets: [{ host: 'localhost', port: 3000 }], tls: { mode: 'terminate', certificate: 'auto' } }, + name: 'Original Route', + }; + const clonedRoute = cloneRoute(originalRoute); - + // Check that the values are identical expect(clonedRoute.name).toEqual(originalRoute.name); expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains); expect(clonedRoute.action.type).toEqual(originalRoute.action.type); expect(clonedRoute.action.targets?.[0]?.port).toEqual(originalRoute.action.targets?.[0]?.port); - + // Modify the clone and check that the original is unchanged clonedRoute.name = 'Modified Clone'; expect(originalRoute.name).toEqual('Original Route'); }); -// --------------------------------- Route Helper Tests --------------------------------- - -tap.test('Route Helpers - createHttpRoute', async () => { - const route = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - - expect(route.match.domains).toEqual('example.com'); - expect(route.match.ports).toEqual(80); - expect(route.action.type).toEqual('forward'); - expect(route.action.targets?.[0]?.host).toEqual('localhost'); - expect(route.action.targets?.[0]?.port).toEqual(3000); - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -tap.test('Route Helpers - createHttpsTerminateRoute', async () => { - const route = createHttpsTerminateRoute('example.com', { host: 'localhost', port: 3000 }, { - certificate: 'auto' - }); - - expect(route.match.domains).toEqual('example.com'); - expect(route.match.ports).toEqual(443); - expect(route.action.type).toEqual('forward'); - expect(route.action.tls.mode).toEqual('terminate'); - expect(route.action.tls.certificate).toEqual('auto'); - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -tap.test('Route Helpers - createHttpToHttpsRedirect', async () => { - const route = createHttpToHttpsRedirect('example.com'); - - expect(route.match.domains).toEqual('example.com'); - expect(route.match.ports).toEqual(80); - expect(route.action.type).toEqual('socket-handler'); - expect(route.action.socketHandler).toBeDefined(); - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -tap.test('Route Helpers - createHttpsPassthroughRoute', async () => { - const route = createHttpsPassthroughRoute('example.com', { host: 'localhost', port: 3000 }); - - expect(route.match.domains).toEqual('example.com'); - expect(route.match.ports).toEqual(443); - expect(route.action.type).toEqual('forward'); - expect(route.action.tls.mode).toEqual('passthrough'); - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -tap.test('Route Helpers - createCompleteHttpsServer', async () => { - const routes = createCompleteHttpsServer('example.com', { host: 'localhost', port: 3000 }, { - certificate: 'auto' - }); - - expect(routes.length).toEqual(2); - - // HTTPS route - expect(routes[0].match.domains).toEqual('example.com'); - expect(routes[0].match.ports).toEqual(443); - expect(routes[0].action.type).toEqual('forward'); - expect(routes[0].action.tls.mode).toEqual('terminate'); - - // HTTP redirect route - expect(routes[1].match.domains).toEqual('example.com'); - expect(routes[1].match.ports).toEqual(80); - expect(routes[1].action.type).toEqual('socket-handler'); - - const validation1 = validateRouteConfig(routes[0]); - const validation2 = validateRouteConfig(routes[1]); - expect(validation1.valid).toBeTrue(); - expect(validation2.valid).toBeTrue(); -}); - -// createStaticFileRoute has been removed - static file serving should be handled by -// external servers (nginx/apache) behind the proxy - -tap.test('Route Helpers - createApiRoute', async () => { - const route = createApiRoute('api.example.com', '/v1', { host: 'localhost', port: 3000 }, { - useTls: true, - certificate: 'auto', - addCorsHeaders: true - }); - - expect(route.match.domains).toEqual('api.example.com'); - expect(route.match.ports).toEqual(443); - expect(route.match.path).toEqual('/v1/*'); - expect(route.action.type).toEqual('forward'); - expect(route.action.tls.mode).toEqual('terminate'); - - // Check CORS headers if they exist - if (route.headers && route.headers.response) { - expect(route.headers.response['Access-Control-Allow-Origin']).toEqual('*'); - } - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -tap.test('Route Helpers - createWebSocketRoute', async () => { - const route = createWebSocketRoute('ws.example.com', '/socket', { host: 'localhost', port: 3000 }, { - useTls: true, - certificate: 'auto', - pingInterval: 15000 - }); - - expect(route.match.domains).toEqual('ws.example.com'); - expect(route.match.ports).toEqual(443); - expect(route.match.path).toEqual('/socket'); - expect(route.action.type).toEqual('forward'); - expect(route.action.tls.mode).toEqual('terminate'); - - // Check websocket configuration if it exists - if (route.action.websocket) { - expect(route.action.websocket.enabled).toBeTrue(); - expect(route.action.websocket.pingInterval).toEqual(15000); - } - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -tap.test('Route Helpers - createLoadBalancerRoute', async () => { - const route = createLoadBalancerRoute( - 'loadbalancer.example.com', - ['server1.local', 'server2.local', 'server3.local'], - 8080, - { - tls: { - mode: 'terminate', - certificate: 'auto' - } - } - ); - - expect(route.match.domains).toEqual('loadbalancer.example.com'); - expect(route.match.ports).toEqual(443); - expect(route.action.type).toEqual('forward'); - expect(route.action.targets).toBeDefined(); - if (route.action.targets && Array.isArray(route.action.targets[0]?.host)) { - expect((route.action.targets[0].host as string[]).length).toEqual(3); - } - expect(route.action.targets?.[0]?.port).toEqual(8080); - expect(route.action.tls.mode).toEqual('terminate'); - - const validationResult = validateRouteConfig(route); - expect(validationResult.valid).toBeTrue(); -}); - -// --------------------------------- Route Pattern Tests --------------------------------- - -tap.test('Route Patterns - createApiGatewayRoute', async () => { - // Create API Gateway route - const apiGatewayRoute = createApiGatewayRoute( - 'api.example.com', - '/v1', - { host: 'localhost', port: 3000 }, - { - useTls: true, - addCorsHeaders: true - } - ); - - // Validate route configuration - expect(apiGatewayRoute.match.domains).toEqual('api.example.com'); - expect(apiGatewayRoute.match.path).toInclude('/v1'); - expect(apiGatewayRoute.action.type).toEqual('forward'); - expect(apiGatewayRoute.action.targets?.[0]?.port).toEqual(3000); - - // Check TLS configuration - if (apiGatewayRoute.action.tls) { - expect(apiGatewayRoute.action.tls.mode).toEqual('terminate'); - } - - // Check CORS headers - if (apiGatewayRoute.headers && apiGatewayRoute.headers.response) { - expect(apiGatewayRoute.headers.response['Access-Control-Allow-Origin']).toEqual('*'); - } - - const result = validateRouteConfig(apiGatewayRoute); - expect(result.valid).toBeTrue(); -}); - -// createStaticFileServerRoute has been removed - static file serving should be handled by -// external servers (nginx/apache) behind the proxy - -tap.test('Route Patterns - createWebSocketPattern', async () => { - // Create WebSocket route pattern - const wsRoute = createWebSocketPattern( - 'ws.example.com', - { host: 'localhost', port: 3000 }, - { - useTls: true, - path: '/socket', - pingInterval: 10000 - } - ); - - // Validate route configuration - expect(wsRoute.match.domains).toEqual('ws.example.com'); - expect(wsRoute.match.path).toEqual('/socket'); - expect(wsRoute.action.type).toEqual('forward'); - expect(wsRoute.action.targets?.[0]?.port).toEqual(3000); - - // Check TLS configuration - if (wsRoute.action.tls) { - expect(wsRoute.action.tls.mode).toEqual('terminate'); - } - - // Check websocket configuration if it exists - if (wsRoute.action.websocket) { - expect(wsRoute.action.websocket.enabled).toBeTrue(); - expect(wsRoute.action.websocket.pingInterval).toEqual(10000); - } - - const result = validateRouteConfig(wsRoute); - expect(result.valid).toBeTrue(); -}); - -tap.test('Route Patterns - createLoadBalancerRoute pattern', async () => { - // Create load balancer route pattern with missing algorithm as it might not be implemented yet - try { - const lbRoute = createLbPattern( - 'lb.example.com', - [ - { host: 'server1.local', port: 8080 }, - { host: 'server2.local', port: 8080 }, - { host: 'server3.local', port: 8080 } - ], - { - useTls: true - } - ); - - // Validate route configuration - expect(lbRoute.match.domains).toEqual('lb.example.com'); - expect(lbRoute.action.type).toEqual('forward'); - - // Check target hosts - if (lbRoute.action.targets && Array.isArray(lbRoute.action.targets[0]?.host)) { - expect((lbRoute.action.targets[0].host as string[]).length).toEqual(3); - } - - // Check TLS configuration - if (lbRoute.action.tls) { - expect(lbRoute.action.tls.mode).toEqual('terminate'); - } - - const result = validateRouteConfig(lbRoute); - expect(result.valid).toBeTrue(); - } catch (error) { - // If the pattern is not implemented yet, skip this test - console.log('Load balancer pattern might not be fully implemented yet'); - } -}); - -tap.test('Route Security - addRateLimiting', async () => { - // Create base route - const baseRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - - // Add rate limiting - const secureRoute = addRateLimiting(baseRoute, { - maxRequests: 100, - window: 60, // 1 minute - keyBy: 'ip' - }); - - // Check if rate limiting is applied - if (secureRoute.security) { - expect(secureRoute.security.rateLimit?.enabled).toBeTrue(); - expect(secureRoute.security.rateLimit?.maxRequests).toEqual(100); - expect(secureRoute.security.rateLimit?.window).toEqual(60); - expect(secureRoute.security.rateLimit?.keyBy).toEqual('ip'); - } else { - // Skip this test if security features are not implemented yet - console.log('Security features not implemented yet in route configuration'); - } - - // Just check that the route itself is valid - const result = validateRouteConfig(secureRoute); - expect(result.valid).toBeTrue(); -}); - -tap.test('Route Security - addBasicAuth', async () => { - // Create base route - const baseRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - - // Add basic authentication - const authRoute = addBasicAuth(baseRoute, { - users: [ - { username: 'admin', password: 'secret' }, - { username: 'user', password: 'password' } - ], - realm: 'Protected Area', - excludePaths: ['/public'] - }); - - // Check if basic auth is applied - if (authRoute.security) { - expect(authRoute.security.basicAuth?.enabled).toBeTrue(); - expect(authRoute.security.basicAuth?.users.length).toEqual(2); - expect(authRoute.security.basicAuth?.realm).toEqual('Protected Area'); - expect(authRoute.security.basicAuth?.excludePaths).toInclude('/public'); - } else { - // Skip this test if security features are not implemented yet - console.log('Security features not implemented yet in route configuration'); - } - - // Check that the route itself is valid - const result = validateRouteConfig(authRoute); - expect(result.valid).toBeTrue(); -}); - -tap.test('Route Security - addJwtAuth', async () => { - // Create base route - const baseRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); - - // Add JWT authentication - const jwtRoute = addJwtAuth(baseRoute, { - secret: 'your-jwt-secret-key', - algorithm: 'HS256', - issuer: 'auth.example.com', - audience: 'api.example.com', - expiresIn: 3600 - }); - - // Check if JWT auth is applied - if (jwtRoute.security) { - expect(jwtRoute.security.jwtAuth?.enabled).toBeTrue(); - expect(jwtRoute.security.jwtAuth?.secret).toEqual('your-jwt-secret-key'); - expect(jwtRoute.security.jwtAuth?.algorithm).toEqual('HS256'); - expect(jwtRoute.security.jwtAuth?.issuer).toEqual('auth.example.com'); - expect(jwtRoute.security.jwtAuth?.audience).toEqual('api.example.com'); - expect(jwtRoute.security.jwtAuth?.expiresIn).toEqual(3600); - } else { - // Skip this test if security features are not implemented yet - console.log('Security features not implemented yet in route configuration'); - } - - // Check that the route itself is valid - const result = validateRouteConfig(jwtRoute); - expect(result.valid).toBeTrue(); -}); - -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8d429d4..84b4408 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '26.3.0', + version: '27.0.0', 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.' } diff --git a/ts/proxies/smart-proxy/utils/index.ts b/ts/proxies/smart-proxy/utils/index.ts index 6884ff1..2d9fe9c 100644 --- a/ts/proxies/smart-proxy/utils/index.ts +++ b/ts/proxies/smart-proxy/utils/index.ts @@ -2,12 +2,9 @@ * SmartProxy Route Utilities * * This file exports all route-related utilities for the SmartProxy module, - * including helpers, validators, utilities, and patterns for working with routes. + * including validators, utilities, and socket handlers for working with routes. */ -// Export route helpers for creating route configurations -export * from './route-helpers.js'; - // Export route validator (class-based and functional API) export * from './route-validator.js'; @@ -20,10 +17,5 @@ export { generateDefaultCertificate } from './default-cert-generator.js'; // Export concurrency semaphore export { ConcurrencySemaphore } from './concurrency-semaphore.js'; -// Export additional functions from route-helpers that weren't already exported -export { - createApiGatewayRoute, - addRateLimiting, - addBasicAuth, - addJwtAuth -} from './route-helpers.js'; \ No newline at end of file +// Export socket handlers +export { SocketHandlers } from './socket-handlers.js'; diff --git a/ts/proxies/smart-proxy/utils/route-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers.ts deleted file mode 100644 index fd9342d..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Route Helper Functions - * - * This file re-exports all route helper functions for backwards compatibility. - * The actual implementations have been split into focused modules in the route-helpers/ directory. - * - * @see ./route-helpers/index.ts for the modular exports - */ - -// Re-export everything from the modular helpers -export * from './route-helpers/index.js'; diff --git a/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts deleted file mode 100644 index 8ecddfe..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * API Route Helper Functions - * - * This module provides utility functions for creating API route configurations. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js'; -import { mergeRouteConfigs } from '../route-utils.js'; -import { createHttpRoute } from './http-helpers.js'; -import { createHttpsTerminateRoute } from './https-helpers.js'; - -/** - * Create an API route configuration - * @param domains Domain(s) to match - * @param apiPath API base path (e.g., "/api") - * @param target Target host and port - * @param options Additional route options - * @returns Route configuration object - */ -export function createApiRoute( - domains: string | string[], - apiPath: string, - target: { host: string | string[]; port: number }, - options: { - useTls?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - addCorsHeaders?: boolean; - httpPort?: number | number[]; - httpsPort?: number | number[]; - name?: string; - [key: string]: any; - } = {} -): IRouteConfig { - // Normalize API path - const normalizedPath = apiPath.startsWith('/') ? apiPath : `/${apiPath}`; - const pathWithWildcard = normalizedPath.endsWith('/') - ? `${normalizedPath}*` - : `${normalizedPath}/*`; - - // Create route match - const match: IRouteMatch = { - ports: options.useTls - ? (options.httpsPort || 443) - : (options.httpPort || 80), - domains, - path: pathWithWildcard - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [target] - }; - - // Add TLS configuration if using HTTPS - if (options.useTls) { - action.tls = { - mode: 'terminate', - certificate: options.certificate || 'auto' - }; - } - - // Add CORS headers if requested - const headers: Record> = {}; - if (options.addCorsHeaders) { - headers.response = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Max-Age': '86400' - }; - } - - // Create the route config - return { - match, - action, - headers: Object.keys(headers).length > 0 ? headers : undefined, - name: options.name || `API Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - priority: options.priority || 100, // Higher priority for specific path matches - ...options - }; -} - -/** - * Create an API Gateway route pattern - * @param domains Domain(s) to match - * @param apiBasePath Base path for API endpoints (e.g., '/api') - * @param target Target host and port - * @param options Additional route options - * @returns API route configuration - */ -export function createApiGatewayRoute( - domains: string | string[], - apiBasePath: string, - target: { host: string | string[]; port: number }, - options: { - useTls?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - addCorsHeaders?: boolean; - [key: string]: any; - } = {} -): IRouteConfig { - // Normalize apiBasePath to ensure it starts with / and doesn't end with / - const normalizedPath = apiBasePath.startsWith('/') - ? apiBasePath - : `/${apiBasePath}`; - - // Add wildcard to path to match all API endpoints - const apiPath = normalizedPath.endsWith('/') - ? `${normalizedPath}*` - : `${normalizedPath}/*`; - - // Create base route - const baseRoute = options.useTls - ? createHttpsTerminateRoute(domains, target, { - certificate: options.certificate || 'auto' - }) - : createHttpRoute(domains, target); - - // Add API-specific configurations - const apiRoute: Partial = { - match: { - ...baseRoute.match, - path: apiPath - }, - name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`, - priority: options.priority || 100 // Higher priority for specific path matching - }; - - // Add CORS headers if requested - if (options.addCorsHeaders) { - apiRoute.headers = { - response: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - 'Access-Control-Max-Age': '86400' - } - }; - } - - return mergeRouteConfigs(baseRoute, apiRoute); -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts deleted file mode 100644 index 9822b7f..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Dynamic Route Helper Functions - * - * This module provides utility functions for creating dynamic routes - * with context-based host and port mapping. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange, IRouteContext } from '../../models/route-types.js'; - -/** - * Create a helper function that applies a port offset - * @param offset The offset to apply to the matched port - * @returns A function that adds the offset to the matched port - */ -export function createPortOffset(offset: number): (context: IRouteContext) => number { - return (context: IRouteContext) => context.port + offset; -} - -/** - * Create a port mapping route with context-based port function - * @param options Port mapping route options - * @returns Route configuration object - */ -export function createPortMappingRoute(options: { - sourcePortRange: TPortRange; - targetHost: string | string[] | ((context: IRouteContext) => string | string[]); - portMapper: (context: IRouteContext) => number; - name?: string; - domains?: string | string[]; - priority?: number; - [key: string]: any; -}): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.sourcePortRange, - domains: options.domains - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [{ - host: options.targetHost, - port: options.portMapper - }] - }; - - // Create the route config - return { - match, - action, - name: options.name || `Port Mapping Route for ${options.domains || 'all domains'}`, - priority: options.priority, - ...options - }; -} - -/** - * Create a simple offset port mapping route - * @param options Offset port mapping route options - * @returns Route configuration object - */ -export function createOffsetPortMappingRoute(options: { - ports: TPortRange; - targetHost: string | string[]; - offset: number; - name?: string; - domains?: string | string[]; - priority?: number; - [key: string]: any; -}): IRouteConfig { - const { ports, targetHost, offset, name, domains, priority, ...rest } = options; - return createPortMappingRoute({ - sourcePortRange: ports, - targetHost, - portMapper: (context) => context.port + offset, - name: name || `Offset Mapping (${offset > 0 ? '+' : ''}${offset}) for ${domains || 'all domains'}`, - domains, - priority, - ...rest - }); -} - -/** - * Create a dynamic route with context-based host and port mapping - * @param options Dynamic route options - * @returns Route configuration object - */ -export function createDynamicRoute(options: { - ports: TPortRange; - targetHost: (context: IRouteContext) => string | string[]; - portMapper: (context: IRouteContext) => number; - name?: string; - domains?: string | string[]; - path?: string; - clientIp?: string[]; - priority?: number; - [key: string]: any; -}): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.ports, - domains: options.domains, - path: options.path, - clientIp: options.clientIp - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [{ - host: options.targetHost, - port: options.portMapper - }] - }; - - // Create the route config - return { - match, - action, - name: options.name || `Dynamic Route for ${options.domains || 'all domains'}`, - priority: options.priority, - ...options - }; -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts deleted file mode 100644 index e80239a..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * HTTP Route Helper Functions - * - * This module provides utility functions for creating HTTP route configurations. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js'; - -/** - * Create an HTTP-only route configuration - * @param domains Domain(s) to match - * @param target Target host and port - * @param options Additional route options - * @returns Route configuration object - */ -export function createHttpRoute( - domains: string | string[], - target: { host: string | string[]; port: number }, - options: Partial = {} -): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.match?.ports || 80, - domains - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [target] - }; - - // Create the route config - return { - match, - action, - name: options.name || `HTTP Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - ...options - }; -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts deleted file mode 100644 index 2d96887..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * HTTPS Route Helper Functions - * - * This module provides utility functions for creating HTTPS route configurations - * including TLS termination and passthrough routes. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js'; -import { SocketHandlers } from './socket-handlers.js'; - -/** - * Create an HTTPS route with TLS termination - * @param domains Domain(s) to match - * @param target Target host and port - * @param options Additional route options - * @returns Route configuration object - */ -export function createHttpsTerminateRoute( - domains: string | string[], - target: { host: string | string[]; port: number }, - options: { - certificate?: 'auto' | { key: string; cert: string }; - httpPort?: number | number[]; - httpsPort?: number | number[]; - reencrypt?: boolean; - name?: string; - [key: string]: any; - } = {} -): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.httpsPort || 443, - domains - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [target], - tls: { - mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate', - certificate: options.certificate || 'auto' - } - }; - - // Create the route config - return { - match, - action, - name: options.name || `HTTPS Route for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - ...options - }; -} - -/** - * Create an HTTP to HTTPS redirect route - * @param domains Domain(s) to match - * @param httpsPort HTTPS port to redirect to (default: 443) - * @param options Additional route options - * @returns Route configuration object - */ -export function createHttpToHttpsRedirect( - domains: string | string[], - httpsPort: number = 443, - options: Partial = {} -): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.match?.ports || 80, - domains - }; - - // Create route action - const action: IRouteAction = { - type: 'socket-handler', - socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301) - }; - - // Create the route config - return { - match, - action, - name: options.name || `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - ...options - }; -} - -/** - * Create an HTTPS passthrough route (SNI-based forwarding without TLS termination) - * @param domains Domain(s) to match - * @param target Target host and port - * @param options Additional route options - * @returns Route configuration object - */ -export function createHttpsPassthroughRoute( - domains: string | string[], - target: { host: string | string[]; port: number }, - options: Partial = {} -): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.match?.ports || 443, - domains - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [target], - tls: { - mode: 'passthrough' - } - }; - - // Create the route config - return { - match, - action, - name: options.name || `HTTPS Passthrough for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - ...options - }; -} - -/** - * Create a complete HTTPS server with HTTP to HTTPS redirects - * @param domains Domain(s) to match - * @param target Target host and port - * @param options Additional configuration options - * @returns Array of two route configurations (HTTPS and HTTP redirect) - */ -export function createCompleteHttpsServer( - domains: string | string[], - target: { host: string | string[]; port: number }, - options: { - certificate?: 'auto' | { key: string; cert: string }; - httpPort?: number | number[]; - httpsPort?: number | number[]; - reencrypt?: boolean; - name?: string; - [key: string]: any; - } = {} -): IRouteConfig[] { - // Create the HTTPS route - const httpsRoute = createHttpsTerminateRoute(domains, target, options); - - // Create the HTTP redirect route - const httpRedirectRoute = createHttpToHttpsRedirect( - domains, - // Extract the HTTPS port from the HTTPS route - ensure it's a number - typeof options.httpsPort === 'number' ? options.httpsPort : - Array.isArray(options.httpsPort) ? options.httpsPort[0] : 443, - { - // Set the HTTP port - match: { - ports: options.httpPort || 80, - domains - }, - name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains}` - } - ); - - return [httpsRoute, httpRedirectRoute]; -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/index.ts b/ts/proxies/smart-proxy/utils/route-helpers/index.ts deleted file mode 100644 index e5d96fb..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Route Helper Functions - * - * This module provides utility functions for creating route configurations for common scenarios. - * These functions aim to simplify the creation of route configurations for typical use cases. - * - * This barrel file re-exports all helper functions for backwards compatibility. - */ - -// HTTP helpers -export { createHttpRoute } from './http-helpers.js'; - -// HTTPS helpers -export { - createHttpsTerminateRoute, - createHttpToHttpsRedirect, - createHttpsPassthroughRoute, - createCompleteHttpsServer -} from './https-helpers.js'; - -// WebSocket helpers -export { createWebSocketRoute } from './websocket-helpers.js'; - -// Load balancer helpers -export { - createLoadBalancerRoute, - createSmartLoadBalancer -} from './load-balancer-helpers.js'; - -// NFTables helpers -export { - createNfTablesRoute, - createNfTablesTerminateRoute, - createCompleteNfTablesHttpsServer -} from './nftables-helpers.js'; - -// Dynamic routing helpers -export { - createPortOffset, - createPortMappingRoute, - createOffsetPortMappingRoute, - createDynamicRoute -} from './dynamic-helpers.js'; - -// API helpers -export { - createApiRoute, - createApiGatewayRoute -} from './api-helpers.js'; - -// Security helpers -export { - addRateLimiting, - addBasicAuth, - addJwtAuth -} from './security-helpers.js'; - -// Socket handlers -export { - SocketHandlers, - createSocketHandlerRoute -} from './socket-handlers.js'; diff --git a/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts deleted file mode 100644 index 35c04c6..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Load Balancer Route Helper Functions - * - * This module provides utility functions for creating load balancer route configurations. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../../models/route-types.js'; - -/** - * Create a load balancer route (round-robin between multiple backend hosts) - * @param domains Domain(s) to match - * @param backendsOrHosts Array of backend servers OR array of host strings (legacy) - * @param portOrOptions Port number (legacy) OR options object - * @param options Additional route options (legacy) - * @returns Route configuration object - */ -export function createLoadBalancerRoute( - domains: string | string[], - backendsOrHosts: Array<{ host: string; port: number }> | string[], - portOrOptions?: number | { - tls?: { - mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; - certificate?: 'auto' | { key: string; cert: string }; - }; - useTls?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - algorithm?: 'round-robin' | 'least-connections' | 'ip-hash'; - healthCheck?: { - path: string; - interval: number; - timeout: number; - unhealthyThreshold: number; - healthyThreshold: number; - }; - [key: string]: any; - }, - options?: { - tls?: { - mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt'; - certificate?: 'auto' | { key: string; cert: string }; - }; - [key: string]: any; - } -): IRouteConfig { - // Handle legacy signature: (domains, hosts[], port, options) - let backends: Array<{ host: string; port: number }>; - let finalOptions: any; - - if (Array.isArray(backendsOrHosts) && backendsOrHosts.length > 0 && typeof backendsOrHosts[0] === 'string') { - // Legacy signature - const hosts = backendsOrHosts as string[]; - const port = portOrOptions as number; - backends = hosts.map(host => ({ host, port })); - finalOptions = options || {}; - } else { - // New signature - backends = backendsOrHosts as Array<{ host: string; port: number }>; - finalOptions = (portOrOptions as any) || {}; - } - - // Extract hosts and ensure all backends use the same port - const port = backends[0].port; - const hosts = backends.map(backend => backend.host); - - // Create route match - const match: IRouteMatch = { - ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80), - domains - }; - - // Create route target - const target: IRouteTarget = { - host: hosts, - port - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [target] - }; - - // Add TLS configuration if provided - if (finalOptions.tls || finalOptions.useTls) { - action.tls = { - mode: finalOptions.tls?.mode || 'terminate', - certificate: finalOptions.tls?.certificate || finalOptions.certificate || 'auto' - }; - } - - // Add load balancing options - if (finalOptions.algorithm || finalOptions.healthCheck) { - action.loadBalancing = { - algorithm: finalOptions.algorithm || 'round-robin', - healthCheck: finalOptions.healthCheck - }; - } - - // Create the route config - return { - match, - action, - name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - ...finalOptions - }; -} - -/** - * Create a smart load balancer with dynamic domain-based backend selection - * @param options Smart load balancer options - * @returns Route configuration object - */ -export function createSmartLoadBalancer(options: { - ports: TPortRange; - domainTargets: Record; - portMapper: (context: IRouteContext) => number; - name?: string; - defaultTarget?: string | string[]; - priority?: number; - [key: string]: any; -}): IRouteConfig { - // Extract all domain keys to create the match criteria - const domains = Object.keys(options.domainTargets); - - // Create the smart host selector function - const hostSelector = (context: IRouteContext) => { - const domain = context.domain || ''; - return options.domainTargets[domain] || options.defaultTarget || 'localhost'; - }; - - // Create route match - const match: IRouteMatch = { - ports: options.ports, - domains - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [{ - host: hostSelector, - port: options.portMapper - }] - }; - - // Create the route config - return { - match, - action, - name: options.name || `Smart Load Balancer for ${domains.join(', ')}`, - priority: options.priority, - ...options - }; -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts deleted file mode 100644 index eaf3e28..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * NFTables Route Helper Functions - * - * This module provides utility functions for creating NFTables-based route configurations - * for high-performance packet forwarding at the kernel level. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange } from '../../models/route-types.js'; -import { createHttpToHttpsRedirect } from './https-helpers.js'; - -/** - * Create an NFTables-based route for high-performance packet forwarding - * @param nameOrDomains Name or domain(s) to match - * @param target Target host and port - * @param options Additional route options - * @returns Route configuration object - */ -export function createNfTablesRoute( - nameOrDomains: string | string[], - target: { host: string; port: number | 'preserve' }, - options: { - ports?: TPortRange; - protocol?: 'tcp' | 'udp' | 'all'; - preserveSourceIP?: boolean; - ipAllowList?: string[]; - ipBlockList?: string[]; - maxRate?: string; - priority?: number; - useTls?: boolean; - tableName?: string; - useIPSets?: boolean; - useAdvancedNAT?: boolean; - } = {} -): IRouteConfig { - // Determine if this is a name or domain - let name: string; - let domains: string | string[] | undefined; - - if (Array.isArray(nameOrDomains) || (typeof nameOrDomains === 'string' && nameOrDomains.includes('.'))) { - domains = nameOrDomains; - name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains; - } else { - name = nameOrDomains; - domains = undefined; // No domains - } - - // Create route match - const match: IRouteMatch = { - domains, - ports: options.ports || 80 - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [{ - host: target.host, - port: target.port - }], - forwardingEngine: 'nftables', - nftables: { - protocol: options.protocol || 'tcp', - preserveSourceIP: options.preserveSourceIP, - maxRate: options.maxRate, - priority: options.priority, - tableName: options.tableName, - useIPSets: options.useIPSets, - useAdvancedNAT: options.useAdvancedNAT - } - }; - - // Add TLS options if needed - if (options.useTls) { - action.tls = { - mode: 'passthrough' - }; - } - - // Create the route config - const routeConfig: IRouteConfig = { - name, - match, - action - }; - - // Add security if allowed or blocked IPs are specified - if (options.ipAllowList?.length || options.ipBlockList?.length) { - routeConfig.security = { - ipAllowList: options.ipAllowList, - ipBlockList: options.ipBlockList - }; - } - - return routeConfig; -} - -/** - * Create an NFTables-based TLS termination route - * @param nameOrDomains Name or domain(s) to match - * @param target Target host and port - * @param options Additional route options - * @returns Route configuration object - */ -export function createNfTablesTerminateRoute( - nameOrDomains: string | string[], - target: { host: string; port: number | 'preserve' }, - options: { - ports?: TPortRange; - protocol?: 'tcp' | 'udp' | 'all'; - preserveSourceIP?: boolean; - ipAllowList?: string[]; - ipBlockList?: string[]; - maxRate?: string; - priority?: number; - tableName?: string; - useIPSets?: boolean; - useAdvancedNAT?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - } = {} -): IRouteConfig { - // Create basic NFTables route - const route = createNfTablesRoute( - nameOrDomains, - target, - { - ...options, - ports: options.ports || 443, - useTls: false - } - ); - - // Set TLS termination - route.action.tls = { - mode: 'terminate', - certificate: options.certificate || 'auto' - }; - - return route; -} - -/** - * Create a complete NFTables-based HTTPS setup with HTTP redirect - * @param nameOrDomains Name or domain(s) to match - * @param target Target host and port - * @param options Additional route options - * @returns Array of two route configurations (HTTPS and HTTP redirect) - */ -export function createCompleteNfTablesHttpsServer( - nameOrDomains: string | string[], - target: { host: string; port: number | 'preserve' }, - options: { - httpPort?: TPortRange; - httpsPort?: TPortRange; - protocol?: 'tcp' | 'udp' | 'all'; - preserveSourceIP?: boolean; - ipAllowList?: string[]; - ipBlockList?: string[]; - maxRate?: string; - priority?: number; - tableName?: string; - useIPSets?: boolean; - useAdvancedNAT?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - } = {} -): IRouteConfig[] { - // Create the HTTPS route using NFTables - const httpsRoute = createNfTablesTerminateRoute( - nameOrDomains, - target, - { - ...options, - ports: options.httpsPort || 443 - } - ); - - // Determine the domain(s) for HTTP redirect - const domains = typeof nameOrDomains === 'string' && !nameOrDomains.includes('.') - ? undefined - : nameOrDomains; - - // Extract the HTTPS port for the redirect destination - const httpsPort = typeof options.httpsPort === 'number' - ? options.httpsPort - : Array.isArray(options.httpsPort) && typeof options.httpsPort[0] === 'number' - ? options.httpsPort[0] - : 443; - - // Create the HTTP redirect route (this uses standard forwarding, not NFTables) - const httpRedirectRoute = createHttpToHttpsRedirect( - domains as any, // Type cast needed since domains can be undefined now - httpsPort, - { - match: { - ports: options.httpPort || 80, - domains: domains as any // Type cast needed since domains can be undefined now - }, - name: `HTTP to HTTPS Redirect for ${Array.isArray(domains) ? domains.join(', ') : domains || 'all domains'}` - } - ); - - return [httpsRoute, httpRedirectRoute]; -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts deleted file mode 100644 index 13447b2..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Security Route Helper Functions - * - * This module provides utility functions for adding security features to routes. - */ - -import type { IRouteConfig } from '../../models/route-types.js'; -import { mergeRouteConfigs } from '../route-utils.js'; - -/** - * Create a rate limiting route pattern - * @param baseRoute Base route to add rate limiting to - * @param rateLimit Rate limiting configuration - * @returns Route with rate limiting - */ -export function addRateLimiting( - baseRoute: IRouteConfig, - rateLimit: { - maxRequests: number; - window: number; // Time window in seconds - keyBy?: 'ip' | 'path' | 'header'; - headerName?: string; // Required if keyBy is 'header' - errorMessage?: string; - } -): IRouteConfig { - return mergeRouteConfigs(baseRoute, { - security: { - rateLimit: { - enabled: true, - maxRequests: rateLimit.maxRequests, - window: rateLimit.window, - keyBy: rateLimit.keyBy || 'ip', - headerName: rateLimit.headerName, - errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.' - } - } - }); -} - -/** - * Create a basic authentication route pattern - * @param baseRoute Base route to add authentication to - * @param auth Authentication configuration - * @returns Route with basic authentication - */ -export function addBasicAuth( - baseRoute: IRouteConfig, - auth: { - users: Array<{ username: string; password: string }>; - realm?: string; - excludePaths?: string[]; - } -): IRouteConfig { - return mergeRouteConfigs(baseRoute, { - security: { - basicAuth: { - enabled: true, - users: auth.users, - realm: auth.realm || 'Restricted Area', - excludePaths: auth.excludePaths || [] - } - } - }); -} - -/** - * Create a JWT authentication route pattern - * @param baseRoute Base route to add JWT authentication to - * @param jwt JWT authentication configuration - * @returns Route with JWT authentication - */ -export function addJwtAuth( - baseRoute: IRouteConfig, - jwt: { - secret: string; - algorithm?: string; - issuer?: string; - audience?: string; - expiresIn?: number; // Time in seconds - excludePaths?: string[]; - } -): IRouteConfig { - return mergeRouteConfigs(baseRoute, { - security: { - jwtAuth: { - enabled: true, - secret: jwt.secret, - algorithm: jwt.algorithm || 'HS256', - issuer: jwt.issuer, - audience: jwt.audience, - expiresIn: jwt.expiresIn, - excludePaths: jwt.excludePaths || [] - } - } - }); -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts deleted file mode 100644 index 52a6a46..0000000 --- a/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * WebSocket Route Helper Functions - * - * This module provides utility functions for creating WebSocket route configurations. - */ - -import type { IRouteConfig, IRouteMatch, IRouteAction } from '../../models/route-types.js'; - -/** - * Create a WebSocket route configuration - * @param domains Domain(s) to match - * @param targetOrPath Target server OR WebSocket path (legacy) - * @param targetOrOptions Target server (legacy) OR options - * @param options Additional route options (legacy) - * @returns Route configuration object - */ -export function createWebSocketRoute( - domains: string | string[], - targetOrPath: { host: string | string[]; port: number } | string, - targetOrOptions?: { host: string | string[]; port: number } | { - useTls?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - path?: string; - httpPort?: number | number[]; - httpsPort?: number | number[]; - pingInterval?: number; - pingTimeout?: number; - name?: string; - [key: string]: any; - }, - options?: { - useTls?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - httpPort?: number | number[]; - httpsPort?: number | number[]; - pingInterval?: number; - pingTimeout?: number; - name?: string; - [key: string]: any; - } -): IRouteConfig { - // Handle different signatures - let target: { host: string | string[]; port: number }; - let wsPath: string; - let finalOptions: any; - - if (typeof targetOrPath === 'string') { - // Legacy signature: (domains, path, target, options) - wsPath = targetOrPath; - target = targetOrOptions as { host: string | string[]; port: number }; - finalOptions = options || {}; - } else { - // New signature: (domains, target, options) - target = targetOrPath; - finalOptions = (targetOrOptions as any) || {}; - wsPath = finalOptions.path || '/ws'; - } - - // Normalize WebSocket path - const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`; - - // Create route match - const match: IRouteMatch = { - ports: finalOptions.useTls - ? (finalOptions.httpsPort || 443) - : (finalOptions.httpPort || 80), - domains, - path: normalizedPath - }; - - // Create route action - const action: IRouteAction = { - type: 'forward', - targets: [target], - websocket: { - enabled: true, - pingInterval: finalOptions.pingInterval || 30000, // 30 seconds - pingTimeout: finalOptions.pingTimeout || 5000 // 5 seconds - } - }; - - // Add TLS configuration if using HTTPS - if (finalOptions.useTls) { - action.tls = { - mode: 'terminate', - certificate: finalOptions.certificate || 'auto' - }; - } - - // Create the route config - return { - match, - action, - name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - priority: finalOptions.priority || 100, // Higher priority for WebSocket routes - ...finalOptions - }; -} diff --git a/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts b/ts/proxies/smart-proxy/utils/socket-handlers.ts similarity index 91% rename from ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts rename to ts/proxies/smart-proxy/utils/socket-handlers.ts index ba5885c..2be511e 100644 --- a/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts +++ b/ts/proxies/smart-proxy/utils/socket-handlers.ts @@ -5,9 +5,9 @@ * like echoing, proxying, HTTP responses, and redirects. */ -import * as plugins from '../../../../plugins.js'; -import type { IRouteConfig, TPortRange, IRouteContext } from '../../models/route-types.js'; -import { createSocketTracker } from '../../../../core/utils/socket-tracker.js'; +import * as plugins from '../../../plugins.js'; +import type { IRouteContext } from '../models/route-types.js'; +import { createSocketTracker } from '../../../core/utils/socket-tracker.js'; /** * Minimal HTTP request parser for socket handlers. @@ -308,31 +308,3 @@ export const SocketHandlers = { }); } }; - -/** - * Create a socket handler route configuration - */ -export function createSocketHandlerRoute( - domains: string | string[], - ports: TPortRange, - handler: (socket: plugins.net.Socket) => void | Promise, - options: { - name?: string; - priority?: number; - path?: string; - } = {} -): IRouteConfig { - return { - name: options.name || 'socket-handler-route', - priority: options.priority !== undefined ? options.priority : 50, - match: { - domains, - ports, - ...(options.path && { path: options.path }) - }, - action: { - type: 'socket-handler', - socketHandler: handler - } - }; -} diff --git a/tsconfig.json b/tsconfig.json index 1e16be0..d5fdeb2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,10 @@ { "compilerOptions": { - "experimentalDecorators": true, - "useDefineForClassFields": false, "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, - "verbatimModuleSyntax": true, - "ignoreDeprecations": "6.0" + "verbatimModuleSyntax": true }, "exclude": [ "dist_*/**/*.d.ts"