mm_ip 1.0.7 → 1.0.9

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/index.js CHANGED
@@ -1,1333 +1,18 @@
1
- /**
2
- * IP管理模块
3
- * @file IP管理模块,支持白名单、黑名单管理,IP请求频率和次数记录
4
- */
5
- require('mm_expand');
1
+ const IP = require('ip');
2
+ const Admin = require('mm_corejs').Admin;
3
+ const { Ip } = require('./lib/ip.js');
6
4
 
7
- /**
8
- * IP管理类
9
- * @class
10
- */
11
- class Ip {
12
- static config = {
13
- max_white: 1000,
14
- max_black: 1000,
15
- check_interval: 60000, // 1分钟
16
- max_req_per_min: 100,
17
- auto_black_enable: true, // 启用自动高频拉黑
18
- auto_black_threshold: 50, // 自动拉黑阈值(每分钟请求数)
19
- auto_black_duration: 3600000, // 自动拉黑持续时间(1小时)
20
- auto_black_grace_count: 3, // 自动拉黑宽限次数,连续触限达到该次数后才拉黑
21
- violation_enable: true, // 启用违规记录
22
- violation_max_count: 10, // 最大违规次数
23
- violation_reset_time: 86400000, // 违规记录重置时间(24小时)
24
- violation_auto_black: true, // 违规次数超限自动拉黑
25
- region_check_enable: true, // 启用国外IP检测
26
- region_checker: null, // IP地区检测函数 (ip) => { country: 'CN' },支持同步或异步
27
- region_cache_time: 86400000, // 地区检测结果缓存时间(24小时)
28
- os_black_enable: false, // 启用系统级拉黑联动(需管理员权限,默认关闭避免误拦截)
29
- os_rule_prefix: 'mm_ip_black_', // 系统防火墙规则名前缀
30
- dir: './cache/ip' // 持久化存储目录
31
- }
32
- /**
33
- * 创建IP管理实例
34
- * @param {Object} config - 配置选项
35
- */
36
- constructor(config) {
37
- this._config = Object.assign({}, Ip.config);
38
-
39
- this._white = new Set();
40
- this._black = new Set();
41
- this._req_count = new Map();
42
- this._req_time = new Map();
43
- this._req_start = new Map(); // 请求窗口开始时间
44
- this._black_time = new Map(); // 记录拉黑时间
45
- this._violation_count = new Map(); // 违规次数记录
46
- this._violation_time = new Map(); // 违规时间记录(首次违规时间)
47
- this._violation_flagged = new Map(); // 同一请求窗口违规标记
48
- this._region_cache = new Map(); // IP地区检测结果缓存
49
- this._logger = $.log || console;
50
-
51
- this.setConfig(config);
52
- // 默认使用ip2region作为地区检测器
53
- if (!this._config.region_checker && this._config.region_check_enable) {
54
- this._initIp2Region();
55
- }
56
- // 自动加载持久化数据
57
- if (this._config.dir) {
58
- this.load();
59
- }
60
- }
61
- }
62
-
63
- /**
64
- * 设置配置选项
65
- * @param {Object} config - 配置选项
66
- */
67
- Ip.prototype.setConfig = function(config) {
68
- if (config) {
69
- Object.assign(this._config, config);
70
- }
71
- }
72
-
73
- /**
74
- * 设置日志器
75
- * @param {Object} logger - 日志对象
76
- */
77
- Ip.prototype.init = function(logger) {
78
- if (logger) {
79
- this._logger = logger;
80
- }
81
- };
82
-
83
- /**
84
- * 日志记录
85
- * @param {string} level - 日志级别(如:info, warn, error)
86
- * @param {string} message - 日志消息
87
- * @param {...*} args - 可选的日志参数
88
- */
89
- Ip.prototype.log = function(level, message, ...args) {
90
- this._logger[level](`${this.constructor.name} ${message}`, ...args);
91
- };
92
-
93
- /**
94
- * 添加IP到白名单
95
- * @param {string} ip - IP地址
96
- * @returns {boolean} 是否添加成功
97
- * @throws {TypeError} 当IP非字符串时
98
- */
99
- Ip.prototype.addWhite = function(ip) {
100
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
101
-
102
- if (this.getAllWhite().length >= this._config.max_white) {
103
- this.log('warn', '白名单已满,无法添加IP:', ip);
104
- return false;
105
- }
106
-
107
- this._white.add(ip);
108
-
109
- // 自动保存到持久化存储
110
- if (this._config.dir) {
111
- this.save();
112
- }
113
-
114
- return true;
115
- };
116
-
117
- /**
118
- * 添加IP到黑名单
119
- * @param {string} ip - IP地址
120
- * @returns {boolean} 是否添加成功
121
- * @throws {TypeError} 当IP非字符串时
122
- */
123
- Ip.prototype.addBlack = function(ip) {
124
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
125
-
126
- if (this.getAllBlack().length >= this._config.max_black) {
127
- this.log('warn', '黑名单已满,无法添加IP:', ip);
128
- return false;
129
- }
130
-
131
- this._black.add(ip);
132
- this.log('info', '添加黑名单IP:', ip);
133
-
134
- // 系统级联动拉黑
135
- if (this._config.os_black_enable) {
136
- this.osBlack(ip);
137
- }
138
-
139
- // 自动保存到持久化存储
140
- if (this._config.dir) {
141
- this.save();
142
- }
143
-
144
- return true;
145
- };
146
-
147
- /**
148
- * 从白名单删除IP
149
- * @param {string} ip - IP地址
150
- * @returns {boolean} 是否删除成功
151
- * @throws {TypeError} 当IP非字符串时
152
- */
153
- Ip.prototype.delWhite = function(ip) {
154
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
155
-
156
- const result = this._white.delete(ip);
157
- if (result) {
158
- this.log('info', '删除白名单IP:', ip);
159
-
160
- // 自动保存到持久化存储
161
- if (this._config.dir) {
162
- this.save();
163
- }
164
- }
165
- return result;
166
- };
167
-
168
- /**
169
- * 从黑名单删除IP
170
- * @param {string} ip - IP地址
171
- * @returns {boolean} 是否删除成功
172
- * @throws {TypeError} 当IP非字符串时
173
- */
174
- Ip.prototype.delBlack = function(ip) {
175
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
176
-
177
- const result = this._black.delete(ip);
178
- if (result) {
179
- this.log('info', '删除黑名单IP:', ip);
180
-
181
- // 系统级联动释放
182
- if (this._config.os_black_enable) {
183
- this.osUnblack(ip);
184
- }
185
-
186
- // 自动保存到持久化存储
187
- if (this._config.dir) {
188
- this.save();
189
- }
190
- }
191
- return result;
192
- };
193
-
194
- /**
195
- * 检查IP是否在白名单
196
- * @param {string} ip - IP地址
197
- * @returns {boolean} 是否在白名单
198
- * @throws {TypeError} 当IP非字符串时
199
- */
200
- Ip.prototype.isWhite = function(ip) {
201
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
202
- return this._white.has(ip);
203
- };
204
-
205
- /**
206
- * 检查IP是否在黑名单
207
- * @param {string} ip - IP地址
208
- * @returns {boolean} 是否在黑名单
209
- * @throws {TypeError} 当IP非字符串时
210
- */
211
- Ip.prototype.isBlack = function(ip) {
212
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
213
- return this._black.has(ip);
214
- };
215
-
216
- /**
217
- * 记录IP请求
218
- * @param {string} ip - IP地址
219
- * @returns {boolean} 是否允许请求
220
- * @throws {TypeError} 当IP非字符串时
221
- */
222
- Ip.prototype.record = function(ip) {
223
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
224
-
225
- // 检查黑名单(包括过期检查)
226
- if (this.isBlack(ip)) {
227
- // 检查是否过期
228
- if (this._isBlackExpired(ip)) {
229
- this.delBlack(ip);
230
- } else {
231
- this.log('warn', '黑名单IP请求被拒绝:', ip);
232
- // 记录违规:黑名单IP尝试访问
233
- if (this._config.violation_enable) {
234
- this.recordViolation(ip, '黑名单IP尝试访问');
235
- }
236
- return false;
237
- }
238
- }
239
-
240
- // 内网IP豁免:不参与限流、违规与地区检测,避免误拉黑内网访问
241
- if (this._isPrivate(ip)) {
242
- this._updateReqCount(ip);
243
- return true;
244
- }
245
-
246
- // 白名单直接通过
247
- if (this.isWhite(ip)) {
248
- this._updateReqCount(ip);
249
- return true;
250
- }
251
-
252
- // 国外IP检测:检测到国外IP自动拉黑
253
- if (this._config.region_check_enable && this._checkForeign(ip)) {
254
- return false;
255
- }
256
-
257
- // 检查频率限制
258
- if (!this._checkRateLimit(ip)) {
259
- this.log('warn', 'IP请求频率超限:', ip);
260
- const rate = this.getReqRate(ip);
261
-
262
- // 同一请求窗口只记一次违规,避免连续请求导致违规次数暴涨
263
- if (this._config.violation_enable) {
264
- const flag_time = this._violation_flagged.get(ip);
265
- const win_start = this._req_start.get(ip);
266
- if (flag_time !== win_start) {
267
- this.recordViolation(ip, '请求频率超限');
268
- this._violation_flagged.set(ip, win_start);
269
- }
270
- }
271
-
272
- // 达到宽限次数且频率超阈值才自动拉黑
273
- if (this._config.auto_black_enable
274
- && rate >= this._config.auto_black_threshold
275
- && this.getViolationCount(ip) >= this._config.auto_black_grace_count) {
276
- this._autoBlack(ip);
277
- return false;
278
- }
279
-
280
- // 重置计数窗口(软限流),下一次请求重新计数
281
- this._req_start.set(ip, Date.now());
282
- this._req_count.set(ip, 0);
283
-
284
- return false;
285
- }
286
-
287
- this._updateReqCount(ip);
288
- return true;
289
- };
290
-
291
- /**
292
- * 获取IP请求次数
293
- * @param {string} ip - IP地址
294
- * @returns {number} 请求次数
295
- * @throws {TypeError} 当IP非字符串时
296
- */
297
- Ip.prototype.getReqCount = function(ip) {
298
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
299
- return this._req_count.get(ip) || 0;
300
- };
301
-
302
- /**
303
- * 获取IP请求频率(每分钟请求数)
304
- * @param {string} ip - IP地址
305
- * @returns {number} 请求频率(每分钟请求数)
306
- * @throws {TypeError} 当IP非字符串时
307
- */
308
- Ip.prototype.getReqRate = function(ip) {
309
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
310
-
311
- const now = Date.now();
312
- const start = this._req_start.get(ip) || now;
313
- const count = this._req_count.get(ip) || 0;
314
-
315
- // 当前窗口已过期,频率为0
316
- if (now - start >= this._config.check_interval) {
317
- return 0;
318
- }
319
-
320
- // 按窗口换算为每分钟请求数,不做时间外推
321
- return Math.round((count / this._config.check_interval) * 60000);
322
- };
323
-
324
- /**
325
- * 获取所有IP的请求统计信息
326
- * @returns {Object} IP统计信息对象
327
- */
328
- Ip.prototype.getAllStats = function() {
329
- const stats = {};
330
- const now = Date.now();
331
-
332
- // 收集所有有请求记录的IP
333
- for (const ip of this.getAllReqIps()) {
334
- const lastTime = this._req_time.get(ip) || now;
335
- const isActive = now - lastTime < this._config.check_interval;
336
-
337
- stats[ip] = {
338
- count: this.getReqCount(ip),
339
- rate: isActive ? this.getReqRate(ip) : 0,
340
- last_time: lastTime,
341
- is_white: this.isWhite(ip),
342
- is_black: this.isBlack(ip),
343
- black_time: this.getBlackExpireTime(ip)
344
- };
345
- }
346
-
347
- return stats;
348
- };
349
-
350
- /**
351
- * 获取所有白名单IP
352
- * @returns {Array} 白名单IP数组
353
- */
354
- Ip.prototype.getAllWhite = function() {
355
- return Array.from(this._white);
356
- };
357
-
358
- /**
359
- * 获取所有黑名单IP
360
- * @returns {Array} 黑名单IP数组
361
- */
362
- Ip.prototype.getAllBlack = function() {
363
- return Array.from(this._black);
364
- };
365
-
366
- /**
367
- * 获取所有有请求记录的IP
368
- * @returns {Array} 有请求记录的IP数组
369
- */
370
- Ip.prototype.getAllReqIps = function() {
371
- return Array.from(this._req_count.keys());
372
- };
373
-
374
- /**
375
- * 清空白名单
376
- */
377
- Ip.prototype.clearWhite = function() {
378
- this._white.clear();
379
- this.log('info', '清空白名单');
380
-
381
- // 自动保存到持久化存储
382
- if (this._config.dir) {
383
- this.save();
384
- }
385
- };
386
-
387
- /**
388
- * 清空黑名单
389
- */
390
- Ip.prototype.clearBlack = function() {
391
- this._black.clear();
392
- this.log('info', '清空黑名单');
393
-
394
- // 自动保存到持久化存储
395
- if (this._config.dir) {
396
- this.save();
397
- }
398
- };
399
-
400
- /**
401
- * 清空所有请求记录
402
- */
403
- Ip.prototype.clearReq = function() {
404
- this._req_count.clear();
405
- this._req_time.clear();
406
- this._req_start.clear();
407
- this.log('info', '清空请求记录');
408
- };
409
-
410
- /**
411
- * 检查IP请求频率限制(私有方法)
412
- * @private
413
- * @param {string} ip - IP地址
414
- * @returns {boolean} 是否允许请求
415
- */
416
- Ip.prototype._checkRateLimit = function(ip) {
417
- const now = Date.now();
418
- let start = this._req_start.get(ip);
419
-
420
- // 无窗口或窗口已过期,开启新窗口并重置计数
421
- if (start === undefined || now - start >= this._config.check_interval) {
422
- start = now;
423
- this._req_start.set(ip, start);
424
- this._req_count.set(ip, 0);
425
- }
426
-
427
- const count = this._req_count.get(ip) || 0;
428
- return count < this._config.max_req_per_min;
429
- };
430
-
431
- /**
432
- * 更新IP请求计数(私有方法)
433
- * @private
434
- * @param {string} ip - IP地址
435
- */
436
- Ip.prototype._updateReqCount = function(ip) {
437
- if (this._req_start.get(ip) === undefined) {
438
- this._req_start.set(ip, Date.now());
439
- }
440
-
441
- const count = this.getReqCount(ip) + 1;
442
- this._req_count.set(ip, count);
443
- this._req_time.set(ip, Date.now());
444
- };
445
-
446
- /**
447
- * 验证IP格式(私有方法)
448
- * @private
449
- * @param {string} ip - IP地址
450
- * @returns {boolean} 是否有效IP
451
- */
452
- Ip.prototype._validIp = function(ip) {
453
- const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
454
- return ipRegex.test(ip);
455
- };
456
-
457
- /**
458
- * 判断IP是否为内网/回环地址(私有方法)
459
- * @private
460
- * @param {string} ip - IP地址
461
- * @returns {boolean} 是否为内网IP
462
- */
463
- Ip.prototype._isPrivate = function(ip) {
464
- const parts = ip.split('.');
465
- if (parts.length !== 4) return false;
466
-
467
- const first = parseInt(parts[0], 10);
468
- const second = parseInt(parts[1], 10);
469
-
470
- // 回环 127.0.0.0/8、A类 10.0.0.0/8、B类 172.16.0.0/12、C类 192.168.0.0/16
471
- if (first === 127) return true;
472
- if (first === 10) return true;
473
- if (first === 172 && second >= 16 && second <= 31) return true;
474
- if (first === 192 && second === 168) return true;
475
-
476
- return false;
477
- };
478
-
479
- /**
480
- * 批量添加IP到白名单
481
- * @param {Array} ip_list - IP地址数组
482
- * @returns {number} 成功添加的数量
483
- */
484
- Ip.prototype.addWhiteBatch = function(ip_list) {
485
- if (!Array.isArray(ip_list)) throw new TypeError('ip_list must be array');
486
-
487
- let success = 0;
488
- for (const ip of ip_list) {
489
- if (typeof ip === 'string' && this._validIp(ip)) {
490
- if (this.addWhite(ip)) {
491
- success++;
492
- }
493
- }
494
- }
495
- return success;
496
- };
497
-
498
- /**
499
- * 批量添加IP到黑名单
500
- * @param {Array} ip_list - IP地址数组
501
- * @returns {number} 成功添加的数量
502
- */
503
- Ip.prototype.addBlackBatch = function(ip_list) {
504
- if (!Array.isArray(ip_list)) throw new TypeError('ip_list must be array');
505
-
506
- let success = 0;
507
- for (const ip of ip_list) {
508
- if (typeof ip === 'string' && this._validIp(ip)) {
509
- if (this.addBlack(ip)) {
510
- success++;
511
- }
512
- }
513
- }
514
- return success;
515
- };
516
-
517
- /**
518
- * 手动高频拉黑IP
519
- * @param {string} ip - IP地址
520
- * @param {number} duration - 拉黑持续时间(毫秒)
521
- * @returns {boolean} 是否拉黑成功
522
- * @throws {TypeError} 当IP非字符串时
523
- */
524
- Ip.prototype.blackByHighFreq = function(ip, duration) {
525
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
526
-
527
- const black_duration = duration || this._config.auto_black_duration;
528
-
529
- if (this.addBlack(ip)) {
530
- this._black_time.set(ip, Date.now() + black_duration);
531
- this.log('warn', '手动高频拉黑IP:', ip, '持续时间:', black_duration + 'ms');
532
- return true;
533
- }
534
-
535
- return false;
536
- };
537
-
538
- /**
539
- * 检查并清理过期的黑名单IP(私有方法)
540
- * @private
541
- */
542
- Ip.prototype._cleanExpiredBlack = function() {
543
- const now = Date.now();
544
- const expired = [];
545
-
546
- // 获取所有有过期时间的黑名单IP
547
- for (const ip of this.getAllBlack()) {
548
- const expireTime = this.getBlackExpireTime(ip);
549
- if (expireTime && now > expireTime) {
550
- expired.push(ip);
551
- }
552
- }
553
-
554
- for (const ip of expired) {
555
- this.delBlack(ip);
556
- this._black_time.delete(ip);
557
- }
558
- };
559
-
560
- /**
561
- * 检查黑名单是否过期(私有方法)
562
- * @private
563
- * @param {string} ip - IP地址
564
- * @returns {boolean} 是否过期
565
- */
566
- Ip.prototype._isBlackExpired = function(ip) {
567
- const expireTime = this.getBlackExpireTime(ip);
568
- if (!expireTime) return false; // 没有设置过期时间,永不过期
569
-
570
- return Date.now() > expireTime;
571
- };
572
-
573
- /**
574
- * 自动拉黑IP(私有方法)
575
- * @private
576
- * @param {string} ip - IP地址
577
- */
578
- Ip.prototype._autoBlack = function(ip) {
579
- if (this.addBlack(ip)) {
580
- this._black_time.set(ip, Date.now() + this._config.auto_black_duration);
581
- this.log('warn', '自动高频拉黑IP:', ip,
582
- '频率:', this.getReqRate(ip) + '/分钟',
583
- '持续时间:', this._config.auto_black_duration + 'ms');
584
- }
585
- };
586
-
587
- /**
588
- * 获取黑名单过期时间
589
- * @param {string} ip - IP地址
590
- * @returns {number|null} 过期时间戳(毫秒),null表示永不过期
591
- * @throws {TypeError} 当IP非字符串时
592
- */
593
- Ip.prototype.getBlackExpireTime = function(ip) {
594
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
595
-
596
- if (!this.isBlack(ip)) return null;
597
-
598
- const expireTime = this._black_time.get(ip);
599
- return expireTime || null;
600
- };
601
-
602
- /**
603
- * 清理所有过期的黑名单IP
604
- * @returns {number} 清理的数量
605
- */
606
- Ip.prototype.cleanExpiredBlack = function() {
607
- const beforeCount = this.getAllBlack().length;
608
- this._cleanExpiredBlack();
609
- return beforeCount - this.getAllBlack().length;
610
- };
611
-
612
- /**
613
- * 记录IP违规。
614
- * @param {string} ip - IP地址
615
- * @param {string} reason - 违规原因
616
- * @returns {number} 当前违规次数
617
- */
618
- Ip.prototype.recordViolation = function(ip, reason) {
619
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
620
- if (!this._config.violation_enable) return 0;
621
-
622
- this._cleanExpiredViolation();
623
-
624
- const now = Date.now();
625
- const count = this.getViolationCount(ip) + 1;
626
-
627
- // 记录首次违规时间,用于违规窗口重置
628
- if (!this._violation_time.has(ip)) {
629
- this._violation_time.set(ip, now);
630
- }
631
- this._violation_count.set(ip, count);
632
-
633
- // 检查是否达到自动拉黑条件
634
- if (this._config.violation_auto_black && count >= this._config.violation_max_count) {
635
- this.blackByHighFreq(ip, this._config.auto_black_duration);
636
- this.log('warn', 'IP因违规次数超限自动拉黑:', {
637
- ip,
638
- count,
639
- duration: this._config.auto_black_duration
640
- });
641
- }
642
-
643
- return count;
644
- };
645
-
646
- /**
647
- * 获取IP违规次数。
648
- * @param {string} ip - IP地址
649
- * @returns {number} 违规次数
650
- */
651
- Ip.prototype.getViolationCount = function(ip) {
652
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
653
-
654
- this._cleanExpiredViolation();
655
- return this._violation_count.get(ip) || 0;
656
- };
657
-
658
- /**
659
- * 重置IP违规记录。
660
- * @param {string} ip - IP地址
661
- * @returns {boolean} 是否成功重置
662
- */
663
- Ip.prototype.resetViolation = function(ip) {
664
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
665
-
666
- const hadRecord = this._violation_count.has(ip);
667
- this._violation_count.delete(ip);
668
- this._violation_time.delete(ip);
669
-
670
- return hadRecord;
671
- };
672
-
673
- /**
674
- * 获取所有有违规记录的IP
675
- * @returns {Array} 有违规记录的IP数组
676
- */
677
- Ip.prototype.getAllViolationIps = function() {
678
- return Array.from(this._violation_count.keys());
679
- };
680
-
681
- /**
682
- * 获取IP违规详细信息
683
- * @param {string} ip - IP地址
684
- * @returns {Object|null} 违规信息
685
- */
686
- Ip.prototype.getViolationInfo = function(ip) {
687
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
688
-
689
- const count = this.getViolationCount(ip);
690
- if (count === 0) return null;
691
-
692
- return {
693
- count: count,
694
- time: this._violation_time.get(ip) // 首次违规时间
695
- };
696
- };
697
-
698
- /**
699
- * 获取所有IP违规统计信息。
700
- * @returns {Object} 违规统计信息
701
- */
702
- Ip.prototype.getAllViolations = function() {
703
- this._cleanExpiredViolation();
704
-
705
- const violations = {};
706
- for (const ip of this.getAllViolationIps()) {
707
- violations[ip] = this.getViolationInfo(ip);
708
- }
709
-
710
- return violations;
711
- };
712
-
713
- /**
714
- * 清理过期的违规记录(私有方法)
715
- * @private
716
- */
717
- Ip.prototype._cleanExpiredViolation = function() {
718
- const now = Date.now();
719
- const expired = [];
720
-
721
- // 直接访问内部数据结构避免递归调用
722
- for (const [ip, time] of this._violation_time) {
723
- if (now - time > this._config.violation_reset_time) {
724
- expired.push(ip);
725
- }
726
- }
727
-
728
- for (const ip of expired) {
729
- this._violation_count.delete(ip);
730
- this._violation_time.delete(ip);
731
- }
732
- };
733
-
734
- /**
735
- * 清空所有违规记录
736
- */
737
- Ip.prototype.clearViolations = function() {
738
- this._violation_count.clear();
739
- this._violation_time.clear();
740
- this.log('info', '清空所有违规记录');
741
-
742
- // 自动保存到持久化存储
743
- if (this._config.dir) {
744
- this.save();
745
- }
746
- };
747
-
748
- /**
749
- * 保存IP数据到文件(持久化存储)
750
- * @returns {boolean} 是否保存成功
751
- */
752
- Ip.prototype.save = function() {
753
- try {
754
- const fs = require('fs');
755
- const path = require('path');
756
-
757
- // 确保存储目录存在
758
- const dir = this._config.dir;
759
- dir.addDir();
760
-
761
- // 分别保存不同数据到不同文件
762
- const timestamp = Date.now();
763
-
764
- // 保存白名单数据
765
- const whiteData = {
766
- white: this.getAllWhite(),
767
- timestamp: timestamp
768
- };
769
- const whitePath = path.join(dir, 'white.json');
770
- fs.writeFileSync(whitePath, JSON.stringify(whiteData, null, 2), 'utf8');
771
-
772
- // 保存黑名单数据
773
- const blackData = {
774
- black: this.getAllBlack(),
775
- black_time: Array.from(this._black_time.entries()),
776
- timestamp: timestamp
777
- };
778
- const blackPath = path.join(dir, 'black.json');
779
- fs.writeFileSync(blackPath, JSON.stringify(blackData, null, 2), 'utf8');
780
-
781
- // 保存违规记录数据
782
- const violationData = {
783
- violation_count: Array.from(this._violation_count.entries()),
784
- violation_time: Array.from(this._violation_time.entries()),
785
- timestamp: timestamp
786
- };
787
- const violationPath = path.join(dir, 'violation.json');
788
- fs.writeFileSync(violationPath, JSON.stringify(violationData, null, 2), 'utf8');
789
-
790
- this.log('info', 'IP数据已分文件保存:', {
791
- white: whitePath,
792
- black: blackPath,
793
- violation: violationPath
794
- });
795
- return true;
796
- } catch (err) {
797
- this.log('error', '保存IP数据失败:', err.message);
798
- return false;
799
- }
800
- };
801
-
802
- /**
803
- * 从文件加载IP数据(持久化存储)
804
- * @returns {boolean} 是否加载成功
805
- */
806
- Ip.prototype.load = function() {
807
- try {
808
- const fs = require('fs');
809
- const path = require('path');
810
-
811
- const dir = this._config.dir;
812
- let loadedCount = 0;
813
-
814
- // 加载白名单数据
815
- const whitePath = path.join(dir, 'white.json');
816
- if (fs.existsSync(whitePath)) {
817
- const content = fs.readFileSync(whitePath, 'utf8');
818
- const data = JSON.parse(content);
819
-
820
- if (Array.isArray(data.white)) {
821
- for (const ip of data.white) {
822
- if (typeof ip === 'string') {
823
- this._white.add(ip);
824
- }
825
- }
826
- loadedCount++;
827
- }
828
- }
829
-
830
- // 加载黑名单数据
831
- const blackPath = path.join(dir, 'black.json');
832
- if (fs.existsSync(blackPath)) {
833
- const content = fs.readFileSync(blackPath, 'utf8');
834
- const data = JSON.parse(content);
835
-
836
- // 加载黑名单
837
- if (Array.isArray(data.black)) {
838
- for (const ip of data.black) {
839
- if (typeof ip === 'string') {
840
- this._black.add(ip);
841
- }
842
- }
843
- }
844
-
845
- // 加载黑名单过期时间
846
- if (Array.isArray(data.black_time)) {
847
- for (const [ip, time] of data.black_time) {
848
- if (typeof ip === 'string' && typeof time === 'number') {
849
- this._black_time.set(ip, time);
850
- }
851
- }
852
- }
853
-
854
- loadedCount++;
855
- }
856
-
857
- // 加载违规记录数据
858
- const violationPath = path.join(dir, 'violation.json');
859
- if (fs.existsSync(violationPath)) {
860
- const content = fs.readFileSync(violationPath, 'utf8');
861
- const data = JSON.parse(content);
862
-
863
- // 加载违规记录
864
- if (Array.isArray(data.violation_count)) {
865
- for (const [ip, count] of data.violation_count) {
866
- if (typeof ip === 'string' && typeof count === 'number') {
867
- this._violation_count.set(ip, count);
868
- }
869
- }
870
- }
871
-
872
- if (Array.isArray(data.violation_time)) {
873
- for (const [ip, time] of data.violation_time) {
874
- if (typeof ip === 'string' && typeof time === 'number') {
875
- this._violation_time.set(ip, time);
876
- }
877
- }
878
- }
879
-
880
- loadedCount++;
881
- }
882
-
883
- return true;
884
- } catch (err) {
885
- this.log('error', '加载IP数据失败:', err.message);
886
- return false;
887
- }
888
- };
889
-
890
- /**
891
- * 自动保存数据(定时保存)
892
- * @param {number} interval - 保存间隔(毫秒),默认5分钟
893
- */
894
- Ip.prototype.autoSave = function(interval = 300000) {
895
- if (this._autoSaveTimer) {
896
- clearInterval(this._autoSaveTimer);
897
- }
898
-
899
- this._autoSaveTimer = setInterval(() => {
900
- this.save();
901
- }, interval);
902
- };
903
-
904
- /**
905
- * 停止自动保存
906
- */
907
- Ip.prototype.stopAutoSave = function() {
908
- if (this._autoSaveTimer) {
909
- clearInterval(this._autoSaveTimer);
910
- this._autoSaveTimer = null;
911
- }
912
- };
913
-
914
- /**
915
- * 设置IP地区检测函数
916
- * @param {Function} fn - 地区检测函数,入参为IP地址,返回地区信息对象或国家代码
917
- * @throws {TypeError} 当fn非函数时
918
- */
919
- Ip.prototype.setRegionChecker = function(fn) {
920
- if (typeof fn !== 'function') throw new TypeError('fn must be function');
921
-
922
- this._config.region_checker = fn;
923
- this._config.region_check_enable = true;
924
- };
925
-
926
- /**
927
- * 初始化ip2region地区检测器(私有方法)
928
- * @private
929
- */
930
- Ip.prototype._initIp2Region = function() {
931
- try {
932
- const IP2Region = require('ip2region').default;
933
- const searcher = new IP2Region();
934
-
935
- this._ip2region = searcher;
936
- this._config.region_checker = function(ip) {
937
- const result = searcher.search(ip);
938
- // 内网IP或查询失败时country为空,不返回地区信息
939
- if (!result || !result.country) return null;
940
- return { country: result.country };
941
- };
942
- this.log('info', '已加载ip2region地区检测器');
943
- } catch (err) {
944
- this.log('warn', 'ip2region加载失败,地区检测不可用:', err.message);
945
- this._config.region_check_enable = false;
946
- }
947
- };
948
-
949
- /**
950
- * 获取IP地区信息(带缓存)
951
- * @param {string} ip - IP地址
952
- * @returns {Object|null|Promise} 地区信息对象,无检测函数或检测失败返回null,异步检测返回Promise
953
- * @throws {TypeError} 当IP非字符串时
954
- */
955
- Ip.prototype.getRegion = function(ip) {
956
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
957
- if (!this._config.region_checker) return null;
958
-
959
- // 优先读取缓存
960
- const cached = this._region_cache.get(ip);
961
- if (cached && Date.now() - cached.time < this._config.region_cache_time) {
962
- return cached.region;
963
- }
964
-
965
- try {
966
- const result = this._config.region_checker(ip);
967
-
968
- // 异步检测函数:缓存结果后返回
969
- if (result && typeof result.then === 'function') {
970
- return result.then(region => {
971
- if (region) {
972
- this._region_cache.set(ip, { region, time: Date.now() });
973
- }
974
- return region;
975
- });
976
- }
977
-
978
- if (result) {
979
- this._region_cache.set(ip, { region: result, time: Date.now() });
980
- }
981
- return result || null;
982
- } catch (err) {
983
- this.log('error', 'IP地区检测失败:', err.message);
984
- return null;
985
- }
986
- };
987
-
988
- /**
989
- * 判断IP是否为国外IP
990
- * @param {string} ip - IP地址
991
- * @returns {boolean} 是否为国外IP
992
- * @throws {TypeError} 当IP非字符串时
993
- */
994
- Ip.prototype.isForeign = function(ip) {
995
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
996
-
997
- const region = this.getRegion(ip);
998
- if (!region || typeof region.then === 'function') return false;
999
-
1000
- return this._isForeign(region);
1001
- };
1002
-
1003
- /**
1004
- * 检测IP地区,国外IP自动加入黑名单(永久)
1005
- * @param {string} ip - IP地址
1006
- * @returns {Object|null|Promise} 检测结果对象,检测失败返回null,异步检测返回Promise
1007
- * @throws {TypeError} 当IP非字符串时
1008
- */
1009
- Ip.prototype.checkRegion = function(ip) {
1010
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
1011
-
1012
- const region = this.getRegion(ip);
1013
- if (region && typeof region.then === 'function') {
1014
- return region.then(region_info => this._handleRegionCheck(ip, region_info));
1015
- }
1016
-
1017
- return this._handleRegionCheck(ip, region);
1018
- };
1019
-
1020
- /**
1021
- * 清空IP地区检测结果缓存
1022
- */
1023
- Ip.prototype.clearRegionCache = function() {
1024
- this._region_cache.clear();
1025
- this.log('info', '清空IP地区缓存');
1026
- };
1027
-
1028
- /**
1029
- * 判断地区信息是否为国外(私有方法)
1030
- * @private
1031
- * @param {Object|string} region - 地区信息对象或国家代码
1032
- * @returns {boolean} 是否为国外
1033
- */
1034
- Ip.prototype._isForeign = function(region) {
1035
- if (!region) return false;
1036
-
1037
- let country = '';
1038
- if (typeof region === 'string') {
1039
- country = region;
1040
- } else if (typeof region === 'object') {
1041
- // 支持直接指定国外标识
1042
- if (typeof region.is_foreign === 'boolean') {
1043
- return region.is_foreign;
1044
- }
1045
- country = region.country || region.country_code || '';
1046
- }
1047
-
1048
- if (!country) return false;
1049
-
1050
- const cn_list = ['cn', 'chn', '中国', '中国大陆', 'china'];
1051
- return !cn_list.includes(country.toLowerCase());
1052
- };
1053
-
1054
- /**
1055
- * 检测国外IP并自动拉黑(私有方法)
1056
- * @private
1057
- * @param {string} ip - IP地址
1058
- * @returns {boolean} 是否已拉黑国外IP
1059
- */
1060
- Ip.prototype._checkForeign = function(ip) {
1061
- if (!this._config.region_checker) return false;
1062
- if (this._isPrivate(ip)) return false;
1063
-
1064
- const region = this.getRegion(ip);
1065
- // 异步检测函数无法同步拉黑,跳过自动处理
1066
- if (!region || typeof region.then === 'function') return false;
1067
-
1068
- if (this._isForeign(region)) {
1069
- if (this.addBlack(ip)) {
1070
- // 国外IP拉黑带过期时间,避免永久封禁
1071
- this._black_time.set(ip, Date.now() + this._config.auto_black_duration);
1072
- }
1073
- this.log('warn', '检测到国外IP,自动拉黑:', ip);
1074
- return true;
1075
- }
1076
-
1077
- return false;
1078
- };
1079
-
1080
- /**
1081
- * 处理地区检测结果并拉黑国外IP(私有方法)
1082
- * @private
1083
- * @param {string} ip - IP地址
1084
- * @param {Object} region - 地区信息
1085
- * @returns {Object|null} 检测结果对象
1086
- */
1087
- Ip.prototype._handleRegionCheck = function(ip, region) {
1088
- if (!region) return null;
1089
- if (this._isPrivate(ip)) return null;
1090
-
1091
- const is_foreign = this._isForeign(region);
1092
- let blacklisted = false;
1093
-
1094
- if (is_foreign) {
1095
- blacklisted = this.addBlack(ip);
1096
- if (blacklisted) {
1097
- // 国外IP拉黑带过期时间,避免永久封禁
1098
- this._black_time.set(ip, Date.now() + this._config.auto_black_duration);
1099
- }
1100
- this.log('warn', '检测到国外IP,加入黑名单:', ip);
1101
- }
1102
-
1103
- return {
1104
- ip: ip,
1105
- region: region,
1106
- is_foreign: is_foreign,
1107
- blacklisted: blacklisted
1108
- };
1109
- };
1110
-
1111
- /**
1112
- * 在操作系统层拉黑IP(入站拦截)
1113
- * @param {string} ip - IP地址
1114
- * @returns {boolean} 是否拉黑成功
1115
- * @throws {TypeError} 当IP非字符串时
1116
- */
1117
- Ip.prototype.osBlack = function(ip) {
1118
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
1119
- if (!this._validIp(ip)) return false;
1120
-
1121
- const cmd = this._getOsCmd(ip, 'black');
1122
- if (!cmd) return false;
1123
-
1124
- const result = this._execOsCmd(cmd);
1125
- if (result.success) {
1126
- this.log('info', '系统层拉黑IP:', ip);
1127
- return true;
1128
- }
1129
-
1130
- this.log('error', '系统层拉黑IP失败:', ip, result.output);
1131
- return false;
1132
- };
1133
-
1134
- /**
1135
- * 在操作系统层释放IP(解除入站拦截)
1136
- * @param {string} ip - IP地址
1137
- * @returns {boolean} 是否释放成功
1138
- * @throws {TypeError} 当IP非字符串时
1139
- */
1140
- Ip.prototype.osUnblack = function(ip) {
1141
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
1142
- if (!this._validIp(ip)) return false;
1143
-
1144
- const cmd = this._getOsCmd(ip, 'unblack');
1145
- if (!cmd) return false;
1146
-
1147
- const result = this._execOsCmd(cmd);
1148
- if (result.success) {
1149
- this.log('info', '系统层释放IP:', ip);
1150
- return true;
1151
- }
1152
-
1153
- this.log('error', '系统层释放IP失败:', ip, result.output);
1154
- return false;
1155
- };
1156
-
1157
- /**
1158
- * 检查IP是否已在操作系统层拉黑
1159
- * @param {string} ip - IP地址
1160
- * @returns {boolean} 是否已系统拉黑
1161
- * @throws {TypeError} 当IP非字符串时
1162
- */
1163
- Ip.prototype.isOsBlack = function(ip) {
1164
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
1165
- if (!this._validIp(ip)) return false;
1166
-
1167
- const cmd = this._getOsCmd(ip, 'check');
1168
- if (!cmd) return false;
1169
-
1170
- const result = this._execOsCmd(cmd);
1171
- if (!result.success) return false;
1172
-
1173
- return result.output.includes(this._config.os_rule_prefix + ip)
1174
- || result.output.includes(ip);
1175
- };
1176
-
1177
- /**
1178
- * 生成系统拉黑命令(私有方法)
1179
- * @private
1180
- * @param {string} ip - IP地址
1181
- * @param {string} action - 操作类型(black, unblack, check)
1182
- * @returns {string|null} 系统命令,不支持的平台返回null
1183
- */
1184
- Ip.prototype._getOsCmd = function(ip, action) {
1185
- if (process.platform === 'win32') {
1186
- return this._getWinCmd(ip, action);
1187
- }
1188
- if (process.platform === 'linux') {
1189
- return this._getLinuxCmd(ip, action);
1190
- }
1191
- return null;
1192
- };
1193
-
1194
- /**
1195
- * 生成Windows防火墙命令(私有方法)
1196
- * @private
1197
- * @param {string} ip - IP地址
1198
- * @param {string} action - 操作类型(black, unblack, check)
1199
- * @returns {string|null} 系统命令
1200
- */
1201
- Ip.prototype._getWinCmd = function(ip, action) {
1202
- const rule = this._config.os_rule_prefix + ip;
1203
-
1204
- if (action === 'black') {
1205
- return `netsh advfirewall firewall add rule name="${rule}" dir=in action=block remoteip=${ip}`;
1206
- }
1207
- if (action === 'unblack') {
1208
- return `netsh advfirewall firewall delete rule name="${rule}"`;
1209
- }
1210
- if (action === 'check') {
1211
- return `netsh advfirewall firewall show rule name="${rule}"`;
1212
- }
1213
- return null;
1214
- };
1215
-
1216
- /**
1217
- * 生成Linux iptables命令(私有方法)
1218
- * @private
1219
- * @param {string} ip - IP地址
1220
- * @param {string} action - 操作类型(black, unblack, check)
1221
- * @returns {string|null} 系统命令
1222
- */
1223
- Ip.prototype._getLinuxCmd = function(ip, action) {
1224
- if (action === 'black') {
1225
- return `iptables -I INPUT -s ${ip} -j DROP`;
1226
- }
1227
- if (action === 'unblack') {
1228
- return `iptables -D INPUT -s ${ip} -j DROP`;
1229
- }
1230
- if (action === 'check') {
1231
- return `iptables -L INPUT -n`;
1232
- }
1233
- return null;
1234
- };
1235
-
1236
- /**
1237
- * 执行系统命令(私有方法)
1238
- * @private
1239
- * @param {string} cmd - 系统命令
1240
- * @returns {Object} 执行结果 { success, output }
1241
- */
1242
- Ip.prototype._execOsCmd = function(cmd) {
1243
- try {
1244
- const { execSync } = require('child_process');
1245
- const output = execSync(cmd, {
1246
- encoding: 'utf8',
1247
- timeout: 8000,
1248
- stdio: ['pipe', 'pipe', 'pipe']
1249
- });
1250
- return { success: true, output: output || '' };
1251
- } catch (err) {
1252
- const output = (err.stdout && err.stdout.toString()) || err.message || '';
1253
- return { success: false, output: output };
1254
- }
1255
- };
1256
-
1257
- /**
1258
- * 删除IP记录(从黑名单、白名单、临时记录中删除)
1259
- * @param {string} ip - IP地址
1260
- * @returns {Object} 删除结果
1261
- * @throws {TypeError} 当IP非字符串时
1262
- */
1263
- Ip.prototype.del = function(ip) {
1264
- if (typeof ip !== 'string') throw new TypeError('ip must be string');
1265
-
1266
- const result = {
1267
- black: false,
1268
- white: false,
1269
- violation: false,
1270
- black_time: false,
1271
- violation_count: false,
1272
- violation_time: false
1273
- };
1274
-
1275
- // 从黑名单删除
1276
- if (this._black.has(ip)) {
1277
- this._black.delete(ip);
1278
- result.black = true;
1279
-
1280
- // 系统级联动释放
1281
- if (this._config.os_black_enable) {
1282
- this.osUnblack(ip);
1283
- }
1284
- }
1285
-
1286
- // 从白名单删除
1287
- if (this._white.has(ip)) {
1288
- this._white.delete(ip);
1289
- result.white = true;
1290
- }
1291
-
1292
- // 从违规记录删除
1293
- if (this._violation_count.has(ip)) {
1294
- this._violation_count.delete(ip);
1295
- result.violation_count = true;
1296
- }
1297
-
1298
- if (this._violation_time.has(ip)) {
1299
- this._violation_time.delete(ip);
1300
- result.violation_time = true;
1301
- }
1302
-
1303
- // 清理请求窗口与违规标记
1304
- this._req_start.delete(ip);
1305
- this._violation_flagged.delete(ip);
1306
-
1307
- // 从黑名单过期时间记录删除
1308
- if (this._black_time.has(ip)) {
1309
- this._black_time.delete(ip);
1310
- result.black_time = true;
1311
- }
1312
-
1313
- // 从地区缓存删除
1314
- this._region_cache.delete(ip);
1315
-
1316
- // 如果有任何删除操作,记录日志并自动保存
1317
- if (Object.values(result).some(Boolean)) {
1318
- // 自动保存到持久化存储
1319
- if (this._config.dir) {
1320
- this.save();
1321
- }
1322
- }
1323
-
1324
- return result;
1325
- };
1326
-
1327
- if (global.$ && !$.ip) {
1328
- $.ip = new Ip();
1329
- }
5
+ const admin = new Admin();
6
+ admin.setClass(Ip);
1330
7
 
1331
8
  module.exports = {
1332
- Ip
9
+ IP,
10
+ Ip,
11
+ admin,
12
+ ipAdmin(scope, config) {
13
+ if (!scope) {
14
+ scope = 'os';
15
+ }
16
+ return admin.get(scope, config);
17
+ }
1333
18
  };