feat: Implement Deno-native STARTTLS handler and connection wrapper

- Refactored STARTTLS implementation to use Deno's native TLS via Deno.startTls().
- Introduced ConnectionWrapper to provide a Node.js net.Socket-compatible interface for Deno.Conn and Deno.TlsConn.
- Updated TlsHandler to utilize the new STARTTLS implementation.
- Added comprehensive SMTP authentication tests for PLAIN and LOGIN mechanisms.
- Implemented rate limiting tests for SMTP server connections and commands.
- Enhanced error handling and logging throughout the STARTTLS and connection upgrade processes.
This commit is contained in:
2025-10-28 18:51:33 +00:00
parent 9cd15342e0
commit 6523c55516
14 changed files with 1328 additions and 429 deletions

View File

@@ -1,21 +1,18 @@
/**
* STARTTLS Implementation
* Provides an improved implementation for STARTTLS upgrades
* STARTTLS Implementation using Deno Native TLS
* Uses Deno.startTls() for reliable TLS 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 { ConnectionWrapper } from './utils/connection-wrapper.ts';
import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.ts';
import { SmtpState } from '../interfaces.ts';
/**
* Enhanced STARTTLS handler for more reliable TLS upgrades
* Perform STARTTLS using Deno's native TLS implementation
* This replaces the broken Node.js TLS compatibility layer
*/
export async function performStartTLS(
socket: plugins.net.Socket,
@@ -26,237 +23,174 @@ export async function performStartTLS(
session?: ISmtpSession;
sessionManager?: ISessionManager;
connectionManager?: IConnectionManager;
onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void;
onSuccess?: (tlsSocket: plugins.tls.TLSSocket | ConnectionWrapper) => void;
onFailure?: (error: Error) => void;
updateSessionState?: (session: ISmtpSession, state: SmtpState) => void;
}
): Promise<plugins.tls.TLSSocket | undefined> {
return new Promise<plugins.tls.TLSSocket | undefined>((resolve) => {
): Promise<plugins.tls.TLSSocket | ConnectionWrapper | undefined> {
return new Promise<plugins.tls.TLSSocket | ConnectionWrapper | undefined>(async (resolve) => {
try {
const socketDetails = getSocketDetails(socket);
SmtpLogger.info('Starting enhanced STARTTLS upgrade process', {
SmtpLogger.info('Starting Deno-native 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);
// Check if this is a ConnectionWrapper (Deno.Conn based)
if (socket instanceof ConnectionWrapper) {
SmtpLogger.info('Using Deno-native STARTTLS implementation for ConnectionWrapper');
// Get the underlying Deno.Conn
const denoConn = socket.getDenoConn();
// Set up timeout for TLS handshake
const handshakeTimeout = 30000; // 30 seconds
const timeoutId = setTimeout(() => {
const error = new Error('TLS handshake timed out');
SmtpLogger.error(error.message, {
remoteAddress: socketDetails.remoteAddress,
remotePort: socketDetails.remotePort
});
if (options.onFailure) {
options.onFailure(error);
}
resolve(undefined);
}, handshakeTimeout);
try {
// Write cert and key to temporary files for Deno.startTls()
const tempDir = await Deno.makeTempDir();
const certFile = `${tempDir}/cert.pem`;
const keyFile = `${tempDir}/key.pem`;
try {
await Deno.writeTextFile(certFile, options.cert);
await Deno.writeTextFile(keyFile, options.key);
// Upgrade connection to TLS using Deno's native API
const tlsConn = await Deno.startTls(denoConn, {
hostname: 'localhost', // Server-side TLS doesn't need hostname validation
certFile,
keyFile,
alpnProtocols: ['smtp'],
});
clearTimeout(timeoutId);
SmtpLogger.info('TLS upgrade successful via Deno-native STARTTLS', {
remoteAddress: socketDetails.remoteAddress,
remotePort: socketDetails.remotePort
});
// Replace the underlying connection in the wrapper
socket.replaceConnection(tlsConn);
// Update socket mapping in session manager
if (options.sessionManager) {
// Socket wrapper remains the same, just upgraded to TLS
const socketReplaced = options.sessionManager.replaceSocket(socket as any, socket as any);
if (!socketReplaced) {
SmtpLogger.warn('Socket already tracked in session manager', {
remoteAddress: socketDetails.remoteAddress,
remotePort: socketDetails.remotePort
});
}
}
// Re-attach event handlers from connection manager if needed
if (options.connectionManager) {
try {
options.connectionManager.setupSocketEventHandlers(socket as any);
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(socket);
}
// Success - return the wrapper with upgraded TLS connection
resolve(socket);
} finally {
// Clean up temporary files
try {
await Deno.remove(tempDir, { recursive: true });
} catch {
// Ignore cleanup errors
}
}
} catch (tlsError) {
clearTimeout(timeoutId);
const error = tlsError instanceof Error ? tlsError : new Error(String(tlsError));
SmtpLogger.error(`Deno TLS upgrade failed: ${error.message}`, {
remoteAddress: socketDetails.remoteAddress,
remotePort: socketDetails.remotePort,
error,
stack: error.stack
});
if (options.onFailure) {
options.onFailure(error);
}
resolve(undefined);
}
// 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', {
} else {
// Fallback: This should not happen since all connections are now ConnectionWrapper
SmtpLogger.error('STARTTLS called on non-ConnectionWrapper socket - this should not happen', {
socketType: socket.constructor.name,
remoteAddress: socketDetails.remoteAddress,
remotePort: socketDetails.remotePort
});
// Clean up socket listeners
cleanupSocket();
const error = new Error('STARTTLS requires ConnectionWrapper (Deno.Conn based socket)');
if (options.onFailure) {
options.onFailure(new Error('TLS handshake timed out'));
options.onFailure(error);
}
// 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)}`, {
SmtpLogger.error(`Unexpected error in Deno-native 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);
}
});
}
}