import fs from 'fs/promises'; import path from 'path'; import { getDataDir } from './paths.js'; import logger from './logger.js'; const BLOCKLIST_FILE = 'ip-blocklist.json'; const SECURITY_CONFIG_FILE = 'security.json'; const SECURITY_CONFIG_EXAMPLE = 'security.json.example'; const DEFAULT_CONFIG = { whitelist: { enabled: true, ips: ['127.0.0.1', '::1'] }, blocking: { enabled: true, tempBlockDuration: 60 * 60 * 1000, maxViolationsBeforeTempBlock: 50, maxTempBlocksBeforePermanent: 10, violationWindow: 5 * 60 * 1000, violationDecayTime: 30 * 60 * 1000 } }; const PRIVATE_IP_RANGES = [ /^127\./, /^10\./, /^172\.(1[6-9]|2[0-9]|3[01])\./, /^192\.168\./, /^::1$/, /^fe80:/, /^fc00:/, /^::ffff:127\./, /^::ffff:10\./, /^::ffff:192\.168\./ ]; class IpBlockManager { constructor() { this.filePath = null; this.configPath = null; this.data = { blocked_ips: {} }; this.config = DEFAULT_CONFIG; this.initialized = false; this.savePromise = Promise.resolve(); } async init() { if (this.initialized) return; const dataDir = getDataDir(); this.filePath = path.join(dataDir, BLOCKLIST_FILE); this.configPath = path.join(process.cwd(), SECURITY_CONFIG_FILE); await this.loadConfig(); await this.load(); this.initialized = true; } async loadConfig() { try { const examplePath = path.join(process.cwd(), SECURITY_CONFIG_EXAMPLE); try { await fs.access(this.configPath); } catch { try { await fs.copyFile(examplePath, this.configPath); logger.info('已从 security.json.example 创建 security.json'); } catch (e) { logger.warn('未找到 security.json.example,使用默认配置'); } } try { const content = await fs.readFile(this.configPath, 'utf8'); const loaded = JSON.parse(content); this.config = { ...DEFAULT_CONFIG, ...loaded }; if (loaded.whitelist) this.config.whitelist = { ...DEFAULT_CONFIG.whitelist, ...loaded.whitelist }; if (loaded.blocking) this.config.blocking = { ...DEFAULT_CONFIG.blocking, ...loaded.blocking }; } catch (e) { if (e.code !== 'ENOENT') { logger.error('加载安全配置失败:', e.message); } this.config = DEFAULT_CONFIG; } } catch (e) { logger.error('初始化安全配置失败:', e.message); this.config = DEFAULT_CONFIG; } } async saveConfig() { try { await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8'); logger.info('安全配置已保存'); } catch (e) { logger.error('保存安全配置失败:', e.message); } } isWhitelisted(ip) { if (!ip) return false; if (!this.config.whitelist.enabled) return false; if (this.config.whitelist.ips.includes(ip)) return true; return PRIVATE_IP_RANGES.some(regex => regex.test(ip)); } async load() { try { await fs.mkdir(path.dirname(this.filePath), { recursive: true }); try { const content = await fs.readFile(this.filePath, 'utf8'); this.data = JSON.parse(content); } catch (e) { if (e.code !== 'ENOENT') { logger.error('加载封禁列表失败:', e.message); } this.data = { blocked_ips: {} }; } } catch (e) { logger.error('初始化封禁管理器失败:', e.message); } } async save() { this.savePromise = this.savePromise.then(async () => { try { await fs.writeFile(this.filePath, JSON.stringify(this.data, null, 2), 'utf8'); } catch (e) { logger.error('保存封禁列表失败:', e.message); } }); return this.savePromise; } check(ip) { if (!ip || this.isWhitelisted(ip)) return { blocked: false }; if (!this.config.blocking.enabled) return { blocked: false }; const info = this.data.blocked_ips[ip]; if (!info) return { blocked: false }; if (info.permanent) { return { blocked: true, reason: 'permanent' }; } if (info.expiresAt && Date.now() < info.expiresAt) { return { blocked: true, reason: 'temporary', expiresAt: info.expiresAt }; } return { blocked: false }; } async recordViolation(ip, type) { if (!ip || this.isWhitelisted(ip)) return; if (!this.config.blocking.enabled) return; if (!this.initialized) await this.init(); let info = this.data.blocked_ips[ip]; const now = Date.now(); if (!info) { info = { permanent: false, expiresAt: 0, violations: 0, tempBlockCount: 0, lastViolation: 0 }; this.data.blocked_ips[ip] = info; } if (info.permanent || (info.expiresAt && now < info.expiresAt)) return; const { violationDecayTime, violationWindow, maxViolationsBeforeTempBlock, maxTempBlocksBeforePermanent, tempBlockDuration } = this.config.blocking; if (now - info.lastViolation > violationDecayTime) { info.violations = Math.max(0, Math.floor(info.violations / 2)); } else if (now - info.lastViolation > violationWindow) { info.violations = 0; } info.violations++; info.lastViolation = now; if (info.violations >= maxViolationsBeforeTempBlock) { info.tempBlockCount++; info.violations = 0; if (info.tempBlockCount >= maxTempBlocksBeforePermanent) { info.permanent = true; info.expiresAt = 0; logger.warn(`IP ${ip} 因频繁违规(${type})被永久封禁`); } else { info.expiresAt = now + tempBlockDuration; logger.warn(`IP ${ip} 因频繁违规(${type})被临时封禁 ${Math.round(tempBlockDuration/60000)} 分钟 (累计封禁 ${info.tempBlockCount} 次)`); } await this.save(); } } async unblock(ip) { if (!ip) return false; if (this.data.blocked_ips[ip]) { delete this.data.blocked_ips[ip]; await this.save(); logger.info(`IP ${ip} 已解除封禁`); return true; } return false; } async listBlocked() { const now = Date.now(); return Object.entries(this.data.blocked_ips) .filter(([_, info]) => { return info.permanent || (info.expiresAt && now < info.expiresAt); }) .map(([ip, info]) => ({ ip, permanent: info.permanent, expiresAt: info.expiresAt, tempBlockCount: info.tempBlockCount })); } getConfig() { return this.config; } async updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; if (newConfig.whitelist) this.config.whitelist = { ...this.config.whitelist, ...newConfig.whitelist }; if (newConfig.blocking) this.config.blocking = { ...this.config.blocking, ...newConfig.blocking }; await this.saveConfig(); } async addWhitelistIP(ip) { if (!this.config.whitelist.ips.includes(ip)) { this.config.whitelist.ips.push(ip); await this.saveConfig(); logger.info(`IP ${ip} 已添加到白名单`); return true; } return false; } async removeWhitelistIP(ip) { const index = this.config.whitelist.ips.indexOf(ip); if (index > -1) { this.config.whitelist.ips.splice(index, 1); await this.saveConfig(); logger.info(`IP ${ip} 已从白名单移除`); return true; } return false; } } export default new IpBlockManager();