dcrouter/test/suite/smtpclient_error-handling/test.cerr-08.rate-limiting.ts
2025-05-24 17:00:59 +00:00

573 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();