Create requestLogger.mjs
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import {Mutex} from 'async-mutex';
|
||||
import winston from 'winston';
|
||||
|
||||
// 请求日志记录
|
||||
const logFilePath = path.join(process.cwd(), 'request_logs.log');
|
||||
|
||||
/*
|
||||
* 黑名单文件格式:
|
||||
* {
|
||||
* "ip1": {
|
||||
* "permanent": true
|
||||
* },
|
||||
* "ip2": {
|
||||
* "permanent": true
|
||||
* }
|
||||
* ...
|
||||
*/
|
||||
const blacklistFilePath = path.join(process.cwd(), 'ip_blacklist.json');
|
||||
|
||||
/*
|
||||
* 临时限制文件格式:
|
||||
* {
|
||||
* "ip1": 1630000000000,
|
||||
* "ip2": 1630000000000
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
const tempLimitFilePath = path.join(process.cwd(), 'temp_limits.json');
|
||||
|
||||
// 是否启用检测
|
||||
const ENABLE_DETECTION = process.env.ENABLE_DETECTION !== 'false';
|
||||
|
||||
class RequestLogger {
|
||||
constructor() {
|
||||
// 日志记录
|
||||
this.logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({format: 'YYYY年MM月DD日 HH时mm分ss秒'}),
|
||||
winston.format.printf(({timestamp, level, message}) => {
|
||||
return `${timestamp} | ${message}`;
|
||||
})
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.File({filename: logFilePath})
|
||||
]
|
||||
});
|
||||
try {
|
||||
if (!fs.existsSync(blacklistFilePath)) {
|
||||
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`初始化黑名单文件出错:${err.message}`);
|
||||
}
|
||||
|
||||
this.ipRequestRecords = {}; // 每个IP的请求时间记录
|
||||
this.ip30sTriggerCount = {}; // 统计每个IP触发30秒限制的次数
|
||||
this.temporaryLimits = {}; // 每个IP的临时限制到期时间
|
||||
this.ipMutexes = {}; // 为每个IP分配一个独立的Mutex
|
||||
this.blacklistMutex = new Mutex(); // 黑名单文件的Mutex
|
||||
try {
|
||||
if (!fs.existsSync(tempLimitFilePath)) {
|
||||
fs.writeFileSync(tempLimitFilePath, JSON.stringify({}), 'utf8');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`初始化临时限制文件出错:${err.message}`);
|
||||
}
|
||||
|
||||
// 加载临时限制
|
||||
this.loadTempLimitsFromFile();
|
||||
|
||||
// 监听文件变化
|
||||
this.setupTempLimitsFileWatcher();
|
||||
|
||||
// 清理无活动IP
|
||||
setInterval(() => {
|
||||
this.cleanUpInactiveIPs();
|
||||
}, 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
// 获取IP的Mutex
|
||||
getMutexForIP(ip) {
|
||||
if (!this.ipMutexes[ip]) {
|
||||
this.ipMutexes[ip] = new Mutex();
|
||||
}
|
||||
return this.ipMutexes[ip];
|
||||
}
|
||||
|
||||
// 从文件加载临时限制
|
||||
loadTempLimitsFromFile() {
|
||||
try {
|
||||
const dataRaw = fs.readFileSync(tempLimitFilePath, 'utf8');
|
||||
if (!dataRaw.trim()) {
|
||||
// 文件为空,则 no-op
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(dataRaw);
|
||||
const now = Date.now();
|
||||
// 清空内存
|
||||
this.temporaryLimits = {};
|
||||
for (const ip in parsed) {
|
||||
const expireTime = parseInt(parsed[ip], 10);
|
||||
if (!isNaN(expireTime) && expireTime > now) {
|
||||
this.temporaryLimits[ip] = expireTime;
|
||||
}
|
||||
}
|
||||
this.saveTempLimitsToFile();
|
||||
} catch (error) {
|
||||
console.error(`读取临时限制文件出错:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
saveTempLimitsToFile() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
// 移除过期记录
|
||||
for (const ip in this.temporaryLimits) {
|
||||
if (this.temporaryLimits[ip] <= now) {
|
||||
delete this.temporaryLimits[ip];
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(
|
||||
tempLimitFilePath,
|
||||
JSON.stringify(this.temporaryLimits, null, 4),
|
||||
'utf8'
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`写入临时限制文件出错:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
setupTempLimitsFileWatcher() {
|
||||
try {
|
||||
fs.watch(tempLimitFilePath, (eventType, filename) => {
|
||||
if (eventType === 'change') {
|
||||
this.loadTempLimitsFromFile();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`监听临时限制文件出错:${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否在黑名单
|
||||
async isBlacklisted(ip) {
|
||||
return await this.blacklistMutex.runExclusive(async () => {
|
||||
try {
|
||||
if (!fs.existsSync(blacklistFilePath)) {
|
||||
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
||||
}
|
||||
const blacklistData = fs.readFileSync(blacklistFilePath, 'utf8');
|
||||
const blacklist = JSON.parse(blacklistData || '{}');
|
||||
const info = blacklist[ip];
|
||||
if (info && info.permanent) {
|
||||
return true;
|
||||
} else if (info) {
|
||||
// 移除非法
|
||||
delete blacklist[ip];
|
||||
fs.writeFileSync(blacklistFilePath, JSON.stringify(blacklist, null, 4), 'utf8');
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error(`读取黑名单文件出错:${err.message}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 将IP添加到黑名单
|
||||
async addToBlacklist(ip) {
|
||||
await this.blacklistMutex.runExclusive(async () => {
|
||||
try {
|
||||
if (!fs.existsSync(blacklistFilePath)) {
|
||||
fs.writeFileSync(blacklistFilePath, JSON.stringify({}), 'utf8');
|
||||
}
|
||||
const dataRaw = fs.readFileSync(blacklistFilePath, 'utf8');
|
||||
const blacklist = JSON.parse(dataRaw || '{}');
|
||||
|
||||
if (!blacklist[ip]) {
|
||||
blacklist[ip] = {permanent: true};
|
||||
fs.writeFileSync(
|
||||
blacklistFilePath,
|
||||
JSON.stringify(blacklist, null, 4),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`写入黑名单文件出错:${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有临时限制
|
||||
_hasTemporaryLimit(ip) {
|
||||
const now = Date.now();
|
||||
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
|
||||
return true;
|
||||
}
|
||||
delete this.temporaryLimits[ip];
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取剩余限制时间
|
||||
_getRemainingTime(ip) {
|
||||
const now = Date.now();
|
||||
if (this.temporaryLimits[ip] && now < this.temporaryLimits[ip]) {
|
||||
const remaining = this.temporaryLimits[ip] - now;
|
||||
const hh = Math.floor(remaining / 3600000);
|
||||
const mm = Math.floor((remaining % 3600000) / 60000);
|
||||
const ss = Math.floor((remaining % 60000) / 1000);
|
||||
if (hh > 0) {
|
||||
return `${hh}小时${mm}分${ss}秒`;
|
||||
} else if (mm > 0) {
|
||||
return `${mm}分${ss}秒`;
|
||||
} else {
|
||||
return `${ss}秒`;
|
||||
}
|
||||
}
|
||||
return '未知时间';
|
||||
}
|
||||
|
||||
// 清理过期的请求记录
|
||||
_cleanUpRecords(ip) {
|
||||
const now = Date.now();
|
||||
// 请求记录保留24小时
|
||||
if (this.ipRequestRecords[ip]) {
|
||||
this.ipRequestRecords[ip] = this.ipRequestRecords[ip].filter(
|
||||
t => now - t <= 24 * 60 * 60 * 1000
|
||||
);
|
||||
}
|
||||
// 30秒触发记录同样保留24小时
|
||||
if (this.ip30sTriggerCount[ip]) {
|
||||
this.ip30sTriggerCount[ip] = this.ip30sTriggerCount[ip].filter(
|
||||
t => now - t <= 24 * 60 * 60 * 1000
|
||||
);
|
||||
}
|
||||
// 清理过期的临时限制
|
||||
if (this.temporaryLimits[ip] && now >= this.temporaryLimits[ip]) {
|
||||
delete this.temporaryLimits[ip];
|
||||
this.saveTempLimitsToFile();
|
||||
}
|
||||
}
|
||||
|
||||
// 定期清理长期未活动的IP数据
|
||||
cleanUpInactiveIPs() {
|
||||
for (const ip in this.ipRequestRecords) {
|
||||
const noRecentRequests = !this.ipRequestRecords[ip] || this.ipRequestRecords[ip].length === 0;
|
||||
const no30sTriggers = !this.ip30sTriggerCount[ip] || this.ip30sTriggerCount[ip].length === 0;
|
||||
|
||||
// 若该IP无请求记录、无30秒触发记录且无临时限制
|
||||
if (noRecentRequests && no30sTriggers && !this._hasTemporaryLimit(ip)) {
|
||||
delete this.ipRequestRecords[ip];
|
||||
delete this.ip30sTriggerCount[ip];
|
||||
delete this.ipMutexes[ip];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录请求并检测
|
||||
async logRequest({time, ip, location, model, session}) {
|
||||
if (ENABLE_DETECTION && await this.isBlacklisted(ip)) {
|
||||
throw new Error(`您已被永久限制。`);
|
||||
}
|
||||
const baseInfo = `IP: ${ip} | Location: ${location} | Model: ${model} | Session: ${session}`;
|
||||
if (!ENABLE_DETECTION) {
|
||||
this.logger.info(baseInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
let limitInfo = '';
|
||||
const ipMutex = this.getMutexForIP(ip);
|
||||
let needLog = true;
|
||||
|
||||
try {
|
||||
await ipMutex.runExclusive(() => {
|
||||
const now = Date.now();
|
||||
|
||||
// 检查临时限制
|
||||
if (this._hasTemporaryLimit(ip)) {
|
||||
const rt = this._getRemainingTime(ip);
|
||||
limitInfo = ` | Limit: ${this.temporaryLimits[ip] - now}ms`;
|
||||
throw new Error(`您被限制访问,请在${rt}后再试。`);
|
||||
}
|
||||
|
||||
// 记录请求 & 清理
|
||||
if (!this.ipRequestRecords[ip]) {
|
||||
this.ipRequestRecords[ip] = [];
|
||||
}
|
||||
this.ipRequestRecords[ip].push(now);
|
||||
this._cleanUpRecords(ip);
|
||||
|
||||
// >=4次 → 加入黑名单
|
||||
const requestsIn6s = this.ipRequestRecords[ip].filter(
|
||||
t => now - t <= 6 * 1000
|
||||
);
|
||||
if (requestsIn6s.length >= 3) {
|
||||
this.addToBlacklist(ip);
|
||||
limitInfo = ' | Limit: PERMANENT';
|
||||
throw new Error(`并发请求过多,已永久限制。`);
|
||||
}
|
||||
|
||||
// (>=3次 → 临时限制)
|
||||
const requestsIn30s = this.ipRequestRecords[ip].filter(
|
||||
t => now - t <= 30 * 1000
|
||||
);
|
||||
if (requestsIn30s.length >= 3) {
|
||||
if (!this.ip30sTriggerCount[ip]) {
|
||||
this.ip30sTriggerCount[ip] = [];
|
||||
}
|
||||
this.ip30sTriggerCount[ip].push(now);
|
||||
this._cleanUpRecords(ip);
|
||||
|
||||
// 24小时内触发次数 >= 5 → 限制6小时
|
||||
if (this.ip30sTriggerCount[ip].length >= 5) {
|
||||
const duration = 6 * 60 * 60 * 1000;
|
||||
this.temporaryLimits[ip] = now + duration;
|
||||
limitInfo = ` | Limit: ${duration}ms`;
|
||||
this.saveTempLimitsToFile();
|
||||
throw new Error(`您在24小时内多次频繁请求,限制6小时`);
|
||||
} else {
|
||||
const duration = 60 * 1000;
|
||||
this.temporaryLimits[ip] = now + duration;
|
||||
limitInfo = ` | Limit: ${duration}ms`;
|
||||
this.saveTempLimitsToFile();
|
||||
throw new Error(`请求过多, 限制1分钟`);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.info(`${baseInfo}${limitInfo}`);
|
||||
throw err;
|
||||
}
|
||||
if (needLog) {
|
||||
this.logger.info(`${baseInfo}${limitInfo}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RequestLogger;
|
||||
Reference in New Issue
Block a user