mm_os 3.2.8 → 3.3.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/README.md +47 -1
- package/core/base/mqtt/index.js +344 -157
- package/core/base/web/index.js +98 -11
- package/core/com/middleware/com.js +152 -152
- package/core/com/socket/config.tpl.json +2 -2
- package/core/com/socket/drive.js +2 -2
- package/core/com/socket/index.js +1 -1
- package/core/com/sql/drive.js +7 -7
- package/index.js +41 -37
- package/middleware/performance/index.js +150 -142
- package/middleware/security_audit/index.js +549 -0
- package/middleware/security_audit/middleware.json +48 -0
- package/middleware/security_headers/index.js +487 -0
- package/middleware/security_headers/middleware.json +45 -0
- package/middleware/waf/index.js +277 -6
- package/middleware/waf/middleware.json +2 -1
- package/middleware/waf_ddos/index.js +520 -0
- package/middleware/waf_ddos/middleware.json +38 -0
- package/middleware/waf_ip/index.js +231 -20
- package/middleware/waf_ip/middleware.json +43 -4
- package/middleware/waf_xss/index.js +269 -0
- package/middleware/waf_xss/middleware.json +18 -0
- package/middleware/web_before/middleware.json +1 -1
- package/middleware/web_socket/middleware.json +2 -2
- package/package.json +18 -7
- package/middleware/cors/index.js +0 -103
- package/middleware/cors/middleware.json +0 -9
- package/middleware/log/index.js +0 -32
- package/middleware/log/middleware.json +0 -9
- package/middleware/rate_limit/index.js +0 -112
- package/middleware/rate_limit/middleware.json +0 -10
- package/nodemon.json +0 -31
- package/package.txt +0 -1
- package/rps.bat +0 -3
- package/test.js +0 -5
- package/tps.bat +0 -3
- package/update.bat +0 -1
- package//347/263/273/347/273/237/346/236/266/346/236/204/350/257/204/344/274/260/344/270/216/344/274/230/345/214/226/345/273/272/350/256/256.md +0 -599
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
const mm_expand = require('mm_expand');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 综合流量控制与DDOS攻击防护中间件
|
|
5
|
+
* 合并了原rate_limit和ddos_protection的功能,提供全面的流量防护机制
|
|
6
|
+
*/
|
|
7
|
+
class Middleware {
|
|
8
|
+
/**
|
|
9
|
+
* 构造函数
|
|
10
|
+
*/
|
|
11
|
+
constructor() {
|
|
12
|
+
// 内存存储,用于记录请求计数和IP信息
|
|
13
|
+
this.ipRequestCount = new Map();
|
|
14
|
+
this.blockedIps = new Map();
|
|
15
|
+
this.connectionCount = new Map();
|
|
16
|
+
|
|
17
|
+
this.default = {
|
|
18
|
+
// 启用综合流量保护
|
|
19
|
+
enable: true,
|
|
20
|
+
// 存储方式: 'memory', 'redis'
|
|
21
|
+
storage: 'memory',
|
|
22
|
+
// 基于IP的速率限制
|
|
23
|
+
ip_rate_limit: {
|
|
24
|
+
// 时间窗口(毫秒)
|
|
25
|
+
window_ms: 60000,
|
|
26
|
+
// 每个IP在时间窗口内的最大请求数
|
|
27
|
+
max_requests: 100,
|
|
28
|
+
// 是否启用
|
|
29
|
+
enable: true
|
|
30
|
+
},
|
|
31
|
+
// 基于路径的速率限制
|
|
32
|
+
path_rate_limit: {
|
|
33
|
+
// 时间窗口(毫秒)
|
|
34
|
+
window_ms: 60000,
|
|
35
|
+
// 每个IP对每个路径在时间窗口内的最大请求数
|
|
36
|
+
max_requests: 50,
|
|
37
|
+
// 是否启用
|
|
38
|
+
enable: true
|
|
39
|
+
},
|
|
40
|
+
// 连接限制
|
|
41
|
+
connection_limit: {
|
|
42
|
+
// 单个IP的最大并发连接数
|
|
43
|
+
max_connections: 50,
|
|
44
|
+
// 是否启用
|
|
45
|
+
enable: true
|
|
46
|
+
},
|
|
47
|
+
// 阻止规则
|
|
48
|
+
blocking: {
|
|
49
|
+
// 触发阻止的阈值倍数
|
|
50
|
+
threshold: 2,
|
|
51
|
+
// 阻止时间(毫秒)
|
|
52
|
+
block_time: 300000, // 5分钟
|
|
53
|
+
// 是否自动解除阻止
|
|
54
|
+
auto_unblock: true
|
|
55
|
+
},
|
|
56
|
+
// 白名单IP
|
|
57
|
+
whitelist: [],
|
|
58
|
+
// 忽略的路径(白名单路径)
|
|
59
|
+
ignore_paths: ['/api/health', '/favicon.ico', '/static/'],
|
|
60
|
+
// 是否记录攻击尝试
|
|
61
|
+
log: true,
|
|
62
|
+
// 是否阻止恶意请求
|
|
63
|
+
block: true,
|
|
64
|
+
// 响应状态码
|
|
65
|
+
status_code: 429,
|
|
66
|
+
// 响应消息
|
|
67
|
+
response_message: '请求过于频繁,请稍后再试',
|
|
68
|
+
// 是否设置X-RateLimit响应头
|
|
69
|
+
set_rate_limit_headers: true
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 初始化中间件
|
|
75
|
+
* @param {Object} config 配置项
|
|
76
|
+
*/
|
|
77
|
+
init(config) {
|
|
78
|
+
config = Object.assign({}, this.default, config);
|
|
79
|
+
this.config = config;
|
|
80
|
+
|
|
81
|
+
// 启动清理过期数据的定时器(仅在内存存储模式下需要)
|
|
82
|
+
if (config.storage === 'memory') {
|
|
83
|
+
this.startCleanupTimer();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return function() {
|
|
87
|
+
// 返回中间件的run方法作为实际的处理函数
|
|
88
|
+
return function(ctx, next) {
|
|
89
|
+
return middleware.run(ctx, next);
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取客户端真实IP
|
|
96
|
+
* 在代理环境中,优先从代理头获取真实IP
|
|
97
|
+
* @param {Object} ctx Koa上下文
|
|
98
|
+
* @returns {String} 客户端真实IP
|
|
99
|
+
*/
|
|
100
|
+
getClientIp(ctx) {
|
|
101
|
+
const headers = ctx.headers;
|
|
102
|
+
// 优先检查常用代理头获取真实IP
|
|
103
|
+
const xForwardedFor = headers['x-forwarded-for'];
|
|
104
|
+
if (xForwardedFor) {
|
|
105
|
+
// X-Forwarded-For 格式通常为: client, proxy1, proxy2
|
|
106
|
+
const ips = xForwardedFor.split(',').map(ip => ip.trim());
|
|
107
|
+
for (let i = 0; i < ips.length; i++) {
|
|
108
|
+
const ip = ips[i];
|
|
109
|
+
if (ip && ip !== 'unknown' && ip !== '127.0.0.1' && ip !== '::1') {
|
|
110
|
+
return ip;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 检查其他常见的代理头
|
|
116
|
+
return headers['x-real-ip'] ||
|
|
117
|
+
headers['x-client-ip'] ||
|
|
118
|
+
headers['cf-connecting-ip'] || // CloudFlare
|
|
119
|
+
headers['fastly-client-ip'] || // Fastly
|
|
120
|
+
headers['true-client-ip'] || // Akamai and Cloudflare
|
|
121
|
+
ctx.ip; // 默认回退到ctx.ip
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 使用Redis存储请求计数(从rate_limit中间件合并的功能)
|
|
126
|
+
* @param {String} clientKey 客户端标识符(通常是IP)
|
|
127
|
+
* @param {String} prefix 键前缀
|
|
128
|
+
* @param {Number} windowMs 时间窗口(秒)
|
|
129
|
+
* @returns {Promise<Number>} 请求计数
|
|
130
|
+
*/
|
|
131
|
+
async incrementRequestWithRedis(clientKey, prefix, windowMs) {
|
|
132
|
+
try {
|
|
133
|
+
// 检查Redis是否可用
|
|
134
|
+
if ($.cache && typeof $.cache.addInt === 'function') {
|
|
135
|
+
const key = `${prefix}:${clientKey}`;
|
|
136
|
+
|
|
137
|
+
// 使用addInt方法增加计数(mm_redis包提供的方法)
|
|
138
|
+
const count = await $.cache.addInt(key, 1);
|
|
139
|
+
|
|
140
|
+
// 设置过期时间
|
|
141
|
+
await $.cache.ttl(key, Math.ceil(windowMs / 1000));
|
|
142
|
+
|
|
143
|
+
return count || 0;
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (this.config.log && $.log && $.log.error) {
|
|
147
|
+
$.log.error('Redis速率限制失败:', error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Redis不可用时返回null,将使用内存存储
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 执行中间件
|
|
157
|
+
* @param {Object} ctx Koa上下文
|
|
158
|
+
* @param {Function} next 下一个中间件
|
|
159
|
+
*/
|
|
160
|
+
async run(ctx, next) {
|
|
161
|
+
const config = this.config;
|
|
162
|
+
// 使用改进的IP获取方法
|
|
163
|
+
const ip = this.getClientIp(ctx);
|
|
164
|
+
const path = ctx.path;
|
|
165
|
+
|
|
166
|
+
if (!config.enable) {
|
|
167
|
+
return await next();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 检查是否在白名单中
|
|
171
|
+
if (this.isWhitelisted(ip)) {
|
|
172
|
+
return await next();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 检查是否应该忽略该路径
|
|
176
|
+
if (this.shouldIgnorePath(path)) {
|
|
177
|
+
return await next();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 检查IP是否被阻止
|
|
181
|
+
if (this.isBlocked(ip)) {
|
|
182
|
+
if (config.log && $.log && $.log.warn) {
|
|
183
|
+
$.log.warn('Blocked IP Attempted Access:', {
|
|
184
|
+
ip: ip,
|
|
185
|
+
path: path,
|
|
186
|
+
method: ctx.method
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
ctx.status = config.status_code;
|
|
191
|
+
ctx.body = {
|
|
192
|
+
code: config.status_code,
|
|
193
|
+
msg: config.response_message
|
|
194
|
+
};
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 检查连接限制
|
|
199
|
+
if (config.connection_limit.enable && !this.checkConnectionLimit(ip)) {
|
|
200
|
+
this.blockIp(ip, 'Exceeded connection limit');
|
|
201
|
+
ctx.status = config.status_code;
|
|
202
|
+
ctx.body = {
|
|
203
|
+
code: config.status_code,
|
|
204
|
+
msg: config.response_message
|
|
205
|
+
};
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 检查IP速率限制
|
|
210
|
+
let requestCount = null;
|
|
211
|
+
|
|
212
|
+
if (config.ip_rate_limit.enable) {
|
|
213
|
+
if (config.storage === 'redis') {
|
|
214
|
+
// 使用Redis进行速率限制
|
|
215
|
+
requestCount = await this.incrementRequestWithRedis(
|
|
216
|
+
ip,
|
|
217
|
+
'rate_limit',
|
|
218
|
+
config.ip_rate_limit.window_ms
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (requestCount !== null) {
|
|
222
|
+
// Redis成功执行了计数
|
|
223
|
+
if (requestCount > config.ip_rate_limit.max_requests) {
|
|
224
|
+
this.blockIp(ip, 'Exceeded IP rate limit');
|
|
225
|
+
|
|
226
|
+
// 设置响应头
|
|
227
|
+
if (config.set_rate_limit_headers) {
|
|
228
|
+
ctx.set('X-RateLimit-Limit', config.ip_rate_limit.max_requests);
|
|
229
|
+
ctx.set('X-RateLimit-Remaining', 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ctx.status = config.status_code;
|
|
233
|
+
ctx.body = {
|
|
234
|
+
code: config.status_code,
|
|
235
|
+
msg: config.response_message
|
|
236
|
+
};
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 设置响应头
|
|
241
|
+
if (config.set_rate_limit_headers) {
|
|
242
|
+
ctx.set('X-RateLimit-Limit', config.ip_rate_limit.max_requests);
|
|
243
|
+
ctx.set('X-RateLimit-Remaining', Math.max(0, config.ip_rate_limit.max_requests -
|
|
244
|
+
requestCount));
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Redis不可用,使用内存存储方式
|
|
248
|
+
if (!this.checkIpRateLimit(ip)) {
|
|
249
|
+
this.blockIp(ip, 'Exceeded IP rate limit');
|
|
250
|
+
ctx.status = config.status_code;
|
|
251
|
+
ctx.body = {
|
|
252
|
+
code: config.status_code,
|
|
253
|
+
msg: config.response_message
|
|
254
|
+
};
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// 使用内存存储方式
|
|
260
|
+
if (!this.checkIpRateLimit(ip)) {
|
|
261
|
+
this.blockIp(ip, 'Exceeded IP rate limit');
|
|
262
|
+
ctx.status = config.status_code;
|
|
263
|
+
ctx.body = {
|
|
264
|
+
code: config.status_code,
|
|
265
|
+
msg: config.response_message
|
|
266
|
+
};
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 检查路径速率限制(仅内存存储支持)
|
|
273
|
+
if (config.path_rate_limit.enable && config.storage === 'memory') {
|
|
274
|
+
if (!this.checkPathRateLimit(ip, path)) {
|
|
275
|
+
this.blockIp(ip, `Exceeded path rate limit for ${path}`);
|
|
276
|
+
ctx.status = config.status_code;
|
|
277
|
+
ctx.body = {
|
|
278
|
+
code: config.status_code,
|
|
279
|
+
msg: config.response_message
|
|
280
|
+
};
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 增加连接计数
|
|
286
|
+
this.incrementConnection(ip);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await next();
|
|
290
|
+
} finally {
|
|
291
|
+
// 减少连接计数
|
|
292
|
+
this.decrementConnection(ip);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 检查IP是否在白名单中
|
|
298
|
+
* @param {String} ip IP地址
|
|
299
|
+
* @returns {Boolean} 是否在白名单中
|
|
300
|
+
*/
|
|
301
|
+
isWhitelisted(ip) {
|
|
302
|
+
return this.config.whitelist.includes(ip);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 检查是否应该忽略该路径
|
|
307
|
+
* @param {String} path 路径
|
|
308
|
+
* @returns {Boolean} 是否忽略
|
|
309
|
+
*/
|
|
310
|
+
shouldIgnorePath(path) {
|
|
311
|
+
return this.config.ignore_paths.some(p => path === p || path.startsWith(p));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 检查IP是否被阻止
|
|
316
|
+
* @param {String} ip IP地址
|
|
317
|
+
* @returns {Boolean} 是否被阻止
|
|
318
|
+
*/
|
|
319
|
+
isBlocked(ip) {
|
|
320
|
+
const blocked = this.blockedIps.get(ip);
|
|
321
|
+
if (!blocked) return false;
|
|
322
|
+
|
|
323
|
+
// 如果启用了自动解除阻止
|
|
324
|
+
if (this.config.blocking.auto_unblock) {
|
|
325
|
+
if (Date.now() > blocked.unblock_time) {
|
|
326
|
+
this.blockedIps.delete(ip);
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 阻止IP
|
|
336
|
+
* @param {String} ip IP地址
|
|
337
|
+
* @param {String} reason 阻止原因
|
|
338
|
+
*/
|
|
339
|
+
blockIp(ip, reason) {
|
|
340
|
+
if (!this.config.block) return;
|
|
341
|
+
|
|
342
|
+
const blockTime = this.config.blocking.block_time;
|
|
343
|
+
const unblockTime = Date.now() + blockTime;
|
|
344
|
+
|
|
345
|
+
this.blockedIps.set(ip, {
|
|
346
|
+
reason: reason,
|
|
347
|
+
block_time: Date.now(),
|
|
348
|
+
unblock_time: unblockTime
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (this.config.log && $.log && $.log.warn) {
|
|
352
|
+
$.log.warn('IP Blocked:', {
|
|
353
|
+
ip: ip,
|
|
354
|
+
reason: reason,
|
|
355
|
+
block_time: new Date().toISOString(),
|
|
356
|
+
unblock_time: new Date(unblockTime).toISOString()
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 检查连接限制
|
|
363
|
+
* @param {String} ip IP地址
|
|
364
|
+
* @returns {Boolean} 是否通过检查
|
|
365
|
+
*/
|
|
366
|
+
checkConnectionLimit(ip) {
|
|
367
|
+
const count = this.connectionCount.get(ip) || 0;
|
|
368
|
+
return count < this.config.connection_limit.max_connections;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 增加连接计数
|
|
373
|
+
* @param {String} ip IP地址
|
|
374
|
+
*/
|
|
375
|
+
incrementConnection(ip) {
|
|
376
|
+
const count = this.connectionCount.get(ip) || 0;
|
|
377
|
+
this.connectionCount.set(ip, count + 1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 减少连接计数
|
|
382
|
+
* @param {String} ip IP地址
|
|
383
|
+
*/
|
|
384
|
+
decrementConnection(ip) {
|
|
385
|
+
const count = this.connectionCount.get(ip);
|
|
386
|
+
if (count && count > 0) {
|
|
387
|
+
this.connectionCount.set(ip, count - 1);
|
|
388
|
+
} else {
|
|
389
|
+
this.connectionCount.delete(ip);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 检查IP速率限制(内存存储方式)
|
|
395
|
+
* @param {String} ip IP地址
|
|
396
|
+
* @returns {Boolean} 是否通过检查
|
|
397
|
+
*/
|
|
398
|
+
checkIpRateLimit(ip) {
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
const windowMs = this.config.ip_rate_limit.window_ms;
|
|
401
|
+
const maxRequests = this.config.ip_rate_limit.max_requests;
|
|
402
|
+
|
|
403
|
+
let requests = this.ipRequestCount.get(ip) || [];
|
|
404
|
+
|
|
405
|
+
// 清理过期的请求记录
|
|
406
|
+
requests = requests.filter(timestamp => now - timestamp < windowMs);
|
|
407
|
+
|
|
408
|
+
// 检查是否超过限制
|
|
409
|
+
if (requests.length >= maxRequests) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 添加当前请求时间戳
|
|
414
|
+
requests.push(now);
|
|
415
|
+
this.ipRequestCount.set(ip, requests);
|
|
416
|
+
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 检查路径速率限制(内存存储方式)
|
|
422
|
+
* @param {String} ip IP地址
|
|
423
|
+
* @param {String} path 路径
|
|
424
|
+
* @returns {Boolean} 是否通过检查
|
|
425
|
+
*/
|
|
426
|
+
checkPathRateLimit(ip, path) {
|
|
427
|
+
const key = `${ip}:${path}`;
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
const windowMs = this.config.path_rate_limit.window_ms;
|
|
430
|
+
const maxRequests = this.config.path_rate_limit.max_requests;
|
|
431
|
+
|
|
432
|
+
let requests = this.ipRequestCount.get(key) || [];
|
|
433
|
+
|
|
434
|
+
// 清理过期的请求记录
|
|
435
|
+
requests = requests.filter(timestamp => now - timestamp < windowMs);
|
|
436
|
+
|
|
437
|
+
// 检查是否超过限制
|
|
438
|
+
if (requests.length >= maxRequests) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 添加当前请求时间戳
|
|
443
|
+
requests.push(now);
|
|
444
|
+
this.ipRequestCount.set(key, requests);
|
|
445
|
+
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 启动清理过期数据的定时器
|
|
451
|
+
*/
|
|
452
|
+
startCleanupTimer() {
|
|
453
|
+
// 每30秒清理一次过期数据
|
|
454
|
+
setInterval(() => {
|
|
455
|
+
this.cleanupExpiredData();
|
|
456
|
+
}, 30000);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 清理过期数据
|
|
461
|
+
*/
|
|
462
|
+
cleanupExpiredData() {
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
|
|
465
|
+
// 清理过期的IP请求记录
|
|
466
|
+
for (const [key, requests] of this.ipRequestCount.entries()) {
|
|
467
|
+
const cleanedRequests = requests.filter(timestamp =>
|
|
468
|
+
now - timestamp < Math.max(this.config.ip_rate_limit.window_ms, this.config.path_rate_limit
|
|
469
|
+
.window_ms)
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (cleanedRequests.length > 0) {
|
|
473
|
+
this.ipRequestCount.set(key, cleanedRequests);
|
|
474
|
+
} else {
|
|
475
|
+
this.ipRequestCount.delete(key);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 自动解除过期的阻止
|
|
480
|
+
if (this.config.blocking.auto_unblock) {
|
|
481
|
+
for (const [ip, blockInfo] of this.blockedIps.entries()) {
|
|
482
|
+
if (now > blockInfo.unblock_time) {
|
|
483
|
+
this.blockedIps.delete(ip);
|
|
484
|
+
if (this.config.log && $.log && $.log.info) {
|
|
485
|
+
$.log.info('IP Unblocked Automatically:', {
|
|
486
|
+
ip: ip,
|
|
487
|
+
reason: blockInfo.reason
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 创建中间件实例
|
|
497
|
+
const middleware = new Middleware();
|
|
498
|
+
|
|
499
|
+
// 导出符合系统期望的函数
|
|
500
|
+
exports = module.exports = function(server, config) {
|
|
501
|
+
// 初始化中间件
|
|
502
|
+
const handlerFactory = middleware.init(config);
|
|
503
|
+
const middlewareHandler = handlerFactory();
|
|
504
|
+
|
|
505
|
+
// 直接使用server.use注册中间件
|
|
506
|
+
server.use(middlewareHandler);
|
|
507
|
+
|
|
508
|
+
// 记录中间件初始化信息
|
|
509
|
+
if ($.log && $.log.info) {
|
|
510
|
+
const cg = middleware.config;
|
|
511
|
+
$.log.info(
|
|
512
|
+
`综合流量控制中间件已加载: 存储方式=${cg.storage}, IP限制=${cg.ip_rate_limit.max_requests}/${cg.ip_rate_limit.window_ms/1000}秒, 连接限制=${cg.connection_limit.max_connections}`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return server;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// 保留原始实例,以便其他方式调用
|
|
520
|
+
exports.middleware = middleware;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "waf_ddos",
|
|
3
|
+
"title": "综合流量控制与DDOS攻击防护中间件",
|
|
4
|
+
"type": "web_before",
|
|
5
|
+
"status": 1,
|
|
6
|
+
"sort": 12,
|
|
7
|
+
"desc": "提供DDOS攻击防护和速率限制,包括IP限流、路径限流、连接控制和IP封禁功能",
|
|
8
|
+
"config": {
|
|
9
|
+
"enable": true,
|
|
10
|
+
"storage": "memory",
|
|
11
|
+
"ip_rate_limit": {
|
|
12
|
+
"window_ms": 60000,
|
|
13
|
+
"max_requests": 100,
|
|
14
|
+
"enable": true
|
|
15
|
+
},
|
|
16
|
+
"path_rate_limit": {
|
|
17
|
+
"window_ms": 60000,
|
|
18
|
+
"max_requests": 50,
|
|
19
|
+
"enable": true
|
|
20
|
+
},
|
|
21
|
+
"connection_limit": {
|
|
22
|
+
"max_connections": 50,
|
|
23
|
+
"enable": true
|
|
24
|
+
},
|
|
25
|
+
"blocking": {
|
|
26
|
+
"threshold": 2,
|
|
27
|
+
"block_time": 300000,
|
|
28
|
+
"auto_unblock": true
|
|
29
|
+
},
|
|
30
|
+
"whitelist": [],
|
|
31
|
+
"ignore_paths": ["/api/health", "/favicon.ico", "/static/"],
|
|
32
|
+
"log": true,
|
|
33
|
+
"block": true,
|
|
34
|
+
"status_code": 429,
|
|
35
|
+
"response_message": "请求过于频繁,请稍后再试",
|
|
36
|
+
"set_rate_limit_headers": true
|
|
37
|
+
}
|
|
38
|
+
}
|