security: 公网生产环境加固 — P0/P1 全部修复
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>
This commit is contained in:
+30
-15
@@ -17,6 +17,7 @@ class User:
|
|||||||
"""用户数据类"""
|
"""用户数据类"""
|
||||||
username: str
|
username: str
|
||||||
password_hash: str
|
password_hash: str
|
||||||
|
salt: str
|
||||||
permissions: List[str]
|
permissions: List[str]
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
created_at: float = None
|
created_at: float = None
|
||||||
@@ -48,36 +49,48 @@ class AuthService:
|
|||||||
def _init_default_users(self):
|
def _init_default_users(self):
|
||||||
"""初始化默认用户"""
|
"""初始化默认用户"""
|
||||||
try:
|
try:
|
||||||
# 创建默认管理员用户
|
# 管理员 — 独立随机盐
|
||||||
admin_password_hash = self._hash_password(os.environ.get("SENSU_ADMIN_PASSWORD","admin123"))
|
admin_salt = secrets.token_hex(16)
|
||||||
|
admin_pw = os.environ.get("SENSU_ADMIN_PASSWORD", "admin123")
|
||||||
admin_user = User(
|
admin_user = User(
|
||||||
username="admin",
|
username="admin",
|
||||||
password_hash=admin_password_hash,
|
password_hash=self._hash_password(admin_pw, admin_salt),
|
||||||
|
salt=admin_salt,
|
||||||
permissions=["admin"],
|
permissions=["admin"],
|
||||||
created_at=time.time()
|
created_at=time.time(),
|
||||||
)
|
)
|
||||||
self.users["admin"] = admin_user
|
self.users["admin"] = admin_user
|
||||||
|
|
||||||
# 创建默认API用户
|
# API 用户 — 独立随机盐
|
||||||
api_password_hash = self._hash_password(os.environ.get("SENSU_API_PASSWORD","api123"))
|
api_salt = secrets.token_hex(16)
|
||||||
|
api_pw = os.environ.get("SENSU_API_PASSWORD", "api123")
|
||||||
api_user = User(
|
api_user = User(
|
||||||
username="api",
|
username="api",
|
||||||
password_hash=api_password_hash,
|
password_hash=self._hash_password(api_pw, api_salt),
|
||||||
|
salt=api_salt,
|
||||||
permissions=["framework.status.read", "plugin.info.read"],
|
permissions=["framework.status.read", "plugin.info.read"],
|
||||||
created_at=time.time()
|
created_at=time.time(),
|
||||||
)
|
)
|
||||||
self.users["api"] = api_user
|
self.users["api"] = api_user
|
||||||
|
|
||||||
logger.debug("默认用户初始化完成")
|
logger.debug("默认用户初始化完成 (独立随机盐)")
|
||||||
|
|
||||||
|
# 安全警告 — 仍在使用默认密码
|
||||||
|
if admin_pw == "admin123" or api_pw == "api123":
|
||||||
|
logger.critical(
|
||||||
|
"⚠️ 安全警告: 正在使用默认密码! "
|
||||||
|
"请设置环境变量 SENSU_ADMIN_PASSWORD 和 SENSU_API_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"初始化默认用户时出错: {str(e)}", exc_info=True)
|
logger.error(f"初始化默认用户时出错: {str(e)}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _hash_password(self, password: str) -> str:
|
def _hash_password(self, password: str, salt: str = None) -> str:
|
||||||
"""哈希密码"""
|
"""哈希密码 — 每个用户独立随机盐"""
|
||||||
try:
|
try:
|
||||||
salt = "catframework_salt" # 实际应该使用随机盐
|
if salt is None:
|
||||||
|
salt = secrets.token_hex(16)
|
||||||
return hashlib.sha256((password + salt).encode()).hexdigest()
|
return hashlib.sha256((password + salt).encode()).hexdigest()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"哈希密码时出错: {str(e)}", exc_info=True)
|
logger.error(f"哈希密码时出错: {str(e)}", exc_info=True)
|
||||||
@@ -98,7 +111,7 @@ class AuthService:
|
|||||||
logger.warning(f"用户已被禁用: {username}")
|
logger.warning(f"用户已被禁用: {username}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
password_hash = self._hash_password(password)
|
password_hash = self._hash_password(password, user.salt)
|
||||||
if user.password_hash != password_hash:
|
if user.password_hash != password_hash:
|
||||||
logger.warning(f"密码错误: {username}")
|
logger.warning(f"密码错误: {username}")
|
||||||
return None
|
return None
|
||||||
@@ -204,12 +217,14 @@ class AuthService:
|
|||||||
logger.warning(f"用户已存在: {username}")
|
logger.warning(f"用户已存在: {username}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
password_hash = self._hash_password(password)
|
salt = secrets.token_hex(16)
|
||||||
|
password_hash = self._hash_password(password, salt)
|
||||||
user = User(
|
user = User(
|
||||||
username=username,
|
username=username,
|
||||||
password_hash=password_hash,
|
password_hash=password_hash,
|
||||||
|
salt=salt,
|
||||||
permissions=permissions,
|
permissions=permissions,
|
||||||
created_at=time.time()
|
created_at=time.time(),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.users[username] = user
|
self.users[username] = user
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class InternetService:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.service_manager = service_manager
|
self.service_manager = service_manager
|
||||||
self.http_app = web.Application()
|
self.http_app = web.Application()
|
||||||
|
self._setup_security_middleware()
|
||||||
self.http_runner = None
|
self.http_runner = None
|
||||||
self.ws_connections: Dict[str, List] = {}
|
self.ws_connections: Dict[str, List] = {}
|
||||||
self.plugin_routes: Dict[str, List] = {}
|
self.plugin_routes: Dict[str, List] = {}
|
||||||
@@ -149,19 +150,37 @@ class InternetService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存网络配置时出错: {str(e)}")
|
logger.error(f"保存网络配置时出错: {str(e)}")
|
||||||
|
|
||||||
|
def _setup_security_middleware(self):
|
||||||
|
"""注入安全响应头中间件"""
|
||||||
|
|
||||||
|
@web.middleware
|
||||||
|
async def security_headers(request, handler):
|
||||||
|
resp = await handler(request)
|
||||||
|
resp.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||||
|
resp.headers.setdefault("X-Frame-Options", "DENY")
|
||||||
|
resp.headers.setdefault("X-XSS-Protection", "1; mode=block")
|
||||||
|
resp.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
|
# 生产环境有反向代理 TLS 时可开启:
|
||||||
|
# resp.headers.setdefault("Strict-Transport-Security", "max-age=31536000")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
self.http_app.middlewares.append(security_headers)
|
||||||
|
|
||||||
def _setup_default_routes(self):
|
def _setup_default_routes(self):
|
||||||
"""设置默认路由"""
|
"""设置默认路由"""
|
||||||
# 健康检查端点
|
from services.web_panel.utils.auth import panel_auth
|
||||||
|
|
||||||
|
# 健康检查端点 (公开,不暴露内部信息)
|
||||||
self.http_app.router.add_get('/health', self._handle_health_check)
|
self.http_app.router.add_get('/health', self._handle_health_check)
|
||||||
|
|
||||||
# 插件API端点
|
# 插件API端点 (加认证)
|
||||||
self.http_app.router.add_get('/api/plugins', self._handle_get_plugins)
|
self.http_app.router.add_get('/api/plugins', panel_auth(self._handle_get_plugins))
|
||||||
self.http_app.router.add_get('/api/commands', self._handle_get_commands)
|
self.http_app.router.add_get('/api/commands', panel_auth(self._handle_get_commands))
|
||||||
|
|
||||||
# 数据接收端点
|
# 数据接收端点 (加认证)
|
||||||
self.http_app.router.add_post('/api/data', self._handle_data_receive)
|
self.http_app.router.add_post('/api/data', panel_auth(self._handle_data_receive))
|
||||||
|
|
||||||
logger.debug("默认路由设置完成")
|
logger.debug("默认路由设置完成 (已加认证)")
|
||||||
|
|
||||||
async def register_plugin_route(self, plugin_name: str, route_path: str,
|
async def register_plugin_route(self, plugin_name: str, route_path: str,
|
||||||
handler: Callable, methods: List[str] = ["GET"],
|
handler: Callable, methods: List[str] = ["GET"],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
import time
|
||||||
import logging
|
import logging
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from ..utils.auth import panel_auth
|
from ..utils.auth import panel_auth
|
||||||
@@ -9,9 +10,41 @@ from ..utils.auth import panel_auth
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 全局 Session 存储 (内存型)
|
# 全局 Session 存储 (内存型)
|
||||||
# 格式: { "token_string": { "username": "...", "perms": [...] } }
|
|
||||||
PANEL_SESSION_STORE = {}
|
PANEL_SESSION_STORE = {}
|
||||||
|
|
||||||
|
# 登录频率限制 — {ip: [fail_count, lock_until_timestamp]}
|
||||||
|
_LOGIN_FAILS: dict[str, list] = {}
|
||||||
|
_MAX_FAILS = 5
|
||||||
|
_LOCK_SECONDS = 60
|
||||||
|
|
||||||
|
|
||||||
|
def _check_rate_limit(ip: str) -> bool:
|
||||||
|
"""检查 IP 是否被限流。返回 True = 允许尝试"""
|
||||||
|
now = time.time()
|
||||||
|
entry = _LOGIN_FAILS.get(ip)
|
||||||
|
if entry:
|
||||||
|
fail_count, lock_until = entry
|
||||||
|
if now < lock_until:
|
||||||
|
return False # still locked
|
||||||
|
if now >= lock_until + _LOCK_SECONDS:
|
||||||
|
_LOGIN_FAILS.pop(ip, None) # expired, reset
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _record_fail(ip: str):
|
||||||
|
now = time.time()
|
||||||
|
entry = _LOGIN_FAILS.get(ip, [0, 0])
|
||||||
|
entry[0] += 1
|
||||||
|
if entry[0] >= _MAX_FAILS:
|
||||||
|
entry[1] = now + _LOCK_SECONDS
|
||||||
|
logger.warning(f"🔒 IP {ip} 登录锁定 {_LOCK_SECONDS}s ({_MAX_FAILS} 次失败)")
|
||||||
|
_LOGIN_FAILS[ip] = entry
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_fails(ip: str):
|
||||||
|
_LOGIN_FAILS.pop(ip, None)
|
||||||
|
|
||||||
|
|
||||||
def setup_routes(app, prefix=''):
|
def setup_routes(app, prefix=''):
|
||||||
"""注册面板认证路由"""
|
"""注册面板认证路由"""
|
||||||
# 🟢 关键:将 Session Store 挂载到 app,供拦截器读取
|
# 🟢 关键:将 Session Store 挂载到 app,供拦截器读取
|
||||||
@@ -26,6 +59,14 @@ def setup_routes(app, prefix=''):
|
|||||||
async def handle_login(req):
|
async def handle_login(req):
|
||||||
"""处理面板登录"""
|
"""处理面板登录"""
|
||||||
try:
|
try:
|
||||||
|
# 频率限制
|
||||||
|
ip = req.remote
|
||||||
|
if not _check_rate_limit(ip):
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "msg": "尝试次数过多,请 60 秒后重试"},
|
||||||
|
status=429,
|
||||||
|
)
|
||||||
|
|
||||||
data = await req.json()
|
data = await req.json()
|
||||||
username = data.get('username')
|
username = data.get('username')
|
||||||
password = data.get('password')
|
password = data.get('password')
|
||||||
@@ -36,6 +77,7 @@ async def handle_login(req):
|
|||||||
|
|
||||||
# 校验配置中的账号密码
|
# 校验配置中的账号密码
|
||||||
if username == cfg_user and password == cfg_pass:
|
if username == cfg_user and password == cfg_pass:
|
||||||
|
_clear_fails(ip)
|
||||||
# 登录成功:生成 Token
|
# 登录成功:生成 Token
|
||||||
token = secrets.token_hex(16)
|
token = secrets.token_hex(16)
|
||||||
|
|
||||||
@@ -54,7 +96,8 @@ async def handle_login(req):
|
|||||||
resp.set_cookie("panel_token", token, max_age=259200, httponly=True, samesite="Lax")
|
resp.set_cookie("panel_token", token, max_age=259200, httponly=True, samesite="Lax")
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
logger.warning(f"❌ 面板登录失败: 用户 {username} 密码错误")
|
_record_fail(ip)
|
||||||
|
logger.warning(f"❌ 面板登录失败: 用户 {username} 密码错误 (IP: {ip})")
|
||||||
return web.json_response({"success": False, "msg": "用户名或密码错误"}, status=401)
|
return web.json_response({"success": False, "msg": "用户名或密码错误"}, status=401)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import logging
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from ..utils.auth import panel_auth
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -412,14 +413,14 @@ def setup_file_routes(app, service_manager, prefix=''):
|
|||||||
return web.json_response({"error": str(e)}, status=500)
|
return web.json_response({"error": str(e)}, status=500)
|
||||||
|
|
||||||
# ── Register routes ──
|
# ── Register routes ──
|
||||||
app.router.add_get(f'{prefix}/api/files/list', list_dir)
|
app.router.add_get(f'{prefix}/api/files/list', panel_auth(list_dir))
|
||||||
app.router.add_post(f'{prefix}/api/files/mkdir', mkdir)
|
app.router.add_post(f'{prefix}/api/files/mkdir', panel_auth(mkdir))
|
||||||
app.router.add_post(f'{prefix}/api/files/touch', touch)
|
app.router.add_post(f'{prefix}/api/files/touch', panel_auth(touch))
|
||||||
app.router.add_post(f'{prefix}/api/files/delete', delete)
|
app.router.add_post(f'{prefix}/api/files/delete', panel_auth(delete))
|
||||||
app.router.add_post(f'{prefix}/api/files/rename', rename)
|
app.router.add_post(f'{prefix}/api/files/rename', panel_auth(rename))
|
||||||
app.router.add_post(f'{prefix}/api/files/upload', upload)
|
app.router.add_post(f'{prefix}/api/files/upload', panel_auth(upload))
|
||||||
app.router.add_get(f'{prefix}/api/files/download', download)
|
app.router.add_get(f'{prefix}/api/files/download', panel_auth(download))
|
||||||
app.router.add_get(f'{prefix}/api/files/read', read_file)
|
app.router.add_get(f'{prefix}/api/files/read', panel_auth(read_file))
|
||||||
app.router.add_post(f'{prefix}/api/files/write', write_file)
|
app.router.add_post(f'{prefix}/api/files/write', panel_auth(write_file))
|
||||||
app.router.add_get(f'{prefix}/api/files/info', file_info)
|
app.router.add_get(f'{prefix}/api/files/info', panel_auth(file_info))
|
||||||
app.router.add_get(f'{prefix}/api/files/picker', picker_api)
|
app.router.add_get(f'{prefix}/api/files/picker', panel_auth(picker_api))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import json, logging
|
import json, logging
|
||||||
|
from ..utils.auth import panel_auth
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def setup_plugin_web_routes(app, service_manager):
|
def setup_plugin_web_routes(app, service_manager):
|
||||||
@@ -32,7 +34,7 @@ def setup_plugin_web_routes(app, service_manager):
|
|||||||
return web.json_response({"ok": True})
|
return web.json_response({"ok": True})
|
||||||
return web.json_response({"ok": False}, status=404)
|
return web.json_response({"ok": False}, status=404)
|
||||||
|
|
||||||
app.router.add_get("/plugin/{name}", plugin_page)
|
app.router.add_get("/plugin/{name}", panel_auth(plugin_page))
|
||||||
app.router.add_get("/plugin/{name}/sse", plugin_sse)
|
app.router.add_get("/plugin/{name}/sse", panel_auth(plugin_sse))
|
||||||
app.router.add_post("/plugin/{name}/event", plugin_event)
|
app.router.add_post("/plugin/{name}/event", panel_auth(plugin_event))
|
||||||
logger.info("Plugin web routes registered")
|
logger.info("Plugin web routes registered (已加认证)")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import json, logging
|
import json, logging
|
||||||
|
from ..utils.auth import panel_auth
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _get_engine(request):
|
def _get_engine(request):
|
||||||
@@ -51,10 +53,10 @@ def setup_project_routes(app, service_manager, prefix=''):
|
|||||||
async def project_page(request):
|
async def project_page(request):
|
||||||
return web.FileResponse("static/web_panel/pages/projects.html")
|
return web.FileResponse("static/web_panel/pages/projects.html")
|
||||||
|
|
||||||
app.router.add_get(f'{prefix}/api/projects', list_projects)
|
app.router.add_get(f'{prefix}/api/projects', panel_auth(list_projects))
|
||||||
app.router.add_post("/api/projects/run", run_project)
|
app.router.add_post(f'{prefix}/api/projects/run', panel_auth(run_project))
|
||||||
app.router.add_get("/api/projects/{name}/logs", get_logs)
|
app.router.add_get(f'{prefix}/api/projects/{{name}}/logs', panel_auth(get_logs))
|
||||||
app.router.add_post("/api/projects/{name}/stop", stop_project)
|
app.router.add_post(f'{prefix}/api/projects/{{name}}/stop', panel_auth(stop_project))
|
||||||
app.router.add_post("/api/projects/{name}/stdin", send_stdin)
|
app.router.add_post(f'{prefix}/api/projects/{{name}}/stdin', panel_auth(send_stdin))
|
||||||
app.router.add_get("/pages/projects", project_page)
|
app.router.add_get(f'{prefix}/pages/projects', panel_auth(project_page))
|
||||||
logger.info("📦 项目管理路由已注册")
|
logger.info("📦 项目管理路由已注册 (已加认证)")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from aiohttp import web, ClientSession
|
from aiohttp import web, ClientSession
|
||||||
import json, logging, asyncio
|
import json, logging, asyncio
|
||||||
|
from ..utils.auth import panel_auth
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def setup_proxy_routes(app, service_manager, prefix=''):
|
def setup_proxy_routes(app, service_manager, prefix=''):
|
||||||
@@ -24,7 +26,7 @@ def setup_proxy_routes(app, service_manager, prefix=''):
|
|||||||
ps.unregister_proxy(path)
|
ps.unregister_proxy(path)
|
||||||
return web.json_response({"ok": True})
|
return web.json_response({"ok": True})
|
||||||
|
|
||||||
app.router.add_get(f'{prefix}/api/proxy', list_proxies)
|
app.router.add_get(f'{prefix}/api/proxy', panel_auth(list_proxies))
|
||||||
app.router.add_post(f'{prefix}/api/proxy', add_proxy)
|
app.router.add_post(f'{prefix}/api/proxy', panel_auth(add_proxy))
|
||||||
app.router.add_delete(f'{prefix}/api/proxy/{{path}}', remove_proxy)
|
app.router.add_delete(f'{prefix}/api/proxy/{{path}}', panel_auth(remove_proxy))
|
||||||
logger.info(f'🔀 代理路由已注册 ({prefix}/api/proxy)')
|
logger.info(f'🔀 代理路由已注册 ({prefix}/api/proxy) (已加认证)')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from ..utils.system_info import SystemInfoCollector
|
from ..utils.system_info import SystemInfoCollector
|
||||||
|
from ..utils.auth import panel_auth
|
||||||
|
|
||||||
collector = SystemInfoCollector()
|
collector = SystemInfoCollector()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -11,11 +12,25 @@ logger = logging.getLogger(__name__)
|
|||||||
# Track active system-status WS clients
|
# Track active system-status WS clients
|
||||||
_sys_ws_clients: set = set()
|
_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=''):
|
def setup_routes(app, prefix=''):
|
||||||
app.router.add_get(f'{prefix}/api/framework', get_framework)
|
app.router.add_get(f'{prefix}/api/framework', panel_auth(get_framework))
|
||||||
app.router.add_get(f'{prefix}/api/system', get_system)
|
app.router.add_get(f'{prefix}/api/system', panel_auth(get_system))
|
||||||
app.router.add_get(f'{prefix}/api/system/ws', sys_ws_handler)
|
app.router.add_get(f'{prefix}/api/system/ws', _ws_auth_wrapper(sys_ws_handler))
|
||||||
logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws")
|
logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws (已加认证)")
|
||||||
|
|
||||||
def _read_version():
|
def _read_version():
|
||||||
"""Read version from config file (shared helper)"""
|
"""Read version from config file (shared helper)"""
|
||||||
|
|||||||
Reference in New Issue
Block a user