update
This commit is contained in:
234
ts/mail/delivery/smtpserver/starttls-handler.ts
Normal file
234
ts/mail/delivery/smtpserver/starttls-handler.ts
Normal file
@ -0,0 +1,234 @@
|
||||
/**
|
||||
* STARTTLS Implementation
|
||||
* Provides an improved implementation for STARTTLS upgrades
|
||||
*/
|
||||
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { SmtpLogger } from './utils/logging.js';
|
||||
import {
|
||||
loadCertificatesFromString,
|
||||
createTlsOptions,
|
||||
type ICertificateData
|
||||
} from './certificate-utils.js';
|
||||
import { getSocketDetails } from './utils/helpers.js';
|
||||
import type { ISmtpSession } from './interfaces.js';
|
||||
import { SmtpState } from '../interfaces.js';
|
||||
|
||||
/**
|
||||
* Enhanced STARTTLS handler for more reliable TLS upgrades
|
||||
*/
|
||||
export async function performStartTLS(
|
||||
socket: plugins.net.Socket,
|
||||
options: {
|
||||
key: string;
|
||||
cert: string;
|
||||
ca?: string;
|
||||
session?: ISmtpSession;
|
||||
onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void;
|
||||
onFailure?: (error: Error) => void;
|
||||
updateSessionState?: (session: ISmtpSession, state: SmtpState) => void;
|
||||
}
|
||||
): Promise<plugins.tls.TLSSocket | undefined> {
|
||||
return new Promise<plugins.tls.TLSSocket | undefined>((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: Error) => {
|
||||
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: ICertificateData;
|
||||
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: NodeJS.Timeout | undefined;
|
||||
|
||||
// Create the TLS socket using a conservative approach for STARTTLS
|
||||
const tlsSocket = new plugins.tls.TLSSocket(socket, {
|
||||
isServer: true,
|
||||
secureContext,
|
||||
// Enable handshake timeout for STARTTLS
|
||||
handshakeTimeout,
|
||||
// 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 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);
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user