update
This commit is contained in:
@@ -88,9 +88,10 @@ export class SMTPServer {
|
|||||||
try {
|
try {
|
||||||
// Convert certificates to Buffer format for Node.js TLS
|
// Convert certificates to Buffer format for Node.js TLS
|
||||||
// This helps prevent ASN.1 encoding issues when Node parses the certificates
|
// This helps prevent ASN.1 encoding issues when Node parses the certificates
|
||||||
const key = Buffer.from(optionsArg.key.trim());
|
// Use explicit 'utf8' encoding to handle PEM certificates properly
|
||||||
const cert = Buffer.from(optionsArg.cert.trim());
|
const key = Buffer.from(optionsArg.key, 'utf8');
|
||||||
const ca = optionsArg.ca ? Buffer.from(optionsArg.ca.trim()) : undefined;
|
const cert = Buffer.from(optionsArg.cert, 'utf8');
|
||||||
|
const ca = optionsArg.ca ? Buffer.from(optionsArg.ca, 'utf8') : undefined;
|
||||||
|
|
||||||
logger.log('debug', 'Creating TLS server with certificates', {
|
logger.log('debug', 'Creating TLS server with certificates', {
|
||||||
keyBufferLength: key.length,
|
keyBufferLength: key.length,
|
||||||
@@ -98,22 +99,31 @@ export class SMTPServer {
|
|||||||
caBufferLength: ca ? ca.length : 0
|
caBufferLength: ca ? ca.length : 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// TLS configuration for secure connections
|
// TLS configuration for secure connections with broader compatibility
|
||||||
const tlsOptions: plugins.tls.TlsOptions = {
|
const tlsOptions: plugins.tls.TlsOptions = {
|
||||||
key: key,
|
key: key,
|
||||||
cert: cert,
|
cert: cert,
|
||||||
ca: ca,
|
ca: ca,
|
||||||
// Recommended security options
|
// Support a wider range of TLS versions for better compatibility
|
||||||
minVersion: 'TLSv1.2',
|
// Note: this is a key fix for the "wrong version number" error
|
||||||
honorCipherOrder: true,
|
minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0)
|
||||||
|
maxVersion: 'TLSv1.3', // Support latest TLS version (1.3)
|
||||||
|
// Let the client choose the cipher for better compatibility
|
||||||
|
honorCipherOrder: false,
|
||||||
// Allow self-signed certificates for test environments
|
// Allow self-signed certificates for test environments
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
// Enable session reuse for better performance
|
// Enable session reuse for better performance
|
||||||
sessionTimeout: 300,
|
sessionTimeout: 600,
|
||||||
// Add cipher suites for better compatibility with clients
|
// Use a broader set of ciphers for maximum compatibility
|
||||||
ciphers: 'HIGH:!aNULL:!MD5:!RC4',
|
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4',
|
||||||
// Allow client-initiated renegotiation for SMTP
|
// Allow client-initiated renegotiation for SMTP
|
||||||
allowRenegotiation: true
|
allowRenegotiation: true,
|
||||||
|
// Longer handshake timeout for reliability
|
||||||
|
handshakeTimeout: 30000,
|
||||||
|
// Disable secure options to allow more flexibility
|
||||||
|
secureOptions: 0,
|
||||||
|
// For debugging
|
||||||
|
enableTrace: true
|
||||||
};
|
};
|
||||||
|
|
||||||
this.secureServer = plugins.tls.createServer(tlsOptions);
|
this.secureServer = plugins.tls.createServer(tlsOptions);
|
||||||
|
283
ts/mail/delivery/smtpserver/certificate-utils.ts
Normal file
283
ts/mail/delivery/smtpserver/certificate-utils.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* Certificate Utilities for SMTP Server
|
||||||
|
* Provides utilities for managing TLS certificates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as tls from 'tls';
|
||||||
|
import { SmtpLogger } from './utils/logging.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificate data
|
||||||
|
*/
|
||||||
|
export interface ICertificateData {
|
||||||
|
key: Buffer;
|
||||||
|
cert: Buffer;
|
||||||
|
ca?: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a PEM certificate string
|
||||||
|
* @param str - Certificate string
|
||||||
|
* @returns Normalized certificate string
|
||||||
|
*/
|
||||||
|
function normalizeCertificate(str: string): string {
|
||||||
|
if (!str) {
|
||||||
|
throw new Error('Empty certificate data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any whitespace around the string
|
||||||
|
let normalizedStr = str.trim();
|
||||||
|
|
||||||
|
// Make sure it has proper PEM format
|
||||||
|
if (!normalizedStr.includes('-----BEGIN ')) {
|
||||||
|
throw new Error('Invalid certificate format: Missing BEGIN marker');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!normalizedStr.includes('-----END ')) {
|
||||||
|
throw new Error('Invalid certificate format: Missing END marker');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize line endings (replace Windows-style \r\n with Unix-style \n)
|
||||||
|
normalizedStr = normalizedStr.replace(/\r\n/g, '\n');
|
||||||
|
|
||||||
|
// Ensure proper line breaks after header and before footer
|
||||||
|
const beginMatch = normalizedStr.match(/^(-----BEGIN [^-]+-----)(.*)$/s);
|
||||||
|
const endMatch = normalizedStr.match(/(.*)(-----END [^-]+-----)$/s);
|
||||||
|
|
||||||
|
if (beginMatch && endMatch) {
|
||||||
|
const header = beginMatch[1];
|
||||||
|
const footer = endMatch[2];
|
||||||
|
let content = normalizedStr.substring(header.length, normalizedStr.length - footer.length);
|
||||||
|
|
||||||
|
// Clean up any existing line breaks or spaces in the content
|
||||||
|
content = content.replace(/[\n\r\s]/g, '');
|
||||||
|
|
||||||
|
// Add proper line breaks (every 64 characters)
|
||||||
|
let formattedContent = '';
|
||||||
|
for (let i = 0; i < content.length; i += 64) {
|
||||||
|
formattedContent += content.substring(i, Math.min(i + 64, content.length)) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the certificate
|
||||||
|
return header + '\n' + formattedContent + footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load certificates from PEM format strings
|
||||||
|
* @param options - Certificate options
|
||||||
|
* @returns Certificate data with Buffer format
|
||||||
|
*/
|
||||||
|
export function loadCertificatesFromString(options: {
|
||||||
|
key: string;
|
||||||
|
cert: string;
|
||||||
|
ca?: string;
|
||||||
|
}): ICertificateData {
|
||||||
|
try {
|
||||||
|
// Try to fix and normalize certificates
|
||||||
|
try {
|
||||||
|
const key = normalizeCertificate(options.key);
|
||||||
|
const cert = normalizeCertificate(options.cert);
|
||||||
|
const ca = options.ca ? normalizeCertificate(options.ca) : undefined;
|
||||||
|
|
||||||
|
// Convert to Buffer with explicit utf8 encoding
|
||||||
|
const keyBuffer = Buffer.from(key, 'utf8');
|
||||||
|
const certBuffer = Buffer.from(cert, 'utf8');
|
||||||
|
const caBuffer = ca ? Buffer.from(ca, 'utf8') : undefined;
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
SmtpLogger.debug('Certificate properties', {
|
||||||
|
keyLength: keyBuffer.length,
|
||||||
|
certLength: certBuffer.length,
|
||||||
|
caLength: caBuffer ? caBuffer.length : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate the certificates by attempting to create a secure context
|
||||||
|
try {
|
||||||
|
const secureContext = tls.createSecureContext({
|
||||||
|
key: keyBuffer,
|
||||||
|
cert: certBuffer,
|
||||||
|
ca: caBuffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// If createSecureContext doesn't throw, the certificates are valid
|
||||||
|
SmtpLogger.info('Successfully validated certificate format');
|
||||||
|
} catch (validationError) {
|
||||||
|
SmtpLogger.error(`Certificate validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
|
||||||
|
throw validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: keyBuffer,
|
||||||
|
cert: certBuffer,
|
||||||
|
ca: caBuffer
|
||||||
|
};
|
||||||
|
} catch (innerError) {
|
||||||
|
SmtpLogger.warn(`Certificate normalization failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`);
|
||||||
|
throw innerError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
SmtpLogger.error(`Error loading certificates: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load certificates from files
|
||||||
|
* @param options - Certificate file paths
|
||||||
|
* @returns Certificate data with Buffer format
|
||||||
|
*/
|
||||||
|
export function loadCertificatesFromFiles(options: {
|
||||||
|
keyPath: string;
|
||||||
|
certPath: string;
|
||||||
|
caPath?: string;
|
||||||
|
}): ICertificateData {
|
||||||
|
try {
|
||||||
|
// Read files directly as Buffers
|
||||||
|
const key = fs.readFileSync(options.keyPath);
|
||||||
|
const cert = fs.readFileSync(options.certPath);
|
||||||
|
const ca = options.caPath ? fs.readFileSync(options.caPath) : undefined;
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
SmtpLogger.debug('Certificate file properties', {
|
||||||
|
keyLength: key.length,
|
||||||
|
certLength: cert.length,
|
||||||
|
caLength: ca ? ca.length : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate the certificates by attempting to create a secure context
|
||||||
|
try {
|
||||||
|
const secureContext = tls.createSecureContext({
|
||||||
|
key,
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
});
|
||||||
|
|
||||||
|
// If createSecureContext doesn't throw, the certificates are valid
|
||||||
|
SmtpLogger.info('Successfully validated certificate files');
|
||||||
|
} catch (validationError) {
|
||||||
|
SmtpLogger.error(`Certificate file validation error: ${validationError instanceof Error ? validationError.message : String(validationError)}`);
|
||||||
|
throw validationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
cert,
|
||||||
|
ca
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
SmtpLogger.error(`Error loading certificate files: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate self-signed certificates for testing
|
||||||
|
* @returns Certificate data with Buffer format
|
||||||
|
*/
|
||||||
|
export function generateSelfSignedCertificates(): ICertificateData {
|
||||||
|
// This is for fallback/testing only - log a warning
|
||||||
|
SmtpLogger.warn('Generating self-signed certificates for testing - DO NOT USE IN PRODUCTION');
|
||||||
|
|
||||||
|
// Create selfsigned certificates using node-forge or similar library
|
||||||
|
// For now, use hardcoded certificates as a last resort
|
||||||
|
const key = Buffer.from(`-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEgJW1HdJPACGB
|
||||||
|
ifoL3PB+HdAVA2nUmMfq43JbIUPXGTxCtzmQhuV04WjITwFw1loPx3ReHh4KR5yJ
|
||||||
|
BVdzUDocHuauMmBycHAjv7mImR/VkuK/SwT0Q5G/9/M55o6HUNol0UKt+uZuBy1r
|
||||||
|
ggFTdTDLw86i9UG5CZbWF/Yb/DTRoAkCr7iLnaZhhhqcdh5BGj7JBylIAV5RIW1y
|
||||||
|
xQxJVJZQT2KgCeCnHRRvYRQ7tVzUQBcSvtW4zYtqK4C39BgRyLUZQVYB7siGT/uP
|
||||||
|
YJE7R73u0xEgDMFWR1pItUYcVQXHQJ+YsLVCzqI22Mik7URdwxoSHSXRYKn6wnKg
|
||||||
|
4JYg65JnAgMBAAECggEAM2LlwRhwP0pnLlLHiPE4jJ3Qdz/NUF0hLnRhcUwW1iJ1
|
||||||
|
03jzCQ4QZ3etfL9O2hVJg49J+QUG50FNduLq4SE7GZj1dEJ/YNnlk9PpI8GSpLuA
|
||||||
|
mGTUKofIEJjNy5gKR0c6/rfgP8UXYSbRnTnZwIXVkUYuAUJLJTBVcJlcvCwJ3/zz
|
||||||
|
C8789JyOO1CNwF3zEIALdW5X5se8V+sw5iHDrHVxkR2xgsYpBBOylFfBxbMvV5o1
|
||||||
|
i+QOD1HaXdmIvjBCnHqrjX5SDnAYwHBSB9y6WbwC+Th76QHkRNcHZH86PJVdLEUi
|
||||||
|
tBPQmQh+SjDRaZzDJvURnOFks+eEsCPVPZnQ4wgnAQKBgQD8oHwGZIZRUjnXULNc
|
||||||
|
vJoPcjLpvdHRO0kXTJHtG2au2i9jVzL9SFwH1lHQM0XdXPnR2BK4Gmgc2dRnSB9n
|
||||||
|
YPPvCgyL2RS0Y7W98yEcgBgwVOJHnPQGRNwxUfCTHgmCQ7lXjQKKG51+dBfOYP3j
|
||||||
|
w8VYbS2pqxZtzzZ5zhk2BrZJdwKBgQDHDZC+NU80f7rLEr5vpwx9epTArwXre8oj
|
||||||
|
nGgzZ9/lE14qDnITBuZPUHWc4/7U1CCmP0vVH6nFVvhN9ra9QCTJBzQ5aj0l3JM7
|
||||||
|
9j8R5QZIPqOu4+aqf0ZFEgmpBK2SAYqNrJ+YVa2T/zLF44Jlr5WiLkPTUyMxV5+k
|
||||||
|
P4ZK8QP7wQKBgQCbeLuRWCuVKNYgYjm9TA55BbJL82J+MvhcbXUccpUksJQRxMV3
|
||||||
|
98PBUW0Qw38WciJxQF4naSKD/jXYndD+wGzpKMIU+tKU+sEYMnuFnx13++K8XrAe
|
||||||
|
NQPHDsK1wRgXk5ygOHx78xnZbMmwBXNLwQXIhyO8FJpwJHj2CtYvjb+2xwKBgQCn
|
||||||
|
KW/RiAHvG6GKjCHCOTlx2qLPxUiXYCk2xwvRnNfY5+2PFoqMI/RZLT/41kTda1fA
|
||||||
|
TDw+j4Uu/fF2ChPadwRiUjXZzZx/UjcMJXTpQ2kpbGJ11U/cL4+Tk0S6wz+HoS7z
|
||||||
|
w3vXT9UoDyFxDBjuMQJxJWTjmymaYUtNnz4iMuRqwQKBgH+HKbYHCZaIzXRMEO5S
|
||||||
|
T3xDMYH59dTEKKXEOA1KJ9Zo5XSD8NE9SQ+9etoOcEq8tdYS45OkHD3VyFQa7THu
|
||||||
|
58awjTdkpSmMPsw3AElOYDYJgD9oxKtTjwkXHqMjDBQZrXqzOImOAJhEVL+XH3LP
|
||||||
|
lv6RZ47YRC88T+P6n1yg6BPp
|
||||||
|
-----END PRIVATE KEY-----`, 'utf8');
|
||||||
|
|
||||||
|
const cert = Buffer.from(`-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDCTCCAfGgAwIBAgIUHxmGQOQoiSbzqh6hIe+7h9xDXIUwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDUyMTE2MDAzM1oXDTI2MDUy
|
||||||
|
MTE2MDAzM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAxICVtR3STwAhgYn6C9zwfh3QFQNp1JjH6uNyWyFD1xk8
|
||||||
|
Qrc5kIbldOFoyE8BcNZaD8d0Xh4eCkeciwOV3FwHR4brjJgcnRwI7+5iJkf1ZLiv
|
||||||
|
0sE9EORv/fzOeaOh1DaJdFCrfrmbgdgOUm62WNQOB2hq0kggjh/S1K+TBfF+8QFs
|
||||||
|
XQyW7y7mHecNgCgK/pI5b1irdajRc7nLvzM/U8qNn4jjrLsRoYqBPpn7aLKIBrmN
|
||||||
|
pNSIe18q8EYWkdmWBcnsZpAYv75SJG8E0lAYpMv9OEUIwsPh7AYUdkZqKtFxVxV5
|
||||||
|
bYlA5ZfnVnWrWEwRXaVdFFRXIjP+EFkGYYWThbvAIb0TPQIDAQABo1MwUTAdBgNV
|
||||||
|
HQ4EFgQUiW1MoYR8YK9KJTyip5oFoUVJoCgwHwYDVR0jBBgwFoAUiW1MoYR8YK9K
|
||||||
|
JTyip5oFoUVJoCgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||||
|
BToM8SbUQXwJ9rTlQB2QI2GJaFwTpCFoQZwGUOCkwGLM3nOPLEbNPMDoIKGPwenB
|
||||||
|
P1xL8uJEgYRqP6UG/xy3HsxYsLCxuoxGGP2QjuiQKnFl0n85usZ5flCxmLC5IzYx
|
||||||
|
FLcR6WPTdj6b5JX0tM8Bi6toQ9Pj3u3dSVPZKRLYvJvZKt1PXI8qsHD/LvNa2wGG
|
||||||
|
Zi1BQFAr2cScNYa+p6IYDJi9TBNxoBIHNTzQPfWaen4MHRJqUNZCzQXcOnU/NW5G
|
||||||
|
+QqQSEMmk8yGucEHWUMFrEbABVgYuBslICEEtBZALB2jZJYSaJnPOJCcmFrxUv61
|
||||||
|
ORWZbz+8rBL0JIeA7eFxEA==
|
||||||
|
-----END CERTIFICATE-----`, 'utf8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
cert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create TLS options for secure server or STARTTLS
|
||||||
|
* @param certificates - Certificate data
|
||||||
|
* @param isServer - Whether this is for server (true) or client (false)
|
||||||
|
* @returns TLS options
|
||||||
|
*/
|
||||||
|
export function createTlsOptions(
|
||||||
|
certificates: ICertificateData,
|
||||||
|
isServer: boolean = true
|
||||||
|
): tls.TlsOptions {
|
||||||
|
const options: tls.TlsOptions = {
|
||||||
|
key: certificates.key,
|
||||||
|
cert: certificates.cert,
|
||||||
|
ca: certificates.ca,
|
||||||
|
// Support a wider range of TLS versions for better compatibility
|
||||||
|
minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0)
|
||||||
|
maxVersion: 'TLSv1.3', // Support latest TLS version (1.3)
|
||||||
|
// Cipher suites for broad compatibility
|
||||||
|
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4',
|
||||||
|
// For testing, allow unauthorized (self-signed certs)
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
// Longer handshake timeout for reliability
|
||||||
|
handshakeTimeout: 30000,
|
||||||
|
// Allow renegotiation for better compatibility
|
||||||
|
allowRenegotiation: true,
|
||||||
|
// Increase timeout for better reliability under test conditions
|
||||||
|
sessionTimeout: 600,
|
||||||
|
// Let the client choose the cipher for better compatibility
|
||||||
|
honorCipherOrder: false,
|
||||||
|
// For debugging
|
||||||
|
enableTrace: true,
|
||||||
|
// Disable secure options to allow more flexibility
|
||||||
|
secureOptions: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Server-specific options
|
||||||
|
if (isServer) {
|
||||||
|
options.ALPNProtocols = ['smtp']; // Accept non-ALPN connections (legacy clients)
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
@@ -23,5 +23,10 @@ export { SmtpLogger } from './utils/logging.js';
|
|||||||
export * from './utils/validation.js';
|
export * from './utils/validation.js';
|
||||||
export * from './utils/helpers.js';
|
export * from './utils/helpers.js';
|
||||||
|
|
||||||
|
// Export TLS and certificate utilities
|
||||||
|
export * from './certificate-utils.js';
|
||||||
|
export * from './secure-server.js';
|
||||||
|
export * from './starttls-handler.js';
|
||||||
|
|
||||||
// Factory function to create a complete SMTP server with default components
|
// Factory function to create a complete SMTP server with default components
|
||||||
export { createSmtpServer } from './create-server.js';
|
export { createSmtpServer } from './create-server.js';
|
97
ts/mail/delivery/smtpserver/secure-server.ts
Normal file
97
ts/mail/delivery/smtpserver/secure-server.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Secure SMTP Server Utility Functions
|
||||||
|
* Provides helper functions for creating and managing secure TLS server
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import {
|
||||||
|
loadCertificatesFromString,
|
||||||
|
generateSelfSignedCertificates,
|
||||||
|
createTlsOptions,
|
||||||
|
type ICertificateData
|
||||||
|
} from './certificate-utils.js';
|
||||||
|
import { SmtpLogger } from './utils/logging.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a secure TLS server for direct TLS connections
|
||||||
|
* @param options - TLS certificate options
|
||||||
|
* @returns A configured TLS server or undefined if TLS is not available
|
||||||
|
*/
|
||||||
|
export function createSecureTlsServer(options: {
|
||||||
|
key: string;
|
||||||
|
cert: string;
|
||||||
|
ca?: string;
|
||||||
|
}): plugins.tls.Server | undefined {
|
||||||
|
try {
|
||||||
|
// Log the creation attempt
|
||||||
|
SmtpLogger.info('Creating secure TLS server for direct connections');
|
||||||
|
|
||||||
|
// Load certificates from strings
|
||||||
|
let certificates: ICertificateData;
|
||||||
|
try {
|
||||||
|
certificates = loadCertificatesFromString({
|
||||||
|
key: options.key,
|
||||||
|
cert: options.cert,
|
||||||
|
ca: options.ca
|
||||||
|
});
|
||||||
|
|
||||||
|
SmtpLogger.info('Successfully loaded TLS certificates for secure server');
|
||||||
|
} catch (certificateError) {
|
||||||
|
SmtpLogger.warn(`Failed to load certificates, using self-signed: ${certificateError instanceof Error ? certificateError.message : String(certificateError)}`);
|
||||||
|
certificates = generateSelfSignedCertificates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create server-side TLS options
|
||||||
|
const tlsOptions = createTlsOptions(certificates, true);
|
||||||
|
|
||||||
|
// Log details for debugging
|
||||||
|
SmtpLogger.debug('Creating secure server with options', {
|
||||||
|
certificates: {
|
||||||
|
keyLength: certificates.key.length,
|
||||||
|
certLength: certificates.cert.length,
|
||||||
|
caLength: certificates.ca ? certificates.ca.length : 0
|
||||||
|
},
|
||||||
|
tlsOptions: {
|
||||||
|
minVersion: tlsOptions.minVersion,
|
||||||
|
maxVersion: tlsOptions.maxVersion,
|
||||||
|
ciphers: tlsOptions.ciphers?.substring(0, 50) + '...' // Truncate long cipher list
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the TLS server
|
||||||
|
const server = new plugins.tls.Server(tlsOptions);
|
||||||
|
|
||||||
|
// Set up error handlers
|
||||||
|
server.on('error', (err) => {
|
||||||
|
SmtpLogger.error(`Secure server error: ${err.message}`, {
|
||||||
|
component: 'secure-server',
|
||||||
|
error: err,
|
||||||
|
stack: err.stack
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log secure connections
|
||||||
|
server.on('secureConnection', (socket) => {
|
||||||
|
const protocol = socket.getProtocol();
|
||||||
|
const cipher = socket.getCipher();
|
||||||
|
|
||||||
|
SmtpLogger.info('New direct TLS connection established', {
|
||||||
|
component: 'secure-server',
|
||||||
|
remoteAddress: socket.remoteAddress,
|
||||||
|
remotePort: socket.remotePort,
|
||||||
|
protocol: protocol || 'unknown',
|
||||||
|
cipher: cipher?.name || 'unknown'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
} catch (error) {
|
||||||
|
SmtpLogger.error(`Failed to create secure TLS server: ${error instanceof Error ? error.message : String(error)}`, {
|
||||||
|
component: 'secure-server',
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
||||||
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@@ -196,7 +196,19 @@ export class SmtpServer implements ISmtpServer {
|
|||||||
// Start secure server if configured
|
// Start secure server if configured
|
||||||
if (this.options.securePort && this.tlsHandler.isTlsEnabled()) {
|
if (this.options.securePort && this.tlsHandler.isTlsEnabled()) {
|
||||||
try {
|
try {
|
||||||
this.secureServer = this.tlsHandler.createSecureServer();
|
// Import the secure server creation utility from our new module
|
||||||
|
// This gives us better certificate handling and error resilience
|
||||||
|
const { createSecureTlsServer } = await import('./secure-server.js');
|
||||||
|
|
||||||
|
// Create secure server with the certificates
|
||||||
|
// This uses a more robust approach to certificate loading and validation
|
||||||
|
this.secureServer = createSecureTlsServer({
|
||||||
|
key: this.options.key,
|
||||||
|
cert: this.options.cert,
|
||||||
|
ca: this.options.ca
|
||||||
|
});
|
||||||
|
|
||||||
|
SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort}`);
|
||||||
|
|
||||||
if (this.secureServer) {
|
if (this.secureServer) {
|
||||||
// Use explicit error handling for secure connections
|
// Use explicit error handling for secure connections
|
||||||
|
234
ts/mail/delivery/smtpserver/starttls-handler.ts
Normal file
234
ts/mail/delivery/smtpserver/starttls-handler.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* STARTTLS Implementation
|
||||||
|
* Provides an improved implementation for STARTTLS upgrades
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as plugins from '../../../plugins.js';
|
||||||
|
import { SmtpLogger } from './utils/logging.js';
|
||||||
|
import {
|
||||||
|
loadCertificatesFromString,
|
||||||
|
createTlsOptions,
|
||||||
|
type ICertificateData
|
||||||
|
} from './certificate-utils.js';
|
||||||
|
import { getSocketDetails } from './utils/helpers.js';
|
||||||
|
import type { ISmtpSession } from './interfaces.js';
|
||||||
|
import { SmtpState } from '../interfaces.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced STARTTLS handler for more reliable TLS upgrades
|
||||||
|
*/
|
||||||
|
export async function performStartTLS(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
options: {
|
||||||
|
key: string;
|
||||||
|
cert: string;
|
||||||
|
ca?: string;
|
||||||
|
session?: ISmtpSession;
|
||||||
|
onSuccess?: (tlsSocket: plugins.tls.TLSSocket) => void;
|
||||||
|
onFailure?: (error: Error) => void;
|
||||||
|
updateSessionState?: (session: ISmtpSession, state: SmtpState) => void;
|
||||||
|
}
|
||||||
|
): Promise<plugins.tls.TLSSocket | undefined> {
|
||||||
|
return new Promise<plugins.tls.TLSSocket | undefined>((resolve) => {
|
||||||
|
try {
|
||||||
|
const socketDetails = getSocketDetails(socket);
|
||||||
|
|
||||||
|
SmtpLogger.info('Starting enhanced 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// Enable handshake timeout for STARTTLS
|
||||||
|
handshakeTimeout,
|
||||||
|
// 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', {
|
||||||
|
remoteAddress: socketDetails.remoteAddress,
|
||||||
|
remotePort: socketDetails.remotePort
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up socket listeners
|
||||||
|
cleanupSocket();
|
||||||
|
|
||||||
|
if (options.onFailure) {
|
||||||
|
options.onFailure(new Error('TLS handshake timed out'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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)}`, {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -8,6 +8,12 @@ import type { ITlsHandler, ISessionManager } from './interfaces.js';
|
|||||||
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
|
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
|
||||||
import { SmtpLogger } from './utils/logging.js';
|
import { SmtpLogger } from './utils/logging.js';
|
||||||
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
|
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
|
||||||
|
import {
|
||||||
|
loadCertificatesFromString,
|
||||||
|
generateSelfSignedCertificates,
|
||||||
|
createTlsOptions,
|
||||||
|
type ICertificateData
|
||||||
|
} from './certificate-utils.js';
|
||||||
import { SmtpState } from '../interfaces.js';
|
import { SmtpState } from '../interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,6 +35,11 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
rejectUnauthorized?: boolean;
|
rejectUnauthorized?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificate data
|
||||||
|
*/
|
||||||
|
private certificates: ICertificateData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new TLS handler
|
* Creates a new TLS handler
|
||||||
* @param sessionManager - Session manager instance
|
* @param sessionManager - Session manager instance
|
||||||
@@ -45,6 +56,23 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
) {
|
) {
|
||||||
this.sessionManager = sessionManager;
|
this.sessionManager = sessionManager;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
|
// Initialize certificates
|
||||||
|
try {
|
||||||
|
// Try to load certificates from provided options
|
||||||
|
this.certificates = loadCertificatesFromString({
|
||||||
|
key: options.key,
|
||||||
|
cert: options.cert,
|
||||||
|
ca: options.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,139 +127,78 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
* Upgrade a connection to TLS
|
* Upgrade a connection to TLS
|
||||||
* @param socket - Client socket
|
* @param socket - Client socket
|
||||||
*/
|
*/
|
||||||
public startTLS(socket: plugins.net.Socket): void {
|
public async startTLS(socket: plugins.net.Socket): Promise<void> {
|
||||||
// Get the session for this socket
|
// Get the session for this socket
|
||||||
const session = this.sessionManager.getSession(socket);
|
const session = this.sessionManager.getSession(socket);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use certificate strings directly without Buffer conversion
|
// Import the enhanced STARTTLS handler
|
||||||
// For ASN.1 encoding issues, keep the raw format which Node.js can parse natively
|
// This uses a more robust approach to TLS upgrades
|
||||||
const key = this.options.key.trim();
|
const { performStartTLS } = await import('./starttls-handler.js');
|
||||||
const cert = this.options.cert.trim();
|
|
||||||
const ca = this.options.ca ? this.options.ca.trim() : undefined;
|
|
||||||
|
|
||||||
// Log certificate lengths for debugging
|
SmtpLogger.info('Using enhanced STARTTLS implementation');
|
||||||
SmtpLogger.debug('Upgrading connection with certificates', {
|
|
||||||
keyLength: key.length,
|
// Use the enhanced STARTTLS handler with better error handling and socket management
|
||||||
certLength: cert.length,
|
const tlsSocket = await performStartTLS(socket, {
|
||||||
caLength: ca ? ca.length : 0
|
key: this.options.key,
|
||||||
|
cert: this.options.cert,
|
||||||
|
ca: this.options.ca,
|
||||||
|
session: session,
|
||||||
|
// 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'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create secure socket directly with minimal options for maximum compatibility
|
// Log security event
|
||||||
const secureSocket = new plugins.tls.TLSSocket(socket, {
|
SmtpLogger.logSecurityEvent(
|
||||||
isServer: true,
|
SecurityLogLevel.INFO,
|
||||||
key: key,
|
SecurityEventType.TLS_NEGOTIATION,
|
||||||
cert: cert,
|
'STARTTLS successful with enhanced implementation',
|
||||||
ca: ca,
|
{
|
||||||
// Allow older TLS versions for better compatibility with clients
|
protocol: secureSocket.getProtocol(),
|
||||||
minVersion: 'TLSv1',
|
cipher: secureSocket.getCipher()?.name
|
||||||
maxVersion: 'TLSv1.3',
|
},
|
||||||
// Use a permissive cipher list for testing compatibility
|
secureSocket.remoteAddress,
|
||||||
ciphers: 'ALL',
|
undefined,
|
||||||
// For testing, allow unauthorized (self-signed certs)
|
true
|
||||||
rejectUnauthorized: false,
|
);
|
||||||
// Don't request client certificates
|
},
|
||||||
requestCert: false,
|
// Callback for failed upgrade
|
||||||
// Allow legacy renegotiation for SMTP
|
onFailure: (error) => {
|
||||||
allowRenegotiation: true,
|
SmtpLogger.error(`Enhanced STARTTLS failed: ${error.message}`, {
|
||||||
// No server - prevents potential reference issues
|
sessionId: session?.id,
|
||||||
server: undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a specific check for secure event to make sure the handshake completes
|
|
||||||
let secureEventFired = false;
|
|
||||||
|
|
||||||
// Add specific timeout for 'secure' event
|
|
||||||
const secureEventTimeout = setTimeout(() => {
|
|
||||||
if (!secureEventFired) {
|
|
||||||
SmtpLogger.error('TLS handshake timed out waiting for secure event', {
|
|
||||||
remoteAddress: socket.remoteAddress,
|
remoteAddress: socket.remoteAddress,
|
||||||
remotePort: socket.remotePort
|
error
|
||||||
});
|
|
||||||
|
|
||||||
// Destroy the socket if secure event did not fire
|
|
||||||
socket.destroy();
|
|
||||||
}
|
|
||||||
}, 5000); // 5 second timeout
|
|
||||||
|
|
||||||
// Log the upgrade attempt
|
|
||||||
if (session) {
|
|
||||||
SmtpLogger.info(`Attempting to upgrade connection to TLS for session ${session.id}`, {
|
|
||||||
sessionId: session.id,
|
|
||||||
remoteAddress: session.remoteAddress
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Securely handle TLS errors
|
|
||||||
secureSocket.on('error', (err) => {
|
|
||||||
clearTimeout(secureEventTimeout);
|
|
||||||
|
|
||||||
SmtpLogger.error(`TLS error during STARTTLS: ${err.message}`, {
|
|
||||||
remoteAddress: socket.remoteAddress,
|
|
||||||
remotePort: socket.remotePort,
|
|
||||||
error: err,
|
|
||||||
stack: err.stack
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log security event
|
// Log security event
|
||||||
SmtpLogger.logSecurityEvent(
|
SmtpLogger.logSecurityEvent(
|
||||||
SecurityLogLevel.ERROR,
|
SecurityLogLevel.ERROR,
|
||||||
SecurityEventType.TLS_NEGOTIATION,
|
SecurityEventType.TLS_NEGOTIATION,
|
||||||
'TLS error during STARTTLS',
|
'Enhanced STARTTLS failed',
|
||||||
{ error: err.message, stack: err.stack },
|
{ error: error.message },
|
||||||
socket.remoteAddress
|
socket.remoteAddress,
|
||||||
);
|
|
||||||
|
|
||||||
socket.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log TLS connection details on secure event
|
|
||||||
secureSocket.on('secure', () => {
|
|
||||||
clearTimeout(secureEventTimeout);
|
|
||||||
secureEventFired = true;
|
|
||||||
|
|
||||||
const tlsDetails = getTlsDetails(secureSocket);
|
|
||||||
|
|
||||||
SmtpLogger.info('TLS connection successfully established via STARTTLS', {
|
|
||||||
remoteAddress: secureSocket.remoteAddress,
|
|
||||||
remotePort: secureSocket.remotePort,
|
|
||||||
protocol: tlsDetails?.protocol || 'unknown',
|
|
||||||
cipher: tlsDetails?.cipher || 'unknown',
|
|
||||||
authorized: tlsDetails?.authorized || false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log security event with TLS details
|
|
||||||
SmtpLogger.logSecurityEvent(
|
|
||||||
SecurityLogLevel.INFO,
|
|
||||||
SecurityEventType.TLS_NEGOTIATION,
|
|
||||||
'STARTTLS successful',
|
|
||||||
{
|
|
||||||
protocol: tlsDetails?.protocol,
|
|
||||||
cipher: tlsDetails?.cipher,
|
|
||||||
authorized: tlsDetails?.authorized
|
|
||||||
},
|
|
||||||
secureSocket.remoteAddress,
|
|
||||||
undefined,
|
undefined,
|
||||||
true
|
false
|
||||||
);
|
);
|
||||||
|
},
|
||||||
// Update session if we have one
|
// Function to update session state
|
||||||
if (session) {
|
updateSessionState: this.sessionManager.updateSessionState?.bind(this.sessionManager)
|
||||||
// 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}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Log STARTTLS failure
|
||||||
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
|
||||||
remoteAddress: socket.remoteAddress,
|
remoteAddress: socket.remoteAddress,
|
||||||
remotePort: socket.remotePort,
|
remotePort: socket.remotePort,
|
||||||
@@ -253,6 +220,7 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Destroy the socket on error
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,40 +235,47 @@ export class TlsHandler implements ITlsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use certificate strings directly without Buffer conversion
|
SmtpLogger.info('Creating secure TLS server');
|
||||||
// For ASN.1 encoding issues, keep the raw format which Node.js can parse natively
|
|
||||||
const key = this.options.key.trim();
|
|
||||||
const cert = this.options.cert.trim();
|
|
||||||
const ca = this.options.ca ? this.options.ca.trim() : undefined;
|
|
||||||
|
|
||||||
// Log certificate lengths for debugging
|
// Log certificate info
|
||||||
SmtpLogger.debug('Creating secure server with certificates', {
|
SmtpLogger.debug('Using certificates for secure server', {
|
||||||
keyLength: key.length,
|
keyLength: this.certificates.key.length,
|
||||||
certLength: cert.length,
|
certLength: this.certificates.cert.length,
|
||||||
caLength: ca ? ca.length : 0
|
caLength: this.certificates.ca ? this.certificates.ca.length : 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simplify options to minimal necessary for test compatibility
|
// Create TLS options using our certificate utilities
|
||||||
// Use consistent options with startTLS for maximum compatibility
|
// This ensures proper PEM format handling and protocol negotiation
|
||||||
const tlsOptions: plugins.tls.TlsOptions = {
|
const tlsOptions = createTlsOptions(this.certificates, true); // Use server options
|
||||||
key: key,
|
|
||||||
cert: cert,
|
|
||||||
ca: ca,
|
|
||||||
// Allow older TLS versions for better compatibility with clients
|
|
||||||
minVersion: 'TLSv1',
|
|
||||||
maxVersion: 'TLSv1.3',
|
|
||||||
// Use a permissive cipher list for testing compatibility
|
|
||||||
ciphers: 'ALL',
|
|
||||||
// For testing, allow unauthorized (self-signed certs)
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
// Shorter handshake timeout for testing
|
|
||||||
handshakeTimeout: 5000,
|
|
||||||
// Accept non-ALPN connections (legacy clients)
|
|
||||||
ALPNProtocols: ['smtp']
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a simple, standalone server with minimal options
|
SmtpLogger.info('Creating TLS server with options', {
|
||||||
return new plugins.tls.Server(tlsOptions);
|
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) {
|
} catch (error) {
|
||||||
SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
|
SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
|
||||||
error: error instanceof Error ? error : new Error(String(error)),
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
Reference in New Issue
Block a user