update
This commit is contained in:
		
							
								
								
									
										341
									
								
								Connection-Cleanup-Patterns.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										341
									
								
								Connection-Cleanup-Patterns.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,341 @@ | ||||
| # Connection Cleanup Code Patterns | ||||
|  | ||||
| ## Pattern 1: Safe Connection Cleanup | ||||
|  | ||||
| ```typescript | ||||
| public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void { | ||||
|   // Prevent duplicate cleanup | ||||
|   if (record.incomingTerminationReason === null ||  | ||||
|       record.incomingTerminationReason === undefined) { | ||||
|     record.incomingTerminationReason = reason; | ||||
|     this.incrementTerminationStat('incoming', reason); | ||||
|   } | ||||
|    | ||||
|   this.cleanupConnection(record, reason); | ||||
| } | ||||
|  | ||||
| public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void { | ||||
|   if (!record.connectionClosed) { | ||||
|     record.connectionClosed = true; | ||||
|      | ||||
|     // Remove from tracking immediately | ||||
|     this.connectionRecords.delete(record.id); | ||||
|     this.securityManager.removeConnectionByIP(record.remoteIP, record.id); | ||||
|      | ||||
|     // Clear timers | ||||
|     if (record.cleanupTimer) { | ||||
|       clearTimeout(record.cleanupTimer); | ||||
|       record.cleanupTimer = undefined; | ||||
|     } | ||||
|      | ||||
|     // Clean up sockets | ||||
|     this.cleanupSocket(record, 'incoming', record.incoming); | ||||
|     if (record.outgoing) { | ||||
|       this.cleanupSocket(record, 'outgoing', record.outgoing); | ||||
|     } | ||||
|      | ||||
|     // Clear memory | ||||
|     record.pendingData = []; | ||||
|     record.pendingDataSize = 0; | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 2: Socket Cleanup with Retry | ||||
|  | ||||
| ```typescript | ||||
| private cleanupSocket( | ||||
|   record: IConnectionRecord,  | ||||
|   side: 'incoming' | 'outgoing',  | ||||
|   socket: plugins.net.Socket | ||||
| ): void { | ||||
|   try { | ||||
|     if (!socket.destroyed) { | ||||
|       // Graceful shutdown first | ||||
|       socket.end(); | ||||
|        | ||||
|       // Force destroy after timeout | ||||
|       const socketTimeout = setTimeout(() => { | ||||
|         try { | ||||
|           if (!socket.destroyed) { | ||||
|             socket.destroy(); | ||||
|           } | ||||
|         } catch (err) { | ||||
|           console.log(`[${record.id}] Error destroying ${side} socket: ${err}`); | ||||
|         } | ||||
|       }, 1000); | ||||
|        | ||||
|       // Don't block process exit | ||||
|       if (socketTimeout.unref) { | ||||
|         socketTimeout.unref(); | ||||
|       } | ||||
|     } | ||||
|   } catch (err) { | ||||
|     console.log(`[${record.id}] Error closing ${side} socket: ${err}`); | ||||
|     // Fallback to destroy | ||||
|     try { | ||||
|       if (!socket.destroyed) { | ||||
|         socket.destroy(); | ||||
|       } | ||||
|     } catch (destroyErr) { | ||||
|       console.log(`[${record.id}] Error destroying ${side} socket: ${destroyErr}`); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 3: NetworkProxy Bridge Cleanup | ||||
|  | ||||
| ```typescript | ||||
| public async forwardToNetworkProxy( | ||||
|   connectionId: string, | ||||
|   socket: plugins.net.Socket, | ||||
|   record: IConnectionRecord, | ||||
|   initialChunk: Buffer, | ||||
|   networkProxyPort: number, | ||||
|   cleanupCallback: (reason: string) => void | ||||
| ): Promise<void> { | ||||
|   const proxySocket = new plugins.net.Socket(); | ||||
|    | ||||
|   // Connect to NetworkProxy | ||||
|   await new Promise<void>((resolve, reject) => { | ||||
|     proxySocket.connect(networkProxyPort, 'localhost', () => { | ||||
|       resolve(); | ||||
|     }); | ||||
|     proxySocket.on('error', reject); | ||||
|   }); | ||||
|    | ||||
|   // Send initial data | ||||
|   if (initialChunk) { | ||||
|     proxySocket.write(initialChunk); | ||||
|   } | ||||
|    | ||||
|   // Setup bidirectional piping | ||||
|   socket.pipe(proxySocket); | ||||
|   proxySocket.pipe(socket); | ||||
|    | ||||
|   // Comprehensive cleanup handler | ||||
|   const cleanup = (reason: string) => { | ||||
|     // Unpipe to prevent data loss | ||||
|     socket.unpipe(proxySocket); | ||||
|     proxySocket.unpipe(socket); | ||||
|      | ||||
|     // Destroy proxy socket | ||||
|     proxySocket.destroy(); | ||||
|      | ||||
|     // Notify SmartProxy | ||||
|     cleanupCallback(reason); | ||||
|   }; | ||||
|    | ||||
|   // Setup all cleanup triggers | ||||
|   socket.on('end', () => cleanup('socket_end')); | ||||
|   socket.on('error', () => cleanup('socket_error')); | ||||
|   proxySocket.on('end', () => cleanup('proxy_end'));   | ||||
|   proxySocket.on('error', () => cleanup('proxy_error')); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 4: Error Handler with Cleanup | ||||
|  | ||||
| ```typescript | ||||
| public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) { | ||||
|   return (err: Error) => { | ||||
|     const code = (err as any).code; | ||||
|     let reason = 'error'; | ||||
|      | ||||
|     // Map error codes to reasons | ||||
|     switch (code) { | ||||
|       case 'ECONNRESET': | ||||
|         reason = 'econnreset'; | ||||
|         break; | ||||
|       case 'ETIMEDOUT': | ||||
|         reason = 'etimedout'; | ||||
|         break; | ||||
|       case 'ECONNREFUSED': | ||||
|         reason = 'connection_refused'; | ||||
|         break; | ||||
|       case 'EHOSTUNREACH': | ||||
|         reason = 'host_unreachable'; | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|     // Log with context | ||||
|     const duration = Date.now() - record.incomingStartTime; | ||||
|     console.log( | ||||
|       `[${record.id}] ${code} on ${side} side from ${record.remoteIP}. ` + | ||||
|       `Duration: ${plugins.prettyMs(duration)}` | ||||
|     ); | ||||
|      | ||||
|     // Track termination reason | ||||
|     if (side === 'incoming' && record.incomingTerminationReason === null) { | ||||
|       record.incomingTerminationReason = reason; | ||||
|       this.incrementTerminationStat('incoming', reason); | ||||
|     } | ||||
|      | ||||
|     // Initiate cleanup | ||||
|     this.initiateCleanupOnce(record, reason); | ||||
|   }; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 5: Inactivity Check with Cleanup | ||||
|  | ||||
| ```typescript | ||||
| public performInactivityCheck(): void { | ||||
|   const now = Date.now(); | ||||
|   const connectionIds = [...this.connectionRecords.keys()]; | ||||
|    | ||||
|   for (const id of connectionIds) { | ||||
|     const record = this.connectionRecords.get(id); | ||||
|     if (!record) continue; | ||||
|      | ||||
|     // Skip if disabled or immortal | ||||
|     if (this.settings.disableInactivityCheck || | ||||
|         (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) { | ||||
|       continue; | ||||
|     } | ||||
|      | ||||
|     const inactivityTime = now - record.lastActivity; | ||||
|     let effectiveTimeout = this.settings.inactivityTimeout!; | ||||
|      | ||||
|     // Extended timeout for keep-alive | ||||
|     if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') { | ||||
|       effectiveTimeout *= (this.settings.keepAliveInactivityMultiplier || 6); | ||||
|     } | ||||
|      | ||||
|     if (inactivityTime > effectiveTimeout && !record.connectionClosed) { | ||||
|       // Warn before closing keep-alive connections | ||||
|       if (record.hasKeepAlive && !record.inactivityWarningIssued) { | ||||
|         console.log(`[${id}] Warning: Keep-alive connection inactive`); | ||||
|         record.inactivityWarningIssued = true; | ||||
|         // Grace period | ||||
|         record.lastActivity = now - (effectiveTimeout - 600000); | ||||
|       } else { | ||||
|         // Close the connection | ||||
|         console.log(`[${id}] Closing due to inactivity`); | ||||
|         this.cleanupConnection(record, 'inactivity'); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 6: Complete Shutdown | ||||
|  | ||||
| ```typescript | ||||
| public clearConnections(): void { | ||||
|   const connectionIds = [...this.connectionRecords.keys()]; | ||||
|    | ||||
|   // Phase 1: Graceful end | ||||
|   for (const id of connectionIds) { | ||||
|     const record = this.connectionRecords.get(id); | ||||
|     if (record) { | ||||
|       try { | ||||
|         // Clear timers | ||||
|         if (record.cleanupTimer) { | ||||
|           clearTimeout(record.cleanupTimer); | ||||
|           record.cleanupTimer = undefined; | ||||
|         } | ||||
|          | ||||
|         // Graceful socket end | ||||
|         if (record.incoming && !record.incoming.destroyed) { | ||||
|           record.incoming.end(); | ||||
|         } | ||||
|         if (record.outgoing && !record.outgoing.destroyed) { | ||||
|           record.outgoing.end(); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         console.log(`Error during graceful end: ${err}`); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   // Phase 2: Force destroy after delay | ||||
|   setTimeout(() => { | ||||
|     for (const id of connectionIds) { | ||||
|       const record = this.connectionRecords.get(id); | ||||
|       if (record) { | ||||
|         try { | ||||
|           // Remove all listeners | ||||
|           if (record.incoming) { | ||||
|             record.incoming.removeAllListeners(); | ||||
|             if (!record.incoming.destroyed) { | ||||
|               record.incoming.destroy(); | ||||
|             } | ||||
|           } | ||||
|           if (record.outgoing) { | ||||
|             record.outgoing.removeAllListeners(); | ||||
|             if (!record.outgoing.destroyed) { | ||||
|               record.outgoing.destroy(); | ||||
|             } | ||||
|           } | ||||
|         } catch (err) { | ||||
|           console.log(`Error during forced destruction: ${err}`); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Clear all tracking | ||||
|     this.connectionRecords.clear(); | ||||
|     this.terminationStats = { incoming: {}, outgoing: {} }; | ||||
|   }, 100); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 7: Safe Event Handler Removal | ||||
|  | ||||
| ```typescript | ||||
| // Store handlers for later removal | ||||
| record.renegotiationHandler = this.tlsManager.createRenegotiationHandler( | ||||
|   connectionId, | ||||
|   serverName, | ||||
|   connInfo, | ||||
|   (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason) | ||||
| ); | ||||
|  | ||||
| // Add the handler | ||||
| socket.on('data', record.renegotiationHandler); | ||||
|  | ||||
| // Remove during cleanup | ||||
| if (record.incoming) { | ||||
|   try { | ||||
|     record.incoming.removeAllListeners('data'); | ||||
|     record.renegotiationHandler = undefined; | ||||
|   } catch (err) { | ||||
|     console.log(`[${record.id}] Error removing data handlers: ${err}`); | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Pattern 8: Connection State Tracking | ||||
|  | ||||
| ```typescript | ||||
| interface IConnectionRecord { | ||||
|   id: string; | ||||
|   connectionClosed: boolean; | ||||
|   incomingTerminationReason: string | null; | ||||
|   outgoingTerminationReason: string | null; | ||||
|   cleanupTimer?: NodeJS.Timeout; | ||||
|   renegotiationHandler?: Function; | ||||
|   // ... other fields | ||||
| } | ||||
|  | ||||
| // Check state before operations | ||||
| if (!record.connectionClosed) { | ||||
|   // Safe to perform operations | ||||
| } | ||||
|  | ||||
| // Track cleanup state | ||||
| record.connectionClosed = true; | ||||
| ``` | ||||
|  | ||||
| ## Key Principles | ||||
|  | ||||
| 1. **Idempotency**: Cleanup operations should be safe to call multiple times | ||||
| 2. **State Tracking**: Always track connection and cleanup state | ||||
| 3. **Error Resilience**: Handle errors during cleanup gracefully | ||||
| 4. **Resource Release**: Clear all references (timers, handlers, buffers) | ||||
| 5. **Graceful First**: Try graceful shutdown before forced destroy | ||||
| 6. **Comprehensive Coverage**: Handle all possible termination scenarios | ||||
| 7. **Logging**: Track termination reasons for debugging | ||||
| 8. **Memory Safety**: Clear data structures to prevent leaks | ||||
							
								
								
									
										248
									
								
								Connection-Termination-Issues.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								Connection-Termination-Issues.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,248 @@ | ||||
| # Connection Termination Issues and Solutions in SmartProxy/NetworkProxy | ||||
|  | ||||
| ## Common Connection Termination Scenarios | ||||
|  | ||||
| ### 1. Normal Connection Closure | ||||
|  | ||||
| **Flow**: | ||||
| - Client or server initiates graceful close | ||||
| - 'close' event triggers cleanup | ||||
| - Connection removed from tracking | ||||
| - Resources freed | ||||
|  | ||||
| **Code Path**: | ||||
| ```typescript | ||||
| // In ConnectionManager | ||||
| handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) { | ||||
|   record.incomingTerminationReason = 'normal'; | ||||
|   this.initiateCleanupOnce(record, 'closed_' + side); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Error-Based Termination | ||||
|  | ||||
| **Common Errors**: | ||||
| - ECONNRESET: Connection reset by peer | ||||
| - ETIMEDOUT: Connection timed out | ||||
| - ECONNREFUSED: Connection refused | ||||
| - EHOSTUNREACH: Host unreachable | ||||
|  | ||||
| **Handling**: | ||||
| ```typescript | ||||
| handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) { | ||||
|   return (err: Error) => { | ||||
|     const code = (err as any).code; | ||||
|     let reason = 'error'; | ||||
|      | ||||
|     if (code === 'ECONNRESET') { | ||||
|       reason = 'econnreset'; | ||||
|     } else if (code === 'ETIMEDOUT') { | ||||
|       reason = 'etimedout'; | ||||
|     } | ||||
|      | ||||
|     this.initiateCleanupOnce(record, reason); | ||||
|   }; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 3. Inactivity Timeout | ||||
|  | ||||
| **Detection**: | ||||
| ```typescript | ||||
| performInactivityCheck(): void { | ||||
|   const now = Date.now(); | ||||
|   for (const record of this.connectionRecords.values()) { | ||||
|     const inactivityTime = now - record.lastActivity; | ||||
|     if (inactivityTime > effectiveTimeout) { | ||||
|       this.cleanupConnection(record, 'inactivity'); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| **Special Cases**: | ||||
| - Keep-alive connections get extended timeouts | ||||
| - "Immortal" connections bypass inactivity checks | ||||
| - Warning issued before closure for keep-alive connections | ||||
|  | ||||
| ### 4. NFTables-Handled Connections | ||||
|  | ||||
| **Special Handling**: | ||||
| ```typescript | ||||
| if (route.action.forwardingEngine === 'nftables') { | ||||
|   socket.end(); | ||||
|   record.nftablesHandled = true; | ||||
|   this.connectionManager.initiateCleanupOnce(record, 'nftables_handled'); | ||||
|   return; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| These connections are: | ||||
| - Handled at kernel level | ||||
| - Closed immediately at application level | ||||
| - Tracked for metrics only | ||||
|  | ||||
| ### 5. NetworkProxy Bridge Termination | ||||
|  | ||||
| **Bridge Cleanup**: | ||||
| ```typescript | ||||
| const cleanup = (reason: string) => { | ||||
|   socket.unpipe(proxySocket); | ||||
|   proxySocket.unpipe(socket); | ||||
|   proxySocket.destroy(); | ||||
|   cleanupCallback(reason); | ||||
| }; | ||||
|  | ||||
| socket.on('end', () => cleanup('socket_end')); | ||||
| socket.on('error', () => cleanup('socket_error')); | ||||
| proxySocket.on('end', () => cleanup('proxy_end')); | ||||
| proxySocket.on('error', () => cleanup('proxy_error')); | ||||
| ``` | ||||
|  | ||||
| ## Preventing Connection Leaks | ||||
|  | ||||
| ### 1. Always Remove Event Listeners | ||||
|  | ||||
| ```typescript | ||||
| cleanupConnection(record: IConnectionRecord, reason: string): void { | ||||
|   if (record.incoming) { | ||||
|     record.incoming.removeAllListeners('data'); | ||||
|     record.renegotiationHandler = undefined; | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Clear Timers | ||||
|  | ||||
| ```typescript | ||||
| if (record.cleanupTimer) { | ||||
|   clearTimeout(record.cleanupTimer); | ||||
|   record.cleanupTimer = undefined; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 3. Proper Socket Cleanup | ||||
|  | ||||
| ```typescript | ||||
| private cleanupSocket(record: IConnectionRecord, side: string, socket: net.Socket): void { | ||||
|   try { | ||||
|     if (!socket.destroyed) { | ||||
|       socket.end(); // Graceful | ||||
|       setTimeout(() => { | ||||
|         if (!socket.destroyed) { | ||||
|           socket.destroy(); // Forced | ||||
|         } | ||||
|       }, 1000); | ||||
|     } | ||||
|   } catch (err) { | ||||
|     console.log(`Error closing ${side} socket: ${err}`); | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 4. Connection Record Cleanup | ||||
|  | ||||
| ```typescript | ||||
| // Clear pending data to prevent memory leaks | ||||
| record.pendingData = []; | ||||
| record.pendingDataSize = 0; | ||||
|  | ||||
| // Remove from tracking map | ||||
| this.connectionRecords.delete(record.id); | ||||
| ``` | ||||
|  | ||||
| ## Monitoring and Debugging | ||||
|  | ||||
| ### 1. Termination Statistics | ||||
|  | ||||
| ```typescript | ||||
| private terminationStats: { | ||||
|   incoming: Record<string, number>; | ||||
|   outgoing: Record<string, number>; | ||||
| } = { incoming: {}, outgoing: {} }; | ||||
|  | ||||
| incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void { | ||||
|   this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### 2. Connection Logging | ||||
|  | ||||
| **Detailed Logging**: | ||||
| ```typescript | ||||
| console.log( | ||||
|   `[${record.id}] Connection from ${record.remoteIP} terminated (${reason}).` + | ||||
|   ` Duration: ${prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}` | ||||
| ); | ||||
| ``` | ||||
|  | ||||
| ### 3. Active Connection Tracking | ||||
|  | ||||
| ```typescript | ||||
| getConnectionCount(): number { | ||||
|   return this.connectionRecords.size; | ||||
| } | ||||
|  | ||||
| // In NetworkProxy | ||||
| metrics = { | ||||
|   activeConnections: this.connectedClients, | ||||
|   portProxyConnections: this.portProxyConnections, | ||||
|   tlsTerminatedConnections: this.tlsTerminatedConnections | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| ## Best Practices for Connection Termination | ||||
|  | ||||
| 1. **Always Use initiateCleanupOnce()**: | ||||
|    - Prevents duplicate cleanup operations | ||||
|    - Ensures proper termination reason tracking | ||||
|  | ||||
| 2. **Handle All Socket Events**: | ||||
|    - 'error', 'close', 'end' events | ||||
|    - Both incoming and outgoing sockets | ||||
|  | ||||
| 3. **Implement Proper Timeouts**: | ||||
|    - Initial data timeout | ||||
|    - Inactivity timeout   | ||||
|    - Maximum connection lifetime | ||||
|  | ||||
| 4. **Track Resources**: | ||||
|    - Connection records | ||||
|    - Socket maps | ||||
|    - Timer references | ||||
|  | ||||
| 5. **Log Termination Reasons**: | ||||
|    - Helps debug connection issues | ||||
|    - Provides metrics for monitoring | ||||
|  | ||||
| 6. **Graceful Shutdown**: | ||||
|    - Try socket.end() before socket.destroy() | ||||
|    - Allow time for graceful closure | ||||
|  | ||||
| 7. **Memory Management**: | ||||
|    - Clear pending data buffers | ||||
|    - Remove event listeners | ||||
|    - Delete connection records | ||||
|  | ||||
| ## Common Issues and Solutions | ||||
|  | ||||
| ### Issue: Memory Leaks from Event Listeners | ||||
| **Solution**: Always call removeAllListeners() during cleanup | ||||
|  | ||||
| ### Issue: Orphaned Connections | ||||
| **Solution**: Implement multiple cleanup triggers (timeout, error, close) | ||||
|  | ||||
| ### Issue: Duplicate Cleanup Operations | ||||
| **Solution**: Use connectionClosed flag and initiateCleanupOnce() | ||||
|  | ||||
| ### Issue: Hanging Connections | ||||
| **Solution**: Implement inactivity checks and maximum lifetime limits | ||||
|  | ||||
| ### Issue: Resource Exhaustion | ||||
| **Solution**: Track connection counts and implement limits | ||||
|  | ||||
| ### Issue: Lost Data During Cleanup | ||||
| **Solution**: Use proper unpipe operations and graceful shutdown | ||||
|  | ||||
| ### Issue: Debugging Connection Issues | ||||
| **Solution**: Track termination reasons and maintain detailed logs | ||||
							
								
								
									
										153
									
								
								NetworkProxy-SmartProxy-Connection-Management.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								NetworkProxy-SmartProxy-Connection-Management.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| # NetworkProxy Connection Termination and SmartProxy Connection Handling | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| The connection management between NetworkProxy and SmartProxy involves complex coordination to handle TLS termination, connection forwarding, and proper cleanup. This document outlines how these systems work together. | ||||
|  | ||||
| ## SmartProxy Connection Management | ||||
|  | ||||
| ### Connection Tracking (ConnectionManager) | ||||
|  | ||||
| 1. **Connection Lifecycle**: | ||||
|    - New connections are registered in `ConnectionManager.createConnection()` | ||||
|    - Each connection gets a unique ID and tracking record | ||||
|    - Connection records track both incoming (client) and outgoing (target) sockets | ||||
|    - Connections are removed from tracking upon cleanup | ||||
|  | ||||
| 2. **Connection Cleanup Flow**: | ||||
|    ``` | ||||
|    initiateCleanupOnce() -> cleanupConnection() -> cleanupSocket() | ||||
|    ``` | ||||
|    - `initiateCleanupOnce()`: Prevents duplicate cleanup operations | ||||
|    - `cleanupConnection()`: Main cleanup logic, removes connections from tracking | ||||
|    - `cleanupSocket()`: Handles socket termination (graceful end, then forced destroy) | ||||
|  | ||||
| 3. **Cleanup Triggers**: | ||||
|    - Socket errors (ECONNRESET, ETIMEDOUT, etc.) | ||||
|    - Socket close events | ||||
|    - Inactivity timeouts | ||||
|    - Connection lifetime limits | ||||
|    - Manual cleanup (e.g., NFTables-handled connections) | ||||
|  | ||||
| ## NetworkProxy Integration | ||||
|  | ||||
| ### NetworkProxyBridge | ||||
|  | ||||
| The `NetworkProxyBridge` class manages the connection between SmartProxy and NetworkProxy: | ||||
|  | ||||
| 1. **Connection Forwarding**: | ||||
|    ```typescript | ||||
|    forwardToNetworkProxy( | ||||
|      connectionId: string, | ||||
|      socket: net.Socket, | ||||
|      record: IConnectionRecord, | ||||
|      initialChunk: Buffer, | ||||
|      networkProxyPort: number, | ||||
|      cleanupCallback: (reason: string) => void | ||||
|    ) | ||||
|    ``` | ||||
|    - Creates a new socket connection to NetworkProxy | ||||
|    - Pipes data between client and NetworkProxy sockets | ||||
|    - Sets up cleanup handlers for both sockets | ||||
|  | ||||
| 2. **Cleanup Coordination**: | ||||
|    - When either socket ends or errors, both are cleaned up | ||||
|    - Cleanup callback notifies SmartProxy's ConnectionManager | ||||
|    - Proper unpipe operations prevent memory leaks | ||||
|  | ||||
| ## NetworkProxy Connection Tracking | ||||
|  | ||||
| ### Connection Tracking in NetworkProxy | ||||
|  | ||||
| 1. **Raw TCP Connection Tracking**: | ||||
|    ```typescript | ||||
|    setupConnectionTracking(): void { | ||||
|      this.httpsServer.on('connection', (connection: net.Socket) => { | ||||
|        // Track connections in socketMap | ||||
|        this.socketMap.add(connection); | ||||
|         | ||||
|        // Setup cleanup handlers | ||||
|        connection.on('close', cleanupConnection); | ||||
|        connection.on('error', cleanupConnection); | ||||
|        connection.on('end', cleanupConnection); | ||||
|      }); | ||||
|    } | ||||
|    ``` | ||||
|  | ||||
| 2. **SmartProxy Connection Detection**: | ||||
|    - Connections from localhost (127.0.0.1) are identified as SmartProxy connections | ||||
|    - Special counter tracks `portProxyConnections` | ||||
|    - Connection counts are updated when connections close | ||||
|  | ||||
| 3. **Metrics and Monitoring**: | ||||
|    - Active connections tracked in `connectedClients` | ||||
|    - TLS handshake completions tracked in `tlsTerminatedConnections` | ||||
|    - Connection pool status monitored periodically | ||||
|  | ||||
| ## Connection Termination Flow | ||||
|  | ||||
| ### Typical TLS Termination Flow: | ||||
|  | ||||
| 1. Client connects to SmartProxy | ||||
| 2. SmartProxy creates connection record and tracks socket | ||||
| 3. SmartProxy determines route requires TLS termination | ||||
| 4. NetworkProxyBridge forwards connection to NetworkProxy | ||||
| 5. NetworkProxy performs TLS termination | ||||
| 6. Data flows through piped sockets | ||||
| 7. When connection ends: | ||||
|    - NetworkProxy cleans up its socket tracking | ||||
|    - NetworkProxyBridge handles cleanup coordination | ||||
|    - SmartProxy's ConnectionManager removes connection record | ||||
|    - All resources are properly released | ||||
|  | ||||
| ### Cleanup Coordination Points: | ||||
|  | ||||
| 1. **SmartProxy Cleanup**: | ||||
|    - ConnectionManager tracks all cleanup reasons | ||||
|    - Socket handlers removed to prevent memory leaks | ||||
|    - Timeout timers cleared | ||||
|    - Connection records removed from maps | ||||
|    - Security manager notified of connection removal | ||||
|  | ||||
| 2. **NetworkProxy Cleanup**: | ||||
|    - Sockets removed from tracking map | ||||
|    - Connection counters updated | ||||
|    - Metrics updated for monitoring | ||||
|    - Connection pool resources freed | ||||
|  | ||||
| 3. **Bridge Cleanup**: | ||||
|    - Unpipe operations prevent data loss | ||||
|    - Both sockets properly destroyed | ||||
|    - Cleanup callback ensures SmartProxy is notified | ||||
|  | ||||
| ## Important Considerations | ||||
|  | ||||
| 1. **Memory Management**: | ||||
|    - All event listeners must be removed during cleanup | ||||
|    - Proper unpipe operations prevent memory leaks | ||||
|    - Connection records cleared from all tracking maps | ||||
|  | ||||
| 2. **Error Handling**: | ||||
|    - Multiple cleanup mechanisms prevent orphaned connections | ||||
|    - Graceful shutdown attempted before forced destruction | ||||
|    - Timeout mechanisms ensure cleanup even in edge cases | ||||
|  | ||||
| 3. **State Consistency**: | ||||
|    - Connection closed flags prevent duplicate cleanup | ||||
|    - Termination reasons tracked for debugging | ||||
|    - Activity timestamps updated for accurate timeout handling | ||||
|  | ||||
| 4. **Performance**: | ||||
|    - Connection pools minimize TCP handshake overhead | ||||
|    - Efficient socket tracking using Maps | ||||
|    - Periodic cleanup prevents resource accumulation | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| 1. Always use `initiateCleanupOnce()` to prevent duplicate cleanup operations | ||||
| 2. Track termination reasons for debugging and monitoring | ||||
| 3. Ensure all event listeners are removed during cleanup | ||||
| 4. Use proper unpipe operations when breaking socket connections | ||||
| 5. Monitor connection counts and cleanup statistics | ||||
| 6. Implement proper timeout handling for all connection types | ||||
| 7. Keep socket tracking maps synchronized with actual socket state | ||||
| @@ -1,86 +0,0 @@ | ||||
| # ACME/Certificate Simplification Summary | ||||
|  | ||||
| ## What Was Done | ||||
|  | ||||
| We successfully implemented the ACME/Certificate simplification plan for SmartProxy: | ||||
|  | ||||
| ### 1. Created New Certificate Management System | ||||
|  | ||||
| - **SmartCertManager** (`ts/proxies/smart-proxy/certificate-manager.ts`): A unified certificate manager that handles both ACME and static certificates | ||||
| - **CertStore** (`ts/proxies/smart-proxy/cert-store.ts`): File-based certificate storage system | ||||
|  | ||||
| ### 2. Updated Route Types | ||||
|  | ||||
| - Added `IRouteAcme` interface for ACME configuration | ||||
| - Added `IStaticResponse` interface for static route responses | ||||
| - Extended `IRouteTls` with comprehensive certificate options | ||||
| - Added `handler` property to `IRouteAction` for static routes | ||||
|  | ||||
| ### 3. Implemented Static Route Handler | ||||
|  | ||||
| - Added `handleStaticAction` method to route-connection-handler.ts | ||||
| - Added support for 'static' route type in the action switch statement | ||||
| - Implemented proper HTTP response formatting | ||||
|  | ||||
| ### 4. Updated SmartProxy Integration | ||||
|  | ||||
| - Removed old CertProvisioner and Port80Handler dependencies | ||||
| - Added `initializeCertificateManager` method | ||||
| - Updated `start` and `stop` methods to use new certificate manager | ||||
| - Added `provisionCertificate`, `renewCertificate`, and `getCertificateStatus` methods | ||||
|  | ||||
| ### 5. Simplified NetworkProxyBridge | ||||
|  | ||||
| - Removed all certificate-related logic | ||||
| - Simplified to only handle network proxy forwarding | ||||
| - Updated to use port-based matching for network proxy routes | ||||
|  | ||||
| ### 6. Cleaned Up HTTP Module | ||||
|  | ||||
| - Removed exports for port80 subdirectory | ||||
| - Kept only router and redirect functionality | ||||
|  | ||||
| ### 7. Created Tests | ||||
|  | ||||
| - Created simplified test for certificate functionality | ||||
| - Test demonstrates static route handling and basic certificate configuration | ||||
|  | ||||
| ## Key Improvements | ||||
|  | ||||
| 1. **No Backward Compatibility**: Clean break from legacy implementations | ||||
| 2. **Direct SmartAcme Integration**: Uses @push.rocks/smartacme directly without custom wrappers | ||||
| 3. **Route-Based ACME Challenges**: No separate HTTP server needed | ||||
| 4. **Simplified Architecture**: Removed unnecessary abstraction layers | ||||
| 5. **Unified Configuration**: Certificate configuration is part of route definitions | ||||
|  | ||||
| ## Configuration Example | ||||
|  | ||||
| ```typescript | ||||
| const proxy = new SmartProxy({ | ||||
|   routes: [{ | ||||
|     name: 'secure-site', | ||||
|     match: { ports: 443, domains: 'example.com' }, | ||||
|     action: { | ||||
|       type: 'forward', | ||||
|       target: { host: 'backend', port: 8080 }, | ||||
|       tls: { | ||||
|         mode: 'terminate', | ||||
|         certificate: 'auto', | ||||
|         acme: { | ||||
|           email: 'admin@example.com', | ||||
|           useProduction: true | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }] | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## Next Steps | ||||
|  | ||||
| 1. Remove old certificate module and port80 directory | ||||
| 2. Update documentation with new configuration format | ||||
| 3. Test with real ACME certificates in staging environment | ||||
| 4. Add more comprehensive tests for renewal and edge cases | ||||
|  | ||||
| The implementation is complete and builds successfully! | ||||
| @@ -1,34 +0,0 @@ | ||||
| # NFTables Naming Consolidation Summary | ||||
|  | ||||
| This document summarizes the changes made to consolidate the naming convention for IP allow/block lists in the NFTables integration. | ||||
|  | ||||
| ## Changes Made | ||||
|  | ||||
| 1. **Updated NFTablesProxy interface** (`ts/proxies/nftables-proxy/models/interfaces.ts`): | ||||
|    - Changed `allowedSourceIPs` to `ipAllowList` | ||||
|    - Changed `bannedSourceIPs` to `ipBlockList` | ||||
|  | ||||
| 2. **Updated NFTablesProxy implementation** (`ts/proxies/nftables-proxy/nftables-proxy.ts`): | ||||
|    - Updated all references from `allowedSourceIPs` to `ipAllowList` | ||||
|    - Updated all references from `bannedSourceIPs` to `ipBlockList` | ||||
|  | ||||
| 3. **Updated NFTablesManager** (`ts/proxies/smart-proxy/nftables-manager.ts`): | ||||
|    - Changed mapping from `allowedSourceIPs` to `ipAllowList` | ||||
|    - Changed mapping from `bannedSourceIPs` to `ipBlockList` | ||||
|  | ||||
| ## Files Already Using Consistent Naming | ||||
|  | ||||
| The following files already used the consistent naming convention `ipAllowList` and `ipBlockList`: | ||||
|  | ||||
| 1. **Route helpers** (`ts/proxies/smart-proxy/utils/route-helpers.ts`) | ||||
| 2. **Integration test** (`test/test.nftables-integration.ts`) | ||||
| 3. **NFTables example** (`examples/nftables-integration.ts`) | ||||
| 4. **Route types** (`ts/proxies/smart-proxy/models/route-types.ts`) | ||||
|  | ||||
| ## Result | ||||
|  | ||||
| The naming is now consistent throughout the codebase: | ||||
| - `ipAllowList` is used for lists of allowed IP addresses | ||||
| - `ipBlockList` is used for lists of blocked IP addresses | ||||
|  | ||||
| This matches the naming convention already established in SmartProxy's core routing system. | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { | ||||
|   EventSystem, | ||||
|   ProxyEvents, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { IpUtils } from '../../../ts/core/utils/ip-utils.js'; | ||||
|  | ||||
| tap.test('ip-utils - normalizeIP', async () => { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as routeUtils from '../../../ts/core/utils/route-utils.js'; | ||||
|  | ||||
| // Test domain matching | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { SharedSecurityManager } from '../../../ts/core/utils/shared-security-manager.js'; | ||||
| import type { IRouteConfig, IRouteContext } from '../../../ts/proxies/smart-proxy/models/route-types.js'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { ValidationUtils } from '../../../ts/core/utils/validation-utils.js'; | ||||
| import type { IDomainOptions, IAcmeOptions } from '../../../ts/core/models/common-types.js'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { AcmeStateManager } from '../ts/proxies/smart-proxy/acme-state-manager.js'; | ||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| const testProxy = new SmartProxy({ | ||||
|   routes: [{ | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| tap.test('should create SmartProxy with certificate routes', async () => { | ||||
|   const proxy = new SmartProxy({ | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as path from 'path'; | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
|  | ||||
| // First, import the components directly to avoid issues with compiled modules | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import { NetworkProxy } from '../ts/proxies/network-proxy/index.js'; | ||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as smartproxy from '../ts/index.js'; | ||||
| import { loadTestCertificates } from './helpers/certificates.js'; | ||||
| import * as https from 'https'; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as child_process from 'child_process'; | ||||
| import { promisify } from 'util'; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { createNfTablesRoute, createNfTablesTerminateRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import * as http from 'http'; | ||||
| import * as https from 'https'; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js'; | ||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||
| import type { ISmartProxyOptions } from '../ts/proxies/smart-proxy/models/interfaces.js'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { NFTablesManager } from '../ts/proxies/smart-proxy/nftables-manager.js'; | ||||
| import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as child_process from 'child_process'; | ||||
| import { promisify } from 'util'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
| import { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { SmartProxy } from '../ts/index.js'; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import { SmartProxy } from '../ts/index.js'; | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * Tests for the unified route-based configuration system | ||||
|  */ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| // Import from core modules | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import { SmartProxy } from '../ts/index.js'; | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
|  | ||||
| let testProxy: SmartProxy; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
|  | ||||
| // Import from individual modules to avoid naming conflicts | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as tsclass from '@tsclass/tsclass'; | ||||
| import * as http from 'http'; | ||||
| import { ProxyRouter, type RouterResult } from '../ts/http/router/proxy-router.js'; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import * as plugins from '../ts/plugins.js'; | ||||
| import { tap, expect } from '@push.rocks/tapbundle'; | ||||
| import { tap, expect } from '@git.zone/tstest/tapbundle'; | ||||
| import { SmartCertManager } from '../ts/proxies/smart-proxy/certificate-manager.js'; | ||||
| import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { expect, tap } from '@push.rocks/tapbundle'; | ||||
| import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||||
| import * as net from 'net'; | ||||
| import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; | ||||
|  | ||||
|   | ||||
| @@ -219,21 +219,12 @@ export class NetworkProxy implements IMetricsTracker { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @deprecated Use SmartCertManager instead | ||||
|    */ | ||||
|   public setExternalPort80Handler(handler: any): void { | ||||
|     this.logger.warn('Port80Handler is deprecated - use SmartCertManager instead'); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Starts the proxy server | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     this.startTime = Date.now(); | ||||
|      | ||||
|     // Certificate management is now handled by SmartCertManager | ||||
|      | ||||
|     // Create HTTP/2 server with HTTP/1 fallback | ||||
|     this.httpsServer = plugins.http2.createSecureServer( | ||||
|       { | ||||
|   | ||||
| @@ -47,6 +47,7 @@ export interface IRouteContext { | ||||
|   path?: string;         // URL path (for HTTP connections) | ||||
|   query?: string;        // Query string (for HTTP connections) | ||||
|   headers?: Record<string, string>; // HTTP headers (for HTTP connections) | ||||
|   method?: string;       // HTTP method (for HTTP connections) | ||||
|  | ||||
|   // TLS information | ||||
|   isTls: boolean;        // Whether the connection is TLS | ||||
|   | ||||
| @@ -728,14 +728,78 @@ export class RouteConnectionHandler { | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     let buffer = Buffer.alloc(0); | ||||
|      | ||||
|     const handleHttpData = async (chunk: Buffer) => { | ||||
|       buffer = Buffer.concat([buffer, chunk]); | ||||
|        | ||||
|       // 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; | ||||
|       } | ||||
|        | ||||
|       // 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; | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // 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 { | ||||
|       // Build route context | ||||
|         // Build route context with parsed HTTP information | ||||
|         const context: IRouteContext = { | ||||
|           port: record.localPort, | ||||
|         domain: record.lockedDomain, | ||||
|           domain: record.lockedDomain || headersMap['host']?.split(':')[0], | ||||
|           clientIp: record.remoteIP, | ||||
|           serverIp: socket.localAddress!, | ||||
|         path: undefined,  // Will need to be extracted from HTTP request | ||||
|           path: pathname, | ||||
|           query: query, | ||||
|           headers: headersMap, | ||||
|           method: method, | ||||
|           isTls: record.isTLS, | ||||
|           tlsVersion: record.tlsVersion, | ||||
|           routeName: route.name, | ||||
| @@ -744,29 +808,59 @@ export class RouteConnectionHandler { | ||||
|           connectionId | ||||
|         }; | ||||
|          | ||||
|       // Call the handler | ||||
|         // Remove the data listener since we're handling the request | ||||
|         socket.removeListener('data', handleHttpData); | ||||
|          | ||||
|         // Call the handler with the properly parsed context | ||||
|         const response = await route.action.handler(context); | ||||
|          | ||||
|       // Send HTTP response | ||||
|       const headers = response.headers || {}; | ||||
|       headers['Content-Length'] = Buffer.byteLength(response.body).toString(); | ||||
|         // 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(headers)) { | ||||
|         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); | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user