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
package/core/base/mqtt/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const mosca = require('mosca');
|
|
|
7
7
|
class MQTT {
|
|
8
8
|
/**
|
|
9
9
|
* 构造函数
|
|
10
|
-
* @param {Object} config 配置参数
|
|
10
|
+
* @param {Object} config 配置参数
|
|
11
11
|
*/
|
|
12
12
|
constructor(config) {
|
|
13
13
|
this.config = Object.assign({
|
|
@@ -24,26 +24,24 @@ class MQTT {
|
|
|
24
24
|
"message_retention": 86400,
|
|
25
25
|
// 消息频率限制配置
|
|
26
26
|
"rate_limit": {
|
|
27
|
-
//
|
|
28
|
-
"time_window":
|
|
27
|
+
// 时间窗口(秒)
|
|
28
|
+
"time_window": 60, // 默认1分钟
|
|
29
29
|
// 时间窗口内允许的最大消息数
|
|
30
30
|
"max_messages": 100, // 人脸识别门禁优化:1分钟内最多20条消息(门禁通常每次识别仅发送1-3条消息)
|
|
31
|
-
//
|
|
32
|
-
"block_duration":
|
|
33
|
-
},
|
|
34
|
-
// 重连检测配置
|
|
35
|
-
"reconnect_detection": {
|
|
36
|
-
// 时间窗口(毫秒)
|
|
37
|
-
"time_window": 60000, // 默认1分钟
|
|
38
|
-
// 最大允许重连次数
|
|
39
|
-
"max_reconnect_attempts": 5,
|
|
40
|
-
// 同一客户端ID不同IP的禁止时间(毫秒)
|
|
41
|
-
"ip_ban_duration": 3600000, // 默认1小时
|
|
42
|
-
// 客户端ID禁止重连时间(毫秒)
|
|
43
|
-
"client_id_ban_duration": 7200000, // 默认2小时
|
|
44
|
-
// 频繁重连时间阈值(毫秒)
|
|
45
|
-
"frequent_reconnect_threshold": 30000 // 默认30秒
|
|
31
|
+
// 拉黑时长(秒)
|
|
32
|
+
"block_duration": 3600 // 人脸识别门禁优化:拉黑30分钟(更合理的临时限制时长)
|
|
46
33
|
},
|
|
34
|
+
// 安全配置 - 扁平化且共用参数
|
|
35
|
+
// 通用安全配置
|
|
36
|
+
"time_window": 60, // 时间窗口(秒)- 共用参数
|
|
37
|
+
"max_attempts": 5, // 最大尝试次数 - 共用参数
|
|
38
|
+
"block_duration": 3600, // 默认拉黑时长(秒)- 共用参数
|
|
39
|
+
// 重连检测特定配置
|
|
40
|
+
"reconnect_ip_duration": 3600, // 同一客户端ID不同IP的禁止时间(秒)
|
|
41
|
+
"reconnect_client_duration": 7200, // 客户端ID禁止重连时间(秒)
|
|
42
|
+
"reconnect_frequency_threshold": 30, // 频繁重连时间阈值(秒)
|
|
43
|
+
// 密码尝试特定配置
|
|
44
|
+
"password_block_ip": true, // 密码错误时是否同时拉黑IP
|
|
47
45
|
// 受限主题列表,用于限制某些敏感主题只能由特定客户端订阅
|
|
48
46
|
restrictedTopics: [],
|
|
49
47
|
// 允许订阅受限主题的客户端列表
|
|
@@ -84,18 +82,24 @@ class MQTT {
|
|
|
84
82
|
*/
|
|
85
83
|
this.blockedClients = {};
|
|
86
84
|
|
|
87
|
-
/**
|
|
88
|
-
* 客户端连接历史记录
|
|
89
|
-
* 格式: { clientId: { ip: 客户端IP, connectCount: 连接次数, lastConnectTime: 上次连接时间, disconnectTime: 上次断开时间, reconnectAttempts: 重连尝试次数 } }
|
|
90
|
-
*/
|
|
91
|
-
this.clientHistory = {};
|
|
92
|
-
|
|
93
85
|
/**
|
|
94
86
|
* 被禁止的IP列表
|
|
95
87
|
* 格式: { ip: 解除禁止时间戳 }
|
|
96
88
|
*/
|
|
97
89
|
this.bannedIps = {};
|
|
98
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 安全记录(合并客户端连接历史和密码错误记录)
|
|
93
|
+
* 格式: {
|
|
94
|
+
* client: { clientId: { ip: 客户端IP, connectCount: 连接次数, lastConnectTime: 上次连接时间, disconnectTime: 上次断开时间, reconnectAttempts: 重连尝试次数 } },
|
|
95
|
+
* password: { username: { count: 错误次数, firstAttempt: 首次尝试时间, lastAttempt: 上次尝试时间, ip: 尝试IP } }
|
|
96
|
+
* }
|
|
97
|
+
*/
|
|
98
|
+
this.securityRecords = {
|
|
99
|
+
client: {},
|
|
100
|
+
password: {}
|
|
101
|
+
};
|
|
102
|
+
|
|
99
103
|
/**
|
|
100
104
|
* 黑名单存储类型
|
|
101
105
|
* 可选值: 'memory', 'redis', 'mongodb', 'mysql'
|
|
@@ -185,16 +189,16 @@ MQTT.prototype.init = function (config) {
|
|
|
185
189
|
factory: mosca.persistence.Redis,
|
|
186
190
|
ttl: {
|
|
187
191
|
// TTL for subscriptions
|
|
188
|
-
subscriptions: cg.message_retention * 1000,
|
|
192
|
+
subscriptions: cg.message_retention * 1000, // 恢复为毫秒
|
|
189
193
|
// TTL for packets
|
|
190
|
-
packets: cg.message_retention * 1000,
|
|
194
|
+
packets: cg.message_retention * 1000, // 恢复为毫秒
|
|
191
195
|
}
|
|
192
196
|
}, conf.backend);
|
|
193
197
|
} else if (cg.cache === 'mongodb' || cg.cache === 'mongo') {
|
|
194
198
|
var url = `mongodb://${mongodb.host}:${mongodb.port}/${mongodb.database}`;
|
|
195
199
|
if (mongodb.user) {
|
|
196
200
|
url =
|
|
197
|
-
`mongodb://${mongodb.user}:${mongodb.password}@${mongodb.host}:${mongodb.port}/${mongodb.database}
|
|
201
|
+
`mongodb://${mongodb.user}:${mongodb.password}@${mongodb.host}:${mongodb.port}/${mongodb.database}`;
|
|
198
202
|
}
|
|
199
203
|
conf.backend = {
|
|
200
204
|
// 增加了此项
|
|
@@ -202,16 +206,16 @@ MQTT.prototype.init = function (config) {
|
|
|
202
206
|
url,
|
|
203
207
|
pubsubCollection: 'ascoltatori',
|
|
204
208
|
mongo: {}
|
|
205
|
-
}
|
|
209
|
+
};
|
|
206
210
|
|
|
207
211
|
// 数据持久化参数设置
|
|
208
212
|
conf.persistence = Object.assign({
|
|
209
213
|
factory: mosca.persistence.Mongo,
|
|
210
214
|
ttl: {
|
|
211
215
|
// TTL for subscriptions
|
|
212
|
-
subscriptions: cg.message_retention * 1000,
|
|
216
|
+
subscriptions: cg.message_retention * 1000, // 恢复为毫秒
|
|
213
217
|
// TTL for packets
|
|
214
|
-
packets: cg.message_retention * 1000,
|
|
218
|
+
packets: cg.message_retention * 1000, // 恢复为毫秒
|
|
215
219
|
}
|
|
216
220
|
}, conf.backend);
|
|
217
221
|
}
|
|
@@ -285,7 +289,9 @@ MQTT.prototype.main = function (state) {
|
|
|
285
289
|
* 身份验证
|
|
286
290
|
*/
|
|
287
291
|
sr.authenticate = (client, username, password, callback) => {
|
|
288
|
-
|
|
292
|
+
// 正确获取客户端IP地址
|
|
293
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream.remoteAddress || 'unknown' : 'unknown';
|
|
294
|
+
$.log.info("收到身份验证", clientIp, client.id, username);
|
|
289
295
|
// 因为auth现在是异步函数,需要使用async/await处理
|
|
290
296
|
(async () => {
|
|
291
297
|
try {
|
|
@@ -310,7 +316,6 @@ MQTT.prototype.main = function (state) {
|
|
|
310
316
|
return true;
|
|
311
317
|
};
|
|
312
318
|
|
|
313
|
-
|
|
314
319
|
/**
|
|
315
320
|
* 验证订阅,决定客户端可以订阅哪些主题
|
|
316
321
|
*/
|
|
@@ -347,9 +352,9 @@ MQTT.prototype.subscribed = function (topic, client) {
|
|
|
347
352
|
};
|
|
348
353
|
|
|
349
354
|
/**
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
355
|
+
* 客户端断开连接时的处理
|
|
356
|
+
* @param {Object} client 客户端对象
|
|
357
|
+
*/
|
|
353
358
|
MQTT.prototype.clientDisconnected = function (client) {
|
|
354
359
|
const clientId = client.id;
|
|
355
360
|
const now = Date.now();
|
|
@@ -357,8 +362,8 @@ MQTT.prototype.clientDisconnected = function (client) {
|
|
|
357
362
|
$.log.info(`MQTT客户端 ${clientId} 断开连接`);
|
|
358
363
|
|
|
359
364
|
// 更新客户端断开连接时间
|
|
360
|
-
if (this.
|
|
361
|
-
this.
|
|
365
|
+
if (this.securityRecords.client[clientId]) {
|
|
366
|
+
this.securityRecords.client[clientId].disconnectTime = now;
|
|
362
367
|
}
|
|
363
368
|
};
|
|
364
369
|
|
|
@@ -372,12 +377,12 @@ MQTT.prototype.published = function (packet, client) {
|
|
|
372
377
|
};
|
|
373
378
|
|
|
374
379
|
/**
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
380
|
+
* 身份验证 - 主要入口函数
|
|
381
|
+
* @param {Object} client 客户端
|
|
382
|
+
* @param {String} username 用户名
|
|
383
|
+
* @param {String} password 密码
|
|
384
|
+
* @param {Function} callback 回调函数,回调返回true,则表示验证通过。
|
|
385
|
+
*/
|
|
381
386
|
MQTT.prototype.auth = async function (client, username, password, callback) {
|
|
382
387
|
try {
|
|
383
388
|
// 检查客户端状态(拉黑、异常重连等)- 现在是异步操作
|
|
@@ -397,6 +402,13 @@ MQTT.prototype.auth = async function (client, username, password, callback) {
|
|
|
397
402
|
return;
|
|
398
403
|
}
|
|
399
404
|
|
|
405
|
+
// 检查密码错误次数限制(防暴力破解)
|
|
406
|
+
const passwordLimitResult = await this.checkPasswordAttemptLimit(client, username);
|
|
407
|
+
if (passwordLimitResult === false) {
|
|
408
|
+
callback(null, false);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
400
412
|
// 查询用户密码并验证(异步操作,需要callback)
|
|
401
413
|
this.checkAccount(client, username, passwordStr, callback);
|
|
402
414
|
} catch (error) {
|
|
@@ -421,7 +433,8 @@ MQTT.prototype.checkClientStatus = async function (client) {
|
|
|
421
433
|
|
|
422
434
|
// 检测是否有异常重连行为
|
|
423
435
|
// 注意:detectReconnectAbuse内部也有黑名单相关操作,后续可以考虑重构为使用异步函数
|
|
424
|
-
|
|
436
|
+
const hasReconnectAbuse = await this.detectReconnectAbuse(client);
|
|
437
|
+
if (hasReconnectAbuse) {
|
|
425
438
|
$.log.warn(`拒绝客户端 ${client.id} 身份验证:检测到异常重连行为`);
|
|
426
439
|
return false;
|
|
427
440
|
}
|
|
@@ -461,58 +474,68 @@ MQTT.prototype.validateCredentials = function (client, username, passwordStr) {
|
|
|
461
474
|
* 查询用户密码并验证
|
|
462
475
|
* @param {Object} client 客户端
|
|
463
476
|
* @param {String} username 用户名
|
|
464
|
-
* @param {String}
|
|
477
|
+
* @param {String} password 密码字符串
|
|
465
478
|
* @param {Function} callback 回调函数
|
|
466
479
|
*/
|
|
467
|
-
MQTT.prototype.checkAccount = function (client, username,
|
|
480
|
+
MQTT.prototype.checkAccount = async function (client, username, password, callback) {
|
|
481
|
+
if (!username || !password) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
468
484
|
var sql = $.mysql_admin('sys');
|
|
469
485
|
try {
|
|
470
|
-
var
|
|
471
|
-
db.table = this.config.table_name || 'face_device';
|
|
486
|
+
var dbs = sql.db();
|
|
472
487
|
// 先尝试通过客户端ID和用户名精确匹配
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
callback(null, false);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
// 验证密码
|
|
483
|
-
this.verifyPassword(client, username, passwordStr, obj.password, callback);
|
|
484
|
-
}).catch((err) => {
|
|
485
|
-
$.log.error(`MQTT客户端 ${client.id} 身份验证数据库错误:`, err);
|
|
486
|
-
callback(null, false);
|
|
487
|
-
return;
|
|
488
|
+
|
|
489
|
+
// 判断设备连接
|
|
490
|
+
var db1 = dbs.new(this.config.table_name || 'face_device', "");
|
|
491
|
+
var obj = await db1.getObj({
|
|
492
|
+
username,
|
|
493
|
+
clientid: client.id
|
|
488
494
|
});
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
495
|
+
if (obj) {
|
|
496
|
+
if (obj.available && obj.state) {
|
|
497
|
+
if (obj.password == password) {
|
|
498
|
+
// 账号密码正确
|
|
499
|
+
return callback(null, true);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:密码错误,用户名: ${username}`);
|
|
503
|
+
// 记录密码错误尝试
|
|
504
|
+
await this.recordPasswordFailure(client, username);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:账户不可用,用户名: ${username}`);
|
|
509
|
+
}
|
|
510
|
+
} else {
|
|
511
|
+
// 判断应用连接
|
|
512
|
+
var db2 = dbs.new("sys_app", "app_id");
|
|
513
|
+
var o = await db2.getObj({
|
|
514
|
+
appid: username
|
|
515
|
+
});
|
|
516
|
+
if (o) {
|
|
517
|
+
if (o.available) {
|
|
518
|
+
if (o.appsecret == password) {
|
|
519
|
+
// 账号密码正确
|
|
520
|
+
return callback(null, true);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:密码错误,用户名: ${username}`);
|
|
524
|
+
// 记录密码错误尝试
|
|
525
|
+
await this.recordPasswordFailure(client, username);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:账户不可用,用户名: ${username}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
493
533
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* 验证密码
|
|
498
|
-
* @param {Object} client 客户端
|
|
499
|
-
* @param {String} username 用户名
|
|
500
|
-
* @param {String} passwordStr 密码字符串
|
|
501
|
-
* @param {String} dbPassword 数据库中的密码
|
|
502
|
-
* @param {Function} callback 回调函数
|
|
503
|
-
*/
|
|
504
|
-
MQTT.prototype.verifyPassword = function (client, username, passwordStr, dbPassword, callback) {
|
|
505
|
-
// 使用简单的密码比较(当前实现,可根据需要扩展为bcrypt验证)
|
|
506
|
-
const isMatch = dbPassword === passwordStr;
|
|
507
|
-
|
|
508
|
-
if (isMatch) {
|
|
509
|
-
$.log.info(`MQTT客户端 ${client.id} 身份验证成功,用户: ${username}`);
|
|
510
|
-
} else {
|
|
511
|
-
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:密码不匹配,用户名: ${username}`);
|
|
534
|
+
catch (err) {
|
|
535
|
+
$.log.error(`MQTT客户端 ${client.id} 身份验证数据库错误:`, err);
|
|
512
536
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
};
|
|
537
|
+
callback(null, false);
|
|
538
|
+
}
|
|
516
539
|
|
|
517
540
|
/**
|
|
518
541
|
* 验证发布,决定客户端可以发布哪些主题
|
|
@@ -623,8 +646,8 @@ MQTT.prototype.checkHighFrequency = async function (client) {
|
|
|
623
646
|
lastReset: now
|
|
624
647
|
};
|
|
625
648
|
} else {
|
|
626
|
-
//
|
|
627
|
-
if (now - this.messageCounters[clientId].lastReset > config.time_window) {
|
|
649
|
+
// 检查是否需要重置计数器(注意:time_window现在是秒,需要转换为毫秒进行比较)
|
|
650
|
+
if (now - this.messageCounters[clientId].lastReset > config.time_window * 1000) {
|
|
628
651
|
this.messageCounters[clientId].count = 1;
|
|
629
652
|
this.messageCounters[clientId].lastReset = now;
|
|
630
653
|
} else {
|
|
@@ -637,7 +660,7 @@ MQTT.prototype.checkHighFrequency = async function (client) {
|
|
|
637
660
|
if (this.messageCounters[clientId].count > config.max_messages) {
|
|
638
661
|
// 拉黑客户端 - 使用新的异步函数
|
|
639
662
|
try {
|
|
640
|
-
await this.
|
|
663
|
+
await this.addBlacklist(clientId, config.block_duration);
|
|
641
664
|
} catch (error) {
|
|
642
665
|
$.log.error(`拉黑高频请求客户端 ${clientId} 时出错:`, error);
|
|
643
666
|
}
|
|
@@ -659,7 +682,7 @@ MQTT.prototype.blockClient = async function (client) {
|
|
|
659
682
|
|
|
660
683
|
try {
|
|
661
684
|
// 使用新的异步函数添加到黑名单
|
|
662
|
-
await this.
|
|
685
|
+
await this.addBlacklist(clientId, config.block_duration);
|
|
663
686
|
} catch (error) {
|
|
664
687
|
$.log.error(`拉黑MQTT客户端 ${clientId} 时出错:`, error);
|
|
665
688
|
}
|
|
@@ -683,80 +706,80 @@ MQTT.prototype.detectReconnectAbuse = async function (client) {
|
|
|
683
706
|
try {
|
|
684
707
|
const clientId = client.id;
|
|
685
708
|
const now = Date.now();
|
|
686
|
-
const config = this.config
|
|
709
|
+
const config = this.config;
|
|
687
710
|
|
|
688
|
-
|
|
689
|
-
|
|
711
|
+
// 从client对象中获取IP
|
|
712
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream.remoteAddress || 'unknown' : 'unknown';
|
|
690
713
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
714
|
+
// 检查IP是否被禁止
|
|
715
|
+
const isIpBanned = await this.isIpBanned(clientIp);
|
|
716
|
+
if (isIpBanned) {
|
|
717
|
+
$.log.warn(`IP ${clientIp} 处于禁止状态,拒绝连接请求`);
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
697
720
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
721
|
+
// 检查是否是首次连接
|
|
722
|
+
if (!this.securityRecords.client[clientId]) {
|
|
723
|
+
// 首次连接,初始化连接历史
|
|
724
|
+
this.securityRecords.client[clientId] = {
|
|
725
|
+
ip: clientIp,
|
|
726
|
+
connectCount: 1,
|
|
727
|
+
lastConnectTime: now,
|
|
728
|
+
disconnectTime: null,
|
|
729
|
+
reconnectAttempts: 0
|
|
730
|
+
};
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
710
733
|
|
|
711
|
-
|
|
712
|
-
|
|
734
|
+
// 获取客户端历史记录
|
|
735
|
+
const history = this.securityRecords.client[clientId];
|
|
713
736
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
737
|
+
// 检查是否是来自不同IP的连接
|
|
738
|
+
if (history.ip !== clientIp) {
|
|
739
|
+
$.log.warn(`检测到客户端ID ${clientId} 从不同IP连接: 原IP ${history.ip}, 新IP ${clientIp}`);
|
|
717
740
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
741
|
+
// 记录新IP的连接行为
|
|
742
|
+
const oldIp = history.ip;
|
|
743
|
+
history.ip = clientIp;
|
|
744
|
+
history.connectCount++;
|
|
745
|
+
history.lastConnectTime = now;
|
|
723
746
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
747
|
+
// 禁止新IP连接
|
|
748
|
+
try {
|
|
749
|
+
await this.addIpToBanlist(clientIp, config.reconnect_ip_duration);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
$.log.error(`禁止IP ${clientIp} 时出错:`, error);
|
|
752
|
+
}
|
|
753
|
+
return true;
|
|
729
754
|
}
|
|
730
|
-
return true;
|
|
731
|
-
}
|
|
732
755
|
|
|
733
|
-
|
|
734
|
-
|
|
756
|
+
// 检查是否在短时间内频繁重连
|
|
757
|
+
const timeSinceLastConnect = now - history.lastConnectTime;
|
|
735
758
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
759
|
+
// 如果两次连接间隔很短,增加重连尝试计数
|
|
760
|
+
if (timeSinceLastConnect < config.reconnect_frequency_threshold * 1000) {
|
|
761
|
+
history.reconnectAttempts++;
|
|
762
|
+
} else {
|
|
763
|
+
// 重置重连计数
|
|
764
|
+
history.reconnectAttempts = 0;
|
|
765
|
+
}
|
|
743
766
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
767
|
+
// 更新连接计数和时间
|
|
768
|
+
history.connectCount++;
|
|
769
|
+
history.lastConnectTime = now;
|
|
747
770
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
771
|
+
// 检查是否超过最大重连尝试次数
|
|
772
|
+
if (history.reconnectAttempts > config.max_attempts) {
|
|
773
|
+
$.log.warn(`客户端 ${clientId} 重连过于频繁,已尝试 ${history.reconnectAttempts} 次重连`);
|
|
751
774
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
775
|
+
// 禁止该客户端ID重连
|
|
776
|
+
try {
|
|
777
|
+
await this.addBlacklist(clientId, config.reconnect_client_duration);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
$.log.error(`禁止频繁重连客户端 ${clientId} 时出错:`, error);
|
|
780
|
+
}
|
|
781
|
+
return true;
|
|
757
782
|
}
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
760
783
|
|
|
761
784
|
// 正常连接
|
|
762
785
|
return false;
|
|
@@ -767,6 +790,96 @@ MQTT.prototype.detectReconnectAbuse = async function (client) {
|
|
|
767
790
|
}
|
|
768
791
|
};
|
|
769
792
|
|
|
793
|
+
/**
|
|
794
|
+
* 异步检查密码尝试次数限制
|
|
795
|
+
* @param {Object} client 客户端
|
|
796
|
+
* @param {String} username 用户名
|
|
797
|
+
* @returns {Promise<Boolean>} true表示未超过限制,false表示已超过限制
|
|
798
|
+
*/
|
|
799
|
+
MQTT.prototype.checkPasswordAttemptLimit = async function (client, username) {
|
|
800
|
+
try {
|
|
801
|
+
const now = Date.now();
|
|
802
|
+
const config = this.config;
|
|
803
|
+
|
|
804
|
+
// 从client对象中获取IP
|
|
805
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream.remoteAddress || 'unknown' : 'unknown';
|
|
806
|
+
|
|
807
|
+
// 检查用户名是否已被禁止(密码错误次数过多)
|
|
808
|
+
if (this.securityRecords.password[username]) {
|
|
809
|
+
const failureRecord = this.securityRecords.password[username];
|
|
810
|
+
const timeSinceFirstFailure = now - failureRecord.firstAttempt;
|
|
811
|
+
const timeSinceLastFailure = now - failureRecord.lastAttempt;
|
|
812
|
+
|
|
813
|
+
// 如果超过了时间窗口,重置错误计数
|
|
814
|
+
if (timeSinceFirstFailure > config.time_window * 1000) {
|
|
815
|
+
// 重置错误计数
|
|
816
|
+
delete this.securityRecords.password[username];
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// 检查错误次数是否超过限制
|
|
821
|
+
if (failureRecord.count >= config.max_attempts) {
|
|
822
|
+
// 记录被拒绝的尝试
|
|
823
|
+
$.log.warn(`拒绝客户端 ${client.id} (IP: ${clientIp}) 连接:用户名 ${username} 密码错误次数过多`);
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return true;
|
|
829
|
+
} catch (error) {
|
|
830
|
+
$.log.error(`检查客户端 ${client.id} 密码尝试次数限制时出错:`, error);
|
|
831
|
+
// 出错时默认视为未超过限制,避免误判
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* 异步记录密码错误尝试
|
|
838
|
+
* @param {Object} client 客户端
|
|
839
|
+
* @param {String} username 用户名
|
|
840
|
+
*/
|
|
841
|
+
MQTT.prototype.recordPasswordFailure = async function (client, username) {
|
|
842
|
+
try {
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
const config = this.config;
|
|
845
|
+
|
|
846
|
+
// 从client对象中获取IP
|
|
847
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream.remoteAddress || 'unknown' : 'unknown';
|
|
848
|
+
|
|
849
|
+
// 初始化或更新密码错误记录
|
|
850
|
+
if (!this.securityRecords.password[username]) {
|
|
851
|
+
this.securityRecords.password[username] = {
|
|
852
|
+
count: 1,
|
|
853
|
+
firstAttempt: now,
|
|
854
|
+
lastAttempt: now,
|
|
855
|
+
ip: clientIp
|
|
856
|
+
};
|
|
857
|
+
} else {
|
|
858
|
+
this.securityRecords.password[username].count++;
|
|
859
|
+
this.securityRecords.password[username].lastAttempt = now;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const failureCount = this.securityRecords.password[username].count;
|
|
863
|
+
$.log.warn(`MQTT客户端 ${client.id} (IP: ${clientIp}) 密码错误,用户名: ${username},当前错误次数: ${failureCount}`);
|
|
864
|
+
|
|
865
|
+
// 检查是否达到最大错误次数,需要拉黑
|
|
866
|
+
if (failureCount >= config.max_attempts) {
|
|
867
|
+
// 拉黑客户端
|
|
868
|
+
await this.addBlacklist(client.id, config.block_duration);
|
|
869
|
+
|
|
870
|
+
// 如果配置了同时拉黑IP,也拉黑IP
|
|
871
|
+
if (config.password_block_ip) {
|
|
872
|
+
await this.addIpToBanlist(clientIp, config.block_duration);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
$.log.warn(`用户名 ${username} 密码错误次数达到上限(${config.max_attempts}次),已拉黑客户端 ${client.id} 和IP ${clientIp},时长 ${config.block_duration / 3600}小时`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
} catch (error) {
|
|
879
|
+
$.log.error(`记录客户端 ${client.id} 密码错误时出错:`, error);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
|
|
770
883
|
/**
|
|
771
884
|
* 运行
|
|
772
885
|
* @param {String} state 状态
|
|
@@ -823,9 +936,9 @@ MQTT.prototype.isClientBlocked = async function (clientId) {
|
|
|
823
936
|
* @param {Number} duration 拉黑时长(毫秒)
|
|
824
937
|
* @returns {Promise<Boolean>} true表示添加成功,false表示添加失败
|
|
825
938
|
*/
|
|
826
|
-
MQTT.prototype.
|
|
939
|
+
MQTT.prototype.addBlacklist = async function (clientId, duration) {
|
|
827
940
|
const now = Date.now();
|
|
828
|
-
const unblockTime = now + (duration || this.config.rate_limit.block_duration);
|
|
941
|
+
const unblockTime = now + (duration || this.config.rate_limit.block_duration) * 1000; // 转换为毫秒
|
|
829
942
|
|
|
830
943
|
try {
|
|
831
944
|
// 根据存储类型选择不同的实现方式
|
|
@@ -839,7 +952,7 @@ MQTT.prototype.addClientToBlacklist = async function (clientId, duration) {
|
|
|
839
952
|
this.blockedClients[clientId] = unblockTime;
|
|
840
953
|
}
|
|
841
954
|
|
|
842
|
-
$.log.warn(`MQTT客户端 ${clientId} 被添加到黑名单,限制时长: ${duration /
|
|
955
|
+
$.log.warn(`MQTT客户端 ${clientId} 被添加到黑名单,限制时长: ${(duration || this.config.rate_limit.block_duration) / 3600}小时`);
|
|
843
956
|
return true;
|
|
844
957
|
} catch (error) {
|
|
845
958
|
$.log.error(`添加客户端 ${clientId} 到黑名单时出错:`, error);
|
|
@@ -895,7 +1008,7 @@ MQTT.prototype.isIpBanned = async function (ip) {
|
|
|
895
1008
|
*/
|
|
896
1009
|
MQTT.prototype.addIpToBanlist = async function (ip, duration) {
|
|
897
1010
|
const now = Date.now();
|
|
898
|
-
const unbanTime = now + (duration || this.config.
|
|
1011
|
+
const unbanTime = now + (duration || this.config.reconnect_ip_duration) * 1000; // 转换为毫秒
|
|
899
1012
|
|
|
900
1013
|
try {
|
|
901
1014
|
// 根据存储类型选择不同的实现方式
|
|
@@ -909,7 +1022,7 @@ MQTT.prototype.addIpToBanlist = async function (ip, duration) {
|
|
|
909
1022
|
this.bannedIps[ip] = unbanTime;
|
|
910
1023
|
}
|
|
911
1024
|
|
|
912
|
-
$.log.warn(`IP ${ip} 被添加到禁止列表,限制时长: ${duration /
|
|
1025
|
+
$.log.warn(`IP ${ip} 被添加到禁止列表,限制时长: ${(duration || this.config.reconnect_ip_duration) / 3600}小时`);
|
|
913
1026
|
return true;
|
|
914
1027
|
} catch (error) {
|
|
915
1028
|
$.log.error(`添加IP ${ip} 到禁止列表时出错:`, error);
|
|
@@ -917,4 +1030,78 @@ MQTT.prototype.addIpToBanlist = async function (ip, duration) {
|
|
|
917
1030
|
}
|
|
918
1031
|
};
|
|
919
1032
|
|
|
1033
|
+
/**
|
|
1034
|
+
* 异步从黑名单中移出客户端
|
|
1035
|
+
* @param {String} clientId 客户端ID
|
|
1036
|
+
* @returns {Promise<Boolean>} true表示移出成功,false表示移出失败
|
|
1037
|
+
*/
|
|
1038
|
+
MQTT.prototype.removeBlacklist = async function (clientId) {
|
|
1039
|
+
try {
|
|
1040
|
+
// 根据存储类型选择不同的实现方式
|
|
1041
|
+
if (this.blacklistStorageType === 'memory') {
|
|
1042
|
+
// 内存存储方式
|
|
1043
|
+
if (this.blockedClients[clientId]) {
|
|
1044
|
+
delete this.blockedClients[clientId];
|
|
1045
|
+
$.log.info(`MQTT客户端 ${clientId} 已从黑名单中移出`);
|
|
1046
|
+
return true;
|
|
1047
|
+
} else {
|
|
1048
|
+
$.log.info(`MQTT客户端 ${clientId} 不在黑名单中`);
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
1053
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
1054
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
1055
|
+
if (this.blockedClients[clientId]) {
|
|
1056
|
+
delete this.blockedClients[clientId];
|
|
1057
|
+
$.log.info(`MQTT客户端 ${clientId} 已从黑名单中移出`);
|
|
1058
|
+
return true;
|
|
1059
|
+
} else {
|
|
1060
|
+
$.log.info(`MQTT客户端 ${clientId} 不在黑名单中`);
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
$.log.error(`从黑名单中移出客户端 ${clientId} 时出错:`, error);
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* 异步从禁止列表中移出IP
|
|
1072
|
+
* @param {String} ip IP地址
|
|
1073
|
+
* @returns {Promise<Boolean>} true表示移出成功,false表示移出失败
|
|
1074
|
+
*/
|
|
1075
|
+
MQTT.prototype.removeIpFromBanlist = async function (ip) {
|
|
1076
|
+
try {
|
|
1077
|
+
// 根据存储类型选择不同的实现方式
|
|
1078
|
+
if (this.blacklistStorageType === 'memory') {
|
|
1079
|
+
// 内存存储方式
|
|
1080
|
+
if (this.bannedIps[ip]) {
|
|
1081
|
+
delete this.bannedIps[ip];
|
|
1082
|
+
$.log.info(`IP ${ip} 已从禁止列表中移出`);
|
|
1083
|
+
return true;
|
|
1084
|
+
} else {
|
|
1085
|
+
$.log.info(`IP ${ip} 不在禁止列表中`);
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
} else {
|
|
1089
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
1090
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
1091
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
1092
|
+
if (this.bannedIps[ip]) {
|
|
1093
|
+
delete this.bannedIps[ip];
|
|
1094
|
+
$.log.info(`IP ${ip} 已从禁止列表中移出`);
|
|
1095
|
+
return true;
|
|
1096
|
+
} else {
|
|
1097
|
+
$.log.info(`IP ${ip} 不在禁止列表中`);
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
$.log.error(`从禁止列表中移出IP ${ip} 时出错:`, error);
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
|
|
920
1107
|
module.exports = MQTT;
|