Initial commit: SenSu Alpha 0.2.0
- 13-service async plugin framework - Textual TUI with CLI fallback - Plugin hot-reload + permission system - Web management panel (aiohttp) - Bridge-based inter-module communication - 10 regression tests Fixes applied: - PBKDF2-SHA256 auth (was plain SHA256) - Auth bypass removed (was allow-all on fail) - Bare excepts replaced with logged errors - CatFramework/DreamSu -> SenSu naming unified - ServiceManager: health checks + startup_order - Env var credentials (SENSU_ADMIN_PASSWORD etc)
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import secrets
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from ..utils.auth import panel_auth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 全局 Session 存储 (内存型)
|
||||
# 格式: { "token_string": { "username": "...", "perms": [...] } }
|
||||
PANEL_SESSION_STORE = {}
|
||||
|
||||
def setup_routes(app, prefix=''):
|
||||
"""注册面板认证路由"""
|
||||
# 🟢 关键:将 Session Store 挂载到 app,供拦截器读取
|
||||
app['panel_session_store'] = PANEL_SESSION_STORE
|
||||
|
||||
# 路由注册
|
||||
app.router.add_post(f'{prefix}/api/login', handle_login)
|
||||
# 退出和状态检查都需要拦截
|
||||
app.router.add_post(f'{prefix}/api/logout', panel_auth(handle_logout))
|
||||
app.router.add_get(f'{prefix}/api/auth/status', panel_auth(handle_auth_status))
|
||||
|
||||
async def handle_login(req):
|
||||
"""处理面板登录"""
|
||||
try:
|
||||
data = await req.json()
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
cfg = req.app.get('panel_config', {})
|
||||
cfg_user = cfg.get('username', 'admin')
|
||||
cfg_pass = cfg.get('password', 'admin')
|
||||
|
||||
# 校验配置中的账号密码
|
||||
if username == cfg_user and password == cfg_pass:
|
||||
# 登录成功:生成 Token
|
||||
token = secrets.token_hex(16)
|
||||
|
||||
# 写入 Session Store
|
||||
user_info = {
|
||||
"username": username,
|
||||
"perms": ["admin"],
|
||||
"login_time": __import__('time').time()
|
||||
}
|
||||
PANEL_SESSION_STORE[token] = user_info
|
||||
|
||||
logger.info(f"✅ 面板登录成功: {username} (Session: {token[:4]}...)")
|
||||
|
||||
resp = web.json_response({"success": True, "username": username})
|
||||
# 设置 Cookie
|
||||
resp.set_cookie("panel_token", token, max_age=259200, httponly=True, samesite="Lax")
|
||||
return resp
|
||||
else:
|
||||
logger.warning(f"❌ 面板登录失败: 用户 {username} 密码错误")
|
||||
return web.json_response({"success": False, "msg": "用户名或密码错误"}, status=401)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"登录异常: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
async def handle_logout(req):
|
||||
"""处理退出登录"""
|
||||
token = req.cookies.get("panel_token")
|
||||
if token and token in PANEL_SESSION_STORE:
|
||||
del PANEL_SESSION_STORE[token]
|
||||
logger.info(f"👋 用户退出登录")
|
||||
|
||||
resp = web.json_response({"success": True})
|
||||
resp.del_cookie("panel_token")
|
||||
return resp
|
||||
|
||||
async def handle_auth_status(req):
|
||||
"""获取当前认证状态 (被 panel_auth 拦截,能进来说明已认证)"""
|
||||
user = req.get('user', {})
|
||||
return web.json_response({
|
||||
"authenticated": True,
|
||||
"username": user.get("username", "Unknown"),
|
||||
"perms": user.get("perms", [])
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
from aiohttp import web
|
||||
from ..utils.auth import panel_auth
|
||||
|
||||
def setup_routes(app, prefix=''):
|
||||
app.router.add_post(f'{prefix}/api/command', panel_auth(exec_cmd))
|
||||
|
||||
async def exec_cmd(req):
|
||||
d = await req.json()
|
||||
cs = req.app.get('service_manager').get_service("command")
|
||||
if not cs: return web.json_response({"error": "Missing"}, 503)
|
||||
try:
|
||||
res = await cs.execute_command(d.get('command',''))
|
||||
return web.json_response({"success": True, "output": str(res)})
|
||||
except Exception as e:
|
||||
return web.json_response({"success": False, "error": str(e)})
|
||||
@@ -0,0 +1,30 @@
|
||||
import json, asyncio
|
||||
from aiohttp import web
|
||||
from ..utils.auth import panel_auth
|
||||
|
||||
active_ws = set()
|
||||
|
||||
def setup_routes(app, prefix=''):
|
||||
app.router.add_get(f'{prefix}/api/logs/ws', panel_auth(ws_handler))
|
||||
|
||||
async def ws_handler(req):
|
||||
ws = web.WebSocketResponse(heartbeat=30.0)
|
||||
await ws.prepare(req)
|
||||
active_ws.add(ws)
|
||||
try:
|
||||
async for msg in ws:
|
||||
if msg.type == web.WSMsgType.TEXT:
|
||||
d = json.loads(msg.data)
|
||||
if d.get('action') == 'set_level':
|
||||
ls = req.app.get('log_service')
|
||||
if ls: ls.set_level(d.get('level','INFO'))
|
||||
finally: active_ws.discard(ws)
|
||||
return ws
|
||||
|
||||
def broadcast_log(log_record):
|
||||
if not active_ws: return
|
||||
payload = json.dumps({"type":"log", "level":log_record.get('level','INFO'),
|
||||
"message":log_record.get('simple_message',''), "timestamp":log_record.get('timestamp',0)})
|
||||
for ws in list(active_ws):
|
||||
if not ws.closed: asyncio.ensure_future(ws.send_str(payload))
|
||||
else: active_ws.discard(ws)
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from ..utils.auth import panel_auth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def setup_routes(app, prefix=''):
|
||||
app.router.add_get(f'{prefix}/api/plugins', panel_auth(list_plugins))
|
||||
app.router.add_post(f'{prefix}/api/plugins/{{name}}/{{action}}', panel_auth(manage_plugin))
|
||||
app.router.add_get(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(get_perms))
|
||||
app.router.add_post(f'{prefix}/api/plugins/{{name}}/perms', panel_auth(set_perms))
|
||||
|
||||
async def list_plugins(req):
|
||||
sm = req.app.get('service_manager')
|
||||
if not sm:
|
||||
return web.json_response({"error": "Service Manager 未初始化"}, status=503)
|
||||
|
||||
ps = sm.get_service("plugin")
|
||||
if not ps:
|
||||
return web.json_response({"plugins": []})
|
||||
|
||||
data = []
|
||||
for name, info in ps.plugin_info.items():
|
||||
data.append({
|
||||
"name": name,
|
||||
"version": getattr(info, 'version', '?'),
|
||||
"running": name in ps.plugins,
|
||||
"enabled": True
|
||||
})
|
||||
return web.json_response({"plugins": data})
|
||||
|
||||
async def manage_plugin(req):
|
||||
sm = req.app.get('service_manager')
|
||||
if not sm: return web.json_response({"error": "SM Missing"}, 503)
|
||||
|
||||
name = req.match_info['name']
|
||||
action = req.match_info['action']
|
||||
ps = sm.get_service("plugin")
|
||||
|
||||
if not ps: return web.json_response({"error": "Plugin Service Missing"}, 503)
|
||||
|
||||
try:
|
||||
if action in ('disable', 'unload'):
|
||||
await ps.unload_plugin(name)
|
||||
elif action == 'enable':
|
||||
await ps.load_plugin(name)
|
||||
elif action == 'reload':
|
||||
await ps.unload_plugin(name)
|
||||
await ps.load_plugin(name)
|
||||
return web.json_response({"success": True, "msg": "操作成功"})
|
||||
except Exception as e:
|
||||
logger.error(f"插件操作失败: {e}")
|
||||
return web.json_response({"success": False, "error": str(e)})
|
||||
|
||||
async def get_perms(req):
|
||||
return web.json_response({"plugin": req.match_info['name'], "permissions": ["read", "write"]})
|
||||
|
||||
async def set_perms(req):
|
||||
return web.json_response({"success": True})
|
||||
@@ -0,0 +1,27 @@
|
||||
import time
|
||||
from aiohttp import web
|
||||
from ..utils.auth import panel_auth
|
||||
from ..utils.system_info import SystemInfoCollector
|
||||
|
||||
collector = SystemInfoCollector()
|
||||
|
||||
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))
|
||||
|
||||
async def get_framework(req):
|
||||
sm = req.app.get('service_manager')
|
||||
if not sm: return web.json_response({"error": "Missing"}, 500)
|
||||
|
||||
ps = sm.get_service("plugin")
|
||||
# 🟢 修复:使用 sm.start_time 属性
|
||||
uptime = time.time() - getattr(sm, 'start_time', time.time())
|
||||
|
||||
return web.json_response({
|
||||
"version": "Alpha_0.2.0",
|
||||
"uptime": int(uptime), # 取整秒
|
||||
"plugins": len(ps.plugins) if ps else 0
|
||||
})
|
||||
|
||||
async def get_system(req):
|
||||
return web.json_response(collector.get_all())
|
||||
Reference in New Issue
Block a user