主要更新: - 新增 security/ 安全模块 (风险评估、威胁检测、蜜罐等) - Dockerfile 添加 curl 以支持 Docker 健康检查 - 前端页面更新 (管理后台、用户端) - 数据库迁移和 schema 更新 - 新增 kdocs 上传服务 - 添加安全相关测试用例 Co-Authored-By: Claude <noreply@anthropic.com>
256 lines
8.3 KiB
Python
256 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from datetime import timedelta
|
|
from typing import List, Optional
|
|
|
|
import db_pool
|
|
from db.utils import get_cst_now, get_cst_now_str
|
|
|
|
|
|
class BlacklistManager:
|
|
"""黑名单管理器"""
|
|
|
|
def __init__(self) -> None:
|
|
self._schema_ready = False
|
|
self._schema_lock = threading.Lock()
|
|
|
|
def is_ip_banned(self, ip: str) -> bool:
|
|
"""检查IP是否被封禁"""
|
|
ip_text = str(ip or "").strip()[:64]
|
|
if not ip_text:
|
|
return False
|
|
now_str = get_cst_now_str()
|
|
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
SELECT 1
|
|
FROM ip_blacklist
|
|
WHERE ip = ?
|
|
AND is_active = 1
|
|
AND (expires_at IS NULL OR expires_at > ?)
|
|
LIMIT 1
|
|
""",
|
|
(ip_text, now_str),
|
|
)
|
|
return cursor.fetchone() is not None
|
|
|
|
def is_user_banned(self, user_id: int) -> bool:
|
|
"""检查用户是否被封禁"""
|
|
if user_id is None:
|
|
return False
|
|
self._ensure_schema()
|
|
user_id_int = int(user_id)
|
|
now_str = get_cst_now_str()
|
|
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
SELECT 1
|
|
FROM user_blacklist
|
|
WHERE user_id = ?
|
|
AND is_active = 1
|
|
AND (expires_at IS NULL OR expires_at > ?)
|
|
LIMIT 1
|
|
""",
|
|
(user_id_int, now_str),
|
|
)
|
|
return cursor.fetchone() is not None
|
|
|
|
def ban_ip(self, ip: str, reason: str, duration_hours: int = 24, permanent: bool = False):
|
|
"""封禁IP"""
|
|
ip_text = str(ip or "").strip()[:64]
|
|
if not ip_text:
|
|
return False
|
|
reason_text = str(reason or "").strip()[:512]
|
|
now_str = get_cst_now_str()
|
|
|
|
expires_at: Optional[str]
|
|
if permanent:
|
|
expires_at = None
|
|
else:
|
|
hours = max(1, int(duration_hours))
|
|
expires_at = (get_cst_now() + timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO ip_blacklist (ip, reason, is_active, added_at, expires_at)
|
|
VALUES (?, ?, 1, ?, ?)
|
|
ON CONFLICT(ip) DO UPDATE SET
|
|
reason = excluded.reason,
|
|
is_active = 1,
|
|
added_at = excluded.added_at,
|
|
expires_at = excluded.expires_at
|
|
""",
|
|
(ip_text, reason_text, now_str, expires_at),
|
|
)
|
|
conn.commit()
|
|
return True
|
|
|
|
def ban_user(self, user_id: int, reason: str):
|
|
"""封禁用户"""
|
|
return self._ban_user_internal(user_id, reason=reason, duration_hours=24, permanent=False)
|
|
|
|
def unban_ip(self, ip: str):
|
|
"""解除IP封禁"""
|
|
ip_text = str(ip or "").strip()[:64]
|
|
if not ip_text:
|
|
return False
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute("UPDATE ip_blacklist SET is_active = 0 WHERE ip = ?", (ip_text,))
|
|
conn.commit()
|
|
return cursor.rowcount > 0
|
|
|
|
def unban_user(self, user_id: int):
|
|
"""解除用户封禁"""
|
|
if user_id is None:
|
|
return False
|
|
self._ensure_schema()
|
|
user_id_int = int(user_id)
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute("UPDATE user_blacklist SET is_active = 0 WHERE user_id = ?", (user_id_int,))
|
|
conn.commit()
|
|
return cursor.rowcount > 0
|
|
|
|
def get_banned_ips(self) -> List[dict]:
|
|
"""获取所有被封禁的IP"""
|
|
now_str = get_cst_now_str()
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
SELECT ip, reason, is_active, added_at, expires_at
|
|
FROM ip_blacklist
|
|
WHERE is_active = 1
|
|
AND (expires_at IS NULL OR expires_at > ?)
|
|
ORDER BY added_at DESC
|
|
""",
|
|
(now_str,),
|
|
)
|
|
return [dict(row) for row in cursor.fetchall()]
|
|
|
|
def get_banned_users(self) -> List[dict]:
|
|
"""获取所有被封禁的用户"""
|
|
self._ensure_schema()
|
|
now_str = get_cst_now_str()
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
SELECT user_id, reason, is_active, added_at, expires_at
|
|
FROM user_blacklist
|
|
WHERE is_active = 1
|
|
AND (expires_at IS NULL OR expires_at > ?)
|
|
ORDER BY added_at DESC
|
|
""",
|
|
(now_str,),
|
|
)
|
|
return [dict(row) for row in cursor.fetchall()]
|
|
|
|
def cleanup_expired(self):
|
|
"""清理过期的封禁记录"""
|
|
now_str = get_cst_now_str()
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
UPDATE ip_blacklist
|
|
SET is_active = 0
|
|
WHERE is_active = 1
|
|
AND expires_at IS NOT NULL
|
|
AND expires_at <= ?
|
|
""",
|
|
(now_str,),
|
|
)
|
|
conn.commit()
|
|
|
|
self._ensure_schema()
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
UPDATE user_blacklist
|
|
SET is_active = 0
|
|
WHERE is_active = 1
|
|
AND expires_at IS NOT NULL
|
|
AND expires_at <= ?
|
|
""",
|
|
(now_str,),
|
|
)
|
|
conn.commit()
|
|
|
|
# ==================== Internal ====================
|
|
|
|
def _ensure_schema(self) -> None:
|
|
if self._schema_ready:
|
|
return
|
|
with self._schema_lock:
|
|
if self._schema_ready:
|
|
return
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS user_blacklist (
|
|
user_id INTEGER PRIMARY KEY,
|
|
reason TEXT,
|
|
is_active INTEGER DEFAULT 1,
|
|
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_active ON user_blacklist(is_active)")
|
|
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_expires ON user_blacklist(expires_at)")
|
|
conn.commit()
|
|
self._schema_ready = True
|
|
|
|
def _ban_user_internal(
|
|
self,
|
|
user_id: int,
|
|
*,
|
|
reason: str,
|
|
duration_hours: int = 24,
|
|
permanent: bool = False,
|
|
) -> bool:
|
|
if user_id is None:
|
|
return False
|
|
self._ensure_schema()
|
|
user_id_int = int(user_id)
|
|
reason_text = str(reason or "").strip()[:512]
|
|
now_str = get_cst_now_str()
|
|
|
|
expires_at: Optional[str]
|
|
if permanent:
|
|
expires_at = None
|
|
else:
|
|
hours = max(1, int(duration_hours))
|
|
expires_at = (get_cst_now() + timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
with db_pool.get_db() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute(
|
|
"""
|
|
INSERT INTO user_blacklist (user_id, reason, is_active, added_at, expires_at)
|
|
VALUES (?, ?, 1, ?, ?)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
reason = excluded.reason,
|
|
is_active = 1,
|
|
added_at = excluded.added_at,
|
|
expires_at = excluded.expires_at
|
|
""",
|
|
(user_id_int, reason_text, now_str, expires_at),
|
|
)
|
|
conn.commit()
|
|
return True
|
|
|