dcrouter/test/suite/smtpclient_edge-cases/test.cedge-03.protocol-violations.ts
2025-05-26 10:35:50 +00:00

446 lines
13 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as net from 'net';
let testServer: ITestServer;
tap.test('setup test SMTP server', async () => {
testServer = await startTestServer({
port: 2572,
tlsEnabled: false,
authRequired: false
});
expect(testServer).toBeTruthy();
expect(testServer.port).toEqual(2572);
});
tap.test('CEDGE-03: Server closes connection during MAIL FROM', async () => {
// Create server that abruptly closes during MAIL FROM
const abruptServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let commandCount = 0;
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
commandCount++;
console.log(`Server received command ${commandCount}: "${line}"`);
if (line.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
// Abruptly close connection
console.log('Server closing connection unexpectedly');
socket.destroy();
}
});
});
});
await new Promise<void>((resolve) => {
abruptServer.listen(0, '127.0.0.1', () => resolve());
});
const abruptPort = (abruptServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: abruptPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Connection closure test',
text: 'Testing unexpected disconnection'
});
try {
const result = await smtpClient.sendMail(email);
// Should not succeed due to connection closure
expect(result.success).toBeFalse();
console.log('✅ Client handled abrupt connection closure gracefully');
} catch (error) {
// Expected to fail due to connection closure
console.log('✅ Client threw expected error for connection closure:', error.message);
expect(error.message).toMatch(/closed|reset|abort|end|timeout/i);
}
await smtpClient.close();
abruptServer.close();
});
tap.test('CEDGE-03: Server sends invalid response codes', async () => {
// Create server that sends non-standard response codes
const invalidServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
let inData = false;
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
if (inData) {
if (line === '.') {
socket.write('999 Invalid response code\r\n'); // Invalid 9xx code
inData = false;
}
} else if (line.startsWith('EHLO')) {
socket.write('150 Intermediate response\r\n'); // Invalid for EHLO
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
inData = true;
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
invalidServer.listen(0, '127.0.0.1', () => resolve());
});
const invalidPort = (invalidServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: invalidPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
try {
// This will likely fail due to invalid EHLO response
const verified = await smtpClient.verify();
expect(verified).toBeFalse();
console.log('✅ Client rejected invalid response codes');
} catch (error) {
console.log('✅ Client properly handled invalid response codes:', error.message);
}
await smtpClient.close();
invalidServer.close();
});
tap.test('CEDGE-03: Server sends malformed multi-line responses', async () => {
// Create server with malformed multi-line responses
const malformedServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
if (line.startsWith('EHLO')) {
// Malformed multi-line response (missing final line)
socket.write('250-mail.example.com\r\n');
socket.write('250-PIPELINING\r\n');
// Missing final 250 line - this violates RFC
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
malformedServer.listen(0, '127.0.0.1', () => resolve());
});
const malformedPort = (malformedServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: malformedPort,
secure: false,
connectionTimeout: 3000, // Shorter timeout for faster test
debug: true
});
try {
// Should timeout due to incomplete EHLO response
const verified = await smtpClient.verify();
// If we get here, the client accepted the malformed response
// This is acceptable if the client can work around it
if (verified === false) {
console.log('✅ Client rejected malformed multi-line response');
} else {
console.log('⚠️ Client accepted malformed multi-line response');
}
} catch (error) {
console.log('✅ Client handled malformed response with error:', error.message);
// Should timeout or error on malformed response
expect(error.message).toMatch(/timeout|Command timeout|Greeting timeout|response|parse/i);
}
// Force close since the connection might still be waiting
try {
await smtpClient.close();
} catch (closeError) {
// Ignore close errors
}
malformedServer.close();
});
tap.test('CEDGE-03: Server violates command sequence rules', async () => {
// Create server that accepts commands out of sequence
const sequenceServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
// Accept any command in any order (protocol violation)
if (line.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (line === '.') {
socket.write('250 Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
sequenceServer.listen(0, '127.0.0.1', () => resolve());
});
const sequencePort = (sequenceServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: sequencePort,
secure: false,
connectionTimeout: 5000,
debug: true
});
// Client should still work correctly despite server violations
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Sequence violation test',
text: 'Testing command sequence violations'
});
const result = await smtpClient.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Client maintains proper sequence despite server violations');
await smtpClient.close();
sequenceServer.close();
});
tap.test('CEDGE-03: Server sends responses without CRLF', async () => {
// Create server that sends responses with incorrect line endings
const crlfServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\n'); // LF only, not CRLF
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
if (line.startsWith('EHLO')) {
socket.write('250 OK\n'); // LF only
} else if (line === 'QUIT') {
socket.write('221 Bye\n'); // LF only
socket.end();
} else {
socket.write('250 OK\n'); // LF only
}
});
});
});
await new Promise<void>((resolve) => {
crlfServer.listen(0, '127.0.0.1', () => resolve());
});
const crlfPort = (crlfServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: crlfPort,
secure: false,
connectionTimeout: 5000,
debug: true
});
try {
const verified = await smtpClient.verify();
if (verified) {
console.log('✅ Client handled non-CRLF responses gracefully');
} else {
console.log('✅ Client rejected non-CRLF responses');
}
} catch (error) {
console.log('✅ Client handled CRLF violation with error:', error.message);
}
await smtpClient.close();
crlfServer.close();
});
tap.test('CEDGE-03: Server sends oversized responses', async () => {
// Create server that sends very long response lines
const oversizeServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
if (line.startsWith('EHLO')) {
// Send an extremely long response line (over RFC limit)
const longResponse = '250 ' + 'x'.repeat(2000) + '\r\n';
socket.write(longResponse);
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('250 OK\r\n');
}
});
});
});
await new Promise<void>((resolve) => {
oversizeServer.listen(0, '127.0.0.1', () => resolve());
});
const oversizePort = (oversizeServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: oversizePort,
secure: false,
connectionTimeout: 5000,
debug: true
});
try {
const verified = await smtpClient.verify();
console.log(`Verification with oversized response: ${verified}`);
console.log('✅ Client handled oversized response');
} catch (error) {
console.log('✅ Client handled oversized response with error:', error.message);
}
await smtpClient.close();
oversizeServer.close();
});
tap.test('CEDGE-03: Server violates RFC timing requirements', async () => {
// Create server that has excessive delays
const slowServer = net.createServer((socket) => {
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (!line && lines[lines.length - 1] === '') return;
console.log(`Server received: "${line}"`);
if (line.startsWith('EHLO')) {
// Extreme delay (violates RFC timing recommendations)
setTimeout(() => {
socket.write('250 OK\r\n');
}, 2000); // 2 second delay
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else {
socket.write('250 OK\r\n');
}
});
});
});
await new Promise<void>((resolve) => {
slowServer.listen(0, '127.0.0.1', () => resolve());
});
const slowPort = (slowServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: slowPort,
secure: false,
connectionTimeout: 10000, // Allow time for slow response
debug: true
});
const startTime = Date.now();
try {
const verified = await smtpClient.verify();
const duration = Date.now() - startTime;
console.log(`Verification completed in ${duration}ms`);
if (verified) {
console.log('✅ Client handled slow server responses');
}
} catch (error) {
console.log('✅ Client handled timing violation with error:', error.message);
}
await smtpClient.close();
slowServer.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
});
export default tap.start();