2025-05-23 19:49:25 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
2025-05-23 19:03:44 +00:00
|
|
|
import * as net from 'net';
|
|
|
|
import * as tls from 'tls';
|
2025-05-24 00:23:35 +00:00
|
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
2025-05-23 19:49:25 +00:00
|
|
|
const TEST_PORT = 2525;
|
2025-05-23 19:03:44 +00:00
|
|
|
const TEST_TIMEOUT = 30000;
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
let testServer: ITestServer;
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
tap.test('setup - start SMTP server with TLS support for version tests', async () => {
|
|
|
|
testServer = await startTestServer({
|
|
|
|
port: TEST_PORT,
|
|
|
|
tlsEnabled: true
|
|
|
|
});
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-23 19:49:25 +00:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
2025-05-24 00:23:35 +00:00
|
|
|
expect(testServer).toBeDefined();
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
tap.test('TLS Versions - should support STARTTLS capability', async (tools) => {
|
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
try {
|
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: TEST_PORT,
|
|
|
|
timeout: TEST_TIMEOUT
|
|
|
|
});
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', () => resolve());
|
|
|
|
socket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get banner
|
|
|
|
await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Send EHLO
|
|
|
|
socket.write('EHLO testhost\r\n');
|
|
|
|
|
|
|
|
const ehloResponse = await new Promise<string>((resolve) => {
|
|
|
|
let data = '';
|
|
|
|
const handler = (chunk: Buffer) => {
|
|
|
|
data += chunk.toString();
|
|
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
|
|
socket.removeListener('data', handler);
|
|
|
|
resolve(data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
socket.on('data', handler);
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('EHLO response:', ehloResponse);
|
|
|
|
|
|
|
|
// Check for STARTTLS support
|
|
|
|
const supportsStarttls = ehloResponse.includes('250-STARTTLS') || ehloResponse.includes('250 STARTTLS');
|
|
|
|
console.log('STARTTLS supported:', supportsStarttls);
|
|
|
|
|
|
|
|
if (supportsStarttls) {
|
|
|
|
// Test STARTTLS upgrade
|
|
|
|
socket.write('STARTTLS\r\n');
|
|
|
|
|
|
|
|
const starttlsResponse = await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(starttlsResponse).toInclude('220');
|
|
|
|
console.log('STARTTLS ready response received');
|
|
|
|
|
|
|
|
// Would upgrade to TLS here in a real implementation
|
|
|
|
// For testing, we just verify the capability
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
|
|
|
|
// STARTTLS is optional but common
|
2025-05-23 21:20:39 +00:00
|
|
|
expect(true).toEqual(true);
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
tap.test('TLS Versions - should support modern TLS versions via STARTTLS', async (tools) => {
|
2025-05-23 19:03:44 +00:00
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
try {
|
2025-05-24 00:23:35 +00:00
|
|
|
// Test TLS 1.2 via STARTTLS
|
|
|
|
console.log('Testing TLS 1.2 support via STARTTLS...');
|
|
|
|
const tls12Result = await testTlsVersionViaStartTls('TLSv1.2', TEST_PORT);
|
2025-05-23 19:03:44 +00:00
|
|
|
console.log('TLS 1.2 result:', tls12Result);
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
// Test TLS 1.3 via STARTTLS
|
|
|
|
console.log('Testing TLS 1.3 support via STARTTLS...');
|
|
|
|
const tls13Result = await testTlsVersionViaStartTls('TLSv1.3', TEST_PORT);
|
2025-05-23 19:03:44 +00:00
|
|
|
console.log('TLS 1.3 result:', tls13Result);
|
|
|
|
|
|
|
|
// At least one modern version should be supported
|
|
|
|
const supportsModernTls = tls12Result.success || tls13Result.success;
|
2025-05-23 21:20:39 +00:00
|
|
|
expect(supportsModernTls).toEqual(true);
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
if (tls12Result.success) {
|
|
|
|
console.log('TLS 1.2 supported with cipher:', tls12Result.cipher);
|
|
|
|
}
|
|
|
|
if (tls13Result.success) {
|
|
|
|
console.log('TLS 1.3 supported with cipher:', tls13Result.cipher);
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
tap.test('TLS Versions - should reject obsolete TLS versions via STARTTLS', async (tools) => {
|
2025-05-23 19:03:44 +00:00
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Test TLS 1.0 (should be rejected by modern servers)
|
2025-05-24 00:23:35 +00:00
|
|
|
console.log('Testing TLS 1.0 (obsolete) via STARTTLS...');
|
|
|
|
const tls10Result = await testTlsVersionViaStartTls('TLSv1', TEST_PORT);
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// Test TLS 1.1 (should be rejected by modern servers)
|
2025-05-24 00:23:35 +00:00
|
|
|
console.log('Testing TLS 1.1 (obsolete) via STARTTLS...');
|
|
|
|
const tls11Result = await testTlsVersionViaStartTls('TLSv1.1', TEST_PORT);
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// Modern servers should reject these old versions
|
|
|
|
// But some might still support them for compatibility
|
|
|
|
console.log(`TLS 1.0 ${tls10Result.success ? 'accepted (legacy support)' : 'rejected (good)'}`);
|
|
|
|
console.log(`TLS 1.1 ${tls11Result.success ? 'accepted (legacy support)' : 'rejected (good)'}`);
|
|
|
|
|
|
|
|
// Either behavior is acceptable - log the results
|
2025-05-23 21:20:39 +00:00
|
|
|
expect(true).toEqual(true);
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
tap.test('TLS Versions - should provide cipher information via STARTTLS', async (tools) => {
|
2025-05-23 19:03:44 +00:00
|
|
|
const done = tools.defer();
|
|
|
|
|
|
|
|
try {
|
2025-05-24 00:23:35 +00:00
|
|
|
// Connect to plain SMTP port
|
|
|
|
const socket = net.createConnection({
|
2025-05-23 19:03:44 +00:00
|
|
|
host: 'localhost',
|
2025-05-24 00:23:35 +00:00
|
|
|
port: TEST_PORT,
|
2025-05-23 19:03:44 +00:00
|
|
|
timeout: TEST_TIMEOUT
|
2025-05-24 00:23:35 +00:00
|
|
|
});
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
socket.once('connect', () => resolve());
|
|
|
|
socket.once('error', reject);
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
// Get banner
|
|
|
|
await new Promise<string>((resolve) => {
|
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
// Send EHLO
|
|
|
|
socket.write('EHLO testhost\r\n');
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
const ehloResponse = await new Promise<string>((resolve) => {
|
|
|
|
let data = '';
|
|
|
|
const handler = (chunk: Buffer) => {
|
|
|
|
data += chunk.toString();
|
|
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
|
|
socket.removeListener('data', handler);
|
|
|
|
resolve(data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
socket.on('data', handler);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Check for STARTTLS
|
|
|
|
if (!ehloResponse.includes('STARTTLS')) {
|
|
|
|
console.log('Server does not support STARTTLS - skipping cipher info test');
|
|
|
|
socket.write('QUIT\r\n');
|
|
|
|
socket.end();
|
|
|
|
done.resolve();
|
|
|
|
return;
|
|
|
|
}
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
// Send STARTTLS
|
|
|
|
socket.write('STARTTLS\r\n');
|
|
|
|
|
|
|
|
await new Promise<string>((resolve) => {
|
2025-05-23 19:03:44 +00:00
|
|
|
socket.once('data', (chunk) => resolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
// Upgrade to TLS
|
|
|
|
const tlsSocket = tls.connect({
|
|
|
|
socket: socket,
|
|
|
|
servername: 'localhost',
|
|
|
|
rejectUnauthorized: false
|
|
|
|
});
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
tlsSocket.once('secureConnect', () => resolve());
|
|
|
|
tlsSocket.once('error', reject);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Get connection details
|
|
|
|
const cipher = tlsSocket.getCipher();
|
|
|
|
const protocol = tlsSocket.getProtocol();
|
|
|
|
const authorized = tlsSocket.authorized;
|
|
|
|
|
|
|
|
console.log('TLS connection established via STARTTLS:');
|
|
|
|
console.log('- Protocol:', protocol);
|
|
|
|
console.log('- Cipher:', cipher?.name);
|
|
|
|
console.log('- Key exchange:', cipher?.standardName);
|
|
|
|
console.log('- Authorized:', authorized);
|
|
|
|
|
|
|
|
if (protocol) {
|
|
|
|
expect(typeof protocol).toEqual('string');
|
|
|
|
}
|
|
|
|
if (cipher) {
|
|
|
|
expect(cipher.name).toBeDefined();
|
|
|
|
}
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
// Clean up
|
2025-05-24 00:23:35 +00:00
|
|
|
tlsSocket.write('QUIT\r\n');
|
|
|
|
tlsSocket.end();
|
2025-05-23 19:03:44 +00:00
|
|
|
|
|
|
|
} finally {
|
|
|
|
done.resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
// Helper function to test specific TLS version via STARTTLS
|
|
|
|
async function testTlsVersionViaStartTls(version: string, port: number): Promise<{success: boolean, cipher?: any, error?: string}> {
|
|
|
|
return new Promise(async (resolve) => {
|
|
|
|
try {
|
|
|
|
// Connect to plain SMTP port
|
|
|
|
const socket = net.createConnection({
|
|
|
|
host: 'localhost',
|
|
|
|
port: port,
|
|
|
|
timeout: 5000
|
|
|
|
});
|
2025-05-23 19:03:44 +00:00
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
await new Promise<void>((socketResolve, socketReject) => {
|
|
|
|
socket.once('connect', () => socketResolve());
|
|
|
|
socket.once('error', socketReject);
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
2025-05-24 00:23:35 +00:00
|
|
|
|
|
|
|
// Get banner
|
|
|
|
await new Promise<string>((bannerResolve) => {
|
|
|
|
socket.once('data', (chunk) => bannerResolve(chunk.toString()));
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
2025-05-24 00:23:35 +00:00
|
|
|
|
|
|
|
// Send EHLO
|
|
|
|
socket.write('EHLO testhost\r\n');
|
|
|
|
|
|
|
|
const ehloResponse = await new Promise<string>((ehloResolve) => {
|
|
|
|
let data = '';
|
|
|
|
const handler = (chunk: Buffer) => {
|
|
|
|
data += chunk.toString();
|
|
|
|
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
|
|
|
socket.removeListener('data', handler);
|
|
|
|
ehloResolve(data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
socket.on('data', handler);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Check for STARTTLS
|
|
|
|
if (!ehloResponse.includes('STARTTLS')) {
|
|
|
|
socket.destroy();
|
|
|
|
resolve({
|
|
|
|
success: false,
|
|
|
|
error: 'STARTTLS not supported'
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send STARTTLS
|
|
|
|
socket.write('STARTTLS\r\n');
|
|
|
|
|
|
|
|
await new Promise<string>((starttlsResolve) => {
|
|
|
|
socket.once('data', (chunk) => starttlsResolve(chunk.toString()));
|
|
|
|
});
|
|
|
|
|
|
|
|
// Set up TLS options with version constraints
|
|
|
|
const tlsOptions: any = {
|
|
|
|
socket: socket,
|
|
|
|
servername: 'localhost',
|
|
|
|
rejectUnauthorized: false
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set version constraints based on requested version
|
|
|
|
switch (version) {
|
|
|
|
case 'TLSv1':
|
|
|
|
tlsOptions.minVersion = 'TLSv1';
|
|
|
|
tlsOptions.maxVersion = 'TLSv1';
|
|
|
|
break;
|
|
|
|
case 'TLSv1.1':
|
|
|
|
tlsOptions.minVersion = 'TLSv1.1';
|
|
|
|
tlsOptions.maxVersion = 'TLSv1.1';
|
|
|
|
break;
|
|
|
|
case 'TLSv1.2':
|
|
|
|
tlsOptions.minVersion = 'TLSv1.2';
|
|
|
|
tlsOptions.maxVersion = 'TLSv1.2';
|
|
|
|
break;
|
|
|
|
case 'TLSv1.3':
|
|
|
|
tlsOptions.minVersion = 'TLSv1.3';
|
|
|
|
tlsOptions.maxVersion = 'TLSv1.3';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upgrade to TLS
|
|
|
|
const tlsSocket = tls.connect(tlsOptions);
|
|
|
|
|
|
|
|
tlsSocket.once('secureConnect', () => {
|
|
|
|
const cipher = tlsSocket.getCipher();
|
|
|
|
const protocol = tlsSocket.getProtocol();
|
|
|
|
|
|
|
|
tlsSocket.destroy();
|
|
|
|
resolve({
|
|
|
|
success: true,
|
|
|
|
cipher: {
|
|
|
|
name: cipher?.name,
|
|
|
|
standardName: cipher?.standardName,
|
|
|
|
protocol: protocol
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
tlsSocket.once('error', (error) => {
|
|
|
|
resolve({
|
|
|
|
success: false,
|
|
|
|
error: error.message
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
tlsSocket.destroy();
|
|
|
|
resolve({
|
|
|
|
success: false,
|
|
|
|
error: 'TLS handshake timeout'
|
|
|
|
});
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-05-23 19:03:44 +00:00
|
|
|
resolve({
|
|
|
|
success: false,
|
2025-05-24 00:23:35 +00:00
|
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
2025-05-24 00:23:35 +00:00
|
|
|
}
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-05-24 00:23:35 +00:00
|
|
|
tap.test('cleanup - stop SMTP server', async () => {
|
|
|
|
await stopTestServer(testServer);
|
2025-05-23 21:20:39 +00:00
|
|
|
expect(true).toEqual(true);
|
2025-05-23 19:03:44 +00:00
|
|
|
});
|
|
|
|
|
2025-05-25 19:05:43 +00:00
|
|
|
export default tap.start();
|