257 lines
7.6 KiB
TypeScript
257 lines
7.6 KiB
TypeScript
import { expect, tap } from '@push.rocks/tapbundle';
|
|
import * as net from 'net';
|
|
import { PortProxy } from '../ts/classes.portproxy.js';
|
|
|
|
let testServer: net.Server;
|
|
let portProxy: PortProxy;
|
|
const TEST_SERVER_PORT = 4000;
|
|
const PROXY_PORT = 4001;
|
|
const TEST_DATA = 'Hello through port proxy!';
|
|
|
|
// Helper function to create a test TCP server
|
|
function createTestServer(port: number): Promise<net.Server> {
|
|
return new Promise((resolve) => {
|
|
const server = net.createServer((socket) => {
|
|
socket.on('data', (data) => {
|
|
// Echo the received data back
|
|
socket.write(`Echo: ${data.toString()}`);
|
|
});
|
|
socket.on('error', (error) => {
|
|
console.error('[Test Server] Socket error:', error);
|
|
});
|
|
});
|
|
server.listen(port, () => {
|
|
console.log(`[Test Server] Listening on port ${port}`);
|
|
resolve(server);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Helper function to create a test client connection
|
|
function createTestClient(port: number, data: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const client = new net.Socket();
|
|
let response = '';
|
|
|
|
client.connect(port, 'localhost', () => {
|
|
console.log('[Test Client] Connected to server');
|
|
client.write(data);
|
|
});
|
|
client.on('data', (chunk) => {
|
|
response += chunk.toString();
|
|
client.end();
|
|
});
|
|
client.on('end', () => {
|
|
resolve(response);
|
|
});
|
|
client.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Setup test environment
|
|
tap.test('setup port proxy test environment', async () => {
|
|
testServer = await createTestServer(TEST_SERVER_PORT);
|
|
portProxy = new PortProxy({
|
|
fromPort: PROXY_PORT,
|
|
toPort: TEST_SERVER_PORT,
|
|
targetIP: 'localhost',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
globalPortRanges: []
|
|
});
|
|
});
|
|
|
|
tap.test('should start port proxy', async () => {
|
|
await portProxy.start();
|
|
// Since netServers is private, we cast to any to verify that all created servers are listening.
|
|
expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue();
|
|
});
|
|
|
|
tap.test('should forward TCP connections and data to localhost', async () => {
|
|
const response = await createTestClient(PROXY_PORT, TEST_DATA);
|
|
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
|
});
|
|
|
|
tap.test('should forward TCP connections to custom host', async () => {
|
|
// Create a new proxy instance with a custom host (targetIP)
|
|
const customHostProxy = new PortProxy({
|
|
fromPort: PROXY_PORT + 1,
|
|
toPort: TEST_SERVER_PORT,
|
|
targetIP: '127.0.0.1',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
globalPortRanges: []
|
|
});
|
|
|
|
await customHostProxy.start();
|
|
const response = await createTestClient(PROXY_PORT + 1, TEST_DATA);
|
|
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
|
await customHostProxy.stop();
|
|
});
|
|
|
|
tap.test('should forward connections based on domain-specific target IP', async () => {
|
|
// Create a second test server on a different port
|
|
const TEST_SERVER_PORT_2 = TEST_SERVER_PORT + 100;
|
|
const testServer2 = await createTestServer(TEST_SERVER_PORT_2);
|
|
|
|
// Create a proxy with domain-specific target IPs
|
|
const domainProxy = new PortProxy({
|
|
fromPort: PROXY_PORT + 2,
|
|
toPort: TEST_SERVER_PORT, // default port (for non-port-range handling)
|
|
targetIP: 'localhost', // default target IP
|
|
domains: [{
|
|
domain: 'domain1.test',
|
|
allowedIPs: ['127.0.0.1'],
|
|
targetIP: '127.0.0.1'
|
|
}, {
|
|
domain: 'domain2.test',
|
|
allowedIPs: ['127.0.0.1'],
|
|
targetIP: 'localhost'
|
|
}],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
globalPortRanges: []
|
|
});
|
|
|
|
await domainProxy.start();
|
|
|
|
// Test default connection (should use default targetIP)
|
|
const response1 = await createTestClient(PROXY_PORT + 2, TEST_DATA);
|
|
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
// Create another proxy with a different default targetIP
|
|
const domainProxy2 = new PortProxy({
|
|
fromPort: PROXY_PORT + 3,
|
|
toPort: TEST_SERVER_PORT,
|
|
targetIP: '127.0.0.1',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
globalPortRanges: []
|
|
});
|
|
|
|
await domainProxy2.start();
|
|
const response2 = await createTestClient(PROXY_PORT + 3, TEST_DATA);
|
|
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
await domainProxy.stop();
|
|
await domainProxy2.stop();
|
|
await new Promise<void>((resolve) => testServer2.close(() => resolve()));
|
|
});
|
|
|
|
tap.test('should handle multiple concurrent connections', async () => {
|
|
const concurrentRequests = 5;
|
|
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
|
|
createTestClient(PROXY_PORT, `${TEST_DATA} ${i + 1}`)
|
|
);
|
|
|
|
const responses = await Promise.all(requests);
|
|
|
|
responses.forEach((response, i) => {
|
|
expect(response).toEqual(`Echo: ${TEST_DATA} ${i + 1}`);
|
|
});
|
|
});
|
|
|
|
tap.test('should handle connection timeouts', async () => {
|
|
const client = new net.Socket();
|
|
await new Promise<void>((resolve) => {
|
|
client.connect(PROXY_PORT, 'localhost', () => {
|
|
// Don't send any data, just wait for timeout
|
|
client.on('close', () => {
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
tap.test('should stop port proxy', async () => {
|
|
await portProxy.stop();
|
|
expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue();
|
|
});
|
|
|
|
// Cleanup chained proxies tests
|
|
tap.test('should support optional source IP preservation in chained proxies', async () => {
|
|
// Test 1: Without IP preservation (default behavior)
|
|
const firstProxyDefault = new PortProxy({
|
|
fromPort: PROXY_PORT + 4,
|
|
toPort: PROXY_PORT + 5,
|
|
targetIP: 'localhost',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
|
|
globalPortRanges: []
|
|
});
|
|
|
|
const secondProxyDefault = new PortProxy({
|
|
fromPort: PROXY_PORT + 5,
|
|
toPort: TEST_SERVER_PORT,
|
|
targetIP: 'localhost',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
|
|
globalPortRanges: []
|
|
});
|
|
|
|
await secondProxyDefault.start();
|
|
await firstProxyDefault.start();
|
|
|
|
// This should work because we explicitly allow both IPv4 and IPv6 formats
|
|
const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
|
|
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
await firstProxyDefault.stop();
|
|
await secondProxyDefault.stop();
|
|
|
|
// Test 2: With IP preservation
|
|
const firstProxyPreserved = new PortProxy({
|
|
fromPort: PROXY_PORT + 6,
|
|
toPort: PROXY_PORT + 7,
|
|
targetIP: 'localhost',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
preserveSourceIP: true,
|
|
globalPortRanges: []
|
|
});
|
|
|
|
const secondProxyPreserved = new PortProxy({
|
|
fromPort: PROXY_PORT + 7,
|
|
toPort: TEST_SERVER_PORT,
|
|
targetIP: 'localhost',
|
|
domains: [],
|
|
sniEnabled: false,
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
preserveSourceIP: true,
|
|
globalPortRanges: []
|
|
});
|
|
|
|
await secondProxyPreserved.start();
|
|
await firstProxyPreserved.start();
|
|
|
|
// This should work with just IPv4 because source IP is preserved
|
|
const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
|
|
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
await firstProxyPreserved.stop();
|
|
await secondProxyPreserved.stop();
|
|
});
|
|
|
|
tap.test('cleanup port proxy test environment', async () => {
|
|
await new Promise<void>((resolve) => testServer.close(() => resolve()));
|
|
});
|
|
|
|
process.on('exit', () => {
|
|
if (testServer) {
|
|
testServer.close();
|
|
}
|
|
// Use a cast to access the private property for cleanup.
|
|
if (portProxy && (portProxy as any).netServers) {
|
|
portProxy.stop();
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |