fix(strcuture): refactor responsibilities
This commit is contained in:
		| @@ -52,6 +52,13 @@ export class ForwardingHandlerFactory { | ||||
|           enabled: true, | ||||
|           ...config.http | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 80; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-passthrough': | ||||
| @@ -65,6 +72,13 @@ export class ForwardingHandlerFactory { | ||||
|           enabled: false, | ||||
|           ...config.http | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 443; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-http': | ||||
| @@ -84,6 +98,13 @@ export class ForwardingHandlerFactory { | ||||
|           maintenance: true, | ||||
|           ...config.acme | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 443; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         break; | ||||
|          | ||||
|       case 'https-terminate-to-https': | ||||
| @@ -101,6 +122,13 @@ export class ForwardingHandlerFactory { | ||||
|           maintenance: true, | ||||
|           ...config.acme | ||||
|         }; | ||||
|         // Set default port and socket if not provided | ||||
|         if (!result.port) { | ||||
|           result.port = 443; | ||||
|         } | ||||
|         if (!result.socket) { | ||||
|           result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`; | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -1,16 +0,0 @@ | ||||
| /** | ||||
|  * HTTP functionality module | ||||
|  */ | ||||
|  | ||||
| // Export types and models | ||||
| export * from './models/http-types.js'; | ||||
|  | ||||
| // Export submodules (remove port80 export) | ||||
| export * from './router/index.js'; | ||||
| export * from './redirects/index.js'; | ||||
| // REMOVED: export * from './port80/index.js'; | ||||
|  | ||||
| // Convenience namespace exports (no more Port80) | ||||
| export const Http = { | ||||
|   // Only router and redirect functionality remain | ||||
| }; | ||||
| @@ -1,108 +0,0 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| // Certificate types have been removed - use SmartCertManager instead | ||||
| export interface IDomainOptions { | ||||
|   domainName: string; | ||||
|   sslRedirect: boolean; | ||||
|   acmeMaintenance: boolean; | ||||
|   forward?: { ip: string; port: number }; | ||||
|   acmeForward?: { ip: string; port: number }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP-specific event types | ||||
|  */ | ||||
| export enum HttpEvents { | ||||
|   REQUEST_RECEIVED = 'request-received', | ||||
|   REQUEST_FORWARDED = 'request-forwarded', | ||||
|   REQUEST_HANDLED = 'request-handled', | ||||
|   REQUEST_ERROR = 'request-error', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP status codes as an enum for better type safety | ||||
|  */ | ||||
| export enum HttpStatus { | ||||
|   OK = 200, | ||||
|   MOVED_PERMANENTLY = 301, | ||||
|   FOUND = 302, | ||||
|   TEMPORARY_REDIRECT = 307, | ||||
|   PERMANENT_REDIRECT = 308, | ||||
|   BAD_REQUEST = 400, | ||||
|   NOT_FOUND = 404, | ||||
|   METHOD_NOT_ALLOWED = 405, | ||||
|   INTERNAL_SERVER_ERROR = 500, | ||||
|   NOT_IMPLEMENTED = 501, | ||||
|   SERVICE_UNAVAILABLE = 503, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Represents a domain configuration with certificate status information | ||||
|  */ | ||||
| export interface IDomainCertificate { | ||||
|   options: IDomainOptions; | ||||
|   certObtained: boolean; | ||||
|   obtainingInProgress: boolean; | ||||
|   certificate?: string; | ||||
|   privateKey?: string; | ||||
|   expiryDate?: Date; | ||||
|   lastRenewalAttempt?: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Base error class for HTTP-related errors | ||||
|  */ | ||||
| export class HttpError extends Error { | ||||
|   constructor(message: string) { | ||||
|     super(message); | ||||
|     this.name = 'HttpError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error related to certificate operations | ||||
|  */ | ||||
| export class CertificateError extends HttpError { | ||||
|   constructor( | ||||
|     message: string, | ||||
|     public readonly domain: string, | ||||
|     public readonly isRenewal: boolean = false | ||||
|   ) { | ||||
|     super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`); | ||||
|     this.name = 'CertificateError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error related to server operations | ||||
|  */ | ||||
| export class ServerError extends HttpError { | ||||
|   constructor(message: string, public readonly code?: string) { | ||||
|     super(message); | ||||
|     this.name = 'ServerError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Redirect configuration for HTTP requests | ||||
|  */ | ||||
| export interface IRedirectConfig { | ||||
|   source: string;           // Source path or pattern | ||||
|   destination: string;      // Destination URL | ||||
|   type: HttpStatus;         // Redirect status code | ||||
|   preserveQuery?: boolean;  // Whether to preserve query parameters | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP router configuration | ||||
|  */ | ||||
| export interface IRouterConfig { | ||||
|   routes: Array<{ | ||||
|     path: string; | ||||
|     handler: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void; | ||||
|   }>; | ||||
|   notFoundHandler?: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void; | ||||
| } | ||||
|  | ||||
| // Backward compatibility interfaces | ||||
| export { HttpError as Port80HandlerError }; | ||||
| export { CertificateError as CertError }; | ||||
| @@ -1,3 +0,0 @@ | ||||
| /** | ||||
|  * HTTP redirects | ||||
|  */ | ||||
							
								
								
									
										22
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								ts/index.ts
									
									
									
									
									
								
							| @@ -6,19 +6,23 @@ | ||||
| // Migrated to the new proxies structure | ||||
| export * from './proxies/nftables-proxy/index.js'; | ||||
|  | ||||
| // Export NetworkProxy elements selectively to avoid RouteManager ambiguity | ||||
| export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './proxies/network-proxy/index.js'; | ||||
| export type { IMetricsTracker, MetricsTracker } from './proxies/network-proxy/index.js'; | ||||
| // Export HttpProxy elements selectively to avoid RouteManager ambiguity | ||||
| export { HttpProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './proxies/http-proxy/index.js'; | ||||
| export type { IMetricsTracker, MetricsTracker } from './proxies/http-proxy/index.js'; | ||||
| // Export models except IAcmeOptions to avoid conflict | ||||
| export type { INetworkProxyOptions, ICertificateEntry, ILogger } from './proxies/network-proxy/models/types.js'; | ||||
| export { RouteManager as NetworkProxyRouteManager } from './proxies/network-proxy/models/types.js'; | ||||
| export type { IHttpProxyOptions, ICertificateEntry, ILogger } from './proxies/http-proxy/models/types.js'; | ||||
| export { RouteManager as HttpProxyRouteManager } from './proxies/http-proxy/models/types.js'; | ||||
|  | ||||
| // Backward compatibility exports (deprecated) | ||||
| export { HttpProxy as NetworkProxy } from './proxies/http-proxy/index.js'; | ||||
| export type { IHttpProxyOptions as INetworkProxyOptions } from './proxies/http-proxy/models/types.js'; | ||||
| export { HttpProxyBridge as NetworkProxyBridge } from './proxies/smart-proxy/index.js'; | ||||
|  | ||||
| // Certificate and Port80 modules have been removed - use SmartCertManager instead | ||||
|  | ||||
| export * from './redirect/classes.redirect.js'; | ||||
| // Redirect module has been removed - use route-based redirects instead | ||||
|  | ||||
| // Export SmartProxy elements selectively to avoid RouteManager ambiguity | ||||
| export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './proxies/smart-proxy/index.js'; | ||||
| export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, HttpProxyBridge, RouteConnectionHandler, SmartCertManager } from './proxies/smart-proxy/index.js'; | ||||
| export { RouteManager } from './proxies/smart-proxy/route-manager.js'; | ||||
| // Export smart-proxy models | ||||
| export type { ISmartProxyOptions, IConnectionRecord, IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteContext } from './proxies/smart-proxy/models/index.js'; | ||||
| @@ -41,4 +45,4 @@ export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js'; | ||||
| export * as forwarding from './forwarding/index.js'; | ||||
| // Certificate module has been removed - use SmartCertManager instead | ||||
| export * as tls from './tls/index.js'; | ||||
| export * as http from './http/index.js'; | ||||
| export * as routing from './routing/index.js'; | ||||
| @@ -2,7 +2,7 @@ import * as plugins from '../../plugins.js'; | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import { fileURLToPath } from 'url'; | ||||
| import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './models/types.js'; | ||||
| import { type IHttpProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './models/types.js'; | ||||
| import type { IRouteConfig } from '../smart-proxy/models/route-types.js'; | ||||
| 
 | ||||
| /** | ||||
| @@ -18,7 +18,7 @@ export class CertificateManager { | ||||
|   private logger: ILogger; | ||||
|   private httpsServer: plugins.https.Server | null = null; | ||||
| 
 | ||||
|   constructor(private options: INetworkProxyOptions) { | ||||
|   constructor(private options: IHttpProxyOptions) { | ||||
|     this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs'); | ||||
|     this.logger = createLogger(options.logLevel || 'info'); | ||||
|      | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { type INetworkProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './models/types.js'; | ||||
| import { type IHttpProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './models/types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Manages a pool of backend connections for efficient reuse | ||||
| @@ -9,7 +9,7 @@ export class ConnectionPool { | ||||
|   private roundRobinPositions: Map<string, number> = new Map(); | ||||
|   private logger: ILogger; | ||||
| 
 | ||||
|   constructor(private options: INetworkProxyOptions) { | ||||
|   constructor(private options: IHttpProxyOptions) { | ||||
|     this.logger = createLogger(options.logLevel || 'info'); | ||||
|   } | ||||
|    | ||||
							
								
								
									
										6
									
								
								ts/proxies/http-proxy/handlers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ts/proxies/http-proxy/handlers/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| /** | ||||
|  * HTTP handlers for various route types | ||||
|  */ | ||||
|  | ||||
| export { RedirectHandler } from './redirect-handler.js'; | ||||
| export { StaticHandler } from './static-handler.js'; | ||||
							
								
								
									
										105
									
								
								ts/proxies/http-proxy/handlers/redirect-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								ts/proxies/http-proxy/handlers/redirect-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| 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<void> { | ||||
|     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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										251
									
								
								ts/proxies/http-proxy/handlers/static-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								ts/proxies/http-proxy/handlers/static-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | ||||
| 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 | ||||
|   ): Promise<void> { | ||||
|     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<string, string> = {}; | ||||
|       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'); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     // Listen for data | ||||
|     socket.on('data', handleHttpData); | ||||
|  | ||||
|     // Ensure cleanup on socket close | ||||
|     socket.once('close', () => { | ||||
|       socket.removeListener('data', handleHttpData); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -5,7 +5,7 @@ import { | ||||
|   convertLegacyConfigToRouteConfig | ||||
| } from './models/types.js'; | ||||
| import type { | ||||
|   INetworkProxyOptions, | ||||
|   IHttpProxyOptions, | ||||
|   ILogger, | ||||
|   IReverseProxyConfig | ||||
| } from './models/types.js'; | ||||
| @@ -16,21 +16,22 @@ import { CertificateManager } from './certificate-manager.js'; | ||||
| import { ConnectionPool } from './connection-pool.js'; | ||||
| import { RequestHandler, type IMetricsTracker } from './request-handler.js'; | ||||
| import { WebSocketHandler } from './websocket-handler.js'; | ||||
| import { ProxyRouter } from '../../http/router/index.js'; | ||||
| import { RouteRouter } from '../../http/router/route-router.js'; | ||||
| import { ProxyRouter } from '../../routing/router/index.js'; | ||||
| import { RouteRouter } from '../../routing/router/route-router.js'; | ||||
| import { FunctionCache } from './function-cache.js'; | ||||
| 
 | ||||
| /** | ||||
|  * NetworkProxy provides a reverse proxy with TLS termination, WebSocket support, | ||||
|  * HttpProxy provides a reverse proxy with TLS termination, WebSocket support, | ||||
|  * automatic certificate management, and high-performance connection pooling. | ||||
|  * Handles all HTTP/HTTPS traffic including redirects, ACME challenges, and static routes. | ||||
|  */ | ||||
| export class NetworkProxy implements IMetricsTracker { | ||||
| export class HttpProxy implements IMetricsTracker { | ||||
|   // Provide a minimal JSON representation to avoid circular references during deep equality checks
 | ||||
|   public toJSON(): any { | ||||
|     return {}; | ||||
|   } | ||||
|   // Configuration
 | ||||
|   public options: INetworkProxyOptions; | ||||
|   public options: IHttpProxyOptions; | ||||
|   public routes: IRouteConfig[] = []; | ||||
| 
 | ||||
|   // Server instances (HTTP/2 with HTTP/1 fallback)
 | ||||
| @@ -66,9 +67,9 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   private logger: ILogger; | ||||
| 
 | ||||
|   /** | ||||
|    * Creates a new NetworkProxy instance | ||||
|    * Creates a new HttpProxy instance | ||||
|    */ | ||||
|   constructor(optionsArg: INetworkProxyOptions) { | ||||
|   constructor(optionsArg: IHttpProxyOptions) { | ||||
|     // Set default options
 | ||||
|     this.options = { | ||||
|       port: optionsArg.port, | ||||
| @@ -155,7 +156,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Returns the port number this NetworkProxy is listening on | ||||
|    * Returns the port number this HttpProxy is listening on | ||||
|    * Useful for SmartProxy to determine where to forward connections | ||||
|    */ | ||||
|   public getListeningPort(): number { | ||||
| @@ -202,7 +203,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
| 
 | ||||
|   /** | ||||
|    * Returns current server metrics | ||||
|    * Useful for SmartProxy to determine which NetworkProxy to use for load balancing | ||||
|    * Useful for SmartProxy to determine which HttpProxy to use for load balancing | ||||
|    */ | ||||
|   public getMetrics(): any { | ||||
|     return { | ||||
| @@ -259,7 +260,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|     // Start the server
 | ||||
|     return new Promise((resolve) => { | ||||
|       this.httpsServer.listen(this.options.port, () => { | ||||
|         this.logger.info(`NetworkProxy started on port ${this.options.port}`); | ||||
|         this.logger.info(`HttpProxy started on port ${this.options.port}`); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
| @@ -352,7 +353,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Updates the route configurations - this is the primary method for configuring NetworkProxy | ||||
|    * Updates the route configurations - this is the primary method for configuring HttpProxy | ||||
|    * @param routes The new route configurations to use | ||||
|    */ | ||||
|   public async updateRouteConfigs(routes: IRouteConfig[]): Promise<void> { | ||||
| @@ -503,7 +504,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|    * Stops the proxy server | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     this.logger.info('Stopping NetworkProxy server'); | ||||
|     this.logger.info('Stopping HttpProxy server'); | ||||
|      | ||||
|     // Clear intervals
 | ||||
|     if (this.metricsInterval) { | ||||
| @@ -534,7 +535,7 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|     // Close the HTTPS server
 | ||||
|     return new Promise((resolve) => { | ||||
|       this.httpsServer.close(() => { | ||||
|         this.logger.info('NetworkProxy server stopped successfully'); | ||||
|         this.logger.info('HttpProxy server stopped successfully'); | ||||
|         resolve(); | ||||
|       }); | ||||
|     }); | ||||
| @@ -1,11 +1,11 @@ | ||||
| /** | ||||
|  * NetworkProxy implementation | ||||
|  * HttpProxy implementation | ||||
|  */ | ||||
| // Re-export models
 | ||||
| export * from './models/index.js'; | ||||
| 
 | ||||
| // Export NetworkProxy and supporting classes
 | ||||
| export { NetworkProxy } from './network-proxy.js'; | ||||
| // Export HttpProxy and supporting classes
 | ||||
| export { HttpProxy } from './http-proxy.js'; | ||||
| export { CertificateManager } from './certificate-manager.js'; | ||||
| export { ConnectionPool } from './connection-pool.js'; | ||||
| export { RequestHandler } from './request-handler.js'; | ||||
							
								
								
									
										165
									
								
								ts/proxies/http-proxy/models/http-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								ts/proxies/http-proxy/models/http-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * HTTP-specific event types | ||||
|  */ | ||||
| export enum HttpEvents { | ||||
|   REQUEST_RECEIVED = 'request-received', | ||||
|   REQUEST_FORWARDED = 'request-forwarded', | ||||
|   REQUEST_HANDLED = 'request-handled', | ||||
|   REQUEST_ERROR = 'request-error', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP status codes as an enum for better type safety | ||||
|  */ | ||||
| export enum HttpStatus { | ||||
|   OK = 200, | ||||
|   MOVED_PERMANENTLY = 301, | ||||
|   FOUND = 302, | ||||
|   TEMPORARY_REDIRECT = 307, | ||||
|   PERMANENT_REDIRECT = 308, | ||||
|   BAD_REQUEST = 400, | ||||
|   UNAUTHORIZED = 401, | ||||
|   FORBIDDEN = 403, | ||||
|   NOT_FOUND = 404, | ||||
|   METHOD_NOT_ALLOWED = 405, | ||||
|   REQUEST_TIMEOUT = 408, | ||||
|   TOO_MANY_REQUESTS = 429, | ||||
|   INTERNAL_SERVER_ERROR = 500, | ||||
|   NOT_IMPLEMENTED = 501, | ||||
|   BAD_GATEWAY = 502, | ||||
|   SERVICE_UNAVAILABLE = 503, | ||||
|   GATEWAY_TIMEOUT = 504, | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Base error class for HTTP-related errors | ||||
|  */ | ||||
| export class HttpError extends Error { | ||||
|   constructor(message: string, public readonly statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR) { | ||||
|     super(message); | ||||
|     this.name = 'HttpError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error related to certificate operations | ||||
|  */ | ||||
| export class CertificateError extends HttpError { | ||||
|   constructor( | ||||
|     message: string, | ||||
|     public readonly domain: string, | ||||
|     public readonly isRenewal: boolean = false | ||||
|   ) { | ||||
|     super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`, HttpStatus.INTERNAL_SERVER_ERROR); | ||||
|     this.name = 'CertificateError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error related to server operations | ||||
|  */ | ||||
| export class ServerError extends HttpError { | ||||
|   constructor(message: string, public readonly code?: string, statusCode: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR) { | ||||
|     super(message, statusCode); | ||||
|     this.name = 'ServerError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error for bad requests | ||||
|  */ | ||||
| export class BadRequestError extends HttpError { | ||||
|   constructor(message: string) { | ||||
|     super(message, HttpStatus.BAD_REQUEST); | ||||
|     this.name = 'BadRequestError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error for not found resources | ||||
|  */ | ||||
| export class NotFoundError extends HttpError { | ||||
|   constructor(message: string = 'Resource not found') { | ||||
|     super(message, HttpStatus.NOT_FOUND); | ||||
|     this.name = 'NotFoundError'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Redirect configuration for HTTP requests | ||||
|  */ | ||||
| export interface IRedirectConfig { | ||||
|   source: string;           // Source path or pattern | ||||
|   destination: string;      // Destination URL   | ||||
|   type: HttpStatus;         // Redirect status code | ||||
|   preserveQuery?: boolean;  // Whether to preserve query parameters | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP router configuration | ||||
|  */ | ||||
| export interface IRouterConfig { | ||||
|   routes: Array<{ | ||||
|     path: string; | ||||
|     method?: string; | ||||
|     handler: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void | Promise<void>; | ||||
|   }>; | ||||
|   notFoundHandler?: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void; | ||||
|   errorHandler?: (error: Error, req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP request method types | ||||
|  */ | ||||
| export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE'; | ||||
|  | ||||
| /** | ||||
|  * Helper function to get HTTP status text | ||||
|  */ | ||||
| export function getStatusText(status: HttpStatus): string { | ||||
|   const statusTexts: Record<HttpStatus, string> = { | ||||
|     [HttpStatus.OK]: 'OK', | ||||
|     [HttpStatus.MOVED_PERMANENTLY]: 'Moved Permanently', | ||||
|     [HttpStatus.FOUND]: 'Found', | ||||
|     [HttpStatus.TEMPORARY_REDIRECT]: 'Temporary Redirect', | ||||
|     [HttpStatus.PERMANENT_REDIRECT]: 'Permanent Redirect', | ||||
|     [HttpStatus.BAD_REQUEST]: 'Bad Request', | ||||
|     [HttpStatus.UNAUTHORIZED]: 'Unauthorized', | ||||
|     [HttpStatus.FORBIDDEN]: 'Forbidden', | ||||
|     [HttpStatus.NOT_FOUND]: 'Not Found', | ||||
|     [HttpStatus.METHOD_NOT_ALLOWED]: 'Method Not Allowed', | ||||
|     [HttpStatus.REQUEST_TIMEOUT]: 'Request Timeout', | ||||
|     [HttpStatus.TOO_MANY_REQUESTS]: 'Too Many Requests', | ||||
|     [HttpStatus.INTERNAL_SERVER_ERROR]: 'Internal Server Error', | ||||
|     [HttpStatus.NOT_IMPLEMENTED]: 'Not Implemented', | ||||
|     [HttpStatus.BAD_GATEWAY]: 'Bad Gateway', | ||||
|     [HttpStatus.SERVICE_UNAVAILABLE]: 'Service Unavailable', | ||||
|     [HttpStatus.GATEWAY_TIMEOUT]: 'Gateway Timeout', | ||||
|   }; | ||||
|   return statusTexts[status] || 'Unknown'; | ||||
| } | ||||
|  | ||||
| // Legacy interfaces for backward compatibility | ||||
| export interface IDomainOptions { | ||||
|   domainName: string; | ||||
|   sslRedirect: boolean; | ||||
|   acmeMaintenance: boolean; | ||||
|   forward?: { ip: string; port: number }; | ||||
|   acmeForward?: { ip: string; port: number }; | ||||
| } | ||||
|  | ||||
| export interface IDomainCertificate { | ||||
|   options: IDomainOptions; | ||||
|   certObtained: boolean; | ||||
|   obtainingInProgress: boolean; | ||||
|   certificate?: string; | ||||
|   privateKey?: string; | ||||
|   expiryDate?: Date; | ||||
|   lastRenewalAttempt?: Date; | ||||
| } | ||||
|  | ||||
| // Backward compatibility exports | ||||
| export { HttpError as Port80HandlerError }; | ||||
| export { CertificateError as CertError }; | ||||
							
								
								
									
										5
									
								
								ts/proxies/http-proxy/models/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ts/proxies/http-proxy/models/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| /** | ||||
|  * HttpProxy models | ||||
|  */ | ||||
| export * from './types.js'; | ||||
| export * from './http-types.js'; | ||||
| @@ -16,9 +16,9 @@ import type { IRouteConfig } from '../../smart-proxy/models/route-types.js'; | ||||
| import type { IRouteContext } from '../../../core/models/route-context.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Configuration options for NetworkProxy | ||||
|  * Configuration options for HttpProxy | ||||
|  */ | ||||
| export interface INetworkProxyOptions { | ||||
| export interface IHttpProxyOptions { | ||||
|   port: number; | ||||
|   maxConnections?: number; | ||||
|   keepAliveTimeout?: number; | ||||
| @@ -1,14 +1,14 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import '../../core/models/socket-augmentation.js'; | ||||
| import { | ||||
|   type INetworkProxyOptions, | ||||
|   type IHttpProxyOptions, | ||||
|   type ILogger, | ||||
|   createLogger, | ||||
|   type IReverseProxyConfig, | ||||
|   RouteManager | ||||
| } from './models/types.js'; | ||||
| import { ConnectionPool } from './connection-pool.js'; | ||||
| import { ProxyRouter } from '../../http/router/index.js'; | ||||
| import { ProxyRouter } from '../../routing/router/index.js'; | ||||
| import { ContextCreator } from './context-creator.js'; | ||||
| import { HttpRequestHandler } from './http-request-handler.js'; | ||||
| import { Http2RequestHandler } from './http2-request-handler.js'; | ||||
| @@ -46,7 +46,7 @@ export class RequestHandler { | ||||
|   public securityManager: SecurityManager; | ||||
| 
 | ||||
|   constructor( | ||||
|     private options: INetworkProxyOptions, | ||||
|     private options: IHttpProxyOptions, | ||||
|     private connectionPool: ConnectionPool, | ||||
|     private legacyRouter: ProxyRouter, // Legacy router for backward compatibility
 | ||||
|     private routeManager?: RouteManager, | ||||
| @@ -1,8 +1,8 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import '../../core/models/socket-augmentation.js'; | ||||
| import { type INetworkProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger, type IReverseProxyConfig } from './models/types.js'; | ||||
| import { type IHttpProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger, type IReverseProxyConfig } from './models/types.js'; | ||||
| import { ConnectionPool } from './connection-pool.js'; | ||||
| import { ProxyRouter, RouteRouter } from '../../http/router/index.js'; | ||||
| import { ProxyRouter, RouteRouter } from '../../routing/router/index.js'; | ||||
| import type { IRouteConfig } from '../smart-proxy/models/route-types.js'; | ||||
| import type { IRouteContext } from '../../core/models/route-context.js'; | ||||
| import { toBaseContext } from '../../core/models/route-context.js'; | ||||
| @@ -23,7 +23,7 @@ export class WebSocketHandler { | ||||
|   private securityManager: SecurityManager; | ||||
| 
 | ||||
|   constructor( | ||||
|     private options: INetworkProxyOptions, | ||||
|     private options: IHttpProxyOptions, | ||||
|     private connectionPool: ConnectionPool, | ||||
|     private legacyRouter: ProxyRouter, // Legacy router for backward compatibility
 | ||||
|     private routes: IRouteConfig[] = [] // Routes for modern router
 | ||||
| @@ -2,15 +2,15 @@ | ||||
|  * Proxy implementations module | ||||
|  */ | ||||
|  | ||||
| // Export NetworkProxy with selective imports to avoid conflicts | ||||
| export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './network-proxy/index.js'; | ||||
| export type { IMetricsTracker, MetricsTracker } from './network-proxy/index.js'; | ||||
| // Export network-proxy models except IAcmeOptions | ||||
| export type { INetworkProxyOptions, ICertificateEntry, ILogger } from './network-proxy/models/types.js'; | ||||
| export { RouteManager as NetworkProxyRouteManager } from './network-proxy/models/types.js'; | ||||
| // Export HttpProxy with selective imports to avoid conflicts | ||||
| export { HttpProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './http-proxy/index.js'; | ||||
| export type { IMetricsTracker, MetricsTracker } from './http-proxy/index.js'; | ||||
| // Export http-proxy models except IAcmeOptions | ||||
| export type { IHttpProxyOptions, ICertificateEntry, ILogger } from './http-proxy/models/types.js'; | ||||
| export { RouteManager as HttpProxyRouteManager } from './http-proxy/models/types.js'; | ||||
|  | ||||
| // Export SmartProxy with selective imports to avoid conflicts | ||||
| export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './smart-proxy/index.js'; | ||||
| export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, HttpProxyBridge, RouteConnectionHandler } from './smart-proxy/index.js'; | ||||
| export { RouteManager as SmartProxyRouteManager } from './smart-proxy/route-manager.js'; | ||||
| export * from './smart-proxy/utils/index.js'; | ||||
| // Export smart-proxy models except IAcmeOptions | ||||
|   | ||||
| @@ -1,4 +0,0 @@ | ||||
| /** | ||||
|  * NetworkProxy models | ||||
|  */ | ||||
| export * from './types.js'; | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { NetworkProxy } from '../network-proxy/index.js'; | ||||
| import { HttpProxy } from '../http-proxy/index.js'; | ||||
| import type { IRouteConfig, IRouteTls } from './models/route-types.js'; | ||||
| import type { IAcmeOptions } from './models/interfaces.js'; | ||||
| import { CertStore } from './cert-store.js'; | ||||
| @@ -25,7 +25,7 @@ export interface ICertificateData { | ||||
| export class SmartCertManager { | ||||
|   private certStore: CertStore; | ||||
|   private smartAcme: plugins.smartacme.SmartAcme | null = null; | ||||
|   private networkProxy: NetworkProxy | null = null; | ||||
|   private httpProxy: HttpProxy | null = null; | ||||
|   private renewalTimer: NodeJS.Timeout | null = null; | ||||
|   private pendingChallenges: Map<string, string> = new Map(); | ||||
|   private challengeRoute: IRouteConfig | null = null; | ||||
| @@ -68,8 +68,8 @@ export class SmartCertManager { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   public setNetworkProxy(networkProxy: NetworkProxy): void { | ||||
|     this.networkProxy = networkProxy; | ||||
|   public setHttpProxy(httpProxy: HttpProxy): void { | ||||
|     this.httpProxy = httpProxy; | ||||
|   } | ||||
|    | ||||
|    | ||||
| @@ -336,23 +336,23 @@ export class SmartCertManager { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Apply certificate to NetworkProxy | ||||
|    * Apply certificate to HttpProxy | ||||
|    */ | ||||
|   private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> { | ||||
|     if (!this.networkProxy) { | ||||
|       console.warn('NetworkProxy not set, cannot apply certificate'); | ||||
|     if (!this.httpProxy) { | ||||
|       console.warn('HttpProxy not set, cannot apply certificate'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     // Apply certificate to NetworkProxy | ||||
|     this.networkProxy.updateCertificate(domain, certData.cert, certData.key); | ||||
|     // Apply certificate to HttpProxy | ||||
|     this.httpProxy.updateCertificate(domain, certData.cert, certData.key); | ||||
|      | ||||
|     // Also apply for wildcard if it's a subdomain | ||||
|     if (domain.includes('.') && !domain.startsWith('*.')) { | ||||
|       const parts = domain.split('.'); | ||||
|       if (parts.length >= 2) { | ||||
|         const wildcardDomain = `*.${parts.slice(-2).join('.')}`; | ||||
|         this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key); | ||||
|         this.httpProxy.updateCertificate(wildcardDomain, certData.cert, certData.key); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,47 +1,47 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { NetworkProxy } from '../network-proxy/index.js'; | ||||
| import { HttpProxy } from '../http-proxy/index.js'; | ||||
| import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; | ||||
| import type { IRouteConfig } from './models/route-types.js'; | ||||
| 
 | ||||
| export class NetworkProxyBridge { | ||||
|   private networkProxy: NetworkProxy | null = null; | ||||
| export class HttpProxyBridge { | ||||
|   private httpProxy: HttpProxy | null = null; | ||||
| 
 | ||||
|   constructor(private settings: ISmartProxyOptions) {} | ||||
|    | ||||
|   /** | ||||
|    * Get the NetworkProxy instance | ||||
|    * Get the HttpProxy instance | ||||
|    */ | ||||
|   public getNetworkProxy(): NetworkProxy | null { | ||||
|     return this.networkProxy; | ||||
|   public getHttpProxy(): HttpProxy | null { | ||||
|     return this.httpProxy; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Initialize NetworkProxy instance | ||||
|    * Initialize HttpProxy instance | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { | ||||
|       const networkProxyOptions: any = { | ||||
|         port: this.settings.networkProxyPort!, | ||||
|     if (!this.httpProxy && this.settings.useHttpProxy && this.settings.useHttpProxy.length > 0) { | ||||
|       const httpProxyOptions: any = { | ||||
|         port: this.settings.httpProxyPort!, | ||||
|         portProxyIntegration: true, | ||||
|         logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info' | ||||
|       }; | ||||
| 
 | ||||
|       this.networkProxy = new NetworkProxy(networkProxyOptions); | ||||
|       console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`); | ||||
|       this.httpProxy = new HttpProxy(httpProxyOptions); | ||||
|       console.log(`Initialized HttpProxy on port ${this.settings.httpProxyPort}`); | ||||
| 
 | ||||
|       // Apply route configurations to NetworkProxy
 | ||||
|       await this.syncRoutesToNetworkProxy(this.settings.routes || []); | ||||
|       // Apply route configurations to HttpProxy
 | ||||
|       await this.syncRoutesToHttpProxy(this.settings.routes || []); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Sync routes to NetworkProxy | ||||
|    * Sync routes to HttpProxy | ||||
|    */ | ||||
|   public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> { | ||||
|     if (!this.networkProxy) return; | ||||
|   public async syncRoutesToHttpProxy(routes: IRouteConfig[]): Promise<void> { | ||||
|     if (!this.httpProxy) return; | ||||
|      | ||||
|     // Convert routes to NetworkProxy format
 | ||||
|     const networkProxyConfigs = routes | ||||
|     // Convert routes to HttpProxy format
 | ||||
|     const httpProxyConfigs = routes | ||||
|       .filter(route => { | ||||
|         // Check if this route matches any of the specified network proxy ports
 | ||||
|         const routePorts = Array.isArray(route.match.ports)  | ||||
| @@ -49,20 +49,20 @@ export class NetworkProxyBridge { | ||||
|           : [route.match.ports]; | ||||
|          | ||||
|         return routePorts.some(port =>  | ||||
|           this.settings.useNetworkProxy?.includes(port) | ||||
|           this.settings.useHttpProxy?.includes(port) | ||||
|         ); | ||||
|       }) | ||||
|       .map(route => this.routeToNetworkProxyConfig(route)); | ||||
|       .map(route => this.routeToHttpProxyConfig(route)); | ||||
|      | ||||
|     // Apply configurations to NetworkProxy
 | ||||
|     await this.networkProxy.updateRouteConfigs(networkProxyConfigs); | ||||
|     // Apply configurations to HttpProxy
 | ||||
|     await this.httpProxy.updateRouteConfigs(httpProxyConfigs); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Convert route to NetworkProxy configuration | ||||
|    * Convert route to HttpProxy configuration | ||||
|    */ | ||||
|   private routeToNetworkProxyConfig(route: IRouteConfig): any { | ||||
|     // Convert route to NetworkProxy domain config format
 | ||||
|   private routeToHttpProxyConfig(route: IRouteConfig): any { | ||||
|     // Convert route to HttpProxy domain config format
 | ||||
|     return { | ||||
|       domain: route.match.domains?.[0] || '*', | ||||
|       target: route.action.target, | ||||
| @@ -72,36 +72,36 @@ export class NetworkProxyBridge { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Check if connection should use NetworkProxy | ||||
|    * Check if connection should use HttpProxy | ||||
|    */ | ||||
|   public shouldUseNetworkProxy(connection: IConnectionRecord, routeMatch: any): boolean { | ||||
|     // Only use NetworkProxy for TLS termination
 | ||||
|   public shouldUseHttpProxy(connection: IConnectionRecord, routeMatch: any): boolean { | ||||
|     // Only use HttpProxy for TLS termination
 | ||||
|     return ( | ||||
|       routeMatch.route.action.tls?.mode === 'terminate' || | ||||
|       routeMatch.route.action.tls?.mode === 'terminate-and-reencrypt' | ||||
|     ) && this.networkProxy !== null; | ||||
|     ) && this.httpProxy !== null; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Forward connection to NetworkProxy | ||||
|    * Forward connection to HttpProxy | ||||
|    */ | ||||
|   public async forwardToNetworkProxy( | ||||
|   public async forwardToHttpProxy( | ||||
|     connectionId: string, | ||||
|     socket: plugins.net.Socket, | ||||
|     record: IConnectionRecord, | ||||
|     initialChunk: Buffer, | ||||
|     networkProxyPort: number, | ||||
|     httpProxyPort: number, | ||||
|     cleanupCallback: (reason: string) => void | ||||
|   ): Promise<void> { | ||||
|     if (!this.networkProxy) { | ||||
|       throw new Error('NetworkProxy not initialized'); | ||||
|     if (!this.httpProxy) { | ||||
|       throw new Error('HttpProxy not initialized'); | ||||
|     } | ||||
|      | ||||
|     const proxySocket = new plugins.net.Socket(); | ||||
|      | ||||
|     await new Promise<void>((resolve, reject) => { | ||||
|       proxySocket.connect(networkProxyPort, 'localhost', () => { | ||||
|         console.log(`[${connectionId}] Connected to NetworkProxy for termination`); | ||||
|       proxySocket.connect(httpProxyPort, 'localhost', () => { | ||||
|         console.log(`[${connectionId}] Connected to HttpProxy for termination`); | ||||
|         resolve(); | ||||
|       }); | ||||
|        | ||||
| @@ -132,21 +132,21 @@ export class NetworkProxyBridge { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Start NetworkProxy | ||||
|    * Start HttpProxy | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     if (this.networkProxy) { | ||||
|       await this.networkProxy.start(); | ||||
|     if (this.httpProxy) { | ||||
|       await this.httpProxy.start(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Stop NetworkProxy | ||||
|    * Stop HttpProxy | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     if (this.networkProxy) { | ||||
|       await this.networkProxy.stop(); | ||||
|       this.networkProxy = null; | ||||
|     if (this.httpProxy) { | ||||
|       await this.httpProxy.stop(); | ||||
|       this.httpProxy = null; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -14,12 +14,15 @@ export { ConnectionManager } from './connection-manager.js'; | ||||
| export { SecurityManager } from './security-manager.js'; | ||||
| export { TimeoutManager } from './timeout-manager.js'; | ||||
| export { TlsManager } from './tls-manager.js'; | ||||
| export { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| export { HttpProxyBridge } from './http-proxy-bridge.js'; | ||||
|  | ||||
| // Export route-based components | ||||
| export { RouteManager } from './route-manager.js'; | ||||
| export { RouteConnectionHandler } from './route-connection-handler.js'; | ||||
| export { NFTablesManager } from './nftables-manager.js'; | ||||
|  | ||||
| // Export certificate management | ||||
| export { SmartCertManager } from './certificate-manager.js'; | ||||
|  | ||||
| // Export all helper functions from the utils directory | ||||
| export * from './utils/index.js'; | ||||
|   | ||||
| @@ -94,9 +94,9 @@ export interface ISmartProxyOptions { | ||||
|   keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections | ||||
|   extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) | ||||
|  | ||||
|   // NetworkProxy integration | ||||
|   useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy | ||||
|   networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443) | ||||
|   // HttpProxy integration | ||||
|   useHttpProxy?: number[]; // Array of ports to forward to HttpProxy | ||||
|   httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443) | ||||
|  | ||||
|   /** | ||||
|    * Global ACME configuration options for SmartProxy | ||||
|   | ||||
| @@ -60,10 +60,10 @@ export class PortManager { | ||||
|     // Start listening on the port | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       server.listen(port, () => { | ||||
|         const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); | ||||
|         const isHttpProxyPort = this.settings.useHttpProxy?.includes(port); | ||||
|         console.log( | ||||
|           `SmartProxy -> OK: Now listening on port ${port}${ | ||||
|             isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : '' | ||||
|             isHttpProxyPort ? ' (HttpProxy forwarding enabled)' : '' | ||||
|           }` | ||||
|         ); | ||||
|          | ||||
|   | ||||
| @@ -5,10 +5,11 @@ import type { IRouteConfig, IRouteAction, IRouteContext } from './models/route-t | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { TlsManager } from './tls-manager.js'; | ||||
| import { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| 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 | ||||
| @@ -24,7 +25,7 @@ export class RouteConnectionHandler { | ||||
|     private connectionManager: ConnectionManager, | ||||
|     private securityManager: SecurityManager, | ||||
|     private tlsManager: TlsManager, | ||||
|     private networkProxyBridge: NetworkProxyBridge, | ||||
|     private httpProxyBridge: HttpProxyBridge, | ||||
|     private timeoutManager: TimeoutManager, | ||||
|     private routeManager: RouteManager | ||||
|   ) { | ||||
| @@ -530,22 +531,22 @@ export class RouteConnectionHandler { | ||||
|  | ||||
|         case 'terminate': | ||||
|         case 'terminate-and-reencrypt': | ||||
|           // For TLS termination, use NetworkProxy | ||||
|           if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|           // For TLS termination, use HttpProxy | ||||
|           if (this.httpProxyBridge.getHttpProxy()) { | ||||
|             if (this.settings.enableDetailedLogging) { | ||||
|               console.log( | ||||
|                 `[${connectionId}] Using NetworkProxy for TLS termination to ${action.target.host}` | ||||
|                 `[${connectionId}] Using HttpProxy for TLS termination to ${action.target.host}` | ||||
|               ); | ||||
|             } | ||||
|  | ||||
|             // If we have an initial chunk with TLS data, start processing it | ||||
|             if (initialChunk && record.isTLS) { | ||||
|               this.networkProxyBridge.forwardToNetworkProxy( | ||||
|               this.httpProxyBridge.forwardToHttpProxy( | ||||
|                 connectionId, | ||||
|                 socket, | ||||
|                 record, | ||||
|                 initialChunk, | ||||
|                 this.settings.networkProxyPort, | ||||
|                 this.settings.httpProxyPort || 8443, | ||||
|                 (reason) => this.connectionManager.initiateCleanupOnce(record, reason) | ||||
|               ); | ||||
|               return; | ||||
| @@ -557,9 +558,9 @@ export class RouteConnectionHandler { | ||||
|             this.connectionManager.cleanupConnection(record, 'tls_error'); | ||||
|             return; | ||||
|           } else { | ||||
|             console.log(`[${connectionId}] NetworkProxy not available for TLS termination`); | ||||
|             console.log(`[${connectionId}] HttpProxy not available for TLS termination`); | ||||
|             socket.end(); | ||||
|             this.connectionManager.cleanupConnection(record, 'no_network_proxy'); | ||||
|             this.connectionManager.cleanupConnection(record, 'no_http_proxy'); | ||||
|             return; | ||||
|           } | ||||
|       } | ||||
| @@ -621,87 +622,20 @@ export class RouteConnectionHandler { | ||||
|     record: IConnectionRecord, | ||||
|     route: IRouteConfig | ||||
|   ): void { | ||||
|     const connectionId = record.id; | ||||
|     const action = route.action; | ||||
|  | ||||
|     // We should have a redirect configuration | ||||
|     if (!action.redirect) { | ||||
|       console.log(`[${connectionId}] Redirect action missing redirect configuration`); | ||||
|       socket.end(); | ||||
|       this.connectionManager.cleanupConnection(record, 'missing_redirect'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // For TLS connections, we can't do redirects at the TCP level | ||||
|     if (record.isTLS) { | ||||
|       console.log(`[${connectionId}] Cannot redirect TLS connection at TCP level`); | ||||
|       console.log(`[${record.id}] Cannot redirect TLS connection at TCP level`); | ||||
|       socket.end(); | ||||
|       this.connectionManager.cleanupConnection(record, 'tls_redirect_error'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // 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() : record.lockedDomain || ''; | ||||
|  | ||||
|         // 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, record.localPort.toString()); | ||||
|  | ||||
|         // 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 (this.settings.enableDetailedLogging) { | ||||
|           console.log( | ||||
|             `[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}` | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         // Send the redirect response | ||||
|         socket.end(redirectResponse); | ||||
|         this.connectionManager.initiateCleanupOnce(record, 'redirect_complete'); | ||||
|       } catch (err) { | ||||
|         console.log(`[${connectionId}] Error processing HTTP redirect: ${err}`); | ||||
|         socket.end(); | ||||
|         this.connectionManager.initiateCleanupOnce(record, 'redirect_error'); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     // Setup the HTTP data handler | ||||
|     socket.once('data', httpDataHandler); | ||||
|     dataListeners.push(httpDataHandler); | ||||
|     // Delegate to HttpProxy's RedirectHandler | ||||
|     RedirectHandler.handleRedirect(socket, route, { | ||||
|       connectionId: record.id, | ||||
|       connectionManager: this.connectionManager, | ||||
|       settings: this.settings | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -733,221 +667,12 @@ export class RouteConnectionHandler { | ||||
|     record: IConnectionRecord, | ||||
|     route: IRouteConfig | ||||
|   ): Promise<void> { | ||||
|     const connectionId = record.id; | ||||
|  | ||||
|     if (!route.action.handler) { | ||||
|       console.error(`[${connectionId}] Static route '${route.name}' has no handler`); | ||||
|       socket.end(); | ||||
|       this.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 | ||||
|           console.error(`[${connectionId}] HTTP headers too large`); | ||||
|           socket.end(); | ||||
|           this.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) { | ||||
|         console.error(`[${connectionId}] Invalid HTTP request`); | ||||
|         socket.end(); | ||||
|         this.connectionManager.cleanupConnection(record, 'invalid_request'); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Parse request line | ||||
|       const requestLine = lines[0]; | ||||
|       const requestParts = requestLine.split(' '); | ||||
|       if (requestParts.length < 3) { | ||||
|         console.error(`[${connectionId}] Invalid HTTP request line`); | ||||
|         socket.end(); | ||||
|         this.connectionManager.cleanupConnection(record, 'invalid_request_line'); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const [method, path, httpVersion] = requestParts; | ||||
|  | ||||
|       // Parse headers | ||||
|       const headersMap: Record<string, string> = {}; | ||||
|       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 | ||||
|             console.error(`[${connectionId}] Request body too large`); | ||||
|             socket.end(); | ||||
|             this.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, | ||||
|           method: method, | ||||
|           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 (this.settings.enableDetailedLogging) { | ||||
|             console.log( | ||||
|               `[${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(), | ||||
|           }; | ||||
|  | ||||
|           // 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 | ||||
|           response = await route.action.handler(context); | ||||
|         } | ||||
|  | ||||
|         // 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(); | ||||
|  | ||||
|         this.connectionManager.cleanupConnection(record, 'completed'); | ||||
|       } catch (error) { | ||||
|         console.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(); | ||||
|  | ||||
|         this.connectionManager.cleanupConnection(record, 'handler_error'); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     // Listen for data | ||||
|     socket.on('data', handleHttpData); | ||||
|  | ||||
|     // Ensure cleanup on socket close | ||||
|     socket.once('close', () => { | ||||
|       socket.removeListener('data', handleHttpData); | ||||
|     }); | ||||
|     // Delegate to HttpProxy's StaticHandler | ||||
|     await StaticHandler.handleStatic(socket, route, { | ||||
|       connectionId: record.id, | ||||
|       connectionManager: this.connectionManager, | ||||
|       settings: this.settings | ||||
|     }, record); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -1378,12 +1103,3 @@ export class RouteConnectionHandler { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Helper function for status text | ||||
| function getStatusText(status: number): string { | ||||
|   const statusTexts: Record<number, string> = { | ||||
|     200: 'OK', | ||||
|     404: 'Not Found', | ||||
|     500: 'Internal Server Error', | ||||
|   }; | ||||
|   return statusTexts[status] || 'Unknown'; | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import * as plugins from '../../plugins.js'; | ||||
| import { ConnectionManager } from './connection-manager.js'; | ||||
| import { SecurityManager } from './security-manager.js'; | ||||
| import { TlsManager } from './tls-manager.js'; | ||||
| import { NetworkProxyBridge } from './network-proxy-bridge.js'; | ||||
| import { HttpProxyBridge } from './http-proxy-bridge.js'; | ||||
| import { TimeoutManager } from './timeout-manager.js'; | ||||
| import { PortManager } from './port-manager.js'; | ||||
| import { RouteManager } from './route-manager.js'; | ||||
| @@ -49,7 +49,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   private connectionManager: ConnectionManager; | ||||
|   private securityManager: SecurityManager; | ||||
|   private tlsManager: TlsManager; | ||||
|   private networkProxyBridge: NetworkProxyBridge; | ||||
|   private httpProxyBridge: HttpProxyBridge; | ||||
|   private timeoutManager: TimeoutManager; | ||||
|   public routeManager: RouteManager; // Made public for route management | ||||
|   private routeConnectionHandler: RouteConnectionHandler; | ||||
| @@ -123,7 +123,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', | ||||
|       keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, | ||||
|       extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, | ||||
|       networkProxyPort: settingsArg.networkProxyPort || 8443, | ||||
|       httpProxyPort: settingsArg.httpProxyPort || 8443, | ||||
|     }; | ||||
|      | ||||
|     // Normalize ACME options if provided (support both email and accountEmail) | ||||
| @@ -164,7 +164,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|      | ||||
|     // Create other required components | ||||
|     this.tlsManager = new TlsManager(this.settings); | ||||
|     this.networkProxyBridge = new NetworkProxyBridge(this.settings); | ||||
|     this.httpProxyBridge = new HttpProxyBridge(this.settings); | ||||
|      | ||||
|     // Initialize connection handler with route support | ||||
|     this.routeConnectionHandler = new RouteConnectionHandler( | ||||
| @@ -172,7 +172,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       this.connectionManager, | ||||
|       this.securityManager, | ||||
|       this.tlsManager, | ||||
|       this.networkProxyBridge, | ||||
|       this.httpProxyBridge, | ||||
|       this.timeoutManager, | ||||
|       this.routeManager | ||||
|     ); | ||||
| @@ -212,9 +212,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       await this.updateRoutes(routes); | ||||
|     }); | ||||
|      | ||||
|     // Connect with NetworkProxy if available | ||||
|     if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|       certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy()); | ||||
|     // Connect with HttpProxy if available | ||||
|     if (this.httpProxyBridge.getHttpProxy()) { | ||||
|       certManager.setHttpProxy(this.httpProxyBridge.getHttpProxy()); | ||||
|     } | ||||
|      | ||||
|     // Set the ACME state manager | ||||
| @@ -312,16 +312,16 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     // Initialize certificate manager before starting servers | ||||
|     await this.initializeCertificateManager(); | ||||
|  | ||||
|     // Initialize and start NetworkProxy if needed | ||||
|     if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { | ||||
|       await this.networkProxyBridge.initialize(); | ||||
|     // Initialize and start HttpProxy if needed | ||||
|     if (this.settings.useHttpProxy && this.settings.useHttpProxy.length > 0) { | ||||
|       await this.httpProxyBridge.initialize(); | ||||
|        | ||||
|       // Connect NetworkProxy with certificate manager | ||||
|       // Connect HttpProxy with certificate manager | ||||
|       if (this.certManager) { | ||||
|         this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy()); | ||||
|         this.certManager.setHttpProxy(this.httpProxyBridge.getHttpProxy()); | ||||
|       } | ||||
|        | ||||
|       await this.networkProxyBridge.start(); | ||||
|       await this.httpProxyBridge.start(); | ||||
|     } | ||||
|  | ||||
|     // Validate the route configuration | ||||
| @@ -368,7 +368,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       let completedTlsHandshakes = 0; | ||||
|       let pendingTlsHandshakes = 0; | ||||
|       let keepAliveConnections = 0; | ||||
|       let networkProxyConnections = 0; | ||||
|       let httpProxyConnections = 0; | ||||
|        | ||||
|       // Get connection records for analysis | ||||
|       const connectionRecords = this.connectionManager.getConnections(); | ||||
| @@ -392,7 +392,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|         } | ||||
|  | ||||
|         if (record.usingNetworkProxy) { | ||||
|           networkProxyConnections++; | ||||
|           httpProxyConnections++; | ||||
|         } | ||||
|  | ||||
|         maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); | ||||
| @@ -408,7 +408,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       console.log( | ||||
|         `Active connections: ${connectionRecords.size}. ` + | ||||
|         `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + | ||||
|         `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` + | ||||
|         `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, HttpProxy=${httpProxyConnections}. ` + | ||||
|         `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` + | ||||
|         `Termination stats: ${JSON.stringify({ | ||||
|           IN: terminationStats.incoming, | ||||
| @@ -460,8 +460,8 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     // Clean up all active connections | ||||
|     this.connectionManager.clearConnections(); | ||||
|  | ||||
|     // Stop NetworkProxy | ||||
|     await this.networkProxyBridge.stop(); | ||||
|     // Stop HttpProxy | ||||
|     await this.httpProxyBridge.stop(); | ||||
|      | ||||
|     // Clear ACME state manager | ||||
|     this.acmeStateManager.clear(); | ||||
| @@ -574,9 +574,9 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       // Update settings with the new routes | ||||
|       this.settings.routes = newRoutes; | ||||
|  | ||||
|       // If NetworkProxy is initialized, resync the configurations | ||||
|       if (this.networkProxyBridge.getNetworkProxy()) { | ||||
|         await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes); | ||||
|       // If HttpProxy is initialized, resync the configurations | ||||
|       if (this.httpProxyBridge.getHttpProxy()) { | ||||
|         await this.httpProxyBridge.syncRoutesToHttpProxy(newRoutes); | ||||
|       } | ||||
|  | ||||
|       // Update certificate manager with new routes | ||||
| @@ -711,14 +711,14 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     let tlsConnections = 0; | ||||
|     let nonTlsConnections = 0; | ||||
|     let keepAliveConnections = 0; | ||||
|     let networkProxyConnections = 0; | ||||
|     let httpProxyConnections = 0; | ||||
|      | ||||
|     // Analyze active connections | ||||
|     for (const record of connectionRecords.values()) { | ||||
|       if (record.isTLS) tlsConnections++; | ||||
|       else nonTlsConnections++; | ||||
|       if (record.hasKeepAlive) keepAliveConnections++; | ||||
|       if (record.usingNetworkProxy) networkProxyConnections++; | ||||
|       if (record.usingNetworkProxy) httpProxyConnections++; | ||||
|     } | ||||
|      | ||||
|     return { | ||||
| @@ -726,7 +726,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       tlsConnections, | ||||
|       nonTlsConnections, | ||||
|       keepAliveConnections, | ||||
|       networkProxyConnections, | ||||
|       httpProxyConnections, | ||||
|       terminationStats, | ||||
|       acmeEnabled: !!this.certManager, | ||||
|       port80HandlerPort: this.certManager ? 80 : null, | ||||
|   | ||||
| @@ -1,295 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| export interface RedirectRule { | ||||
|   /** | ||||
|    * Optional protocol to match (http or https). If not specified, matches both. | ||||
|    */ | ||||
|   fromProtocol?: 'http' | 'https'; | ||||
|    | ||||
|   /** | ||||
|    * Optional hostname pattern to match. Can use * as wildcard. | ||||
|    * If not specified, matches all hosts. | ||||
|    */ | ||||
|   fromHost?: string; | ||||
|    | ||||
|   /** | ||||
|    * Optional path prefix to match. If not specified, matches all paths. | ||||
|    */ | ||||
|   fromPath?: string; | ||||
|    | ||||
|   /** | ||||
|    * Target protocol for the redirect (http or https) | ||||
|    */ | ||||
|   toProtocol: 'http' | 'https'; | ||||
|    | ||||
|   /** | ||||
|    * Target hostname for the redirect. Can use $1, $2, etc. to reference | ||||
|    * captured groups from wildcard matches in fromHost. | ||||
|    */ | ||||
|   toHost: string; | ||||
|    | ||||
|   /** | ||||
|    * Optional target path prefix. If not specified, keeps original path. | ||||
|    * Can use $path to reference the original path. | ||||
|    */ | ||||
|   toPath?: string; | ||||
|    | ||||
|   /** | ||||
|    * HTTP status code for the redirect (301 for permanent, 302 for temporary) | ||||
|    */ | ||||
|   statusCode?: 301 | 302 | 307 | 308; | ||||
| } | ||||
|  | ||||
| export class Redirect { | ||||
|   private httpServer?: plugins.http.Server; | ||||
|   private httpsServer?: plugins.https.Server; | ||||
|   private rules: RedirectRule[] = []; | ||||
|   private httpPort: number = 80; | ||||
|   private httpsPort: number = 443; | ||||
|   private sslOptions?: { | ||||
|     key: Buffer; | ||||
|     cert: Buffer; | ||||
|   }; | ||||
|  | ||||
|   /** | ||||
|    * Create a new Redirect instance | ||||
|    * @param options Configuration options | ||||
|    */ | ||||
|   constructor(options: { | ||||
|     httpPort?: number; | ||||
|     httpsPort?: number; | ||||
|     sslOptions?: { | ||||
|       key: Buffer; | ||||
|       cert: Buffer; | ||||
|     }; | ||||
|     rules?: RedirectRule[]; | ||||
|   } = {}) { | ||||
|     if (options.httpPort) this.httpPort = options.httpPort; | ||||
|     if (options.httpsPort) this.httpsPort = options.httpsPort; | ||||
|     if (options.sslOptions) this.sslOptions = options.sslOptions; | ||||
|     if (options.rules) this.rules = options.rules; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add a redirect rule | ||||
|    */ | ||||
|   public addRule(rule: RedirectRule): void { | ||||
|     this.rules.push(rule); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Remove all redirect rules | ||||
|    */ | ||||
|   public clearRules(): void { | ||||
|     this.rules = []; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set SSL options for HTTPS redirects | ||||
|    */ | ||||
|   public setSslOptions(options: { key: Buffer; cert: Buffer }): void { | ||||
|     this.sslOptions = options; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Process a request according to the configured rules | ||||
|    */ | ||||
|   private handleRequest( | ||||
|     request: plugins.http.IncomingMessage, | ||||
|     response: plugins.http.ServerResponse, | ||||
|     protocol: 'http' | 'https' | ||||
|   ): void { | ||||
|     const requestUrl = new URL( | ||||
|       request.url || '/', | ||||
|       `${protocol}://${request.headers.host || 'localhost'}` | ||||
|     ); | ||||
|      | ||||
|     const host = requestUrl.hostname; | ||||
|     const path = requestUrl.pathname + requestUrl.search; | ||||
|      | ||||
|     // Find matching rule | ||||
|     const matchedRule = this.findMatchingRule(protocol, host, path); | ||||
|      | ||||
|     if (matchedRule) { | ||||
|       const targetUrl = this.buildTargetUrl(matchedRule, host, path); | ||||
|        | ||||
|       console.log(`Redirecting ${protocol}://${host}${path} to ${targetUrl}`); | ||||
|        | ||||
|       response.writeHead(matchedRule.statusCode || 302, { | ||||
|         Location: targetUrl, | ||||
|       }); | ||||
|       response.end(); | ||||
|     } else { | ||||
|       // No matching rule, send 404 | ||||
|       response.writeHead(404, { 'Content-Type': 'text/plain' }); | ||||
|       response.end('Not Found'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Find a matching redirect rule for the given request | ||||
|    */ | ||||
|   private findMatchingRule( | ||||
|     protocol: 'http' | 'https', | ||||
|     host: string, | ||||
|     path: string | ||||
|   ): RedirectRule | undefined { | ||||
|     return this.rules.find((rule) => { | ||||
|       // Check protocol match | ||||
|       if (rule.fromProtocol && rule.fromProtocol !== protocol) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       // Check host match | ||||
|       if (rule.fromHost) { | ||||
|         const pattern = rule.fromHost.replace(/\*/g, '(.*)'); | ||||
|         const regex = new RegExp(`^${pattern}$`); | ||||
|         if (!regex.test(host)) { | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Check path match | ||||
|       if (rule.fromPath && !path.startsWith(rule.fromPath)) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       return true; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Build the target URL for a redirect | ||||
|    */ | ||||
|   private buildTargetUrl(rule: RedirectRule, originalHost: string, originalPath: string): string { | ||||
|     let targetHost = rule.toHost; | ||||
|      | ||||
|     // Replace wildcards in host | ||||
|     if (rule.fromHost && rule.fromHost.includes('*')) { | ||||
|       const pattern = rule.fromHost.replace(/\*/g, '(.*)'); | ||||
|       const regex = new RegExp(`^${pattern}$`); | ||||
|       const matches = originalHost.match(regex); | ||||
|        | ||||
|       if (matches) { | ||||
|         for (let i = 1; i < matches.length; i++) { | ||||
|           targetHost = targetHost.replace(`$${i}`, matches[i]); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Build target path | ||||
|     let targetPath = originalPath; | ||||
|     if (rule.toPath) { | ||||
|       if (rule.toPath.includes('$path')) { | ||||
|         // Replace $path with original path, optionally removing the fromPath prefix | ||||
|         const pathSuffix = rule.fromPath ?  | ||||
|           originalPath.substring(rule.fromPath.length) :  | ||||
|           originalPath; | ||||
|          | ||||
|         targetPath = rule.toPath.replace('$path', pathSuffix); | ||||
|       } else { | ||||
|         targetPath = rule.toPath; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     return `${rule.toProtocol}://${targetHost}${targetPath}`; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Start the redirect server(s) | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     const tasks = []; | ||||
|  | ||||
|     // Create and start HTTP server if we have a port | ||||
|     if (this.httpPort) { | ||||
|       this.httpServer = plugins.http.createServer((req, res) =>  | ||||
|         this.handleRequest(req, res, 'http') | ||||
|       ); | ||||
|        | ||||
|       const httpStartPromise = new Promise<void>((resolve) => { | ||||
|         this.httpServer?.listen(this.httpPort, () => { | ||||
|           console.log(`HTTP redirect server started on port ${this.httpPort}`); | ||||
|           resolve(); | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       tasks.push(httpStartPromise); | ||||
|     } | ||||
|  | ||||
|     // Create and start HTTPS server if we have SSL options and a port | ||||
|     if (this.httpsPort && this.sslOptions) { | ||||
|       this.httpsServer = plugins.https.createServer(this.sslOptions, (req, res) =>  | ||||
|         this.handleRequest(req, res, 'https') | ||||
|       ); | ||||
|        | ||||
|       const httpsStartPromise = new Promise<void>((resolve) => { | ||||
|         this.httpsServer?.listen(this.httpsPort, () => { | ||||
|           console.log(`HTTPS redirect server started on port ${this.httpsPort}`); | ||||
|           resolve(); | ||||
|         }); | ||||
|       }); | ||||
|        | ||||
|       tasks.push(httpsStartPromise); | ||||
|     } | ||||
|  | ||||
|     // Wait for all servers to start | ||||
|     await Promise.all(tasks); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Stop the redirect server(s) | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     const tasks = []; | ||||
|  | ||||
|     if (this.httpServer) { | ||||
|       const httpStopPromise = new Promise<void>((resolve) => { | ||||
|         this.httpServer?.close(() => { | ||||
|           console.log('HTTP redirect server stopped'); | ||||
|           resolve(); | ||||
|         }); | ||||
|       }); | ||||
|       tasks.push(httpStopPromise); | ||||
|     } | ||||
|  | ||||
|     if (this.httpsServer) { | ||||
|       const httpsStopPromise = new Promise<void>((resolve) => { | ||||
|         this.httpsServer?.close(() => { | ||||
|           console.log('HTTPS redirect server stopped'); | ||||
|           resolve(); | ||||
|         }); | ||||
|       }); | ||||
|       tasks.push(httpsStopPromise); | ||||
|     } | ||||
|  | ||||
|     await Promise.all(tasks); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // For backward compatibility | ||||
| export class SslRedirect { | ||||
|   private redirect: Redirect; | ||||
|   port: number; | ||||
|  | ||||
|   constructor(portArg: number) { | ||||
|     this.port = portArg; | ||||
|     this.redirect = new Redirect({ | ||||
|       httpPort: portArg, | ||||
|       rules: [{ | ||||
|         fromProtocol: 'http', | ||||
|         toProtocol: 'https', | ||||
|         toHost: '$1', | ||||
|         statusCode: 302 | ||||
|       }] | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public async start() { | ||||
|     await this.redirect.start(); | ||||
|   } | ||||
|  | ||||
|   public async stop() { | ||||
|     await this.redirect.stop(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								ts/routing/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ts/routing/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| /** | ||||
|  * Routing functionality module | ||||
|  */ | ||||
|  | ||||
| // Export types and models from HttpProxy | ||||
| export * from '../proxies/http-proxy/models/http-types.js'; | ||||
|  | ||||
| // Export router functionality | ||||
| export * from './router/index.js'; | ||||
							
								
								
									
										6
									
								
								ts/routing/models/http-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ts/routing/models/http-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| /** | ||||
|  * This file re-exports HTTP types from the HttpProxy module | ||||
|  * for backward compatibility. All HTTP types are now consolidated | ||||
|  * in the HttpProxy module. | ||||
|  */ | ||||
| export * from '../../proxies/http-proxy/models/http-types.js'; | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { IReverseProxyConfig } from '../../proxies/network-proxy/models/types.js'; | ||||
| import type { IReverseProxyConfig } from '../../proxies/http-proxy/models/types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Optional path pattern configuration that can be added to proxy configs | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; | ||||
| import type { ILogger } from '../../proxies/network-proxy/models/types.js'; | ||||
| import type { ILogger } from '../../proxies/http-proxy/models/types.js'; | ||||
| 
 | ||||
| /** | ||||
|  * Optional path pattern configuration that can be added to proxy configs | ||||
		Reference in New Issue
	
	Block a user