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'); | ||
|  |   } | ||
|  | } |