207 lines
18 KiB
JavaScript
207 lines
18 KiB
JavaScript
/**
|
|
* STARTTLS Implementation
|
|
* Provides an improved implementation for STARTTLS upgrades
|
|
*/
|
|
import * as plugins from '../../../plugins.js';
|
|
import { SmtpLogger } from './utils/logging.js';
|
|
import { loadCertificatesFromString, createTlsOptions } from './certificate-utils.js';
|
|
import { getSocketDetails } from './utils/helpers.js';
|
|
import { SmtpState } from '../interfaces.js';
|
|
/**
|
|
* Enhanced STARTTLS handler for more reliable TLS upgrades
|
|
*/
|
|
export async function performStartTLS(socket, options) {
|
|
return new Promise((resolve) => {
|
|
try {
|
|
const socketDetails = getSocketDetails(socket);
|
|
SmtpLogger.info('Starting enhanced STARTTLS upgrade process', {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort
|
|
});
|
|
// Create a proper socket cleanup function
|
|
const cleanupSocket = () => {
|
|
// Remove all listeners to prevent memory leaks
|
|
socket.removeAllListeners('data');
|
|
socket.removeAllListeners('error');
|
|
socket.removeAllListeners('close');
|
|
socket.removeAllListeners('end');
|
|
socket.removeAllListeners('drain');
|
|
};
|
|
// Prepare the socket for TLS upgrade
|
|
socket.setNoDelay(true);
|
|
// Critical: make sure there's no pending data before TLS handshake
|
|
socket.pause();
|
|
// Add error handling for the base socket
|
|
const handleSocketError = (err) => {
|
|
SmtpLogger.error(`Socket error during STARTTLS preparation: ${err.message}`, {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort,
|
|
error: err,
|
|
stack: err.stack
|
|
});
|
|
if (options.onFailure) {
|
|
options.onFailure(err);
|
|
}
|
|
// Resolve with undefined to indicate failure
|
|
resolve(undefined);
|
|
};
|
|
socket.once('error', handleSocketError);
|
|
// Load certificates
|
|
let certificates;
|
|
try {
|
|
certificates = loadCertificatesFromString({
|
|
key: options.key,
|
|
cert: options.cert,
|
|
ca: options.ca
|
|
});
|
|
}
|
|
catch (certError) {
|
|
SmtpLogger.error(`Certificate error during STARTTLS: ${certError instanceof Error ? certError.message : String(certError)}`);
|
|
if (options.onFailure) {
|
|
options.onFailure(certError instanceof Error ? certError : new Error(String(certError)));
|
|
}
|
|
resolve(undefined);
|
|
return;
|
|
}
|
|
// Create TLS options optimized for STARTTLS
|
|
const tlsOptions = createTlsOptions(certificates, true);
|
|
// Create secure context
|
|
let secureContext;
|
|
try {
|
|
secureContext = plugins.tls.createSecureContext(tlsOptions);
|
|
}
|
|
catch (contextError) {
|
|
SmtpLogger.error(`Failed to create secure context: ${contextError instanceof Error ? contextError.message : String(contextError)}`);
|
|
if (options.onFailure) {
|
|
options.onFailure(contextError instanceof Error ? contextError : new Error(String(contextError)));
|
|
}
|
|
resolve(undefined);
|
|
return;
|
|
}
|
|
// Log STARTTLS upgrade attempt
|
|
SmtpLogger.debug('Attempting TLS socket upgrade with options', {
|
|
minVersion: tlsOptions.minVersion,
|
|
maxVersion: tlsOptions.maxVersion,
|
|
handshakeTimeout: tlsOptions.handshakeTimeout
|
|
});
|
|
// Use a safer approach to create the TLS socket
|
|
const handshakeTimeout = 30000; // 30 seconds timeout for TLS handshake
|
|
let handshakeTimeoutId;
|
|
// Create the TLS socket using a conservative approach for STARTTLS
|
|
const tlsSocket = new plugins.tls.TLSSocket(socket, {
|
|
isServer: true,
|
|
secureContext,
|
|
// Server-side options (simpler is more reliable for STARTTLS)
|
|
requestCert: false,
|
|
rejectUnauthorized: false
|
|
});
|
|
// Set up error handling for the TLS socket
|
|
tlsSocket.once('error', (err) => {
|
|
if (handshakeTimeoutId) {
|
|
clearTimeout(handshakeTimeoutId);
|
|
}
|
|
SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort,
|
|
error: err,
|
|
stack: err.stack
|
|
});
|
|
// Clean up socket listeners
|
|
cleanupSocket();
|
|
if (options.onFailure) {
|
|
options.onFailure(err);
|
|
}
|
|
// Destroy the socket to ensure we don't have hanging connections
|
|
tlsSocket.destroy();
|
|
resolve(undefined);
|
|
});
|
|
// Set up handshake timeout manually for extra safety
|
|
handshakeTimeoutId = setTimeout(() => {
|
|
SmtpLogger.error('TLS handshake timed out', {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort
|
|
});
|
|
// Clean up socket listeners
|
|
cleanupSocket();
|
|
if (options.onFailure) {
|
|
options.onFailure(new Error('TLS handshake timed out'));
|
|
}
|
|
// Destroy the socket to ensure we don't have hanging connections
|
|
tlsSocket.destroy();
|
|
resolve(undefined);
|
|
}, handshakeTimeout);
|
|
// Set up handler for successful TLS negotiation
|
|
tlsSocket.once('secure', () => {
|
|
if (handshakeTimeoutId) {
|
|
clearTimeout(handshakeTimeoutId);
|
|
}
|
|
const protocol = tlsSocket.getProtocol();
|
|
const cipher = tlsSocket.getCipher();
|
|
SmtpLogger.info('TLS upgrade successful via STARTTLS', {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort,
|
|
protocol: protocol || 'unknown',
|
|
cipher: cipher?.name || 'unknown'
|
|
});
|
|
// Update socket mapping in session manager
|
|
if (options.sessionManager) {
|
|
const socketReplaced = options.sessionManager.replaceSocket(socket, tlsSocket);
|
|
if (!socketReplaced) {
|
|
SmtpLogger.error('Failed to replace socket in session manager after STARTTLS', {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort
|
|
});
|
|
}
|
|
}
|
|
// Re-attach event handlers from connection manager
|
|
if (options.connectionManager) {
|
|
try {
|
|
options.connectionManager.setupSocketEventHandlers(tlsSocket);
|
|
SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort
|
|
});
|
|
}
|
|
catch (handlerError) {
|
|
SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', {
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort,
|
|
error: handlerError instanceof Error ? handlerError : new Error(String(handlerError))
|
|
});
|
|
}
|
|
}
|
|
// Update session if provided
|
|
if (options.session) {
|
|
// Update session properties to indicate TLS is active
|
|
options.session.useTLS = true;
|
|
options.session.secure = true;
|
|
// Reset session state as required by RFC 3207
|
|
// After STARTTLS, client must issue a new EHLO
|
|
if (options.updateSessionState) {
|
|
options.updateSessionState(options.session, SmtpState.GREETING);
|
|
}
|
|
}
|
|
// Call success callback if provided
|
|
if (options.onSuccess) {
|
|
options.onSuccess(tlsSocket);
|
|
}
|
|
// Success - return the TLS socket
|
|
resolve(tlsSocket);
|
|
});
|
|
// Resume the socket after we've set up all handlers
|
|
// This allows the TLS handshake to proceed
|
|
socket.resume();
|
|
}
|
|
catch (error) {
|
|
SmtpLogger.error(`Unexpected error in STARTTLS: ${error instanceof Error ? error.message : String(error)}`, {
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
|
});
|
|
if (options.onFailure) {
|
|
options.onFailure(error instanceof Error ? error : new Error(String(error)));
|
|
}
|
|
resolve(undefined);
|
|
}
|
|
});
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhcnR0bHMtaGFuZGxlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvc210cHNlcnZlci9zdGFydHRscy1oYW5kbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7R0FHRztBQUVILE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFDL0MsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFDTCwwQkFBMEIsRUFDMUIsZ0JBQWdCLEVBRWpCLE1BQU0sd0JBQXdCLENBQUM7QUFDaEMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFdEQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRTdDOztHQUVHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxlQUFlLENBQ25DLE1BQTBCLEVBQzFCLE9BVUM7SUFFRCxPQUFPLElBQUksT0FBTyxDQUFvQyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQ2hFLElBQUksQ0FBQztZQUNILE1BQU0sYUFBYSxHQUFHLGdCQUFnQixDQUFDLE1BQU0sQ0FBQyxDQUFDO1lBRS9DLFVBQVUsQ0FBQyxJQUFJLENBQUMsNENBQTRDLEVBQUU7Z0JBQzVELGFBQWEsRUFBRSxhQUFhLENBQUMsYUFBYTtnQkFDMUMsVUFBVSxFQUFFLGFBQWEsQ0FBQyxVQUFVO2FBQ3JDLENBQUMsQ0FBQztZQUVILDBDQUEwQztZQUMxQyxNQUFNLGFBQWEsR0FBRyxHQUFHLEVBQUU7Z0JBQ3pCLCtDQUErQztnQkFDL0MsTUFBTSxDQUFDLGtCQUFrQixDQUFDLE1BQU0sQ0FBQyxDQUFDO2dCQUNsQyxNQUFNLENBQUMsa0JBQWtCLENBQUMsT0FBTyxDQUFDLENBQUM7Z0JBQ25DLE1BQU0sQ0FBQyxrQkFBa0IsQ0FBQyxPQUFPLENBQUMsQ0FBQztnQkFDbkMsTUFBTSxDQUFDLGtCQUFrQixD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
|