From 6a08bbc558f9424d5bec0bbc763cd6e39f7fc0a0 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Thu, 29 May 2025 10:13:41 +0000 Subject: [PATCH] update --- certs/static-route/meta.json | 6 +- package.json | 2 +- test/test.acme-http-challenge.ts | 46 +++--- test/test.acme-http01-challenge.ts | 68 ++++---- test/test.acme-route-creation.ts | 31 +++- test/test.acme-simple.ts | 40 ++--- test/test.certificate-provisioning.ts | 29 ++-- test/test.certificate-simple.ts | 47 +++--- test/test.forwarding.examples.ts | 22 +-- test/test.forwarding.ts | 7 +- test/test.route-config.ts | 49 +----- test/test.route-utils.ts | 150 +++++------------- ts/proxies/smart-proxy/utils/route-utils.ts | 15 +- .../smart-proxy/utils/route-validators.ts | 2 +- 14 files changed, 211 insertions(+), 303 deletions(-) diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index e4e9f31..b480bec 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -1,5 +1,5 @@ { - "expiryDate": "2025-08-17T16:58:47.999Z", - "issueDate": "2025-05-19T16:58:47.999Z", - "savedAt": "2025-05-19T16:58:48.001Z" + "expiryDate": "2025-08-27T01:45:41.917Z", + "issueDate": "2025-05-29T01:45:41.917Z", + "savedAt": "2025-05-29T01:45:41.919Z" } \ No newline at end of file diff --git a/package.json b/package.json index f4461db..52e7b0e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "author": "Lossless GmbH", "license": "MIT", "scripts": { - "test": "(tstest test/**/test*.ts --verbose)", + "test": "(tstest test/**/test*.ts --verbose --timeout 600)", "build": "(tsbuild tsfolders --allowimplicitany)", "format": "(gitzone format)", "buildDocs": "tsdoc" diff --git a/test/test.acme-http-challenge.ts b/test/test.acme-http-challenge.ts index 6c8b486..ea0de3f 100644 --- a/test/test.acme-http-challenge.ts +++ b/test/test.acme-http-challenge.ts @@ -1,6 +1,6 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; -import { SmartProxy } from '../ts/index.js'; +import { SmartProxy, SocketHandlers } from '../ts/index.js'; tap.test('should handle HTTP requests on port 80 for ACME challenges', async (tools) => { tools.timeout(10000); @@ -17,22 +17,19 @@ tap.test('should handle HTTP requests on port 80 for ACME challenges', async (to path: '/.well-known/acme-challenge/*' }, action: { - type: 'static' as const, - handler: async (context) => { + type: 'socket-handler' as const, + socketHandler: SocketHandlers.httpServer((req, res) => { handledRequests.push({ - path: context.path, - method: context.method, - headers: context.headers + path: req.url, + method: req.method, + headers: req.headers }); // Simulate ACME challenge response - const token = context.path?.split('/').pop() || ''; - return { - status: 200, - headers: { 'Content-Type': 'text/plain' }, - body: `challenge-response-for-${token}` - }; - } + const token = req.url?.split('/').pop() || ''; + res.header('Content-Type', 'text/plain'); + res.send(`challenge-response-for-${token}`); + }) } } ] @@ -79,17 +76,18 @@ tap.test('should parse HTTP headers correctly', async (tools) => { ports: [18081] }, action: { - type: 'static' as const, - handler: async (context) => { - Object.assign(capturedContext, context); - return { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - received: context.headers - }) - }; - } + type: 'socket-handler' as const, + socketHandler: SocketHandlers.httpServer((req, res) => { + Object.assign(capturedContext, { + path: req.url, + method: req.method, + headers: req.headers + }); + res.header('Content-Type', 'application/json'); + res.send(JSON.stringify({ + received: req.headers + })); + }) } } ] diff --git a/test/test.acme-http01-challenge.ts b/test/test.acme-http01-challenge.ts index bc8eb3d..c47abe8 100644 --- a/test/test.acme-http01-challenge.ts +++ b/test/test.acme-http01-challenge.ts @@ -1,5 +1,5 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { SmartProxy } from '../ts/index.js'; +import { SmartProxy, SocketHandlers } from '../ts/index.js'; import * as net from 'net'; // Test that HTTP-01 challenges are properly processed when the initial data arrives @@ -9,36 +9,28 @@ tap.test('should correctly handle HTTP-01 challenge requests with initial data c const challengeResponse = 'mock-response-for-challenge'; const challengePath = `/.well-known/acme-challenge/${challengeToken}`; - // Create a handler function that responds to ACME challenges - const acmeHandler = async (context: any) => { + // Create a socket handler that responds to ACME challenges using httpServer + const acmeHandler = SocketHandlers.httpServer((req, res) => { // Log request details for debugging - console.log(`Received request: ${context.method} ${context.path}`); + console.log(`Received request: ${req.method} ${req.url}`); // Check if this is an ACME challenge request - if (context.path.startsWith('/.well-known/acme-challenge/')) { - const token = context.path.substring('/.well-known/acme-challenge/'.length); + if (req.url?.startsWith('/.well-known/acme-challenge/')) { + const token = req.url.substring('/.well-known/acme-challenge/'.length); // If the token matches our test token, return the response if (token === challengeToken) { - return { - status: 200, - headers: { - 'Content-Type': 'text/plain' - }, - body: challengeResponse - }; + res.header('Content-Type', 'text/plain'); + res.send(challengeResponse); + return; } } // For any other requests, return 404 - return { - status: 404, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Not found' - }; - }; + res.status(404); + res.header('Content-Type', 'text/plain'); + res.send('Not found'); + }); // Create a proxy with the ACME challenge route const proxy = new SmartProxy({ @@ -49,8 +41,8 @@ tap.test('should correctly handle HTTP-01 challenge requests with initial data c path: '/.well-known/acme-challenge/*' }, action: { - type: 'static', - handler: acmeHandler + type: 'socket-handler', + socketHandler: acmeHandler } }] }); @@ -98,27 +90,23 @@ tap.test('should correctly handle HTTP-01 challenge requests with initial data c // Test that non-existent challenge tokens return 404 tap.test('should return 404 for non-existent challenge tokens', async (tapTest) => { - // Create a handler function that behaves like a real ACME handler - const acmeHandler = async (context: any) => { - if (context.path.startsWith('/.well-known/acme-challenge/')) { - const token = context.path.substring('/.well-known/acme-challenge/'.length); + // Create a socket handler that behaves like a real ACME handler + const acmeHandler = SocketHandlers.httpServer((req, res) => { + if (req.url?.startsWith('/.well-known/acme-challenge/')) { + const token = req.url.substring('/.well-known/acme-challenge/'.length); // In this test, we only recognize one specific token if (token === 'valid-token') { - return { - status: 200, - headers: { 'Content-Type': 'text/plain' }, - body: 'valid-response' - }; + res.header('Content-Type', 'text/plain'); + res.send('valid-response'); + return; } } // For all other paths or unrecognized tokens, return 404 - return { - status: 404, - headers: { 'Content-Type': 'text/plain' }, - body: 'Not found' - }; - }; + res.status(404); + res.header('Content-Type', 'text/plain'); + res.send('Not found'); + }); // Create a proxy with the ACME challenge route const proxy = new SmartProxy({ @@ -129,8 +117,8 @@ tap.test('should return 404 for non-existent challenge tokens', async (tapTest) path: '/.well-known/acme-challenge/*' }, action: { - type: 'static', - handler: acmeHandler + type: 'socket-handler', + socketHandler: acmeHandler } }] }); diff --git a/test/test.acme-route-creation.ts b/test/test.acme-route-creation.ts index 6f146d3..fcd7faf 100644 --- a/test/test.acme-route-creation.ts +++ b/test/test.acme-route-creation.ts @@ -53,7 +53,7 @@ tap.test('should create ACME challenge route with high ports', async (tools) => expect(challengeRoute).toBeDefined(); expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*'); expect(challengeRoute.match.ports).toEqual(18080); - expect(challengeRoute.action.type).toEqual('static'); + expect(challengeRoute.action.type).toEqual('socket-handler'); expect(challengeRoute.priority).toEqual(1000); await proxy.stop(); @@ -74,15 +74,30 @@ tap.test('should handle HTTP request parsing correctly', async (tools) => { path: '/test/*' }, action: { - type: 'static' as const, - handler: async (context) => { + type: 'socket-handler' as const, + socketHandler: (socket, context) => { handlerCalled = true; receivedContext = context; - return { - status: 200, - headers: { 'Content-Type': 'text/plain' }, - body: 'OK' - }; + + // Parse HTTP request from socket + socket.once('data', (data) => { + const request = data.toString(); + const lines = request.split('\r\n'); + const [method, path, protocol] = lines[0].split(' '); + + // Send HTTP response + const response = [ + 'HTTP/1.1 200 OK', + 'Content-Type: text/plain', + 'Content-Length: 2', + 'Connection: close', + '', + 'OK' + ].join('\r\n'); + + socket.write(response); + socket.end(); + }); } } } diff --git a/test/test.acme-simple.ts b/test/test.acme-simple.ts index 2200d5c..c028d96 100644 --- a/test/test.acme-simple.ts +++ b/test/test.acme-simple.ts @@ -84,14 +84,26 @@ tap.test('should configure ACME challenge route', async () => { path: '/.well-known/acme-challenge/*' }, action: { - type: 'static', - handler: async (context: any) => { - const token = context.path?.split('/').pop() || ''; - return { - status: 200, - headers: { 'Content-Type': 'text/plain' }, - body: `challenge-response-${token}` - }; + type: 'socket-handler', + socketHandler: (socket: any, context: any) => { + socket.once('data', (data: Buffer) => { + const request = data.toString(); + const lines = request.split('\r\n'); + const [method, path] = lines[0].split(' '); + const token = path?.split('/').pop() || ''; + + const response = [ + 'HTTP/1.1 200 OK', + 'Content-Type: text/plain', + `Content-Length: ${('challenge-response-' + token).length}`, + 'Connection: close', + '', + `challenge-response-${token}` + ].join('\r\n'); + + socket.write(response); + socket.end(); + }); } } }; @@ -101,16 +113,8 @@ tap.test('should configure ACME challenge route', async () => { expect(challengeRoute.match.ports).toEqual(80); expect(challengeRoute.priority).toEqual(1000); - // Test the handler - const context = { - path: '/.well-known/acme-challenge/test-token', - method: 'GET', - headers: {} - }; - - const response = await challengeRoute.action.handler(context); - expect(response.status).toEqual(200); - expect(response.body).toEqual('challenge-response-test-token'); + // Socket handlers are tested differently - they handle raw sockets + expect(challengeRoute.action.socketHandler).toBeDefined(); }); tap.start(); \ No newline at end of file diff --git a/test/test.certificate-provisioning.ts b/test/test.certificate-provisioning.ts index 9010c6d..8f1a545 100644 --- a/test/test.certificate-provisioning.ts +++ b/test/test.certificate-provisioning.ts @@ -4,7 +4,7 @@ import { expect, tap } from '@git.zone/tstest/tapbundle'; const testProxy = new SmartProxy({ routes: [{ name: 'test-route', - match: { ports: 443, domains: 'test.example.com' }, + match: { ports: 9443, domains: 'test.example.com' }, action: { type: 'forward', target: { host: 'localhost', port: 8080 }, @@ -17,7 +17,10 @@ const testProxy = new SmartProxy({ } } } - }] + }], + acme: { + port: 9080 // Use high port for ACME challenges + } }); tap.test('should provision certificate automatically', async () => { @@ -38,7 +41,7 @@ tap.test('should handle static certificates', async () => { const proxy = new SmartProxy({ routes: [{ name: 'static-route', - match: { ports: 443, domains: 'static.example.com' }, + match: { ports: 9444, domains: 'static.example.com' }, action: { type: 'forward', target: { host: 'localhost', port: 8080 }, @@ -67,7 +70,7 @@ tap.test('should handle ACME challenge routes', async () => { const proxy = new SmartProxy({ routes: [{ name: 'auto-cert-route', - match: { ports: 443, domains: 'acme.example.com' }, + match: { ports: 9445, domains: 'acme.example.com' }, action: { type: 'forward', target: { host: 'localhost', port: 8080 }, @@ -77,18 +80,21 @@ tap.test('should handle ACME challenge routes', async () => { acme: { email: 'acme@example.com', useProduction: false, - challengePort: 80 + challengePort: 9081 } } } }, { - name: 'port-80-route', - match: { ports: 80, domains: 'acme.example.com' }, + name: 'port-9081-route', + match: { ports: 9081, domains: 'acme.example.com' }, action: { type: 'forward', target: { host: 'localhost', port: 8080 } } - }] + }], + acme: { + port: 9081 // Use high port for ACME challenges + } }); await proxy.start(); @@ -109,7 +115,7 @@ tap.test('should renew certificates', async () => { const proxy = new SmartProxy({ routes: [{ name: 'renew-route', - match: { ports: 443, domains: 'renew.example.com' }, + match: { ports: 9446, domains: 'renew.example.com' }, action: { type: 'forward', target: { host: 'localhost', port: 8080 }, @@ -123,7 +129,10 @@ tap.test('should renew certificates', async () => { } } } - }] + }], + acme: { + port: 9082 // Use high port for ACME challenges + } }); await proxy.start(); diff --git a/test/test.certificate-simple.ts b/test/test.certificate-simple.ts index 0b584a3..e9d832e 100644 --- a/test/test.certificate-simple.ts +++ b/test/test.certificate-simple.ts @@ -25,41 +25,36 @@ tap.test('should create SmartProxy with certificate routes', async () => { expect(proxy.settings.routes.length).toEqual(1); }); -tap.test('should handle static route type', async () => { - // Create a test route with static handler - const testResponse = { - status: 200, - headers: { 'Content-Type': 'text/plain' }, - body: 'Hello from static route' - }; - +tap.test('should handle socket handler route type', async () => { + // Create a test route with socket handler const proxy = new SmartProxy({ routes: [{ - name: 'static-test', + name: 'socket-handler-test', match: { ports: 8080, path: '/test' }, action: { - type: 'static', - handler: async () => testResponse + type: 'socket-handler', + socketHandler: (socket, context) => { + socket.once('data', (data) => { + const response = [ + 'HTTP/1.1 200 OK', + 'Content-Type: text/plain', + 'Content-Length: 23', + 'Connection: close', + '', + 'Hello from socket handler' + ].join('\r\n'); + + socket.write(response); + socket.end(); + }); + } } }] }); const route = proxy.settings.routes[0]; - expect(route.action.type).toEqual('static'); - expect(route.action.handler).toBeDefined(); - - // Test the handler - const result = await route.action.handler!({ - port: 8080, - path: '/test', - clientIp: '127.0.0.1', - serverIp: '127.0.0.1', - isTls: false, - timestamp: Date.now(), - connectionId: 'test-123' - }); - - expect(result).toEqual(testResponse); + expect(route.action.type).toEqual('socket-handler'); + expect(route.action.socketHandler).toBeDefined(); }); tap.start(); \ No newline at end of file diff --git a/test/test.forwarding.examples.ts b/test/test.forwarding.examples.ts index 06b5afc..336fe59 100644 --- a/test/test.forwarding.examples.ts +++ b/test/test.forwarding.examples.ts @@ -9,7 +9,6 @@ import { createHttpToHttpsRedirect, createCompleteHttpsServer, createLoadBalancerRoute, - createStaticFileRoute, createApiRoute, createWebSocketRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; @@ -73,7 +72,7 @@ tap.test('Route-based configuration examples', async (tools) => { expect(terminateToHttpRoute).toBeTruthy(); expect(terminateToHttpRoute.action.tls?.mode).toEqual('terminate'); - expect(httpToHttpsRedirect.action.type).toEqual('redirect'); + expect(httpToHttpsRedirect.action.type).toEqual('socket-handler'); // Example 4: Load Balancer with HTTPS const loadBalancerRoute = createLoadBalancerRoute( @@ -124,21 +123,9 @@ tap.test('Route-based configuration examples', async (tools) => { 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('redirect'); + expect(httpsServerRoutes[1].action.type).toEqual('socket-handler'); - // Example 7: Static File Server - const staticFileRoute = createStaticFileRoute( - 'static.example.com', - '/var/www/static', - { - serveOnHttps: true, - certificate: 'auto', - name: 'Static File Server' - } - ); - - expect(staticFileRoute.action.type).toEqual('static'); - expect(staticFileRoute.action.static?.root).toEqual('/var/www/static'); + // Example 7: Static File Server - removed (use nginx/apache behind proxy) // Example 8: WebSocket Route const webSocketRoute = createWebSocketRoute( @@ -163,7 +150,6 @@ tap.test('Route-based configuration examples', async (tools) => { loadBalancerRoute, apiRoute, ...httpsServerRoutes, - staticFileRoute, webSocketRoute ]; @@ -175,7 +161,7 @@ tap.test('Route-based configuration examples', async (tools) => { // Just verify that all routes are configured correctly console.log(`Created ${allRoutes.length} example routes`); - expect(allRoutes.length).toEqual(10); + expect(allRoutes.length).toEqual(9); // One less without static file route }); export default tap.start(); \ No newline at end of file diff --git a/test/test.forwarding.ts b/test/test.forwarding.ts index 6ef860a..d5d89bd 100644 --- a/test/test.forwarding.ts +++ b/test/test.forwarding.ts @@ -72,9 +72,10 @@ tap.test('Route Helpers - Create complete HTTPS server with redirect', async () expect(routes.length).toEqual(2); - // Check HTTP to HTTPS redirect - find route by action type - const redirectRoute = routes.find(r => r.action.type === 'redirect'); - expect(redirectRoute.action.type).toEqual('redirect'); + // 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 diff --git a/test/test.route-config.ts b/test/test.route-config.ts index 690c6cf..6faee68 100644 --- a/test/test.route-config.ts +++ b/test/test.route-config.ts @@ -35,7 +35,6 @@ import { createHttpToHttpsRedirect, createCompleteHttpsServer, createLoadBalancerRoute, - createStaticFileRoute, createApiRoute, createWebSocketRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; @@ -87,9 +86,8 @@ tap.test('Routes: Should create HTTP to HTTPS redirect', async () => { // Validate the route configuration expect(redirectRoute.match.ports).toEqual(80); expect(redirectRoute.match.domains).toEqual('example.com'); - expect(redirectRoute.action.type).toEqual('redirect'); - expect(redirectRoute.action.redirect?.to).toEqual('https://{domain}:443{path}'); - expect(redirectRoute.action.redirect?.status).toEqual(301); + expect(redirectRoute.action.type).toEqual('socket-handler'); + expect(redirectRoute.action.socketHandler).toBeDefined(); }); tap.test('Routes: Should create complete HTTPS server with redirects', async () => { @@ -111,8 +109,8 @@ tap.test('Routes: Should create complete HTTPS server with redirects', async () // Validate HTTP redirect route const redirectRoute = routes[1]; expect(redirectRoute.match.ports).toEqual(80); - expect(redirectRoute.action.type).toEqual('redirect'); - expect(redirectRoute.action.redirect?.to).toEqual('https://{domain}:443{path}'); + expect(redirectRoute.action.type).toEqual('socket-handler'); + expect(redirectRoute.action.socketHandler).toBeDefined(); }); tap.test('Routes: Should create load balancer route', async () => { @@ -190,24 +188,7 @@ tap.test('Routes: Should create WebSocket route', async () => { } }); -tap.test('Routes: Should create static file route', async () => { - // Create a static file route - const staticRoute = createStaticFileRoute('static.example.com', '/var/www/html', { - serveOnHttps: true, - certificate: 'auto', - indexFiles: ['index.html', 'index.htm', 'default.html'], - name: 'Static File Route' - }); - - // Validate the route configuration - expect(staticRoute.match.domains).toEqual('static.example.com'); - expect(staticRoute.action.type).toEqual('static'); - expect(staticRoute.action.static?.root).toEqual('/var/www/html'); - expect(staticRoute.action.static?.index).toBeInstanceOf(Array); - expect(staticRoute.action.static?.index).toInclude('index.html'); - expect(staticRoute.action.static?.index).toInclude('default.html'); - expect(staticRoute.action.tls?.mode).toEqual('terminate'); -}); +// 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 @@ -515,11 +496,6 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { certificate: 'auto' }), - // Static assets - createStaticFileRoute('static.example.com', '/var/www/assets', { - serveOnHttps: true, - certificate: 'auto' - }), // Legacy system with passthrough createHttpsPassthroughRoute('legacy.example.com', { host: 'legacy-server', port: 443 }) @@ -540,11 +516,11 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(webServerMatch.action.target.host).toEqual('web-server'); } - // Web server (HTTP redirect) + // 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('redirect'); + expect(webRedirectMatch.action.type).toEqual('socket-handler'); } // API server @@ -572,16 +548,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(wsMatch.action.websocket?.enabled).toBeTrue(); } - // Static assets - const staticMatch = findBestMatchingRoute(routes, { - domain: 'static.example.com', - port: 443 - }); - expect(staticMatch).not.toBeUndefined(); - if (staticMatch) { - expect(staticMatch.action.type).toEqual('static'); - expect(staticMatch.action.static.root).toEqual('/var/www/assets'); - } + // Static assets route was removed - static file serving should be handled externally // Legacy system const legacyMatch = findBestMatchingRoute(routes, { diff --git a/test/test.route-utils.ts b/test/test.route-utils.ts index 21c7c68..43cd82a 100644 --- a/test/test.route-utils.ts +++ b/test/test.route-utils.ts @@ -6,7 +6,6 @@ import { // Route helpers createHttpRoute, createHttpsTerminateRoute, - createStaticFileRoute, createApiRoute, createWebSocketRoute, createHttpToHttpsRedirect, @@ -43,7 +42,6 @@ import { import { // Route patterns createApiGatewayRoute, - createStaticFileServerRoute, createWebSocketRoute as createWebSocketPattern, createLoadBalancerRoute as createLbPattern, addRateLimiting, @@ -145,28 +143,16 @@ tap.test('Route Validation - validateRouteAction', async () => { expect(validForwardResult.valid).toBeTrue(); expect(validForwardResult.errors.length).toEqual(0); - // Valid redirect action - const validRedirectAction: IRouteAction = { - type: 'redirect', - redirect: { - to: 'https://example.com', - status: 301 + // Valid socket-handler action + const validSocketAction: IRouteAction = { + type: 'socket-handler', + socketHandler: (socket, context) => { + socket.end(); } }; - const validRedirectResult = validateRouteAction(validRedirectAction); - expect(validRedirectResult.valid).toBeTrue(); - expect(validRedirectResult.errors.length).toEqual(0); - - // Valid static action - const validStaticAction: IRouteAction = { - type: 'static', - static: { - root: '/var/www/html' - } - }; - const validStaticResult = validateRouteAction(validStaticAction); - expect(validStaticResult.valid).toBeTrue(); - expect(validStaticResult.errors.length).toEqual(0); + const validSocketResult = validateRouteAction(validSocketAction); + expect(validSocketResult.valid).toBeTrue(); + expect(validSocketResult.errors.length).toEqual(0); // Invalid action (missing target) const invalidAction: IRouteAction = { @@ -177,24 +163,14 @@ tap.test('Route Validation - validateRouteAction', async () => { expect(invalidResult.errors.length).toBeGreaterThan(0); expect(invalidResult.errors[0]).toInclude('Target is required'); - // Invalid action (missing redirect configuration) - const invalidRedirectAction: IRouteAction = { - type: 'redirect' + // Invalid action (missing socket handler) + const invalidSocketAction: IRouteAction = { + type: 'socket-handler' }; - const invalidRedirectResult = validateRouteAction(invalidRedirectAction); - expect(invalidRedirectResult.valid).toBeFalse(); - expect(invalidRedirectResult.errors.length).toBeGreaterThan(0); - expect(invalidRedirectResult.errors[0]).toInclude('Redirect configuration is required'); - - // Invalid action (missing static root) - const invalidStaticAction: IRouteAction = { - type: 'static', - static: {} as any // Testing invalid static config without required 'root' property - }; - const invalidStaticResult = validateRouteAction(invalidStaticAction); - expect(invalidStaticResult.valid).toBeFalse(); - expect(invalidStaticResult.errors.length).toBeGreaterThan(0); - expect(invalidStaticResult.errors[0]).toInclude('Static file root directory is required'); + const invalidSocketResult = validateRouteAction(invalidSocketAction); + expect(invalidSocketResult.valid).toBeFalse(); + expect(invalidSocketResult.errors.length).toBeGreaterThan(0); + expect(invalidSocketResult.errors[0]).toInclude('Socket handler function is required'); }); tap.test('Route Validation - validateRouteConfig', async () => { @@ -253,26 +229,25 @@ tap.test('Route Validation - hasRequiredPropertiesForAction', async () => { const forwardRoute = createHttpRoute('example.com', { host: 'localhost', port: 3000 }); expect(hasRequiredPropertiesForAction(forwardRoute, 'forward')).toBeTrue(); - // Redirect action + // Socket handler action (redirect functionality) const redirectRoute = createHttpToHttpsRedirect('example.com'); - expect(hasRequiredPropertiesForAction(redirectRoute, 'redirect')).toBeTrue(); + expect(hasRequiredPropertiesForAction(redirectRoute, 'socket-handler')).toBeTrue(); - // Static action - const staticRoute = createStaticFileRoute('example.com', '/var/www/html'); - expect(hasRequiredPropertiesForAction(staticRoute, 'static')).toBeTrue(); - - // Block action - const blockRoute: IRouteConfig = { + // Socket handler action + const socketRoute: IRouteConfig = { match: { - domains: 'blocked.example.com', + domains: 'socket.example.com', ports: 80 }, action: { - type: 'block' + type: 'socket-handler', + socketHandler: (socket, context) => { + socket.end(); + } }, - name: 'Block Route' + name: 'Socket Handler Route' }; - expect(hasRequiredPropertiesForAction(blockRoute, 'block')).toBeTrue(); + expect(hasRequiredPropertiesForAction(socketRoute, 'socket-handler')).toBeTrue(); // Missing required properties const invalidForwardRoute: IRouteConfig = { @@ -345,20 +320,22 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { expect(actionMergedRoute.action.target.host).toEqual('new-host.local'); expect(actionMergedRoute.action.target.port).toEqual(5000); - // Test replacing action with different type + // Test replacing action with socket handler const typeChangeOverride: Partial = { action: { - type: 'redirect', - redirect: { - to: 'https://example.com', - status: 301 + type: 'socket-handler', + socketHandler: (socket, context) => { + socket.write('HTTP/1.1 301 Moved Permanently\r\n'); + socket.write('Location: https://example.com\r\n'); + socket.write('\r\n'); + socket.end(); } } }; const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride); - expect(typeChangedRoute.action.type).toEqual('redirect'); - expect(typeChangedRoute.action.redirect.to).toEqual('https://example.com'); + expect(typeChangedRoute.action.type).toEqual('socket-handler'); + expect(typeChangedRoute.action.socketHandler).toBeDefined(); expect(typeChangedRoute.action.target).toBeUndefined(); }); @@ -705,9 +682,8 @@ tap.test('Route Helpers - createHttpToHttpsRedirect', async () => { expect(route.match.domains).toEqual('example.com'); expect(route.match.ports).toEqual(80); - expect(route.action.type).toEqual('redirect'); - expect(route.action.redirect.to).toEqual('https://{domain}:443{path}'); - expect(route.action.redirect.status).toEqual(301); + expect(route.action.type).toEqual('socket-handler'); + expect(route.action.socketHandler).toBeDefined(); const validationResult = validateRouteConfig(route); expect(validationResult.valid).toBeTrue(); @@ -741,7 +717,7 @@ tap.test('Route Helpers - createCompleteHttpsServer', async () => { // HTTP redirect route expect(routes[1].match.domains).toEqual('example.com'); expect(routes[1].match.ports).toEqual(80); - expect(routes[1].action.type).toEqual('redirect'); + expect(routes[1].action.type).toEqual('socket-handler'); const validation1 = validateRouteConfig(routes[0]); const validation2 = validateRouteConfig(routes[1]); @@ -749,24 +725,8 @@ tap.test('Route Helpers - createCompleteHttpsServer', async () => { expect(validation2.valid).toBeTrue(); }); -tap.test('Route Helpers - createStaticFileRoute', async () => { - const route = createStaticFileRoute('example.com', '/var/www/html', { - serveOnHttps: true, - certificate: 'auto', - indexFiles: ['index.html', 'index.htm', 'default.html'] - }); - - expect(route.match.domains).toEqual('example.com'); - expect(route.match.ports).toEqual(443); - expect(route.action.type).toEqual('static'); - expect(route.action.static.root).toEqual('/var/www/html'); - expect(route.action.static.index).toInclude('index.html'); - expect(route.action.static.index).toInclude('default.html'); - expect(route.action.tls.mode).toEqual('terminate'); - - const validationResult = validateRouteConfig(route); - expect(validationResult.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 }, { @@ -874,34 +834,8 @@ tap.test('Route Patterns - createApiGatewayRoute', async () => { expect(result.valid).toBeTrue(); }); -tap.test('Route Patterns - createStaticFileServerRoute', async () => { - // Create static file server route - const staticRoute = createStaticFileServerRoute( - 'static.example.com', - '/var/www/html', - { - useTls: true, - cacheControl: 'public, max-age=7200' - } - ); - - // Validate route configuration - expect(staticRoute.match.domains).toEqual('static.example.com'); - expect(staticRoute.action.type).toEqual('static'); - - // Check static configuration - if (staticRoute.action.static) { - expect(staticRoute.action.static.root).toEqual('/var/www/html'); - - // Check cache control headers if they exist - if (staticRoute.action.static.headers) { - expect(staticRoute.action.static.headers['Cache-Control']).toEqual('public, max-age=7200'); - } - } - - const result = validateRouteConfig(staticRoute); - 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 diff --git a/ts/proxies/smart-proxy/utils/route-utils.ts b/ts/proxies/smart-proxy/utils/route-utils.ts index 25a6a4c..868e434 100644 --- a/ts/proxies/smart-proxy/utils/route-utils.ts +++ b/ts/proxies/smart-proxy/utils/route-utils.ts @@ -53,7 +53,15 @@ export function mergeRouteConfigs( if (overrideRoute.action) { // If action types are different, replace the entire action if (overrideRoute.action.type && overrideRoute.action.type !== mergedRoute.action.type) { - mergedRoute.action = JSON.parse(JSON.stringify(overrideRoute.action)); + // Handle socket handler specially since it's a function + if (overrideRoute.action.type === 'socket-handler' && overrideRoute.action.socketHandler) { + mergedRoute.action = { + type: 'socket-handler', + socketHandler: overrideRoute.action.socketHandler + }; + } else { + mergedRoute.action = JSON.parse(JSON.stringify(overrideRoute.action)); + } } else { // Otherwise merge the action properties mergedRoute.action = { ...mergedRoute.action }; @@ -74,7 +82,10 @@ export function mergeRouteConfigs( }; } - // No special merging needed for socket handlers - they are functions + // Handle socket handler update + if (overrideRoute.action.socketHandler) { + mergedRoute.action.socketHandler = overrideRoute.action.socketHandler; + } } } diff --git a/ts/proxies/smart-proxy/utils/route-validators.ts b/ts/proxies/smart-proxy/utils/route-validators.ts index 87312d4..da3417e 100644 --- a/ts/proxies/smart-proxy/utils/route-validators.ts +++ b/ts/proxies/smart-proxy/utils/route-validators.ts @@ -98,7 +98,7 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err // Validate action type if (!action.type) { errors.push('Action type is required'); - } else if (!['forward', 'redirect', 'static', 'block'].includes(action.type)) { + } else if (!['forward', 'socket-handler'].includes(action.type)) { errors.push(`Invalid action type: ${action.type}`); }