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