import * as plugins from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('ERR-05: Resource exhaustion handling - Connection limit', async (tools) => { const done = tools.defer(); const connections: net.Socket[] = []; const maxAttempts = 150; // Try to exceed typical connection limits let exhaustionDetected = false; let connectionsEstablished = 0; let lastError: string | null = null; try { for (let i = 0; i < maxAttempts; i++) { try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 5000 }); await new Promise((resolve, reject) => { socket.once('connect', () => { connections.push(socket); connectionsEstablished++; resolve(); }); socket.once('error', (err) => { reject(err); }); }); // Try EHLO on each connection const response = await new Promise((resolve) => { let data = ''; socket.once('data', (chunk) => { data += chunk.toString(); if (data.includes('\r\n')) { resolve(data); } }); }); // Send EHLO socket.write('EHLO testhost\r\n'); const ehloResponse = await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && data.includes('\r\n')) { socket.removeListener('data', handleData); resolve(data); } }; socket.on('data', handleData); }); // Check for resource exhaustion indicators if (ehloResponse.includes('421') || ehloResponse.includes('too many') || ehloResponse.includes('limit') || ehloResponse.includes('resource')) { exhaustionDetected = true; break; } // Small delay every 10 connections to avoid overwhelming if (i % 10 === 0 && i > 0) { await new Promise(resolve => setTimeout(resolve, 50)); } } catch (err) { const error = err as Error; lastError = error.message; // Connection refused or resource errors indicate exhaustion handling if (error.message.includes('ECONNREFUSED') || error.message.includes('EMFILE') || error.message.includes('ENFILE') || error.message.includes('too many') || error.message.includes('resource')) { exhaustionDetected = true; break; } // For other errors, continue trying } } // Clean up connections for (const socket of connections) { try { if (!socket.destroyed) { socket.write('QUIT\r\n'); socket.end(); } } catch (e) { // Ignore cleanup errors } } // Wait for connections to close await new Promise(resolve => setTimeout(resolve, 500)); // Test passes if we either: // 1. Detected resource exhaustion (server properly limits connections) // 2. Established fewer connections than attempted (server has limits) const hasResourceProtection = exhaustionDetected || connectionsEstablished < maxAttempts; console.log(`Connections established: ${connectionsEstablished}/${maxAttempts}`); console.log(`Exhaustion detected: ${exhaustionDetected}`); if (lastError) console.log(`Last error: ${lastError}`); expect(hasResourceProtection).toEqual(true); done.resolve(); } catch (error) { console.error('Test error:', error); done.reject(error); } }); tap.test('ERR-05: Resource exhaustion handling - Memory limits', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('connect', async () => { try { // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && data.includes('\r\n')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); // Try to send a very large email that might exhaust memory socket.write('MAIL FROM:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write('RCPT TO:\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toInclude('250'); resolve(); }); }); socket.write('DATA\r\n'); const dataResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); expect(dataResponse).toInclude('354'); // Try to send extremely large headers to test memory limits const largeHeader = 'X-Test-Header: ' + 'A'.repeat(1024 * 100) + '\r\n'; let resourceError = false; try { // Send multiple large headers for (let i = 0; i < 100; i++) { socket.write(largeHeader); // Check if socket is still writable if (!socket.writable) { resourceError = true; break; } } socket.write('\r\n.\r\n'); const endResponse = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout waiting for response')); }, 10000); socket.once('data', (chunk) => { clearTimeout(timeout); resolve(chunk.toString()); }); socket.once('error', (err) => { clearTimeout(timeout); // Connection errors during large data handling indicate resource protection resourceError = true; resolve(''); }); }); // Check for resource protection responses if (endResponse.includes('552') || // Message too large endResponse.includes('451') || // Temporary failure endResponse.includes('421') || // Service unavailable endResponse.includes('resource') || endResponse.includes('memory') || endResponse.includes('limit')) { resourceError = true; } // Resource protection is working if we got an error or protective response expect(resourceError || endResponse.includes('552') || endResponse.includes('451')).toEqual(true); } catch (err) { // Errors during large data transmission indicate resource protection console.log('Expected resource protection error:', err); expect(true).toEqual(true); } socket.write('QUIT\r\n'); socket.end(); done.resolve(); } catch (error) { socket.end(); done.reject(error); } }); socket.on('error', (error) => { done.reject(error); }); }); tap.test('cleanup server', async () => { await stopTestServer(); }); tap.start();