From a10458e407083e8fa3168e479403542be17532d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=81=97=E5=BF=98=E7=9A=84=E8=AE=B0=E5=BF=86?= <32097237+YIWANG-sketch@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:33:25 +0800 Subject: [PATCH] Create requestLogger.mjs --- requestLogger.mjs | 342 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 requestLogger.mjs diff --git a/requestLogger.mjs b/requestLogger.mjs new file mode 100644 index 0000000..46b31f3 --- /dev/null +++ b/requestLogger.mjs @@ -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;