mm_os 3.2.9 → 3.3.1
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 +1109 -1106
- package/core/base/web/index.js +245 -156
- package/core/com/event/README.md +4 -4
- package/core/com/event/com.json +3 -3
- package/core/com/event/config.tpl.json +18 -18
- package/core/com/event/drive.js +132 -132
- package/core/com/event/index.js +344 -344
- package/core/com/event/script.js +25 -25
- package/core/com/middleware/com.js +152 -151
- 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/core/com/static/index.js +1 -1
- package/index.js +34 -5
- package/middleware/cors/index.js +112 -96
- package/middleware/cors/middleware.json +18 -7
- package/middleware/csrf/index.js +202 -0
- package/middleware/csrf/middleware.json +24 -0
- package/middleware/ip_firewall/index.js +476 -0
- package/middleware/ip_firewall/middleware.json +109 -0
- package/middleware/mqtt_base/middleware.json +2 -1
- package/middleware/security_audit/index.js +543 -0
- package/middleware/security_audit/middleware.json +48 -0
- package/middleware/waf/index.js +273 -7
- 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_xss/index.js +269 -0
- package/middleware/waf_xss/middleware.json +18 -0
- package/middleware/web_after/middleware.json +2 -1
- package/middleware/web_base/middleware.json +2 -1
- package/middleware/web_before/middleware.json +3 -2
- package/middleware/web_check/middleware.json +2 -1
- package/middleware/web_main/middleware.json +2 -1
- package/middleware/web_proxy/middleware.json +2 -1
- package/middleware/web_render/middleware.json +2 -1
- package/middleware/web_socket/middleware.json +4 -3
- package/middleware/web_static/middleware.json +2 -1
- package/package.json +28 -15
- package/middleware/log/index.js +0 -32
- package/middleware/log/middleware.json +0 -9
- package/middleware/performance/index.js +0 -143
- package/middleware/performance/middleware.json +0 -16
- package/middleware/rate_limit/index.js +0 -112
- package/middleware/rate_limit/middleware.json +0 -10
- package/middleware/waf_ip/index.js +0 -168
- package/middleware/waf_ip/middleware.json +0 -10
- package/nodemon.json +0 -31
- package/package.txt +0 -1
- package/rps.bat +0 -3
- package/test.js +0 -10
- 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
|
@@ -1,1107 +1,1110 @@
|
|
|
1
|
-
require('./lib.js');
|
|
2
|
-
const mosca = require('mosca');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* mqtt 物联网通讯类
|
|
6
|
-
*/
|
|
7
|
-
class MQTT {
|
|
8
|
-
/**
|
|
9
|
-
* 构造函数
|
|
10
|
-
* @param {Object} config 配置参数
|
|
11
|
-
*/
|
|
12
|
-
constructor(config) {
|
|
13
|
-
this.config = Object.assign({
|
|
14
|
-
"state": true,
|
|
15
|
-
// mqtt访问端口号
|
|
16
|
-
"socket_port": 1883,
|
|
17
|
-
// 服务端
|
|
18
|
-
"http_host": "localhost",
|
|
19
|
-
// websocket 访问端口
|
|
20
|
-
"http_port": 8083,
|
|
21
|
-
// 缓存方式
|
|
22
|
-
"cache": "mongodb", // "mongodb",
|
|
23
|
-
// 消息保留时长(秒)
|
|
24
|
-
"message_retention": 86400,
|
|
25
|
-
// 消息频率限制配置
|
|
26
|
-
"rate_limit": {
|
|
27
|
-
// 时间窗口(秒)
|
|
28
|
-
"time_window": 60, // 默认1分钟
|
|
29
|
-
// 时间窗口内允许的最大消息数
|
|
30
|
-
"max_messages": 100,
|
|
31
|
-
// 拉黑时长(秒)
|
|
32
|
-
"block_duration": 3600 // 人脸识别门禁优化:拉黑30分钟(更合理的临时限制时长)
|
|
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
|
|
45
|
-
// 受限主题列表,用于限制某些敏感主题只能由特定客户端订阅
|
|
46
|
-
restrictedTopics: [],
|
|
47
|
-
// 允许订阅受限主题的客户端列表
|
|
48
|
-
allowedClients: [],
|
|
49
|
-
// 敏感主题列表,用于限制某些敏感主题只能由管理员发布
|
|
50
|
-
sensitiveTopics: [],
|
|
51
|
-
redis: {
|
|
52
|
-
host: "localhost",
|
|
53
|
-
port: 6379,
|
|
54
|
-
password: "asd159357",
|
|
55
|
-
db: 12
|
|
56
|
-
},
|
|
57
|
-
mongodb: {
|
|
58
|
-
user: "",
|
|
59
|
-
password: "",
|
|
60
|
-
host: "localhost",
|
|
61
|
-
port: 27017,
|
|
62
|
-
database: "mqtt"
|
|
63
|
-
}
|
|
64
|
-
}, config);
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* MQTT服务器
|
|
68
|
-
*/
|
|
69
|
-
this.server = null;
|
|
70
|
-
|
|
71
|
-
this.list = [];
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 客户端消息计数器
|
|
75
|
-
* 格式: { clientId: { count: 消息数, lastReset: 上次重置时间戳 } }
|
|
76
|
-
*/
|
|
77
|
-
this.messageCounters = {};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 拉黑的客户端列表
|
|
81
|
-
* 格式: { clientId: 解除拉黑时间戳 }
|
|
82
|
-
*/
|
|
83
|
-
this.blockedClients = {};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 被禁止的IP列表
|
|
87
|
-
* 格式: { ip: 解除禁止时间戳 }
|
|
88
|
-
*/
|
|
89
|
-
this.bannedIps = {};
|
|
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
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 黑名单存储类型
|
|
105
|
-
* 可选值: 'memory', 'redis', 'mongodb', 'mysql'
|
|
106
|
-
*/
|
|
107
|
-
this.blacklistStorageType = this.config.blacklistStorageType || 'memory';
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 异步客户端连接成功
|
|
113
|
-
* @param {Object} client 客户端信息
|
|
114
|
-
* @returns {Promise<Boolean>} true表示连接成功,false表示连接被拒绝
|
|
115
|
-
*/
|
|
116
|
-
MQTT.prototype.connected = async function
|
|
117
|
-
try {
|
|
118
|
-
// 检测是否有异常重连行为 - 现在是异步函数
|
|
119
|
-
const hasReconnectAbuse = await this.detectReconnectAbuse(client);
|
|
120
|
-
if (hasReconnectAbuse) {
|
|
121
|
-
$.log.warn(`拒绝客户端 ${client.id} 连接:检测到异常重连行为`);
|
|
122
|
-
// 断开客户端连接
|
|
123
|
-
if (client && client.close) {
|
|
124
|
-
try {
|
|
125
|
-
client.close();
|
|
126
|
-
} catch (error) {
|
|
127
|
-
$.log.error(`断开异常重连客户端 ${client.id} 连接时出错:`, error);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 正常连接
|
|
134
|
-
$.log.info('client connected', client.id);
|
|
135
|
-
return true;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
$.log.error(`处理客户端 ${client.id} 连接时出错:`, error);
|
|
138
|
-
// 出错时默认拒绝连接
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 初始化
|
|
145
|
-
* @param {Object} config
|
|
146
|
-
*/
|
|
147
|
-
MQTT.prototype.init = function
|
|
148
|
-
if (config) {
|
|
149
|
-
this.config = Object.assign(this.config, config);
|
|
150
|
-
}
|
|
151
|
-
var cg = Object.assign({}, this.config);
|
|
152
|
-
var {
|
|
153
|
-
redis,
|
|
154
|
-
mongodb
|
|
155
|
-
} = cg;
|
|
156
|
-
var conf = {
|
|
157
|
-
port: cg.socket_port,
|
|
158
|
-
http: {
|
|
159
|
-
host: cg.http_host || "localhost",
|
|
160
|
-
port: cg.http_port,
|
|
161
|
-
bundle: true,
|
|
162
|
-
static: './'
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (cg.cache === 'redis') {
|
|
166
|
-
if (cg.redis) {
|
|
167
|
-
conf.backend = {
|
|
168
|
-
type: "redis",
|
|
169
|
-
redis: require('redis'),
|
|
170
|
-
host: redis.host || "localhost",
|
|
171
|
-
port: redis.port || 6379,
|
|
172
|
-
password: redis.password,
|
|
173
|
-
db: redis.db || 12,
|
|
174
|
-
return_buffers: true
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
conf.backend = {
|
|
178
|
-
type: "redis",
|
|
179
|
-
redis: require('redis'),
|
|
180
|
-
host: "localhost",
|
|
181
|
-
password: "asd159357",
|
|
182
|
-
port: 6379,
|
|
183
|
-
db: 12,
|
|
184
|
-
return_buffers: true
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// 数据持久化参数设置
|
|
188
|
-
conf.persistence = Object.assign({
|
|
189
|
-
factory: mosca.persistence.Redis,
|
|
190
|
-
ttl: {
|
|
191
|
-
// TTL for subscriptions
|
|
192
|
-
subscriptions: cg.message_retention * 1000, // 恢复为毫秒
|
|
193
|
-
// TTL for packets
|
|
194
|
-
packets: cg.message_retention * 1000, // 恢复为毫秒
|
|
195
|
-
}
|
|
196
|
-
}, conf.backend);
|
|
197
|
-
} else if (cg.cache === 'mongodb' || cg.cache === 'mongo') {
|
|
198
|
-
var url = `mongodb://${mongodb.host}:${mongodb.port}/${mongodb.database}`;
|
|
199
|
-
if (mongodb.user) {
|
|
200
|
-
url =
|
|
201
|
-
`mongodb://${mongodb.user}:${mongodb.password}@${mongodb.host}:${mongodb.port}/${mongodb.database}`;
|
|
202
|
-
}
|
|
203
|
-
conf.backend = {
|
|
204
|
-
// 增加了此项
|
|
205
|
-
type: 'mongo',
|
|
206
|
-
url,
|
|
207
|
-
pubsubCollection: 'ascoltatori',
|
|
208
|
-
mongo: {}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// 数据持久化参数设置
|
|
212
|
-
conf.persistence = Object.assign({
|
|
213
|
-
factory: mosca.persistence.Mongo,
|
|
214
|
-
ttl: {
|
|
215
|
-
// TTL for subscriptions
|
|
216
|
-
subscriptions: cg.message_retention * 1000, // 恢复为毫秒
|
|
217
|
-
// TTL for packets
|
|
218
|
-
packets: cg.message_retention * 1000, // 恢复为毫秒
|
|
219
|
-
}
|
|
220
|
-
}, conf.backend);
|
|
221
|
-
}
|
|
222
|
-
this.server = new mosca.Server(conf);
|
|
223
|
-
return this;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 引用
|
|
228
|
-
* @param {Function} 函数
|
|
229
|
-
*/
|
|
230
|
-
MQTT.prototype.use = function
|
|
231
|
-
// this.server.use(func);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 运行主程序
|
|
236
|
-
* @param {String} state 状态
|
|
237
|
-
*/
|
|
238
|
-
MQTT.prototype.main = function
|
|
239
|
-
var cg = this.config;
|
|
240
|
-
var sr = this.server;
|
|
241
|
-
|
|
242
|
-
var _this = this;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 对服务器端口进行配置,在此端口进行监听
|
|
246
|
-
* @param {Object} client 客户端信息
|
|
247
|
-
*/
|
|
248
|
-
sr.on('clientConnected', async function
|
|
249
|
-
return await _this.connected(client);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 监听MQTT主题消息
|
|
254
|
-
* @param {Object} packet 订阅消息
|
|
255
|
-
* @param {Object} client 客户端信息
|
|
256
|
-
*/
|
|
257
|
-
sr.on('published', async function
|
|
258
|
-
// 检查客户端是否高频请求
|
|
259
|
-
if (client) {
|
|
260
|
-
try {
|
|
261
|
-
const isHighFrequency = await _this.checkHighFrequency(client);
|
|
262
|
-
if (isHighFrequency) {
|
|
263
|
-
// 高频请求,拉黑并断开连接
|
|
264
|
-
await _this.blockClient(client);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
} catch (error) {
|
|
268
|
-
$.log.error(`检查客户端高频请求时出错:`, error);
|
|
269
|
-
// 发生错误时,默认拒绝请求
|
|
270
|
-
if (client && client.close) {
|
|
271
|
-
client.close();
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
_this.published(packet, client);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* 监听MQTT主题消息
|
|
281
|
-
* @param {String} topic 订阅主题
|
|
282
|
-
* @param {Object} client 客户端信息
|
|
283
|
-
*/
|
|
284
|
-
sr.on('subscribed', function
|
|
285
|
-
_this.subscribed(topic, client);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* 身份验证
|
|
290
|
-
*/
|
|
291
|
-
sr.authenticate = (client, username, password, callback) => {
|
|
292
|
-
// 正确获取客户端IP地址
|
|
293
|
-
const clientIp = client.connection && client.connection.stream ? client.connection.stream
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// auth
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
*
|
|
348
|
-
* @param {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*
|
|
373
|
-
* @param {Object}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
*
|
|
423
|
-
* @
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
*
|
|
453
|
-
* @param {
|
|
454
|
-
* @param {String}
|
|
455
|
-
* @
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
*
|
|
476
|
-
* @param {
|
|
477
|
-
* @param {String}
|
|
478
|
-
* @param {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
var
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
else {
|
|
502
|
-
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:密码错误,用户名: ${username}`);
|
|
503
|
-
// 记录密码错误尝试
|
|
504
|
-
await this.recordPasswordFailure(client, username);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
var
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
*
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
this.messageCounters[clientId].count
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
*
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
*
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
*
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
await this.
|
|
890
|
-
await this.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
*
|
|
896
|
-
* @
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
let
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
//
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
*
|
|
936
|
-
* @param {
|
|
937
|
-
* @
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
//
|
|
951
|
-
|
|
952
|
-
this.
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
return
|
|
960
|
-
}
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
*
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
//
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
$.log.
|
|
1029
|
-
return
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
$.log.info(`MQTT客户端 ${clientId}
|
|
1049
|
-
return
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
$.log.info(`MQTT客户端 ${clientId}
|
|
1061
|
-
return
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
$.log.info(`IP ${ip}
|
|
1086
|
-
return
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
$.log.info(`IP ${ip}
|
|
1098
|
-
return
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
}
|
|
1105
|
-
};
|
|
1106
|
-
|
|
1
|
+
require('./lib.js');
|
|
2
|
+
const mosca = require('mosca');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mqtt 物联网通讯类
|
|
6
|
+
*/
|
|
7
|
+
class MQTT {
|
|
8
|
+
/**
|
|
9
|
+
* 构造函数
|
|
10
|
+
* @param {Object} config 配置参数
|
|
11
|
+
*/
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = Object.assign({
|
|
14
|
+
"state": true,
|
|
15
|
+
// mqtt访问端口号
|
|
16
|
+
"socket_port": 1883,
|
|
17
|
+
// 服务端
|
|
18
|
+
"http_host": "localhost",
|
|
19
|
+
// websocket 访问端口
|
|
20
|
+
"http_port": 8083,
|
|
21
|
+
// 缓存方式
|
|
22
|
+
"cache": "mongodb", // "mongodb",
|
|
23
|
+
// 消息保留时长(秒)
|
|
24
|
+
"message_retention": 86400,
|
|
25
|
+
// 消息频率限制配置
|
|
26
|
+
"rate_limit": {
|
|
27
|
+
// 时间窗口(秒)
|
|
28
|
+
"time_window": 60, // 默认1分钟
|
|
29
|
+
// 时间窗口内允许的最大消息数
|
|
30
|
+
"max_messages": 100, // 人脸识别门禁优化:1分钟内最多20条消息(门禁通常每次识别仅发送1-3条消息)
|
|
31
|
+
// 拉黑时长(秒)
|
|
32
|
+
"block_duration": 3600 // 人脸识别门禁优化:拉黑30分钟(更合理的临时限制时长)
|
|
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
|
|
45
|
+
// 受限主题列表,用于限制某些敏感主题只能由特定客户端订阅
|
|
46
|
+
restrictedTopics: [],
|
|
47
|
+
// 允许订阅受限主题的客户端列表
|
|
48
|
+
allowedClients: [],
|
|
49
|
+
// 敏感主题列表,用于限制某些敏感主题只能由管理员发布
|
|
50
|
+
sensitiveTopics: [],
|
|
51
|
+
redis: {
|
|
52
|
+
host: "localhost",
|
|
53
|
+
port: 6379,
|
|
54
|
+
password: "asd159357",
|
|
55
|
+
db: 12
|
|
56
|
+
},
|
|
57
|
+
mongodb: {
|
|
58
|
+
user: "",
|
|
59
|
+
password: "",
|
|
60
|
+
host: "localhost",
|
|
61
|
+
port: 27017,
|
|
62
|
+
database: "mqtt"
|
|
63
|
+
}
|
|
64
|
+
}, config);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* MQTT服务器
|
|
68
|
+
*/
|
|
69
|
+
this.server = null;
|
|
70
|
+
|
|
71
|
+
this.list = [];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 客户端消息计数器
|
|
75
|
+
* 格式: { clientId: { count: 消息数, lastReset: 上次重置时间戳 } }
|
|
76
|
+
*/
|
|
77
|
+
this.messageCounters = {};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 拉黑的客户端列表
|
|
81
|
+
* 格式: { clientId: 解除拉黑时间戳 }
|
|
82
|
+
*/
|
|
83
|
+
this.blockedClients = {};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 被禁止的IP列表
|
|
87
|
+
* 格式: { ip: 解除禁止时间戳 }
|
|
88
|
+
*/
|
|
89
|
+
this.bannedIps = {};
|
|
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
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 黑名单存储类型
|
|
105
|
+
* 可选值: 'memory', 'redis', 'mongodb', 'mysql'
|
|
106
|
+
*/
|
|
107
|
+
this.blacklistStorageType = this.config.blacklistStorageType || 'memory';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 异步客户端连接成功
|
|
113
|
+
* @param {Object} client 客户端信息
|
|
114
|
+
* @returns {Promise<Boolean>} true表示连接成功,false表示连接被拒绝
|
|
115
|
+
*/
|
|
116
|
+
MQTT.prototype.connected = async function(client) {
|
|
117
|
+
try {
|
|
118
|
+
// 检测是否有异常重连行为 - 现在是异步函数
|
|
119
|
+
const hasReconnectAbuse = await this.detectReconnectAbuse(client);
|
|
120
|
+
if (hasReconnectAbuse) {
|
|
121
|
+
$.log.warn(`拒绝客户端 ${client.id} 连接:检测到异常重连行为`);
|
|
122
|
+
// 断开客户端连接
|
|
123
|
+
if (client && client.close) {
|
|
124
|
+
try {
|
|
125
|
+
client.close();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
$.log.error(`断开异常重连客户端 ${client.id} 连接时出错:`, error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 正常连接
|
|
134
|
+
$.log.info('client connected', client.id);
|
|
135
|
+
return true;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
$.log.error(`处理客户端 ${client.id} 连接时出错:`, error);
|
|
138
|
+
// 出错时默认拒绝连接
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 初始化
|
|
145
|
+
* @param {Object} config
|
|
146
|
+
*/
|
|
147
|
+
MQTT.prototype.init = function(config) {
|
|
148
|
+
if (config) {
|
|
149
|
+
this.config = Object.assign(this.config, config);
|
|
150
|
+
}
|
|
151
|
+
var cg = Object.assign({}, this.config);
|
|
152
|
+
var {
|
|
153
|
+
redis,
|
|
154
|
+
mongodb
|
|
155
|
+
} = cg;
|
|
156
|
+
var conf = {
|
|
157
|
+
port: cg.socket_port,
|
|
158
|
+
http: {
|
|
159
|
+
host: cg.http_host || "localhost",
|
|
160
|
+
port: cg.http_port,
|
|
161
|
+
bundle: true,
|
|
162
|
+
static: './'
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (cg.cache === 'redis') {
|
|
166
|
+
if (cg.redis) {
|
|
167
|
+
conf.backend = {
|
|
168
|
+
type: "redis",
|
|
169
|
+
redis: require('redis'),
|
|
170
|
+
host: redis.host || "localhost",
|
|
171
|
+
port: redis.port || 6379,
|
|
172
|
+
password: redis.password,
|
|
173
|
+
db: redis.db || 12,
|
|
174
|
+
return_buffers: true
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
conf.backend = {
|
|
178
|
+
type: "redis",
|
|
179
|
+
redis: require('redis'),
|
|
180
|
+
host: "localhost",
|
|
181
|
+
password: "asd159357",
|
|
182
|
+
port: 6379,
|
|
183
|
+
db: 12,
|
|
184
|
+
return_buffers: true
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// 数据持久化参数设置
|
|
188
|
+
conf.persistence = Object.assign({
|
|
189
|
+
factory: mosca.persistence.Redis,
|
|
190
|
+
ttl: {
|
|
191
|
+
// TTL for subscriptions
|
|
192
|
+
subscriptions: cg.message_retention * 1000, // 恢复为毫秒
|
|
193
|
+
// TTL for packets
|
|
194
|
+
packets: cg.message_retention * 1000, // 恢复为毫秒
|
|
195
|
+
}
|
|
196
|
+
}, conf.backend);
|
|
197
|
+
} else if (cg.cache === 'mongodb' || cg.cache === 'mongo') {
|
|
198
|
+
var url = `mongodb://${mongodb.host}:${mongodb.port}/${mongodb.database}`;
|
|
199
|
+
if (mongodb.user) {
|
|
200
|
+
url =
|
|
201
|
+
`mongodb://${mongodb.user}:${mongodb.password}@${mongodb.host}:${mongodb.port}/${mongodb.database}`;
|
|
202
|
+
}
|
|
203
|
+
conf.backend = {
|
|
204
|
+
// 增加了此项
|
|
205
|
+
type: 'mongo',
|
|
206
|
+
url,
|
|
207
|
+
pubsubCollection: 'ascoltatori',
|
|
208
|
+
mongo: {}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// 数据持久化参数设置
|
|
212
|
+
conf.persistence = Object.assign({
|
|
213
|
+
factory: mosca.persistence.Mongo,
|
|
214
|
+
ttl: {
|
|
215
|
+
// TTL for subscriptions
|
|
216
|
+
subscriptions: cg.message_retention * 1000, // 恢复为毫秒
|
|
217
|
+
// TTL for packets
|
|
218
|
+
packets: cg.message_retention * 1000, // 恢复为毫秒
|
|
219
|
+
}
|
|
220
|
+
}, conf.backend);
|
|
221
|
+
}
|
|
222
|
+
this.server = new mosca.Server(conf);
|
|
223
|
+
return this;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 引用
|
|
228
|
+
* @param {Function} 函数
|
|
229
|
+
*/
|
|
230
|
+
MQTT.prototype.use = function(func) {
|
|
231
|
+
// this.server.use(func);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 运行主程序
|
|
236
|
+
* @param {String} state 状态
|
|
237
|
+
*/
|
|
238
|
+
MQTT.prototype.main = function(state) {
|
|
239
|
+
var cg = this.config;
|
|
240
|
+
var sr = this.server;
|
|
241
|
+
|
|
242
|
+
var _this = this;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 对服务器端口进行配置,在此端口进行监听
|
|
246
|
+
* @param {Object} client 客户端信息
|
|
247
|
+
*/
|
|
248
|
+
sr.on('clientConnected', async function(client) {
|
|
249
|
+
return await _this.connected(client);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 监听MQTT主题消息
|
|
254
|
+
* @param {Object} packet 订阅消息
|
|
255
|
+
* @param {Object} client 客户端信息
|
|
256
|
+
*/
|
|
257
|
+
sr.on('published', async function(packet, client) {
|
|
258
|
+
// 检查客户端是否高频请求
|
|
259
|
+
if (client) {
|
|
260
|
+
try {
|
|
261
|
+
const isHighFrequency = await _this.checkHighFrequency(client);
|
|
262
|
+
if (isHighFrequency) {
|
|
263
|
+
// 高频请求,拉黑并断开连接
|
|
264
|
+
await _this.blockClient(client);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
$.log.error(`检查客户端高频请求时出错:`, error);
|
|
269
|
+
// 发生错误时,默认拒绝请求
|
|
270
|
+
if (client && client.close) {
|
|
271
|
+
client.close();
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
_this.published(packet, client);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 监听MQTT主题消息
|
|
281
|
+
* @param {String} topic 订阅主题
|
|
282
|
+
* @param {Object} client 客户端信息
|
|
283
|
+
*/
|
|
284
|
+
sr.on('subscribed', function(topic, client) {
|
|
285
|
+
_this.subscribed(topic, client);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 身份验证
|
|
290
|
+
*/
|
|
291
|
+
sr.authenticate = (client, username, password, callback) => {
|
|
292
|
+
// 正确获取客户端IP地址
|
|
293
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream
|
|
294
|
+
.remoteAddress || 'unknown' : 'unknown';
|
|
295
|
+
$.log.info("收到身份验证", clientIp, client.id, username);
|
|
296
|
+
// 因为auth现在是异步函数,需要使用async/await处理
|
|
297
|
+
(async () => {
|
|
298
|
+
try {
|
|
299
|
+
// 调用异步的auth函数,但仍然使用callback返回结果
|
|
300
|
+
// auth函数内部已经处理了callback调用
|
|
301
|
+
await this.auth(client, username, password, callback);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
$.log.error(`身份验证过程发生异常:`, error);
|
|
304
|
+
callback(null, false);
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
// 返回true表示继续处理
|
|
308
|
+
return true;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 验证发布,决定客户端可以发布哪些主题
|
|
313
|
+
*/
|
|
314
|
+
sr.authorizePublish = (client, topic, payload, callback) => {
|
|
315
|
+
const isAllowed = this.authPublish(client, topic, payload);
|
|
316
|
+
callback(null, isAllowed);
|
|
317
|
+
return true;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 验证订阅,决定客户端可以订阅哪些主题
|
|
322
|
+
*/
|
|
323
|
+
sr.authorizeSubscribe = (client, topic, callback) => {
|
|
324
|
+
const isAllowed = this.authSubscribe(client, topic);
|
|
325
|
+
callback(null, isAllowed);
|
|
326
|
+
return true;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 当服务开启时
|
|
332
|
+
*/
|
|
333
|
+
sr.on('ready', function() {
|
|
334
|
+
// 当服务开启时
|
|
335
|
+
$.log.info(`MQTT访问 mqtt://127.0.0.1:${cg.socket_port} || ws://127.0.0.1:${cg.http_port}`);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 监听客户端断开连接
|
|
340
|
+
*/
|
|
341
|
+
sr.on('clientDisconnected', async function(client) {
|
|
342
|
+
_this.clientDisconnected(client);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 收到订阅时
|
|
348
|
+
* @param {String} topic 订阅主题
|
|
349
|
+
* @param {Object} client 客户端信息
|
|
350
|
+
*/
|
|
351
|
+
MQTT.prototype.subscribed = function(topic, client) {
|
|
352
|
+
// console.log("订阅", topic, client.id);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 客户端断开连接时的处理
|
|
357
|
+
* @param {Object} client 客户端对象
|
|
358
|
+
*/
|
|
359
|
+
MQTT.prototype.clientDisconnected = function(client) {
|
|
360
|
+
const clientId = client.id;
|
|
361
|
+
const now = Date.now();
|
|
362
|
+
|
|
363
|
+
$.log.info(`MQTT客户端 ${clientId} 断开连接`);
|
|
364
|
+
|
|
365
|
+
// 更新客户端断开连接时间
|
|
366
|
+
if (this.securityRecords.client[clientId]) {
|
|
367
|
+
this.securityRecords.client[clientId].disconnectTime = now;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 收到推送消息时
|
|
373
|
+
* @param {Object} packet 订阅的消息
|
|
374
|
+
* @param {Object} client 客户端信息
|
|
375
|
+
*/
|
|
376
|
+
MQTT.prototype.published = function(packet, client) {
|
|
377
|
+
// console.log("发布", packet.topic, packet.payload.toString());
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 身份验证 - 主要入口函数
|
|
382
|
+
* @param {Object} client 客户端
|
|
383
|
+
* @param {String} username 用户名
|
|
384
|
+
* @param {String} password 密码
|
|
385
|
+
* @param {Function} callback 回调函数,回调返回true,则表示验证通过。
|
|
386
|
+
*/
|
|
387
|
+
MQTT.prototype.auth = async function(client, username, password, callback) {
|
|
388
|
+
try {
|
|
389
|
+
// 检查客户端状态(拉黑、异常重连等)- 现在是异步操作
|
|
390
|
+
const clientStatusResult = await this.checkClientStatus(client);
|
|
391
|
+
if (clientStatusResult === false) {
|
|
392
|
+
callback(null, false);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 密码可能是Buffer类型,需要转换为字符串
|
|
397
|
+
const passwordStr = password ? password.toString() : '';
|
|
398
|
+
|
|
399
|
+
// 验证凭证格式
|
|
400
|
+
const credentialsResult = this.validateCredentials(client, username, passwordStr);
|
|
401
|
+
if (credentialsResult === false) {
|
|
402
|
+
callback(null, false);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 检查密码错误次数限制(防暴力破解)
|
|
407
|
+
const passwordLimitResult = await this.checkPasswordAttemptLimit(client, username);
|
|
408
|
+
if (passwordLimitResult === false) {
|
|
409
|
+
callback(null, false);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 查询用户密码并验证(异步操作,需要callback)
|
|
414
|
+
this.checkAccount(client, username, passwordStr, callback);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
$.log.error(`MQTT客户端 ${client.id} 身份验证过程中出错:`, error);
|
|
417
|
+
callback(null, false);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* 异步检查客户端状态(拉黑、异常重连等)
|
|
423
|
+
* @param {Object} client 客户端
|
|
424
|
+
* @returns {Promise<Boolean>} true表示客户端状态正常,false表示客户端状态异常
|
|
425
|
+
*/
|
|
426
|
+
MQTT.prototype.checkClientStatus = async function(client) {
|
|
427
|
+
try {
|
|
428
|
+
// 检查客户端是否被拉黑(使用新的异步函数)
|
|
429
|
+
const isBlocked = await this.isClientBlocked(client.id);
|
|
430
|
+
if (isBlocked) {
|
|
431
|
+
$.log.warn(`MQTT客户端 ${client.id} 处于拉黑状态,拒绝身份验证`);
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 检测是否有异常重连行为
|
|
436
|
+
// 注意:detectReconnectAbuse内部也有黑名单相关操作,后续可以考虑重构为使用异步函数
|
|
437
|
+
const hasReconnectAbuse = await this.detectReconnectAbuse(client);
|
|
438
|
+
if (hasReconnectAbuse) {
|
|
439
|
+
$.log.warn(`拒绝客户端 ${client.id} 身份验证:检测到异常重连行为`);
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return true;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
$.log.error(`检查客户端 ${client.id} 状态时出错:`, error);
|
|
446
|
+
// 出错时默认视为客户端状态异常
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 验证凭证格式
|
|
453
|
+
* @param {Object} client 客户端
|
|
454
|
+
* @param {String} username 用户名
|
|
455
|
+
* @param {String} passwordStr 密码字符串
|
|
456
|
+
* @returns {Boolean} true表示凭证格式有效,false表示凭证格式无效
|
|
457
|
+
*/
|
|
458
|
+
MQTT.prototype.validateCredentials = function(client, username, passwordStr) {
|
|
459
|
+
// 检查必要参数
|
|
460
|
+
if (!username || !passwordStr) {
|
|
461
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:用户名或密码为空`);
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 检查是否有可用的MySQL连接
|
|
466
|
+
if (!$.mysql_admin) {
|
|
467
|
+
$.log.error(`MQTT客户端 ${client.id} 身份验证错误:MySQL连接不可用`);
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return true;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 查询用户密码并验证
|
|
476
|
+
* @param {Object} client 客户端
|
|
477
|
+
* @param {String} username 用户名
|
|
478
|
+
* @param {String} password 密码字符串
|
|
479
|
+
* @param {Function} callback 回调函数
|
|
480
|
+
*/
|
|
481
|
+
MQTT.prototype.checkAccount = async function(client, username, password, callback) {
|
|
482
|
+
if (!username || !password) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
var sql = $.mysql_admin('sys');
|
|
486
|
+
try {
|
|
487
|
+
var dbs = sql.db();
|
|
488
|
+
// 先尝试通过客户端ID和用户名精确匹配
|
|
489
|
+
|
|
490
|
+
// 判断设备连接
|
|
491
|
+
var db1 = dbs.new(this.config.table_name || 'face_device', "");
|
|
492
|
+
var obj = await db1.getObj({
|
|
493
|
+
username,
|
|
494
|
+
clientid: client.id
|
|
495
|
+
});
|
|
496
|
+
if (obj) {
|
|
497
|
+
if (obj.available && obj.state) {
|
|
498
|
+
if (obj.password == password) {
|
|
499
|
+
// 账号密码正确
|
|
500
|
+
return callback(null, true);
|
|
501
|
+
} else {
|
|
502
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:密码错误,用户名: ${username}`);
|
|
503
|
+
// 记录密码错误尝试
|
|
504
|
+
await this.recordPasswordFailure(client, username);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:账户不可用,用户名: ${username}`);
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
// 判断应用连接
|
|
511
|
+
var db2 = dbs.new("sys_app", "app_id");
|
|
512
|
+
var o = await db2.getObj({
|
|
513
|
+
appid: username
|
|
514
|
+
});
|
|
515
|
+
if (o) {
|
|
516
|
+
if (o.available) {
|
|
517
|
+
if (o.appsecret == password) {
|
|
518
|
+
// 账号密码正确
|
|
519
|
+
return callback(null, true);
|
|
520
|
+
} else {
|
|
521
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:密码错误,用户名: ${username}`);
|
|
522
|
+
// 记录密码错误尝试
|
|
523
|
+
await this.recordPasswordFailure(client, username);
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
$.log.warn(`MQTT客户端 ${client.id} 身份验证失败:账户不可用,用户名: ${username}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
$.log.error(`MQTT客户端 ${client.id} 身份验证数据库错误:`, err);
|
|
532
|
+
}
|
|
533
|
+
callback(null, false);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 验证发布,决定客户端可以发布哪些主题
|
|
538
|
+
* @param {Object} client 客户端
|
|
539
|
+
* @param {String} topic 主题
|
|
540
|
+
* @param {Object} payload 参数
|
|
541
|
+
* @returns {Boolean} true表示验证通过,false表示验证失败
|
|
542
|
+
*/
|
|
543
|
+
MQTT.prototype.authPublish = function(client, topic, payload) {
|
|
544
|
+
// 示例:可以基于客户端ID或用户名实现更细粒度的权限控制
|
|
545
|
+
// 在实际应用中,应该从数据库或配置中获取客户端的发布权限
|
|
546
|
+
let isAllowed = true;
|
|
547
|
+
|
|
548
|
+
// 例如:限制某些敏感主题只能由管理员发布
|
|
549
|
+
const isSensitiveTopic = (this.config.sensitiveTopics || []).some(sensitiveTopic =>
|
|
550
|
+
topic === sensitiveTopic || topic.startsWith(sensitiveTopic + '/')
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// 简单示例:检查客户端ID是否表示管理员客户端
|
|
554
|
+
const isAdminClient = client.id === 'admin' || client.id.startsWith('server_');
|
|
555
|
+
|
|
556
|
+
if (isSensitiveTopic && !isAdminClient) {
|
|
557
|
+
isAllowed = false;
|
|
558
|
+
$.log.warn(`MQTT客户端 ${client.id} 被拒绝发布到敏感主题: ${topic}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return isAllowed;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* 验证订阅,决定客户端可以订阅哪些主题
|
|
566
|
+
* @param {Object} client 客户端
|
|
567
|
+
* @param {String} topic 主题
|
|
568
|
+
* @returns {Boolean} true表示验证通过,false表示验证失败
|
|
569
|
+
*/
|
|
570
|
+
MQTT.prototype.authSubscribe = function(client, topic) {
|
|
571
|
+
// 示例:可以基于客户端ID或用户名实现更细粒度的权限控制
|
|
572
|
+
// 在实际应用中,应该从数据库或配置中获取客户端的订阅权限
|
|
573
|
+
let isAllowed = true;
|
|
574
|
+
|
|
575
|
+
// 例如:限制某些敏感主题只能由特定客户端订阅
|
|
576
|
+
const isRestrictedTopic = (this.config.restrictedTopics || []).some(restrictedTopic => {
|
|
577
|
+
// 处理通配符,例如:user/+/profile 匹配 user/123/profile, user/456/profile 等
|
|
578
|
+
const pattern = restrictedTopic.replace(/\+/g, '[^/]+');
|
|
579
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
580
|
+
return regex.test(topic);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// 简单示例:允许特定客户端订阅受限主题
|
|
584
|
+
const isAllowedClient = (this.config.allowedClients || []).includes(client.id);
|
|
585
|
+
|
|
586
|
+
if (isRestrictedTopic && !isAllowedClient) {
|
|
587
|
+
isAllowed = false;
|
|
588
|
+
$.log.warn(`MQTT客户端 ${client.id} 被拒绝订阅受限主题: ${topic}`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return isAllowed;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* 运行主程序前
|
|
596
|
+
* @param {String} state 状态
|
|
597
|
+
*/
|
|
598
|
+
MQTT.prototype.before = async function(state) {
|
|
599
|
+
var list = this.list;
|
|
600
|
+
for (var i = 0; i < list.length; i++) {
|
|
601
|
+
var o = list[i];
|
|
602
|
+
o.func = require(o.func_file);
|
|
603
|
+
if (o.func) {
|
|
604
|
+
o.func(this.server, this.config);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* 运行主程序后
|
|
611
|
+
* @param {String} state 状态
|
|
612
|
+
*/
|
|
613
|
+
MQTT.prototype.after = async function(state) {};
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 检查客户端是否高频请求
|
|
617
|
+
* @param {Object} client 客户端对象
|
|
618
|
+
* @returns {Boolean} true表示高频请求,false表示正常请求
|
|
619
|
+
*/
|
|
620
|
+
MQTT.prototype.checkHighFrequency = async function(client) {
|
|
621
|
+
const clientId = client.id;
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
const config = this.config.rate_limit;
|
|
624
|
+
|
|
625
|
+
// 检查是否已经被拉黑
|
|
626
|
+
try {
|
|
627
|
+
const isBlocked = await this.isClientBlocked(clientId);
|
|
628
|
+
if (isBlocked) {
|
|
629
|
+
$.log.warn(`MQTT客户端 ${clientId} 处于拉黑状态,拒绝请求`);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
$.log.error(`检查客户端 ${clientId} 拉黑状态时出错:`, error);
|
|
634
|
+
// 发生错误时,默认拒绝请求
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// 初始化或更新消息计数器
|
|
639
|
+
if (!this.messageCounters[clientId]) {
|
|
640
|
+
this.messageCounters[clientId] = {
|
|
641
|
+
count: 1,
|
|
642
|
+
lastReset: now
|
|
643
|
+
};
|
|
644
|
+
} else {
|
|
645
|
+
// 检查是否需要重置计数器(注意:time_window现在是秒,需要转换为毫秒进行比较)
|
|
646
|
+
if (now - this.messageCounters[clientId].lastReset > config.time_window * 1000) {
|
|
647
|
+
this.messageCounters[clientId].count = 1;
|
|
648
|
+
this.messageCounters[clientId].lastReset = now;
|
|
649
|
+
} else {
|
|
650
|
+
// 增加消息计数
|
|
651
|
+
this.messageCounters[clientId].count++;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// 检查是否超过频率限制
|
|
656
|
+
if (this.messageCounters[clientId].count > config.max_messages) {
|
|
657
|
+
// 拉黑客户端 - 使用新的异步函数
|
|
658
|
+
try {
|
|
659
|
+
await this.addBlacklist(clientId, config.block_duration);
|
|
660
|
+
} catch (error) {
|
|
661
|
+
$.log.error(`拉黑高频请求客户端 ${clientId} 时出错:`, error);
|
|
662
|
+
}
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 正常请求
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* 异步拉黑客户端并断开连接
|
|
672
|
+
* @param {Object} client 客户端对象
|
|
673
|
+
*/
|
|
674
|
+
MQTT.prototype.blockClient = async function(client) {
|
|
675
|
+
const clientId = client.id;
|
|
676
|
+
const now = Date.now();
|
|
677
|
+
const config = this.config.rate_limit;
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
// 使用新的异步函数添加到黑名单
|
|
681
|
+
await this.addBlacklist(clientId, config.block_duration);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
$.log.error(`拉黑MQTT客户端 ${clientId} 时出错:`, error);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 断开客户端连接
|
|
687
|
+
if (client && client.close) {
|
|
688
|
+
try {
|
|
689
|
+
client.close();
|
|
690
|
+
} catch (error) {
|
|
691
|
+
$.log.error(`断开MQTT客户端 ${clientId} 连接时出错:`, error);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* 异步检测客户端重连行为
|
|
698
|
+
* @param {Object} client 客户端对象
|
|
699
|
+
* @returns {Promise<Boolean>} true表示检测到异常重连,false表示正常连接
|
|
700
|
+
*/
|
|
701
|
+
MQTT.prototype.detectReconnectAbuse = async function(client) {
|
|
702
|
+
try {
|
|
703
|
+
const clientId = client.id;
|
|
704
|
+
const now = Date.now();
|
|
705
|
+
const config = this.config;
|
|
706
|
+
|
|
707
|
+
// 从client对象中获取IP
|
|
708
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream
|
|
709
|
+
.remoteAddress || 'unknown' : 'unknown';
|
|
710
|
+
|
|
711
|
+
// 检查IP是否被禁止
|
|
712
|
+
const isIpBanned = await this.isIpBanned(clientIp);
|
|
713
|
+
if (isIpBanned) {
|
|
714
|
+
$.log.warn(`IP ${clientIp} 处于禁止状态,拒绝连接请求`);
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 检查是否是首次连接
|
|
719
|
+
if (!this.securityRecords.client[clientId]) {
|
|
720
|
+
// 首次连接,初始化连接历史
|
|
721
|
+
this.securityRecords.client[clientId] = {
|
|
722
|
+
ip: clientIp,
|
|
723
|
+
connectCount: 1,
|
|
724
|
+
lastConnectTime: now,
|
|
725
|
+
disconnectTime: null,
|
|
726
|
+
reconnectAttempts: 0
|
|
727
|
+
};
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 获取客户端历史记录
|
|
732
|
+
const history = this.securityRecords.client[clientId];
|
|
733
|
+
|
|
734
|
+
// 检查是否是来自不同IP的连接
|
|
735
|
+
if (history.ip !== clientIp) {
|
|
736
|
+
$.log.warn(`检测到客户端ID ${clientId} 从不同IP连接: 原IP ${history.ip}, 新IP ${clientIp}`);
|
|
737
|
+
|
|
738
|
+
// 记录新IP的连接行为
|
|
739
|
+
const oldIp = history.ip;
|
|
740
|
+
history.ip = clientIp;
|
|
741
|
+
history.connectCount++;
|
|
742
|
+
history.lastConnectTime = now;
|
|
743
|
+
|
|
744
|
+
// 禁止新IP连接
|
|
745
|
+
try {
|
|
746
|
+
await this.addIpToBanlist(clientIp, config.reconnect_ip_duration);
|
|
747
|
+
} catch (error) {
|
|
748
|
+
$.log.error(`禁止IP ${clientIp} 时出错:`, error);
|
|
749
|
+
}
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 检查是否在短时间内频繁重连
|
|
754
|
+
const timeSinceLastConnect = now - history.lastConnectTime;
|
|
755
|
+
|
|
756
|
+
// 如果两次连接间隔很短,增加重连尝试计数
|
|
757
|
+
if (timeSinceLastConnect < config.reconnect_frequency_threshold * 1000) {
|
|
758
|
+
history.reconnectAttempts++;
|
|
759
|
+
} else {
|
|
760
|
+
// 重置重连计数
|
|
761
|
+
history.reconnectAttempts = 0;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 更新连接计数和时间
|
|
765
|
+
history.connectCount++;
|
|
766
|
+
history.lastConnectTime = now;
|
|
767
|
+
|
|
768
|
+
// 检查是否超过最大重连尝试次数
|
|
769
|
+
if (history.reconnectAttempts > config.max_attempts) {
|
|
770
|
+
$.log.warn(`客户端 ${clientId} 重连过于频繁,已尝试 ${history.reconnectAttempts} 次重连`);
|
|
771
|
+
|
|
772
|
+
// 禁止该客户端ID重连
|
|
773
|
+
try {
|
|
774
|
+
await this.addBlacklist(clientId, config.reconnect_client_duration);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
$.log.error(`禁止频繁重连客户端 ${clientId} 时出错:`, error);
|
|
777
|
+
}
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 正常连接
|
|
782
|
+
return false;
|
|
783
|
+
} catch (error) {
|
|
784
|
+
$.log.error(`检测客户端 ${client.id} 重连行为时出错:`, error);
|
|
785
|
+
// 出错时默认视为正常连接,避免误判
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* 异步检查密码尝试次数限制
|
|
792
|
+
* @param {Object} client 客户端
|
|
793
|
+
* @param {String} username 用户名
|
|
794
|
+
* @returns {Promise<Boolean>} true表示未超过限制,false表示已超过限制
|
|
795
|
+
*/
|
|
796
|
+
MQTT.prototype.checkPasswordAttemptLimit = async function(client, username) {
|
|
797
|
+
try {
|
|
798
|
+
const now = Date.now();
|
|
799
|
+
const config = this.config;
|
|
800
|
+
|
|
801
|
+
// 从client对象中获取IP
|
|
802
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream
|
|
803
|
+
.remoteAddress || 'unknown' : 'unknown';
|
|
804
|
+
|
|
805
|
+
// 检查用户名是否已被禁止(密码错误次数过多)
|
|
806
|
+
if (this.securityRecords.password[username]) {
|
|
807
|
+
const failureRecord = this.securityRecords.password[username];
|
|
808
|
+
const timeSinceFirstFailure = now - failureRecord.firstAttempt;
|
|
809
|
+
const timeSinceLastFailure = now - failureRecord.lastAttempt;
|
|
810
|
+
|
|
811
|
+
// 如果超过了时间窗口,重置错误计数
|
|
812
|
+
if (timeSinceFirstFailure > config.time_window * 1000) {
|
|
813
|
+
// 重置错误计数
|
|
814
|
+
delete this.securityRecords.password[username];
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// 检查错误次数是否超过限制
|
|
819
|
+
if (failureRecord.count >= config.max_attempts) {
|
|
820
|
+
// 记录被拒绝的尝试
|
|
821
|
+
$.log.warn(`拒绝客户端 ${client.id} (IP: ${clientIp}) 连接:用户名 ${username} 密码错误次数过多`);
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return true;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
$.log.error(`检查客户端 ${client.id} 密码尝试次数限制时出错:`, error);
|
|
829
|
+
// 出错时默认视为未超过限制,避免误判
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* 异步记录密码错误尝试
|
|
836
|
+
* @param {Object} client 客户端
|
|
837
|
+
* @param {String} username 用户名
|
|
838
|
+
*/
|
|
839
|
+
MQTT.prototype.recordPasswordFailure = async function(client, username) {
|
|
840
|
+
try {
|
|
841
|
+
const now = Date.now();
|
|
842
|
+
const config = this.config;
|
|
843
|
+
|
|
844
|
+
// 从client对象中获取IP
|
|
845
|
+
const clientIp = client.connection && client.connection.stream ? client.connection.stream
|
|
846
|
+
.remoteAddress || 'unknown' : 'unknown';
|
|
847
|
+
|
|
848
|
+
// 初始化或更新密码错误记录
|
|
849
|
+
if (!this.securityRecords.password[username]) {
|
|
850
|
+
this.securityRecords.password[username] = {
|
|
851
|
+
count: 1,
|
|
852
|
+
firstAttempt: now,
|
|
853
|
+
lastAttempt: now,
|
|
854
|
+
ip: clientIp
|
|
855
|
+
};
|
|
856
|
+
} else {
|
|
857
|
+
this.securityRecords.password[username].count++;
|
|
858
|
+
this.securityRecords.password[username].lastAttempt = now;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const failureCount = this.securityRecords.password[username].count;
|
|
862
|
+
$.log.warn(`MQTT客户端 ${client.id} (IP: ${clientIp}) 密码错误,用户名: ${username},当前错误次数: ${failureCount}`);
|
|
863
|
+
|
|
864
|
+
// 检查是否达到最大错误次数,需要拉黑
|
|
865
|
+
if (failureCount >= config.max_attempts) {
|
|
866
|
+
// 拉黑客户端
|
|
867
|
+
await this.addBlacklist(client.id, config.block_duration);
|
|
868
|
+
|
|
869
|
+
// 如果配置了同时拉黑IP,也拉黑IP
|
|
870
|
+
if (config.password_block_ip) {
|
|
871
|
+
await this.addIpToBanlist(clientIp, config.block_duration);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
$.log.warn(
|
|
875
|
+
`用户名 ${username} 密码错误次数达到上限(${config.max_attempts}次),已拉黑客户端 ${client.id} 和IP ${clientIp},时长 ${config.block_duration / 3600}小时`
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
} catch (error) {
|
|
880
|
+
$.log.error(`记录客户端 ${client.id} 密码错误时出错:`, error);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* 运行
|
|
886
|
+
* @param {String} state 状态
|
|
887
|
+
*/
|
|
888
|
+
MQTT.prototype.run = async function(state = 'start') {
|
|
889
|
+
await this.before(state);
|
|
890
|
+
await this.main(state);
|
|
891
|
+
await this.after(state);
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* 异步检查客户端是否被拉黑
|
|
896
|
+
* @param {String} clientId 客户端ID
|
|
897
|
+
* @returns {Promise<Boolean>} true表示被拉黑,false表示未被拉黑
|
|
898
|
+
*/
|
|
899
|
+
MQTT.prototype.isClientBlocked = async function(clientId) {
|
|
900
|
+
const now = Date.now();
|
|
901
|
+
let isBlocked = false;
|
|
902
|
+
let unblockTime = null;
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
// 根据存储类型选择不同的实现方式
|
|
906
|
+
if (this.blacklistStorageType === 'memory') {
|
|
907
|
+
// 内存存储方式
|
|
908
|
+
if (this.blockedClients[clientId]) {
|
|
909
|
+
unblockTime = this.blockedClients[clientId];
|
|
910
|
+
isBlocked = now < unblockTime;
|
|
911
|
+
// 如果已过期,自动移除
|
|
912
|
+
if (!isBlocked) {
|
|
913
|
+
delete this.blockedClients[clientId];
|
|
914
|
+
$.log.info(`MQTT客户端 ${clientId} 解除拉黑`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
919
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
920
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
921
|
+
if (this.blockedClients[clientId]) {
|
|
922
|
+
unblockTime = this.blockedClients[clientId];
|
|
923
|
+
isBlocked = now < unblockTime;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} catch (error) {
|
|
927
|
+
$.log.error(`检查客户端 ${clientId} 是否被拉黑时出错:`, error);
|
|
928
|
+
// 出错时默认视为未被拉黑
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return isBlocked;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* 异步添加客户端到黑名单
|
|
936
|
+
* @param {String} clientId 客户端ID
|
|
937
|
+
* @param {Number} duration 拉黑时长(毫秒)
|
|
938
|
+
* @returns {Promise<Boolean>} true表示添加成功,false表示添加失败
|
|
939
|
+
*/
|
|
940
|
+
MQTT.prototype.addBlacklist = async function(clientId, duration) {
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
const unblockTime = now + (duration || this.config.rate_limit.block_duration) * 1000; // 转换为毫秒
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
// 根据存储类型选择不同的实现方式
|
|
946
|
+
if (this.blacklistStorageType === 'memory') {
|
|
947
|
+
// 内存存储方式
|
|
948
|
+
this.blockedClients[clientId] = unblockTime;
|
|
949
|
+
} else {
|
|
950
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
951
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
952
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
953
|
+
this.blockedClients[clientId] = unblockTime;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
$.log.warn(
|
|
957
|
+
`MQTT客户端 ${clientId} 被添加到黑名单,限制时长: ${(duration || this.config.rate_limit.block_duration) / 3600}小时`
|
|
958
|
+
);
|
|
959
|
+
return true;
|
|
960
|
+
} catch (error) {
|
|
961
|
+
$.log.error(`添加客户端 ${clientId} 到黑名单时出错:`, error);
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* 异步检查IP是否被禁止
|
|
968
|
+
* @param {String} ip IP地址
|
|
969
|
+
* @returns {Promise<Boolean>} true表示被禁止,false表示未被禁止
|
|
970
|
+
*/
|
|
971
|
+
MQTT.prototype.isIpBanned = async function(ip) {
|
|
972
|
+
const now = Date.now();
|
|
973
|
+
let isBanned = false;
|
|
974
|
+
let unbanTime = null;
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
// 根据存储类型选择不同的实现方式
|
|
978
|
+
if (this.blacklistStorageType === 'memory') {
|
|
979
|
+
// 内存存储方式
|
|
980
|
+
if (this.bannedIps[ip]) {
|
|
981
|
+
unbanTime = this.bannedIps[ip];
|
|
982
|
+
isBanned = now < unbanTime;
|
|
983
|
+
// 如果已过期,自动移除
|
|
984
|
+
if (!isBanned) {
|
|
985
|
+
delete this.bannedIps[ip];
|
|
986
|
+
$.log.info(`IP ${ip} 解除禁止`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
991
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
992
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
993
|
+
if (this.bannedIps[ip]) {
|
|
994
|
+
unbanTime = this.bannedIps[ip];
|
|
995
|
+
isBanned = now < unbanTime;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
} catch (error) {
|
|
999
|
+
$.log.error(`检查IP ${ip} 是否被禁止时出错:`, error);
|
|
1000
|
+
// 出错时默认视为未被禁止
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return isBanned;
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* 异步添加IP到禁止列表
|
|
1008
|
+
* @param {String} ip IP地址
|
|
1009
|
+
* @param {Number} duration 禁止时长(毫秒)
|
|
1010
|
+
* @returns {Promise<Boolean>} true表示添加成功,false表示添加失败
|
|
1011
|
+
*/
|
|
1012
|
+
MQTT.prototype.addIpToBanlist = async function(ip, duration) {
|
|
1013
|
+
const now = Date.now();
|
|
1014
|
+
const unbanTime = now + (duration || this.config.reconnect_ip_duration) * 1000; // 转换为毫秒
|
|
1015
|
+
|
|
1016
|
+
try {
|
|
1017
|
+
// 根据存储类型选择不同的实现方式
|
|
1018
|
+
if (this.blacklistStorageType === 'memory') {
|
|
1019
|
+
// 内存存储方式
|
|
1020
|
+
this.bannedIps[ip] = unbanTime;
|
|
1021
|
+
} else {
|
|
1022
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
1023
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
1024
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
1025
|
+
this.bannedIps[ip] = unbanTime;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
$.log.warn(`IP ${ip} 被添加到禁止列表,限制时长: ${(duration || this.config.reconnect_ip_duration) / 3600}小时`);
|
|
1029
|
+
return true;
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
$.log.error(`添加IP ${ip} 到禁止列表时出错:`, error);
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* 异步从黑名单中移出客户端
|
|
1038
|
+
* @param {String} clientId 客户端ID
|
|
1039
|
+
* @returns {Promise<Boolean>} true表示移出成功,false表示移出失败
|
|
1040
|
+
*/
|
|
1041
|
+
MQTT.prototype.removeBlacklist = async function(clientId) {
|
|
1042
|
+
try {
|
|
1043
|
+
// 根据存储类型选择不同的实现方式
|
|
1044
|
+
if (this.blacklistStorageType === 'memory') {
|
|
1045
|
+
// 内存存储方式
|
|
1046
|
+
if (this.blockedClients[clientId]) {
|
|
1047
|
+
delete this.blockedClients[clientId];
|
|
1048
|
+
$.log.info(`MQTT客户端 ${clientId} 已从黑名单中移出`);
|
|
1049
|
+
return true;
|
|
1050
|
+
} else {
|
|
1051
|
+
$.log.info(`MQTT客户端 ${clientId} 不在黑名单中`);
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
1056
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
1057
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
1058
|
+
if (this.blockedClients[clientId]) {
|
|
1059
|
+
delete this.blockedClients[clientId];
|
|
1060
|
+
$.log.info(`MQTT客户端 ${clientId} 已从黑名单中移出`);
|
|
1061
|
+
return true;
|
|
1062
|
+
} else {
|
|
1063
|
+
$.log.info(`MQTT客户端 ${clientId} 不在黑名单中`);
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
$.log.error(`从黑名单中移出客户端 ${clientId} 时出错:`, error);
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* 异步从禁止列表中移出IP
|
|
1075
|
+
* @param {String} ip IP地址
|
|
1076
|
+
* @returns {Promise<Boolean>} true表示移出成功,false表示移出失败
|
|
1077
|
+
*/
|
|
1078
|
+
MQTT.prototype.removeIpFromBanlist = async function(ip) {
|
|
1079
|
+
try {
|
|
1080
|
+
// 根据存储类型选择不同的实现方式
|
|
1081
|
+
if (this.blacklistStorageType === 'memory') {
|
|
1082
|
+
// 内存存储方式
|
|
1083
|
+
if (this.bannedIps[ip]) {
|
|
1084
|
+
delete this.bannedIps[ip];
|
|
1085
|
+
$.log.info(`IP ${ip} 已从禁止列表中移出`);
|
|
1086
|
+
return true;
|
|
1087
|
+
} else {
|
|
1088
|
+
$.log.info(`IP ${ip} 不在禁止列表中`);
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
// 其他存储方式(redis、mongodb、mysql)可以在这里扩展实现
|
|
1093
|
+
// 目前仅实现内存存储,其他存储方式需要根据实际需求补充
|
|
1094
|
+
$.log.warn(`黑名单存储类型 ${this.blacklistStorageType} 尚未实现,默认使用内存存储`);
|
|
1095
|
+
if (this.bannedIps[ip]) {
|
|
1096
|
+
delete this.bannedIps[ip];
|
|
1097
|
+
$.log.info(`IP ${ip} 已从禁止列表中移出`);
|
|
1098
|
+
return true;
|
|
1099
|
+
} else {
|
|
1100
|
+
$.log.info(`IP ${ip} 不在禁止列表中`);
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
$.log.error(`从禁止列表中移出IP ${ip} 时出错:`, error);
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1107
1110
|
module.exports = MQTT;
|