重大重构:引擎模块拆分 + P0插件实现 + 55个Bug修复
核心变更: - engine.py(1781行)拆分为8个独立模块: lifecycle/security/deps/ datastore/pl_injector/watcher/signature/manager - 新增plugin-bridge: 事件总线 + 服务注册 + RPC通信 - 新增i18n: 国际化/多语言翻译支持 - 新增plugin-storage: 插件键值/文件存储 - 新增ws-api: WebSocket实时通信(pub/sub + 自定义处理器) - nodejs-adapter统一为Plugin ABC模式 Bug修复: - 修复load_all()中store_dir未定义崩溃 - 修复DependencyResolver入度计算(拓扑排序) - 修复PermissionError隐藏内置异常 - 修复CORS中间件头部未附加到响应 - 修复IntegrityChecker跳过__pycache__目录 - 修复版本号不一致(v2.0.0→v1.2.0) - 修复测试文件的Logger导入/路径/私有方法调用 - 修复context.py缺少typing导入 - 修复config.py STORE_DIR默认路径(./mods→./store) 测试覆盖: 14→91个测试, 全部通过
This commit is contained in:
277
oss/core/security.py
Normal file
277
oss/core/security.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import hashlib
|
||||
import time
|
||||
import json
|
||||
import functools
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Callable, TYPE_CHECKING
|
||||
from collections import deque
|
||||
|
||||
from oss.logger.logger import Log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from oss.core.manager import PluginManager
|
||||
|
||||
|
||||
class PluginPermissionError(Exception):
|
||||
"""插件权限错误"""
|
||||
pass
|
||||
|
||||
|
||||
class PluginProxy:
|
||||
"""插件代理 - 防止越级访问"""
|
||||
def __init__(self, plugin_name: str, plugin_instance: Any, allowed_plugins: list[str], all_plugins: dict):
|
||||
self._plugin_name = plugin_name
|
||||
self._plugin_instance = plugin_instance
|
||||
self._allowed_plugins = set(allowed_plugins)
|
||||
self._all_plugins = all_plugins
|
||||
|
||||
def get_plugin(self, name: str) -> Any:
|
||||
if name not in self._allowed_plugins and "*" not in self._allowed_plugins:
|
||||
raise PluginPermissionError(f"插件 '{self._plugin_name}' 无权访问插件 '{name}'")
|
||||
if name not in self._all_plugins:
|
||||
return None
|
||||
return self._all_plugins[name]["instance"]
|
||||
|
||||
def list_plugins(self) -> list[str]:
|
||||
if "*" in self._allowed_plugins:
|
||||
return list(self._all_plugins.keys())
|
||||
return [n for n in self._allowed_plugins if n in self._all_plugins]
|
||||
|
||||
def get_capability(self, capability: str) -> Any:
|
||||
return None
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
return getattr(self._plugin_instance, name)
|
||||
|
||||
|
||||
class IntegrityChecker:
|
||||
"""文件完整性检查"""
|
||||
|
||||
def __init__(self):
|
||||
self._hashes: dict[str, str] = {}
|
||||
|
||||
def compute_hash(self, plugin_dir: Path) -> str:
|
||||
"""计算插件目录的 SHA-256 hash"""
|
||||
hasher = hashlib.sha256()
|
||||
for file_path in sorted(plugin_dir.rglob("*")):
|
||||
if file_path.is_file() and "__pycache__" not in file_path.parts and file_path.name != "SIGNATURE":
|
||||
rel_path = str(file_path.relative_to(plugin_dir))
|
||||
hasher.update(rel_path.encode("utf-8"))
|
||||
hasher.update(file_path.read_bytes())
|
||||
return hasher.hexdigest()
|
||||
|
||||
def register(self, plugin_name: str, plugin_dir: Path):
|
||||
"""注册插件的初始 hash"""
|
||||
self._hashes[plugin_name] = self.compute_hash(plugin_dir)
|
||||
|
||||
def verify(self, plugin_name: str, plugin_dir: Path) -> tuple[bool, str]:
|
||||
"""验证插件文件是否被篡改"""
|
||||
if plugin_name not in self._hashes:
|
||||
return False, f"插件 '{plugin_name}' 未注册完整性检查"
|
||||
current = self.compute_hash(plugin_dir)
|
||||
if current == self._hashes[plugin_name]:
|
||||
return True, "完整性验证通过"
|
||||
return False, f"文件 hash 不匹配,插件可能被篡改"
|
||||
|
||||
def get_hash(self, plugin_name: str) -> Optional[str]:
|
||||
return self._hashes.get(plugin_name)
|
||||
|
||||
|
||||
class MemoryGuard:
|
||||
"""运行时内存保护 - 防止插件修改 Core 内部状态"""
|
||||
|
||||
FROZEN_ATTRS = {
|
||||
"plugins", "capability_registry", "lifecycle_manager",
|
||||
"dependency_resolver", "signature_verifier", "pl_injector",
|
||||
"integrity_checker", "audit_logger", "tamper_monitor",
|
||||
"fallback_manager", "http_server", "repl_shell",
|
||||
}
|
||||
|
||||
def __init__(self, manager: PluginManager):
|
||||
self._manager = manager
|
||||
self._protected = True
|
||||
|
||||
def enable(self):
|
||||
self._protected = True
|
||||
|
||||
def disable(self):
|
||||
self._protected = False
|
||||
|
||||
def check_setattr(self, obj: Any, name: str, value: Any) -> bool:
|
||||
"""检查是否允许设置属性,返回 False 表示拒绝"""
|
||||
if not self._protected:
|
||||
return True
|
||||
if obj is self._manager and name in self.FROZEN_ATTRS:
|
||||
Log.warn("Core", f"内存防护: 阻止了对 Core 内部属性 '{name}' 的修改")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class AuditLogger:
|
||||
"""插件行为审计"""
|
||||
|
||||
def __init__(self, max_logs: int = 1000):
|
||||
self._logs: deque = deque(maxlen=max_logs)
|
||||
self._enabled = True
|
||||
|
||||
def enable(self):
|
||||
self._enabled = True
|
||||
|
||||
def disable(self):
|
||||
self._enabled = False
|
||||
|
||||
def log(self, plugin_name: str, action: str, detail: str = ""):
|
||||
"""记录插件行为"""
|
||||
if not self._enabled:
|
||||
return
|
||||
self._logs.append({
|
||||
"time": time.time(),
|
||||
"plugin": plugin_name,
|
||||
"action": action,
|
||||
"detail": detail,
|
||||
})
|
||||
|
||||
def get_logs(self, plugin_name: str = None, limit: int = 50) -> list[dict]:
|
||||
"""查询审计日志"""
|
||||
if plugin_name:
|
||||
filtered = [log for log in self._logs if log["plugin"] == plugin_name]
|
||||
else:
|
||||
filtered = list(self._logs)
|
||||
return filtered[-limit:]
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
"""获取审计统计"""
|
||||
stats: dict[str, int] = {}
|
||||
for log in self._logs:
|
||||
stats[log["plugin"]] = stats.get(log["plugin"], 0) + 1
|
||||
return stats
|
||||
|
||||
|
||||
class TamperMonitor:
|
||||
"""防篡改监控 - 定期检查已加载插件的文件完整性"""
|
||||
|
||||
def __init__(self, manager: PluginManager, interval: int = 30):
|
||||
self._manager = manager
|
||||
self._interval = interval
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self._alerts: deque = deque(maxlen=100)
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self._thread.start()
|
||||
Log.info("Core", f"防篡改监控已启动 (间隔: {self._interval}s)")
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
if self._thread:
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
def _monitor_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
for plugin_name, info in self._manager.plugins.items():
|
||||
plugin_dir = self._manager._get_plugin_dir(plugin_name)
|
||||
if not plugin_dir:
|
||||
continue
|
||||
valid, msg = self._manager.integrity_checker.verify(plugin_name, plugin_dir)
|
||||
if not valid:
|
||||
alert = {
|
||||
"time": time.time(),
|
||||
"plugin": plugin_name,
|
||||
"message": msg,
|
||||
}
|
||||
self._alerts.append(alert)
|
||||
Log.error("Core", f"防篡改告警: 插件 '{plugin_name}' 可能被篡改!")
|
||||
# 自动停止被篡改的插件
|
||||
try:
|
||||
info["instance"].stop()
|
||||
lifecycle = self._manager.lifecycle_manager.get(plugin_name)
|
||||
if lifecycle:
|
||||
lifecycle.mark_crashed()
|
||||
except Exception as e:
|
||||
Log.error("Core", f"停止被篡改插件 '{plugin_name}' 失败: {e}")
|
||||
except Exception as e:
|
||||
Log.error("Core", f"防篡改监控异常: {e}")
|
||||
time.sleep(self._interval)
|
||||
|
||||
def get_alerts(self) -> list[dict]:
|
||||
return list(self._alerts)
|
||||
|
||||
|
||||
class FallbackManager:
|
||||
"""降级恢复机制 - 插件崩溃时自动重启"""
|
||||
|
||||
def __init__(self, manager: PluginManager, max_retries: int = 3):
|
||||
self._manager = manager
|
||||
self._max_retries = max_retries
|
||||
self._retry_counts: dict[str, int] = {}
|
||||
self._degraded: set[str] = set()
|
||||
|
||||
def wrap_plugin_method(self, plugin_name: str, method: Callable) -> Callable:
|
||||
"""包装插件方法,捕获异常后自动重试"""
|
||||
|
||||
@functools.wraps(method)
|
||||
def safe_method(*args, **kwargs):
|
||||
try:
|
||||
return method(*args, **kwargs)
|
||||
except Exception as e:
|
||||
Log.error("Core", f"插件 '{plugin_name}' 方法 '{method.__name__}' 异常: {e}")
|
||||
self._handle_crash(plugin_name)
|
||||
return None
|
||||
|
||||
return safe_method
|
||||
|
||||
def _handle_crash(self, plugin_name: str):
|
||||
"""处理插件崩溃"""
|
||||
retry_count = self._retry_counts.get(plugin_name, 0)
|
||||
lifecycle = self._manager.lifecycle_manager.get(plugin_name)
|
||||
|
||||
bridge = self._manager._get_bridge()
|
||||
if bridge and plugin_name != "plugin-bridge":
|
||||
bridge.emit("plugin.crashed", name=plugin_name, retry=retry_count)
|
||||
|
||||
if retry_count < self._max_retries:
|
||||
self._retry_counts[plugin_name] = retry_count + 1
|
||||
Log.warn("Core", f"插件 '{plugin_name}' 崩溃,正在重启 (第 {retry_count + 1}/{self._max_retries} 次)")
|
||||
try:
|
||||
if lifecycle:
|
||||
lifecycle.mark_crashed()
|
||||
self._manager._restart_plugin(plugin_name)
|
||||
if lifecycle:
|
||||
lifecycle.start()
|
||||
Log.ok("Core", f"插件 '{plugin_name}' 重启成功")
|
||||
except Exception as e:
|
||||
Log.error("Core", f"插件 '{plugin_name}' 重启失败: {e}")
|
||||
else:
|
||||
Log.error("Core", f"插件 '{plugin_name}' 超过最大重试次数 ({self._max_retries}),标记为降级")
|
||||
self._degraded.add(plugin_name)
|
||||
if lifecycle:
|
||||
lifecycle.mark_degraded()
|
||||
|
||||
def recover(self, plugin_name: str) -> bool:
|
||||
"""手动恢复降级的插件"""
|
||||
if plugin_name not in self._degraded:
|
||||
return False
|
||||
self._retry_counts[plugin_name] = 0
|
||||
self._degraded.discard(plugin_name)
|
||||
try:
|
||||
self._manager._restart_plugin(plugin_name)
|
||||
lifecycle = self._manager.lifecycle_manager.get(plugin_name)
|
||||
if lifecycle:
|
||||
lifecycle.start()
|
||||
Log.ok("Core", f"插件 '{plugin_name}' 已手动恢复")
|
||||
return True
|
||||
except Exception as e:
|
||||
Log.error("Core", f"恢复插件 '{plugin_name}' 失败: {e}")
|
||||
return False
|
||||
|
||||
def is_degraded(self, plugin_name: str) -> bool:
|
||||
return plugin_name in self._degraded
|
||||
|
||||
def get_degraded_plugins(self) -> list[str]:
|
||||
return list(self._degraded)
|
||||
Reference in New Issue
Block a user