diff --git a/readme.plan2.md b/readme.plan2.md index c42b438..74feb1d 100644 --- a/readme.plan2.md +++ b/readme.plan2.md @@ -478,16 +478,16 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => { ## Detailed Implementation Tasks ### Step 1: Update TSocketHandler Type (15 minutes) -- [ ] Open `ts/proxies/smart-proxy/models/route-types.ts` -- [ ] Find line 14: `export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise;` -- [ ] Import IRouteContext at top of file: `import type { IRouteContext } from '../../../core/models/route-context.js';` -- [ ] Update TSocketHandler to: `export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise;` -- [ ] Save file +- [x] Open `ts/proxies/smart-proxy/models/route-types.ts` +- [x] Find line 14: `export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise;` +- [x] Import IRouteContext at top of file: `import type { IRouteContext } from '../../../core/models/route-context.js';` +- [x] Update TSocketHandler to: `export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise;` +- [x] Save file ### Step 2: Update Socket Handler Implementation (30 minutes) -- [ ] Open `ts/proxies/smart-proxy/route-connection-handler.ts` -- [ ] Find `handleSocketHandlerAction` method (around line 790) -- [ ] Add route context creation after line 809: +- [x] Open `ts/proxies/smart-proxy/route-connection-handler.ts` +- [x] Find `handleSocketHandlerAction` method (around line 790) +- [x] Add route context creation after line 809: ```typescript // Create route context for the handler const routeContext = this.createRouteContext({ @@ -502,19 +502,19 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => { routeId: route.id, }); ``` -- [ ] Update line 812 from `const result = route.action.socketHandler(socket);` -- [ ] To: `const result = route.action.socketHandler(socket, routeContext);` -- [ ] Save file +- [x] Update line 812 from `const result = route.action.socketHandler(socket);` +- [x] To: `const result = route.action.socketHandler(socket, routeContext);` +- [x] Save file ### Step 3: Update Existing Socket Handlers in route-helpers.ts (20 minutes) -- [ ] Open `ts/proxies/smart-proxy/utils/route-helpers.ts` -- [ ] Update `echo` handler (line 856): +- [x] Open `ts/proxies/smart-proxy/utils/route-helpers.ts` +- [x] Update `echo` handler (line 856): - From: `echo: (socket: plugins.net.Socket) => {` - To: `echo: (socket: plugins.net.Socket, context: IRouteContext) => {` -- [ ] Update `proxy` handler (line 864): +- [x] Update `proxy` handler (line 864): - From: `proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket) => {` - To: `proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => {` -- [ ] Update `lineProtocol` handler (line 879): +- [x] Update `lineProtocol` handler (line 879): - From: `lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket) => {` - To: `lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => {` - [ ] Update `httpResponse` handler (line 896): @@ -635,11 +635,11 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => { }); } ``` -- [ ] Save file +- [x] Save file ### Step 9: Update Helper Functions (20 minutes) -- [ ] Still in `route-helpers.ts` -- [ ] Update `createHttpToHttpsRedirect` function (around line 109): +- [x] Still in `route-helpers.ts` +- [x] Update `createHttpToHttpsRedirect` function (around line 109): - Change the action to use socket handler: ```typescript action: { @@ -647,8 +647,8 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => { socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301) } ``` -- [ ] Delete entire `createStaticFileRoute` function (lines 277-322) -- [ ] Save file +- [x] Delete entire `createStaticFileRoute` function (lines 277-322) +- [x] Save file ### Step 10: Update Test Files (1.5 hours) #### 10.1 Update Socket Handler Tests diff --git a/test/test.route-redirects.ts b/test/test.route-redirects.ts deleted file mode 100644 index 10d3d8d..0000000 --- a/test/test.route-redirects.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; -import { createHttpToHttpsRedirect } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; -import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; - -// Test that HTTP to HTTPS redirects work correctly -tap.test('should handle HTTP to HTTPS redirects', async (tools) => { - // Create a simple HTTP to HTTPS redirect route - const redirectRoute = createHttpToHttpsRedirect( - 'example.com', - 443, - { - name: 'HTTP to HTTPS Redirect Test' - } - ); - - // Verify the route is configured correctly - expect(redirectRoute.action.type).toEqual('redirect'); - expect(redirectRoute.action.redirect).toBeTruthy(); - expect(redirectRoute.action.redirect?.to).toEqual('https://{domain}:443{path}'); - expect(redirectRoute.action.redirect?.status).toEqual(301); - expect(redirectRoute.match.ports).toEqual(80); - expect(redirectRoute.match.domains).toEqual('example.com'); -}); - -tap.test('should handle custom redirect configurations', async (tools) => { - // Create a custom redirect route - const customRedirect: IRouteConfig = { - name: 'custom-redirect', - match: { - ports: [8080], - domains: ['old.example.com'] - }, - action: { - type: 'redirect', - redirect: { - to: 'https://new.example.com{path}', - status: 302 - } - } - }; - - // Verify the route structure - expect(customRedirect.action.redirect?.to).toEqual('https://new.example.com{path}'); - expect(customRedirect.action.redirect?.status).toEqual(302); -}); - -tap.test('should support multiple redirect scenarios', async (tools) => { - const routes: IRouteConfig[] = [ - // HTTP to HTTPS redirect - createHttpToHttpsRedirect(['example.com', 'www.example.com']), - - // Custom redirect with different port - { - name: 'custom-port-redirect', - match: { - ports: 8080, - domains: 'api.example.com' - }, - action: { - type: 'redirect', - redirect: { - to: 'https://{domain}:8443{path}', - status: 308 - } - } - }, - - // Redirect to different domain entirely - { - name: 'domain-redirect', - match: { - ports: 80, - domains: 'old-domain.com' - }, - action: { - type: 'redirect', - redirect: { - to: 'https://new-domain.com{path}', - status: 301 - } - } - } - ]; - - // Create SmartProxy with redirect routes - const proxy = new SmartProxy({ - routes - }); - - // Verify all routes are redirect type - routes.forEach(route => { - expect(route.action.type).toEqual('redirect'); - expect(route.action.redirect).toBeTruthy(); - }); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.socket-handler-race.ts b/test/test.socket-handler-race.ts index 38b1d28..d88257b 100644 --- a/test/test.socket-handler-race.ts +++ b/test/test.socket-handler-race.ts @@ -9,7 +9,7 @@ tap.test('should handle async handler that sets up listeners after delay', async match: { ports: 7777 }, action: { type: 'socket-handler', - socketHandler: async (socket) => { + socketHandler: async (socket, context) => { // Simulate async work BEFORE setting up listeners await new Promise(resolve => setTimeout(resolve, 50)); diff --git a/test/test.socket-handler.simple.ts b/test/test.socket-handler.simple.ts index 6d7309a..2f6afa2 100644 --- a/test/test.socket-handler.simple.ts +++ b/test/test.socket-handler.simple.ts @@ -12,7 +12,7 @@ tap.test('simple socket handler test', async () => { }, action: { type: 'socket-handler', - socketHandler: (socket) => { + socketHandler: (socket, context) => { console.log('Handler called!'); socket.write('HELLO\n'); socket.end(); diff --git a/test/test.socket-handler.ts b/test/test.socket-handler.ts index f3b44f6..4041d25 100644 --- a/test/test.socket-handler.ts +++ b/test/test.socket-handler.ts @@ -15,7 +15,7 @@ tap.test('setup socket handler test', async () => { }, action: { type: 'socket-handler', - socketHandler: (socket) => { + socketHandler: (socket, context) => { console.log('Socket handler called'); // Simple echo server socket.write('ECHO SERVER\n'); @@ -81,7 +81,7 @@ tap.test('should handle async socket handler', async () => { match: { ports: 9999 }, action: { type: 'socket-handler', - socketHandler: async (socket) => { + socketHandler: async (socket, context) => { // Set up data handler first socket.on('data', async (data) => { console.log('Async handler received:', data.toString()); @@ -134,7 +134,7 @@ tap.test('should handle errors in socket handler', async () => { match: { ports: 9999 }, action: { type: 'socket-handler', - socketHandler: (socket) => { + socketHandler: (socket, context) => { throw new Error('Handler error'); } } diff --git a/ts/proxies/http-proxy/handlers/index.ts b/ts/proxies/http-proxy/handlers/index.ts index 6ca5574..586da59 100644 --- a/ts/proxies/http-proxy/handlers/index.ts +++ b/ts/proxies/http-proxy/handlers/index.ts @@ -2,5 +2,4 @@ * HTTP handlers for various route types */ -export { RedirectHandler } from './redirect-handler.js'; -export { StaticHandler } from './static-handler.js'; \ No newline at end of file +// Empty - all handlers have been removed \ No newline at end of file diff --git a/ts/proxies/http-proxy/handlers/redirect-handler.ts b/ts/proxies/http-proxy/handlers/redirect-handler.ts deleted file mode 100644 index 22f4472..0000000 --- a/ts/proxies/http-proxy/handlers/redirect-handler.ts +++ /dev/null @@ -1,105 +0,0 @@ -import * as plugins from '../../../plugins.js'; -import type { IRouteConfig } from '../../smart-proxy/models/route-types.js'; -import type { IConnectionRecord } from '../../smart-proxy/models/interfaces.js'; -import type { ILogger } from '../models/types.js'; -import { createLogger } from '../models/types.js'; -import { HttpStatus, getStatusText } from '../models/http-types.js'; - -export interface IRedirectHandlerContext { - connectionId: string; - connectionManager: any; // Avoid circular deps - settings: any; - logger?: ILogger; -} - -/** - * Handles HTTP redirect routes - */ -export class RedirectHandler { - /** - * Handle redirect routes - */ - public static async handleRedirect( - socket: plugins.net.Socket, - route: IRouteConfig, - context: IRedirectHandlerContext - ): Promise { - const { connectionId, connectionManager, settings } = context; - const logger = context.logger || createLogger(settings.logLevel || 'info'); - const action = route.action; - - // We should have a redirect configuration - if (!action.redirect) { - logger.error(`[${connectionId}] Redirect action missing redirect configuration`); - socket.end(); - connectionManager.cleanupConnection({ id: connectionId }, 'missing_redirect'); - return; - } - - // For TLS connections, we can't do redirects at the TCP level - // This check should be done before calling this handler - - // Wait for the first HTTP request to perform the redirect - const dataListeners: ((chunk: Buffer) => void)[] = []; - - const httpDataHandler = (chunk: Buffer) => { - // Remove all data listeners to avoid duplicated processing - for (const listener of dataListeners) { - socket.removeListener('data', listener); - } - - // Parse HTTP request to get path - try { - const headersEnd = chunk.indexOf('\r\n\r\n'); - if (headersEnd === -1) { - // Not a complete HTTP request, need more data - socket.once('data', httpDataHandler); - dataListeners.push(httpDataHandler); - return; - } - - const httpHeaders = chunk.slice(0, headersEnd).toString(); - const requestLine = httpHeaders.split('\r\n')[0]; - const [method, path] = requestLine.split(' '); - - // Extract Host header - const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i); - const host = hostMatch ? hostMatch[1].trim() : ''; - - // Process the redirect URL with template variables - let redirectUrl = action.redirect.to; - redirectUrl = redirectUrl.replace(/\{domain\}/g, host); - redirectUrl = redirectUrl.replace(/\{path\}/g, path || ''); - redirectUrl = redirectUrl.replace(/\{port\}/g, socket.localPort?.toString() || '80'); - - // Prepare the HTTP redirect response - const redirectResponse = [ - `HTTP/1.1 ${action.redirect.status} Moved`, - `Location: ${redirectUrl}`, - 'Connection: close', - 'Content-Length: 0', - '', - '', - ].join('\r\n'); - - if (settings.enableDetailedLogging) { - logger.info( - `[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}` - ); - } - - // Send the redirect response - socket.end(redirectResponse); - connectionManager.initiateCleanupOnce({ id: connectionId }, 'redirect_complete'); - } catch (err) { - logger.error(`[${connectionId}] Error processing HTTP redirect: ${err}`); - socket.end(); - connectionManager.initiateCleanupOnce({ id: connectionId }, 'redirect_error'); - } - }; - - // Setup the HTTP data handler - socket.once('data', httpDataHandler); - dataListeners.push(httpDataHandler); - } -} \ No newline at end of file diff --git a/ts/proxies/http-proxy/handlers/static-handler.ts b/ts/proxies/http-proxy/handlers/static-handler.ts deleted file mode 100644 index d3b2011..0000000 --- a/ts/proxies/http-proxy/handlers/static-handler.ts +++ /dev/null @@ -1,261 +0,0 @@ -import * as plugins from '../../../plugins.js'; -import type { IRouteConfig } from '../../smart-proxy/models/route-types.js'; -import type { IConnectionRecord } from '../../smart-proxy/models/interfaces.js'; -import type { ILogger } from '../models/types.js'; -import { createLogger } from '../models/types.js'; -import type { IRouteContext } from '../../../core/models/route-context.js'; -import { HttpStatus, getStatusText } from '../models/http-types.js'; - -export interface IStaticHandlerContext { - connectionId: string; - connectionManager: any; // Avoid circular deps - settings: any; - logger?: ILogger; -} - -/** - * Handles static routes including ACME challenges - */ -export class StaticHandler { - /** - * Handle static routes - */ - public static async handleStatic( - socket: plugins.net.Socket, - route: IRouteConfig, - context: IStaticHandlerContext, - record: IConnectionRecord, - initialChunk?: Buffer - ): Promise { - const { connectionId, connectionManager, settings } = context; - const logger = context.logger || createLogger(settings.logLevel || 'info'); - - if (!route.action.handler) { - logger.error(`[${connectionId}] Static route '${route.name}' has no handler`); - socket.end(); - connectionManager.cleanupConnection(record, 'no_handler'); - return; - } - - let buffer = Buffer.alloc(0); - let processingData = false; - - const handleHttpData = async (chunk: Buffer) => { - // Accumulate the data - buffer = Buffer.concat([buffer, chunk]); - - // Prevent concurrent processing of the same buffer - if (processingData) return; - processingData = true; - - try { - // Process data until we have a complete request or need more data - await processBuffer(); - } finally { - processingData = false; - } - }; - - const processBuffer = async () => { - // Look for end of HTTP headers - const headerEndIndex = buffer.indexOf('\r\n\r\n'); - if (headerEndIndex === -1) { - // Need more data - if (buffer.length > 8192) { - // Prevent excessive buffering - logger.error(`[${connectionId}] HTTP headers too large`); - socket.end(); - connectionManager.cleanupConnection(record, 'headers_too_large'); - } - return; // Wait for more data to arrive - } - - // Parse the HTTP request - const headerBuffer = buffer.slice(0, headerEndIndex); - const headers = headerBuffer.toString(); - const lines = headers.split('\r\n'); - - if (lines.length === 0) { - logger.error(`[${connectionId}] Invalid HTTP request`); - socket.end(); - connectionManager.cleanupConnection(record, 'invalid_request'); - return; - } - - // Parse request line - const requestLine = lines[0]; - const requestParts = requestLine.split(' '); - if (requestParts.length < 3) { - logger.error(`[${connectionId}] Invalid HTTP request line`); - socket.end(); - connectionManager.cleanupConnection(record, 'invalid_request_line'); - return; - } - - const [method, path, httpVersion] = requestParts; - - // Parse headers - const headersMap: Record = {}; - for (let i = 1; i < lines.length; i++) { - const colonIndex = lines[i].indexOf(':'); - if (colonIndex > 0) { - const key = lines[i].slice(0, colonIndex).trim().toLowerCase(); - const value = lines[i].slice(colonIndex + 1).trim(); - headersMap[key] = value; - } - } - - // Check for Content-Length to handle request body - const requestBodyLength = parseInt(headersMap['content-length'] || '0', 10); - const bodyStartIndex = headerEndIndex + 4; // Skip the \r\n\r\n - - // If there's a body, ensure we have the full body - if (requestBodyLength > 0) { - const totalExpectedLength = bodyStartIndex + requestBodyLength; - - // If we don't have the complete body yet, wait for more data - if (buffer.length < totalExpectedLength) { - // Implement a reasonable body size limit to prevent memory issues - if (requestBodyLength > 1024 * 1024) { - // 1MB limit - logger.error(`[${connectionId}] Request body too large`); - socket.end(); - connectionManager.cleanupConnection(record, 'body_too_large'); - return; - } - return; // Wait for more data - } - } - - // Extract query string if present - let pathname = path; - let query: string | undefined; - const queryIndex = path.indexOf('?'); - if (queryIndex !== -1) { - pathname = path.slice(0, queryIndex); - query = path.slice(queryIndex + 1); - } - - try { - // Get request body if present - let requestBody: Buffer | undefined; - if (requestBodyLength > 0) { - requestBody = buffer.slice(bodyStartIndex, bodyStartIndex + requestBodyLength); - } - - // Pause socket to prevent data loss during async processing - socket.pause(); - - // Remove the data listener since we're handling the request - socket.removeListener('data', handleHttpData); - - // Build route context with parsed HTTP information - const context: IRouteContext = { - port: record.localPort, - domain: record.lockedDomain || headersMap['host']?.split(':')[0], - clientIp: record.remoteIP, - serverIp: socket.localAddress!, - path: pathname, - query: query, - headers: headersMap, - isTls: record.isTLS, - tlsVersion: record.tlsVersion, - routeName: route.name, - routeId: route.id, - timestamp: Date.now(), - connectionId, - }; - - // Since IRouteContext doesn't have a body property, - // we need an alternative approach to handle the body - let response; - - if (requestBody) { - if (settings.enableDetailedLogging) { - logger.info( - `[${connectionId}] Processing request with body (${requestBody.length} bytes)` - ); - } - - // Pass the body as an additional parameter by extending the context object - // This is not type-safe, but it allows handlers that expect a body to work - const extendedContext = { - ...context, - // Provide both raw buffer and string representation - requestBody: requestBody, - requestBodyText: requestBody.toString(), - method: method, - }; - - // Call the handler with the extended context - // The handler needs to know to look for the non-standard properties - response = await route.action.handler(extendedContext as any); - } else { - // Call the handler with the standard context - const extendedContext = { - ...context, - method: method, - }; - response = await route.action.handler(extendedContext as any); - } - - // Prepare the HTTP response - const responseHeaders = response.headers || {}; - const contentLength = Buffer.byteLength(response.body || ''); - responseHeaders['Content-Length'] = contentLength.toString(); - - if (!responseHeaders['Content-Type']) { - responseHeaders['Content-Type'] = 'text/plain'; - } - - // Build the response - let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`; - for (const [key, value] of Object.entries(responseHeaders)) { - httpResponse += `${key}: ${value}\r\n`; - } - httpResponse += '\r\n'; - - // Send response - socket.write(httpResponse); - if (response.body) { - socket.write(response.body); - } - socket.end(); - - connectionManager.cleanupConnection(record, 'completed'); - } catch (error) { - logger.error(`[${connectionId}] Error in static handler: ${error}`); - - // Send error response - const errorResponse = - 'HTTP/1.1 500 Internal Server Error\r\n' + - 'Content-Type: text/plain\r\n' + - 'Content-Length: 21\r\n' + - '\r\n' + - 'Internal Server Error'; - socket.write(errorResponse); - socket.end(); - - connectionManager.cleanupConnection(record, 'handler_error'); - } - }; - - // Process initial chunk if provided - if (initialChunk && initialChunk.length > 0) { - if (settings.enableDetailedLogging) { - logger.info(`[${connectionId}] Processing initial data chunk (${initialChunk.length} bytes)`); - } - // Process the initial chunk immediately - handleHttpData(initialChunk); - } - - // Listen for additional data - socket.on('data', handleHttpData); - - // Ensure cleanup on socket close - socket.once('close', () => { - socket.removeListener('data', handleHttpData); - }); - } -} - diff --git a/ts/proxies/smart-proxy/certificate-manager.ts b/ts/proxies/smart-proxy/certificate-manager.ts index 8b4c60d..429bed4 100644 --- a/ts/proxies/smart-proxy/certificate-manager.ts +++ b/ts/proxies/smart-proxy/certificate-manager.ts @@ -693,48 +693,70 @@ export class SmartCertManager { path: '/.well-known/acme-challenge/*' }, action: { - type: 'static', - handler: async (context) => { - // Extract the token from the path - const token = context.path?.split('/').pop(); - if (!token) { - return { status: 404, body: 'Not found' }; - } - - // Create mock request/response objects for SmartAcme - const mockReq = { - url: context.path, - method: 'GET', - headers: context.headers || {} - }; - - let responseData: any = null; - const mockRes = { - statusCode: 200, - setHeader: (name: string, value: string) => {}, - end: (data: any) => { - responseData = data; + type: 'socket-handler', + socketHandler: (socket, context) => { + // Wait for HTTP request data + socket.once('data', async (data) => { + const request = data.toString(); + const lines = request.split('\r\n'); + const [method, path] = lines[0].split(' '); + + // Extract the token from the path + const token = path?.split('/').pop(); + if (!token) { + socket.write('HTTP/1.1 404 Not Found\r\n'); + socket.write('Content-Type: text/plain\r\n'); + socket.write('Content-Length: 9\r\n'); + socket.write('Connection: close\r\n'); + socket.write('\r\n'); + socket.write('Not found'); + socket.end(); + return; } - }; - - // Use SmartAcme's handler - const handled = await new Promise((resolve) => { - http01Handler.handleRequest(mockReq as any, mockRes as any, () => { - resolve(false); - }); - // Give it a moment to process - setTimeout(() => resolve(true), 100); - }); - - if (handled && responseData) { - return { - status: mockRes.statusCode, - headers: { 'Content-Type': 'text/plain' }, - body: responseData + + // Create mock request/response objects for SmartAcme + const mockReq = { + url: path, + method: 'GET', + headers: {} }; - } else { - return { status: 404, body: 'Not found' }; - } + + let responseData: any = null; + const mockRes = { + statusCode: 200, + setHeader: (name: string, value: string) => {}, + end: (data: any) => { + responseData = data; + } + }; + + // Use SmartAcme's handler + const handled = await new Promise((resolve) => { + http01Handler.handleRequest(mockReq as any, mockRes as any, () => { + resolve(false); + }); + // Give it a moment to process + setTimeout(() => resolve(true), 100); + }); + + if (handled && responseData) { + const body = String(responseData); + socket.write(`HTTP/1.1 ${mockRes.statusCode} OK\r\n`); + socket.write('Content-Type: text/plain\r\n'); + socket.write(`Content-Length: ${body.length}\r\n`); + socket.write('Connection: close\r\n'); + socket.write('\r\n'); + socket.write(body); + } else { + socket.write('HTTP/1.1 404 Not Found\r\n'); + socket.write('Content-Type: text/plain\r\n'); + socket.write('Content-Length: 9\r\n'); + socket.write('Connection: close\r\n'); + socket.write('\r\n'); + socket.write('Not found'); + } + socket.end(); + }); } } }; diff --git a/ts/proxies/smart-proxy/models/route-types.ts b/ts/proxies/smart-proxy/models/route-types.ts index 3b4f47f..c117965 100644 --- a/ts/proxies/smart-proxy/models/route-types.ts +++ b/ts/proxies/smart-proxy/models/route-types.ts @@ -2,16 +2,20 @@ import * as plugins from '../../../plugins.js'; // Certificate types removed - use local definition import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; +import type { IRouteContext } from '../../../core/models/route-context.js'; + +// Re-export IRouteContext for convenience +export type { IRouteContext }; /** * Supported action types for route configurations */ -export type TRouteActionType = 'forward' | 'redirect' | 'block' | 'static' | 'socket-handler'; +export type TRouteActionType = 'forward' | 'socket-handler'; /** * Socket handler function type */ -export type TSocketHandler = (socket: plugins.net.Socket) => void | Promise; +export type TSocketHandler = (socket: plugins.net.Socket, context: IRouteContext) => void | Promise; /** * TLS handling modes for route configurations @@ -40,36 +44,6 @@ export interface IRouteMatch { headers?: Record; // Match specific HTTP headers } -/** - * Context provided to port and host mapping functions - */ -export interface IRouteContext { - // Connection information - port: number; // The matched incoming port - domain?: string; // The domain from SNI or Host header - clientIp: string; // The client's IP address - serverIp: string; // The server's IP address - path?: string; // URL path (for HTTP connections) - query?: string; // Query string (for HTTP connections) - headers?: Record; // HTTP headers (for HTTP connections) - method?: string; // HTTP method (for HTTP connections) - - // TLS information - isTls: boolean; // Whether the connection is TLS - tlsVersion?: string; // TLS version if applicable - - // Route information - routeName?: string; // The name of the matched route - routeId?: string; // The ID of the matched route - - // Target information (resolved from dynamic mapping) - targetHost?: string | string[]; // The resolved target host(s) - targetPort?: number; // The resolved target port - - // Additional properties - timestamp: number; // The request timestamp - connectionId: string; // Unique connection identifier -} /** * Target configuration for forwarding @@ -89,15 +63,6 @@ export interface IRouteAcme { renewBeforeDays?: number; // Days before expiry to renew (default: 30) } -/** - * Static route handler response - */ -export interface IStaticResponse { - status: number; - headers?: Record; - body: string | Buffer; -} - /** * TLS configuration for route actions */ @@ -117,14 +82,6 @@ export interface IRouteTls { sessionTimeout?: number; // TLS session timeout in seconds } -/** - * Redirect configuration for route actions - */ -export interface IRouteRedirect { - to: string; // URL or template with {domain}, {port}, etc. - status: 301 | 302 | 307 | 308; -} - /** * Authentication options */ @@ -270,12 +227,6 @@ export interface IRouteAction { // TLS handling tls?: IRouteTls; - // For redirects - redirect?: IRouteRedirect; - - // For static files - static?: IRouteStaticFiles; - // WebSocket support websocket?: IRouteWebSocket; @@ -300,9 +251,6 @@ export interface IRouteAction { // NFTables-specific options nftables?: INfTablesOptions; - // Handler function for static routes - handler?: (context: IRouteContext) => Promise; - // Socket handler function (when type is 'socket-handler') socketHandler?: TSocketHandler; } diff --git a/ts/proxies/smart-proxy/route-connection-handler.ts b/ts/proxies/smart-proxy/route-connection-handler.ts index c7ca306..12dce03 100644 --- a/ts/proxies/smart-proxy/route-connection-handler.ts +++ b/ts/proxies/smart-proxy/route-connection-handler.ts @@ -10,7 +10,6 @@ import { HttpProxyBridge } from './http-proxy-bridge.js'; import { TimeoutManager } from './timeout-manager.js'; import { RouteManager } from './route-manager.js'; import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; -import { RedirectHandler, StaticHandler } from '../http-proxy/handlers/index.js'; /** * Handles new connection processing and setup logic with support for route-based configuration @@ -389,16 +388,6 @@ export class RouteConnectionHandler { case 'forward': return this.handleForwardAction(socket, record, route, initialChunk); - case 'redirect': - return this.handleRedirectAction(socket, record, route); - - case 'block': - return this.handleBlockAction(socket, record, route); - - case 'static': - this.handleStaticAction(socket, record, route, initialChunk); - return; - case 'socket-handler': logger.log('info', `Handling socket-handler action for route ${route.name}`, { connectionId, @@ -718,73 +707,6 @@ export class RouteConnectionHandler { } } - /** - * Handle a redirect action for a route - */ - private handleRedirectAction( - socket: plugins.net.Socket, - record: IConnectionRecord, - route: IRouteConfig - ): void { - // For TLS connections, we can't do redirects at the TCP level - if (record.isTLS) { - logger.log('warn', `Cannot redirect TLS connection ${record.id} at TCP level`, { - connectionId: record.id, - component: 'route-handler' - }); - socket.end(); - this.connectionManager.cleanupConnection(record, 'tls_redirect_error'); - return; - } - - // Delegate to HttpProxy's RedirectHandler - RedirectHandler.handleRedirect(socket, route, { - connectionId: record.id, - connectionManager: this.connectionManager, - settings: this.settings - }); - } - - /** - * Handle a block action for a route - */ - private handleBlockAction( - socket: plugins.net.Socket, - record: IConnectionRecord, - route: IRouteConfig - ): void { - const connectionId = record.id; - - if (this.settings.enableDetailedLogging) { - logger.log('info', `Blocking connection ${connectionId} based on route '${route.name || 'unnamed'}'`, { - connectionId, - routeName: route.name || 'unnamed', - component: 'route-handler' - }); - } - - // Simply close the connection - socket.end(); - this.connectionManager.initiateCleanupOnce(record, 'route_blocked'); - } - - /** - * Handle a static action for a route - */ - private async handleStaticAction( - socket: plugins.net.Socket, - record: IConnectionRecord, - route: IRouteConfig, - initialChunk?: Buffer - ): Promise { - // Delegate to HttpProxy's StaticHandler - await StaticHandler.handleStatic(socket, route, { - connectionId: record.id, - connectionManager: this.connectionManager, - settings: this.settings - }, record, initialChunk); - } - /** * Handle a socket-handler action for a route */ @@ -807,9 +729,22 @@ export class RouteConnectionHandler { return; } + // Create route context for the handler + const routeContext = this.createRouteContext({ + connectionId: record.id, + port: record.localPort, + domain: record.lockedDomain, + clientIp: record.remoteIP, + serverIp: socket.localAddress || '', + isTls: record.isTLS || false, + tlsVersion: record.tlsVersion, + routeName: route.name, + routeId: route.id, + }); + try { - // Call the handler - const result = route.action.socketHandler(socket); + // Call the handler with socket AND context + const result = route.action.socketHandler(socket, routeContext); // Handle async handlers properly if (result instanceof Promise) { diff --git a/ts/proxies/smart-proxy/utils/index.ts b/ts/proxies/smart-proxy/utils/index.ts index 6b2aa18..d02885d 100644 --- a/ts/proxies/smart-proxy/utils/index.ts +++ b/ts/proxies/smart-proxy/utils/index.ts @@ -19,7 +19,6 @@ import { createWebSocketRoute as createWebSocketPatternRoute, createLoadBalancerRoute as createLoadBalancerPatternRoute, createApiGatewayRoute, - createStaticFileServerRoute, addRateLimiting, addBasicAuth, addJwtAuth @@ -29,7 +28,6 @@ export { createWebSocketPatternRoute, createLoadBalancerPatternRoute, createApiGatewayRoute, - createStaticFileServerRoute, addRateLimiting, addBasicAuth, addJwtAuth diff --git a/ts/proxies/smart-proxy/utils/route-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers.ts index 2e90428..3ae90aa 100644 --- a/ts/proxies/smart-proxy/utils/route-helpers.ts +++ b/ts/proxies/smart-proxy/utils/route-helpers.ts @@ -11,7 +11,6 @@ * - HTTPS passthrough routes (createHttpsPassthroughRoute) * - Complete HTTPS servers with redirects (createCompleteHttpsServer) * - Load balancer routes (createLoadBalancerRoute) - * - Static file server routes (createStaticFileRoute) * - API routes (createApiRoute) * - WebSocket routes (createWebSocketRoute) * - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute) @@ -119,11 +118,8 @@ export function createHttpToHttpsRedirect( // Create route action const action: IRouteAction = { - type: 'redirect', - redirect: { - to: `https://{domain}:${httpsPort}{path}`, - status: 301 - } + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect(`https://{domain}:${httpsPort}{path}`, 301) }; // Create the route config @@ -267,60 +263,6 @@ export function createLoadBalancerRoute( }; } -/** - * Create a static file server route - * @param domains Domain(s) to match - * @param rootDir Root directory path for static files - * @param options Additional route options - * @returns Route configuration object - */ -export function createStaticFileRoute( - domains: string | string[], - rootDir: string, - options: { - indexFiles?: string[]; - serveOnHttps?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - httpPort?: number | number[]; - httpsPort?: number | number[]; - name?: string; - [key: string]: any; - } = {} -): IRouteConfig { - // Create route match - const match: IRouteMatch = { - ports: options.serveOnHttps - ? (options.httpsPort || 443) - : (options.httpPort || 80), - domains - }; - - // Create route action - const action: IRouteAction = { - type: 'static', - static: { - root: rootDir, - index: options.indexFiles || ['index.html', 'index.htm'] - } - }; - - // Add TLS configuration if serving on HTTPS - if (options.serveOnHttps) { - action.tls = { - mode: 'terminate', - certificate: options.certificate || 'auto' - }; - } - - // Create the route config - return { - match, - action, - name: options.name || `Static Files for ${Array.isArray(domains) ? domains.join(', ') : domains}`, - ...options - }; -} - /** * Create an API route configuration * @param domains Domain(s) to match @@ -853,7 +795,7 @@ export const SocketHandlers = { /** * Simple echo server handler */ - echo: (socket: plugins.net.Socket) => { + echo: (socket: plugins.net.Socket, context: IRouteContext) => { socket.write('ECHO SERVER READY\n'); socket.on('data', data => socket.write(data)); }, @@ -861,7 +803,7 @@ export const SocketHandlers = { /** * TCP proxy handler */ - proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket) => { + proxy: (targetHost: string, targetPort: number) => (socket: plugins.net.Socket, context: IRouteContext) => { const target = plugins.net.connect(targetPort, targetHost); socket.pipe(target); target.pipe(socket); @@ -876,7 +818,7 @@ export const SocketHandlers = { /** * Line-based protocol handler */ - lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket) => { + lineProtocol: (handler: (line: string, socket: plugins.net.Socket) => void) => (socket: plugins.net.Socket, context: IRouteContext) => { let buffer = ''; socket.on('data', (data) => { buffer += data.toString(); @@ -893,7 +835,7 @@ export const SocketHandlers = { /** * Simple HTTP response handler (for testing) */ - httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket) => { + httpResponse: (statusCode: number, body: string) => (socket: plugins.net.Socket, context: IRouteContext) => { const response = [ `HTTP/1.1 ${statusCode} ${statusCode === 200 ? 'OK' : 'Error'}`, 'Content-Type: text/plain', @@ -905,5 +847,74 @@ export const SocketHandlers = { socket.write(response); socket.end(); + }, + + /** + * Block connection immediately + */ + block: (message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => { + const finalMessage = message || `Connection blocked from ${context.clientIp}`; + if (finalMessage) { + socket.write(finalMessage); + } + socket.end(); + }, + + /** + * HTTP block response + */ + httpBlock: (statusCode: number = 403, message?: string) => (socket: plugins.net.Socket, context: IRouteContext) => { + const defaultMessage = `Access forbidden for ${context.domain || context.clientIp}`; + const finalMessage = message || defaultMessage; + + const response = [ + `HTTP/1.1 ${statusCode} ${finalMessage}`, + 'Content-Type: text/plain', + `Content-Length: ${finalMessage.length}`, + 'Connection: close', + '', + finalMessage + ].join('\r\n'); + + socket.write(response); + socket.end(); + }, + + /** + * HTTP redirect handler + */ + httpRedirect: (locationTemplate: string, statusCode: number = 301) => (socket: plugins.net.Socket, context: IRouteContext) => { + let buffer = ''; + + socket.once('data', (data) => { + buffer += data.toString(); + + const lines = buffer.split('\r\n'); + const requestLine = lines[0]; + const [method, path] = requestLine.split(' '); + + const domain = context.domain || 'localhost'; + const port = context.port; + + let finalLocation = locationTemplate + .replace('{domain}', domain) + .replace('{port}', String(port)) + .replace('{path}', path) + .replace('{clientIp}', context.clientIp); + + const message = `Redirecting to ${finalLocation}`; + const response = [ + `HTTP/1.1 ${statusCode} ${statusCode === 301 ? 'Moved Permanently' : 'Found'}`, + `Location: ${finalLocation}`, + 'Content-Type: text/plain', + `Content-Length: ${message.length}`, + 'Connection: close', + '', + message + ].join('\r\n'); + + socket.write(response); + socket.end(); + }); } }; diff --git a/ts/proxies/smart-proxy/utils/route-patterns.ts b/ts/proxies/smart-proxy/utils/route-patterns.ts index ab26c00..5b4e92a 100644 --- a/ts/proxies/smart-proxy/utils/route-patterns.ts +++ b/ts/proxies/smart-proxy/utils/route-patterns.ts @@ -7,6 +7,7 @@ import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js'; import { mergeRouteConfigs } from './route-utils.js'; +import { SocketHandlers } from './route-helpers.js'; /** * Create a basic HTTP route configuration @@ -112,11 +113,11 @@ export function createHttpToHttpsRedirect( ports: 80 }, action: { - type: 'redirect', - redirect: { - to: options.preservePath ? 'https://{domain}{path}' : 'https://{domain}', - status: options.redirectCode || 301 - } + type: 'socket-handler', + socketHandler: SocketHandlers.httpRedirect( + options.preservePath ? 'https://{domain}{path}' : 'https://{domain}', + options.redirectCode || 301 + ) }, name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}` }; @@ -214,57 +215,6 @@ export function createApiGatewayRoute( return mergeRouteConfigs(baseRoute, apiRoute); } -/** - * Create a static file server route pattern - * @param domains Domain(s) to match - * @param rootDirectory Root directory for static files - * @param options Additional route options - * @returns Static file server route configuration - */ -export function createStaticFileServerRoute( - domains: string | string[], - rootDirectory: string, - options: { - useTls?: boolean; - certificate?: 'auto' | { key: string; cert: string }; - indexFiles?: string[]; - cacheControl?: string; - path?: string; - [key: string]: any; - } = {} -): IRouteConfig { - // Create base route with static action - const baseRoute: IRouteConfig = { - match: { - domains, - ports: options.useTls ? 443 : 80, - path: options.path || '/' - }, - action: { - type: 'static', - static: { - root: rootDirectory, - index: options.indexFiles || ['index.html', 'index.htm'], - headers: { - 'Cache-Control': options.cacheControl || 'public, max-age=3600' - } - } - }, - name: options.name || `Static Server: ${Array.isArray(domains) ? domains.join(', ') : domains}`, - priority: options.priority || 50 - }; - - // Add TLS configuration if requested - if (options.useTls) { - baseRoute.action.tls = { - mode: 'terminate', - certificate: options.certificate || 'auto' - }; - } - - return baseRoute; -} - /** * Create a WebSocket route pattern * @param domains Domain(s) to match