fix(networkproxy): Refactor and improve WebSocket handling and request processing
This commit is contained in:
		| @@ -1,5 +1,11 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-02-04 - 3.1.3 - fix(networkproxy) | ||||||
|  | Refactor and improve WebSocket handling and request processing | ||||||
|  |  | ||||||
|  | - Improved error handling in WebSocket connection and request processing. | ||||||
|  | - Refactored the WebSocket handling in NetworkProxy to use a unified error logging mechanism. | ||||||
|  |  | ||||||
| ## 2025-02-04 - 3.1.2 - fix(core) | ## 2025-02-04 - 3.1.2 - fix(core) | ||||||
| Refactor certificate handling across the project | Refactor certificate handling across the project | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										133
									
								
								test/test.ts
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								test/test.ts
									
									
									
									
									
								
							| @@ -14,19 +14,34 @@ let testCertificates: { privateKey: string; publicKey: string }; | |||||||
| async function makeHttpsRequest( | async function makeHttpsRequest( | ||||||
|   options: https.RequestOptions, |   options: https.RequestOptions, | ||||||
| ): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders; body: string }> { | ): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders; body: string }> { | ||||||
|  |   console.log('[TEST] Making HTTPS request:', { | ||||||
|  |     hostname: options.hostname, | ||||||
|  |     port: options.port, | ||||||
|  |     path: options.path, | ||||||
|  |     method: options.method, | ||||||
|  |     headers: options.headers, | ||||||
|  |   }); | ||||||
|   return new Promise((resolve, reject) => { |   return new Promise((resolve, reject) => { | ||||||
|     const req = https.request(options, (res) => { |     const req = https.request(options, (res) => { | ||||||
|  |       console.log('[TEST] Received HTTPS response:', { | ||||||
|  |         statusCode: res.statusCode, | ||||||
|  |         headers: res.headers, | ||||||
|  |       }); | ||||||
|       let data = ''; |       let data = ''; | ||||||
|       res.on('data', (chunk) => (data += chunk)); |       res.on('data', (chunk) => (data += chunk)); | ||||||
|       res.on('end', () => |       res.on('end', () => { | ||||||
|  |         console.log('[TEST] Response completed:', { data }); | ||||||
|         resolve({ |         resolve({ | ||||||
|           statusCode: res.statusCode!, |           statusCode: res.statusCode!, | ||||||
|           headers: res.headers, |           headers: res.headers, | ||||||
|           body: data, |           body: data, | ||||||
|         }), |         }); | ||||||
|       ); |       }); | ||||||
|  |     }); | ||||||
|  |     req.on('error', (error) => { | ||||||
|  |       console.error('[TEST] Request error:', error); | ||||||
|  |       reject(error); | ||||||
|     }); |     }); | ||||||
|     req.on('error', reject); |  | ||||||
|     req.end(); |     req.end(); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| @@ -37,12 +52,13 @@ tap.test('setup test environment', async () => { | |||||||
|   console.log('[TEST] Loading and validating certificates'); |   console.log('[TEST] Loading and validating certificates'); | ||||||
|   testCertificates = loadTestCertificates(); |   testCertificates = loadTestCertificates(); | ||||||
|   console.log('[TEST] Certificates loaded and validated'); |   console.log('[TEST] Certificates loaded and validated'); | ||||||
|  |  | ||||||
|   // Create a test HTTP server |   // Create a test HTTP server | ||||||
|   testServer = http.createServer((req, res) => { |   testServer = http.createServer((req, res) => { | ||||||
|     console.log('[TEST SERVER] Received HTTP request:', { |     console.log('[TEST SERVER] Received HTTP request:', { | ||||||
|       url: req.url, |       url: req.url, | ||||||
|       method: req.method, |       method: req.method, | ||||||
|       headers: req.headers |       headers: req.headers, | ||||||
|     }); |     }); | ||||||
|     res.writeHead(200, { 'Content-Type': 'text/plain' }); |     res.writeHead(200, { 'Content-Type': 'text/plain' }); | ||||||
|     res.end('Hello from test server!'); |     res.end('Hello from test server!'); | ||||||
| @@ -59,8 +75,8 @@ tap.test('setup test environment', async () => { | |||||||
|         connection: request.headers.connection, |         connection: request.headers.connection, | ||||||
|         'sec-websocket-key': request.headers['sec-websocket-key'], |         'sec-websocket-key': request.headers['sec-websocket-key'], | ||||||
|         'sec-websocket-version': request.headers['sec-websocket-version'], |         'sec-websocket-version': request.headers['sec-websocket-version'], | ||||||
|         'sec-websocket-protocol': request.headers['sec-websocket-protocol'] |         'sec-websocket-protocol': request.headers['sec-websocket-protocol'], | ||||||
|       } |       }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (request.headers.upgrade?.toLowerCase() !== 'websocket') { |     if (request.headers.upgrade?.toLowerCase() !== 'websocket') { | ||||||
| @@ -76,13 +92,13 @@ tap.test('setup test environment', async () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // Create a WebSocket server |   // Create a WebSocket server (for the test HTTP server) | ||||||
|   console.log('[TEST SERVER] Creating WebSocket server'); |   console.log('[TEST SERVER] Creating WebSocket server'); | ||||||
|   wsServer = new WebSocketServer({ |   wsServer = new WebSocketServer({ | ||||||
|     noServer: true, |     noServer: true, | ||||||
|     perMessageDeflate: false, |     perMessageDeflate: false, | ||||||
|     clientTracking: true, |     clientTracking: true, | ||||||
|     handleProtocols: () => 'echo-protocol' |     handleProtocols: () => 'echo-protocol', | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   wsServer.on('connection', (ws, request) => { |   wsServer.on('connection', (ws, request) => { | ||||||
| @@ -94,8 +110,8 @@ tap.test('setup test environment', async () => { | |||||||
|         connection: request.headers.connection, |         connection: request.headers.connection, | ||||||
|         'sec-websocket-key': request.headers['sec-websocket-key'], |         'sec-websocket-key': request.headers['sec-websocket-key'], | ||||||
|         'sec-websocket-version': request.headers['sec-websocket-version'], |         'sec-websocket-version': request.headers['sec-websocket-version'], | ||||||
|         'sec-websocket-protocol': request.headers['sec-websocket-protocol'] |         'sec-websocket-protocol': request.headers['sec-websocket-protocol'], | ||||||
|       } |       }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // Set up connection timeout |     // Set up connection timeout | ||||||
| @@ -132,7 +148,7 @@ tap.test('setup test environment', async () => { | |||||||
|       console.log('[TEST SERVER] WebSocket connection closed:', { |       console.log('[TEST SERVER] WebSocket connection closed:', { | ||||||
|         code, |         code, | ||||||
|         reason: reason.toString(), |         reason: reason.toString(), | ||||||
|         wasClean: code === 1000 || code === 1001 |         wasClean: code === 1000 || code === 1001, | ||||||
|       }); |       }); | ||||||
|       clearConnectionTimeout(); |       clearConnectionTimeout(); | ||||||
|     }); |     }); | ||||||
| @@ -175,30 +191,42 @@ tap.test('should create proxy instance', async () => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should start the proxy server', async () => { | tap.test('should start the proxy server', async () => { | ||||||
|  |   // Ensure any previous server is closed | ||||||
|  |   if (testProxy && testProxy.httpsServer) { | ||||||
|  |     await new Promise<void>((resolve) => | ||||||
|  |       testProxy.httpsServer.close(() => resolve()) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   console.log('[TEST] Starting the proxy server'); |   console.log('[TEST] Starting the proxy server'); | ||||||
|   await testProxy.start(); |   await testProxy.start(); | ||||||
|   console.log('[TEST] Proxy server started'); |   console.log('[TEST] Proxy server started'); | ||||||
|  |  | ||||||
|   // Configure proxy with test certificates |   // Configure proxy with test certificates | ||||||
|   testProxy.updateProxyConfigs([ |   // Awaiting the update ensures that the SNI context is added before any requests come in. | ||||||
|  |   await testProxy.updateProxyConfigs([ | ||||||
|     { |     { | ||||||
|       destinationIp: '127.0.0.1', |       destinationIp: '127.0.0.1', | ||||||
|       destinationPort: '3000', |       destinationPort: '3000', | ||||||
|       hostName: 'push.rocks', |       hostName: 'push.rocks', | ||||||
|       publicKey: testCertificates.publicKey, |       publicKey: testCertificates.publicKey, | ||||||
|       privateKey: testCertificates.privateKey, |       privateKey: testCertificates.privateKey, | ||||||
|     } |     }, | ||||||
|   ]); |   ]); | ||||||
|  |  | ||||||
|   console.log('[TEST] Proxy configuration updated'); |   console.log('[TEST] Proxy configuration updated'); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should route HTTPS requests based on host header', async () => { | tap.test('should route HTTPS requests based on host header', async () => { | ||||||
|  |   // IMPORTANT: Connect to localhost (where the proxy is listening) but use the Host header "push.rocks" | ||||||
|   const response = await makeHttpsRequest({ |   const response = await makeHttpsRequest({ | ||||||
|     hostname: 'push.rocks', |     hostname: 'localhost', // changed from 'push.rocks' to 'localhost' | ||||||
|     port: 3001, |     port: 3001, | ||||||
|     path: '/', |     path: '/', | ||||||
|     method: 'GET', |     method: 'GET', | ||||||
|  |     headers: { | ||||||
|  |       host: 'push.rocks', // virtual host for routing | ||||||
|  |     }, | ||||||
|     rejectUnauthorized: false, |     rejectUnauthorized: false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -207,15 +235,21 @@ tap.test('should route HTTPS requests based on host header', async () => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should handle unknown host headers', async () => { | tap.test('should handle unknown host headers', async () => { | ||||||
|  |   // Connect to localhost but use an unknown host header. | ||||||
|   const response = await makeHttpsRequest({ |   const response = await makeHttpsRequest({ | ||||||
|     hostname: 'unknown.host', |     hostname: 'localhost', // connecting to localhost | ||||||
|     port: 3001, |     port: 3001, | ||||||
|     path: '/', |     path: '/', | ||||||
|     method: 'GET', |     method: 'GET', | ||||||
|  |     headers: { | ||||||
|  |       host: 'unknown.host', // this should not match any proxy config | ||||||
|  |     }, | ||||||
|     rejectUnauthorized: false, |     rejectUnauthorized: false, | ||||||
|   }).catch((e) => e); |   }); | ||||||
|  |  | ||||||
|   expect(response instanceof Error).toEqual(true); |   // Expect a 404 response with the appropriate error message. | ||||||
|  |   expect(response.statusCode).toEqual(404); | ||||||
|  |   expect(response.body).toEqual('This route is not available on this server.'); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should support WebSocket connections', async () => { | tap.test('should support WebSocket connections', async () => { | ||||||
| @@ -224,39 +258,38 @@ tap.test('should support WebSocket connections', async () => { | |||||||
|   console.log('[TEST] Proxy server port:', 3001); |   console.log('[TEST] Proxy server port:', 3001); | ||||||
|   console.log('\n[TEST] Starting WebSocket test'); |   console.log('\n[TEST] Starting WebSocket test'); | ||||||
|  |  | ||||||
|   // First configure the proxy with test certificates |   // Reconfigure proxy with test certificates if necessary | ||||||
|   console.log('[TEST] Configuring proxy with test certificates'); |   await testProxy.updateProxyConfigs([ | ||||||
|   testProxy.updateProxyConfigs([ |  | ||||||
|     { |     { | ||||||
|       destinationIp: '127.0.0.1', |       destinationIp: '127.0.0.1', | ||||||
|       destinationPort: '3000', |       destinationPort: '3000', | ||||||
|       hostName: 'push.rocks', |       hostName: 'push.rocks', | ||||||
|       publicKey: testCertificates.publicKey, |       publicKey: testCertificates.publicKey, | ||||||
|       privateKey: testCertificates.privateKey, |       privateKey: testCertificates.privateKey, | ||||||
|     } |     }, | ||||||
|   ]); |   ]); | ||||||
|  |  | ||||||
|   return new Promise<void>((resolve, reject) => { |   return new Promise<void>((resolve, reject) => { | ||||||
|     console.log('[TEST] Creating WebSocket client'); |     console.log('[TEST] Creating WebSocket client'); | ||||||
|  |  | ||||||
|     // Create WebSocket client with SSL/TLS options |     // IMPORTANT: Connect to localhost but specify the SNI servername and Host header as "push.rocks" | ||||||
|     const wsUrl = 'wss://push.rocks:3001'; |     const wsUrl = 'wss://localhost:3001'; // changed from 'wss://push.rocks:3001' | ||||||
|     console.log('[TEST] Creating WebSocket connection to:', wsUrl); |     console.log('[TEST] Creating WebSocket connection to:', wsUrl); | ||||||
|      |  | ||||||
|     const ws = new WebSocket(wsUrl, { |     const ws = new WebSocket(wsUrl, { | ||||||
|       rejectUnauthorized: false, // Accept self-signed certificates |       rejectUnauthorized: false, // Accept self-signed certificates | ||||||
|       handshakeTimeout: 5000, |       handshakeTimeout: 5000, | ||||||
|       perMessageDeflate: false, |       perMessageDeflate: false, | ||||||
|       headers: { |       headers: { | ||||||
|         'Host': 'push.rocks', |         Host: 'push.rocks', // required for SNI and routing on the proxy | ||||||
|         'Connection': 'Upgrade', |         Connection: 'Upgrade', | ||||||
|         'Upgrade': 'websocket', |         Upgrade: 'websocket', | ||||||
|         'Sec-WebSocket-Version': '13' |         'Sec-WebSocket-Version': '13', | ||||||
|       }, |       }, | ||||||
|       protocol: 'echo-protocol', |       protocol: 'echo-protocol', | ||||||
|       agent: new https.Agent({ |       agent: new https.Agent({ | ||||||
|         rejectUnauthorized: false // Also needed for the underlying HTTPS connection |         rejectUnauthorized: false, // Also needed for the underlying HTTPS connection | ||||||
|       }) |       }), | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     console.log('[TEST] WebSocket client created'); |     console.log('[TEST] WebSocket client created'); | ||||||
| @@ -286,7 +319,7 @@ tap.test('should support WebSocket connections', async () => { | |||||||
|     ws.on('upgrade', (response) => { |     ws.on('upgrade', (response) => { | ||||||
|       console.log('[TEST] WebSocket upgrade response received:', { |       console.log('[TEST] WebSocket upgrade response received:', { | ||||||
|         headers: response.headers, |         headers: response.headers, | ||||||
|         statusCode: response.statusCode |         statusCode: response.statusCode, | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
| @@ -304,7 +337,10 @@ tap.test('should support WebSocket connections', async () => { | |||||||
|  |  | ||||||
|     ws.on('message', (message) => { |     ws.on('message', (message) => { | ||||||
|       console.log('[TEST] Received message:', message.toString()); |       console.log('[TEST] Received message:', message.toString()); | ||||||
|       if (message.toString() === 'Hello WebSocket') { |       if ( | ||||||
|  |         message.toString() === 'Hello WebSocket' || | ||||||
|  |         message.toString() === 'Echo: Hello WebSocket' | ||||||
|  |       ) { | ||||||
|         console.log('[TEST] Message received correctly'); |         console.log('[TEST] Message received correctly'); | ||||||
|         clearTimeout(timeout); |         clearTimeout(timeout); | ||||||
|         cleanup(); |         cleanup(); | ||||||
| @@ -320,7 +356,7 @@ tap.test('should support WebSocket connections', async () => { | |||||||
|     ws.on('close', (code, reason) => { |     ws.on('close', (code, reason) => { | ||||||
|       console.log('[TEST] WebSocket connection closed:', { |       console.log('[TEST] WebSocket connection closed:', { | ||||||
|         code, |         code, | ||||||
|         reason: reason.toString() |         reason: reason.toString(), | ||||||
|       }); |       }); | ||||||
|       cleanup(); |       cleanup(); | ||||||
|     }); |     }); | ||||||
| @@ -328,15 +364,18 @@ tap.test('should support WebSocket connections', async () => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.test('should handle custom headers', async () => { | tap.test('should handle custom headers', async () => { | ||||||
|   testProxy.addDefaultHeaders({ |   await testProxy.addDefaultHeaders({ | ||||||
|     'X-Proxy-Header': 'test-value', |     'X-Proxy-Header': 'test-value', | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const response = await makeHttpsRequest({ |   const response = await makeHttpsRequest({ | ||||||
|     hostname: 'push.rocks', |     hostname: 'localhost', // changed to 'localhost' | ||||||
|     port: 3001, |     port: 3001, | ||||||
|     path: '/', |     path: '/', | ||||||
|     method: 'GET', |     method: 'GET', | ||||||
|  |     headers: { | ||||||
|  |       host: 'push.rocks', // still routing to push.rocks | ||||||
|  |     }, | ||||||
|     rejectUnauthorized: false, |     rejectUnauthorized: false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -353,16 +392,20 @@ tap.test('cleanup', async () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   console.log('[TEST] Closing WebSocket server'); |   console.log('[TEST] Closing WebSocket server'); | ||||||
|   await new Promise<void>((resolve) => wsServer.close(() => { |   await new Promise<void>((resolve) => | ||||||
|     console.log('[TEST] WebSocket server closed'); |     wsServer.close(() => { | ||||||
|     resolve(); |       console.log('[TEST] WebSocket server closed'); | ||||||
|   })); |       resolve(); | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   console.log('[TEST] Closing test server'); |   console.log('[TEST] Closing test server'); | ||||||
|   await new Promise<void>((resolve) => testServer.close(() => { |   await new Promise<void>((resolve) => | ||||||
|     console.log('[TEST] Test server closed'); |     testServer.close(() => { | ||||||
|     resolve(); |       console.log('[TEST] Test server closed'); | ||||||
|   })); |       resolve(); | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   console.log('[TEST] Stopping proxy'); |   console.log('[TEST] Stopping proxy'); | ||||||
|   await testProxy.stop(); |   await testProxy.stop(); | ||||||
| @@ -376,4 +419,4 @@ process.on('exit', () => { | |||||||
|   testProxy.stop().then(() => console.log('[TEST] Proxy server stopped')); |   testProxy.stop().then(() => console.log('[TEST] Proxy server stopped')); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| tap.start(); | tap.start(); | ||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   name: '@push.rocks/smartproxy', | ||||||
|   version: '3.1.2', |   version: '3.1.3', | ||||||
|   description: 'a proxy for handling high workloads of proxying' |   description: 'a proxy for handling high workloads of proxying' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,12 +8,11 @@ export interface INetworkProxyOptions { | |||||||
|   port: number; |   port: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface WebSocketWithHeartbeat extends plugins.wsDefault { | interface IWebSocketWithHeartbeat extends plugins.wsDefault { | ||||||
|   lastPong: number; |   lastPong: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class NetworkProxy { | export class NetworkProxy { | ||||||
|   // INSTANCE |  | ||||||
|   public options: INetworkProxyOptions; |   public options: INetworkProxyOptions; | ||||||
|   public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; |   public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; | ||||||
|   public httpsServer: plugins.https.Server; |   public httpsServer: plugins.https.Server; | ||||||
| @@ -43,148 +42,23 @@ export class NetworkProxy { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * starts the proxyInstance |  | ||||||
|    */ |  | ||||||
|   public async start() { |   public async start() { | ||||||
|  |     // Instead of marking the callback async (which Node won't await), | ||||||
|  |     // we call our async handler and catch errors. | ||||||
|     this.httpsServer = plugins.https.createServer( |     this.httpsServer = plugins.https.createServer( | ||||||
|       { |       { | ||||||
|         key: this.defaultCertificates.key, |         key: this.defaultCertificates.key, | ||||||
|         cert: this.defaultCertificates.cert |         cert: this.defaultCertificates.cert | ||||||
|       }, |       }, | ||||||
|       async (originRequest, originResponse) => { |       (originRequest, originResponse) => { | ||||||
|         /** |         this.handleRequest(originRequest, originResponse).catch((error) => { | ||||||
|          * endRequest function |           console.error('Unhandled error in request handler:', error); | ||||||
|          * can be used to prematurely end a request |           try { | ||||||
|          */ |  | ||||||
|         const endOriginReqRes = ( |  | ||||||
|           statusArg: number = 404, |  | ||||||
|           messageArg: string = 'This route is not available on this server.', |  | ||||||
|           headers: plugins.http.OutgoingHttpHeaders = {}, |  | ||||||
|         ) => { |  | ||||||
|           originResponse.writeHead(statusArg, messageArg); |  | ||||||
|           originResponse.end(messageArg); |  | ||||||
|           if (originRequest.socket !== originResponse.socket) { |  | ||||||
|             console.log('hey, something is strange.'); |  | ||||||
|           } |  | ||||||
|           originResponse.destroy(); |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         console.log( |  | ||||||
|           `got request: ${originRequest.headers.host}${plugins.url.parse(originRequest.url).path}`, |  | ||||||
|         ); |  | ||||||
|         const destinationConfig = this.router.routeReq(originRequest); |  | ||||||
|  |  | ||||||
|         if (!destinationConfig) { |  | ||||||
|           console.log( |  | ||||||
|             `${originRequest.headers.host} can't be routed properly. Terminating request.`, |  | ||||||
|           ); |  | ||||||
|           endOriginReqRes(); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // authentication |  | ||||||
|         if (destinationConfig.authentication) { |  | ||||||
|           const authInfo = destinationConfig.authentication; |  | ||||||
|           switch (authInfo.type) { |  | ||||||
|             case 'Basic': |  | ||||||
|               const authHeader = originRequest.headers.authorization; |  | ||||||
|               if (authHeader) { |  | ||||||
|                 if (!authHeader.includes('Basic ')) { |  | ||||||
|                   return endOriginReqRes(401, 'Authentication required', { |  | ||||||
|                     'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"', |  | ||||||
|                   }); |  | ||||||
|                 } |  | ||||||
|                 const authStringBase64 = originRequest.headers.authorization.replace('Basic ', ''); |  | ||||||
|                 const authString: string = plugins.smartstring.base64.decode(authStringBase64); |  | ||||||
|                 const userPassArray = authString.split(':'); |  | ||||||
|                 const user = userPassArray[0]; |  | ||||||
|                 const pass = userPassArray[1]; |  | ||||||
|                 if (user === authInfo.user && pass === authInfo.pass) { |  | ||||||
|                   console.log('request successfully authenticated'); |  | ||||||
|                 } else { |  | ||||||
|                   return endOriginReqRes(403, 'Forbidden: Wrong credentials'); |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|               break; |  | ||||||
|             default: |  | ||||||
|               return endOriginReqRes( |  | ||||||
|                 403, |  | ||||||
|                 'Forbidden: unsupported authentication method configured. Please report to the admin.', |  | ||||||
|               ); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         let destinationUrl: string; |  | ||||||
|         if (destinationConfig) { |  | ||||||
|           destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`; |  | ||||||
|         } else { |  | ||||||
|           return endOriginReqRes(); |  | ||||||
|         } |  | ||||||
|         console.log(destinationUrl); |  | ||||||
|         try { |  | ||||||
|           const proxyResponse = await plugins.smartrequest.request( |  | ||||||
|             destinationUrl, |  | ||||||
|             { |  | ||||||
|               method: originRequest.method, |  | ||||||
|               headers: { |  | ||||||
|                 ...originRequest.headers, |  | ||||||
|                 'X-Forwarded-Host': originRequest.headers.host, |  | ||||||
|                 'X-Forwarded-Proto': 'https', |  | ||||||
|               }, |  | ||||||
|               keepAlive: true, |  | ||||||
|             }, |  | ||||||
|             true, // lets make this streaming (keepAlive) |  | ||||||
|             (proxyRequest) => { |  | ||||||
|               originRequest.on('data', (data) => { |  | ||||||
|                 proxyRequest.write(data); |  | ||||||
|               }); |  | ||||||
|               originRequest.on('end', () => { |  | ||||||
|                 proxyRequest.end(); |  | ||||||
|               }); |  | ||||||
|               originRequest.on('error', () => { |  | ||||||
|                 proxyRequest.end(); |  | ||||||
|               }); |  | ||||||
|               originRequest.on('close', () => { |  | ||||||
|                 proxyRequest.end(); |  | ||||||
|               }); |  | ||||||
|               originRequest.on('timeout', () => { |  | ||||||
|                 proxyRequest.end(); |  | ||||||
|                 originRequest.destroy(); |  | ||||||
|               }); |  | ||||||
|               proxyRequest.on('error', () => { |  | ||||||
|                 endOriginReqRes(); |  | ||||||
|               }); |  | ||||||
|             }, |  | ||||||
|           ); |  | ||||||
|           originResponse.statusCode = proxyResponse.statusCode; |  | ||||||
|           console.log(proxyResponse.statusCode); |  | ||||||
|           for (const defaultHeader of Object.keys(this.defaultHeaders)) { |  | ||||||
|             originResponse.setHeader(defaultHeader, this.defaultHeaders[defaultHeader]); |  | ||||||
|           } |  | ||||||
|           for (const header of Object.keys(proxyResponse.headers)) { |  | ||||||
|             originResponse.setHeader(header, proxyResponse.headers[header]); |  | ||||||
|           } |  | ||||||
|           proxyResponse.on('data', (data) => { |  | ||||||
|             originResponse.write(data); |  | ||||||
|           }); |  | ||||||
|           proxyResponse.on('end', () => { |  | ||||||
|             originResponse.end(); |             originResponse.end(); | ||||||
|           }); |           } catch (err) { | ||||||
|           proxyResponse.on('error', () => { |             // ignore errors during cleanup | ||||||
|             originResponse.destroy(); |           } | ||||||
|           }); |         }); | ||||||
|           proxyResponse.on('close', () => { |  | ||||||
|             originResponse.end(); |  | ||||||
|           }); |  | ||||||
|           proxyResponse.on('timeout', () => { |  | ||||||
|             originResponse.end(); |  | ||||||
|             originResponse.destroy(); |  | ||||||
|           }); |  | ||||||
|         } catch (error) { |  | ||||||
|           console.error('Error while processing request:', error); |  | ||||||
|           endOriginReqRes(502, 'Bad Gateway: Error processing the request'); |  | ||||||
|         } |  | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -194,7 +68,7 @@ export class NetworkProxy { | |||||||
|     // Set up the heartbeat interval |     // Set up the heartbeat interval | ||||||
|     this.heartbeatInterval = setInterval(() => { |     this.heartbeatInterval = setInterval(() => { | ||||||
|       wsServer.clients.forEach((ws: plugins.wsDefault) => { |       wsServer.clients.forEach((ws: plugins.wsDefault) => { | ||||||
|         const wsIncoming = ws as WebSocketWithHeartbeat; |         const wsIncoming = ws as IWebSocketWithHeartbeat; | ||||||
|         if (!wsIncoming.lastPong) { |         if (!wsIncoming.lastPong) { | ||||||
|           wsIncoming.lastPong = Date.now(); |           wsIncoming.lastPong = Date.now(); | ||||||
|         } |         } | ||||||
| @@ -209,7 +83,7 @@ export class NetworkProxy { | |||||||
|  |  | ||||||
|     wsServer.on( |     wsServer.on( | ||||||
|       'connection', |       'connection', | ||||||
|       async (wsIncoming: WebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => { |       (wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => { | ||||||
|         console.log( |         console.log( | ||||||
|           `wss proxy: got connection for wsc for https://${reqArg.headers.host}${reqArg.url}`, |           `wss proxy: got connection for wsc for https://${reqArg.headers.host}${reqArg.url}`, | ||||||
|         ); |         ); | ||||||
| @@ -220,21 +94,24 @@ export class NetworkProxy { | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         let wsOutgoing: plugins.wsDefault; |         let wsOutgoing: plugins.wsDefault; | ||||||
|  |  | ||||||
|         const outGoingDeferred = plugins.smartpromise.defer(); |         const outGoingDeferred = plugins.smartpromise.defer(); | ||||||
|  |  | ||||||
|  |         // --- Improvement 2: Only call routeReq once --- | ||||||
|  |         const wsDestinationConfig = this.router.routeReq(reqArg); | ||||||
|  |         if (!wsDestinationConfig) { | ||||||
|  |           wsIncoming.terminate(); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|         try { |         try { | ||||||
|           wsOutgoing = new plugins.wsDefault( |           wsOutgoing = new plugins.wsDefault( | ||||||
|             `ws://${this.router.routeReq(reqArg).destinationIp}:${ |             `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`, | ||||||
|               this.router.routeReq(reqArg).destinationPort |  | ||||||
|             }${reqArg.url}`, |  | ||||||
|           ); |           ); | ||||||
|           console.log('wss proxy: initiated outgoing proxy'); |           console.log('wss proxy: initiated outgoing proxy'); | ||||||
|           wsOutgoing.on('open', async () => { |           wsOutgoing.on('open', async () => { | ||||||
|             outGoingDeferred.resolve(); |             outGoingDeferred.resolve(); | ||||||
|           }); |           }); | ||||||
|         } catch (err) { |         } catch (err) { | ||||||
|           console.log(err); |           console.error('Error initiating outgoing WebSocket:', err); | ||||||
|           wsIncoming.terminate(); |           wsIncoming.terminate(); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
| @@ -259,20 +136,20 @@ export class NetworkProxy { | |||||||
|         const terminateWsOutgoing = () => { |         const terminateWsOutgoing = () => { | ||||||
|           if (wsOutgoing) { |           if (wsOutgoing) { | ||||||
|             wsOutgoing.terminate(); |             wsOutgoing.terminate(); | ||||||
|             console.log('terminated outgoing ws.'); |             console.log('Terminated outgoing ws.'); | ||||||
|           } |           } | ||||||
|         }; |         }; | ||||||
|         wsIncoming.on('error', () => terminateWsOutgoing()); |         wsIncoming.on('error', terminateWsOutgoing); | ||||||
|         wsIncoming.on('close', () => terminateWsOutgoing()); |         wsIncoming.on('close', terminateWsOutgoing); | ||||||
|  |  | ||||||
|         const terminateWsIncoming = () => { |         const terminateWsIncoming = () => { | ||||||
|           if (wsIncoming) { |           if (wsIncoming) { | ||||||
|             wsIncoming.terminate(); |             wsIncoming.terminate(); | ||||||
|             console.log('terminated incoming ws.'); |             console.log('Terminated incoming ws.'); | ||||||
|           } |           } | ||||||
|         }; |         }; | ||||||
|         wsOutgoing.on('error', () => terminateWsIncoming()); |         wsOutgoing.on('error', terminateWsIncoming); | ||||||
|         wsOutgoing.on('close', () => terminateWsIncoming()); |         wsOutgoing.on('close', terminateWsIncoming); | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -281,26 +158,18 @@ export class NetworkProxy { | |||||||
|  |  | ||||||
|     this.httpsServer.on('connection', (connection: plugins.net.Socket) => { |     this.httpsServer.on('connection', (connection: plugins.net.Socket) => { | ||||||
|       this.socketMap.add(connection); |       this.socketMap.add(connection); | ||||||
|       console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`); |       console.log(`Added connection. Now ${this.socketMap.getArray().length} sockets connected.`); | ||||||
|       const cleanupConnection = () => { |       const cleanupConnection = () => { | ||||||
|         if (this.socketMap.checkForObject(connection)) { |         if (this.socketMap.checkForObject(connection)) { | ||||||
|           this.socketMap.remove(connection); |           this.socketMap.remove(connection); | ||||||
|           console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`); |           console.log(`Removed connection. ${this.socketMap.getArray().length} sockets remaining.`); | ||||||
|           connection.destroy(); |           connection.destroy(); | ||||||
|         } |         } | ||||||
|       }; |       }; | ||||||
|       connection.on('close', () => { |       connection.on('close', cleanupConnection); | ||||||
|         cleanupConnection(); |       connection.on('error', cleanupConnection); | ||||||
|       }); |       connection.on('end', cleanupConnection); | ||||||
|       connection.on('error', () => { |       connection.on('timeout', cleanupConnection); | ||||||
|         cleanupConnection(); |  | ||||||
|       }); |  | ||||||
|       connection.on('end', () => { |  | ||||||
|         cleanupConnection(); |  | ||||||
|       }); |  | ||||||
|       connection.on('timeout', () => { |  | ||||||
|         cleanupConnection(); |  | ||||||
|       }); |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     this.httpsServer.listen(this.options.port); |     this.httpsServer.listen(this.options.port); | ||||||
| @@ -309,7 +178,150 @@ export class NetworkProxy { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async updateProxyConfigs(proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]) { |   /** | ||||||
|  |    * Internal async handler for processing HTTP/HTTPS requests. | ||||||
|  |    */ | ||||||
|  |   private async handleRequest( | ||||||
|  |     originRequest: plugins.http.IncomingMessage, | ||||||
|  |     originResponse: plugins.http.ServerResponse, | ||||||
|  |   ): Promise<void> { | ||||||
|  |     const endOriginReqRes = ( | ||||||
|  |       statusArg: number = 404, | ||||||
|  |       messageArg: string = 'This route is not available on this server.', | ||||||
|  |       headers: plugins.http.OutgoingHttpHeaders = {}, | ||||||
|  |     ) => { | ||||||
|  |       originResponse.writeHead(statusArg, messageArg); | ||||||
|  |       originResponse.end(messageArg); | ||||||
|  |       if (originRequest.socket !== originResponse.socket) { | ||||||
|  |         console.log('hey, something is strange.'); | ||||||
|  |       } | ||||||
|  |       originResponse.destroy(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     console.log( | ||||||
|  |       `got request: ${originRequest.headers.host}${plugins.url.parse(originRequest.url).path}`, | ||||||
|  |     ); | ||||||
|  |     const destinationConfig = this.router.routeReq(originRequest); | ||||||
|  |  | ||||||
|  |     if (!destinationConfig) { | ||||||
|  |       console.log( | ||||||
|  |         `${originRequest.headers.host} can't be routed properly. Terminating request.`, | ||||||
|  |       ); | ||||||
|  |       endOriginReqRes(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // authentication | ||||||
|  |     if (destinationConfig.authentication) { | ||||||
|  |       const authInfo = destinationConfig.authentication; | ||||||
|  |       switch (authInfo.type) { | ||||||
|  |         case 'Basic': { | ||||||
|  |           const authHeader = originRequest.headers.authorization; | ||||||
|  |           if (!authHeader) { | ||||||
|  |             return endOriginReqRes(401, 'Authentication required', { | ||||||
|  |               'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"', | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |           if (!authHeader.includes('Basic ')) { | ||||||
|  |             return endOriginReqRes(401, 'Authentication required', { | ||||||
|  |               'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"', | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |           const authStringBase64 = authHeader.replace('Basic ', ''); | ||||||
|  |           const authString: string = plugins.smartstring.base64.decode(authStringBase64); | ||||||
|  |           const userPassArray = authString.split(':'); | ||||||
|  |           const user = userPassArray[0]; | ||||||
|  |           const pass = userPassArray[1]; | ||||||
|  |           if (user === authInfo.user && pass === authInfo.pass) { | ||||||
|  |             console.log('Request successfully authenticated'); | ||||||
|  |           } else { | ||||||
|  |             return endOriginReqRes(403, 'Forbidden: Wrong credentials'); | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         default: | ||||||
|  |           return endOriginReqRes( | ||||||
|  |             403, | ||||||
|  |             'Forbidden: unsupported authentication method configured. Please report to the admin.', | ||||||
|  |           ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let destinationUrl: string; | ||||||
|  |     if (destinationConfig) { | ||||||
|  |       destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`; | ||||||
|  |     } else { | ||||||
|  |       return endOriginReqRes(); | ||||||
|  |     } | ||||||
|  |     console.log(destinationUrl); | ||||||
|  |     try { | ||||||
|  |       const proxyResponse = await plugins.smartrequest.request( | ||||||
|  |         destinationUrl, | ||||||
|  |         { | ||||||
|  |           method: originRequest.method, | ||||||
|  |           headers: { | ||||||
|  |             ...originRequest.headers, | ||||||
|  |             'X-Forwarded-Host': originRequest.headers.host, | ||||||
|  |             'X-Forwarded-Proto': 'https', | ||||||
|  |           }, | ||||||
|  |           keepAlive: true, | ||||||
|  |         }, | ||||||
|  |         true, // streaming (keepAlive) | ||||||
|  |         (proxyRequest) => { | ||||||
|  |           originRequest.on('data', (data) => { | ||||||
|  |             proxyRequest.write(data); | ||||||
|  |           }); | ||||||
|  |           originRequest.on('end', () => { | ||||||
|  |             proxyRequest.end(); | ||||||
|  |           }); | ||||||
|  |           originRequest.on('error', () => { | ||||||
|  |             proxyRequest.end(); | ||||||
|  |           }); | ||||||
|  |           originRequest.on('close', () => { | ||||||
|  |             proxyRequest.end(); | ||||||
|  |           }); | ||||||
|  |           originRequest.on('timeout', () => { | ||||||
|  |             proxyRequest.end(); | ||||||
|  |             originRequest.destroy(); | ||||||
|  |           }); | ||||||
|  |           proxyRequest.on('error', () => { | ||||||
|  |             endOriginReqRes(); | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |       originResponse.statusCode = proxyResponse.statusCode; | ||||||
|  |       console.log(proxyResponse.statusCode); | ||||||
|  |       for (const defaultHeader of Object.keys(this.defaultHeaders)) { | ||||||
|  |         originResponse.setHeader(defaultHeader, this.defaultHeaders[defaultHeader]); | ||||||
|  |       } | ||||||
|  |       for (const header of Object.keys(proxyResponse.headers)) { | ||||||
|  |         originResponse.setHeader(header, proxyResponse.headers[header]); | ||||||
|  |       } | ||||||
|  |       proxyResponse.on('data', (data) => { | ||||||
|  |         originResponse.write(data); | ||||||
|  |       }); | ||||||
|  |       proxyResponse.on('end', () => { | ||||||
|  |         originResponse.end(); | ||||||
|  |       }); | ||||||
|  |       proxyResponse.on('error', () => { | ||||||
|  |         originResponse.destroy(); | ||||||
|  |       }); | ||||||
|  |       proxyResponse.on('close', () => { | ||||||
|  |         originResponse.end(); | ||||||
|  |       }); | ||||||
|  |       proxyResponse.on('timeout', () => { | ||||||
|  |         originResponse.end(); | ||||||
|  |         originResponse.destroy(); | ||||||
|  |       }); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error while processing request:', error); | ||||||
|  |       endOriginReqRes(502, 'Bad Gateway: Error processing the request'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async updateProxyConfigs( | ||||||
|  |     proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[], | ||||||
|  |   ) { | ||||||
|     console.log(`got new proxy configs`); |     console.log(`got new proxy configs`); | ||||||
|     this.proxyConfigs = proxyConfigsArg; |     this.proxyConfigs = proxyConfigsArg; | ||||||
|     this.router.setNewProxyConfigs(proxyConfigsArg); |     this.router.setNewProxyConfigs(proxyConfigsArg); | ||||||
| @@ -347,11 +359,11 @@ export class NetworkProxy { | |||||||
|     this.httpsServer.close(() => { |     this.httpsServer.close(() => { | ||||||
|       done.resolve(); |       done.resolve(); | ||||||
|     }); |     }); | ||||||
|     await this.socketMap.forEach(async (socket) => { |     for (const socket of this.socketMap.getArray()) { | ||||||
|       socket.destroy(); |       socket.destroy(); | ||||||
|     }); |     } | ||||||
|     await done.promise; |     await done.promise; | ||||||
|     clearInterval(this.heartbeatInterval); |     clearInterval(this.heartbeatInterval); | ||||||
|     console.log('NetworkProxy -> OK: Server has been stopped and all connections closed.'); |     console.log('NetworkProxy -> OK: Server has been stopped and all connections closed.'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user