smartproxy/test/test.route-security.ts

275 lines
7.0 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as smartproxy from '../ts/index.js';
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
import * as net from 'net';
tap.test('route-specific security should be enforced', async () => {
// Create a simple echo server for testing
const echoServer = net.createServer((socket) => {
socket.on('data', (data) => {
socket.write(data);
});
});
await new Promise<void>((resolve) => {
echoServer.listen(8877, '127.0.0.1', () => {
console.log('Echo server listening on port 8877');
resolve();
});
});
// Create proxy with route-specific security
const routes: IRouteConfig[] = [{
name: 'secure-route',
match: {
ports: 8878
},
action: {
type: 'forward',
target: {
host: '127.0.0.1',
port: 8877
}
},
security: {
ipAllowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
}
}];
const proxy = new smartproxy.SmartProxy({
enableDetailedLogging: true,
routes: routes
});
await proxy.start();
// Test 1: Connection from allowed IP should work
const client1 = new net.Socket();
const connected = await new Promise<boolean>((resolve) => {
client1.connect(8878, '127.0.0.1', () => {
console.log('Client connected from allowed IP');
resolve(true);
});
client1.on('error', (err) => {
console.log('Connection error:', err.message);
resolve(false);
});
// Set timeout to prevent hanging
setTimeout(() => resolve(false), 2000);
});
if (connected) {
// Test echo
const testData = 'Hello from allowed IP';
client1.write(testData);
const response = await new Promise<string>((resolve) => {
client1.once('data', (data) => {
resolve(data.toString());
});
setTimeout(() => resolve(''), 2000);
});
expect(response).toEqual(testData);
client1.destroy();
} else {
expect(connected).toBeTrue();
}
// Clean up
await proxy.stop();
await new Promise<void>((resolve) => {
echoServer.close(() => resolve());
});
});
tap.test('route-specific IP block list should be enforced', async () => {
// Create a simple echo server for testing
const echoServer = net.createServer((socket) => {
socket.on('data', (data) => {
socket.write(data);
});
});
await new Promise<void>((resolve) => {
echoServer.listen(8879, '127.0.0.1', () => {
console.log('Echo server listening on port 8879');
resolve();
});
});
// Create proxy with route-specific block list
const routes: IRouteConfig[] = [{
name: 'blocked-route',
match: {
ports: 8880
},
action: {
type: 'forward',
target: {
host: '127.0.0.1',
port: 8879
}
},
security: {
ipAllowList: ['0.0.0.0/0', '::/0'], // Allow all IPs
ipBlockList: ['127.0.0.1', '::1', '::ffff:127.0.0.1'] // But block localhost
}
}];
const proxy = new smartproxy.SmartProxy({
enableDetailedLogging: true,
routes: routes
});
await proxy.start();
// Test: Connection from blocked IP should fail or be immediately closed
const client = new net.Socket();
let connectionSuccessful = false;
const result = await new Promise<{ connected: boolean; dataReceived: boolean }>((resolve) => {
let resolved = false;
let dataReceived = false;
const doResolve = (connected: boolean) => {
if (!resolved) {
resolved = true;
resolve({ connected, dataReceived });
}
};
client.connect(8880, '127.0.0.1', () => {
console.log('Client connect event fired');
connectionSuccessful = true;
// Try to send data to test if the connection is really established
try {
client.write('test data');
} catch (e) {
console.log('Write failed:', e.message);
}
});
client.on('data', () => {
dataReceived = true;
});
client.on('error', (err) => {
console.log('Connection error:', err.message);
doResolve(false);
});
client.on('close', () => {
console.log('Connection closed, connectionSuccessful:', connectionSuccessful, 'dataReceived:', dataReceived);
doResolve(connectionSuccessful);
});
// Set timeout
setTimeout(() => doResolve(connectionSuccessful), 1000);
});
// The connection should either fail to connect OR connect but immediately close without data exchange
if (result.connected) {
// If connected, it should have been immediately closed without data exchange
expect(result.dataReceived).toBeFalse();
console.log('Connection was established but immediately closed (acceptable behavior)');
} else {
// Connection failed entirely (also acceptable)
expect(result.connected).toBeFalse();
console.log('Connection was blocked entirely (preferred behavior)');
}
if (client.readyState !== 'closed') {
client.destroy();
}
// Clean up
await proxy.stop();
await new Promise<void>((resolve) => {
echoServer.close(() => resolve());
});
});
tap.test('routes without security should allow all connections', async () => {
// Create a simple echo server for testing
const echoServer = net.createServer((socket) => {
socket.on('data', (data) => {
socket.write(data);
});
});
await new Promise<void>((resolve) => {
echoServer.listen(8881, '127.0.0.1', () => {
console.log('Echo server listening on port 8881');
resolve();
});
});
// Create proxy without route-specific security
const routes: IRouteConfig[] = [{
name: 'open-route',
match: {
ports: 8882
},
action: {
type: 'forward',
target: {
host: '127.0.0.1',
port: 8881
}
// No security section - should allow all
}
}];
const proxy = new smartproxy.SmartProxy({
enableDetailedLogging: true,
routes: routes
});
await proxy.start();
// Test: Connection should work without security restrictions
const client = new net.Socket();
const connected = await new Promise<boolean>((resolve) => {
client.connect(8882, '127.0.0.1', () => {
console.log('Client connected to open route');
resolve(true);
});
client.on('error', (err) => {
console.log('Connection error:', err.message);
resolve(false);
});
// Set timeout
setTimeout(() => resolve(false), 2000);
});
expect(connected).toBeTrue();
if (connected) {
// Test echo
const testData = 'Hello from open route';
client.write(testData);
const response = await new Promise<string>((resolve) => {
client.once('data', (data) => {
resolve(data.toString());
});
setTimeout(() => resolve(''), 2000);
});
expect(response).toEqual(testData);
client.destroy();
}
// Clean up
await proxy.stop();
await new Promise<void>((resolve) => {
echoServer.close(() => resolve());
});
});
export default tap.start();