345 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			345 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * SMTP Security Handler
 | |
|  * Responsible for security aspects including IP reputation checking, 
 | |
|  * email validation, and authentication
 | |
|  */
 | |
| 
 | |
| import * as plugins from '../../../plugins.ts';
 | |
| import type { ISmtpSession, ISmtpAuth } from './interfaces.ts';
 | |
| import type { ISecurityHandler, ISmtpServer } from './interfaces.ts';
 | |
| import { SmtpLogger } from './utils/logging.ts';
 | |
| import { SecurityEventType, SecurityLogLevel } from './constants.ts';
 | |
| import { isValidEmail } from './utils/validation.ts';
 | |
| import { getSocketDetails, getTlsDetails } from './utils/helpers.ts';
 | |
| import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.ts';
 | |
| 
 | |
| /**
 | |
|  * Interface for IP denylist entry
 | |
|  */
 | |
| interface IIpDenylistEntry {
 | |
|   ip: string;
 | |
|   reason: string;
 | |
|   expiresAt?: number;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handles security aspects for SMTP server
 | |
|  */
 | |
| export class SecurityHandler implements ISecurityHandler {
 | |
|   /**
 | |
|    * Reference to the SMTP server instance
 | |
|    */
 | |
|   private smtpServer: ISmtpServer;
 | |
|   
 | |
|   /**
 | |
|    * IP reputation checker service
 | |
|    */
 | |
|   private ipReputationService: IPReputationChecker;
 | |
|   
 | |
|   /**
 | |
|    * Simple in-memory IP denylist
 | |
|    */
 | |
|   private ipDenylist: IIpDenylistEntry[] = [];
 | |
|   
 | |
|   /**
 | |
|    * Cleanup interval timer
 | |
|    */
 | |
|   private cleanupInterval: NodeJS.Timeout | null = null;
 | |
|   
 | |
|   /**
 | |
|    * Creates a new security handler
 | |
|    * @param smtpServer - SMTP server instance
 | |
|    */
 | |
|   constructor(smtpServer: ISmtpServer) {
 | |
|     this.smtpServer = smtpServer;
 | |
|     
 | |
|     // Initialize IP reputation checker
 | |
|     this.ipReputationService = new IPReputationChecker();
 | |
|     
 | |
|     // Clean expired denylist entries periodically
 | |
|     this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check IP reputation for a connection
 | |
|    * @param socket - Client socket
 | |
|    * @returns Promise that resolves to true if IP is allowed, false if blocked
 | |
|    */
 | |
|   public async checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise<boolean> {
 | |
|     const socketDetails = getSocketDetails(socket);
 | |
|     const ip = socketDetails.remoteAddress;
 | |
|     
 | |
|     // Check local denylist first
 | |
|     if (this.isIpDenylisted(ip)) {
 | |
|       // Log the blocked connection
 | |
|       this.logSecurityEvent(
 | |
|         SecurityEventType.IP_REPUTATION,
 | |
|         SecurityLogLevel.WARN,
 | |
|         `Connection blocked from denylisted IP: ${ip}`,
 | |
|         { reason: this.getDenylistReason(ip) }
 | |
|       );
 | |
|       
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     // Check with IP reputation service
 | |
|     if (!this.ipReputationService) {
 | |
|       return true;
 | |
|     }
 | |
|     
 | |
|     try {
 | |
|       // Check with IP reputation service
 | |
|       const reputationResult = await this.ipReputationService.checkReputation(ip);
 | |
|       
 | |
|       // Block if score is below HIGH_RISK threshold (20) or if it's spam/proxy/tor/vpn
 | |
|       const isBlocked = reputationResult.score < 20 || 
 | |
|                        reputationResult.isSpam || 
 | |
|                        reputationResult.isTor || 
 | |
|                        reputationResult.isProxy;
 | |
|       
 | |
|       if (isBlocked) {
 | |
|         // Add to local denylist temporarily
 | |
|         const reason = reputationResult.isSpam ? 'spam' : 
 | |
|                       reputationResult.isTor ? 'tor' : 
 | |
|                       reputationResult.isProxy ? 'proxy' : 
 | |
|                       `low reputation score: ${reputationResult.score}`;
 | |
|         this.addToDenylist(ip, reason, 3600000); // 1 hour
 | |
|         
 | |
|         // Log the blocked connection
 | |
|         this.logSecurityEvent(
 | |
|           SecurityEventType.IP_REPUTATION,
 | |
|           SecurityLogLevel.WARN,
 | |
|           `Connection blocked by reputation service: ${ip}`,
 | |
|           { 
 | |
|             reason,
 | |
|             score: reputationResult.score,
 | |
|             isSpam: reputationResult.isSpam,
 | |
|             isTor: reputationResult.isTor,
 | |
|             isProxy: reputationResult.isProxy,
 | |
|             isVPN: reputationResult.isVPN
 | |
|           }
 | |
|         );
 | |
|         
 | |
|         return false;
 | |
|       }
 | |
|       
 | |
|       // Log the allowed connection
 | |
|       this.logSecurityEvent(
 | |
|         SecurityEventType.IP_REPUTATION,
 | |
|         SecurityLogLevel.INFO,
 | |
|         `IP reputation check passed: ${ip}`,
 | |
|         { 
 | |
|           score: reputationResult.score,
 | |
|           country: reputationResult.country,
 | |
|           org: reputationResult.org
 | |
|         }
 | |
|       );
 | |
|       
 | |
|       return true;
 | |
|     } catch (error) {
 | |
|       // Log the error
 | |
|       SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
 | |
|         ip,
 | |
|         error: error instanceof Error ? error : new Error(String(error))
 | |
|       });
 | |
|       
 | |
|       // Allow the connection on error (fail open)
 | |
|       return true;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Validate an email address
 | |
|    * @param email - Email address to validate
 | |
|    * @returns Whether the email address is valid
 | |
|    */
 | |
|   public isValidEmail(email: string): boolean {
 | |
|     return isValidEmail(email);
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Validate authentication credentials
 | |
|    * @param auth - Authentication credentials
 | |
|    * @returns Promise that resolves to true if authenticated
 | |
|    */
 | |
|   public async authenticate(auth: ISmtpAuth): Promise<boolean> {
 | |
|     const { username, password } = auth;
 | |
|     // Get auth options from server
 | |
|     const options = this.smtpServer.getOptions();
 | |
|     const authOptions = options.auth;
 | |
|     
 | |
|     // Check if authentication is enabled
 | |
|     if (!authOptions) {
 | |
|       this.logSecurityEvent(
 | |
|         SecurityEventType.AUTHENTICATION,
 | |
|         SecurityLogLevel.WARN,
 | |
|         'Authentication attempt when auth is disabled',
 | |
|         { username }
 | |
|       );
 | |
|       
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     // Note: Method validation and TLS requirement checks would need to be done
 | |
|     // at the caller level since the interface doesn't include session/method info
 | |
|     
 | |
|     try {
 | |
|       let authenticated = false;
 | |
|       
 | |
|       // Use custom validation function if provided
 | |
|       if ((authOptions as any).validateUser) {
 | |
|         authenticated = await (authOptions as any).validateUser(username, password);
 | |
|       } else {
 | |
|         // Default behavior - no authentication
 | |
|         authenticated = false;
 | |
|       }
 | |
|       
 | |
|       // Log the authentication result
 | |
|       this.logSecurityEvent(
 | |
|         SecurityEventType.AUTHENTICATION,
 | |
|         authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
 | |
|         authenticated ? 'Authentication successful' : 'Authentication failed',
 | |
|         { username }
 | |
|       );
 | |
|       
 | |
|       return authenticated;
 | |
|     } catch (error) {
 | |
|       // Log authentication error
 | |
|       this.logSecurityEvent(
 | |
|         SecurityEventType.AUTHENTICATION,
 | |
|         SecurityLogLevel.ERROR,
 | |
|         `Authentication error: ${error instanceof Error ? error.message : String(error)}`,
 | |
|         { username, error: error instanceof Error ? error.message : String(error) }
 | |
|       );
 | |
|       
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Log a security event
 | |
|    * @param event - Event type
 | |
|    * @param level - Log level
 | |
|    * @param details - Event details
 | |
|    */
 | |
|   public logSecurityEvent(event: string, level: string, message: string, details: Record<string, any>): void {
 | |
|     SmtpLogger.logSecurityEvent(
 | |
|       level as SecurityLogLevel,
 | |
|       event as SecurityEventType,
 | |
|       message,
 | |
|       details,
 | |
|       details.ip,
 | |
|       details.domain,
 | |
|       details.success
 | |
|     );
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Add an IP to the denylist
 | |
|    * @param ip - IP address
 | |
|    * @param reason - Reason for denylisting
 | |
|    * @param duration - Duration in milliseconds (optional, indefinite if not specified)
 | |
|    */
 | |
|   private addToDenylist(ip: string, reason: string, duration?: number): void {
 | |
|     // Remove existing entry if present
 | |
|     this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip);
 | |
|     
 | |
|     // Create new entry
 | |
|     const entry: IIpDenylistEntry = {
 | |
|       ip,
 | |
|       reason,
 | |
|       expiresAt: duration ? Date.now() + duration : undefined
 | |
|     };
 | |
|     
 | |
|     // Add to denylist
 | |
|     this.ipDenylist.push(entry);
 | |
|     
 | |
|     // Log the action
 | |
|     this.logSecurityEvent(
 | |
|       SecurityEventType.ACCESS_CONTROL,
 | |
|       SecurityLogLevel.INFO,
 | |
|       `Added IP to denylist: ${ip}`,
 | |
|       { 
 | |
|         ip,
 | |
|         reason,
 | |
|         duration: duration ? `${duration / 1000} seconds` : 'indefinite'
 | |
|       }
 | |
|     );
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if an IP is denylisted
 | |
|    * @param ip - IP address
 | |
|    * @returns Whether the IP is denylisted
 | |
|    */
 | |
|   private isIpDenylisted(ip: string): boolean {
 | |
|     const entry = this.ipDenylist.find(e => e.ip === ip);
 | |
|     
 | |
|     if (!entry) {
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     // Check if entry has expired
 | |
|     if (entry.expiresAt && entry.expiresAt < Date.now()) {
 | |
|       // Remove expired entry
 | |
|       this.ipDenylist = this.ipDenylist.filter(e => e !== entry);
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     return true;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get the reason an IP was denylisted
 | |
|    * @param ip - IP address
 | |
|    * @returns Reason for denylisting or undefined if not denylisted
 | |
|    */
 | |
|   private getDenylistReason(ip: string): string | undefined {
 | |
|     const entry = this.ipDenylist.find(e => e.ip === ip);
 | |
|     return entry?.reason;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Clean expired denylist entries
 | |
|    */
 | |
|   private cleanExpiredDenylistEntries(): void {
 | |
|     const now = Date.now();
 | |
|     const initialCount = this.ipDenylist.length;
 | |
|     
 | |
|     this.ipDenylist = this.ipDenylist.filter(entry => {
 | |
|       return !entry.expiresAt || entry.expiresAt > now;
 | |
|     });
 | |
|     
 | |
|     const removedCount = initialCount - this.ipDenylist.length;
 | |
|     
 | |
|     if (removedCount > 0) {
 | |
|       this.logSecurityEvent(
 | |
|         SecurityEventType.ACCESS_CONTROL,
 | |
|         SecurityLogLevel.INFO,
 | |
|         `Cleaned up ${removedCount} expired denylist entries`,
 | |
|         { remainingCount: this.ipDenylist.length }
 | |
|       );
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Clean up resources
 | |
|    */
 | |
|   public destroy(): void {
 | |
|     // Clear the cleanup interval
 | |
|     if (this.cleanupInterval) {
 | |
|       clearInterval(this.cleanupInterval);
 | |
|       this.cleanupInterval = null;
 | |
|     }
 | |
|     
 | |
|     // Clear the denylist
 | |
|     this.ipDenylist = [];
 | |
|     
 | |
|     // Clean up IP reputation service if it has a destroy method
 | |
|     if (this.ipReputationService && typeof (this.ipReputationService as any).destroy === 'function') {
 | |
|       (this.ipReputationService as any).destroy();
 | |
|     }
 | |
|     
 | |
|     SmtpLogger.debug('SecurityHandler destroyed');
 | |
|   }
 | |
| } |