feat(core): Add backendProtocol option to support HTTP/2 client sessions alongside HTTP/1. This update enhances NetworkProxy's core functionality by integrating HTTP/2 support in server creation and request handling, while updating plugin exports and documentation accordingly.
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '7.0.1', | ||||
|   version: '7.1.0', | ||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -16,8 +16,8 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|   public options: INetworkProxyOptions; | ||||
|   public proxyConfigs: IReverseProxyConfig[] = []; | ||||
|    | ||||
|   // Server instances | ||||
|   public httpsServer: plugins.https.Server; | ||||
|   // Server instances (HTTP/2 with HTTP/1 fallback) | ||||
|   public httpsServer: any; | ||||
|    | ||||
|   // Core components | ||||
|   private certificateManager: CertificateManager; | ||||
| @@ -66,6 +66,8 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|       connectionPoolSize: optionsArg.connectionPoolSize || 50, | ||||
|       portProxyIntegration: optionsArg.portProxyIntegration || false, | ||||
|       useExternalPort80Handler: optionsArg.useExternalPort80Handler || false, | ||||
|       // Backend protocol (http1 or http2) | ||||
|       backendProtocol: optionsArg.backendProtocol || 'http1', | ||||
|       // Default ACME options | ||||
|       acme: { | ||||
|         enabled: optionsArg.acme?.enabled || false, | ||||
| @@ -185,33 +187,35 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|       await this.certificateManager.initializePort80Handler(); | ||||
|     } | ||||
|      | ||||
|     // Create the HTTPS server | ||||
|     this.httpsServer = plugins.https.createServer( | ||||
|     // Create HTTP/2 server with HTTP/1 fallback | ||||
|     this.httpsServer = plugins.http2.createSecureServer( | ||||
|       { | ||||
|         key: this.certificateManager.getDefaultCertificates().key, | ||||
|         cert: this.certificateManager.getDefaultCertificates().cert, | ||||
|         SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb) | ||||
|       }, | ||||
|       (req, res) => this.requestHandler.handleRequest(req, res) | ||||
|         allowHTTP1: true, | ||||
|         ALPNProtocols: ['h2', 'http/1.1'] | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     // Configure server timeouts | ||||
|     this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout; | ||||
|     this.httpsServer.headersTimeout = this.options.headersTimeout; | ||||
|      | ||||
|     // Setup connection tracking | ||||
|     // Track raw TCP connections for metrics and limits | ||||
|     this.setupConnectionTracking(); | ||||
|      | ||||
|     // Share HTTPS server with certificate manager | ||||
|  | ||||
|     // Handle incoming HTTP/2 streams | ||||
|     this.httpsServer.on('stream', (stream: any, headers: any) => { | ||||
|       this.requestHandler.handleHttp2(stream, headers); | ||||
|     }); | ||||
|     // Handle HTTP/1.x fallback requests | ||||
|     this.httpsServer.on('request', (req: any, res: any) => { | ||||
|       this.requestHandler.handleRequest(req, res); | ||||
|     }); | ||||
|  | ||||
|     // Share server with certificate manager for dynamic contexts | ||||
|     this.certificateManager.setHttpsServer(this.httpsServer); | ||||
|      | ||||
|     // Setup WebSocket support | ||||
|     // Setup WebSocket support on HTTP/1 fallback | ||||
|     this.webSocketHandler.initialize(this.httpsServer); | ||||
|      | ||||
|     // Start metrics collection | ||||
|     // Start metrics logging | ||||
|     this.setupMetricsCollection(); | ||||
|      | ||||
|     // Setup connection pool cleanup interval | ||||
|     // Start periodic connection pool cleanup | ||||
|     this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup(); | ||||
|  | ||||
|     // Start the server | ||||
|   | ||||
| @@ -18,6 +18,8 @@ export class RequestHandler { | ||||
|   private defaultHeaders: { [key: string]: string } = {}; | ||||
|   private logger: ILogger; | ||||
|   private metricsTracker: IMetricsTracker | null = null; | ||||
|   // HTTP/2 client sessions for backend proxying | ||||
|   private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map(); | ||||
|    | ||||
|   constructor( | ||||
|     private options: INetworkProxyOptions, | ||||
| @@ -130,6 +132,70 @@ export class RequestHandler { | ||||
|      | ||||
|     // Apply default headers | ||||
|     this.applyDefaultHeaders(res); | ||||
|     // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions | ||||
|     if (this.options.backendProtocol === 'http2') { | ||||
|       // Route and validate config | ||||
|       const proxyConfig = this.router.routeReq(req); | ||||
|       if (!proxyConfig) { | ||||
|         this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); | ||||
|         res.statusCode = 404; | ||||
|         res.end('Not Found: No proxy configuration for this host'); | ||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|         return; | ||||
|       } | ||||
|       // Determine backend target | ||||
|       const destination = this.connectionPool.getNextTarget( | ||||
|         proxyConfig.destinationIps, | ||||
|         proxyConfig.destinationPorts[0] | ||||
|       ); | ||||
|       // Obtain or create HTTP/2 session | ||||
|       const key = `${destination.host}:${destination.port}`; | ||||
|       let session = this.h2Sessions.get(key); | ||||
|       if (!session || session.closed || (session as any).destroyed) { | ||||
|         session = plugins.http2.connect(`http://${destination.host}:${destination.port}`); | ||||
|         this.h2Sessions.set(key, session); | ||||
|         session.on('error', () => this.h2Sessions.delete(key)); | ||||
|         session.on('close', () => this.h2Sessions.delete(key)); | ||||
|       } | ||||
|       // Build headers for HTTP/2 request | ||||
|       const h2Headers: Record<string, string> = { | ||||
|         ':method': req.method || 'GET', | ||||
|         ':path': req.url || '/', | ||||
|         ':authority': `${destination.host}:${destination.port}` | ||||
|       }; | ||||
|       for (const [k, v] of Object.entries(req.headers)) { | ||||
|         if (typeof v === 'string' && !k.startsWith(':')) { | ||||
|           h2Headers[k] = v; | ||||
|         } | ||||
|       } | ||||
|       // Open HTTP/2 stream | ||||
|       const h2Stream = session.request(h2Headers); | ||||
|       // Pipe client request body to backend | ||||
|       req.pipe(h2Stream); | ||||
|       // Handle backend response | ||||
|       h2Stream.on('response', (headers, flags) => { | ||||
|         const status = headers[':status'] as number || 502; | ||||
|         // Map headers | ||||
|         for (const [hk, hv] of Object.entries(headers)) { | ||||
|           if (!hk.startsWith(':') && hv) { | ||||
|             res.setHeader(hk, hv as string); | ||||
|           } | ||||
|         } | ||||
|         res.statusCode = status; | ||||
|         h2Stream.pipe(res); | ||||
|       }); | ||||
|       h2Stream.on('error', (err) => { | ||||
|         this.logger.error(`HTTP/2 proxy error: ${err.message}`); | ||||
|         if (!res.headersSent) { | ||||
|           res.statusCode = 502; | ||||
|           res.end(`Bad Gateway: ${err.message}`); | ||||
|         } else { | ||||
|           res.end(); | ||||
|         } | ||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Find target based on hostname | ||||
| @@ -275,4 +341,119 @@ export class RequestHandler { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle HTTP/2 stream requests by proxying to HTTP/1 backends | ||||
|    */ | ||||
|   public async handleHttp2(stream: any, headers: any): Promise<void> { | ||||
|     const startTime = Date.now(); | ||||
|     const method = headers[':method'] || 'GET'; | ||||
|     const path = headers[':path'] || '/'; | ||||
|     // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions | ||||
|     if (this.options.backendProtocol === 'http2') { | ||||
|       const authority = headers[':authority'] as string || ''; | ||||
|       const host = authority.split(':')[0]; | ||||
|       const fakeReq: any = { headers: { host }, method: headers[':method'], url: headers[':path'], socket: (stream.session as any).socket }; | ||||
|       const proxyConfig = this.router.routeReq(fakeReq); | ||||
|       if (!proxyConfig) { | ||||
|         stream.respond({ ':status': 404 }); | ||||
|         stream.end('Not Found'); | ||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|         return; | ||||
|       } | ||||
|       const destination = this.connectionPool.getNextTarget(proxyConfig.destinationIps, proxyConfig.destinationPorts[0]); | ||||
|       const key = `${destination.host}:${destination.port}`; | ||||
|       let session = this.h2Sessions.get(key); | ||||
|       if (!session || session.closed || (session as any).destroyed) { | ||||
|         session = plugins.http2.connect(`http://${destination.host}:${destination.port}`); | ||||
|         this.h2Sessions.set(key, session); | ||||
|         session.on('error', () => this.h2Sessions.delete(key)); | ||||
|         session.on('close', () => this.h2Sessions.delete(key)); | ||||
|       } | ||||
|       // Build headers for backend HTTP/2 request | ||||
|       const h2Headers: Record<string, any> = { | ||||
|         ':method': headers[':method'], | ||||
|         ':path': headers[':path'], | ||||
|         ':authority': `${destination.host}:${destination.port}` | ||||
|       }; | ||||
|       for (const [k, v] of Object.entries(headers)) { | ||||
|         if (!k.startsWith(':') && typeof v === 'string') { | ||||
|           h2Headers[k] = v; | ||||
|         } | ||||
|       } | ||||
|       const h2Stream2 = session.request(h2Headers); | ||||
|       stream.pipe(h2Stream2); | ||||
|       h2Stream2.on('response', (hdrs: any) => { | ||||
|         // Map status and headers to client | ||||
|         const resp: Record<string, any> = { ':status': hdrs[':status'] as number }; | ||||
|         for (const [hk, hv] of Object.entries(hdrs)) { | ||||
|           if (!hk.startsWith(':') && hv) resp[hk] = hv; | ||||
|         } | ||||
|         stream.respond(resp); | ||||
|         h2Stream2.pipe(stream); | ||||
|       }); | ||||
|       h2Stream2.on('error', (err) => { | ||||
|         stream.respond({ ':status': 502 }); | ||||
|         stream.end(`Bad Gateway: ${err.message}`); | ||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     try { | ||||
|       // Determine host for routing | ||||
|       const authority = headers[':authority'] as string || ''; | ||||
|       const host = authority.split(':')[0]; | ||||
|       // Fake request object for routing | ||||
|       const fakeReq: any = { headers: { host }, method, url: path, socket: (stream.session as any).socket }; | ||||
|       const proxyConfig = this.router.routeReq(fakeReq as any); | ||||
|       if (!proxyConfig) { | ||||
|         stream.respond({ ':status': 404 }); | ||||
|         stream.end('Not Found'); | ||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|         return; | ||||
|       } | ||||
|       // Select backend target | ||||
|       const destination = this.connectionPool.getNextTarget( | ||||
|         proxyConfig.destinationIps, | ||||
|         proxyConfig.destinationPorts[0] | ||||
|       ); | ||||
|       // Build headers for HTTP/1 proxy | ||||
|       const outboundHeaders: Record<string,string> = {}; | ||||
|       for (const [key, value] of Object.entries(headers)) { | ||||
|         if (typeof key === 'string' && typeof value === 'string' && !key.startsWith(':')) { | ||||
|           outboundHeaders[key] = value; | ||||
|         } | ||||
|       } | ||||
|       if (outboundHeaders.host && (proxyConfig as any).rewriteHostHeader) { | ||||
|         outboundHeaders.host = `${destination.host}:${destination.port}`; | ||||
|       } | ||||
|       // Create HTTP/1 proxy request | ||||
|       const proxyReq = plugins.http.request( | ||||
|         { hostname: destination.host, port: destination.port, path, method, headers: outboundHeaders }, | ||||
|         (proxyRes) => { | ||||
|           // Map status and headers back to HTTP/2 | ||||
|           const responseHeaders: Record<string, number|string|string[]> = {}; | ||||
|           for (const [k, v] of Object.entries(proxyRes.headers)) { | ||||
|             if (v !== undefined) responseHeaders[k] = v; | ||||
|           } | ||||
|           stream.respond({ ':status': proxyRes.statusCode || 500, ...responseHeaders }); | ||||
|           proxyRes.pipe(stream); | ||||
|           stream.on('close', () => proxyReq.destroy()); | ||||
|           stream.on('error', () => proxyReq.destroy()); | ||||
|           if (this.metricsTracker) stream.on('end', () => this.metricsTracker.incrementRequestsServed()); | ||||
|         } | ||||
|       ); | ||||
|       proxyReq.on('error', (err) => { | ||||
|         stream.respond({ ':status': 502 }); | ||||
|         stream.end(`Bad Gateway: ${err.message}`); | ||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|       }); | ||||
|       // Pipe client stream to backend | ||||
|       stream.pipe(proxyReq); | ||||
|     } catch (err: any) { | ||||
|       stream.respond({ ':status': 500 }); | ||||
|       stream.end('Internal Server Error'); | ||||
|       if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -20,6 +20,8 @@ export interface INetworkProxyOptions { | ||||
|   connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend | ||||
|   portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy | ||||
|   useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler | ||||
|   // Protocol to use when proxying to backends: HTTP/1.x or HTTP/2 | ||||
|   backendProtocol?: 'http1' | 'http2'; | ||||
|    | ||||
|   // ACME certificate management options | ||||
|   acme?: { | ||||
|   | ||||
| @@ -5,9 +5,10 @@ import * as https from 'https'; | ||||
| import * as net from 'net'; | ||||
| import * as tls from 'tls'; | ||||
| import * as url from 'url'; | ||||
| import * as http2 from 'http2'; | ||||
|  | ||||
|  | ||||
| export { EventEmitter, http, https, net, tls, url }; | ||||
| export { EventEmitter, http, https, net, tls, url, http2 }; | ||||
|  | ||||
| // tsclass scope | ||||
| import * as tsclass from '@tsclass/tsclass'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user