f7b4908322
API 认证 (16 个未保护端点 → 全部加 panel_auth): - 文件管理器: 11 端点 (list/mkdir/delete/upload/download/read/write/...) - 项目管理: 6 端点 (list/run/stop/logs/stdin/page), 修复硬编码路径前缀 - 代理管理: 3 端点 (list/add/remove) - 系统状态: 2 HTTP + 1 WS (token 校验) - 插件页面: 3 端点 (page/sse/event) - 内部路由: 3 端点 (plugins/commands/data) 认证系统加固: - 密码哈希: 固定盐 → 每用户独立 secrets.token_hex(16) 随机盐 - 默认密码警告: 启动时检测并打印 critical 级别日志 - 登录频率限制: 5 次失败 / IP → 锁定 60 秒, 返回 429 基础设施: - 安全响应头: X-Content-Type-Options/X-Frame-Options/X-XSS-Protection/Referrer-Policy - WebSocket 鉴权: query string token 校验 Co-Authored-By: Claude <noreply@anthropic.com>
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
import time
|
|
import json
|
|
import asyncio
|
|
import logging
|
|
from aiohttp import web
|
|
from ..utils.system_info import SystemInfoCollector
|
|
from ..utils.auth import panel_auth
|
|
|
|
collector = SystemInfoCollector()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Track active system-status WS clients
|
|
_sys_ws_clients: set = set()
|
|
|
|
def _ws_auth_wrapper(handler):
|
|
"""WebSocket 鉴权包装 — 从 query string 取 token 验证"""
|
|
async def wrapper(request):
|
|
token = request.query.get("token", "")
|
|
session_store = request.app.get("panel_session_store", {})
|
|
if not token or token not in session_store:
|
|
ws = web.WebSocketResponse()
|
|
await ws.prepare(request)
|
|
await ws.send_str(json.dumps({"error": "Unauthorized"}))
|
|
await ws.close(code=4001, message="Unauthorized")
|
|
return ws
|
|
return await handler(request)
|
|
return wrapper
|
|
|
|
def setup_routes(app, prefix=''):
|
|
app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework))
|
|
app.router.add_get(f'{prefix}/api/system', panel_auth(get_system))
|
|
app.router.add_get(f'{prefix}/api/system/ws', _ws_auth_wrapper(sys_ws_handler))
|
|
logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws (已加认证)")
|
|
|
|
def _read_version():
|
|
"""Read version from config file (shared helper)"""
|
|
ver = "v0.6.0"
|
|
try:
|
|
import yaml, os
|
|
config_path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "config", "framework", "base_config.yaml")
|
|
with open(config_path) as f:
|
|
cfg = yaml.safe_load(f)
|
|
ver = cfg.get("framework", {}).get("version", ver)
|
|
except:
|
|
pass
|
|
return ver
|
|
|
|
def _get_framework_data(sm):
|
|
"""Collect framework status (reused by HTTP and WS handlers)"""
|
|
ver = _read_version()
|
|
ps = sm.get_service("plugin") if sm else None
|
|
uptime = int(time.time() - getattr(sm, 'start_time', time.time()))
|
|
return {
|
|
"version": ver,
|
|
"uptime": uptime,
|
|
"plugins": len(ps.plugins) if ps else 0
|
|
}
|
|
|
|
async def get_framework(req):
|
|
sm = req.app.get('service_manager')
|
|
if not sm:
|
|
return web.json_response({"error": "Missing"}, status=500)
|
|
return web.json_response(_get_framework_data(sm))
|
|
|
|
def _get_collector(req):
|
|
"""获取共享的 SystemInfoCollector(优先从 service_manager),fallback 到模块级实例"""
|
|
sm = req.app.get('service_manager')
|
|
if sm:
|
|
try:
|
|
shared = sm.get_service("sys_collector")
|
|
if shared:
|
|
return shared
|
|
except Exception:
|
|
pass
|
|
return collector
|
|
|
|
async def get_system(req):
|
|
c = _get_collector(req)
|
|
return web.json_response(c.get_all())
|
|
|
|
async def sys_ws_handler(req):
|
|
"""WebSocket push endpoint: system+framwork stats every 2s (no logging per message)"""
|
|
ws = web.WebSocketResponse(heartbeat=30.0)
|
|
await ws.prepare(req)
|
|
_sys_ws_clients.add(ws)
|
|
|
|
async def push():
|
|
"""Send one snapshot to this client (silent on error)"""
|
|
try:
|
|
sm = req.app.get('service_manager')
|
|
c = _get_collector(req)
|
|
payload = json.dumps({
|
|
"type": "sys",
|
|
"system": c.get_all(),
|
|
"framework": _get_framework_data(sm)
|
|
}, ensure_ascii=False)
|
|
if not ws.closed:
|
|
await ws.send_str(payload)
|
|
except Exception:
|
|
pass
|
|
|
|
# Background push loop — runs until client disconnects
|
|
async def push_loop():
|
|
while not ws.closed:
|
|
await push()
|
|
await asyncio.sleep(2)
|
|
|
|
task = asyncio.ensure_future(push_loop())
|
|
try:
|
|
async for msg in ws:
|
|
if msg.type == web.WSMsgType.ERROR:
|
|
break
|
|
finally:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
_sys_ws_clients.discard(ws)
|
|
return ws
|