From 200a735876433b73eca4a58442b323d4c7a625e9 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Thu, 29 May 2025 01:07:39 +0000 Subject: [PATCH] update --- readme.plan2.md | 98 ++++++++-------- ts/proxies/smart-proxy/certificate-manager.ts | 106 +++++++---------- ts/proxies/smart-proxy/utils/route-helpers.ts | 110 ++++++++++++++++++ ts/proxies/smart-proxy/utils/route-utils.ts | 16 +-- .../smart-proxy/utils/route-validators.ts | 38 ++---- 5 files changed, 213 insertions(+), 155 deletions(-) diff --git a/readme.plan2.md b/readme.plan2.md index 74feb1d..0a6d2f1 100644 --- a/readme.plan2.md +++ b/readme.plan2.md @@ -652,69 +652,69 @@ const domainRouter = (socket: net.Socket, context: IRouteContext) => { ### Step 10: Update Test Files (1.5 hours) #### 10.1 Update Socket Handler Tests -- [ ] Open `test/test.socket-handler.ts` -- [ ] Update all handler functions to accept context parameter -- [ ] Open `test/test.socket-handler.simple.ts` -- [ ] Update handler to accept context parameter -- [ ] Open `test/test.socket-handler-race.ts` -- [ ] Update handler to accept context parameter +- [x] Open `test/test.socket-handler.ts` +- [x] Update all handler functions to accept context parameter +- [x] Open `test/test.socket-handler.simple.ts` +- [x] Update handler to accept context parameter +- [x] Open `test/test.socket-handler-race.ts` +- [x] Update handler to accept context parameter #### 10.2 Find and Update/Delete Redirect Tests -- [ ] Search for files containing `type: 'redirect'` in test directory -- [ ] For each file: - - [ ] If it's a redirect-specific test, delete the file - - [ ] If it's a mixed test, update redirect actions to use socket handlers -- [ ] Files to check: - - [ ] `test/test.route-redirects.ts` - likely delete entire file - - [ ] `test/test.forwarding.ts` - update any redirect tests - - [ ] `test/test.forwarding.examples.ts` - update any redirect tests - - [ ] `test/test.route-config.ts` - update any redirect tests +- [x] Search for files containing `type: 'redirect'` in test directory +- [x] For each file: + - [x] If it's a redirect-specific test, delete the file + - [x] If it's a mixed test, update redirect actions to use socket handlers +- [x] Files to check: + - [x] `test/test.route-redirects.ts` - deleted entire file + - [x] `test/test.forwarding.ts` - update any redirect tests + - [x] `test/test.forwarding.examples.ts` - update any redirect tests + - [x] `test/test.route-config.ts` - update any redirect tests #### 10.3 Find and Update/Delete Block Tests -- [ ] Search for files containing `type: 'block'` in test directory -- [ ] Update or delete as appropriate +- [x] Search for files containing `type: 'block'` in test directory +- [x] Update or delete as appropriate #### 10.4 Find and Delete Static Tests -- [ ] Search for files containing `type: 'static'` in test directory -- [ ] Delete static-specific test files -- [ ] Remove static tests from mixed test files +- [x] Search for files containing `type: 'static'` in test directory +- [x] Delete static-specific test files +- [x] Remove static tests from mixed test files ### Step 11: Clean Up Imports and Exports (20 minutes) -- [ ] Open `ts/proxies/smart-proxy/utils/index.ts` -- [ ] Ensure route-helpers.ts is exported -- [ ] Remove any exports of deleted functions -- [ ] Open `ts/index.ts` -- [ ] Remove any exports of deleted types/interfaces -- [ ] Search for any remaining imports of RedirectHandler or StaticHandler -- [ ] Remove any found imports +- [x] Open `ts/proxies/smart-proxy/utils/index.ts` +- [x] Ensure route-helpers.ts is exported +- [x] Remove any exports of deleted functions +- [x] Open `ts/index.ts` +- [x] Remove any exports of deleted types/interfaces +- [x] Search for any remaining imports of RedirectHandler or StaticHandler +- [x] Remove any found imports ### Step 12: Documentation Updates (30 minutes) -- [ ] Update README.md: - - [ ] Remove any mention of redirect, block, static action types - - [ ] Add examples of socket handlers with context - - [ ] Document the two action types: forward and socket-handler -- [ ] Update any JSDoc comments in modified files -- [ ] Add examples showing context usage +- [x] Update README.md: + - [x] Remove any mention of redirect, block, static action types + - [x] Add examples of socket handlers with context + - [x] Document the two action types: forward and socket-handler +- [x] Update any JSDoc comments in modified files +- [x] Add examples showing context usage ### Step 13: Final Verification (15 minutes) -- [ ] Run build: `pnpm build` -- [ ] Fix any compilation errors -- [ ] Run tests: `pnpm test` -- [ ] Fix any failing tests -- [ ] Search codebase for any remaining references to: - - [ ] 'redirect' action type - - [ ] 'block' action type - - [ ] 'static' action type - - [ ] RedirectHandler - - [ ] StaticHandler - - [ ] IRouteRedirect - - [ ] IRouteStatic +- [x] Run build: `pnpm build` +- [x] Fix any compilation errors +- [x] Run tests: `pnpm test` +- [x] Fix any failing tests +- [x] Search codebase for any remaining references to: + - [x] 'redirect' action type + - [x] 'block' action type + - [x] 'static' action type + - [x] RedirectHandler + - [x] StaticHandler + - [x] IRouteRedirect + - [x] IRouteStatic ### Step 14: Test New Functionality (30 minutes) -- [ ] Create test for block socket handler with context -- [ ] Create test for httpBlock socket handler with context -- [ ] Create test for httpRedirect socket handler with context -- [ ] Verify context is properly passed in all scenarios +- [x] Create test for block socket handler with context +- [x] Create test for httpBlock socket handler with context +- [x] Create test for httpRedirect socket handler with context +- [x] Verify context is properly passed in all scenarios --- diff --git a/ts/proxies/smart-proxy/certificate-manager.ts b/ts/proxies/smart-proxy/certificate-manager.ts index 429bed4..eb20c3b 100644 --- a/ts/proxies/smart-proxy/certificate-manager.ts +++ b/ts/proxies/smart-proxy/certificate-manager.ts @@ -5,6 +5,7 @@ import type { IAcmeOptions } from './models/interfaces.js'; import { CertStore } from './cert-store.js'; import type { AcmeStateManager } from './acme-state-manager.js'; import { logger } from '../../core/utils/logger.js'; +import { SocketHandlers } from './utils/route-helpers.js'; export interface ICertStatus { domain: string; @@ -694,70 +695,53 @@ export class SmartCertManager { }, action: { 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; - } - - // Create mock request/response objects for SmartAcme - const mockReq = { - url: path, - method: 'GET', - headers: {} - }; + socketHandler: SocketHandlers.httpServer((req, res) => { + // Extract the token from the path + const token = req.url?.split('/').pop(); + if (!token) { + res.status(404); + res.send('Not found'); + return; + } - 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); + // Create mock request/response objects for SmartAcme + let responseData: any = null; + const mockReq = { + url: req.url, + method: req.method, + headers: req.headers + }; + + const mockRes = { + statusCode: 200, + setHeader: (name: string, value: string) => {}, + end: (data: any) => { + responseData = data; + } + }; + + // Use SmartAcme's handler + const handleAcme = () => { + http01Handler.handleRequest(mockReq as any, mockRes as any, () => { + // Not handled by ACME + res.status(404); + res.send('Not found'); }); - 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(); - }); - } + // Give it a moment to process, then send response + setTimeout(() => { + if (responseData) { + res.header('Content-Type', 'text/plain'); + res.send(String(responseData)); + } else { + res.status(404); + res.send('Not found'); + } + }, 100); + }; + + handleAcme(); + }) } }; diff --git a/ts/proxies/smart-proxy/utils/route-helpers.ts b/ts/proxies/smart-proxy/utils/route-helpers.ts index 3ae90aa..121ad92 100644 --- a/ts/proxies/smart-proxy/utils/route-helpers.ts +++ b/ts/proxies/smart-proxy/utils/route-helpers.ts @@ -916,5 +916,115 @@ export const SocketHandlers = { socket.write(response); socket.end(); }); + }, + + /** + * HTTP server handler for ACME challenges and other HTTP needs + */ + httpServer: (handler: (req: { method: string; url: string; headers: Record; body?: string }, res: { status: (code: number) => void; header: (name: string, value: string) => void; send: (data: string) => void; end: () => void }) => void) => (socket: plugins.net.Socket, context: IRouteContext) => { + let buffer = ''; + let requestParsed = false; + + socket.on('data', (data) => { + if (requestParsed) return; // Only handle the first request + + buffer += data.toString(); + + // Check if we have a complete HTTP request + const headerEndIndex = buffer.indexOf('\r\n\r\n'); + if (headerEndIndex === -1) return; // Need more data + + requestParsed = true; + + // Parse the HTTP request + const headerPart = buffer.substring(0, headerEndIndex); + const bodyPart = buffer.substring(headerEndIndex + 4); + + const lines = headerPart.split('\r\n'); + const [method, url] = lines[0].split(' '); + + const headers: Record = {}; + for (let i = 1; i < lines.length; i++) { + const colonIndex = lines[i].indexOf(':'); + if (colonIndex > 0) { + const name = lines[i].substring(0, colonIndex).trim().toLowerCase(); + const value = lines[i].substring(colonIndex + 1).trim(); + headers[name] = value; + } + } + + // Create request object + const req = { + method: method || 'GET', + url: url || '/', + headers, + body: bodyPart + }; + + // Create response object + let statusCode = 200; + const responseHeaders: Record = {}; + let ended = false; + + const res = { + status: (code: number) => { + statusCode = code; + }, + header: (name: string, value: string) => { + responseHeaders[name] = value; + }, + send: (data: string) => { + if (ended) return; + ended = true; + + if (!responseHeaders['content-type']) { + responseHeaders['content-type'] = 'text/plain'; + } + responseHeaders['content-length'] = String(data.length); + responseHeaders['connection'] = 'close'; + + const statusText = statusCode === 200 ? 'OK' : + statusCode === 404 ? 'Not Found' : + statusCode === 500 ? 'Internal Server Error' : 'Response'; + + let response = `HTTP/1.1 ${statusCode} ${statusText}\r\n`; + for (const [name, value] of Object.entries(responseHeaders)) { + response += `${name}: ${value}\r\n`; + } + response += '\r\n'; + response += data; + + socket.write(response); + socket.end(); + }, + end: () => { + if (ended) return; + ended = true; + socket.write('HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'); + socket.end(); + } + }; + + try { + handler(req, res); + // Ensure response is sent even if handler doesn't call send() + setTimeout(() => { + if (!ended) { + res.send(''); + } + }, 1000); + } catch (error) { + if (!ended) { + res.status(500); + res.send('Internal Server Error'); + } + } + }); + + socket.on('error', () => { + if (!requestParsed) { + socket.end(); + } + }); } }; diff --git a/ts/proxies/smart-proxy/utils/route-utils.ts b/ts/proxies/smart-proxy/utils/route-utils.ts index 284f819..25a6a4c 100644 --- a/ts/proxies/smart-proxy/utils/route-utils.ts +++ b/ts/proxies/smart-proxy/utils/route-utils.ts @@ -74,21 +74,7 @@ export function mergeRouteConfigs( }; } - // Merge redirect options - if (overrideRoute.action.redirect) { - mergedRoute.action.redirect = { - ...mergedRoute.action.redirect, - ...overrideRoute.action.redirect - }; - } - - // Merge static options - if (overrideRoute.action.static) { - mergedRoute.action.static = { - ...mergedRoute.action.static, - ...overrideRoute.action.static - }; - } + // No special merging needed for socket handlers - they are functions } } diff --git a/ts/proxies/smart-proxy/utils/route-validators.ts b/ts/proxies/smart-proxy/utils/route-validators.ts index 6e4891f..87312d4 100644 --- a/ts/proxies/smart-proxy/utils/route-validators.ts +++ b/ts/proxies/smart-proxy/utils/route-validators.ts @@ -143,30 +143,12 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err } } - // Validate redirect for 'redirect' action - if (action.type === 'redirect') { - if (!action.redirect) { - errors.push('Redirect configuration is required for redirect action'); - } else { - if (!action.redirect.to) { - errors.push('Redirect target (to) is required'); - } - - if (action.redirect.status && - ![301, 302, 303, 307, 308].includes(action.redirect.status)) { - errors.push('Invalid redirect status code'); - } - } - } - - // Validate static file config for 'static' action - if (action.type === 'static') { - if (!action.static) { - errors.push('Static file configuration is required for static action'); - } else { - if (!action.static.root) { - errors.push('Static file root directory is required'); - } + // Validate socket handler for 'socket-handler' action + if (action.type === 'socket-handler') { + if (!action.socketHandler) { + errors.push('Socket handler function is required for socket-handler action'); + } else if (typeof action.socketHandler !== 'function') { + errors.push('Socket handler must be a function'); } } @@ -261,12 +243,8 @@ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType: switch (actionType) { case 'forward': return !!route.action.target && !!route.action.target.host && !!route.action.target.port; - case 'redirect': - return !!route.action.redirect && !!route.action.redirect.to; - case 'static': - return !!route.action.static && !!route.action.static.root; - case 'block': - return true; // Block action doesn't require additional properties + case 'socket-handler': + return !!route.action.socketHandler && typeof route.action.socketHandler === 'function'; default: return false; }