@push.rocks/smartmta 5.1.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +14 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/logger.d.ts +17 -0
- package/dist_ts/logger.js +76 -0
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
- package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
- package/dist_ts/mail/core/classes.email.d.ts +291 -0
- package/dist_ts/mail/core/classes.email.js +802 -0
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
- package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
- package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
- package/dist_ts/mail/core/classes.templatemanager.js +240 -0
- package/dist_ts/mail/core/index.d.ts +4 -0
- package/dist_ts/mail/core/index.js +6 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
- package/dist_ts/mail/delivery/index.d.ts +4 -0
- package/dist_ts/mail/delivery/index.js +6 -0
- package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
- package/dist_ts/mail/delivery/interfaces.js +17 -0
- package/dist_ts/mail/index.d.ts +7 -0
- package/dist_ts/mail/index.js +12 -0
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
- package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
- package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
- package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
- package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
- package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
- package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
- package/dist_ts/mail/routing/classes.email.router.js +494 -0
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
- package/dist_ts/mail/routing/index.d.ts +7 -0
- package/dist_ts/mail/routing/index.js +9 -0
- package/dist_ts/mail/routing/interfaces.d.ts +187 -0
- package/dist_ts/mail/routing/interfaces.js +2 -0
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
- package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
- package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
- package/dist_ts/mail/security/classes.spfverifier.js +87 -0
- package/dist_ts/mail/security/index.d.ts +2 -0
- package/dist_ts/mail/security/index.js +4 -0
- package/dist_ts/paths.d.ts +14 -0
- package/dist_ts/paths.js +39 -0
- package/dist_ts/plugins.d.ts +24 -0
- package/dist_ts/plugins.js +28 -0
- package/dist_ts/security/classes.contentscanner.d.ts +130 -0
- package/dist_ts/security/classes.contentscanner.js +338 -0
- package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
- package/dist_ts/security/classes.ipreputationchecker.js +263 -0
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +398 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +484 -0
- package/dist_ts/security/classes.securitylogger.d.ts +140 -0
- package/dist_ts/security/classes.securitylogger.js +235 -0
- package/dist_ts/security/index.d.ts +4 -0
- package/dist_ts/security/index.js +5 -0
- package/package.json +6 -1
- package/readme.md +52 -9
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/logger.ts +91 -0
- package/ts/mail/core/classes.bouncemanager.ts +731 -0
- package/ts/mail/core/classes.email.ts +942 -0
- package/ts/mail/core/classes.emailvalidator.ts +239 -0
- package/ts/mail/core/classes.templatemanager.ts +320 -0
- package/ts/mail/core/index.ts +5 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
- package/ts/mail/delivery/classes.delivery.system.ts +816 -0
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
- package/ts/mail/delivery/index.ts +5 -0
- package/ts/mail/delivery/interfaces.ts +167 -0
- package/ts/mail/index.ts +17 -0
- package/ts/mail/routing/classes.dkim.manager.ts +157 -0
- package/ts/mail/routing/classes.dns.manager.ts +573 -0
- package/ts/mail/routing/classes.domain.registry.ts +139 -0
- package/ts/mail/routing/classes.email.action.executor.ts +175 -0
- package/ts/mail/routing/classes.email.router.ts +575 -0
- package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
- package/ts/mail/routing/index.ts +9 -0
- package/ts/mail/routing/interfaces.ts +202 -0
- package/ts/mail/security/classes.dkimcreator.ts +447 -0
- package/ts/mail/security/classes.spfverifier.ts +126 -0
- package/ts/mail/security/index.ts +3 -0
- package/ts/paths.ts +48 -0
- package/ts/plugins.ts +53 -0
- package/ts/security/classes.contentscanner.ts +400 -0
- package/ts/security/classes.ipreputationchecker.ts +315 -0
- package/ts/security/classes.rustsecuritybridge.ts +943 -0
- package/ts/security/classes.securitylogger.ts +299 -0
- package/ts/security/index.ts +40 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { logger } from '../../logger.js';
|
|
4
|
+
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for rate limit configuration
|
|
8
|
+
*/
|
|
9
|
+
export interface IRateLimitConfig {
|
|
10
|
+
maxMessagesPerMinute?: number;
|
|
11
|
+
maxRecipientsPerMessage?: number;
|
|
12
|
+
maxConnectionsPerIP?: number;
|
|
13
|
+
maxErrorsPerIP?: number;
|
|
14
|
+
maxAuthFailuresPerIP?: number;
|
|
15
|
+
blockDuration?: number; // in milliseconds
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface for hierarchical rate limits
|
|
20
|
+
*/
|
|
21
|
+
export interface IHierarchicalRateLimits {
|
|
22
|
+
// Global rate limits (applied to all traffic)
|
|
23
|
+
global: IRateLimitConfig;
|
|
24
|
+
|
|
25
|
+
// Pattern-specific rate limits (applied to matching patterns)
|
|
26
|
+
patterns?: Record<string, IRateLimitConfig>;
|
|
27
|
+
|
|
28
|
+
// IP-specific rate limits (applied to specific IPs)
|
|
29
|
+
ips?: Record<string, IRateLimitConfig>;
|
|
30
|
+
|
|
31
|
+
// Domain-specific rate limits (applied to specific email domains)
|
|
32
|
+
domains?: Record<string, IRateLimitConfig>;
|
|
33
|
+
|
|
34
|
+
// Temporary blocks list and their expiry times
|
|
35
|
+
blocks?: Record<string, number>; // IP to expiry timestamp
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Counter interface for rate limiting
|
|
40
|
+
*/
|
|
41
|
+
interface ILimitCounter {
|
|
42
|
+
count: number;
|
|
43
|
+
lastReset: number;
|
|
44
|
+
recipients: number;
|
|
45
|
+
errors: number;
|
|
46
|
+
authFailures: number;
|
|
47
|
+
connections: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rate limiter statistics
|
|
52
|
+
*/
|
|
53
|
+
export interface IRateLimiterStats {
|
|
54
|
+
activeCounters: number;
|
|
55
|
+
totalBlocked: number;
|
|
56
|
+
currentlyBlocked: number;
|
|
57
|
+
byPattern: Record<string, {
|
|
58
|
+
messagesPerMinute: number;
|
|
59
|
+
totalMessages: number;
|
|
60
|
+
totalBlocked: number;
|
|
61
|
+
}>;
|
|
62
|
+
byIp: Record<string, {
|
|
63
|
+
messagesPerMinute: number;
|
|
64
|
+
totalMessages: number;
|
|
65
|
+
totalBlocked: number;
|
|
66
|
+
connections: number;
|
|
67
|
+
errors: number;
|
|
68
|
+
authFailures: number;
|
|
69
|
+
blocked: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Result of a rate limit check
|
|
75
|
+
*/
|
|
76
|
+
export interface IRateLimitResult {
|
|
77
|
+
allowed: boolean;
|
|
78
|
+
reason?: string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
current?: number;
|
|
81
|
+
resetIn?: number; // milliseconds until reset
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Unified rate limiter for all email processing modes
|
|
86
|
+
*/
|
|
87
|
+
export class UnifiedRateLimiter extends EventEmitter {
|
|
88
|
+
private config: IHierarchicalRateLimits;
|
|
89
|
+
private counters: Map<string, ILimitCounter> = new Map();
|
|
90
|
+
private patternCounters: Map<string, ILimitCounter> = new Map();
|
|
91
|
+
private ipCounters: Map<string, ILimitCounter> = new Map();
|
|
92
|
+
private domainCounters: Map<string, ILimitCounter> = new Map();
|
|
93
|
+
private cleanupInterval?: NodeJS.Timeout;
|
|
94
|
+
private stats: IRateLimiterStats;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new unified rate limiter
|
|
98
|
+
* @param config Rate limit configuration
|
|
99
|
+
*/
|
|
100
|
+
constructor(config: IHierarchicalRateLimits) {
|
|
101
|
+
super();
|
|
102
|
+
|
|
103
|
+
// Set default configuration
|
|
104
|
+
this.config = {
|
|
105
|
+
global: {
|
|
106
|
+
maxMessagesPerMinute: config.global.maxMessagesPerMinute || 100,
|
|
107
|
+
maxRecipientsPerMessage: config.global.maxRecipientsPerMessage || 100,
|
|
108
|
+
maxConnectionsPerIP: config.global.maxConnectionsPerIP || 20,
|
|
109
|
+
maxErrorsPerIP: config.global.maxErrorsPerIP || 10,
|
|
110
|
+
maxAuthFailuresPerIP: config.global.maxAuthFailuresPerIP || 5,
|
|
111
|
+
blockDuration: config.global.blockDuration || 3600000 // 1 hour
|
|
112
|
+
},
|
|
113
|
+
patterns: config.patterns || {},
|
|
114
|
+
ips: config.ips || {},
|
|
115
|
+
blocks: config.blocks || {}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Initialize statistics
|
|
119
|
+
this.stats = {
|
|
120
|
+
activeCounters: 0,
|
|
121
|
+
totalBlocked: 0,
|
|
122
|
+
currentlyBlocked: 0,
|
|
123
|
+
byPattern: {},
|
|
124
|
+
byIp: {}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Start cleanup interval
|
|
128
|
+
this.startCleanupInterval();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Start the cleanup interval
|
|
133
|
+
*/
|
|
134
|
+
private startCleanupInterval(): void {
|
|
135
|
+
if (this.cleanupInterval) {
|
|
136
|
+
clearInterval(this.cleanupInterval);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Run cleanup every minute
|
|
140
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Stop the cleanup interval
|
|
145
|
+
*/
|
|
146
|
+
public stop(): void {
|
|
147
|
+
if (this.cleanupInterval) {
|
|
148
|
+
clearInterval(this.cleanupInterval);
|
|
149
|
+
this.cleanupInterval = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Destroy the rate limiter and clean up all resources
|
|
155
|
+
*/
|
|
156
|
+
public destroy(): void {
|
|
157
|
+
// Stop the cleanup interval
|
|
158
|
+
this.stop();
|
|
159
|
+
|
|
160
|
+
// Clear all maps to free memory
|
|
161
|
+
this.counters.clear();
|
|
162
|
+
this.ipCounters.clear();
|
|
163
|
+
this.patternCounters.clear();
|
|
164
|
+
|
|
165
|
+
// Clear blocks
|
|
166
|
+
if (this.config.blocks) {
|
|
167
|
+
this.config.blocks = {};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Clear statistics
|
|
171
|
+
this.stats = {
|
|
172
|
+
activeCounters: 0,
|
|
173
|
+
totalBlocked: 0,
|
|
174
|
+
currentlyBlocked: 0,
|
|
175
|
+
byPattern: {},
|
|
176
|
+
byIp: {}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
logger.log('info', 'UnifiedRateLimiter destroyed');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clean up expired counters and blocks
|
|
184
|
+
*/
|
|
185
|
+
private cleanup(): void {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
|
|
188
|
+
// Clean up expired blocks
|
|
189
|
+
if (this.config.blocks) {
|
|
190
|
+
for (const [ip, expiry] of Object.entries(this.config.blocks)) {
|
|
191
|
+
if (expiry <= now) {
|
|
192
|
+
delete this.config.blocks[ip];
|
|
193
|
+
logger.log('info', `Rate limit block expired for IP ${ip}`);
|
|
194
|
+
|
|
195
|
+
// Update statistics
|
|
196
|
+
if (this.stats.byIp[ip]) {
|
|
197
|
+
this.stats.byIp[ip].blocked = false;
|
|
198
|
+
}
|
|
199
|
+
this.stats.currentlyBlocked--;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Clean up old counters (older than 10 minutes)
|
|
205
|
+
const cutoff = now - 600000;
|
|
206
|
+
|
|
207
|
+
// Clean global counters
|
|
208
|
+
for (const [key, counter] of this.counters.entries()) {
|
|
209
|
+
if (counter.lastReset < cutoff) {
|
|
210
|
+
this.counters.delete(key);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clean pattern counters
|
|
215
|
+
for (const [key, counter] of this.patternCounters.entries()) {
|
|
216
|
+
if (counter.lastReset < cutoff) {
|
|
217
|
+
this.patternCounters.delete(key);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Clean IP counters
|
|
222
|
+
for (const [key, counter] of this.ipCounters.entries()) {
|
|
223
|
+
if (counter.lastReset < cutoff) {
|
|
224
|
+
this.ipCounters.delete(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Clean domain counters
|
|
229
|
+
for (const [key, counter] of this.domainCounters.entries()) {
|
|
230
|
+
if (counter.lastReset < cutoff) {
|
|
231
|
+
this.domainCounters.delete(key);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Update statistics
|
|
236
|
+
this.updateStats();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if a message is allowed by rate limits
|
|
241
|
+
* @param email Email address
|
|
242
|
+
* @param ip IP address
|
|
243
|
+
* @param recipients Number of recipients
|
|
244
|
+
* @param pattern Matched pattern
|
|
245
|
+
* @param domain Domain name for domain-specific limits
|
|
246
|
+
* @returns Result of rate limit check
|
|
247
|
+
*/
|
|
248
|
+
public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: string, domain?: string): IRateLimitResult {
|
|
249
|
+
// Check if IP is blocked
|
|
250
|
+
if (this.isIpBlocked(ip)) {
|
|
251
|
+
return {
|
|
252
|
+
allowed: false,
|
|
253
|
+
reason: 'IP is blocked',
|
|
254
|
+
resetIn: this.getBlockReleaseTime(ip)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check global message rate limit
|
|
259
|
+
const globalResult = this.checkGlobalMessageLimit(email);
|
|
260
|
+
if (!globalResult.allowed) {
|
|
261
|
+
return globalResult;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check pattern-specific limit if pattern is provided
|
|
265
|
+
if (pattern) {
|
|
266
|
+
const patternResult = this.checkPatternMessageLimit(pattern);
|
|
267
|
+
if (!patternResult.allowed) {
|
|
268
|
+
return patternResult;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check domain-specific limit if domain is provided
|
|
273
|
+
if (domain) {
|
|
274
|
+
const domainResult = this.checkDomainMessageLimit(domain);
|
|
275
|
+
if (!domainResult.allowed) {
|
|
276
|
+
return domainResult;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check IP-specific limit
|
|
281
|
+
const ipResult = this.checkIpMessageLimit(ip);
|
|
282
|
+
if (!ipResult.allowed) {
|
|
283
|
+
return ipResult;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check recipient limit
|
|
287
|
+
const recipientResult = this.checkRecipientLimit(email, recipients, pattern, domain);
|
|
288
|
+
if (!recipientResult.allowed) {
|
|
289
|
+
return recipientResult;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// All checks passed
|
|
293
|
+
return { allowed: true };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check global message rate limit
|
|
298
|
+
* @param email Email address
|
|
299
|
+
*/
|
|
300
|
+
private checkGlobalMessageLimit(email: string): IRateLimitResult {
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
const limit = this.config.global.maxMessagesPerMinute!;
|
|
303
|
+
|
|
304
|
+
if (!limit) {
|
|
305
|
+
return { allowed: true };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Get or create counter
|
|
309
|
+
const key = 'global';
|
|
310
|
+
let counter = this.counters.get(key);
|
|
311
|
+
|
|
312
|
+
if (!counter) {
|
|
313
|
+
counter = {
|
|
314
|
+
count: 0,
|
|
315
|
+
lastReset: now,
|
|
316
|
+
recipients: 0,
|
|
317
|
+
errors: 0,
|
|
318
|
+
authFailures: 0,
|
|
319
|
+
connections: 0
|
|
320
|
+
};
|
|
321
|
+
this.counters.set(key, counter);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check if counter needs to be reset
|
|
325
|
+
if (now - counter.lastReset >= 60000) {
|
|
326
|
+
counter.count = 0;
|
|
327
|
+
counter.lastReset = now;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if limit is exceeded
|
|
331
|
+
if (counter.count >= limit) {
|
|
332
|
+
// Calculate reset time
|
|
333
|
+
const resetIn = 60000 - (now - counter.lastReset);
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
allowed: false,
|
|
337
|
+
reason: 'Global message rate limit exceeded',
|
|
338
|
+
limit,
|
|
339
|
+
current: counter.count,
|
|
340
|
+
resetIn
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Increment counter
|
|
345
|
+
counter.count++;
|
|
346
|
+
|
|
347
|
+
// Update statistics
|
|
348
|
+
this.updateStats();
|
|
349
|
+
|
|
350
|
+
return { allowed: true };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check pattern-specific message rate limit
|
|
355
|
+
* @param pattern Pattern to check
|
|
356
|
+
*/
|
|
357
|
+
private checkPatternMessageLimit(pattern: string): IRateLimitResult {
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
|
|
360
|
+
// Get pattern-specific limit or use global
|
|
361
|
+
const patternConfig = this.config.patterns?.[pattern];
|
|
362
|
+
const limit = patternConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!;
|
|
363
|
+
|
|
364
|
+
if (!limit) {
|
|
365
|
+
return { allowed: true };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Get or create counter
|
|
369
|
+
let counter = this.patternCounters.get(pattern);
|
|
370
|
+
|
|
371
|
+
if (!counter) {
|
|
372
|
+
counter = {
|
|
373
|
+
count: 0,
|
|
374
|
+
lastReset: now,
|
|
375
|
+
recipients: 0,
|
|
376
|
+
errors: 0,
|
|
377
|
+
authFailures: 0,
|
|
378
|
+
connections: 0
|
|
379
|
+
};
|
|
380
|
+
this.patternCounters.set(pattern, counter);
|
|
381
|
+
|
|
382
|
+
// Initialize pattern stats if needed
|
|
383
|
+
if (!this.stats.byPattern[pattern]) {
|
|
384
|
+
this.stats.byPattern[pattern] = {
|
|
385
|
+
messagesPerMinute: 0,
|
|
386
|
+
totalMessages: 0,
|
|
387
|
+
totalBlocked: 0
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check if counter needs to be reset
|
|
393
|
+
if (now - counter.lastReset >= 60000) {
|
|
394
|
+
counter.count = 0;
|
|
395
|
+
counter.lastReset = now;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Check if limit is exceeded
|
|
399
|
+
if (counter.count >= limit) {
|
|
400
|
+
// Calculate reset time
|
|
401
|
+
const resetIn = 60000 - (now - counter.lastReset);
|
|
402
|
+
|
|
403
|
+
// Update statistics
|
|
404
|
+
this.stats.byPattern[pattern].totalBlocked++;
|
|
405
|
+
this.stats.totalBlocked++;
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
allowed: false,
|
|
409
|
+
reason: `Pattern "${pattern}" message rate limit exceeded`,
|
|
410
|
+
limit,
|
|
411
|
+
current: counter.count,
|
|
412
|
+
resetIn
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Increment counter
|
|
417
|
+
counter.count++;
|
|
418
|
+
|
|
419
|
+
// Update statistics
|
|
420
|
+
this.stats.byPattern[pattern].messagesPerMinute = counter.count;
|
|
421
|
+
this.stats.byPattern[pattern].totalMessages++;
|
|
422
|
+
|
|
423
|
+
return { allowed: true };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Check domain-specific message rate limit
|
|
428
|
+
* @param domain Domain to check
|
|
429
|
+
*/
|
|
430
|
+
private checkDomainMessageLimit(domain: string): IRateLimitResult {
|
|
431
|
+
const now = Date.now();
|
|
432
|
+
|
|
433
|
+
// Get domain-specific limit or use global
|
|
434
|
+
const domainConfig = this.config.domains?.[domain];
|
|
435
|
+
const limit = domainConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!;
|
|
436
|
+
|
|
437
|
+
if (!limit) {
|
|
438
|
+
return { allowed: true };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Get or create counter
|
|
442
|
+
let counter = this.domainCounters.get(domain);
|
|
443
|
+
|
|
444
|
+
if (!counter) {
|
|
445
|
+
counter = {
|
|
446
|
+
count: 0,
|
|
447
|
+
lastReset: now,
|
|
448
|
+
recipients: 0,
|
|
449
|
+
errors: 0,
|
|
450
|
+
authFailures: 0,
|
|
451
|
+
connections: 0
|
|
452
|
+
};
|
|
453
|
+
this.domainCounters.set(domain, counter);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check if counter needs to be reset
|
|
457
|
+
if (now - counter.lastReset >= 60000) {
|
|
458
|
+
counter.count = 0;
|
|
459
|
+
counter.lastReset = now;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Check if limit is exceeded
|
|
463
|
+
if (counter.count >= limit) {
|
|
464
|
+
// Calculate reset time
|
|
465
|
+
const resetIn = 60000 - (now - counter.lastReset);
|
|
466
|
+
|
|
467
|
+
logger.log('warn', `Domain ${domain} rate limit exceeded: ${counter.count}/${limit} messages per minute`);
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
allowed: false,
|
|
471
|
+
reason: `Domain "${domain}" message rate limit exceeded`,
|
|
472
|
+
limit,
|
|
473
|
+
current: counter.count,
|
|
474
|
+
resetIn
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Increment counter
|
|
479
|
+
counter.count++;
|
|
480
|
+
|
|
481
|
+
return { allowed: true };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Check IP-specific message rate limit
|
|
486
|
+
* @param ip IP address
|
|
487
|
+
*/
|
|
488
|
+
private checkIpMessageLimit(ip: string): IRateLimitResult {
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
|
|
491
|
+
// Get IP-specific limit or use global
|
|
492
|
+
const ipConfig = this.config.ips?.[ip];
|
|
493
|
+
const limit = ipConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!;
|
|
494
|
+
|
|
495
|
+
if (!limit) {
|
|
496
|
+
return { allowed: true };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Get or create counter
|
|
500
|
+
let counter = this.ipCounters.get(ip);
|
|
501
|
+
|
|
502
|
+
if (!counter) {
|
|
503
|
+
counter = {
|
|
504
|
+
count: 0,
|
|
505
|
+
lastReset: now,
|
|
506
|
+
recipients: 0,
|
|
507
|
+
errors: 0,
|
|
508
|
+
authFailures: 0,
|
|
509
|
+
connections: 0
|
|
510
|
+
};
|
|
511
|
+
this.ipCounters.set(ip, counter);
|
|
512
|
+
|
|
513
|
+
// Initialize IP stats if needed
|
|
514
|
+
if (!this.stats.byIp[ip]) {
|
|
515
|
+
this.stats.byIp[ip] = {
|
|
516
|
+
messagesPerMinute: 0,
|
|
517
|
+
totalMessages: 0,
|
|
518
|
+
totalBlocked: 0,
|
|
519
|
+
connections: 0,
|
|
520
|
+
errors: 0,
|
|
521
|
+
authFailures: 0,
|
|
522
|
+
blocked: false
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check if counter needs to be reset
|
|
528
|
+
if (now - counter.lastReset >= 60000) {
|
|
529
|
+
counter.count = 0;
|
|
530
|
+
counter.lastReset = now;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Check if limit is exceeded
|
|
534
|
+
if (counter.count >= limit) {
|
|
535
|
+
// Calculate reset time
|
|
536
|
+
const resetIn = 60000 - (now - counter.lastReset);
|
|
537
|
+
|
|
538
|
+
// Update statistics
|
|
539
|
+
this.stats.byIp[ip].totalBlocked++;
|
|
540
|
+
this.stats.totalBlocked++;
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
allowed: false,
|
|
544
|
+
reason: `IP ${ip} message rate limit exceeded`,
|
|
545
|
+
limit,
|
|
546
|
+
current: counter.count,
|
|
547
|
+
resetIn
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Increment counter
|
|
552
|
+
counter.count++;
|
|
553
|
+
|
|
554
|
+
// Update statistics
|
|
555
|
+
this.stats.byIp[ip].messagesPerMinute = counter.count;
|
|
556
|
+
this.stats.byIp[ip].totalMessages++;
|
|
557
|
+
|
|
558
|
+
return { allowed: true };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Check recipient limit
|
|
563
|
+
* @param email Email address
|
|
564
|
+
* @param recipients Number of recipients
|
|
565
|
+
* @param pattern Matched pattern
|
|
566
|
+
* @param domain Domain name
|
|
567
|
+
*/
|
|
568
|
+
private checkRecipientLimit(email: string, recipients: number, pattern?: string, domain?: string): IRateLimitResult {
|
|
569
|
+
// Get the most specific limit available
|
|
570
|
+
let limit = this.config.global.maxRecipientsPerMessage!;
|
|
571
|
+
|
|
572
|
+
// Check pattern-specific limit
|
|
573
|
+
if (pattern && this.config.patterns?.[pattern]?.maxRecipientsPerMessage) {
|
|
574
|
+
limit = this.config.patterns[pattern].maxRecipientsPerMessage!;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Check domain-specific limit (overrides pattern if present)
|
|
578
|
+
if (domain && this.config.domains?.[domain]?.maxRecipientsPerMessage) {
|
|
579
|
+
limit = this.config.domains[domain].maxRecipientsPerMessage!;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (!limit) {
|
|
583
|
+
return { allowed: true };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Check if limit is exceeded
|
|
587
|
+
if (recipients > limit) {
|
|
588
|
+
return {
|
|
589
|
+
allowed: false,
|
|
590
|
+
reason: 'Recipient limit exceeded',
|
|
591
|
+
limit,
|
|
592
|
+
current: recipients
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { allowed: true };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Record a connection from an IP
|
|
601
|
+
* @param ip IP address
|
|
602
|
+
* @returns Result of rate limit check
|
|
603
|
+
*/
|
|
604
|
+
public recordConnection(ip: string): IRateLimitResult {
|
|
605
|
+
const now = Date.now();
|
|
606
|
+
|
|
607
|
+
// Check if IP is blocked
|
|
608
|
+
if (this.isIpBlocked(ip)) {
|
|
609
|
+
return {
|
|
610
|
+
allowed: false,
|
|
611
|
+
reason: 'IP is blocked',
|
|
612
|
+
resetIn: this.getBlockReleaseTime(ip)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Get IP-specific limit or use global
|
|
617
|
+
const ipConfig = this.config.ips?.[ip];
|
|
618
|
+
const limit = ipConfig?.maxConnectionsPerIP || this.config.global.maxConnectionsPerIP!;
|
|
619
|
+
|
|
620
|
+
if (!limit) {
|
|
621
|
+
return { allowed: true };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Get or create counter
|
|
625
|
+
let counter = this.ipCounters.get(ip);
|
|
626
|
+
|
|
627
|
+
if (!counter) {
|
|
628
|
+
counter = {
|
|
629
|
+
count: 0,
|
|
630
|
+
lastReset: now,
|
|
631
|
+
recipients: 0,
|
|
632
|
+
errors: 0,
|
|
633
|
+
authFailures: 0,
|
|
634
|
+
connections: 0
|
|
635
|
+
};
|
|
636
|
+
this.ipCounters.set(ip, counter);
|
|
637
|
+
|
|
638
|
+
// Initialize IP stats if needed
|
|
639
|
+
if (!this.stats.byIp[ip]) {
|
|
640
|
+
this.stats.byIp[ip] = {
|
|
641
|
+
messagesPerMinute: 0,
|
|
642
|
+
totalMessages: 0,
|
|
643
|
+
totalBlocked: 0,
|
|
644
|
+
connections: 0,
|
|
645
|
+
errors: 0,
|
|
646
|
+
authFailures: 0,
|
|
647
|
+
blocked: false
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Check if counter needs to be reset
|
|
653
|
+
if (now - counter.lastReset >= 60000) {
|
|
654
|
+
counter.connections = 0;
|
|
655
|
+
counter.lastReset = now;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Check if limit is exceeded
|
|
659
|
+
if (counter.connections >= limit) {
|
|
660
|
+
// Calculate reset time
|
|
661
|
+
const resetIn = 60000 - (now - counter.lastReset);
|
|
662
|
+
|
|
663
|
+
// Update statistics
|
|
664
|
+
this.stats.byIp[ip].totalBlocked++;
|
|
665
|
+
this.stats.totalBlocked++;
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
allowed: false,
|
|
669
|
+
reason: `IP ${ip} connection rate limit exceeded`,
|
|
670
|
+
limit,
|
|
671
|
+
current: counter.connections,
|
|
672
|
+
resetIn
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Increment counter
|
|
677
|
+
counter.connections++;
|
|
678
|
+
|
|
679
|
+
// Update statistics
|
|
680
|
+
this.stats.byIp[ip].connections = counter.connections;
|
|
681
|
+
|
|
682
|
+
return { allowed: true };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Record an error from an IP
|
|
687
|
+
* @param ip IP address
|
|
688
|
+
* @returns True if IP should be blocked
|
|
689
|
+
*/
|
|
690
|
+
public recordError(ip: string): boolean {
|
|
691
|
+
const now = Date.now();
|
|
692
|
+
|
|
693
|
+
// Get IP-specific limit or use global
|
|
694
|
+
const ipConfig = this.config.ips?.[ip];
|
|
695
|
+
const limit = ipConfig?.maxErrorsPerIP || this.config.global.maxErrorsPerIP!;
|
|
696
|
+
|
|
697
|
+
if (!limit) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Get or create counter
|
|
702
|
+
let counter = this.ipCounters.get(ip);
|
|
703
|
+
|
|
704
|
+
if (!counter) {
|
|
705
|
+
counter = {
|
|
706
|
+
count: 0,
|
|
707
|
+
lastReset: now,
|
|
708
|
+
recipients: 0,
|
|
709
|
+
errors: 0,
|
|
710
|
+
authFailures: 0,
|
|
711
|
+
connections: 0
|
|
712
|
+
};
|
|
713
|
+
this.ipCounters.set(ip, counter);
|
|
714
|
+
|
|
715
|
+
// Initialize IP stats if needed
|
|
716
|
+
if (!this.stats.byIp[ip]) {
|
|
717
|
+
this.stats.byIp[ip] = {
|
|
718
|
+
messagesPerMinute: 0,
|
|
719
|
+
totalMessages: 0,
|
|
720
|
+
totalBlocked: 0,
|
|
721
|
+
connections: 0,
|
|
722
|
+
errors: 0,
|
|
723
|
+
authFailures: 0,
|
|
724
|
+
blocked: false
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Check if counter needs to be reset
|
|
730
|
+
if (now - counter.lastReset >= 60000) {
|
|
731
|
+
counter.errors = 0;
|
|
732
|
+
counter.lastReset = now;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Increment counter
|
|
736
|
+
counter.errors++;
|
|
737
|
+
|
|
738
|
+
// Update statistics
|
|
739
|
+
this.stats.byIp[ip].errors = counter.errors;
|
|
740
|
+
|
|
741
|
+
// Check if limit is exceeded
|
|
742
|
+
if (counter.errors >= limit) {
|
|
743
|
+
// Block the IP
|
|
744
|
+
this.blockIp(ip);
|
|
745
|
+
|
|
746
|
+
logger.log('warn', `IP ${ip} blocked due to excessive errors (${counter.errors}/${limit})`);
|
|
747
|
+
|
|
748
|
+
SecurityLogger.getInstance().logEvent({
|
|
749
|
+
level: SecurityLogLevel.WARN,
|
|
750
|
+
type: SecurityEventType.RATE_LIMITING,
|
|
751
|
+
message: 'IP blocked due to excessive errors',
|
|
752
|
+
ipAddress: ip,
|
|
753
|
+
details: {
|
|
754
|
+
errors: counter.errors,
|
|
755
|
+
limit
|
|
756
|
+
},
|
|
757
|
+
success: false
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Record an authentication failure from an IP
|
|
768
|
+
* @param ip IP address
|
|
769
|
+
* @returns True if IP should be blocked
|
|
770
|
+
*/
|
|
771
|
+
public recordAuthFailure(ip: string): boolean {
|
|
772
|
+
const now = Date.now();
|
|
773
|
+
|
|
774
|
+
// Get IP-specific limit or use global
|
|
775
|
+
const ipConfig = this.config.ips?.[ip];
|
|
776
|
+
const limit = ipConfig?.maxAuthFailuresPerIP || this.config.global.maxAuthFailuresPerIP!;
|
|
777
|
+
|
|
778
|
+
if (!limit) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Get or create counter
|
|
783
|
+
let counter = this.ipCounters.get(ip);
|
|
784
|
+
|
|
785
|
+
if (!counter) {
|
|
786
|
+
counter = {
|
|
787
|
+
count: 0,
|
|
788
|
+
lastReset: now,
|
|
789
|
+
recipients: 0,
|
|
790
|
+
errors: 0,
|
|
791
|
+
authFailures: 0,
|
|
792
|
+
connections: 0
|
|
793
|
+
};
|
|
794
|
+
this.ipCounters.set(ip, counter);
|
|
795
|
+
|
|
796
|
+
// Initialize IP stats if needed
|
|
797
|
+
if (!this.stats.byIp[ip]) {
|
|
798
|
+
this.stats.byIp[ip] = {
|
|
799
|
+
messagesPerMinute: 0,
|
|
800
|
+
totalMessages: 0,
|
|
801
|
+
totalBlocked: 0,
|
|
802
|
+
connections: 0,
|
|
803
|
+
errors: 0,
|
|
804
|
+
authFailures: 0,
|
|
805
|
+
blocked: false
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Check if counter needs to be reset
|
|
811
|
+
if (now - counter.lastReset >= 60000) {
|
|
812
|
+
counter.authFailures = 0;
|
|
813
|
+
counter.lastReset = now;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Increment counter
|
|
817
|
+
counter.authFailures++;
|
|
818
|
+
|
|
819
|
+
// Update statistics
|
|
820
|
+
this.stats.byIp[ip].authFailures = counter.authFailures;
|
|
821
|
+
|
|
822
|
+
// Check if limit is exceeded
|
|
823
|
+
if (counter.authFailures >= limit) {
|
|
824
|
+
// Block the IP
|
|
825
|
+
this.blockIp(ip);
|
|
826
|
+
|
|
827
|
+
logger.log('warn', `IP ${ip} blocked due to excessive authentication failures (${counter.authFailures}/${limit})`);
|
|
828
|
+
|
|
829
|
+
SecurityLogger.getInstance().logEvent({
|
|
830
|
+
level: SecurityLogLevel.WARN,
|
|
831
|
+
type: SecurityEventType.AUTHENTICATION,
|
|
832
|
+
message: 'IP blocked due to excessive authentication failures',
|
|
833
|
+
ipAddress: ip,
|
|
834
|
+
details: {
|
|
835
|
+
authFailures: counter.authFailures,
|
|
836
|
+
limit
|
|
837
|
+
},
|
|
838
|
+
success: false
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Block an IP address
|
|
849
|
+
* @param ip IP address to block
|
|
850
|
+
* @param duration Override the default block duration (milliseconds)
|
|
851
|
+
*/
|
|
852
|
+
public blockIp(ip: string, duration?: number): void {
|
|
853
|
+
if (!this.config.blocks) {
|
|
854
|
+
this.config.blocks = {};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Set block expiry time
|
|
858
|
+
const expiry = Date.now() + (duration || this.config.global.blockDuration || 3600000);
|
|
859
|
+
this.config.blocks[ip] = expiry;
|
|
860
|
+
|
|
861
|
+
// Update statistics
|
|
862
|
+
if (!this.stats.byIp[ip]) {
|
|
863
|
+
this.stats.byIp[ip] = {
|
|
864
|
+
messagesPerMinute: 0,
|
|
865
|
+
totalMessages: 0,
|
|
866
|
+
totalBlocked: 0,
|
|
867
|
+
connections: 0,
|
|
868
|
+
errors: 0,
|
|
869
|
+
authFailures: 0,
|
|
870
|
+
blocked: false
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
this.stats.byIp[ip].blocked = true;
|
|
874
|
+
this.stats.currentlyBlocked++;
|
|
875
|
+
|
|
876
|
+
// Emit event
|
|
877
|
+
this.emit('ipBlocked', {
|
|
878
|
+
ip,
|
|
879
|
+
expiry,
|
|
880
|
+
duration: duration || this.config.global.blockDuration
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
logger.log('warn', `IP ${ip} blocked until ${new Date(expiry).toISOString()}`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Unblock an IP address
|
|
888
|
+
* @param ip IP address to unblock
|
|
889
|
+
*/
|
|
890
|
+
public unblockIp(ip: string): void {
|
|
891
|
+
if (!this.config.blocks) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Remove block
|
|
896
|
+
delete this.config.blocks[ip];
|
|
897
|
+
|
|
898
|
+
// Update statistics
|
|
899
|
+
if (this.stats.byIp[ip]) {
|
|
900
|
+
this.stats.byIp[ip].blocked = false;
|
|
901
|
+
this.stats.currentlyBlocked--;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Emit event
|
|
905
|
+
this.emit('ipUnblocked', { ip });
|
|
906
|
+
|
|
907
|
+
logger.log('info', `IP ${ip} unblocked`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Check if an IP is blocked
|
|
912
|
+
* @param ip IP address to check
|
|
913
|
+
*/
|
|
914
|
+
public isIpBlocked(ip: string): boolean {
|
|
915
|
+
if (!this.config.blocks) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Check if IP is in blocks
|
|
920
|
+
if (!(ip in this.config.blocks)) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Check if block has expired
|
|
925
|
+
const expiry = this.config.blocks[ip];
|
|
926
|
+
if (expiry <= Date.now()) {
|
|
927
|
+
// Remove expired block
|
|
928
|
+
delete this.config.blocks[ip];
|
|
929
|
+
|
|
930
|
+
// Update statistics
|
|
931
|
+
if (this.stats.byIp[ip]) {
|
|
932
|
+
this.stats.byIp[ip].blocked = false;
|
|
933
|
+
this.stats.currentlyBlocked--;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Get the time until a block is released
|
|
944
|
+
* @param ip IP address
|
|
945
|
+
* @returns Milliseconds until release or 0 if not blocked
|
|
946
|
+
*/
|
|
947
|
+
public getBlockReleaseTime(ip: string): number {
|
|
948
|
+
if (!this.config.blocks || !(ip in this.config.blocks)) {
|
|
949
|
+
return 0;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const expiry = this.config.blocks[ip];
|
|
953
|
+
const now = Date.now();
|
|
954
|
+
|
|
955
|
+
return expiry > now ? expiry - now : 0;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Update rate limiter statistics
|
|
960
|
+
*/
|
|
961
|
+
private updateStats(): void {
|
|
962
|
+
// Update active counters count
|
|
963
|
+
this.stats.activeCounters = this.counters.size + this.patternCounters.size + this.ipCounters.size;
|
|
964
|
+
|
|
965
|
+
// Emit statistics update
|
|
966
|
+
this.emit('statsUpdated', this.stats);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Get rate limiter statistics
|
|
971
|
+
*/
|
|
972
|
+
public getStats(): IRateLimiterStats {
|
|
973
|
+
return { ...this.stats };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Update rate limiter configuration
|
|
978
|
+
* @param config New configuration
|
|
979
|
+
*/
|
|
980
|
+
public updateConfig(config: Partial<IHierarchicalRateLimits>): void {
|
|
981
|
+
if (config.global) {
|
|
982
|
+
this.config.global = {
|
|
983
|
+
...this.config.global,
|
|
984
|
+
...config.global
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (config.patterns) {
|
|
989
|
+
this.config.patterns = {
|
|
990
|
+
...this.config.patterns,
|
|
991
|
+
...config.patterns
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (config.ips) {
|
|
996
|
+
this.config.ips = {
|
|
997
|
+
...this.config.ips,
|
|
998
|
+
...config.ips
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
logger.log('info', 'Rate limiter configuration updated');
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Get configuration for debugging
|
|
1007
|
+
*/
|
|
1008
|
+
public getConfig(): IHierarchicalRateLimits {
|
|
1009
|
+
return { ...this.config };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Apply domain-specific rate limits
|
|
1014
|
+
* Merges domain limits with existing configuration
|
|
1015
|
+
* @param domain Domain name
|
|
1016
|
+
* @param limits Rate limit configuration for the domain
|
|
1017
|
+
*/
|
|
1018
|
+
public applyDomainLimits(domain: string, limits: IRateLimitConfig): void {
|
|
1019
|
+
if (!this.config.domains) {
|
|
1020
|
+
this.config.domains = {};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Merge the limits with any existing domain config
|
|
1024
|
+
this.config.domains[domain] = {
|
|
1025
|
+
...this.config.domains[domain],
|
|
1026
|
+
...limits
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
logger.log('info', `Applied rate limits for domain ${domain}:`, limits);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Remove domain-specific rate limits
|
|
1034
|
+
* @param domain Domain name
|
|
1035
|
+
*/
|
|
1036
|
+
public removeDomainLimits(domain: string): void {
|
|
1037
|
+
if (this.config.domains && this.config.domains[domain]) {
|
|
1038
|
+
delete this.config.domains[domain];
|
|
1039
|
+
// Also remove the counter
|
|
1040
|
+
this.domainCounters.delete(domain);
|
|
1041
|
+
logger.log('info', `Removed rate limits for domain ${domain}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Get domain-specific rate limits
|
|
1047
|
+
* @param domain Domain name
|
|
1048
|
+
* @returns Domain rate limit config or undefined
|
|
1049
|
+
*/
|
|
1050
|
+
public getDomainLimits(domain: string): IRateLimitConfig | undefined {
|
|
1051
|
+
return this.config.domains?.[domain];
|
|
1052
|
+
}
|
|
1053
|
+
}
|