dcrouter/test/suite/error-handling/test.invalid-sequence.ts
2025-05-24 00:23:35 +00:00

425 lines
12 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import * as path from 'path';
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js';
import type { ITestServer } from '../../helpers/server.loader.js';
// Test configuration
const TEST_PORT = 30051;
const TEST_TIMEOUT = 10000;
let testServer: ITestServer;
// Setup
tap.test('setup - start SMTP server', async () => {
testServer = await startTestServer({
port: TEST_PORT,
tlsEnabled: false,
hostname: 'localhost'
});
expect(testServer).toBeDefined();
expect(testServer.port).toEqual(TEST_PORT);
});
// Test: MAIL FROM before EHLO/HELO
tap.test('Invalid Sequence - should reject MAIL FROM before EHLO', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'mail_from_without_ehlo';
socket.write('MAIL FROM:<test@example.com>\r\n');
} else if (currentStep === 'mail_from_without_ehlo' && receivedData.includes('503')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503'); // Bad sequence of commands
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: RCPT TO before MAIL FROM
tap.test('Invalid Sequence - should reject RCPT TO before MAIL FROM', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'rcpt_without_mail';
socket.write('RCPT TO:<test@example.com>\r\n');
} else if (currentStep === 'rcpt_without_mail' && receivedData.includes('503')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: DATA before RCPT TO
tap.test('Invalid Sequence - should reject DATA before RCPT TO', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<test@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'data_without_rcpt';
socket.write('DATA\r\n');
} else if (currentStep === 'data_without_rcpt' && receivedData.includes('503')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Multiple EHLO commands (should be allowed)
tap.test('Invalid Sequence - should allow multiple EHLO commands', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let ehloCount = 0;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'first_ehlo';
socket.write('EHLO test1.example.com\r\n');
} else if (currentStep === 'first_ehlo' && receivedData.includes('250')) {
ehloCount++;
currentStep = 'second_ehlo';
receivedData = ''; // Clear buffer
socket.write('EHLO test2.example.com\r\n');
} else if (currentStep === 'second_ehlo' && receivedData.includes('250')) {
ehloCount++;
currentStep = 'third_ehlo';
receivedData = ''; // Clear buffer
socket.write('EHLO test3.example.com\r\n');
} else if (currentStep === 'third_ehlo' && receivedData.includes('250')) {
ehloCount++;
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(ehloCount).toEqual(3); // All EHLO commands should succeed
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Multiple MAIL FROM without RSET
tap.test('Invalid Sequence - should reject second MAIL FROM without RSET', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'first_mail_from';
socket.write('MAIL FROM:<sender1@example.com>\r\n');
} else if (currentStep === 'first_mail_from' && receivedData.includes('250')) {
currentStep = 'second_mail_from';
socket.write('MAIL FROM:<sender2@example.com>\r\n');
} else if (currentStep === 'second_mail_from' && receivedData.includes('503')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: DATA without MAIL FROM
tap.test('Invalid Sequence - should reject DATA without MAIL FROM', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'data_without_mail';
socket.write('DATA\r\n');
} else if (currentStep === 'data_without_mail' && receivedData.includes('503')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('503');
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: Commands after QUIT
tap.test('Invalid Sequence - should reject commands after QUIT', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
let quitResponseReceived = false;
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'quit';
socket.write('QUIT\r\n');
} else if (currentStep === 'quit' && receivedData.includes('221')) {
quitResponseReceived = true;
// Try to send command after QUIT
try {
socket.write('EHLO test.example.com\r\n');
// If write succeeds, wait to see if we get a response
setTimeout(() => {
socket.destroy();
done.resolve(); // No response expected after QUIT
}, 1000);
} catch (err) {
// Write failed - connection already closed
done.resolve();
}
}
});
socket.on('close', () => {
if (quitResponseReceived) {
done.resolve();
}
});
socket.on('error', (error) => {
if (quitResponseReceived && error.message.includes('EPIPE')) {
done.resolve();
} else {
done.reject(error);
}
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Test: RCPT TO without proper email brackets
tap.test('Invalid Sequence - should handle commands with wrong syntax in sequence', async (tools) => {
const done = tools.defer();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: TEST_TIMEOUT
});
let receivedData = '';
let currentStep = 'connecting';
socket.on('data', (data) => {
receivedData += data.toString();
if (currentStep === 'connecting' && receivedData.includes('220')) {
currentStep = 'ehlo';
socket.write('EHLO test.example.com\r\n');
} else if (currentStep === 'ehlo' && receivedData.includes('250')) {
currentStep = 'mail_from';
socket.write('MAIL FROM:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'bad_rcpt';
// RCPT TO with wrong syntax
socket.write('RCPT TO:recipient@example.com\r\n'); // Missing brackets
} else if (currentStep === 'bad_rcpt' && receivedData.includes('501')) {
// After syntax error, try valid command
currentStep = 'valid_rcpt';
socket.write('RCPT TO:<recipient@example.com>\r\n');
} else if (currentStep === 'valid_rcpt' && receivedData.includes('250')) {
socket.write('QUIT\r\n');
setTimeout(() => {
socket.destroy();
expect(receivedData).toInclude('501'); // Syntax error
expect(receivedData).toInclude('250'); // Valid command worked
done.resolve();
}, 100);
}
});
socket.on('error', (error) => {
done.reject(error);
});
socket.on('timeout', () => {
socket.destroy();
done.reject(new Error(`Connection timeout at step: ${currentStep}`));
});
await done.promise;
});
// Teardown
tap.test('teardown - stop SMTP server', async () => {
if (testServer) {
await stopTestServer(testServer);
}
expect(true).toEqual(true);
});
// Start the test
tap.start();