dcrouter/test/suite/smtpclient_error-handling/test.cerr-08.rate-limiting.ts

573 lines
16 KiB
TypeScript
Raw Normal View History

2025-05-24 17:00:59 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as net from 'net';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CERR-08: Connection rate limiting', async () => {
// Create server with connection rate limiting
let connectionCount = 0;
let connectionTimes: number[] = [];
const maxConnectionsPerMinute = 10;
const rateLimitServer = net.createServer((socket) => {
const now = Date.now();
connectionTimes.push(now);
connectionCount++;
// Remove old connection times (older than 1 minute)
connectionTimes = connectionTimes.filter(time => now - time < 60000);
if (connectionTimes.length > maxConnectionsPerMinute) {
socket.write('421 4.7.0 Too many connections, please try again later\r\n');
socket.end();
return;
}
socket.write('220 Rate Limit Test Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('250 OK\r\n');
}
});
});
await new Promise<void>((resolve) => {
rateLimitServer.listen(0, '127.0.0.1', () => resolve());
});
const rateLimitPort = (rateLimitServer.address() as net.AddressInfo).port;
console.log('\nTesting connection rate limiting...');
console.log(`Server limit: ${maxConnectionsPerMinute} connections per minute`);
// Try to make many connections rapidly
const connections: any[] = [];
let accepted = 0;
let rejected = 0;
for (let i = 0; i < 15; i++) {
try {
const client = createSmtpClient({
host: '127.0.0.1',
port: rateLimitPort,
secure: false,
connectionTimeout: 2000,
debug: false
});
await client.connect();
accepted++;
connections.push(client);
console.log(` Connection ${i + 1}: Accepted`);
} catch (error) {
rejected++;
console.log(` Connection ${i + 1}: Rejected - ${error.message}`);
expect(error.message).toMatch(/421|too many|rate/i);
}
}
console.log(`\nResults: ${accepted} accepted, ${rejected} rejected`);
expect(rejected).toBeGreaterThan(0); // Some should be rate limited
// Clean up connections
for (const client of connections) {
await client.close();
}
rateLimitServer.close();
});
tap.test('CERR-08: Message rate limiting', async () => {
// Create server with message rate limiting
const messageRateLimits: { [key: string]: { count: number; resetTime: number } } = {};
const messagesPerHour = 100;
const messageRateLimitServer = net.createServer((socket) => {
let senderAddress = '';
socket.write('220 Message Rate Limit Server\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM')) {
const match = command.match(/<([^>]+)>/);
if (match) {
senderAddress = match[1];
const now = Date.now();
if (!messageRateLimits[senderAddress]) {
messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 };
}
// Reset if hour has passed
if (now > messageRateLimits[senderAddress].resetTime) {
messageRateLimits[senderAddress] = { count: 0, resetTime: now + 3600000 };
}
messageRateLimits[senderAddress].count++;
if (messageRateLimits[senderAddress].count > messagesPerHour) {
socket.write(`421 4.7.0 Message rate limit exceeded (${messagesPerHour}/hour)\r\n`);
return;
}
}
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Send data\r\n');
} else if (command === '.') {
const remaining = messagesPerHour - messageRateLimits[senderAddress].count;
socket.write(`250 OK (${remaining} messages remaining this hour)\r\n`);
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
await new Promise<void>((resolve) => {
messageRateLimitServer.listen(0, '127.0.0.1', () => resolve());
});
const messageRateLimitPort = (messageRateLimitServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: messageRateLimitPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
console.log('\nTesting message rate limiting...');
console.log(`Server limit: ${messagesPerHour} messages per hour per sender`);
await smtpClient.connect();
// Simulate sending many messages
const testMessageCount = 10;
const sender = 'bulk-sender@example.com';
for (let i = 0; i < testMessageCount; i++) {
const email = new Email({
from: sender,
to: [`recipient${i}@example.com`],
subject: `Test message ${i + 1}`,
text: 'Testing message rate limits'
});
try {
const result = await smtpClient.sendMail(email);
// Extract remaining count from response
const remainingMatch = result.response?.match(/(\d+) messages remaining/);
if (remainingMatch) {
console.log(` Message ${i + 1}: Sent (${remainingMatch[1]} remaining)`);
} else {
console.log(` Message ${i + 1}: Sent`);
}
} catch (error) {
console.log(` Message ${i + 1}: Rate limited - ${error.message}`);
}
}
await smtpClient.close();
messageRateLimitServer.close();
});
tap.test('CERR-08: Recipient rate limiting', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
console.log('\nTesting recipient rate limiting...');
// Test different recipient rate limit scenarios
const recipientTests = [
{
name: 'Many recipients in single message',
recipients: Array.from({ length: 200 }, (_, i) => `user${i}@example.com`),
expectedLimit: 100
},
{
name: 'Rapid sequential messages',
recipients: Array.from({ length: 50 }, (_, i) => `rapid${i}@example.com`),
delay: 0
}
];
for (const test of recipientTests) {
console.log(`\n${test.name}:`);
const email = new Email({
from: 'sender@example.com',
to: test.recipients,
subject: test.name,
text: 'Testing recipient limits'
});
let acceptedCount = 0;
let rejectedCount = 0;
// Monitor RCPT TO responses
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const response = await originalSendCommand(command);
if (command.startsWith('RCPT TO')) {
if (response.startsWith('250')) {
acceptedCount++;
} else if (response.match(/^[45]/)) {
rejectedCount++;
if (response.match(/rate|limit|too many|slow down/i)) {
console.log(` Rate limit hit after ${acceptedCount} recipients`);
}
}
}
return response;
};
try {
await smtpClient.sendMail(email);
console.log(` All ${acceptedCount} recipients accepted`);
} catch (error) {
console.log(` Accepted: ${acceptedCount}, Rejected: ${rejectedCount}`);
console.log(` Error: ${error.message}`);
}
await smtpClient.sendCommand('RSET');
}
await smtpClient.close();
});
tap.test('CERR-08: Rate limit response codes', async () => {
console.log('\nCommon rate limiting response codes:');
const rateLimitCodes = [
{
code: '421 4.7.0',
message: 'Too many connections',
type: 'Connection rate limit',
action: 'Close connection, retry later'
},
{
code: '450 4.7.1',
message: 'Rate limit exceeded, try again later',
type: 'Command rate limit',
action: 'Temporary failure, queue and retry'
},
{
code: '451 4.7.1',
message: 'Please slow down',
type: 'Throttling request',
action: 'Add delay before next command'
},
{
code: '452 4.5.3',
message: 'Too many recipients',
type: 'Recipient limit',
action: 'Split into multiple messages'
},
{
code: '454 4.7.0',
message: 'Temporary authentication failure',
type: 'Auth rate limit',
action: 'Delay and retry authentication'
},
{
code: '550 5.7.1',
message: 'Daily sending quota exceeded',
type: 'Hard quota limit',
action: 'Stop sending until quota resets'
}
];
rateLimitCodes.forEach(limit => {
console.log(`\n${limit.code} ${limit.message}`);
console.log(` Type: ${limit.type}`);
console.log(` Action: ${limit.action}`);
});
});
tap.test('CERR-08: Adaptive rate limiting', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
adaptiveRateLimit: true,
initialDelay: 100, // Start with 100ms between commands
maxDelay: 5000, // Max 5 seconds between commands
debug: true
});
await smtpClient.connect();
console.log('\nTesting adaptive rate limiting...');
// Track delays
const delays: number[] = [];
let lastCommandTime = Date.now();
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const now = Date.now();
const delay = now - lastCommandTime;
delays.push(delay);
lastCommandTime = now;
return originalSendCommand(command);
};
// Send multiple emails and observe delay adaptation
for (let i = 0; i < 5; i++) {
const email = new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Adaptive test ${i + 1}`,
text: 'Testing adaptive rate limiting'
});
try {
await smtpClient.sendMail(email);
console.log(` Email ${i + 1}: Sent with ${delays[delays.length - 1]}ms delay`);
} catch (error) {
console.log(` Email ${i + 1}: Failed - ${error.message}`);
// Check if delay increased
if (delays.length > 1) {
const lastDelay = delays[delays.length - 1];
const previousDelay = delays[delays.length - 2];
if (lastDelay > previousDelay) {
console.log(` Delay increased from ${previousDelay}ms to ${lastDelay}ms`);
}
}
}
}
await smtpClient.close();
});
tap.test('CERR-08: Rate limit headers', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
console.log('\nChecking for rate limit information in responses...');
// Send email and monitor for rate limit headers
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Rate limit header test',
text: 'Checking for rate limit information'
});
// Monitor responses for rate limit info
const rateLimitInfo: string[] = [];
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
smtpClient.sendCommand = async (command: string) => {
const response = await originalSendCommand(command);
// Look for rate limit information in responses
const patterns = [
/X-RateLimit-Limit: (\d+)/i,
/X-RateLimit-Remaining: (\d+)/i,
/X-RateLimit-Reset: (\d+)/i,
/(\d+) requests? remaining/i,
/limit.* (\d+) per/i,
/retry.* (\d+) seconds?/i
];
patterns.forEach(pattern => {
const match = response.match(pattern);
if (match) {
rateLimitInfo.push(match[0]);
}
});
return response;
};
await smtpClient.sendMail(email);
if (rateLimitInfo.length > 0) {
console.log('Rate limit information found:');
rateLimitInfo.forEach(info => console.log(` ${info}`));
} else {
console.log('No rate limit information in responses');
}
await smtpClient.close();
});
tap.test('CERR-08: Distributed rate limiting', async () => {
console.log('\nDistributed rate limiting strategies:');
const strategies = [
{
name: 'Token bucket',
description: 'Fixed number of tokens replenished at constant rate',
pros: 'Allows bursts, smooth rate control',
cons: 'Can be complex to implement distributed'
},
{
name: 'Sliding window',
description: 'Count requests in moving time window',
pros: 'More accurate than fixed windows',
cons: 'Higher memory usage'
},
{
name: 'Fixed window',
description: 'Reset counter at fixed intervals',
pros: 'Simple to implement',
cons: 'Can allow 2x rate at window boundaries'
},
{
name: 'Leaky bucket',
description: 'Queue with constant drain rate',
pros: 'Smooth output rate',
cons: 'Can drop messages if bucket overflows'
}
];
strategies.forEach(strategy => {
console.log(`\n${strategy.name}:`);
console.log(` Description: ${strategy.description}`);
console.log(` Pros: ${strategy.pros}`);
console.log(` Cons: ${strategy.cons}`);
});
// Simulate distributed rate limiting
const distributedLimiter = {
nodes: ['server1', 'server2', 'server3'],
globalLimit: 1000, // 1000 messages per minute globally
perNodeLimit: 400, // Each node can handle 400/min
currentCounts: { server1: 0, server2: 0, server3: 0 }
};
console.log('\n\nSimulating distributed rate limiting:');
console.log(`Global limit: ${distributedLimiter.globalLimit}/min`);
console.log(`Per-node limit: ${distributedLimiter.perNodeLimit}/min`);
// Simulate load distribution
for (let i = 0; i < 20; i++) {
// Pick least loaded node
const node = distributedLimiter.nodes.reduce((min, node) =>
distributedLimiter.currentCounts[node] < distributedLimiter.currentCounts[min] ? node : min
);
distributedLimiter.currentCounts[node]++;
if (i % 5 === 4) {
console.log(`\nAfter ${i + 1} messages:`);
distributedLimiter.nodes.forEach(n => {
console.log(` ${n}: ${distributedLimiter.currentCounts[n]} messages`);
});
}
}
});
tap.test('CERR-08: Rate limit bypass strategies', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
console.log('\nLegitimate rate limit management strategies:');
// 1. Message batching
console.log('\n1. Message batching:');
const recipients = Array.from({ length: 50 }, (_, i) => `user${i}@example.com`);
const batchSize = 10;
for (let i = 0; i < recipients.length; i += batchSize) {
const batch = recipients.slice(i, i + batchSize);
const email = new Email({
from: 'sender@example.com',
to: batch,
subject: 'Batched message',
text: 'Sending in batches to respect rate limits'
});
console.log(` Batch ${Math.floor(i/batchSize) + 1}: ${batch.length} recipients`);
try {
await smtpClient.sendMail(email);
// Add delay between batches
if (i + batchSize < recipients.length) {
console.log(' Waiting 2 seconds before next batch...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (error) {
console.log(` Batch failed: ${error.message}`);
}
}
// 2. Connection pooling with limits
console.log('\n2. Connection pooling:');
console.log(' Using multiple connections with per-connection limits');
console.log(' Example: 5 connections × 20 msg/min = 100 msg/min total');
// 3. Retry with backoff
console.log('\n3. Exponential backoff on rate limits:');
const backoffDelays = [1, 2, 4, 8, 16, 32];
backoffDelays.forEach((delay, attempt) => {
console.log(` Attempt ${attempt + 1}: Wait ${delay} seconds`);
});
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();