368 lines
9.9 KiB
TypeScript
368 lines
9.9 KiB
TypeScript
|
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<void>((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<void>((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<void>((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<void>((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<void>((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();
|