273 lines
22 KiB
JavaScript
273 lines
22 KiB
JavaScript
|
|
/**
|
||
|
|
* SMTP TLS Handler
|
||
|
|
* Responsible for handling TLS-related SMTP functionality
|
||
|
|
*/
|
||
|
|
import * as plugins from '../../../plugins.js';
|
||
|
|
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
|
||
|
|
import { SmtpLogger } from './utils/logging.js';
|
||
|
|
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
|
||
|
|
import { loadCertificatesFromString, generateSelfSignedCertificates, createTlsOptions } from './certificate-utils.js';
|
||
|
|
import { SmtpState } from '../interfaces.js';
|
||
|
|
/**
|
||
|
|
* Handles TLS functionality for SMTP server
|
||
|
|
*/
|
||
|
|
export class TlsHandler {
|
||
|
|
/**
|
||
|
|
* Reference to the SMTP server instance
|
||
|
|
*/
|
||
|
|
smtpServer;
|
||
|
|
/**
|
||
|
|
* Certificate data
|
||
|
|
*/
|
||
|
|
certificates;
|
||
|
|
/**
|
||
|
|
* TLS options
|
||
|
|
*/
|
||
|
|
options;
|
||
|
|
/**
|
||
|
|
* Creates a new TLS handler
|
||
|
|
* @param smtpServer - SMTP server instance
|
||
|
|
*/
|
||
|
|
constructor(smtpServer) {
|
||
|
|
this.smtpServer = smtpServer;
|
||
|
|
// Initialize certificates
|
||
|
|
const serverOptions = this.smtpServer.getOptions();
|
||
|
|
try {
|
||
|
|
// Try to load certificates from provided options
|
||
|
|
this.certificates = loadCertificatesFromString({
|
||
|
|
key: serverOptions.key,
|
||
|
|
cert: serverOptions.cert,
|
||
|
|
ca: serverOptions.ca
|
||
|
|
});
|
||
|
|
SmtpLogger.info('Successfully loaded TLS certificates');
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
SmtpLogger.warn(`Failed to load certificates from options, using self-signed: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
|
// Fall back to self-signed certificates for testing
|
||
|
|
this.certificates = generateSelfSignedCertificates();
|
||
|
|
}
|
||
|
|
// Initialize TLS options
|
||
|
|
this.options = createTlsOptions(this.certificates);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Handle STARTTLS command
|
||
|
|
* @param socket - Client socket
|
||
|
|
*/
|
||
|
|
async handleStartTls(socket, session) {
|
||
|
|
// Check if already using TLS
|
||
|
|
if (session.useTLS) {
|
||
|
|
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// Check if we have the necessary TLS certificates
|
||
|
|
if (!this.isTlsEnabled()) {
|
||
|
|
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// Send ready for TLS response
|
||
|
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`);
|
||
|
|
// Upgrade the connection to TLS
|
||
|
|
try {
|
||
|
|
const tlsSocket = await this.startTLS(socket);
|
||
|
|
return tlsSocket;
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, {
|
||
|
|
sessionId: session.id,
|
||
|
|
remoteAddress: session.remoteAddress,
|
||
|
|
error: error instanceof Error ? error : new Error(String(error))
|
||
|
|
});
|
||
|
|
// Log security event
|
||
|
|
SmtpLogger.logSecurityEvent(SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS negotiation failed', { error: error instanceof Error ? error.message : String(error) }, session.remoteAddress);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Upgrade a connection to TLS
|
||
|
|
* @param socket - Client socket
|
||
|
|
*/
|
||
|
|
async startTLS(socket) {
|
||
|
|
// Get the session for this socket
|
||
|
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
||
|
|
try {
|
||
|
|
// Import the enhanced STARTTLS handler
|
||
|
|
// This uses a more robust approach to TLS upgrades
|
||
|
|
const { performStartTLS } = await import('./starttls-handler.js');
|
||
|
|
SmtpLogger.info('Using enhanced STARTTLS implementation');
|
||
|
|
// Use the enhanced STARTTLS handler with better error handling and socket management
|
||
|
|
const serverOptions = this.smtpServer.getOptions();
|
||
|
|
const tlsSocket = await performStartTLS(socket, {
|
||
|
|
key: serverOptions.key,
|
||
|
|
cert: serverOptions.cert,
|
||
|
|
ca: serverOptions.ca,
|
||
|
|
session: session,
|
||
|
|
sessionManager: this.smtpServer.getSessionManager(),
|
||
|
|
connectionManager: this.smtpServer.getConnectionManager(),
|
||
|
|
// Callback for successful upgrade
|
||
|
|
onSuccess: (secureSocket) => {
|
||
|
|
SmtpLogger.info('TLS connection successfully established via enhanced STARTTLS', {
|
||
|
|
remoteAddress: secureSocket.remoteAddress,
|
||
|
|
remotePort: secureSocket.remotePort,
|
||
|
|
protocol: secureSocket.getProtocol() || 'unknown',
|
||
|
|
cipher: secureSocket.getCipher()?.name || 'unknown'
|
||
|
|
});
|
||
|
|
// Log security event
|
||
|
|
SmtpLogger.logSecurityEvent(SecurityLogLevel.INFO, SecurityEventType.TLS_NEGOTIATION, 'STARTTLS successful with enhanced implementation', {
|
||
|
|
protocol: secureSocket.getProtocol(),
|
||
|
|
cipher: secureSocket.getCipher()?.name
|
||
|
|
}, secureSocket.remoteAddress, undefined, true);
|
||
|
|
},
|
||
|
|
// Callback for failed upgrade
|
||
|
|
onFailure: (error) => {
|
||
|
|
SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, {
|
||
|
|
sessionId: session?.id,
|
||
|
|
remoteAddress: socket.remoteAddress,
|
||
|
|
error
|
||
|
|
});
|
||
|
|
// Log security event
|
||
|
|
SmtpLogger.logSecurityEvent(SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Enhanced STARTTLS failed', { error: error.message }, socket.remoteAddress, undefined, false);
|
||
|
|
},
|
||
|
|
// Function to update session state
|
||
|
|
updateSessionState: this.smtpServer.getSessionManager().updateSessionState?.bind(this.smtpServer.getSessionManager())
|
||
|
|
});
|
||
|
|
// If STARTTLS failed with the enhanced implementation, log the error
|
||
|
|
if (!tlsSocket) {
|
||
|
|
SmtpLogger.warn('Enhanced STARTTLS implementation failed to create TLS socket', {
|
||
|
|
sessionId: session?.id,
|
||
|
|
remoteAddress: socket.remoteAddress
|
||
|
|
});
|
||
|
|
throw new Error('Failed to create TLS socket');
|
||
|
|
}
|
||
|
|
return tlsSocket;
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
// Log STARTTLS failure
|
||
|
|
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
||
|
|
remoteAddress: socket.remoteAddress,
|
||
|
|
remotePort: socket.remotePort,
|
||
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
||
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
||
|
|
});
|
||
|
|
// Log security event
|
||
|
|
SmtpLogger.logSecurityEvent(SecurityLogLevel.ERROR, SecurityEventType.TLS_NEGOTIATION, 'Failed to upgrade connection to TLS', {
|
||
|
|
error: error instanceof Error ? error.message : String(error),
|
||
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
||
|
|
}, socket.remoteAddress, undefined, false);
|
||
|
|
// Destroy the socket on error
|
||
|
|
socket.destroy();
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Create a secure server
|
||
|
|
* @returns TLS server instance or undefined if TLS is not enabled
|
||
|
|
*/
|
||
|
|
createSecureServer() {
|
||
|
|
if (!this.isTlsEnabled()) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
SmtpLogger.info('Creating secure TLS server');
|
||
|
|
// Log certificate info
|
||
|
|
SmtpLogger.debug('Using certificates for secure server', {
|
||
|
|
keyLength: this.certificates.key.length,
|
||
|
|
certLength: this.certificates.cert.length,
|
||
|
|
caLength: this.certificates.ca ? this.certificates.ca.length : 0
|
||
|
|
});
|
||
|
|
// Create TLS options using our certificate utilities
|
||
|
|
// This ensures proper PEM format handling and protocol negotiation
|
||
|
|
const tlsOptions = createTlsOptions(this.certificates, true); // Use server options
|
||
|
|
SmtpLogger.info('Creating TLS server with options', {
|
||
|
|
minVersion: tlsOptions.minVersion,
|
||
|
|
maxVersion: tlsOptions.maxVersion,
|
||
|
|
handshakeTimeout: tlsOptions.handshakeTimeout
|
||
|
|
});
|
||
|
|
// Create a server with wider TLS compatibility
|
||
|
|
const server = new plugins.tls.Server(tlsOptions);
|
||
|
|
// Add error handling
|
||
|
|
server.on('error', (err) => {
|
||
|
|
SmtpLogger.error(`TLS server error: ${err.message}`, {
|
||
|
|
error: err,
|
||
|
|
stack: err.stack
|
||
|
|
});
|
||
|
|
});
|
||
|
|
// Log TLS details for each connection
|
||
|
|
server.on('secureConnection', (socket) => {
|
||
|
|
SmtpLogger.info('New secure connection established', {
|
||
|
|
protocol: socket.getProtocol(),
|
||
|
|
cipher: socket.getCipher()?.name,
|
||
|
|
remoteAddress: socket.remoteAddress,
|
||
|
|
remotePort: socket.remotePort
|
||
|
|
});
|
||
|
|
});
|
||
|
|
return server;
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
SmtpLogger.error(`Failed to create secure server: ${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'
|
||
|
|
});
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if TLS is enabled
|
||
|
|
* @returns Whether TLS is enabled
|
||
|
|
*/
|
||
|
|
isTlsEnabled() {
|
||
|
|
const options = this.smtpServer.getOptions();
|
||
|
|
return !!(options.key && options.cert);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Send a response to the client
|
||
|
|
* @param socket - Client socket
|
||
|
|
* @param response - Response message
|
||
|
|
*/
|
||
|
|
sendResponse(socket, response) {
|
||
|
|
// Check if socket is still writable before attempting to write
|
||
|
|
if (socket.destroyed || socket.readyState !== 'open' || !socket.writable) {
|
||
|
|
SmtpLogger.debug(`Skipping response to closed/destroyed socket: ${response}`, {
|
||
|
|
remoteAddress: socket.remoteAddress,
|
||
|
|
remotePort: socket.remotePort,
|
||
|
|
destroyed: socket.destroyed,
|
||
|
|
readyState: socket.readyState,
|
||
|
|
writable: socket.writable
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
socket.write(`${response}\r\n`);
|
||
|
|
SmtpLogger.logResponse(response, socket);
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
|
||
|
|
response,
|
||
|
|
remoteAddress: socket.remoteAddress,
|
||
|
|
remotePort: socket.remotePort,
|
||
|
|
error: error instanceof Error ? error : new Error(String(error))
|
||
|
|
});
|
||
|
|
socket.destroy();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if TLS is available (interface requirement)
|
||
|
|
*/
|
||
|
|
isTlsAvailable() {
|
||
|
|
return this.isTlsEnabled();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get TLS options (interface requirement)
|
||
|
|
*/
|
||
|
|
getTlsOptions() {
|
||
|
|
return this.options;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clean up resources
|
||
|
|
*/
|
||
|
|
destroy() {
|
||
|
|
// Clear any cached certificates or TLS contexts
|
||
|
|
// TlsHandler doesn't have timers but may have cached resources
|
||
|
|
SmtpLogger.debug('TlsHandler destroyed');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGxzLWhhbmRsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBzZXJ2ZXIvdGxzLWhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUUvQyxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUN2RixPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDaEQsT0FBTyxFQUFFLGdCQUFnQixFQUFFLGFBQWEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ3JFLE9BQU8sRUFDTCwwQkFBMEIsRUFDMUIsOEJBQThCLEVBQzlCLGdCQUFnQixFQUVqQixNQUFNLHdCQUF3QixDQUFDO0FBQ2hDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUU3Qzs7R0FFRztBQUNILE1BQU0sT0FBTyxVQUFVO0lBQ3JCOztPQUVHO0lBQ0ssVUFBVSxDQUFjO0lBRWhDOztPQUVHO0lBQ0ssWUFBWSxDQUFtQjtJQUV2Qzs7T0FFRztJQUNLLE9BQU8sQ0FBeUI7SUFFeEM7OztPQUdHO0lBQ0gsWUFBWSxVQUF1QjtRQUNqQyxJQUFJLENBQUMsVUFBVSxHQUFHLFVBQVUsQ0FBQztRQUU3QiwwQkFBMEI7UUFDMUIsTUFBTSxhQUFhLEdBQUcsSUFBSSxDQUFDLFVBQVUsQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUNuRCxJQUFJLENBQUM7WUFDSCxpREFBaUQ7WUFDakQsSUFBSSxDQUFDLFlBQVksR0FBRywwQkFBMEIsQ0FBQztnQkFDN0MsR0FBRyxFQUFFLGFBQWEsQ0FBQyxHQUFHO2dCQUN0QixJQUFJLEVBQUUsYUFBYSxDQUFDLElBQUk7Z0JBQ3hCLEVBQUUsRUFBRSxhQUFhLENBQUMsRUFBRTthQUNyQixDQUFDLENB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
|