346 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * SMTP TLS Handler
 | |
|  * Responsible for handling TLS-related SMTP functionality
 | |
|  */
 | |
| 
 | |
| import * as plugins from '../../../plugins.ts';
 | |
| import type { ITlsHandler, ISmtpServer, ISmtpSession } from './interfaces.ts';
 | |
| import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.ts';
 | |
| import { SmtpLogger } from './utils/logging.ts';
 | |
| import { getSocketDetails, getTlsDetails } from './utils/helpers.ts';
 | |
| import { 
 | |
|   loadCertificatesFromString, 
 | |
|   generateSelfSignedCertificates,
 | |
|   createTlsOptions,
 | |
|   type ICertificateData
 | |
| } from './certificate-utils.ts';
 | |
| import { SmtpState } from '../interfaces.ts';
 | |
| 
 | |
| /**
 | |
|  * Handles TLS functionality for SMTP server
 | |
|  */
 | |
| export class TlsHandler implements ITlsHandler {
 | |
|   /**
 | |
|    * Reference to the SMTP server instance
 | |
|    */
 | |
|   private smtpServer: ISmtpServer;
 | |
|   
 | |
|   /**
 | |
|    * Certificate data
 | |
|    */
 | |
|   private certificates: ICertificateData;
 | |
|   
 | |
|   /**
 | |
|    * TLS options
 | |
|    */
 | |
|   private options: plugins.tls.TlsOptions;
 | |
|   
 | |
|   /**
 | |
|    * Creates a new TLS handler
 | |
|    * @param smtpServer - SMTP server instance
 | |
|    */
 | |
|   constructor(smtpServer: ISmtpServer) {
 | |
|     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
 | |
|    */
 | |
|   public async handleStartTls(socket: plugins.net.Socket, session: ISmtpSession): Promise<plugins.tls.TLSSocket | null> {
 | |
|     
 | |
|     // 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
 | |
|    */
 | |
|   public async startTLS(socket: plugins.net.Socket): Promise<plugins.tls.TLSSocket> {
 | |
|     // 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.ts');
 | |
|       
 | |
|       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
 | |
|    */
 | |
|   public createSecureServer(): plugins.tls.Server | undefined {
 | |
|     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
 | |
|    */
 | |
|   public isTlsEnabled(): boolean {
 | |
|     const options = this.smtpServer.getOptions();
 | |
|     return !!(options.key && options.cert);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * 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 {
 | |
|     // 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)
 | |
|    */
 | |
|   public isTlsAvailable(): boolean {
 | |
|     return this.isTlsEnabled();
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get TLS options (interface requirement)
 | |
|    */
 | |
|   public getTlsOptions(): plugins.tls.TlsOptions {
 | |
|     return this.options;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * 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');
 | |
|   }
 | |
| } |