343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
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;
|