import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as plugins from '../ts/plugins.js'; // Import SmartProxy and configurations import { SmartProxy } from '../ts/index.js'; tap.test('should handle proxy chaining without connection accumulation', async () => { console.log('\n=== Testing Proxy Chaining Connection Accumulation ==='); console.log('Setup: Client → SmartProxy1 → SmartProxy2 → Backend (down)'); // Create SmartProxy2 (downstream proxy) const proxy2 = new SmartProxy({ ports: [8581], enableDetailedLogging: false, socketTimeout: 5000, routes: [{ name: 'backend-route', match: { ports: 8581 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 // Non-existent backend } } }] }); // Create SmartProxy1 (upstream proxy) const proxy1 = new SmartProxy({ ports: [8580], enableDetailedLogging: false, socketTimeout: 5000, routes: [{ name: 'chain-route', match: { ports: 8580 }, action: { type: 'forward', target: { host: 'localhost', port: 8581 // Forward to proxy2 } } }] }); // Start both proxies await proxy2.start(); console.log('✓ SmartProxy2 started on port 8581'); await proxy1.start(); console.log('✓ SmartProxy1 started on port 8580'); // Helper to get connection counts const getConnectionCounts = () => { const conn1 = (proxy1 as any).connectionManager; const conn2 = (proxy2 as any).connectionManager; return { proxy1: conn1 ? conn1.getConnectionCount() : 0, proxy2: conn2 ? conn2.getConnectionCount() : 0 }; }; const initialCounts = getConnectionCounts(); console.log(`\nInitial connection counts - Proxy1: ${initialCounts.proxy1}, Proxy2: ${initialCounts.proxy2}`); // Test 1: Single connection attempt console.log('\n--- Test 1: Single connection through chain ---'); await new Promise((resolve) => { const client = new net.Socket(); client.on('error', (err) => { console.log(`Client received error: ${err.code}`); resolve(); }); client.on('close', () => { console.log('Client connection closed'); resolve(); }); client.connect(8580, 'localhost', () => { console.log('Client connected to Proxy1'); // Send data to trigger routing client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n'); }); // Timeout setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 1000); }); // Check connections after single attempt await new Promise(resolve => setTimeout(resolve, 500)); let counts = getConnectionCounts(); console.log(`After single connection - Proxy1: ${counts.proxy1}, Proxy2: ${counts.proxy2}`); // Test 2: Multiple simultaneous connections console.log('\n--- Test 2: Multiple simultaneous connections ---'); const promises = []; for (let i = 0; i < 10; i++) { promises.push(new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { resolve(); }); client.on('close', () => { resolve(); }); client.connect(8580, 'localhost', () => { // Send data client.write(`GET /test${i} HTTP/1.1\r\nHost: test.com\r\n\r\n`); }); // Timeout setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 500); })); } await Promise.all(promises); console.log('✓ All simultaneous connections completed'); // Check connections counts = getConnectionCounts(); console.log(`After simultaneous connections - Proxy1: ${counts.proxy1}, Proxy2: ${counts.proxy2}`); // Test 3: Rapid serial connections (simulating retries) console.log('\n--- Test 3: Rapid serial connections (retries) ---'); for (let i = 0; i < 20; i++) { await new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { resolve(); }); client.on('close', () => { resolve(); }); client.connect(8580, 'localhost', () => { client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n'); // Quick disconnect to simulate retry behavior setTimeout(() => client.destroy(), 50); }); // Timeout setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 200); }); if ((i + 1) % 5 === 0) { counts = getConnectionCounts(); console.log(`After ${i + 1} retries - Proxy1: ${counts.proxy1}, Proxy2: ${counts.proxy2}`); } // Small delay between retries await new Promise(resolve => setTimeout(resolve, 50)); } // Test 4: Long-lived connection attempt console.log('\n--- Test 4: Long-lived connection attempt ---'); await new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { resolve(); }); client.on('close', () => { console.log('Long-lived client closed'); resolve(); }); client.connect(8580, 'localhost', () => { console.log('Long-lived client connected'); // Send data periodically const interval = setInterval(() => { if (!client.destroyed && client.writable) { client.write('PING\r\n'); } else { clearInterval(interval); } }, 100); // Close after 2 seconds setTimeout(() => { clearInterval(interval); client.destroy(); }, 2000); }); // Timeout setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 3000); }); // Final check await new Promise(resolve => setTimeout(resolve, 1000)); const finalCounts = getConnectionCounts(); console.log(`\nFinal connection counts - Proxy1: ${finalCounts.proxy1}, Proxy2: ${finalCounts.proxy2}`); // Monitor for a bit to see if connections are cleaned up console.log('\nMonitoring connection cleanup...'); for (let i = 0; i < 3; i++) { await new Promise(resolve => setTimeout(resolve, 500)); counts = getConnectionCounts(); console.log(`After ${(i + 1) * 0.5}s - Proxy1: ${counts.proxy1}, Proxy2: ${counts.proxy2}`); } // Stop proxies await proxy1.stop(); console.log('\n✓ SmartProxy1 stopped'); await proxy2.stop(); console.log('✓ SmartProxy2 stopped'); // Analysis console.log('\n=== Analysis ==='); if (finalCounts.proxy1 > 0 || finalCounts.proxy2 > 0) { console.log('❌ FAIL: Connections accumulated!'); console.log(`Proxy1 leaked ${finalCounts.proxy1} connections`); console.log(`Proxy2 leaked ${finalCounts.proxy2} connections`); } else { console.log('✅ PASS: No connection accumulation detected'); } // Verify expect(finalCounts.proxy1).toEqual(0); expect(finalCounts.proxy2).toEqual(0); }); tap.test('should handle proxy chain with HTTP traffic', async () => { console.log('\n=== Testing Proxy Chain with HTTP Traffic ==='); // Create SmartProxy2 with HTTP handling const proxy2 = new SmartProxy({ ports: [8583], useHttpProxy: [8583], // Enable HTTP proxy handling httpProxyPort: 8584, enableDetailedLogging: false, routes: [{ name: 'http-backend', match: { ports: 8583 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 // Non-existent backend } } }] }); // Create SmartProxy1 with HTTP handling const proxy1 = new SmartProxy({ ports: [8582], useHttpProxy: [8582], // Enable HTTP proxy handling httpProxyPort: 8585, enableDetailedLogging: false, routes: [{ name: 'http-chain', match: { ports: 8582 }, action: { type: 'forward', target: { host: 'localhost', port: 8583 // Forward to proxy2 } } }] }); await proxy2.start(); console.log('✓ SmartProxy2 (HTTP) started on port 8583'); await proxy1.start(); console.log('✓ SmartProxy1 (HTTP) started on port 8582'); // Helper to get connection counts const getConnectionCounts = () => { const conn1 = (proxy1 as any).connectionManager; const conn2 = (proxy2 as any).connectionManager; return { proxy1: conn1 ? conn1.getConnectionCount() : 0, proxy2: conn2 ? conn2.getConnectionCount() : 0 }; }; console.log('\nSending HTTP requests through chain...'); // Make HTTP requests for (let i = 0; i < 5; i++) { await new Promise((resolve) => { const client = new net.Socket(); let responseData = ''; client.on('data', (data) => { responseData += data.toString(); // Check if we got a complete HTTP response if (responseData.includes('\r\n\r\n')) { console.log(`Response ${i + 1}: ${responseData.split('\r\n')[0]}`); client.destroy(); } }); client.on('error', () => { resolve(); }); client.on('close', () => { resolve(); }); client.connect(8582, 'localhost', () => { client.write(`GET /test${i} HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n`); }); setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 1000); }); await new Promise(resolve => setTimeout(resolve, 100)); } await new Promise(resolve => setTimeout(resolve, 1000)); const finalCounts = getConnectionCounts(); console.log(`\nFinal HTTP proxy counts - Proxy1: ${finalCounts.proxy1}, Proxy2: ${finalCounts.proxy2}`); await proxy1.stop(); await proxy2.stop(); expect(finalCounts.proxy1).toEqual(0); expect(finalCounts.proxy2).toEqual(0); }); tap.start();