mirror of https://github.com/Mai-with-u/MaiBot.git
Ruff Format
parent
ee4cb3dc67
commit
74a2f4346a
|
|
@ -126,6 +126,7 @@ SCANNER_SPECIFIC_HEADERS = {
|
|||
# basic: 基础模式(只记录恶意访问,不阻止,不限制请求数,不跟踪IP)
|
||||
ANTI_CRAWLER_MODE = os.getenv("WEBUI_ANTI_CRAWLER_MODE", "basic").lower()
|
||||
|
||||
|
||||
# IP白名单配置(从环境变量读取,逗号分隔)
|
||||
# 支持格式:
|
||||
# - 精确IP:127.0.0.1, 192.168.1.100
|
||||
|
|
@ -230,6 +231,7 @@ def _convert_wildcard_to_regex(wildcard_pattern: str) -> Optional[str]:
|
|||
regex = r"^" + r"\.".join(regex_parts) + r"$"
|
||||
return regex
|
||||
|
||||
|
||||
ALLOWED_IPS = _parse_allowed_ips(os.getenv("WEBUI_ALLOWED_IPS", ""))
|
||||
|
||||
# 信任的代理IP配置(从环境变量读取,逗号分隔)
|
||||
|
|
@ -354,7 +356,6 @@ class AntiCrawlerMiddleware(BaseHTTPMiddleware):
|
|||
|
||||
return False
|
||||
|
||||
|
||||
def _is_asset_scanner_header(self, request: Request) -> bool:
|
||||
"""
|
||||
检测是否为资产测绘工具的HTTP头(只检查特定头,收紧匹配)
|
||||
|
|
@ -499,7 +500,7 @@ class AntiCrawlerMiddleware(BaseHTTPMiddleware):
|
|||
empty_ips = []
|
||||
# 找到最久未访问的IP(最旧时间戳)
|
||||
oldest_ip = None
|
||||
oldest_time = float('inf')
|
||||
oldest_time = float("inf")
|
||||
|
||||
# 全量遍历找真正的oldest(超限时性能可接受)
|
||||
for ip, times in self.request_times.items():
|
||||
|
|
@ -689,12 +690,27 @@ class AntiCrawlerMiddleware(BaseHTTPMiddleware):
|
|||
# 允许访问静态资源(CSS、JS、图片等)
|
||||
# 注意:.json 已移除,避免 API 路径绕过防护
|
||||
# 静态资源只在特定前缀下放行(/static/、/assets/、/dist/)
|
||||
static_extensions = {".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot"}
|
||||
static_extensions = {
|
||||
".css",
|
||||
".js",
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".svg",
|
||||
".ico",
|
||||
".woff",
|
||||
".woff2",
|
||||
".ttf",
|
||||
".eot",
|
||||
}
|
||||
static_prefixes = {"/static/", "/assets/", "/dist/"}
|
||||
|
||||
# 检查是否是静态资源路径(特定前缀下的静态文件)
|
||||
path = request.url.path
|
||||
is_static_path = any(path.startswith(prefix) for prefix in static_prefixes) and any(path.endswith(ext) for ext in static_extensions)
|
||||
is_static_path = any(path.startswith(prefix) for prefix in static_prefixes) and any(
|
||||
path.endswith(ext) for ext in static_extensions
|
||||
)
|
||||
|
||||
# 也允许根路径下的静态文件(如 /favicon.ico)
|
||||
is_root_static = path.count("/") == 1 and any(path.endswith(ext) for ext in static_extensions)
|
||||
|
|
@ -729,9 +745,7 @@ class AntiCrawlerMiddleware(BaseHTTPMiddleware):
|
|||
|
||||
# 检测爬虫 User-Agent
|
||||
if self.check_user_agent and self._is_crawler_user_agent(user_agent):
|
||||
logger.warning(
|
||||
f"🚫 检测到爬虫请求 - IP: {client_ip}, User-Agent: {user_agent}, Path: {request.url.path}"
|
||||
)
|
||||
logger.warning(f"🚫 检测到爬虫请求 - IP: {client_ip}, User-Agent: {user_agent}, Path: {request.url.path}")
|
||||
# 根据配置决定是否阻止
|
||||
if self.block_on_detect:
|
||||
return PlainTextResponse(
|
||||
|
|
@ -741,9 +755,7 @@ class AntiCrawlerMiddleware(BaseHTTPMiddleware):
|
|||
|
||||
# 检查请求频率限制
|
||||
if self.check_rate_limit and self._check_rate_limit(client_ip):
|
||||
logger.warning(
|
||||
f"🚫 请求频率过高 - IP: {client_ip}, User-Agent: {user_agent}, Path: {request.url.path}"
|
||||
)
|
||||
logger.warning(f"🚫 请求频率过高 - IP: {client_ip}, User-Agent: {user_agent}, Path: {request.url.path}")
|
||||
return PlainTextResponse(
|
||||
"Too Many Requests: Rate limit exceeded",
|
||||
status_code=429,
|
||||
|
|
@ -770,4 +782,3 @@ Disallow: /
|
|||
media_type="text/plain",
|
||||
headers={"Cache-Control": "public, max-age=86400"}, # 缓存24小时
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ def require_auth(
|
|||
"""认证依赖:验证用户是否已登录"""
|
||||
return verify_auth_token_from_cookie_or_header(maibot_session, authorization)
|
||||
|
||||
|
||||
# WebUI 聊天的虚拟群组 ID
|
||||
WEBUI_CHAT_GROUP_ID = "webui_local_chat"
|
||||
WEBUI_CHAT_PLATFORM = "webui"
|
||||
|
|
|
|||
|
|
@ -346,7 +346,9 @@ async def update_bot_config_raw(raw_content: RawContentBody, _auth: bool = Depen
|
|||
|
||||
|
||||
@router.post("/model/section/{section_name}")
|
||||
async def update_model_config_section(section_name: str, section_data: SectionBody, _auth: bool = Depends(require_auth)):
|
||||
async def update_model_config_section(
|
||||
section_name: str, section_data: SectionBody, _auth: bool = Depends(require_auth)
|
||||
):
|
||||
"""更新模型配置的指定节(保留注释和格式)"""
|
||||
try:
|
||||
# 读取现有配置
|
||||
|
|
@ -383,8 +385,7 @@ async def update_model_config_section(section_name: str, section_data: SectionBo
|
|||
provider_names = {p.get("name") for p in section_data if isinstance(p, dict)}
|
||||
models = config_data.get("models", [])
|
||||
orphaned_models = [
|
||||
m.get("name") for m in models
|
||||
if isinstance(m, dict) and m.get("api_provider") not in provider_names
|
||||
m.get("name") for m in models if isinstance(m, dict) and m.get("api_provider") not in provider_names
|
||||
]
|
||||
if orphaned_models:
|
||||
error_msg = f"以下模型引用了已删除的提供商: {', '.join(orphaned_models)}。请先在模型管理页面删除这些模型,或重新分配它们的提供商。"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from pydantic import BaseModel, Field
|
|||
from typing import Optional, List, Dict, Any, get_origin
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
from src.common.logger import get_logger
|
||||
from src.common.toml_utils import save_toml_with_format
|
||||
from src.config.config import MMC_VERSION
|
||||
|
|
@ -556,10 +555,7 @@ async def fetch_raw_file(
|
|||
if not token or not token_manager.verify_token(token):
|
||||
raise HTTPException(status_code=401, detail="未授权:无效的访问令牌")
|
||||
|
||||
logger.info(
|
||||
f"收到获取 Raw 文件请求: "
|
||||
f"{request.owner}/{request.repo}/{request.branch}/{request.file_path}"
|
||||
)
|
||||
logger.info(f"收到获取 Raw 文件请求: {request.owner}/{request.repo}/{request.branch}/{request.file_path}")
|
||||
|
||||
# 发送开始加载进度
|
||||
await update_progress(
|
||||
|
|
|
|||
|
|
@ -47,10 +47,7 @@ class RateLimiter:
|
|||
"""清理过期的请求记录"""
|
||||
now = time.time()
|
||||
cutoff = now - window_seconds
|
||||
self._requests[key] = [
|
||||
(ts, count) for ts, count in self._requests[key]
|
||||
if ts > cutoff
|
||||
]
|
||||
self._requests[key] = [(ts, count) for ts, count in self._requests[key] if ts > cutoff]
|
||||
|
||||
def _cleanup_expired_blocks(self):
|
||||
"""清理过期的封禁"""
|
||||
|
|
@ -77,11 +74,7 @@ class RateLimiter:
|
|||
return False, None
|
||||
|
||||
def check_rate_limit(
|
||||
self,
|
||||
request: Request,
|
||||
max_requests: int,
|
||||
window_seconds: int,
|
||||
key_suffix: str = ""
|
||||
self, request: Request, max_requests: int, window_seconds: int, key_suffix: str = ""
|
||||
) -> Tuple[bool, int]:
|
||||
"""
|
||||
检查请求是否超过频率限制
|
||||
|
|
@ -127,11 +120,7 @@ class RateLimiter:
|
|||
logger.warning(f"🔒 IP {ip} 已被封禁 {duration_seconds} 秒")
|
||||
|
||||
def record_failed_attempt(
|
||||
self,
|
||||
request: Request,
|
||||
max_failures: int = 5,
|
||||
window_seconds: int = 300,
|
||||
block_duration: int = 600
|
||||
self, request: Request, max_failures: int = 5, window_seconds: int = 300, block_duration: int = 600
|
||||
) -> Tuple[bool, int]:
|
||||
"""
|
||||
记录失败尝试(如登录失败)
|
||||
|
|
@ -212,7 +201,7 @@ async def check_auth_rate_limit(request: Request):
|
|||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail=f"请求过于频繁,请在 {remaining_block} 秒后重试",
|
||||
headers={"Retry-After": str(remaining_block)}
|
||||
headers={"Retry-After": str(remaining_block)},
|
||||
)
|
||||
|
||||
# 检查频率限制
|
||||
|
|
@ -220,15 +209,11 @@ async def check_auth_rate_limit(request: Request):
|
|||
request,
|
||||
max_requests=10, # 每分钟 10 次
|
||||
window_seconds=60,
|
||||
key_suffix="auth"
|
||||
key_suffix="auth",
|
||||
)
|
||||
|
||||
if not allowed:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="认证请求过于频繁,请稍后重试",
|
||||
headers={"Retry-After": "60"}
|
||||
)
|
||||
raise HTTPException(status_code=429, detail="认证请求过于频繁,请稍后重试", headers={"Retry-After": "60"})
|
||||
|
||||
|
||||
async def check_api_rate_limit(request: Request):
|
||||
|
|
@ -245,7 +230,7 @@ async def check_api_rate_limit(request: Request):
|
|||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail=f"请求过于频繁,请在 {remaining_block} 秒后重试",
|
||||
headers={"Retry-After": str(remaining_block)}
|
||||
headers={"Retry-After": str(remaining_block)},
|
||||
)
|
||||
|
||||
# 检查频率限制
|
||||
|
|
@ -253,12 +238,8 @@ async def check_api_rate_limit(request: Request):
|
|||
request,
|
||||
max_requests=100, # 每分钟 100 次
|
||||
window_seconds=60,
|
||||
key_suffix="api"
|
||||
key_suffix="api",
|
||||
)
|
||||
|
||||
if not allowed:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="请求过于频繁,请稍后重试",
|
||||
headers={"Retry-After": "60"}
|
||||
)
|
||||
raise HTTPException(status_code=429, detail="请求过于频繁,请稍后重试", headers={"Retry-After": "60"})
|
||||
|
|
|
|||
|
|
@ -148,14 +148,11 @@ async def verify_token(
|
|||
request,
|
||||
max_failures=5, # 5 次失败
|
||||
window_seconds=300, # 5 分钟窗口
|
||||
block_duration=600 # 封禁 10 分钟
|
||||
block_duration=600, # 封禁 10 分钟
|
||||
)
|
||||
|
||||
if blocked:
|
||||
raise HTTPException(
|
||||
status_code=429,
|
||||
detail="认证失败次数过多,您的 IP 已被临时封禁 10 分钟"
|
||||
)
|
||||
raise HTTPException(status_code=429, detail="认证失败次数过多,您的 IP 已被临时封禁 10 分钟")
|
||||
|
||||
message = "Token 无效或已过期"
|
||||
if remaining <= 2:
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class WebUIServer:
|
|||
media_type = mimetypes.guess_type(str(file_path))[0]
|
||||
response = FileResponse(file_path, media_type=media_type)
|
||||
# HTML 文件添加防索引头
|
||||
if str(file_path).endswith('.html'):
|
||||
if str(file_path).endswith(".html"):
|
||||
response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive"
|
||||
return response
|
||||
|
||||
|
|
@ -136,17 +136,9 @@ class WebUIServer:
|
|||
|
||||
# 注意:中间件按注册顺序反向执行,所以先注册的中间件后执行
|
||||
# 我们需要在CORS之前注册,这样防爬虫检查会在CORS之前执行
|
||||
self.app.add_middleware(
|
||||
AntiCrawlerMiddleware,
|
||||
mode=anti_crawler_mode
|
||||
)
|
||||
self.app.add_middleware(AntiCrawlerMiddleware, mode=anti_crawler_mode)
|
||||
|
||||
mode_descriptions = {
|
||||
"false": "已禁用",
|
||||
"strict": "严格模式",
|
||||
"loose": "宽松模式",
|
||||
"basic": "基础模式"
|
||||
}
|
||||
mode_descriptions = {"false": "已禁用", "strict": "严格模式", "loose": "宽松模式", "basic": "基础模式"}
|
||||
mode_desc = mode_descriptions.get(anti_crawler_mode, "基础模式")
|
||||
logger.info(f"🛡️ 防爬虫中间件已配置: {mode_desc}")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
临时 token 有效期 60 秒,且只能使用一次,用于解决 WebSocket 握手时 Cookie 不可用的问题。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Cookie, Header, HTTPException
|
||||
from fastapi import APIRouter, Cookie, Header
|
||||
from typing import Optional
|
||||
import secrets
|
||||
import time
|
||||
|
|
|
|||
Loading…
Reference in New Issue