262 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			262 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * STARTTLS Implementation
 | |
|  * Provides an improved implementation for STARTTLS upgrades
 | |
|  */
 | |
| 
 | |
| import * as plugins from '../../../plugins.ts';
 | |
| import { SmtpLogger } from './utils/logging.ts';
 | |
| import { 
 | |
|   loadCertificatesFromString, 
 | |
|   createTlsOptions,
 | |
|   type ICertificateData
 | |
| } from './certificate-utils.ts';
 | |
| import { getSocketDetails } from './utils/helpers.ts';
 | |
| import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.ts';
 | |
| import { SmtpState } from '../interfaces.ts';
 | |
| 
 | |
| /**
 | |
|  * 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;
 | |
|     sessionManager?: ISessionManager;
 | |
|     connectionManager?: IConnectionManager;
 | |
|     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,
 | |
|         // 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 socket mapping in session manager
 | |
|         if (options.sessionManager) {
 | |
|           const socketReplaced = options.sessionManager.replaceSocket(socket, tlsSocket);
 | |
|           if (!socketReplaced) {
 | |
|             SmtpLogger.error('Failed to replace socket in session manager after STARTTLS', {
 | |
|               remoteAddress: socketDetails.remoteAddress,
 | |
|               remotePort: socketDetails.remotePort
 | |
|             });
 | |
|           }
 | |
|         }
 | |
|         
 | |
|         // Re-attach event handlers from connection manager
 | |
|         if (options.connectionManager) {
 | |
|           try {
 | |
|             options.connectionManager.setupSocketEventHandlers(tlsSocket);
 | |
|             SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', {
 | |
|               remoteAddress: socketDetails.remoteAddress,
 | |
|               remotePort: socketDetails.remotePort
 | |
|             });
 | |
|           } catch (handlerError) {
 | |
|             SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', {
 | |
|               remoteAddress: socketDetails.remoteAddress,
 | |
|               remotePort: socketDetails.remotePort,
 | |
|               error: handlerError instanceof Error ? handlerError : new Error(String(handlerError))
 | |
|             });
 | |
|           }
 | |
|         }
 | |
|         
 | |
|         // 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);
 | |
|     }
 | |
|   });
 | |
| } |