initial
This commit is contained in:
346
ts/mail/delivery/smtpserver/tls-handler.ts
Normal file
346
ts/mail/delivery/smtpserver/tls-handler.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user