346 lines
11 KiB
TypeScript
Raw Normal View History

2025-05-21 12:52:24 +00:00
/**
* SMTP TLS Handler
* Responsible for handling TLS-related SMTP functionality
*/
import * as plugins from '../../../plugins.js';
2025-05-23 00:06:07 +00:00
import type { ITlsHandler, ISmtpServer, ISmtpSession } from './interfaces.js';
2025-05-21 12:52:24 +00:00
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
import { SmtpLogger } from './utils/logging.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
2025-05-21 16:17:17 +00:00
import {
loadCertificatesFromString,
generateSelfSignedCertificates,
createTlsOptions,
type ICertificateData
} from './certificate-utils.js';
2025-05-21 14:28:33 +00:00
import { SmtpState } from '../interfaces.js';
2025-05-21 12:52:24 +00:00
/**
* Handles TLS functionality for SMTP server
*/
export class TlsHandler implements ITlsHandler {
/**
2025-05-22 23:02:37 +00:00
* Reference to the SMTP server instance
2025-05-21 12:52:24 +00:00
*/
2025-05-22 23:02:37 +00:00
private smtpServer: ISmtpServer;
2025-05-21 12:52:24 +00:00
2025-05-21 16:17:17 +00:00
/**
* Certificate data
*/
private certificates: ICertificateData;
2025-05-23 00:06:07 +00:00
/**
* TLS options
*/
private options: plugins.tls.TlsOptions;
2025-05-21 12:52:24 +00:00
/**
* Creates a new TLS handler
2025-05-22 23:02:37 +00:00
* @param smtpServer - SMTP server instance
2025-05-21 12:52:24 +00:00
*/
2025-05-22 23:02:37 +00:00
constructor(smtpServer: ISmtpServer) {
this.smtpServer = smtpServer;
2025-05-21 16:17:17 +00:00
// Initialize certificates
2025-05-23 00:06:07 +00:00
const serverOptions = this.smtpServer.getOptions();
2025-05-21 16:17:17 +00:00
try {
// Try to load certificates from provided options
this.certificates = loadCertificatesFromString({
2025-05-23 00:06:07 +00:00
key: serverOptions.key,
cert: serverOptions.cert,
ca: serverOptions.ca
2025-05-21 16:17:17 +00:00
});
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();
}
2025-05-23 00:06:07 +00:00
// Initialize TLS options
this.options = createTlsOptions(this.certificates);
2025-05-21 12:52:24 +00:00
}
/**
* Handle STARTTLS command
* @param socket - Client socket
*/
2025-05-23 00:06:07 +00:00
public async handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise<plugins.tls.TLSSocket | null> {
2025-05-21 12:52:24 +00:00
// Check if already using TLS
if (session.useTLS) {
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
2025-05-23 00:06:07 +00:00
return null;
2025-05-21 12:52:24 +00:00
}
// Check if we have the necessary TLS certificates
if (!this.isTlsEnabled()) {
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
2025-05-23 00:06:07 +00:00
return null;
2025-05-21 12:52:24 +00:00
}
// Send ready for TLS response
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`);
// Upgrade the connection to TLS
try {
2025-05-23 00:06:07 +00:00
const tlsSocket = await this.startTLS(socket);
return tlsSocket;
2025-05-21 12:52:24 +00:00
} 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
);
2025-05-23 00:06:07 +00:00
return null;
2025-05-21 12:52:24 +00:00
}
}
/**
* Upgrade a connection to TLS
* @param socket - Client socket
*/
2025-05-23 00:06:07 +00:00
public async startTLS(socket: plugins.net.Socket): Promise<plugins.tls.TLSSocket> {
2025-05-21 12:52:24 +00:00
// Get the session for this socket
2025-05-22 23:02:37 +00:00
const session = this.smtpServer.getSessionManager().getSession(socket);
2025-05-21 12:52:24 +00:00
try {
2025-05-21 16:17:17 +00:00
// Import the enhanced STARTTLS handler
// This uses a more robust approach to TLS upgrades
const { performStartTLS } = await import('./starttls-handler.js');
2025-05-21 14:28:33 +00:00
2025-05-21 16:17:17 +00:00
SmtpLogger.info('Using enhanced STARTTLS implementation');
2025-05-21 14:45:17 +00:00
2025-05-21 16:17:17 +00:00
// Use the enhanced STARTTLS handler with better error handling and socket management
2025-05-23 00:06:07 +00:00
const serverOptions = this.smtpServer.getOptions();
2025-05-21 16:17:17 +00:00
const tlsSocket = await performStartTLS(socket, {
2025-05-23 00:06:07 +00:00
key: serverOptions.key,
cert: serverOptions.cert,
ca: serverOptions.ca,
2025-05-21 16:17:17 +00:00
session: session,
2025-05-22 23:02:37 +00:00
sessionManager: this.smtpServer.getSessionManager(),
connectionManager: this.smtpServer.getConnectionManager(),
2025-05-21 16:17:17 +00:00
// 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,
2025-05-21 14:28:33 +00:00
remoteAddress: socket.remoteAddress,
2025-05-21 16:17:17 +00:00
error
2025-05-21 14:28:33 +00:00
});
2025-05-21 16:17:17 +00:00
// 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
2025-05-22 23:02:37 +00:00
updateSessionState: this.smtpServer.getSessionManager().updateSessionState?.bind(this.smtpServer.getSessionManager())
2025-05-21 12:52:24 +00:00
});
2025-05-21 16:17:17 +00:00
// 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
2025-05-21 14:28:33 +00:00
});
2025-05-23 00:06:07 +00:00
throw new Error('Failed to create TLS socket');
2025-05-21 16:17:17 +00:00
}
2025-05-23 00:06:07 +00:00
return tlsSocket;
2025-05-21 12:52:24 +00:00
} catch (error) {
2025-05-21 16:17:17 +00:00
// Log STARTTLS failure
2025-05-21 12:52:24 +00:00
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
2025-05-21 14:28:33 +00:00
error: error instanceof Error ? error : new Error(String(error)),
stack: error instanceof Error ? error.stack : 'No stack trace available'
2025-05-21 12:52:24 +00:00
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'Failed to upgrade connection to TLS',
2025-05-21 14:28:33 +00:00
{
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : 'No stack trace available'
},
2025-05-21 12:52:24 +00:00
socket.remoteAddress,
undefined,
false
);
2025-05-21 16:17:17 +00:00
// Destroy the socket on error
2025-05-21 12:52:24 +00:00
socket.destroy();
2025-05-23 00:06:07 +00:00
throw error;
2025-05-21 12:52:24 +00:00
}
}
/**
* Create a secure server
* @returns TLS server instance or undefined if TLS is not enabled
*/
public createSecureServer(): plugins.tls.Server | undefined {
if (!this.isTlsEnabled()) {
return undefined;
}
try {
2025-05-21 16:17:17 +00:00
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
});
2025-05-21 14:28:33 +00:00
2025-05-21 16:17:17 +00:00
// 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
});
2025-05-21 14:28:33 +00:00
});
2025-05-21 16:17:17 +00:00
// 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
});
});
2025-05-21 12:52:24 +00:00
2025-05-21 16:17:17 +00:00
return server;
2025-05-21 12:52:24 +00:00
} catch (error) {
SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
2025-05-21 14:28:33 +00:00
error: error instanceof Error ? error : new Error(String(error)),
stack: error instanceof Error ? error.stack : 'No stack trace available'
2025-05-21 12:52:24 +00:00
});
return undefined;
}
}
/**
* Check if TLS is enabled
* @returns Whether TLS is enabled
*/
public isTlsEnabled(): boolean {
2025-05-22 23:02:37 +00:00
const options = this.smtpServer.getOptions();
return !!(options.key && options.cert);
2025-05-21 12:52:24 +00:00
}
/**
* Send a response to the client
* @param socket - Client socket
* @param response - Response message
*/
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
2025-05-22 18:38:04 +00:00
// 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;
}
2025-05-21 12:52:24 +00:00
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();
}
}
2025-05-22 23:02:37 +00:00
2025-05-23 00:06:07 +00:00
/**
* Check if TLS is available (interface requirement)
*/
public isTlsAvailable(): boolean {
return this.isTlsEnabled();
}
/**
* Get TLS options (interface requirement)
*/
public getTlsOptions(): plugins.tls.TlsOptions {
return this.options;
}
2025-05-22 23:02:37 +00:00
/**
* Clean up resources
*/
public destroy(): void {
// Clear any cached certificates or TLS contexts
// TlsHandler doesn't have timers but may have cached resources
SmtpLogger.debug('TlsHandler destroyed');
}
2025-05-21 14:28:33 +00:00
}