296 lines
8.4 KiB
TypeScript
296 lines
8.4 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
|
|
import type { ITestServer } from '../../helpers/server.loader.js';
|
|
const TEST_PORT = 2525;
|
|
const TEST_TIMEOUT = 30000;
|
|
|
|
let testServer: ITestServer;
|
|
|
|
tap.test('setup - start SMTP server for connection rejection tests', async () => {
|
|
testServer = await startTestServer({ port: TEST_PORT });
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
});
|
|
|
|
tap.test('Connection Rejection - should handle suspicious domains', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner
|
|
const banner = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
expect(banner).toInclude('220');
|
|
|
|
// Send EHLO with suspicious domain
|
|
socket.write('EHLO blocked.spammer.com\r\n');
|
|
|
|
const response = await new Promise<string>((resolve) => {
|
|
let data = '';
|
|
const handler = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
if (data.includes('\r\n')) {
|
|
socket.removeListener('data', handler);
|
|
resolve(data);
|
|
}
|
|
};
|
|
socket.on('data', handler);
|
|
|
|
// Timeout after 5 seconds
|
|
setTimeout(() => {
|
|
socket.removeListener('data', handler);
|
|
resolve(data || 'TIMEOUT');
|
|
}, 5000);
|
|
});
|
|
|
|
console.log('Response to suspicious domain:', response);
|
|
|
|
// Server might reject with 421, 550, or accept (depends on configuration)
|
|
// We just verify it responds appropriately
|
|
const validResponses = ['250', '421', '550', '501'];
|
|
const hasValidResponse = validResponses.some(code => response.includes(code));
|
|
expect(hasValidResponse).toEqual(true);
|
|
|
|
// Clean up
|
|
if (!socket.destroyed) {
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
}
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('Connection Rejection - should handle overload conditions', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
const connections: net.Socket[] = [];
|
|
|
|
try {
|
|
// Create many connections rapidly
|
|
const rapidConnectionCount = 20; // Reduced from 50 to be more reasonable
|
|
const connectionPromises: Promise<net.Socket | null>[] = [];
|
|
|
|
for (let i = 0; i < rapidConnectionCount; i++) {
|
|
connectionPromises.push(
|
|
new Promise((resolve) => {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT
|
|
});
|
|
|
|
socket.on('connect', () => {
|
|
connections.push(socket);
|
|
resolve(socket);
|
|
});
|
|
|
|
socket.on('error', () => {
|
|
// Connection rejected - this is OK during overload
|
|
resolve(null);
|
|
});
|
|
|
|
// Timeout individual connections
|
|
setTimeout(() => resolve(null), 2000);
|
|
})
|
|
);
|
|
}
|
|
|
|
// Wait for all connection attempts
|
|
const results = await Promise.all(connectionPromises);
|
|
const successfulConnections = results.filter(r => r !== null).length;
|
|
|
|
console.log(`Created ${successfulConnections}/${rapidConnectionCount} connections`);
|
|
|
|
// Now try one more connection
|
|
let overloadRejected = false;
|
|
try {
|
|
const testSocket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: 5000
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
testSocket.once('connect', () => {
|
|
testSocket.end();
|
|
resolve();
|
|
});
|
|
testSocket.once('error', (err) => {
|
|
overloadRejected = true;
|
|
reject(err);
|
|
});
|
|
|
|
setTimeout(() => {
|
|
testSocket.destroy();
|
|
resolve();
|
|
}, 5000);
|
|
});
|
|
} catch (error) {
|
|
console.log('Additional connection was rejected:', error);
|
|
overloadRejected = true;
|
|
}
|
|
|
|
console.log(`Overload test results:
|
|
- Successful connections: ${successfulConnections}
|
|
- Additional connection rejected: ${overloadRejected}
|
|
- Server behavior: ${overloadRejected ? 'Properly rejected under load' : 'Accepted all connections'}`);
|
|
|
|
// Either behavior is acceptable - rejection shows overload protection,
|
|
// acceptance shows high capacity
|
|
expect(true).toEqual(true);
|
|
|
|
} finally {
|
|
// Clean up all connections
|
|
for (const socket of connections) {
|
|
try {
|
|
if (!socket.destroyed) {
|
|
socket.end();
|
|
}
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('Connection Rejection - should reject invalid protocol', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner first
|
|
const banner = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
console.log('Got banner:', banner);
|
|
|
|
// Send HTTP request instead of SMTP
|
|
socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n');
|
|
|
|
const response = await new Promise<string>((resolve) => {
|
|
let data = '';
|
|
const handler = (chunk: Buffer) => {
|
|
data += chunk.toString();
|
|
};
|
|
socket.on('data', handler);
|
|
|
|
// Wait for response or connection close
|
|
socket.on('close', () => {
|
|
socket.removeListener('data', handler);
|
|
resolve(data);
|
|
});
|
|
|
|
// Timeout
|
|
setTimeout(() => {
|
|
socket.removeListener('data', handler);
|
|
socket.destroy();
|
|
resolve(data || 'CLOSED_WITHOUT_RESPONSE');
|
|
}, 3000);
|
|
});
|
|
|
|
console.log('Response to HTTP request:', response);
|
|
|
|
// Server should either:
|
|
// - Send error response (500, 501, 502, 421)
|
|
// - Close connection immediately
|
|
// - Send nothing and close
|
|
const errorResponses = ['500', '501', '502', '421'];
|
|
const hasErrorResponse = errorResponses.some(code => response.includes(code));
|
|
const closedWithoutResponse = response === 'CLOSED_WITHOUT_RESPONSE' || response === '';
|
|
|
|
expect(hasErrorResponse || closedWithoutResponse).toEqual(true);
|
|
|
|
if (hasErrorResponse) {
|
|
console.log('Server properly rejected with error response');
|
|
} else if (closedWithoutResponse) {
|
|
console.log('Server closed connection without response (also valid)');
|
|
}
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('Connection Rejection - should handle invalid commands gracefully', async (tools) => {
|
|
const done = tools.defer();
|
|
|
|
try {
|
|
const socket = net.createConnection({
|
|
host: 'localhost',
|
|
port: TEST_PORT,
|
|
timeout: TEST_TIMEOUT
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.once('connect', () => resolve());
|
|
socket.once('error', reject);
|
|
});
|
|
|
|
// Get banner
|
|
await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
// Send completely invalid command
|
|
socket.write('INVALID_COMMAND_12345\r\n');
|
|
|
|
const response = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
console.log('Response to invalid command:', response);
|
|
|
|
// Should get 500 or 502 error
|
|
expect(response).toMatch(/^5\d{2}/);
|
|
|
|
// Server should still be responsive
|
|
socket.write('NOOP\r\n');
|
|
|
|
const noopResponse = await new Promise<string>((resolve) => {
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
});
|
|
|
|
console.log('NOOP response after error:', noopResponse);
|
|
expect(noopResponse).toInclude('250');
|
|
|
|
// Clean up
|
|
socket.write('QUIT\r\n');
|
|
socket.end();
|
|
|
|
} finally {
|
|
done.resolve();
|
|
}
|
|
});
|
|
|
|
tap.test('cleanup - stop SMTP server', async () => {
|
|
await stopTestServer(testServer);
|
|
expect(true).toEqual(true);
|
|
});
|
|
|
|
export default tap.start(); |