918 lines
75 KiB
JavaScript
918 lines
75 KiB
JavaScript
/**
|
|
* SMTP Connection Manager
|
|
* Responsible for managing socket connections to the SMTP server
|
|
*/
|
|
import * as plugins from '../../../plugins.js';
|
|
import { SmtpResponseCode, SMTP_DEFAULTS, SmtpState } from './constants.js';
|
|
import { SmtpLogger } from './utils/logging.js';
|
|
import { adaptiveLogger } from './utils/adaptive-logging.js';
|
|
import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js';
|
|
/**
|
|
* Manager for SMTP connections
|
|
* Handles connection setup, event listeners, and lifecycle management
|
|
* Provides resource management, connection tracking, and monitoring
|
|
*/
|
|
export class ConnectionManager {
|
|
/**
|
|
* Reference to the SMTP server instance
|
|
*/
|
|
smtpServer;
|
|
/**
|
|
* Set of active socket connections
|
|
*/
|
|
activeConnections = new Set();
|
|
/**
|
|
* Connection tracking for resource management
|
|
*/
|
|
connectionStats = {
|
|
totalConnections: 0,
|
|
activeConnections: 0,
|
|
peakConnections: 0,
|
|
rejectedConnections: 0,
|
|
closedConnections: 0,
|
|
erroredConnections: 0,
|
|
timedOutConnections: 0
|
|
};
|
|
/**
|
|
* Per-IP connection tracking for rate limiting
|
|
*/
|
|
ipConnections = new Map();
|
|
/**
|
|
* Resource monitoring interval
|
|
*/
|
|
resourceCheckInterval = null;
|
|
/**
|
|
* Track cleanup timers so we can clear them
|
|
*/
|
|
cleanupTimers = new Set();
|
|
/**
|
|
* SMTP server options with enhanced resource controls
|
|
*/
|
|
options;
|
|
/**
|
|
* Creates a new connection manager with enhanced resource management
|
|
* @param smtpServer - SMTP server instance
|
|
*/
|
|
constructor(smtpServer) {
|
|
this.smtpServer = smtpServer;
|
|
// Get options from server
|
|
const serverOptions = this.smtpServer.getOptions();
|
|
// Default values for resource management - adjusted for production scalability
|
|
const DEFAULT_MAX_CONNECTIONS_PER_IP = 50; // Increased to support high-concurrency scenarios
|
|
const DEFAULT_CONNECTION_RATE_LIMIT = 200; // Increased for production load handling
|
|
const DEFAULT_CONNECTION_RATE_WINDOW = 60 * 1000; // 60 seconds window
|
|
const DEFAULT_BUFFER_SIZE_LIMIT = 10 * 1024 * 1024; // 10 MB
|
|
const DEFAULT_RESOURCE_CHECK_INTERVAL = 30 * 1000; // 30 seconds
|
|
this.options = {
|
|
hostname: serverOptions.hostname || SMTP_DEFAULTS.HOSTNAME,
|
|
maxConnections: serverOptions.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS,
|
|
socketTimeout: serverOptions.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT,
|
|
maxConnectionsPerIP: DEFAULT_MAX_CONNECTIONS_PER_IP,
|
|
connectionRateLimit: DEFAULT_CONNECTION_RATE_LIMIT,
|
|
connectionRateWindow: DEFAULT_CONNECTION_RATE_WINDOW,
|
|
bufferSizeLimit: DEFAULT_BUFFER_SIZE_LIMIT,
|
|
resourceCheckInterval: DEFAULT_RESOURCE_CHECK_INTERVAL
|
|
};
|
|
// Start resource monitoring
|
|
this.startResourceMonitoring();
|
|
}
|
|
/**
|
|
* Start resource monitoring interval to check resource usage
|
|
*/
|
|
startResourceMonitoring() {
|
|
// Clear any existing interval
|
|
if (this.resourceCheckInterval) {
|
|
clearInterval(this.resourceCheckInterval);
|
|
}
|
|
// Set up new interval
|
|
this.resourceCheckInterval = setInterval(() => {
|
|
this.monitorResourceUsage();
|
|
}, this.options.resourceCheckInterval);
|
|
}
|
|
/**
|
|
* Monitor resource usage and log statistics
|
|
*/
|
|
monitorResourceUsage() {
|
|
// Calculate memory usage
|
|
const memoryUsage = process.memoryUsage();
|
|
const memoryUsageMB = {
|
|
rss: Math.round(memoryUsage.rss / 1024 / 1024),
|
|
heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024),
|
|
heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024),
|
|
external: Math.round(memoryUsage.external / 1024 / 1024)
|
|
};
|
|
// Calculate connection rate metrics
|
|
const activeIPs = Array.from(this.ipConnections.entries())
|
|
.filter(([_, data]) => data.count > 0).length;
|
|
const highVolumeIPs = Array.from(this.ipConnections.entries())
|
|
.filter(([_, data]) => data.count > this.options.connectionRateLimit / 2).length;
|
|
// Log resource usage with more detailed metrics
|
|
SmtpLogger.info('Resource usage stats', {
|
|
connections: {
|
|
active: this.activeConnections.size,
|
|
total: this.connectionStats.totalConnections,
|
|
peak: this.connectionStats.peakConnections,
|
|
rejected: this.connectionStats.rejectedConnections,
|
|
closed: this.connectionStats.closedConnections,
|
|
errored: this.connectionStats.erroredConnections,
|
|
timedOut: this.connectionStats.timedOutConnections
|
|
},
|
|
memory: memoryUsageMB,
|
|
ipTracking: {
|
|
uniqueIPs: this.ipConnections.size,
|
|
activeIPs: activeIPs,
|
|
highVolumeIPs: highVolumeIPs
|
|
},
|
|
resourceLimits: {
|
|
maxConnections: this.options.maxConnections,
|
|
maxConnectionsPerIP: this.options.maxConnectionsPerIP,
|
|
connectionRateLimit: this.options.connectionRateLimit,
|
|
bufferSizeLimit: Math.round(this.options.bufferSizeLimit / 1024 / 1024) + 'MB'
|
|
}
|
|
});
|
|
// Check for potential DoS conditions
|
|
if (highVolumeIPs > 3) {
|
|
SmtpLogger.warn(`Potential DoS detected: ${highVolumeIPs} IPs with high connection rates`);
|
|
}
|
|
// Assess memory usage trends
|
|
if (memoryUsageMB.heapUsed > 500) { // Over 500MB heap used
|
|
SmtpLogger.warn(`High memory usage detected: ${memoryUsageMB.heapUsed}MB heap used`);
|
|
}
|
|
// Clean up expired IP rate limits and validate resource tracking
|
|
this.cleanupIpRateLimits();
|
|
}
|
|
/**
|
|
* Clean up expired IP rate limits and perform additional resource monitoring
|
|
*/
|
|
cleanupIpRateLimits() {
|
|
const now = Date.now();
|
|
const windowThreshold = now - this.options.connectionRateWindow;
|
|
let activeIps = 0;
|
|
let removedEntries = 0;
|
|
// Iterate through IP connections and manage entries
|
|
for (const [ip, data] of this.ipConnections.entries()) {
|
|
// If the last connection was before the window threshold + one extra window, remove the entry
|
|
if (data.lastConnection < windowThreshold - this.options.connectionRateWindow) {
|
|
// Remove stale entries to prevent memory growth
|
|
this.ipConnections.delete(ip);
|
|
removedEntries++;
|
|
}
|
|
// If last connection was before the window threshold, reset the count
|
|
else if (data.lastConnection < windowThreshold) {
|
|
if (data.count > 0) {
|
|
// Reset but keep the IP in the map with a zero count
|
|
this.ipConnections.set(ip, {
|
|
count: 0,
|
|
firstConnection: now,
|
|
lastConnection: now
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// This IP is still active within the current window
|
|
activeIps++;
|
|
}
|
|
}
|
|
// Log cleanup activity if significant changes occurred
|
|
if (removedEntries > 0) {
|
|
SmtpLogger.debug(`IP rate limit cleanup: removed ${removedEntries} stale entries, ${this.ipConnections.size} remaining, ${activeIps} active in current window`);
|
|
}
|
|
// Check for memory leaks in connection tracking
|
|
if (this.activeConnections.size > 0 && this.connectionStats.activeConnections !== this.activeConnections.size) {
|
|
SmtpLogger.warn(`Connection tracking inconsistency detected: stats.active=${this.connectionStats.activeConnections}, actual=${this.activeConnections.size}`);
|
|
// Fix the inconsistency
|
|
this.connectionStats.activeConnections = this.activeConnections.size;
|
|
}
|
|
// Validate and clean leaked resources if needed
|
|
this.validateResourceTracking();
|
|
}
|
|
/**
|
|
* Validate and repair resource tracking to prevent leaks
|
|
*/
|
|
validateResourceTracking() {
|
|
// Prepare a detailed report if inconsistencies are found
|
|
const inconsistenciesFound = [];
|
|
// 1. Check active connections count matches activeConnections set size
|
|
if (this.connectionStats.activeConnections !== this.activeConnections.size) {
|
|
inconsistenciesFound.push({
|
|
issue: 'Active connection count mismatch',
|
|
stats: this.connectionStats.activeConnections,
|
|
actual: this.activeConnections.size,
|
|
action: 'Auto-corrected'
|
|
});
|
|
this.connectionStats.activeConnections = this.activeConnections.size;
|
|
}
|
|
// 2. Check for destroyed sockets in active connections
|
|
let destroyedSocketsCount = 0;
|
|
const socketsToRemove = [];
|
|
for (const socket of this.activeConnections) {
|
|
if (socket.destroyed) {
|
|
destroyedSocketsCount++;
|
|
socketsToRemove.push(socket);
|
|
}
|
|
}
|
|
// Remove destroyed sockets from tracking
|
|
for (const socket of socketsToRemove) {
|
|
this.activeConnections.delete(socket);
|
|
// Also ensure all listeners are removed
|
|
try {
|
|
socket.removeAllListeners();
|
|
}
|
|
catch {
|
|
// Ignore errors from removeAllListeners
|
|
}
|
|
}
|
|
if (destroyedSocketsCount > 0) {
|
|
inconsistenciesFound.push({
|
|
issue: 'Destroyed sockets in active list',
|
|
count: destroyedSocketsCount,
|
|
action: 'Removed from tracking'
|
|
});
|
|
// Update active connections count after cleanup
|
|
this.connectionStats.activeConnections = this.activeConnections.size;
|
|
}
|
|
// 3. Check for sessions without corresponding active connections
|
|
const sessionCount = this.smtpServer.getSessionManager().getSessionCount();
|
|
if (sessionCount > this.activeConnections.size) {
|
|
inconsistenciesFound.push({
|
|
issue: 'Orphaned sessions',
|
|
sessions: sessionCount,
|
|
connections: this.activeConnections.size,
|
|
action: 'Session cleanup recommended'
|
|
});
|
|
}
|
|
// If any inconsistencies found, log a detailed report
|
|
if (inconsistenciesFound.length > 0) {
|
|
SmtpLogger.warn('Resource tracking inconsistencies detected and repaired', { inconsistencies: inconsistenciesFound });
|
|
}
|
|
}
|
|
/**
|
|
* Handle a new connection with resource management
|
|
* @param socket - Client socket
|
|
*/
|
|
async handleNewConnection(socket) {
|
|
// Update connection stats
|
|
this.connectionStats.totalConnections++;
|
|
this.connectionStats.activeConnections = this.activeConnections.size + 1;
|
|
if (this.connectionStats.activeConnections > this.connectionStats.peakConnections) {
|
|
this.connectionStats.peakConnections = this.connectionStats.activeConnections;
|
|
}
|
|
// Get client IP
|
|
const remoteAddress = socket.remoteAddress || '0.0.0.0';
|
|
// Use UnifiedRateLimiter for connection rate limiting
|
|
const emailServer = this.smtpServer.getEmailServer();
|
|
const rateLimiter = emailServer.getRateLimiter();
|
|
// Check connection limit with UnifiedRateLimiter
|
|
const connectionResult = rateLimiter.recordConnection(remoteAddress);
|
|
if (!connectionResult.allowed) {
|
|
this.rejectConnection(socket, connectionResult.reason || 'Rate limit exceeded');
|
|
this.connectionStats.rejectedConnections++;
|
|
return;
|
|
}
|
|
// Still track IP connections locally for cleanup purposes
|
|
this.trackIPConnection(remoteAddress);
|
|
// Check if maximum global connections reached
|
|
if (this.hasReachedMaxConnections()) {
|
|
this.rejectConnection(socket, 'Too many connections');
|
|
this.connectionStats.rejectedConnections++;
|
|
return;
|
|
}
|
|
// Add socket to active connections
|
|
this.activeConnections.add(socket);
|
|
// Set up socket options
|
|
socket.setKeepAlive(true);
|
|
socket.setTimeout(this.options.socketTimeout);
|
|
// Explicitly set socket buffer sizes to prevent memory issues
|
|
socket.setNoDelay(true); // Disable Nagle's algorithm for better responsiveness
|
|
// Set limits on socket buffer size if supported by Node.js version
|
|
try {
|
|
// Here we set reasonable buffer limits to prevent memory exhaustion attacks
|
|
const highWaterMark = 64 * 1024; // 64 KB
|
|
// Note: Socket high water mark methods can't be set directly in newer Node.js versions
|
|
// These would need to be set during socket creation or with a different API
|
|
}
|
|
catch (error) {
|
|
// Ignore errors from older Node.js versions that don't support these methods
|
|
SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
// Set up event handlers
|
|
this.setupSocketEventHandlers(socket);
|
|
// Create a session for this connection
|
|
this.smtpServer.getSessionManager().createSession(socket, false);
|
|
// Log the new connection using adaptive logger
|
|
const socketDetails = getSocketDetails(socket);
|
|
adaptiveLogger.logConnection(socket, 'connect');
|
|
// Update adaptive logger with current connection count
|
|
adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections);
|
|
// Send greeting
|
|
this.sendGreeting(socket);
|
|
}
|
|
/**
|
|
* Check if an IP has exceeded the rate limit
|
|
* @param ip - Client IP address
|
|
* @returns True if rate limited
|
|
*/
|
|
isIPRateLimited(ip) {
|
|
const now = Date.now();
|
|
const ipData = this.ipConnections.get(ip);
|
|
if (!ipData) {
|
|
return false; // No previous connections
|
|
}
|
|
// Check if we're within the rate window
|
|
const isWithinWindow = now - ipData.firstConnection <= this.options.connectionRateWindow;
|
|
// If within window and count exceeds limit, rate limit is applied
|
|
if (isWithinWindow && ipData.count >= this.options.connectionRateLimit) {
|
|
SmtpLogger.warn(`Rate limit exceeded for IP ${ip}: ${ipData.count} connections in ${Math.round((now - ipData.firstConnection) / 1000)}s`);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Track a new connection from an IP
|
|
* @param ip - Client IP address
|
|
*/
|
|
trackIPConnection(ip) {
|
|
const now = Date.now();
|
|
const ipData = this.ipConnections.get(ip);
|
|
if (!ipData) {
|
|
// First connection from this IP
|
|
this.ipConnections.set(ip, {
|
|
count: 1,
|
|
firstConnection: now,
|
|
lastConnection: now
|
|
});
|
|
}
|
|
else {
|
|
// Check if we need to reset the window
|
|
if (now - ipData.lastConnection > this.options.connectionRateWindow) {
|
|
// Reset the window
|
|
this.ipConnections.set(ip, {
|
|
count: 1,
|
|
firstConnection: now,
|
|
lastConnection: now
|
|
});
|
|
}
|
|
else {
|
|
// Increment within the current window
|
|
this.ipConnections.set(ip, {
|
|
count: ipData.count + 1,
|
|
firstConnection: ipData.firstConnection,
|
|
lastConnection: now
|
|
});
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Check if an IP has reached its connection limit
|
|
* @param ip - Client IP address
|
|
* @returns True if limit reached
|
|
*/
|
|
hasReachedIPConnectionLimit(ip) {
|
|
let ipConnectionCount = 0;
|
|
// Count active connections from this IP
|
|
for (const socket of this.activeConnections) {
|
|
if (socket.remoteAddress === ip) {
|
|
ipConnectionCount++;
|
|
}
|
|
}
|
|
return ipConnectionCount >= this.options.maxConnectionsPerIP;
|
|
}
|
|
/**
|
|
* Handle a new secure TLS connection with resource management
|
|
* @param socket - Client TLS socket
|
|
*/
|
|
async handleNewSecureConnection(socket) {
|
|
// Update connection stats
|
|
this.connectionStats.totalConnections++;
|
|
this.connectionStats.activeConnections = this.activeConnections.size + 1;
|
|
if (this.connectionStats.activeConnections > this.connectionStats.peakConnections) {
|
|
this.connectionStats.peakConnections = this.connectionStats.activeConnections;
|
|
}
|
|
// Get client IP
|
|
const remoteAddress = socket.remoteAddress || '0.0.0.0';
|
|
// Use UnifiedRateLimiter for connection rate limiting
|
|
const emailServer = this.smtpServer.getEmailServer();
|
|
const rateLimiter = emailServer.getRateLimiter();
|
|
// Check connection limit with UnifiedRateLimiter
|
|
const connectionResult = rateLimiter.recordConnection(remoteAddress);
|
|
if (!connectionResult.allowed) {
|
|
this.rejectConnection(socket, connectionResult.reason || 'Rate limit exceeded');
|
|
this.connectionStats.rejectedConnections++;
|
|
return;
|
|
}
|
|
// Still track IP connections locally for cleanup purposes
|
|
this.trackIPConnection(remoteAddress);
|
|
// Check if maximum global connections reached
|
|
if (this.hasReachedMaxConnections()) {
|
|
this.rejectConnection(socket, 'Too many connections');
|
|
this.connectionStats.rejectedConnections++;
|
|
return;
|
|
}
|
|
// Add socket to active connections
|
|
this.activeConnections.add(socket);
|
|
// Set up socket options
|
|
socket.setKeepAlive(true);
|
|
socket.setTimeout(this.options.socketTimeout);
|
|
// Explicitly set socket buffer sizes to prevent memory issues
|
|
socket.setNoDelay(true); // Disable Nagle's algorithm for better responsiveness
|
|
// Set limits on socket buffer size if supported by Node.js version
|
|
try {
|
|
// Here we set reasonable buffer limits to prevent memory exhaustion attacks
|
|
const highWaterMark = 64 * 1024; // 64 KB
|
|
// Note: Socket high water mark methods can't be set directly in newer Node.js versions
|
|
// These would need to be set during socket creation or with a different API
|
|
}
|
|
catch (error) {
|
|
// Ignore errors from older Node.js versions that don't support these methods
|
|
SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
// Set up event handlers
|
|
this.setupSocketEventHandlers(socket);
|
|
// Create a session for this connection
|
|
this.smtpServer.getSessionManager().createSession(socket, true);
|
|
// Log the new secure connection using adaptive logger
|
|
adaptiveLogger.logConnection(socket, 'connect');
|
|
// Update adaptive logger with current connection count
|
|
adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections);
|
|
// Send greeting
|
|
this.sendGreeting(socket);
|
|
}
|
|
/**
|
|
* Set up event handlers for a socket with enhanced resource management
|
|
* @param socket - Client socket
|
|
*/
|
|
setupSocketEventHandlers(socket) {
|
|
// Store existing socket event handlers before adding new ones
|
|
const existingDataHandler = socket.listeners('data')[0];
|
|
const existingCloseHandler = socket.listeners('close')[0];
|
|
const existingErrorHandler = socket.listeners('error')[0];
|
|
const existingTimeoutHandler = socket.listeners('timeout')[0];
|
|
// Remove existing event handlers if they exist
|
|
if (existingDataHandler)
|
|
socket.removeListener('data', existingDataHandler);
|
|
if (existingCloseHandler)
|
|
socket.removeListener('close', existingCloseHandler);
|
|
if (existingErrorHandler)
|
|
socket.removeListener('error', existingErrorHandler);
|
|
if (existingTimeoutHandler)
|
|
socket.removeListener('timeout', existingTimeoutHandler);
|
|
// Data event - process incoming data from the client with resource limits
|
|
let buffer = '';
|
|
let totalBytesReceived = 0;
|
|
socket.on('data', async (data) => {
|
|
try {
|
|
// Get current session and update activity timestamp
|
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
|
if (session) {
|
|
this.smtpServer.getSessionManager().updateSessionActivity(session);
|
|
}
|
|
// Check if we're in DATA receiving mode - handle differently
|
|
if (session && session.state === SmtpState.DATA_RECEIVING) {
|
|
// In DATA mode, pass raw chunks directly to command handler with special marker
|
|
// Don't line-buffer large email content
|
|
try {
|
|
const dataString = data.toString('utf8');
|
|
// Use a special prefix to indicate this is raw data, not a command line
|
|
// CRITICAL FIX: Must await to prevent async pile-up
|
|
await this.smtpServer.getCommandHandler().processCommand(socket, `__RAW_DATA__${dataString}`);
|
|
return;
|
|
}
|
|
catch (dataError) {
|
|
SmtpLogger.error(`Data handler error during DATA mode: ${dataError instanceof Error ? dataError.message : String(dataError)}`);
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
}
|
|
// For command mode, continue with line-buffered processing
|
|
// Check buffer size limits to prevent memory attacks
|
|
totalBytesReceived += data.length;
|
|
if (buffer.length > this.options.bufferSizeLimit) {
|
|
// Buffer is too large, reject the connection
|
|
SmtpLogger.warn(`Buffer size limit exceeded: ${buffer.length} bytes for ${socket.remoteAddress}`);
|
|
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too large, disconnecting`);
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
// Impose a total transfer limit to prevent DoS
|
|
if (totalBytesReceived > this.options.bufferSizeLimit * 2) {
|
|
SmtpLogger.warn(`Total transfer limit exceeded: ${totalBytesReceived} bytes for ${socket.remoteAddress}`);
|
|
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Transfer limit exceeded, disconnecting`);
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
// Convert buffer to string safely with explicit encoding
|
|
const dataString = data.toString('utf8');
|
|
// Buffer incoming data
|
|
buffer += dataString;
|
|
// Process complete lines
|
|
let lineEndPos;
|
|
while ((lineEndPos = buffer.indexOf(SMTP_DEFAULTS.CRLF)) !== -1) {
|
|
// Extract a complete line
|
|
const line = buffer.substring(0, lineEndPos);
|
|
buffer = buffer.substring(lineEndPos + 2); // +2 to skip CRLF
|
|
// Check line length to prevent extremely long lines
|
|
if (line.length > 4096) { // 4KB line limit is reasonable for SMTP
|
|
SmtpLogger.warn(`Line length limit exceeded: ${line.length} bytes for ${socket.remoteAddress}`);
|
|
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Line too long, disconnecting`);
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
// Process non-empty lines
|
|
if (line.length > 0) {
|
|
try {
|
|
// CRITICAL FIX: Must await processCommand to prevent async pile-up
|
|
// This was causing the busy loop with high CPU usage when many empty lines were processed
|
|
await this.smtpServer.getCommandHandler().processCommand(socket, line);
|
|
}
|
|
catch (error) {
|
|
// Handle any errors in command processing
|
|
SmtpLogger.error(`Command handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error`);
|
|
// If there's a severe error, close the connection
|
|
if (error instanceof Error &&
|
|
(error.message.includes('fatal') || error.message.includes('critical'))) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If buffer is getting too large without CRLF, it might be a DoS attempt
|
|
if (buffer.length > 10240) { // 10KB is a reasonable limit for a line without CRLF
|
|
SmtpLogger.warn(`Incomplete line too large: ${buffer.length} bytes for ${socket.remoteAddress}`);
|
|
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR} Incomplete line too large, disconnecting`);
|
|
socket.destroy();
|
|
}
|
|
}
|
|
catch (error) {
|
|
// Handle any unexpected errors during data processing
|
|
SmtpLogger.error(`Data handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
socket.destroy();
|
|
}
|
|
});
|
|
// Add drain event handler to manage flow control
|
|
socket.on('drain', () => {
|
|
// Socket buffer has been emptied, resume data flow if needed
|
|
if (socket.isPaused()) {
|
|
socket.resume();
|
|
SmtpLogger.debug(`Resumed socket for ${socket.remoteAddress} after drain`);
|
|
}
|
|
});
|
|
// Close event - clean up when connection is closed
|
|
socket.on('close', (hadError) => {
|
|
this.handleSocketClose(socket, hadError);
|
|
});
|
|
// Error event - handle socket errors
|
|
socket.on('error', (err) => {
|
|
this.handleSocketError(socket, err);
|
|
});
|
|
// Timeout event - handle socket timeouts
|
|
socket.on('timeout', () => {
|
|
this.handleSocketTimeout(socket);
|
|
});
|
|
}
|
|
/**
|
|
* Get the current connection count
|
|
* @returns Number of active connections
|
|
*/
|
|
getConnectionCount() {
|
|
return this.activeConnections.size;
|
|
}
|
|
/**
|
|
* Check if the server has reached the maximum number of connections
|
|
* @returns True if max connections reached
|
|
*/
|
|
hasReachedMaxConnections() {
|
|
return this.activeConnections.size >= this.options.maxConnections;
|
|
}
|
|
/**
|
|
* Close all active connections
|
|
*/
|
|
closeAllConnections() {
|
|
const connectionCount = this.activeConnections.size;
|
|
if (connectionCount === 0) {
|
|
return;
|
|
}
|
|
SmtpLogger.info(`Closing all connections (count: ${connectionCount})`);
|
|
for (const socket of this.activeConnections) {
|
|
try {
|
|
// Send service closing notification
|
|
this.sendServiceClosing(socket);
|
|
// End the socket gracefully
|
|
socket.end();
|
|
// Force destroy after a short delay if not already destroyed
|
|
const destroyTimer = setTimeout(() => {
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
this.cleanupTimers.delete(destroyTimer);
|
|
}, 100);
|
|
this.cleanupTimers.add(destroyTimer);
|
|
}
|
|
catch (error) {
|
|
SmtpLogger.error(`Error closing connection: ${error instanceof Error ? error.message : String(error)}`);
|
|
// Force destroy on error
|
|
try {
|
|
socket.destroy();
|
|
}
|
|
catch (e) {
|
|
// Ignore destroy errors
|
|
}
|
|
}
|
|
}
|
|
// Clear active connections
|
|
this.activeConnections.clear();
|
|
// Stop resource monitoring to prevent hanging timers
|
|
if (this.resourceCheckInterval) {
|
|
clearInterval(this.resourceCheckInterval);
|
|
this.resourceCheckInterval = null;
|
|
}
|
|
}
|
|
/**
|
|
* Handle socket close event
|
|
* @param socket - Client socket
|
|
* @param hadError - Whether the socket was closed due to error
|
|
*/
|
|
handleSocketClose(socket, hadError) {
|
|
try {
|
|
// Update connection statistics
|
|
this.connectionStats.closedConnections++;
|
|
this.connectionStats.activeConnections = this.activeConnections.size - 1;
|
|
// Get socket details for logging
|
|
const socketDetails = getSocketDetails(socket);
|
|
const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`;
|
|
// Log with appropriate level based on whether there was an error
|
|
if (hadError) {
|
|
SmtpLogger.warn(`Socket closed with error: ${socketId}`);
|
|
}
|
|
else {
|
|
SmtpLogger.debug(`Socket closed normally: ${socketId}`);
|
|
}
|
|
// Get the session before removing it
|
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
|
// Remove from active connections
|
|
this.activeConnections.delete(socket);
|
|
// Remove from session manager
|
|
this.smtpServer.getSessionManager().removeSession(socket);
|
|
// Cancel any timeout ID stored in the session
|
|
if (session?.dataTimeoutId) {
|
|
clearTimeout(session.dataTimeoutId);
|
|
}
|
|
// Remove all event listeners to prevent memory leaks
|
|
socket.removeAllListeners();
|
|
// Log connection close with session details if available
|
|
adaptiveLogger.logConnection(socket, 'close', session);
|
|
// Update adaptive logger with new connection count
|
|
adaptiveLogger.updateConnectionCount(this.connectionStats.activeConnections);
|
|
}
|
|
catch (error) {
|
|
// Handle any unexpected errors during cleanup
|
|
SmtpLogger.error(`Error in handleSocketClose: ${error instanceof Error ? error.message : String(error)}`);
|
|
// Ensure socket is removed from active connections even if an error occurs
|
|
this.activeConnections.delete(socket);
|
|
// Always try to remove all listeners even on error
|
|
try {
|
|
socket.removeAllListeners();
|
|
}
|
|
catch {
|
|
// Ignore errors from removeAllListeners
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Handle socket error event
|
|
* @param socket - Client socket
|
|
* @param error - Error object
|
|
*/
|
|
handleSocketError(socket, error) {
|
|
try {
|
|
// Update connection statistics
|
|
this.connectionStats.erroredConnections++;
|
|
// Get socket details for context
|
|
const socketDetails = getSocketDetails(socket);
|
|
const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`;
|
|
// Get the session
|
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
|
// Detailed error logging with context information
|
|
SmtpLogger.error(`Socket error for ${socketId}: ${error.message}`, {
|
|
errorCode: error.code,
|
|
errorStack: error.stack,
|
|
sessionId: session?.id,
|
|
sessionState: session?.state,
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
remotePort: socketDetails.remotePort
|
|
});
|
|
// Log the error for connection tracking using adaptive logger
|
|
adaptiveLogger.logConnection(socket, 'error', session, error);
|
|
// Cancel any timeout ID stored in the session
|
|
if (session?.dataTimeoutId) {
|
|
clearTimeout(session.dataTimeoutId);
|
|
}
|
|
// Close the socket if not already closed
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
// Remove from active connections (cleanup after error)
|
|
this.activeConnections.delete(socket);
|
|
// Remove from session manager
|
|
this.smtpServer.getSessionManager().removeSession(socket);
|
|
}
|
|
catch (handlerError) {
|
|
// Meta-error handling (errors in the error handler)
|
|
SmtpLogger.error(`Error in handleSocketError: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`);
|
|
// Ensure socket is destroyed and removed from active connections
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
this.activeConnections.delete(socket);
|
|
}
|
|
}
|
|
/**
|
|
* Handle socket timeout event
|
|
* @param socket - Client socket
|
|
*/
|
|
handleSocketTimeout(socket) {
|
|
try {
|
|
// Update connection statistics
|
|
this.connectionStats.timedOutConnections++;
|
|
// Get socket details for context
|
|
const socketDetails = getSocketDetails(socket);
|
|
const socketId = `${socketDetails.remoteAddress}:${socketDetails.remotePort}`;
|
|
// Get the session
|
|
const session = this.smtpServer.getSessionManager().getSession(socket);
|
|
// Get timing information for better debugging
|
|
const now = Date.now();
|
|
const idleTime = session?.lastActivity ? now - session.lastActivity : 'unknown';
|
|
if (session) {
|
|
// Log the timeout with extended details
|
|
SmtpLogger.warn(`Socket timeout from ${session.remoteAddress}`, {
|
|
sessionId: session.id,
|
|
remoteAddress: session.remoteAddress,
|
|
state: session.state,
|
|
timeout: this.options.socketTimeout,
|
|
idleTime: idleTime,
|
|
emailState: session.envelope?.mailFrom ? 'has-sender' : 'no-sender',
|
|
recipientCount: session.envelope?.rcptTo?.length || 0
|
|
});
|
|
// Cancel any timeout ID stored in the session
|
|
if (session.dataTimeoutId) {
|
|
clearTimeout(session.dataTimeoutId);
|
|
}
|
|
// Send timeout notification to client
|
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} Connection timeout - closing connection`);
|
|
}
|
|
else {
|
|
// Log timeout without session context
|
|
SmtpLogger.warn(`Socket timeout without session from ${socketId}`);
|
|
}
|
|
// Close the socket gracefully
|
|
try {
|
|
socket.end();
|
|
// Set a forced close timeout in case socket.end() doesn't close the connection
|
|
const timeoutDestroyTimer = setTimeout(() => {
|
|
if (!socket.destroyed) {
|
|
SmtpLogger.warn(`Forcing destroy of timed out socket: ${socketId}`);
|
|
socket.destroy();
|
|
}
|
|
this.cleanupTimers.delete(timeoutDestroyTimer);
|
|
}, 5000); // 5 second grace period for socket to end properly
|
|
this.cleanupTimers.add(timeoutDestroyTimer);
|
|
}
|
|
catch (error) {
|
|
SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`);
|
|
// Ensure socket is destroyed even if end() fails
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
}
|
|
// Clean up resources
|
|
this.activeConnections.delete(socket);
|
|
this.smtpServer.getSessionManager().removeSession(socket);
|
|
}
|
|
catch (handlerError) {
|
|
// Handle any unexpected errors during timeout handling
|
|
SmtpLogger.error(`Error in handleSocketTimeout: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`);
|
|
// Ensure socket is destroyed and removed from tracking
|
|
if (!socket.destroyed) {
|
|
socket.destroy();
|
|
}
|
|
this.activeConnections.delete(socket);
|
|
}
|
|
}
|
|
/**
|
|
* Reject a connection
|
|
* @param socket - Client socket
|
|
* @param reason - Reason for rejection
|
|
*/
|
|
rejectConnection(socket, reason) {
|
|
// Log the rejection
|
|
const socketDetails = getSocketDetails(socket);
|
|
SmtpLogger.warn(`Connection rejected from ${socketDetails.remoteAddress}:${socketDetails.remotePort}: ${reason}`);
|
|
// Send rejection message
|
|
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} ${this.options.hostname} Service temporarily unavailable - ${reason}`);
|
|
// Close the socket
|
|
try {
|
|
socket.end();
|
|
}
|
|
catch (error) {
|
|
SmtpLogger.error(`Error ending rejected socket: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
/**
|
|
* Send greeting message
|
|
* @param socket - Client socket
|
|
*/
|
|
sendGreeting(socket) {
|
|
const greeting = `${SmtpResponseCode.SERVICE_READY} ${this.options.hostname} ESMTP service ready`;
|
|
this.sendResponse(socket, greeting);
|
|
}
|
|
/**
|
|
* Send service closing notification
|
|
* @param socket - Client socket
|
|
*/
|
|
sendServiceClosing(socket) {
|
|
const message = `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`;
|
|
this.sendResponse(socket, message);
|
|
}
|
|
/**
|
|
* Send response to client
|
|
* @param socket - Client socket
|
|
* @param response - Response to send
|
|
*/
|
|
sendResponse(socket, response) {
|
|
// 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}${SMTP_DEFAULTS.CRLF}`);
|
|
adaptiveLogger.logResponse(response, socket);
|
|
}
|
|
catch (error) {
|
|
// Log error and destroy socket
|
|
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();
|
|
}
|
|
}
|
|
/**
|
|
* Handle a new connection (interface requirement)
|
|
*/
|
|
async handleConnection(socket, secure) {
|
|
if (secure) {
|
|
this.handleNewSecureConnection(socket);
|
|
}
|
|
else {
|
|
this.handleNewConnection(socket);
|
|
}
|
|
}
|
|
/**
|
|
* Check if accepting new connections (interface requirement)
|
|
*/
|
|
canAcceptConnection() {
|
|
return !this.hasReachedMaxConnections();
|
|
}
|
|
/**
|
|
* Clean up resources
|
|
*/
|
|
destroy() {
|
|
// Clear resource monitoring interval
|
|
if (this.resourceCheckInterval) {
|
|
clearInterval(this.resourceCheckInterval);
|
|
this.resourceCheckInterval = null;
|
|
}
|
|
// Clear all cleanup timers
|
|
for (const timer of this.cleanupTimers) {
|
|
clearTimeout(timer);
|
|
}
|
|
this.cleanupTimers.clear();
|
|
// Close all active connections
|
|
this.closeAllConnections();
|
|
// Clear maps
|
|
this.activeConnections.clear();
|
|
this.ipConnections.clear();
|
|
// Reset connection stats
|
|
this.connectionStats = {
|
|
totalConnections: 0,
|
|
activeConnections: 0,
|
|
peakConnections: 0,
|
|
rejectedConnections: 0,
|
|
closedConnections: 0,
|
|
erroredConnections: 0,
|
|
timedOutConnections: 0
|
|
};
|
|
SmtpLogger.debug('ConnectionManager destroyed');
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29ubmVjdGlvbi1tYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vdHMvbWFpbC9kZWxpdmVyeS9zbXRwc2VydmVyL2Nvbm5lY3Rpb24tbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFFSCxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBRS9DLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxhQUFhLEVBQUUsU0FBUyxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDNUUsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsdUJBQXVCLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUUvRTs7OztHQUlHO0FBQ0gsTUFBTSxPQUFPLGlCQUFpQjtJQUM1Qjs7T0FFRztJQUNLLFVBQVUsQ0FBYztJQUVoQzs7T0FFRztJQUNLLGlCQUFpQixHQUFvRCxJQUFJLEdBQUcsRUFBRSxDQUFDO0lBRXZGOztPQUVHO0lBQ0ssZUFBZSxHQUFHO1FBQ3hCLGdCQUFnQixFQUFFLENBQUM7UUFDbkIsaUJBQWlCLEVBQUUsQ0FBQztRQUNwQixlQUFlLEVBQUUsQ0FBQztRQUNsQixtQkFBbUIsRUFBRSxDQUFDO1FBQ3RCLGlCQUFpQixFQUFFLENBQUM7UUFDcEIsa0JBQWtCLEVBQUUsQ0FBQztRQUNyQixtQkFBbUIsRUFBRSxDQUFDO0tBQ3ZCLENBQUM7SUFFRjs7T0FFRztJQUNLLGFBQWEsR0FJaEIsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUVmOztPQUVHO0lBQ0sscUJBQXFCLEdBQTBCLElBQUksQ0FBQztJQUU1RDs7T0FFRztJQUNLLGFBQWEsR0FBd0IsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUV2RDs7T0FFRztJQUNLLE9BQU8sQ0FTYjtJQUVGOzs7T0FHRztJ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
|