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