mm_ip 1.0.2 → 1.0.3
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/cache/ip/black.json +5 -5
- package/cache/ip/violation.json +1 -1
- package/cache/ip/white.json +1 -1
- package/eslint.config.js +68 -0
- package/index.js +596 -570
- package/package.json +2 -2
- package/test.js +0 -11
package/index.js
CHANGED
|
@@ -9,50 +9,62 @@ require('mm_expand');
|
|
|
9
9
|
* @class
|
|
10
10
|
*/
|
|
11
11
|
class Ip {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
violation_enable: true, // 启用违规记录
|
|
21
|
+
violation_max_count: 10, // 最大违规次数
|
|
22
|
+
violation_reset_time: 86400000, // 违规记录重置时间(24小时)
|
|
23
|
+
violation_auto_black: true, // 违规次数超限自动拉黑
|
|
24
|
+
dir: './cache/ip' // 持久化存储目录
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 创建IP管理实例
|
|
28
|
+
* @param {Object} config - 配置选项
|
|
29
|
+
*/
|
|
30
|
+
constructor(config) {
|
|
31
|
+
this._config = Object.assign({}, Ip.config);
|
|
32
|
+
|
|
33
|
+
this._white = new Set();
|
|
34
|
+
this._black = new Set();
|
|
35
|
+
this._req_count = new Map();
|
|
36
|
+
this._req_time = new Map();
|
|
37
|
+
this._black_time = new Map(); // 记录拉黑时间
|
|
38
|
+
this._violation_count = new Map(); // 违规次数记录
|
|
39
|
+
this._violation_time = new Map(); // 违规时间记录
|
|
40
|
+
this._logger = $.log || console;
|
|
41
|
+
|
|
42
|
+
this.setConfig(config);
|
|
43
|
+
// 自动加载持久化数据
|
|
44
|
+
if (this._config.dir) {
|
|
45
|
+
this.load();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 设置配置选项
|
|
52
|
+
* @param {Object} config - 配置选项
|
|
53
|
+
*/
|
|
54
|
+
Ip.prototype.setConfig = function(config) {
|
|
55
|
+
if (config) {
|
|
56
|
+
Object.assign(this._config, config);
|
|
57
|
+
}
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
61
|
* 设置日志器
|
|
50
62
|
* @param {Object} logger - 日志对象
|
|
51
63
|
*/
|
|
52
|
-
Ip.prototype.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
Ip.prototype.init = function(logger) {
|
|
65
|
+
if (logger) {
|
|
66
|
+
this._logger = logger;
|
|
67
|
+
}
|
|
56
68
|
};
|
|
57
69
|
|
|
58
70
|
/**
|
|
@@ -61,8 +73,8 @@ Ip.prototype.setup = function (logger) {
|
|
|
61
73
|
* @param {string} message - 日志消息
|
|
62
74
|
* @param {...*} args - 可选的日志参数
|
|
63
75
|
*/
|
|
64
|
-
Ip.prototype.
|
|
65
|
-
|
|
76
|
+
Ip.prototype.log = function(level, message, ...args) {
|
|
77
|
+
this._logger[level](`${this.constructor.name} ${message}`, ...args);
|
|
66
78
|
};
|
|
67
79
|
|
|
68
80
|
/**
|
|
@@ -71,23 +83,23 @@ Ip.prototype.logger = function (level, message, ...args) {
|
|
|
71
83
|
* @returns {boolean} 是否添加成功
|
|
72
84
|
* @throws {TypeError} 当IP非字符串时
|
|
73
85
|
*/
|
|
74
|
-
Ip.prototype.addWhite = function
|
|
75
|
-
|
|
86
|
+
Ip.prototype.addWhite = function(ip) {
|
|
87
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
if (this.getAllWhite().length >= this._config.max_white) {
|
|
90
|
+
this.log('warn', '白名单已满,无法添加IP:', ip);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
81
93
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
this._white.add(ip);
|
|
95
|
+
this.log('info', '添加白名单IP:', ip);
|
|
96
|
+
|
|
97
|
+
// 自动保存到持久化存储
|
|
98
|
+
if (this._config.dir) {
|
|
99
|
+
this.save();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
91
103
|
};
|
|
92
104
|
|
|
93
105
|
/**
|
|
@@ -96,23 +108,23 @@ Ip.prototype.addWhite = function (ip) {
|
|
|
96
108
|
* @returns {boolean} 是否添加成功
|
|
97
109
|
* @throws {TypeError} 当IP非字符串时
|
|
98
110
|
*/
|
|
99
|
-
Ip.prototype.addBlack = function
|
|
100
|
-
|
|
111
|
+
Ip.prototype.addBlack = function(ip) {
|
|
112
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
113
|
+
|
|
114
|
+
if (this.getAllBlack().length >= this._config.max_black) {
|
|
115
|
+
this.log('warn', '黑名单已满,无法添加IP:', ip);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
101
118
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
119
|
+
this._black.add(ip);
|
|
120
|
+
this.log('info', '添加黑名单IP:', ip);
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return true;
|
|
122
|
+
// 自动保存到持久化存储
|
|
123
|
+
if (this._config.dir) {
|
|
124
|
+
this.save();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
116
128
|
};
|
|
117
129
|
|
|
118
130
|
/**
|
|
@@ -121,19 +133,19 @@ Ip.prototype.addBlack = function (ip) {
|
|
|
121
133
|
* @returns {boolean} 是否删除成功
|
|
122
134
|
* @throws {TypeError} 当IP非字符串时
|
|
123
135
|
*/
|
|
124
|
-
Ip.prototype.delWhite = function
|
|
125
|
-
|
|
136
|
+
Ip.prototype.delWhite = function(ip) {
|
|
137
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
138
|
+
|
|
139
|
+
const result = this._white.delete(ip);
|
|
140
|
+
if (result) {
|
|
141
|
+
this.log('info', '删除白名单IP:', ip);
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
this.save();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return result;
|
|
143
|
+
// 自动保存到持久化存储
|
|
144
|
+
if (this._config.dir) {
|
|
145
|
+
this.save();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
137
149
|
};
|
|
138
150
|
|
|
139
151
|
/**
|
|
@@ -142,19 +154,19 @@ Ip.prototype.delWhite = function (ip) {
|
|
|
142
154
|
* @returns {boolean} 是否删除成功
|
|
143
155
|
* @throws {TypeError} 当IP非字符串时
|
|
144
156
|
*/
|
|
145
|
-
Ip.prototype.delBlack = function
|
|
146
|
-
|
|
157
|
+
Ip.prototype.delBlack = function(ip) {
|
|
158
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
147
159
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
const result = this._black.delete(ip);
|
|
161
|
+
if (result) {
|
|
162
|
+
this.log('info', '删除黑名单IP:', ip);
|
|
163
|
+
|
|
164
|
+
// 自动保存到持久化存储
|
|
165
|
+
if (this._config.dir) {
|
|
166
|
+
this.save();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
158
170
|
};
|
|
159
171
|
|
|
160
172
|
/**
|
|
@@ -163,9 +175,9 @@ Ip.prototype.delBlack = function (ip) {
|
|
|
163
175
|
* @returns {boolean} 是否在白名单
|
|
164
176
|
* @throws {TypeError} 当IP非字符串时
|
|
165
177
|
*/
|
|
166
|
-
Ip.prototype.isWhite = function
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
Ip.prototype.isWhite = function(ip) {
|
|
179
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
180
|
+
return this._white.has(ip);
|
|
169
181
|
};
|
|
170
182
|
|
|
171
183
|
/**
|
|
@@ -174,9 +186,9 @@ Ip.prototype.isWhite = function (ip) {
|
|
|
174
186
|
* @returns {boolean} 是否在黑名单
|
|
175
187
|
* @throws {TypeError} 当IP非字符串时
|
|
176
188
|
*/
|
|
177
|
-
Ip.prototype.isBlack = function
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
Ip.prototype.isBlack = function(ip) {
|
|
190
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
191
|
+
return this._black.has(ip);
|
|
180
192
|
};
|
|
181
193
|
|
|
182
194
|
/**
|
|
@@ -185,54 +197,54 @@ Ip.prototype.isBlack = function (ip) {
|
|
|
185
197
|
* @returns {boolean} 是否允许请求
|
|
186
198
|
* @throws {TypeError} 当IP非字符串时
|
|
187
199
|
*/
|
|
188
|
-
Ip.prototype.record = function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
200
|
+
Ip.prototype.record = function(ip) {
|
|
201
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
202
|
+
|
|
203
|
+
// 检查黑名单(包括过期检查)
|
|
204
|
+
if (this.isBlack(ip)) {
|
|
205
|
+
// 检查是否过期
|
|
206
|
+
if (this._isBlackExpired(ip)) {
|
|
207
|
+
this.delBlack(ip);
|
|
208
|
+
this.log('info', '黑名单IP已过期,自动移除:', ip);
|
|
209
|
+
} else {
|
|
210
|
+
this.log('warn', '黑名单IP请求被拒绝:', ip);
|
|
211
|
+
// 记录违规:黑名单IP尝试访问
|
|
212
|
+
if (this._config.violation_enable) {
|
|
213
|
+
this.recordViolation(ip, '黑名单IP尝试访问');
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 白名单直接通过
|
|
220
|
+
if (this.isWhite(ip)) {
|
|
221
|
+
this._updateReqCount(ip);
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 检查频率限制
|
|
226
|
+
if (!this._checkRateLimit(ip)) {
|
|
227
|
+
this.log('warn', 'IP请求频率超限:', ip);
|
|
228
|
+
|
|
229
|
+
// 记录违规:请求频率超限
|
|
230
|
+
if (this._config.violation_enable) {
|
|
231
|
+
this.recordViolation(ip, '请求频率超限');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 检查是否触发自动拉黑
|
|
235
|
+
if (this._config.auto_black_enable) {
|
|
236
|
+
const rate = this.getReqRate(ip);
|
|
237
|
+
if (rate >= this._config.auto_black_threshold) {
|
|
238
|
+
this._autoBlack(ip);
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this._updateReqCount(ip);
|
|
247
|
+
return true;
|
|
236
248
|
};
|
|
237
249
|
|
|
238
250
|
/**
|
|
@@ -241,9 +253,9 @@ Ip.prototype.record = function (ip) {
|
|
|
241
253
|
* @returns {number} 请求次数
|
|
242
254
|
* @throws {TypeError} 当IP非字符串时
|
|
243
255
|
*/
|
|
244
|
-
Ip.prototype.getReqCount = function
|
|
245
|
-
|
|
246
|
-
|
|
256
|
+
Ip.prototype.getReqCount = function(ip) {
|
|
257
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
258
|
+
return this._req_count.get(ip) || 0;
|
|
247
259
|
};
|
|
248
260
|
|
|
249
261
|
/**
|
|
@@ -252,106 +264,106 @@ Ip.prototype.getReqCount = function (ip) {
|
|
|
252
264
|
* @returns {number} 请求频率(每分钟请求数)
|
|
253
265
|
* @throws {TypeError} 当IP非字符串时
|
|
254
266
|
*/
|
|
255
|
-
Ip.prototype.getReqRate = function
|
|
256
|
-
|
|
267
|
+
Ip.prototype.getReqRate = function(ip) {
|
|
268
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
257
269
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const lastTime = this._req_time.get(ip) || now;
|
|
272
|
+
const count = this._req_count.get(ip) || 0;
|
|
261
273
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
if (now - lastTime < this._config.check_interval) {
|
|
275
|
+
// 在检查间隔内,计算实际频率
|
|
276
|
+
const elapsed = Math.max(1, now - lastTime); // 避免除零
|
|
277
|
+
return Math.round((count / elapsed) * this._config.check_interval);
|
|
278
|
+
} else {
|
|
279
|
+
// 超过检查间隔,频率为0
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
270
282
|
};
|
|
271
283
|
|
|
272
284
|
/**
|
|
273
285
|
* 获取所有IP的请求统计信息
|
|
274
286
|
* @returns {Object} IP统计信息对象
|
|
275
287
|
*/
|
|
276
|
-
Ip.prototype.getAllStats = function
|
|
277
|
-
|
|
278
|
-
|
|
288
|
+
Ip.prototype.getAllStats = function() {
|
|
289
|
+
const stats = {};
|
|
290
|
+
const now = Date.now();
|
|
279
291
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
292
|
+
// 收集所有有请求记录的IP
|
|
293
|
+
for (const ip of this.getAllReqIps()) {
|
|
294
|
+
const lastTime = this._req_time.get(ip) || now;
|
|
295
|
+
const isActive = now - lastTime < this._config.check_interval;
|
|
284
296
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
stats[ip] = {
|
|
298
|
+
count: this.getReqCount(ip),
|
|
299
|
+
rate: isActive ? this.getReqRate(ip) : 0,
|
|
300
|
+
last_time: lastTime,
|
|
301
|
+
is_white: this.isWhite(ip),
|
|
302
|
+
is_black: this.isBlack(ip),
|
|
303
|
+
black_time: this.getBlackExpireTime(ip)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
294
306
|
|
|
295
|
-
|
|
307
|
+
return stats;
|
|
296
308
|
};
|
|
297
309
|
|
|
298
310
|
/**
|
|
299
311
|
* 获取所有白名单IP
|
|
300
312
|
* @returns {Array} 白名单IP数组
|
|
301
313
|
*/
|
|
302
|
-
Ip.prototype.getAllWhite = function
|
|
303
|
-
|
|
314
|
+
Ip.prototype.getAllWhite = function() {
|
|
315
|
+
return Array.from(this._white);
|
|
304
316
|
};
|
|
305
317
|
|
|
306
318
|
/**
|
|
307
319
|
* 获取所有黑名单IP
|
|
308
320
|
* @returns {Array} 黑名单IP数组
|
|
309
321
|
*/
|
|
310
|
-
Ip.prototype.getAllBlack = function
|
|
311
|
-
|
|
322
|
+
Ip.prototype.getAllBlack = function() {
|
|
323
|
+
return Array.from(this._black);
|
|
312
324
|
};
|
|
313
325
|
|
|
314
326
|
/**
|
|
315
327
|
* 获取所有有请求记录的IP
|
|
316
328
|
* @returns {Array} 有请求记录的IP数组
|
|
317
329
|
*/
|
|
318
|
-
Ip.prototype.getAllReqIps = function
|
|
319
|
-
|
|
330
|
+
Ip.prototype.getAllReqIps = function() {
|
|
331
|
+
return Array.from(this._req_count.keys());
|
|
320
332
|
};
|
|
321
333
|
|
|
322
334
|
/**
|
|
323
335
|
* 清空白名单
|
|
324
336
|
*/
|
|
325
337
|
Ip.prototype.clearWhite = function() {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
338
|
+
this._white.clear();
|
|
339
|
+
this.log('info', '清空白名单');
|
|
340
|
+
|
|
341
|
+
// 自动保存到持久化存储
|
|
342
|
+
if (this._config.dir) {
|
|
343
|
+
this.save();
|
|
344
|
+
}
|
|
333
345
|
};
|
|
334
346
|
|
|
335
347
|
/**
|
|
336
348
|
* 清空黑名单
|
|
337
349
|
*/
|
|
338
350
|
Ip.prototype.clearBlack = function() {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
351
|
+
this._black.clear();
|
|
352
|
+
this.log('info', '清空黑名单');
|
|
353
|
+
|
|
354
|
+
// 自动保存到持久化存储
|
|
355
|
+
if (this._config.dir) {
|
|
356
|
+
this.save();
|
|
357
|
+
}
|
|
346
358
|
};
|
|
347
359
|
|
|
348
360
|
/**
|
|
349
361
|
* 清空所有请求记录
|
|
350
362
|
*/
|
|
351
|
-
Ip.prototype.clearReq = function
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
363
|
+
Ip.prototype.clearReq = function() {
|
|
364
|
+
this._req_count.clear();
|
|
365
|
+
this._req_time.clear();
|
|
366
|
+
this.log('info', '清空请求记录');
|
|
355
367
|
};
|
|
356
368
|
|
|
357
369
|
/**
|
|
@@ -360,22 +372,22 @@ Ip.prototype.clearReq = function () {
|
|
|
360
372
|
* @param {string} ip - IP地址
|
|
361
373
|
* @returns {boolean} 是否允许请求
|
|
362
374
|
*/
|
|
363
|
-
Ip.prototype._checkRateLimit = function
|
|
364
|
-
|
|
365
|
-
|
|
375
|
+
Ip.prototype._checkRateLimit = function(ip) {
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const lastTime = this._req_time.get(ip) || 0;
|
|
366
378
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
379
|
+
// 检查时间间隔
|
|
380
|
+
if (now - lastTime < this._config.check_interval) {
|
|
381
|
+
const count = this.getReqCount(ip);
|
|
382
|
+
if (count >= this._config.max_req_per_min) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
// 重置计数
|
|
387
|
+
this._req_count.set(ip, 0);
|
|
388
|
+
}
|
|
377
389
|
|
|
378
|
-
|
|
390
|
+
return true;
|
|
379
391
|
};
|
|
380
392
|
|
|
381
393
|
/**
|
|
@@ -383,10 +395,10 @@ Ip.prototype._checkRateLimit = function (ip) {
|
|
|
383
395
|
* @private
|
|
384
396
|
* @param {string} ip - IP地址
|
|
385
397
|
*/
|
|
386
|
-
Ip.prototype._updateReqCount = function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
398
|
+
Ip.prototype._updateReqCount = function(ip) {
|
|
399
|
+
const count = this.getReqCount(ip) + 1;
|
|
400
|
+
this._req_count.set(ip, count);
|
|
401
|
+
this._req_time.set(ip, Date.now());
|
|
390
402
|
};
|
|
391
403
|
|
|
392
404
|
/**
|
|
@@ -395,9 +407,9 @@ Ip.prototype._updateReqCount = function (ip) {
|
|
|
395
407
|
* @param {string} ip - IP地址
|
|
396
408
|
* @returns {boolean} 是否有效IP
|
|
397
409
|
*/
|
|
398
|
-
Ip.prototype._validIp = function
|
|
399
|
-
|
|
400
|
-
|
|
410
|
+
Ip.prototype._validIp = function(ip) {
|
|
411
|
+
const ipRegex = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
|
412
|
+
return ipRegex.test(ip);
|
|
401
413
|
};
|
|
402
414
|
|
|
403
415
|
/**
|
|
@@ -405,18 +417,18 @@ Ip.prototype._validIp = function (ip) {
|
|
|
405
417
|
* @param {Array} ip_list - IP地址数组
|
|
406
418
|
* @returns {number} 成功添加的数量
|
|
407
419
|
*/
|
|
408
|
-
Ip.prototype.addWhiteBatch = function
|
|
409
|
-
|
|
420
|
+
Ip.prototype.addWhiteBatch = function(ip_list) {
|
|
421
|
+
if (!Array.isArray(ip_list)) throw new TypeError('ip_list must be array');
|
|
410
422
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
423
|
+
let success = 0;
|
|
424
|
+
for (const ip of ip_list) {
|
|
425
|
+
if (typeof ip === 'string' && this._validIp(ip)) {
|
|
426
|
+
if (this.addWhite(ip)) {
|
|
427
|
+
success++;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return success;
|
|
420
432
|
};
|
|
421
433
|
|
|
422
434
|
/**
|
|
@@ -424,18 +436,18 @@ Ip.prototype.addWhiteBatch = function (ip_list) {
|
|
|
424
436
|
* @param {Array} ip_list - IP地址数组
|
|
425
437
|
* @returns {number} 成功添加的数量
|
|
426
438
|
*/
|
|
427
|
-
Ip.prototype.addBlackBatch = function
|
|
428
|
-
|
|
439
|
+
Ip.prototype.addBlackBatch = function(ip_list) {
|
|
440
|
+
if (!Array.isArray(ip_list)) throw new TypeError('ip_list must be array');
|
|
429
441
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
let success = 0;
|
|
443
|
+
for (const ip of ip_list) {
|
|
444
|
+
if (typeof ip === 'string' && this._validIp(ip)) {
|
|
445
|
+
if (this.addBlack(ip)) {
|
|
446
|
+
success++;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return success;
|
|
439
451
|
};
|
|
440
452
|
|
|
441
453
|
/**
|
|
@@ -445,41 +457,41 @@ Ip.prototype.addBlackBatch = function (ip_list) {
|
|
|
445
457
|
* @returns {boolean} 是否拉黑成功
|
|
446
458
|
* @throws {TypeError} 当IP非字符串时
|
|
447
459
|
*/
|
|
448
|
-
Ip.prototype.blackByHighFreq = function
|
|
449
|
-
|
|
460
|
+
Ip.prototype.blackByHighFreq = function(ip, duration) {
|
|
461
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
450
462
|
|
|
451
|
-
|
|
463
|
+
const black_duration = duration || this._config.auto_black_duration;
|
|
452
464
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
465
|
+
if (this.addBlack(ip)) {
|
|
466
|
+
this._black_time.set(ip, Date.now() + black_duration);
|
|
467
|
+
this.log('warn', '手动高频拉黑IP:', ip, '持续时间:', black_duration + 'ms');
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
458
470
|
|
|
459
|
-
|
|
471
|
+
return false;
|
|
460
472
|
};
|
|
461
473
|
|
|
462
474
|
/**
|
|
463
475
|
* 检查并清理过期的黑名单IP(私有方法)
|
|
464
476
|
* @private
|
|
465
477
|
*/
|
|
466
|
-
Ip.prototype._cleanExpiredBlack = function
|
|
467
|
-
|
|
468
|
-
|
|
478
|
+
Ip.prototype._cleanExpiredBlack = function() {
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
const expired = [];
|
|
469
481
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
482
|
+
// 获取所有有过期时间的黑名单IP
|
|
483
|
+
for (const ip of this.getAllBlack()) {
|
|
484
|
+
const expireTime = this.getBlackExpireTime(ip);
|
|
485
|
+
if (expireTime && now > expireTime) {
|
|
486
|
+
expired.push(ip);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
477
489
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
490
|
+
for (const ip of expired) {
|
|
491
|
+
this.delBlack(ip);
|
|
492
|
+
this._black_time.delete(ip);
|
|
493
|
+
this.log('info', '清理过期黑名单IP:', ip);
|
|
494
|
+
}
|
|
483
495
|
};
|
|
484
496
|
|
|
485
497
|
/**
|
|
@@ -488,11 +500,11 @@ Ip.prototype._cleanExpiredBlack = function () {
|
|
|
488
500
|
* @param {string} ip - IP地址
|
|
489
501
|
* @returns {boolean} 是否过期
|
|
490
502
|
*/
|
|
491
|
-
Ip.prototype._isBlackExpired = function
|
|
492
|
-
|
|
493
|
-
|
|
503
|
+
Ip.prototype._isBlackExpired = function(ip) {
|
|
504
|
+
const expireTime = this.getBlackExpireTime(ip);
|
|
505
|
+
if (!expireTime) return false; // 没有设置过期时间,永不过期
|
|
494
506
|
|
|
495
|
-
|
|
507
|
+
return Date.now() > expireTime;
|
|
496
508
|
};
|
|
497
509
|
|
|
498
510
|
/**
|
|
@@ -500,13 +512,13 @@ Ip.prototype._isBlackExpired = function (ip) {
|
|
|
500
512
|
* @private
|
|
501
513
|
* @param {string} ip - IP地址
|
|
502
514
|
*/
|
|
503
|
-
Ip.prototype._autoBlack = function
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
515
|
+
Ip.prototype._autoBlack = function(ip) {
|
|
516
|
+
if (this.addBlack(ip)) {
|
|
517
|
+
this._black_time.set(ip, Date.now() + this._config.auto_black_duration);
|
|
518
|
+
this.log('warn', '自动高频拉黑IP:', ip,
|
|
519
|
+
'频率:', this.getReqRate(ip) + '/分钟',
|
|
520
|
+
'持续时间:', this._config.auto_black_duration + 'ms');
|
|
521
|
+
}
|
|
510
522
|
};
|
|
511
523
|
|
|
512
524
|
/**
|
|
@@ -515,23 +527,23 @@ Ip.prototype._autoBlack = function (ip) {
|
|
|
515
527
|
* @returns {number|null} 过期时间戳(毫秒),null表示永不过期
|
|
516
528
|
* @throws {TypeError} 当IP非字符串时
|
|
517
529
|
*/
|
|
518
|
-
Ip.prototype.getBlackExpireTime = function
|
|
519
|
-
|
|
530
|
+
Ip.prototype.getBlackExpireTime = function(ip) {
|
|
531
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
520
532
|
|
|
521
|
-
|
|
533
|
+
if (!this.isBlack(ip)) return null;
|
|
522
534
|
|
|
523
|
-
|
|
524
|
-
|
|
535
|
+
const expireTime = this._black_time.get(ip);
|
|
536
|
+
return expireTime || null;
|
|
525
537
|
};
|
|
526
538
|
|
|
527
539
|
/**
|
|
528
540
|
* 清理所有过期的黑名单IP
|
|
529
541
|
* @returns {number} 清理的数量
|
|
530
542
|
*/
|
|
531
|
-
Ip.prototype.cleanExpiredBlack = function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
543
|
+
Ip.prototype.cleanExpiredBlack = function() {
|
|
544
|
+
const beforeCount = this.getAllBlack().length;
|
|
545
|
+
this._cleanExpiredBlack();
|
|
546
|
+
return beforeCount - this.getAllBlack().length;
|
|
535
547
|
};
|
|
536
548
|
|
|
537
549
|
/**
|
|
@@ -541,26 +553,35 @@ Ip.prototype.cleanExpiredBlack = function () {
|
|
|
541
553
|
* @returns {number} 当前违规次数
|
|
542
554
|
*/
|
|
543
555
|
Ip.prototype.recordViolation = function(ip, reason) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
556
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
557
|
+
if (!this._config.violation_enable) return 0;
|
|
558
|
+
|
|
559
|
+
this._cleanExpiredViolation();
|
|
560
|
+
|
|
561
|
+
const now = Date.now();
|
|
562
|
+
const count = this.getViolationCount(ip) + 1;
|
|
563
|
+
|
|
564
|
+
this._violation_count.set(ip, count);
|
|
565
|
+
this._violation_time.set(ip, now);
|
|
566
|
+
|
|
567
|
+
this.log('info', 'IP违规记录:', {
|
|
568
|
+
ip,
|
|
569
|
+
reason,
|
|
570
|
+
count,
|
|
571
|
+
time: new Date(now).toISOString()
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// 检查是否达到自动拉黑条件
|
|
575
|
+
if (this._config.violation_auto_black && count >= this._config.violation_max_count) {
|
|
576
|
+
this.blackByHighFreq(ip, this._config.auto_black_duration);
|
|
577
|
+
this.log('warn', 'IP因违规次数超限自动拉黑:', {
|
|
578
|
+
ip,
|
|
579
|
+
count,
|
|
580
|
+
duration: this._config.auto_black_duration
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return count;
|
|
564
585
|
};
|
|
565
586
|
|
|
566
587
|
/**
|
|
@@ -569,10 +590,10 @@ Ip.prototype.recordViolation = function(ip, reason) {
|
|
|
569
590
|
* @returns {number} 违规次数
|
|
570
591
|
*/
|
|
571
592
|
Ip.prototype.getViolationCount = function(ip) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
593
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
594
|
+
|
|
595
|
+
this._cleanExpiredViolation();
|
|
596
|
+
return this._violation_count.get(ip) || 0;
|
|
576
597
|
};
|
|
577
598
|
|
|
578
599
|
/**
|
|
@@ -581,13 +602,13 @@ Ip.prototype.getViolationCount = function(ip) {
|
|
|
581
602
|
* @returns {boolean} 是否成功重置
|
|
582
603
|
*/
|
|
583
604
|
Ip.prototype.resetViolation = function(ip) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
605
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
606
|
+
|
|
607
|
+
const hadRecord = this._violation_count.has(ip);
|
|
608
|
+
this._violation_count.delete(ip);
|
|
609
|
+
this._violation_time.delete(ip);
|
|
610
|
+
|
|
611
|
+
return hadRecord;
|
|
591
612
|
};
|
|
592
613
|
|
|
593
614
|
/**
|
|
@@ -595,7 +616,7 @@ Ip.prototype.resetViolation = function(ip) {
|
|
|
595
616
|
* @returns {Array} 有违规记录的IP数组
|
|
596
617
|
*/
|
|
597
618
|
Ip.prototype.getAllViolationIps = function() {
|
|
598
|
-
|
|
619
|
+
return Array.from(this._violation_count.keys());
|
|
599
620
|
};
|
|
600
621
|
|
|
601
622
|
/**
|
|
@@ -604,15 +625,15 @@ Ip.prototype.getAllViolationIps = function() {
|
|
|
604
625
|
* @returns {Object|null} 违规信息
|
|
605
626
|
*/
|
|
606
627
|
Ip.prototype.getViolationInfo = function(ip) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
628
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
629
|
+
|
|
630
|
+
const count = this.getViolationCount(ip);
|
|
631
|
+
if (count === 0) return null;
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
count: count,
|
|
635
|
+
time: this._violation_time.get(ip)
|
|
636
|
+
};
|
|
616
637
|
};
|
|
617
638
|
|
|
618
639
|
/**
|
|
@@ -620,14 +641,14 @@ Ip.prototype.getViolationInfo = function(ip) {
|
|
|
620
641
|
* @returns {Object} 违规统计信息
|
|
621
642
|
*/
|
|
622
643
|
Ip.prototype.getAllViolations = function() {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
644
|
+
this._cleanExpiredViolation();
|
|
645
|
+
|
|
646
|
+
const violations = {};
|
|
647
|
+
for (const ip of this.getAllViolationIps()) {
|
|
648
|
+
violations[ip] = this.getViolationInfo(ip);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return violations;
|
|
631
652
|
};
|
|
632
653
|
|
|
633
654
|
/**
|
|
@@ -635,34 +656,34 @@ Ip.prototype.getAllViolations = function() {
|
|
|
635
656
|
* @private
|
|
636
657
|
*/
|
|
637
658
|
Ip.prototype._cleanExpiredViolation = function() {
|
|
638
|
-
|
|
639
|
-
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
const expired = [];
|
|
640
661
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
662
|
+
// 直接访问内部数据结构避免递归调用
|
|
663
|
+
for (const [ip, time] of this._violation_time) {
|
|
664
|
+
if (now - time > this._config.violation_reset_time) {
|
|
665
|
+
expired.push(ip);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
647
668
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
669
|
+
for (const ip of expired) {
|
|
670
|
+
this._violation_count.delete(ip);
|
|
671
|
+
this._violation_time.delete(ip);
|
|
672
|
+
}
|
|
652
673
|
};
|
|
653
674
|
|
|
654
675
|
/**
|
|
655
676
|
* 清空所有违规记录
|
|
656
677
|
*/
|
|
657
678
|
Ip.prototype.clearViolations = function() {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
679
|
+
this._violation_count.clear();
|
|
680
|
+
this._violation_time.clear();
|
|
681
|
+
this.log('info', '清空所有违规记录');
|
|
682
|
+
|
|
683
|
+
// 自动保存到持久化存储
|
|
684
|
+
if (this._config.dir) {
|
|
685
|
+
this.save();
|
|
686
|
+
}
|
|
666
687
|
};
|
|
667
688
|
|
|
668
689
|
/**
|
|
@@ -670,53 +691,53 @@ Ip.prototype.clearViolations = function() {
|
|
|
670
691
|
* @returns {boolean} 是否保存成功
|
|
671
692
|
*/
|
|
672
693
|
Ip.prototype.save = function() {
|
|
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
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
694
|
+
try {
|
|
695
|
+
const fs = require('fs');
|
|
696
|
+
const path = require('path');
|
|
697
|
+
|
|
698
|
+
// 确保存储目录存在
|
|
699
|
+
const dir = this._config.dir;
|
|
700
|
+
dir.addDir();
|
|
701
|
+
|
|
702
|
+
// 分别保存不同数据到不同文件
|
|
703
|
+
const timestamp = Date.now();
|
|
704
|
+
|
|
705
|
+
// 保存白名单数据
|
|
706
|
+
const whiteData = {
|
|
707
|
+
white: this.getAllWhite(),
|
|
708
|
+
timestamp: timestamp
|
|
709
|
+
};
|
|
710
|
+
const whitePath = path.join(dir, 'white.json');
|
|
711
|
+
fs.writeFileSync(whitePath, JSON.stringify(whiteData, null, 2), 'utf8');
|
|
712
|
+
|
|
713
|
+
// 保存黑名单数据
|
|
714
|
+
const blackData = {
|
|
715
|
+
black: this.getAllBlack(),
|
|
716
|
+
black_time: Array.from(this._black_time.entries()),
|
|
717
|
+
timestamp: timestamp
|
|
718
|
+
};
|
|
719
|
+
const blackPath = path.join(dir, 'black.json');
|
|
720
|
+
fs.writeFileSync(blackPath, JSON.stringify(blackData, null, 2), 'utf8');
|
|
721
|
+
|
|
722
|
+
// 保存违规记录数据
|
|
723
|
+
const violationData = {
|
|
724
|
+
violation_count: Array.from(this._violation_count.entries()),
|
|
725
|
+
violation_time: Array.from(this._violation_time.entries()),
|
|
726
|
+
timestamp: timestamp
|
|
727
|
+
};
|
|
728
|
+
const violationPath = path.join(dir, 'violation.json');
|
|
729
|
+
fs.writeFileSync(violationPath, JSON.stringify(violationData, null, 2), 'utf8');
|
|
730
|
+
|
|
731
|
+
this.log('info', 'IP数据已分文件保存:', {
|
|
732
|
+
white: whitePath,
|
|
733
|
+
black: blackPath,
|
|
734
|
+
violation: violationPath
|
|
735
|
+
});
|
|
736
|
+
return true;
|
|
737
|
+
} catch (err) {
|
|
738
|
+
this.log('error', '保存IP数据失败:', err.message);
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
720
741
|
};
|
|
721
742
|
|
|
722
743
|
/**
|
|
@@ -724,101 +745,101 @@ Ip.prototype.save = function() {
|
|
|
724
745
|
* @returns {boolean} 是否加载成功
|
|
725
746
|
*/
|
|
726
747
|
Ip.prototype.load = function() {
|
|
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
|
-
|
|
748
|
+
try {
|
|
749
|
+
const fs = require('fs');
|
|
750
|
+
const path = require('path');
|
|
751
|
+
|
|
752
|
+
const dir = this._config.dir;
|
|
753
|
+
let loadedCount = 0;
|
|
754
|
+
|
|
755
|
+
// 加载白名单数据
|
|
756
|
+
const whitePath = path.join(dir, 'white.json');
|
|
757
|
+
if (fs.existsSync(whitePath)) {
|
|
758
|
+
const content = fs.readFileSync(whitePath, 'utf8');
|
|
759
|
+
const data = JSON.parse(content);
|
|
760
|
+
|
|
761
|
+
if (Array.isArray(data.white)) {
|
|
762
|
+
for (const ip of data.white) {
|
|
763
|
+
if (typeof ip === 'string') {
|
|
764
|
+
this._white.add(ip);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
loadedCount++;
|
|
768
|
+
this.log('info', '白名单数据已加载:', whitePath);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 加载黑名单数据
|
|
773
|
+
const blackPath = path.join(dir, 'black.json');
|
|
774
|
+
if (fs.existsSync(blackPath)) {
|
|
775
|
+
const content = fs.readFileSync(blackPath, 'utf8');
|
|
776
|
+
const data = JSON.parse(content);
|
|
777
|
+
|
|
778
|
+
// 加载黑名单
|
|
779
|
+
if (Array.isArray(data.black)) {
|
|
780
|
+
for (const ip of data.black) {
|
|
781
|
+
if (typeof ip === 'string') {
|
|
782
|
+
this._black.add(ip);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 加载黑名单过期时间
|
|
788
|
+
if (Array.isArray(data.black_time)) {
|
|
789
|
+
for (const [ip, time] of data.black_time) {
|
|
790
|
+
if (typeof ip === 'string' && typeof time === 'number') {
|
|
791
|
+
this._black_time.set(ip, time);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
loadedCount++;
|
|
797
|
+
this.log('info', '黑名单数据已加载:', blackPath);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 加载违规记录数据
|
|
801
|
+
const violationPath = path.join(dir, 'violation.json');
|
|
802
|
+
if (fs.existsSync(violationPath)) {
|
|
803
|
+
const content = fs.readFileSync(violationPath, 'utf8');
|
|
804
|
+
const data = JSON.parse(content);
|
|
805
|
+
|
|
806
|
+
// 加载违规记录
|
|
807
|
+
if (Array.isArray(data.violation_count)) {
|
|
808
|
+
for (const [ip, count] of data.violation_count) {
|
|
809
|
+
if (typeof ip === 'string' && typeof count === 'number') {
|
|
810
|
+
this._violation_count.set(ip, count);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (Array.isArray(data.violation_time)) {
|
|
816
|
+
for (const [ip, time] of data.violation_time) {
|
|
817
|
+
if (typeof ip === 'string' && typeof time === 'number') {
|
|
818
|
+
this._violation_time.set(ip, time);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
loadedCount++;
|
|
824
|
+
this.log('info', '违规记录数据已加载:', violationPath);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (loadedCount === 0) {
|
|
828
|
+
this.log('info', '未找到任何IP数据文件,跳过加载');
|
|
829
|
+
} else {
|
|
830
|
+
this.log('info', 'IP数据加载完成,统计:', {
|
|
831
|
+
white_count: this.getAllWhite().length,
|
|
832
|
+
black_count: this.getAllBlack().length,
|
|
833
|
+
violation_count: this.getAllViolationIps().length,
|
|
834
|
+
files_loaded: loadedCount
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return true;
|
|
839
|
+
} catch (err) {
|
|
840
|
+
this.log('error', '加载IP数据失败:', err.message);
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
822
843
|
};
|
|
823
844
|
|
|
824
845
|
/**
|
|
@@ -826,26 +847,26 @@ Ip.prototype.load = function() {
|
|
|
826
847
|
* @param {number} interval - 保存间隔(毫秒),默认5分钟
|
|
827
848
|
*/
|
|
828
849
|
Ip.prototype.autoSave = function(interval = 300000) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
850
|
+
if (this._autoSaveTimer) {
|
|
851
|
+
clearInterval(this._autoSaveTimer);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
this._autoSaveTimer = setInterval(() => {
|
|
855
|
+
this.save();
|
|
856
|
+
}, interval);
|
|
857
|
+
|
|
858
|
+
this.log('info', '自动保存已启动,间隔:', interval, 'ms');
|
|
838
859
|
};
|
|
839
860
|
|
|
840
861
|
/**
|
|
841
862
|
* 停止自动保存
|
|
842
863
|
*/
|
|
843
864
|
Ip.prototype.stopAutoSave = function() {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
865
|
+
if (this._autoSaveTimer) {
|
|
866
|
+
clearInterval(this._autoSaveTimer);
|
|
867
|
+
this._autoSaveTimer = null;
|
|
868
|
+
this.log('info', '自动保存已停止');
|
|
869
|
+
}
|
|
849
870
|
};
|
|
850
871
|
|
|
851
872
|
/**
|
|
@@ -855,61 +876,66 @@ Ip.prototype.stopAutoSave = function() {
|
|
|
855
876
|
* @throws {TypeError} 当IP非字符串时
|
|
856
877
|
*/
|
|
857
878
|
Ip.prototype.del = function(ip) {
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
879
|
+
if (typeof ip !== 'string') throw new TypeError('ip must be string');
|
|
880
|
+
|
|
881
|
+
const result = {
|
|
882
|
+
black: false,
|
|
883
|
+
white: false,
|
|
884
|
+
violation: false,
|
|
885
|
+
black_time: false,
|
|
886
|
+
violation_count: false,
|
|
887
|
+
violation_time: false
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// 从黑名单删除
|
|
891
|
+
if (this._black.has(ip)) {
|
|
892
|
+
this._black.delete(ip);
|
|
893
|
+
result.black = true;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// 从白名单删除
|
|
897
|
+
if (this._white.has(ip)) {
|
|
898
|
+
this._white.delete(ip);
|
|
899
|
+
result.white = true;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// 从违规记录删除
|
|
903
|
+
if (this._violation_count.has(ip)) {
|
|
904
|
+
this._violation_count.delete(ip);
|
|
905
|
+
result.violation_count = true;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (this._violation_time.has(ip)) {
|
|
909
|
+
this._violation_time.delete(ip);
|
|
910
|
+
result.violation_time = true;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// 从黑名单过期时间记录删除
|
|
914
|
+
if (this._black_time.has(ip)) {
|
|
915
|
+
this._black_time.delete(ip);
|
|
916
|
+
result.black_time = true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// 如果有任何删除操作,记录日志并自动保存
|
|
920
|
+
if (Object.values(result).some(Boolean)) {
|
|
921
|
+
this.log('info', '删除IP记录:', {
|
|
922
|
+
ip,
|
|
923
|
+
result
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// 自动保存到持久化存储
|
|
927
|
+
if (this._config.dir) {
|
|
928
|
+
this.save();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return result;
|
|
909
933
|
};
|
|
910
934
|
|
|
911
935
|
if (global.$ && !$.ip) {
|
|
912
|
-
|
|
936
|
+
$.ip = new Ip();
|
|
913
937
|
}
|
|
914
938
|
|
|
915
|
-
module.exports = {
|
|
939
|
+
module.exports = {
|
|
940
|
+
Ip
|
|
941
|
+
};
|