This commit is contained in:
2025-05-22 23:02:37 +00:00
parent f065a9c952
commit 50350bd78d
10 changed files with 633 additions and 779 deletions

View File

@ -6,12 +6,12 @@
import * as plugins from '../../../plugins.js';
import type { ISmtpSession, ISmtpAuth } from './interfaces.js';
import type { ISecurityHandler } from './interfaces.js';
import type { ISecurityHandler, ISmtpServer } from './interfaces.js';
import { SmtpLogger } from './utils/logging.js';
import { SecurityEventType, SecurityLogLevel } from './constants.js';
import { isValidEmail } from './utils/validation.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
import { IPReputationChecker } from '../../../security/classes.ipreputationchecker.js';
/**
* Interface for IP denylist entry
@ -27,23 +27,14 @@ interface IIpDenylistEntry {
*/
export class SecurityHandler implements ISecurityHandler {
/**
* Email server reference
* Reference to the SMTP server instance
*/
private emailServer: UnifiedEmailServer;
private smtpServer: ISmtpServer;
/**
* IP reputation service
* IP reputation checker service
*/
private ipReputationService?: any;
/**
* Authentication options
*/
private authOptions?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
validateUser?: (username: string, password: string) => Promise<boolean>;
};
private ipReputationService: IPReputationChecker;
/**
* Simple in-memory IP denylist
@ -51,26 +42,22 @@ export class SecurityHandler implements ISecurityHandler {
private ipDenylist: IIpDenylistEntry[] = [];
/**
* Creates a new security handler
* @param emailServer - Email server reference
* @param ipReputationService - Optional IP reputation service
* @param authOptions - Authentication options
* Cleanup interval timer
*/
constructor(
emailServer: UnifiedEmailServer,
ipReputationService?: any,
authOptions?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
validateUser?: (username: string, password: string) => Promise<boolean>;
}
) {
this.emailServer = emailServer;
this.ipReputationService = ipReputationService;
this.authOptions = authOptions;
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
setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute
this.cleanupInterval = setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute
}
/**
@ -95,18 +82,28 @@ export class SecurityHandler implements ISecurityHandler {
return false;
}
// If no reputation service, allow by default
// Check with IP reputation service
if (!this.ipReputationService) {
return true;
}
try {
// Check with IP reputation service
const reputationResult = await this.ipReputationService.checkIp(ip);
const reputationResult = await this.ipReputationService.checkReputation(ip);
if (!reputationResult.allowed) {
// 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
this.addToDenylist(ip, reputationResult.reason, 3600000); // 1 hour
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(
@ -114,9 +111,12 @@ export class SecurityHandler implements ISecurityHandler {
SecurityLogLevel.WARN,
`Connection blocked by reputation service: ${ip}`,
{
reason: reputationResult.reason,
reason,
score: reputationResult.score,
categories: reputationResult.categories
isSpam: reputationResult.isSpam,
isTor: reputationResult.isTor,
isProxy: reputationResult.isProxy,
isVPN: reputationResult.isVPN
}
);
@ -130,7 +130,8 @@ export class SecurityHandler implements ISecurityHandler {
`IP reputation check passed: ${ip}`,
{
score: reputationResult.score,
categories: reputationResult.categories
country: reputationResult.country,
org: reputationResult.org
}
);
@ -165,8 +166,12 @@ export class SecurityHandler implements ISecurityHandler {
* @returns Promise that resolves to true if authenticated
*/
public async authenticate(session: ISmtpSession, username: string, password: string, method: string): Promise<boolean> {
// Get auth options from server
const options = this.smtpServer.getOptions();
const authOptions = options.auth;
// Check if authentication is enabled
if (!this.authOptions) {
if (!authOptions) {
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN,
@ -178,7 +183,7 @@ export class SecurityHandler implements ISecurityHandler {
}
// Check if method is supported
if (!this.authOptions.methods.includes(method as any)) {
if (!authOptions.methods.includes(method as any)) {
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN,
@ -205,8 +210,8 @@ export class SecurityHandler implements ISecurityHandler {
let authenticated = false;
// Use custom validation function if provided
if (this.authOptions.validateUser) {
authenticated = await this.authOptions.validateUser(username, password);
if ((authOptions as any).validateUser) {
authenticated = await (authOptions as any).validateUser(username, password);
} else {
// Default behavior - no authentication
authenticated = false;
@ -339,4 +344,25 @@ export class SecurityHandler implements ISecurityHandler {
);
}
}
/**
* 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');
}
}