- Refactor cleanupSocket function to support options for immediate destruction, allowing drain, and grace periods. - Introduce createIndependentSocketHandlers for better management of half-open connections between client and server sockets. - Update various handlers (HTTP, HTTPS passthrough, HTTPS terminate) to utilize new cleanup and socket management functions. - Implement custom timeout handling in socket setup to prevent immediate closure during keep-alive connections. - Add tests for long-lived connections and half-open connection scenarios to ensure stability and reliability. - Adjust connection manager to handle socket cleanup based on activity status, improving resource management.
192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import * as tls from 'tls';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
|
|
let testProxy: SmartProxy;
|
|
let targetServer: net.Server;
|
|
|
|
// Create a simple echo server as target
|
|
tap.test('setup test environment', async () => {
|
|
// Create target server that echoes data back
|
|
targetServer = net.createServer((socket) => {
|
|
console.log('Target server: client connected');
|
|
|
|
// Echo data back
|
|
socket.on('data', (data) => {
|
|
console.log(`Target server received: ${data.toString().trim()}`);
|
|
socket.write(data);
|
|
});
|
|
|
|
socket.on('close', () => {
|
|
console.log('Target server: client disconnected');
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
targetServer.listen(9876, () => {
|
|
console.log('Target server listening on port 9876');
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// Create proxy with simple TCP forwarding (no TLS)
|
|
testProxy = new SmartProxy({
|
|
routes: [{
|
|
name: 'tcp-forward-test',
|
|
match: {
|
|
ports: 8888 // Plain TCP port
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'localhost',
|
|
port: 9876
|
|
}
|
|
// No TLS configuration - just plain TCP forwarding
|
|
}
|
|
}],
|
|
defaults: {
|
|
target: {
|
|
host: 'localhost',
|
|
port: 9876
|
|
}
|
|
},
|
|
enableDetailedLogging: true,
|
|
keepAliveTreatment: 'extended', // Allow long-lived connections
|
|
inactivityTimeout: 3600000, // 1 hour
|
|
socketTimeout: 3600000, // 1 hour
|
|
keepAlive: true,
|
|
keepAliveInitialDelay: 1000
|
|
});
|
|
|
|
await testProxy.start();
|
|
});
|
|
|
|
tap.test('should keep WebSocket-like connection open for extended period', async (tools) => {
|
|
tools.timeout(65000); // 65 second test timeout
|
|
|
|
const client = new net.Socket();
|
|
let messagesReceived = 0;
|
|
let connectionClosed = false;
|
|
|
|
// Connect to proxy
|
|
await new Promise<void>((resolve, reject) => {
|
|
client.connect(8888, 'localhost', () => {
|
|
console.log('Client connected to proxy');
|
|
resolve();
|
|
});
|
|
|
|
client.on('error', reject);
|
|
});
|
|
|
|
// Set up data handler
|
|
client.on('data', (data) => {
|
|
console.log(`Client received: ${data.toString().trim()}`);
|
|
messagesReceived++;
|
|
});
|
|
|
|
client.on('close', () => {
|
|
console.log('Client connection closed');
|
|
connectionClosed = true;
|
|
});
|
|
|
|
// Send initial handshake-like data
|
|
client.write('HELLO\n');
|
|
|
|
// Wait for response
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
expect(messagesReceived).toEqual(1);
|
|
|
|
// Simulate WebSocket-like keep-alive pattern
|
|
// Send periodic messages over 60 seconds
|
|
const startTime = Date.now();
|
|
const pingInterval = setInterval(() => {
|
|
if (!connectionClosed && Date.now() - startTime < 60000) {
|
|
console.log('Sending ping...');
|
|
client.write('PING\n');
|
|
} else {
|
|
clearInterval(pingInterval);
|
|
}
|
|
}, 10000); // Every 10 seconds
|
|
|
|
// Wait for 61 seconds
|
|
await new Promise(resolve => setTimeout(resolve, 61000));
|
|
|
|
// Clean up interval
|
|
clearInterval(pingInterval);
|
|
|
|
// Connection should still be open
|
|
expect(connectionClosed).toEqual(false);
|
|
|
|
// Should have received responses (1 hello + 6 pings)
|
|
expect(messagesReceived).toBeGreaterThan(5);
|
|
|
|
// Close connection gracefully
|
|
client.end();
|
|
|
|
// Wait for close
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
expect(connectionClosed).toEqual(true);
|
|
});
|
|
|
|
tap.test('should support half-open connections', async () => {
|
|
const client = new net.Socket();
|
|
const serverSocket = await new Promise<net.Socket>((resolve) => {
|
|
targetServer.once('connection', resolve);
|
|
client.connect(8888, 'localhost');
|
|
});
|
|
|
|
let clientClosed = false;
|
|
let serverClosed = false;
|
|
let serverReceivedData = false;
|
|
|
|
client.on('close', () => {
|
|
clientClosed = true;
|
|
});
|
|
|
|
serverSocket.on('close', () => {
|
|
serverClosed = true;
|
|
});
|
|
|
|
serverSocket.on('data', () => {
|
|
serverReceivedData = true;
|
|
});
|
|
|
|
// Client sends data then closes write side
|
|
client.write('HALF-OPEN TEST\n');
|
|
client.end(); // Close write side only
|
|
|
|
// Wait a bit
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Server should still be able to send data
|
|
expect(serverClosed).toEqual(false);
|
|
serverSocket.write('RESPONSE\n');
|
|
|
|
// Wait for data
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Now close server side
|
|
serverSocket.end();
|
|
|
|
// Wait for full close
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
expect(clientClosed).toEqual(true);
|
|
expect(serverClosed).toEqual(true);
|
|
expect(serverReceivedData).toEqual(true);
|
|
});
|
|
|
|
tap.test('cleanup', async () => {
|
|
await testProxy.stop();
|
|
|
|
await new Promise<void>((resolve) => {
|
|
targetServer.close(() => {
|
|
console.log('Target server closed');
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
export default tap.start(); |