284 lines
8.4 KiB
TypeScript
Raw Normal View History

2025-05-21 12:52:24 +00:00
/**
* SMTP TLS Handler
* Responsible for handling TLS-related SMTP functionality
*/
import * as plugins from '../../../plugins.js';
2025-05-21 13:42:12 +00:00
import type { ITlsHandler, ISessionManager } from './interfaces.js';
2025-05-21 12:52:24 +00:00
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
import { SmtpLogger } from './utils/logging.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
/**
* Handles TLS functionality for SMTP server
*/
export class TlsHandler implements ITlsHandler {
/**
* Session manager instance
*/
private sessionManager: ISessionManager;
/**
* TLS options
*/
private options: {
key: string;
cert: string;
ca?: string;
rejectUnauthorized?: boolean;
};
/**
* Creates a new TLS handler
* @param sessionManager - Session manager instance
* @param options - TLS options
*/
constructor(
sessionManager: ISessionManager,
options: {
key: string;
cert: string;
ca?: string;
rejectUnauthorized?: boolean;
}
) {
this.sessionManager = sessionManager;
this.options = options;
}
/**
* Handle STARTTLS command
* @param socket - Client socket
*/
public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Check if already using TLS
if (session.useTLS) {
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
return;
}
// Check if we have the necessary TLS certificates
if (!this.isTlsEnabled()) {
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
return;
}
// Send ready for TLS response
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`);
// Upgrade the connection to TLS
try {
this.startTLS(socket);
} 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
);
}
}
/**
* Upgrade a connection to TLS
* @param socket - Client socket
*/
public startTLS(socket: plugins.net.Socket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
// Create TLS context
const context = {
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca,
isServer: true,
rejectUnauthorized: this.options.rejectUnauthorized || false
};
try {
// Upgrade the connection
const secureSocket = new plugins.tls.TLSSocket(socket, context);
// Store reference to the original socket to facilitate cleanup
(secureSocket as any).originalSocket = socket;
// Log the successful upgrade
if (session) {
SmtpLogger.info(`Upgraded connection to TLS for session ${session.id}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.INFO,
SecurityEventType.TLS_NEGOTIATION,
'STARTTLS negotiation successful',
{},
session.remoteAddress,
undefined,
true
);
// Update session properties
session.useTLS = true;
session.secure = true;
// Reset session state (per RFC 3207)
// After STARTTLS, client must issue a new EHLO
if (this.sessionManager.updateSessionState) {
this.sessionManager.updateSessionState(session, SmtpState.GREETING);
}
} else {
const socketDetails = getSocketDetails(socket);
SmtpLogger.info(`Upgraded connection to TLS without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`);
}
// Securely handle TLS errors
secureSocket.on('error', (err) => {
SmtpLogger.error(`TLS error: ${err.message}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: err
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'TLS error after successful negotiation',
{ error: err.message },
socket.remoteAddress
);
socket.destroy();
});
// Log TLS connection details on secure
secureSocket.on('secure', () => {
const tlsDetails = getTlsDetails(secureSocket);
if (tlsDetails) {
SmtpLogger.info('TLS connection established', {
remoteAddress: secureSocket.remoteAddress,
remotePort: secureSocket.remotePort,
protocol: tlsDetails.protocol,
cipher: tlsDetails.cipher,
authorized: tlsDetails.authorized
});
// Log security event with TLS details
SmtpLogger.logSecurityEvent(
SecurityLogLevel.INFO,
SecurityEventType.TLS_NEGOTIATION,
'TLS connection details',
{
protocol: tlsDetails.protocol,
cipher: tlsDetails.cipher,
authorized: tlsDetails.authorized
},
secureSocket.remoteAddress,
undefined,
true
);
}
});
} catch (error) {
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))
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'Failed to upgrade connection to TLS',
{ error: error instanceof Error ? error.message : String(error) },
socket.remoteAddress,
undefined,
false
);
socket.destroy();
}
}
/**
* 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 {
// Create TLS context
const context = {
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca,
rejectUnauthorized: this.options.rejectUnauthorized || false
};
// Create secure server
return new plugins.tls.Server(context);
} 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))
});
return undefined;
}
}
/**
* Check if TLS is enabled
* @returns Whether TLS is enabled
*/
public isTlsEnabled(): boolean {
return !!(this.options.key && this.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 {
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();
}
}
}
// Import SmtpState only for type reference, not available at runtime
import { SmtpState } from '../interfaces.js';