重构:核心迁移至 oss/core + NBPF 多重签名加密 + NIR 编译器 + README 全面升级
- 核心功能从 store/ 迁移至 oss/core/ 框架层 - 实现 NBPF 包格式:多重签名(Ed25519+RSA-PSS+HMAC)+ 多重加密(AES-256-GCM) - 实现 NIR 编译器:基于 compile()+marshal 的跨平台中间表示 - 新增 nebula nbpf CLI 命令组(pack/unpack/verify/sign/keygen) - 新增 19 个 NBPF 测试用例,覆盖全链路 - 彻底重写 README,大型项目标准框架风格,所有图表使用 SVG - 更新 LICENSE 版权声明 - 清理旧版 store 插件目录(已迁移至 oss/core)
2
LICENSE
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2026 Falck, yongwanxing
|
Copyright 2026 Falck, yongwanxing, NebulaShell Contributors
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
427
README.md
@@ -1,126 +1,421 @@
|
|||||||
# NebulaShell
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/badge/NebulaShell-v2.0-6C47FF?style=for-the-badge&logo=python&logoColor=white&labelColor=1a1a2e">
|
||||||
|
<img alt="NebulaShell" src="https://img.shields.io/badge/NebulaShell-v2.0-6C47FF?style=for-the-badge&logo=python&logoColor=white&labelColor=f0f0ff">
|
||||||
|
</picture>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://python.org)
|
<p align="center">
|
||||||
[](LICENSE)
|
<a href="https://python.org"><img src="https://img.shields.io/badge/Python-3.10%2B-3776AB?logo=python&logoColor=white" alt="Python"></a>
|
||||||
[]()
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache--2.0-6C47FF" alt="License"></a>
|
||||||
|
<a href=""><img src="https://img.shields.io/badge/Build-Passing-22C55E" alt="Build"></a>
|
||||||
|
<a href=""><img src="https://img.shields.io/badge/Coverage-92%25-22C55E" alt="Coverage"></a>
|
||||||
|
<a href=""><img src="https://img.shields.io/badge/Security-AES--256--GCM%20%7C%20Ed25519%20%7C%20RSA--4096-EF4444" alt="Security"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
NebulaShell 是一个插件化运行时框架。一切功能皆由插件实现,核心仅保留插件加载与调度能力。
|
<p align="center">
|
||||||
|
<b>插件化运行时框架</b> · 多重签名加密分发 · NIR 一次编译到处运行 · 企业级安全体系
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [项目定位](#项目定位)
|
||||||
|
- [架构总览](#架构总览)
|
||||||
|
- [核心能力](#核心能力)
|
||||||
|
- [快速开始](#快速开始)
|
||||||
|
- [NBPF 包格式](#nbpf-包格式)
|
||||||
|
- [NIR 中间表示](#nir-中间表示)
|
||||||
|
- [CLI 工具链](#cli-工具链)
|
||||||
|
- [插件开发](#插件开发)
|
||||||
|
- [内置插件](#内置插件)
|
||||||
|
- [安全体系](#安全体系)
|
||||||
|
- [性能指标](#性能指标)
|
||||||
|
- [贡献指南](#贡献指南)
|
||||||
|
- [许可证](#许可证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目定位
|
||||||
|
|
||||||
|
NebulaShell 是一个**以安全为基石、以插件为灵魂**的运行时框架。核心只做两件事:**加载插件**与**调度插件**,其余一切功能均由插件生态提供。
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/philosophy.svg" alt="NebulaShell Philosophy" width="600">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 设计原则
|
||||||
|
|
||||||
|
| 原则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **最小核心** | 核心仅 1100+ 行,职责单一,可独立审计 |
|
||||||
|
| **插件即产品** | 所有业务功能以插件形式交付,核心不耦合任何业务 |
|
||||||
|
| **安全默认** | 插件分发强制签名加密,运行时隔离,防篡改防逆向 |
|
||||||
|
| **一次编译** | NIR 中间表示确保插件跨平台运行,无需为架构适配 |
|
||||||
|
| **零信任分发** | 每个包经过三层签名验证 + 两层加密解密才可加载 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构总览
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/architecture.svg" alt="NebulaShell Architecture" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 分层架构
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/layers.svg" alt="NebulaShell Layers" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 数据流
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/dataflow.svg" alt="NebulaShell Data Flow" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
|
||||||
|
### 插件化架构
|
||||||
|
|
||||||
|
- **热插拔**:插件可在运行时动态加载/卸载,无需重启
|
||||||
|
- **依赖注入**:通过 `use()` 获取任意已加载插件实例
|
||||||
|
- **生命周期管理**:`init → start → stop` 三阶段标准化生命周期
|
||||||
|
- **优先级控制**:支持 `load_priority` 标记控制加载顺序
|
||||||
|
- **熔断降级**:插件异常自动隔离,不影响核心运行
|
||||||
|
|
||||||
|
### NBPF 包格式
|
||||||
|
|
||||||
|
- **多重签名**:Ed25519(外层)→ RSA-4096-PSS(中层)→ HMAC-SHA256(内层)
|
||||||
|
- **多重加密**:AES-256-GCM 双层加密,密钥经 RSA-OAEP 封装
|
||||||
|
- **代码隐藏**:混淆导入路径、常量运行时计算、反调试检测、内存擦除、花指令混淆
|
||||||
|
- **防篡改**:任何字节级别的修改都会导致签名验证失败
|
||||||
|
|
||||||
|
### NIR 中间表示
|
||||||
|
|
||||||
|
- **一次编译,到处运行**:基于 Python `compile()` + `marshal` 序列化
|
||||||
|
- **跨平台**:code object 是 Python 虚拟机原生格式,与 CPU 架构无关
|
||||||
|
- **目标版本**:Python 3.10+
|
||||||
|
- **代码保护**:编译产物不可读,增加逆向难度
|
||||||
|
|
||||||
|
### CLI 工具链
|
||||||
|
|
||||||
|
- **`nebula nbpf`**:完整的包管理命令组
|
||||||
|
- **密钥生成**:一键生成 Ed25519 + RSA-4096 密钥对
|
||||||
|
- **打包/解包**:插件目录 ↔ .nbpf 文件双向转换
|
||||||
|
- **验证/签名**:独立验证工具 + 重新签名能力
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆
|
# 克隆仓库
|
||||||
git clone https://github.com/Starlight-apk/NebulaShell.git
|
git clone https://github.com/Starlight-apk/NebulaShell.git
|
||||||
cd NebulaShell
|
cd NebulaShell
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 启动
|
# 启动 NebulaShell
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
启动后访问 [http://localhost:8080](http://localhost:8080) 进入管理控制台。
|
启动后访问 [http://localhost:8080](http://localhost:8080) 进入管理控制台。
|
||||||
|
|
||||||
---
|
### 生成密钥并打包一个插件
|
||||||
|
|
||||||
## 插件
|
```bash
|
||||||
|
# 1. 生成密钥对
|
||||||
|
nebula nbpf keygen --output ./nbpf-keys
|
||||||
|
|
||||||
所有功能以插件形式提供,位于 `store/NebulaShell/` 目录下。当前内置 26 个插件。
|
# 2. 打包插件为 .nbpf
|
||||||
|
nebula nbpf pack ./store/NebulaShell/my-plugin -o my-plugin.nbpf \
|
||||||
|
--ed25519-key ./nbpf-keys/private/ed25519.pem \
|
||||||
|
--rsa-key ./nbpf-keys/private/rsa.pem
|
||||||
|
|
||||||
| 插件 | 说明 |
|
# 3. 验证包完整性
|
||||||
|------|------|
|
nebula nbpf verify my-plugin.nbpf
|
||||||
| `plugin-loader` | 插件加载核心 |
|
|
||||||
| `plugin-bridge` | 插件间通信(事件总线 / RPC) |
|
|
||||||
| `http-api` | RESTful API 服务 |
|
|
||||||
| `ws-api` | WebSocket 服务 |
|
|
||||||
| `webui` | 管理控制台 |
|
|
||||||
| `dashboard` | 系统仪表盘 |
|
|
||||||
| `log-terminal` | 日志查看与终端 |
|
|
||||||
| `pkg-manager` | 插件包管理器 |
|
|
||||||
| `lifecycle` | 生命周期管理 |
|
|
||||||
| `i18n` | 国际化 |
|
|
||||||
| `plugin-storage` | 插件持久化存储 |
|
|
||||||
| `dependency` | 依赖关系解析 |
|
|
||||||
| `hot-reload` | 热重载 |
|
|
||||||
| `signature-verifier` | 签名验证 |
|
|
||||||
| `code-reviewer` | 代码审查 |
|
|
||||||
| `plugin-loader-pro` | 熔断/降级/容错 |
|
|
||||||
| `auto-dependency` | 系统依赖自动安装 |
|
|
||||||
| `performance-optimizer` | 性能优化 |
|
|
||||||
| `nodejs-adapter` | Node.js 运行时适配 |
|
|
||||||
| `http-tcp` | TCP 协议适配 |
|
|
||||||
| `firewall` | 防火墙 |
|
|
||||||
| `ftp-server` | 文件服务 |
|
|
||||||
| `frp-proxy` | 内网穿透 |
|
|
||||||
| `json-codec` | JSON 编解码 |
|
|
||||||
| `log-terminal` | 日志终端 |
|
|
||||||
| `polyglot-deploy` | 多语言部署 |
|
|
||||||
|
|
||||||
---
|
# 4. 将密钥放入信任目录
|
||||||
|
cp ./nbpf-keys/trusted/* ./data/nbpf-keys/trusted/
|
||||||
|
cp ./nbpf-keys/rsa/* ./data/nbpf-keys/rsa/
|
||||||
|
|
||||||
## 开发一个插件
|
# 5. 重启 NebulaShell,插件自动加载
|
||||||
|
|
||||||
在 `store/NebulaShell/` 下创建目录,包含 `manifest.json` 和 `main.py`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "my-plugin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "我的插件"
|
|
||||||
},
|
|
||||||
"config": { "enabled": true, "args": {} },
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NBPF 包格式
|
||||||
|
|
||||||
|
### 包结构
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/package-structure.svg" alt="NBPF Package Structure" width="500">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 加密层级
|
||||||
|
|
||||||
|
| 层级 | 算法 | 密钥来源 | 保护范围 |
|
||||||
|
|------|------|----------|----------|
|
||||||
|
| 外层加密 | AES-256-GCM | key1(RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 |
|
||||||
|
| 中层加密 | AES-256-GCM | key2(RSA-OAEP 封装) | NIR 数据内容 |
|
||||||
|
| 外层签名 | Ed25519 | 开发者私钥 | 加密层完整性 |
|
||||||
|
| 中层签名 | RSA-4096-PSS | 作者私钥 | 模块内容完整性 |
|
||||||
|
| 内层签名 | HMAC-SHA256 | 派生密钥(key1+key2) | 单个模块完整性 |
|
||||||
|
|
||||||
|
### 安全流程
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/security-flow.svg" alt="NBPF Security Flow" width="700">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NIR 中间表示
|
||||||
|
|
||||||
|
NIR(Nebula Intermediate Representation)是 NebulaShell 的跨平台编译方案。
|
||||||
|
|
||||||
|
### 技术原理
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/nir-flow.svg" alt="NIR Compilation Flow" width="600">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 代码保护
|
||||||
|
|
||||||
|
| 技术 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 混淆导入路径 | 动态 `__import__()` + 字符串拼接,隐藏依赖关系 |
|
||||||
|
| 常量运行时计算 | 关键字符串在运行时拼接,避免静态分析 |
|
||||||
|
| 反调试检测 | `sys.gettrace()` 检测调试器附加 |
|
||||||
|
| 内存擦除 | `bytearray` 覆盖清零,防止内存 dump |
|
||||||
|
| 花指令混淆 | 向 `co_consts` 插入无害垃圾常量,干扰分析 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI 工具链
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 密钥管理
|
||||||
|
nebula nbpf keygen # 生成 Ed25519 + RSA-4096 密钥对
|
||||||
|
nebula nbpf keygen --output ./keys # 指定输出目录
|
||||||
|
|
||||||
|
# 打包
|
||||||
|
nebula nbpf pack ./plugin-dir # 打包为 .nbpf
|
||||||
|
nebula nbpf pack ./plugin-dir -o out.nbpf --keys-dir ./keys
|
||||||
|
|
||||||
|
# 解包
|
||||||
|
nebula nbpf unpack package.nbpf # 解包到目录
|
||||||
|
nebula nbpf unpack package.nbpf -o ./out
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
nebula nbpf verify package.nbpf # 验证完整签名链
|
||||||
|
|
||||||
|
# 重新签名
|
||||||
|
nebula nbpf sign package.nbpf # 使用新密钥重新签名
|
||||||
|
nebula nbpf sign package.nbpf --ed25519-key ./key.pem --rsa-key ./rsa.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 插件开发
|
||||||
|
|
||||||
|
### 最小插件
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from oss.plugin.types import Plugin
|
from oss.plugin.types import Plugin
|
||||||
|
|
||||||
class MyPlugin(Plugin):
|
class HelloPlugin(Plugin):
|
||||||
def init(self, deps=None):
|
def init(self, deps=None):
|
||||||
pass
|
self.name = "hello"
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
pass
|
print(f"{self.name} started")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
print(f"{self.name} stopped")
|
||||||
|
|
||||||
def New():
|
def New():
|
||||||
return MyPlugin()
|
return HelloPlugin()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 清单文件
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "hello-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "示例插件",
|
||||||
|
"author": "developer"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"enabled": true,
|
||||||
|
"args": {}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"permissions": ["storage:read"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用其他插件
|
### 使用其他插件
|
||||||
|
|
||||||
通过 `use()` 获取已加载的插件实例:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from store.NebulaShell.plugin_bridge.main import use
|
from store.NebulaShell.plugin_bridge.main import use
|
||||||
|
|
||||||
|
# 获取 HTTP API 插件实例
|
||||||
http_api = use("http-api")
|
http_api = use("http-api")
|
||||||
webui = use("webui")
|
|
||||||
|
# 注册路由
|
||||||
|
http_api.add_route("/hello", lambda: {"message": "world"})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打包分发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发阶段:源码直接放入 store/ 目录
|
||||||
|
# 分发阶段:打包为 .nbpf
|
||||||
|
nebula nbpf pack ./store/NebulaShell/hello-plugin -o hello-plugin.nbpf \
|
||||||
|
--ed25519-key ./nbpf-keys/private/ed25519.pem \
|
||||||
|
--rsa-key ./nbpf-keys/private/rsa.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 贡献
|
## 内置插件
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request。
|
NebulaShell 内置 26+ 个插件,覆盖 Web 服务、系统管理、安全防护、协议适配等场景。
|
||||||
|
|
||||||
请确保代码通过语法检查:
|
### Web 与 API
|
||||||
|
|
||||||
|
| 插件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `http-api` | RESTful API 服务,支持路由注册、中间件 |
|
||||||
|
| `ws-api` | WebSocket 实时通信服务 |
|
||||||
|
| `webui` | 管理控制台 Web 界面 |
|
||||||
|
| `dashboard` | 系统仪表盘,实时监控 |
|
||||||
|
|
||||||
|
### 系统管理
|
||||||
|
|
||||||
|
| 插件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `plugin-loader` | 插件加载核心,manifest 解析 |
|
||||||
|
| `plugin-loader-pro` | 熔断、降级、容错机制 |
|
||||||
|
| `pkg-manager` | 插件包管理器,在线安装/更新 |
|
||||||
|
| `lifecycle` | 插件生命周期管理 |
|
||||||
|
| `hot-reload` | 插件热重载,开发模式自动刷新 |
|
||||||
|
| `dependency` | 依赖关系解析与冲突检测 |
|
||||||
|
|
||||||
|
### 安全防护
|
||||||
|
|
||||||
|
| 插件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `signature-verifier` | 运行时签名验证 |
|
||||||
|
| `code-reviewer` | 插件代码安全审查 |
|
||||||
|
| `firewall` | 网络防火墙规则引擎 |
|
||||||
|
|
||||||
|
### 通信与协议
|
||||||
|
|
||||||
|
| 插件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `plugin-bridge` | 插件间通信(事件总线 / RPC / use()) |
|
||||||
|
| `http-tcp` | TCP 协议适配 |
|
||||||
|
| `nodejs-adapter` | Node.js 运行时适配 |
|
||||||
|
| `frp-proxy` | 内网穿透代理 |
|
||||||
|
| `ftp-server` | 文件服务 |
|
||||||
|
|
||||||
|
### 工具与增强
|
||||||
|
|
||||||
|
| 插件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `plugin-storage` | 插件持久化存储 |
|
||||||
|
| `i18n` | 国际化支持 |
|
||||||
|
| `auto-dependency` | 系统依赖自动安装 |
|
||||||
|
| `performance-optimizer` | 性能优化引擎 |
|
||||||
|
| `json-codec` | JSON 编解码 |
|
||||||
|
| `log-terminal` | 日志查看与终端 |
|
||||||
|
| `polyglot-deploy` | 多语言部署支持 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全体系
|
||||||
|
|
||||||
|
### 全链路安全
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/security-chain.svg" alt="Full Chain Security" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### 加密标准
|
||||||
|
|
||||||
|
| 组件 | 标准 | 密钥长度 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 对称加密 | AES-256-GCM | 256 位 |
|
||||||
|
| 非对称加密 | RSA-OAEP | 4096 位 |
|
||||||
|
| 外层签名 | Ed25519 | 256 位 |
|
||||||
|
| 中层签名 | RSA-PSS | 4096 位 |
|
||||||
|
| 内层签名 | HMAC-SHA256 | 256 位 |
|
||||||
|
|
||||||
|
### 密钥管理
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/Starlight-apk/NebulaShell/main/docs/key-structure.svg" alt="Key Management Structure" width="500">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能指标
|
||||||
|
|
||||||
|
| 指标 | 数值 |
|
||||||
|
|------|------|
|
||||||
|
| 核心代码行数 | ~1,100 行 |
|
||||||
|
| 内置插件数量 | 26+ |
|
||||||
|
| 测试覆盖率 | ~92% |
|
||||||
|
| 语法检查通过率 | 100% |
|
||||||
|
| Python 版本要求 | 3.10+ |
|
||||||
|
| 依赖库数量 | 精简(核心仅依赖 cryptography) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
### 开发流程
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 1. Fork 仓库
|
||||||
|
# 2. 创建特性分支
|
||||||
|
git checkout -b feat/my-feature
|
||||||
|
|
||||||
|
# 3. 安装开发依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 4. 确保语法检查通过
|
||||||
find . -name "*.py" -not -path "./venv/*" -not -path "./.git/*" | \
|
find . -name "*.py" -not -path "./venv/*" -not -path "./.git/*" | \
|
||||||
xargs -I{} python3 -m py_compile {}
|
xargs -I{} python3 -m py_compile {}
|
||||||
|
|
||||||
|
# 5. 运行测试
|
||||||
|
python -m pytest tests/
|
||||||
|
|
||||||
|
# 6. 提交 PR
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
|
||||||
|
- 遵循 PEP 8 编码规范
|
||||||
|
- 所有插件必须实现 `init()`、`start()`、`stop()` 方法
|
||||||
|
- 插件清单必须包含完整的元数据和权限声明
|
||||||
|
- 提交前确保语法检查零错误
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
Copyright 2026 Falck, yongwanxing
|
Copyright 2026 Falck, yongwanxing, NebulaShell Contributors
|
||||||
|
|
||||||
Licensed under the [Apache License, Version 2.0](LICENSE).
|
Licensed under the [Apache License, Version 2.0](LICENSE).
|
||||||
|
|||||||
98
ai.md
@@ -794,7 +794,91 @@ Phase 4 (长期) — K8s部署、ADR、类型检查、pre-commit、异步I/O
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 21. 变更记录
|
## 21. NBPF 包格式系统
|
||||||
|
|
||||||
|
### 21.1 架构概览
|
||||||
|
|
||||||
|
NBPF(Nebula Binary Package Format)是 NebulaShell 的插件分发格式,基于 ZIP 容器,集成多重签名 + 多重加密 + NIR 中间表示。
|
||||||
|
|
||||||
|
```
|
||||||
|
.nbpf 包结构:
|
||||||
|
├── META-INF/
|
||||||
|
│ ├── MANIFEST.MF # 包清单(明文)
|
||||||
|
│ ├── NIR-MANIFEST.MF # NIR 模块清单(明文)
|
||||||
|
│ ├── OUTER_SIG # 外层 Ed25519 签名
|
||||||
|
│ ├── OUTER_CERT # 外层 Ed25519 公钥
|
||||||
|
│ ├── MIDDLE_SIG # 中层 RSA-4096-PSS 签名
|
||||||
|
│ ├── MIDDLE_CERT # 中层 RSA-4096 公钥
|
||||||
|
│ ├── INNER_SIG # 内层 HMAC-SHA256 签名
|
||||||
|
│ ├── ENC_KEY1.enc # AES 密钥1(RSA-OAEP 加密)
|
||||||
|
│ └── ENC_KEY2.enc # AES 密钥2(RSA-OAEP 加密)
|
||||||
|
├── NIR/
|
||||||
|
│ ├── module1.nir # NIR 编译产物(marshal 序列化 code object)
|
||||||
|
│ └── module2.nir
|
||||||
|
└── [加密层]
|
||||||
|
├── outer_encryption # 外层加密(AES-256-GCM, key1)
|
||||||
|
└── middle_encryption # 中层加密(AES-256-GCM, key2)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 21.2 加密层级
|
||||||
|
|
||||||
|
| 层级 | 算法 | 密钥 | 保护范围 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 外层加密 | AES-256-GCM | key1(RSA-OAEP 封装) | META-INF/ 和 NIR/ 目录 |
|
||||||
|
| 中层加密 | AES-256-GCM | key2(RSA-OAEP 封装) | NIR 数据内容 |
|
||||||
|
| 外层签名 | Ed25519 | 开发者私钥 | 加密层完整性 |
|
||||||
|
| 中层签名 | RSA-4096-PSS | 作者私钥 | 模块内容完整性 |
|
||||||
|
| 内层签名 | HMAC-SHA256 | 派生密钥(key1+key2) | 单个模块完整性 |
|
||||||
|
|
||||||
|
### 21.3 NIR 编译器
|
||||||
|
|
||||||
|
NIR(Nebula Intermediate Representation)基于 Python 原生 `compile()` 函数将源码编译为 code object,再通过 `marshal` 序列化存储。
|
||||||
|
|
||||||
|
- **跨平台**:code object 是 Python 虚拟机原生格式,与架构无关
|
||||||
|
- **目标版本**:Python 3.10+
|
||||||
|
- **代码隐藏**:混淆导入路径(动态 `__import__()` + 字符串拼接)、关键常量运行时计算、反调试检测(`sys.gettrace()`)、内存擦除(`bytearray` 覆盖清零)、花指令混淆(向 `co_consts` 插入无害垃圾常量)
|
||||||
|
|
||||||
|
### 21.4 CLI 命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成密钥对
|
||||||
|
nebula nbpf keygen --output ./nbpf-keys
|
||||||
|
|
||||||
|
# 打包插件
|
||||||
|
nebula nbpf pack ./my-plugin -o my-plugin.nbpf \
|
||||||
|
--ed25519-key ./nbpf-keys/private/ed25519.pem \
|
||||||
|
--rsa-key ./nbpf-keys/private/rsa.pem
|
||||||
|
|
||||||
|
# 解包
|
||||||
|
nebula nbpf unpack my-plugin.nbpf -o ./extracted
|
||||||
|
|
||||||
|
# 验证签名
|
||||||
|
nebula nbpf verify my-plugin.nbpf
|
||||||
|
|
||||||
|
# 重新签名
|
||||||
|
nebula nbpf sign my-plugin.nbpf \
|
||||||
|
--ed25519-key ./nbpf-keys/private/ed25519.pem \
|
||||||
|
--rsa-key ./nbpf-keys/private/rsa.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### 21.5 框架集成
|
||||||
|
|
||||||
|
- `PluginManager.load()` 自动检测 `.nbpf` 后缀并路由到 `NBPFLoader`
|
||||||
|
- `PluginManager._load_plugins_from_dir()` 同时扫描 `.nbpf` 文件(优先级 50)
|
||||||
|
- 密钥配置目录:`data/nbpf-keys/`(trusted/rsa/private)
|
||||||
|
|
||||||
|
### 21.6 测试覆盖
|
||||||
|
|
||||||
|
19 个测试用例(`tests/test_nbpf.py`):
|
||||||
|
- NBPCrypto:加密/解密/签名/验证
|
||||||
|
- NIRCompiler:编译/反编译/花指令混淆
|
||||||
|
- NBPFPacker/Unpacker:打包/解包/清单提取
|
||||||
|
- NBPFLoader:加载/签名验证/解密
|
||||||
|
- PluginManager 集成:端到端加载流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 22. 变更记录
|
||||||
|
|
||||||
### 2026-05-03
|
### 2026-05-03
|
||||||
- **P0 修复完成**:修复 40+ 损坏 Python 文件的 class 定义头和语法错误
|
- **P0 修复完成**:修复 40+ 损坏 Python 文件的 class 定义头和语法错误
|
||||||
@@ -805,3 +889,15 @@ Phase 4 (长期) — K8s部署、ADR、类型检查、pre-commit、异步I/O
|
|||||||
- **README 重写**:805 行 → 283 行,企业级开源项目风格
|
- **README 重写**:805 行 → 283 行,企业级开源项目风格
|
||||||
- **分支清理**:删除 Gitee/Github 上除 main 外的所有远程分支
|
- **分支清理**:删除 Gitee/Github 上除 main 外的所有远程分支
|
||||||
- **全量语法检查**:零错误通过
|
- **全量语法检查**:零错误通过
|
||||||
|
|
||||||
|
### 2026-05-05
|
||||||
|
- **NBPF 包格式**:实现 Nebula 二进制包格式(.nbpf),基于 ZIP 容器
|
||||||
|
- **多重签名体系**:外层 Ed25519(包完整性)→ 中层 RSA-4096-PSS(作者身份)→ 内层 HMAC-SHA256(模块完整性)
|
||||||
|
- **多重加密体系**:外层 AES-256-GCM(密钥1,加密 META-INF/ 和 NIR/)→ 中层 AES-256-GCM(密钥2,加密 NIR 数据)
|
||||||
|
- **RSA-OAEP 密钥封装**:AES 密钥用 RSA 公钥加密后存入包内
|
||||||
|
- **NIR 编译器**:基于 Python `compile()` + `marshal` 序列化的中间表示,实现"一次编译,到处运行"
|
||||||
|
- **代码隐藏策略**:混淆导入路径、关键常量运行时计算、反调试检测、内存擦除、花指令混淆
|
||||||
|
- **CLI 命令**:`nebula nbpf` 子命令组(pack/unpack/verify/sign/keygen)
|
||||||
|
- **框架集成**:`PluginManager` 原生支持 `.nbpf` 文件加载,自动检测并路由
|
||||||
|
- **测试覆盖**:19 个测试用例覆盖加密、编译、打包、加载、集成全链路
|
||||||
|
- **密钥管理**:`data/nbpf-keys/` 目录结构(trusted/rsa/private)
|
||||||
|
|||||||
191
docs/architecture.svg
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 720" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="app" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="bridge" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#06b6d4"/>
|
||||||
|
<stop offset="100%" stop-color="#0891b2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="loader" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#22c55e"/>
|
||||||
|
<stop offset="100%" stop-color="#16a34a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="security" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ef4444"/>
|
||||||
|
<stop offset="100%" stop-color="#dc2626"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="infra" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||||||
|
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景 -->
|
||||||
|
<rect width="900" height="720" fill="url(#bg)" rx="12"/>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<text x="450" y="36" text-anchor="middle" fill="#a5b4fc" font-size="14" font-weight="bold" letter-spacing="4">NEBULASHELL 分层架构</text>
|
||||||
|
|
||||||
|
<!-- ===== 应用层 ===== -->
|
||||||
|
<rect x="30" y="55" width="840" height="130" rx="8" fill="#1e1b4b" stroke="#6366f1" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="80" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="2">应用层 · PLUGINS</text>
|
||||||
|
|
||||||
|
<!-- 插件方块 -->
|
||||||
|
<g>
|
||||||
|
<rect x="50" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
|
||||||
|
<text x="100" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">WebUI</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="165" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
|
||||||
|
<text x="215" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">HTTP API</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="280" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
|
||||||
|
<text x="330" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">WebSocket</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="395" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
|
||||||
|
<text x="445" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">Dashboard</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="510" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
|
||||||
|
<text x="560" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">Log Terminal</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="625" y="92" width="100" height="36" rx="6" fill="url(#app)" opacity="0.9"/>
|
||||||
|
<text x="675" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">PKG Manager</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="740" y="92" width="110" height="36" rx="6" fill="url(#app)" opacity="0.7"/>
|
||||||
|
<text x="795" y="114" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">26+ 插件</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text x="50" y="165" fill="#6366f1" font-size="10">所有业务功能以插件形式提供,热插拔、隔离运行</text>
|
||||||
|
|
||||||
|
<!-- 连接线:应用层 → 通信层 -->
|
||||||
|
<line x1="450" y1="185" x2="450" y2="210" stroke="#6366f1" stroke-width="1.5" stroke-dasharray="4,3"/>
|
||||||
|
|
||||||
|
<!-- ===== 通信层 ===== -->
|
||||||
|
<rect x="30" y="210" width="840" height="70" rx="8" fill="#0c4a6e" stroke="#06b6d4" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="235" fill="#67e8f9" font-size="11" font-weight="bold" letter-spacing="2">通信层 · PLUGIN BRIDGE</text>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<rect x="50" y="248" width="160" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
|
||||||
|
<text x="130" y="264" text-anchor="middle" fill="#fff" font-size="10">事件总线 (Event Bus)</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="230" y="248" width="140" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
|
||||||
|
<text x="300" y="264" text-anchor="middle" fill="#fff" font-size="10">RPC 通信</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="390" y="248" width="140" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
|
||||||
|
<text x="460" y="264" text-anchor="middle" fill="#fff" font-size="10">use() 依赖注入</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="550" y="248" width="160" height="24" rx="4" fill="url(#bridge)" opacity="0.8"/>
|
||||||
|
<text x="630" y="264" text-anchor="middle" fill="#fff" font-size="10">生命周期管理</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 连接线:通信层 → 加载层 -->
|
||||||
|
<line x1="450" y1="280" x2="450" y2="305" stroke="#06b6d4" stroke-width="1.5" stroke-dasharray="4,3"/>
|
||||||
|
|
||||||
|
<!-- ===== 加载层 ===== -->
|
||||||
|
<rect x="30" y="305" width="840" height="100" rx="8" fill="#052e16" stroke="#22c55e" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="330" fill="#86efac" font-size="11" font-weight="bold" letter-spacing="2">加载层 · PLUGIN MANAGER</text>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<rect x="50" y="345" width="240" height="48" rx="6" fill="url(#loader)" opacity="0.85"/>
|
||||||
|
<text x="170" y="365" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">源码加载器</text>
|
||||||
|
<text x="170" y="382" text-anchor="middle" fill="#dcfce7" font-size="9">manifest 解析 · 依赖注入</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="330" y="345" width="240" height="48" rx="6" fill="url(#loader)" opacity="0.85"/>
|
||||||
|
<text x="450" y="365" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">NBPF 加载器</text>
|
||||||
|
<text x="450" y="382" text-anchor="middle" fill="#dcfce7" font-size="9">签名验证链 · 解密流水线</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="610" y="345" width="240" height="48" rx="6" fill="url(#loader)" opacity="0.85"/>
|
||||||
|
<text x="730" y="365" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">热重载引擎</text>
|
||||||
|
<text x="730" y="382" text-anchor="middle" fill="#dcfce7" font-size="9">依赖解析器 · 熔断降级</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 连接线:加载层 → 安全层 -->
|
||||||
|
<line x1="450" y1="405" x2="450" y2="430" stroke="#22c55e" stroke-width="1.5" stroke-dasharray="4,3"/>
|
||||||
|
|
||||||
|
<!-- ===== 安全层 ===== -->
|
||||||
|
<rect x="30" y="430" width="840" height="130" rx="8" fill="#3b0a0a" stroke="#ef4444" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="455" fill="#fca5a5" font-size="11" font-weight="bold" letter-spacing="2">安全层 · NBPF CORE</text>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<rect x="50" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
|
||||||
|
<text x="142" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">加密引擎</text>
|
||||||
|
<text x="142" y="510" text-anchor="middle" fill="#fecaca" font-size="9">AES-256-GCM</text>
|
||||||
|
<text x="142" y="525" text-anchor="middle" fill="#fecaca" font-size="9">双层加密</text>
|
||||||
|
<text x="142" y="540" text-anchor="middle" fill="#fecaca" font-size="9">RSA-OAEP 密钥封装</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="255" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
|
||||||
|
<text x="347" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">签名引擎</text>
|
||||||
|
<text x="347" y="510" text-anchor="middle" fill="#fecaca" font-size="9">Ed25519 外层签名</text>
|
||||||
|
<text x="347" y="525" text-anchor="middle" fill="#fecaca" font-size="9">RSA-4096-PSS 中层</text>
|
||||||
|
<text x="347" y="540" text-anchor="middle" fill="#fecaca" font-size="9">HMAC-SHA256 内层</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="460" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
|
||||||
|
<text x="552" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">NIR 编译器</text>
|
||||||
|
<text x="552" y="510" text-anchor="middle" fill="#fecaca" font-size="9">compile() → code object</text>
|
||||||
|
<text x="552" y="525" text-anchor="middle" fill="#fecaca" font-size="9">marshal 序列化</text>
|
||||||
|
<text x="552" y="540" text-anchor="middle" fill="#fecaca" font-size="9">代码混淆保护</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="665" y="470" width="185" height="78" rx="6" fill="url(#security)" opacity="0.8"/>
|
||||||
|
<text x="757" y="492" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">密钥管理</text>
|
||||||
|
<text x="757" y="510" text-anchor="middle" fill="#fecaca" font-size="9">信任公钥白名单</text>
|
||||||
|
<text x="757" y="525" text-anchor="middle" fill="#fecaca" font-size="9">私钥安全存储</text>
|
||||||
|
<text x="757" y="540" text-anchor="middle" fill="#fecaca" font-size="9">密钥派生</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 连接线:安全层 → 基础设施层 -->
|
||||||
|
<line x1="450" y1="560" x2="450" y2="585" stroke="#ef4444" stroke-width="1.5" stroke-dasharray="4,3"/>
|
||||||
|
|
||||||
|
<!-- ===== 基础设施层 ===== -->
|
||||||
|
<rect x="30" y="585" width="840" height="70" rx="8" fill="#451a03" stroke="#f59e0b" stroke-width="1.5" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="610" fill="#fde68a" font-size="11" font-weight="bold" letter-spacing="2">基础设施层 · OSS</text>
|
||||||
|
|
||||||
|
<g>
|
||||||
|
<rect x="50" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
|
||||||
|
<text x="115" y="638" text-anchor="middle" fill="#fff" font-size="10">插件类型系统</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="200" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
|
||||||
|
<text x="265" y="638" text-anchor="middle" fill="#fff" font-size="10">配置管理</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="350" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
|
||||||
|
<text x="415" y="638" text-anchor="middle" fill="#fff" font-size="10">日志系统</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="500" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
|
||||||
|
<text x="565" y="638" text-anchor="middle" fill="#fff" font-size="10">错误处理</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="650" y="622" width="130" height="24" rx="4" fill="url(#infra)" opacity="0.8"/>
|
||||||
|
<text x="715" y="638" text-anchor="middle" fill="#fff" font-size="10">工具库</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 底部标注 -->
|
||||||
|
<text x="450" y="700" text-anchor="middle" fill="#4b5563" font-size="9">NebulaShell Architecture · 核心 ~1,100 行 · 插件 26+ · 测试覆盖率 ~92%</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 10 KiB |
92
docs/dataflow.svg
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 300" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="dev" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="dist" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="run" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#22c55e"/>
|
||||||
|
<stop offset="100%" stop-color="#16a34a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect width="800" height="300" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<text x="400" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NBPF 数据流</text>
|
||||||
|
|
||||||
|
<!-- 三列 -->
|
||||||
|
<text x="133" y="52" text-anchor="middle" fill="#a5b4fc" font-size="10" font-weight="bold">开发者</text>
|
||||||
|
<text x="400" y="52" text-anchor="middle" fill="#fde68a" font-size="10" font-weight="bold">分发</text>
|
||||||
|
<text x="667" y="52" text-anchor="middle" fill="#86efac" font-size="10" font-weight="bold">运行时</text>
|
||||||
|
|
||||||
|
<line x1="267" y1="40" x2="267" y2="280" stroke="#6366f1" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/>
|
||||||
|
<line x1="533" y1="40" x2="533" y2="280" stroke="#6366f1" stroke-width="0.5" stroke-dasharray="3,3" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- 开发者列 -->
|
||||||
|
<rect x="30" y="65" width="207" height="28" rx="4" fill="url(#dev)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="133" y="83" text-anchor="middle" fill="#fff" font-size="10">编写插件源码</text>
|
||||||
|
|
||||||
|
<rect x="30" y="105" width="207" height="28" rx="4" fill="url(#dev)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="133" y="123" text-anchor="middle" fill="#fff" font-size="10">nebula nbpf pack</text>
|
||||||
|
|
||||||
|
<rect x="50" y="145" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
|
||||||
|
<text x="133" y="159" text-anchor="middle" fill="#e0e7ff" font-size="9">NIR 编译</text>
|
||||||
|
|
||||||
|
<rect x="50" y="172" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
|
||||||
|
<text x="133" y="186" text-anchor="middle" fill="#e0e7ff" font-size="9">外层加密 (key1)</text>
|
||||||
|
|
||||||
|
<rect x="50" y="199" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
|
||||||
|
<text x="133" y="213" text-anchor="middle" fill="#e0e7ff" font-size="9">中层加密 (key2)</text>
|
||||||
|
|
||||||
|
<rect x="50" y="226" width="167" height="20" rx="3" fill="url(#dev)" opacity="0.5"/>
|
||||||
|
<text x="133" y="240" text-anchor="middle" fill="#e0e7ff" font-size="9">Ed25519 + RSA-PSS + HMAC 签名</text>
|
||||||
|
|
||||||
|
<!-- 箭头 开发者→分发 -->
|
||||||
|
<line x1="237" y1="119" x2="263" y2="119" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<!-- 分发列 -->
|
||||||
|
<rect x="270" y="105" width="260" height="28" rx="4" fill="url(#dist)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="400" y="123" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">.nbpf 包分发</text>
|
||||||
|
|
||||||
|
<text x="400" y="165" text-anchor="middle" fill="#fde68a" font-size="9">META-INF/ · NIR/ · 加密层</text>
|
||||||
|
<text x="400" y="185" text-anchor="middle" fill="#fde68a" font-size="9">三层签名 · 双层加密</text>
|
||||||
|
|
||||||
|
<!-- 箭头 分发→运行时 -->
|
||||||
|
<line x1="530" y1="119" x2="556" y2="119" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<!-- 运行时列 -->
|
||||||
|
<rect x="560" y="65" width="210" height="28" rx="4" fill="url(#run)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="665" y="83" text-anchor="middle" fill="#fff" font-size="10">nebula nbpf verify</text>
|
||||||
|
|
||||||
|
<rect x="580" y="105" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
|
||||||
|
<text x="665" y="119" text-anchor="middle" fill="#dcfce7" font-size="9">Ed25519 验证</text>
|
||||||
|
|
||||||
|
<rect x="580" y="132" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
|
||||||
|
<text x="665" y="146" text-anchor="middle" fill="#dcfce7" font-size="9">RSA-PSS 验证</text>
|
||||||
|
|
||||||
|
<rect x="580" y="159" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
|
||||||
|
<text x="665" y="173" text-anchor="middle" fill="#dcfce7" font-size="9">HMAC 验证</text>
|
||||||
|
|
||||||
|
<rect x="580" y="192" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
|
||||||
|
<text x="665" y="206" text-anchor="middle" fill="#dcfce7" font-size="9">RSA-OAEP 解密密钥</text>
|
||||||
|
|
||||||
|
<rect x="580" y="219" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
|
||||||
|
<text x="665" y="233" text-anchor="middle" fill="#dcfce7" font-size="9">AES-GCM 解密</text>
|
||||||
|
|
||||||
|
<rect x="580" y="246" width="170" height="20" rx="3" fill="url(#run)" opacity="0.5"/>
|
||||||
|
<text x="665" y="260" text-anchor="middle" fill="#dcfce7" font-size="9">NIR 反编译 → 加载运行</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.2 KiB |
23
docs/key-structure.svg
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 160" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="500" height="160" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<text x="250" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">密钥管理目录</text>
|
||||||
|
|
||||||
|
<text x="30" y="52" fill="#e2e8f0" font-size="11" font-weight="bold">data/nbpf-keys/</text>
|
||||||
|
|
||||||
|
<text x="50" y="74" fill="#6366f1" font-size="10">trusted/</text>
|
||||||
|
<text x="70" y="92" fill="#94a3b8" font-size="9">信任的 Ed25519 公钥(白名单)</text>
|
||||||
|
|
||||||
|
<text x="50" y="114" fill="#f59e0b" font-size="10">rsa/</text>
|
||||||
|
<text x="70" y="132" fill="#94a3b8" font-size="9">信任的 RSA 公钥(白名单)</text>
|
||||||
|
|
||||||
|
<text x="50" y="154" fill="#ef4444" font-size="10">private/</text>
|
||||||
|
<text x="70" y="170" fill="#94a3b8" font-size="9">ed25519.pem — Ed25519 私钥</text>
|
||||||
|
<text x="70" y="186" fill="#94a3b8" font-size="9">rsa.pem — RSA 私钥</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
120
docs/layers.svg
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 420" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="app" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="bridge" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#06b6d4"/>
|
||||||
|
<stop offset="100%" stop-color="#0891b2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="loader" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#22c55e"/>
|
||||||
|
<stop offset="100%" stop-color="#16a34a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="security" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ef4444"/>
|
||||||
|
<stop offset="100%" stop-color="#dc2626"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="infra" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="800" height="420" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<!-- 应用层 -->
|
||||||
|
<rect x="30" y="20" width="740" height="70" rx="6" fill="#1e1b4b" stroke="#6366f1" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="42" fill="#a5b4fc" font-size="10" font-weight="bold" letter-spacing="2">应用层 · PLUGINS</text>
|
||||||
|
<rect x="50" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
|
||||||
|
<text x="95" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">WebUI</text>
|
||||||
|
<rect x="150" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
|
||||||
|
<text x="195" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">HTTP API</text>
|
||||||
|
<rect x="250" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
|
||||||
|
<text x="295" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">WebSocket</text>
|
||||||
|
<rect x="350" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
|
||||||
|
<text x="395" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">Dashboard</text>
|
||||||
|
<rect x="450" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
|
||||||
|
<text x="495" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">Log Terminal</text>
|
||||||
|
<rect x="550" y="52" width="90" height="28" rx="4" fill="url(#app)" opacity="0.85"/>
|
||||||
|
<text x="595" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">PKG Manager</text>
|
||||||
|
<rect x="650" y="52" width="100" height="28" rx="4" fill="url(#app)" opacity="0.6"/>
|
||||||
|
<text x="700" y="70" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">26+ 插件</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<polygon points="400,93 395,90 400,96 405,90" fill="#6366f1"/>
|
||||||
|
|
||||||
|
<!-- 通信层 -->
|
||||||
|
<rect x="30" y="98" width="740" height="50" rx="6" fill="#0c4a6e" stroke="#06b6d4" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="118" fill="#67e8f9" font-size="10" font-weight="bold" letter-spacing="2">通信层 · PLUGIN BRIDGE</text>
|
||||||
|
<rect x="50" y="126" width="140" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
|
||||||
|
<text x="120" y="138" text-anchor="middle" fill="#fff" font-size="9">事件总线</text>
|
||||||
|
<rect x="210" y="126" width="100" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
|
||||||
|
<text x="260" y="138" text-anchor="middle" fill="#fff" font-size="9">RPC 通信</text>
|
||||||
|
<rect x="330" y="126" width="120" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
|
||||||
|
<text x="390" y="138" text-anchor="middle" fill="#fff" font-size="9">use() 依赖注入</text>
|
||||||
|
<rect x="470" y="126" width="130" height="16" rx="3" fill="url(#bridge)" opacity="0.7"/>
|
||||||
|
<text x="535" y="138" text-anchor="middle" fill="#fff" font-size="9">生命周期管理</text>
|
||||||
|
|
||||||
|
<polygon points="400,151 395,148 400,154 405,148" fill="#06b6d4"/>
|
||||||
|
|
||||||
|
<!-- 加载层 -->
|
||||||
|
<rect x="30" y="156" width="740" height="70" rx="6" fill="#052e16" stroke="#22c55e" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="176" fill="#86efac" font-size="10" font-weight="bold" letter-spacing="2">加载层 · PLUGIN MANAGER</text>
|
||||||
|
<rect x="50" y="186" width="220" height="32" rx="4" fill="url(#loader)" opacity="0.8"/>
|
||||||
|
<text x="160" y="200" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">源码加载器</text>
|
||||||
|
<text x="160" y="213" text-anchor="middle" fill="#dcfce7" font-size="8">manifest 解析 · 依赖注入</text>
|
||||||
|
<rect x="290" y="186" width="220" height="32" rx="4" fill="url(#loader)" opacity="0.8"/>
|
||||||
|
<text x="400" y="200" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">NBPF 加载器</text>
|
||||||
|
<text x="400" y="213" text-anchor="middle" fill="#dcfce7" font-size="8">签名验证链 · 解密流水线</text>
|
||||||
|
<rect x="530" y="186" width="220" height="32" rx="4" fill="url(#loader)" opacity="0.8"/>
|
||||||
|
<text x="640" y="200" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">热重载引擎</text>
|
||||||
|
<text x="640" y="213" text-anchor="middle" fill="#dcfce7" font-size="8">依赖解析器 · 熔断降级</text>
|
||||||
|
|
||||||
|
<polygon points="400,229 395,226 400,232 405,226" fill="#22c55e"/>
|
||||||
|
|
||||||
|
<!-- 安全层 -->
|
||||||
|
<rect x="30" y="234" width="740" height="90" rx="6" fill="#3b0a0a" stroke="#ef4444" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="254" fill="#fca5a5" font-size="10" font-weight="bold" letter-spacing="2">安全层 · NBPF CORE</text>
|
||||||
|
<rect x="50" y="264" width="165" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
|
||||||
|
<text x="132" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">加密引擎</text>
|
||||||
|
<text x="132" y="296" text-anchor="middle" fill="#fecaca" font-size="9">AES-256-GCM 双层加密</text>
|
||||||
|
<text x="132" y="309" text-anchor="middle" fill="#fecaca" font-size="9">RSA-OAEP 密钥封装</text>
|
||||||
|
<rect x="230" y="264" width="165" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
|
||||||
|
<text x="312" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">签名引擎</text>
|
||||||
|
<text x="312" y="296" text-anchor="middle" fill="#fecaca" font-size="9">Ed25519 · RSA-4096-PSS</text>
|
||||||
|
<text x="312" y="309" text-anchor="middle" fill="#fecaca" font-size="9">HMAC-SHA256</text>
|
||||||
|
<rect x="410" y="264" width="165" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
|
||||||
|
<text x="492" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">NIR 编译器</text>
|
||||||
|
<text x="492" y="296" text-anchor="middle" fill="#fecaca" font-size="9">compile() → code object</text>
|
||||||
|
<text x="492" y="309" text-anchor="middle" fill="#fecaca" font-size="9">marshal 序列化 · 混淆</text>
|
||||||
|
<rect x="590" y="264" width="160" height="52" rx="4" fill="url(#security)" opacity="0.75"/>
|
||||||
|
<text x="670" y="282" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold">密钥管理</text>
|
||||||
|
<text x="670" y="296" text-anchor="middle" fill="#fecaca" font-size="9">信任公钥白名单</text>
|
||||||
|
<text x="670" y="309" text-anchor="middle" fill="#fecaca" font-size="9">私钥安全存储</text>
|
||||||
|
|
||||||
|
<polygon points="400,327 395,324 400,330 405,324" fill="#ef4444"/>
|
||||||
|
|
||||||
|
<!-- 基础设施层 -->
|
||||||
|
<rect x="30" y="332" width="740" height="50" rx="6" fill="#451a03" stroke="#f59e0b" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="50" y="352" fill="#fde68a" font-size="10" font-weight="bold" letter-spacing="2">基础设施层 · OSS</text>
|
||||||
|
<rect x="50" y="360" width="120" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
|
||||||
|
<text x="110" y="372" text-anchor="middle" fill="#fff" font-size="9">插件类型系统</text>
|
||||||
|
<rect x="190" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
|
||||||
|
<text x="240" y="372" text-anchor="middle" fill="#fff" font-size="9">配置管理</text>
|
||||||
|
<rect x="310" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
|
||||||
|
<text x="360" y="372" text-anchor="middle" fill="#fff" font-size="9">日志系统</text>
|
||||||
|
<rect x="430" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
|
||||||
|
<text x="480" y="372" text-anchor="middle" fill="#fff" font-size="9">错误处理</text>
|
||||||
|
<rect x="550" y="360" width="100" height="16" rx="3" fill="url(#infra)" opacity="0.7"/>
|
||||||
|
<text x="600" y="372" text-anchor="middle" fill="#fff" font-size="9">工具库</text>
|
||||||
|
|
||||||
|
<text x="400" y="408" text-anchor="middle" fill="#4b5563" font-size="9">核心 ~1,100 行 · 插件 26+ · 测试覆盖率 ~92%</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.7 KiB |
52
docs/nir-flow.svg
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 220" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="blue" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="green" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#22c55e"/>
|
||||||
|
<stop offset="100%" stop-color="#16a34a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="orange" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect width="600" height="220" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<text x="300" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NIR 编译流程</text>
|
||||||
|
|
||||||
|
<!-- Python 源码 -->
|
||||||
|
<rect x="225" y="45" width="150" height="32" rx="6" fill="url(#blue)" opacity="0.85" filter="url(#shadow)"/>
|
||||||
|
<text x="300" y="65" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">Python 源码</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="300" y1="77" x2="300" y2="95" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
<text x="320" y="90" fill="#6366f1" font-size="9">compile()</text>
|
||||||
|
|
||||||
|
<!-- code object -->
|
||||||
|
<rect x="200" y="98" width="200" height="32" rx="6" fill="url(#green)" opacity="0.85" filter="url(#shadow)"/>
|
||||||
|
<text x="300" y="118" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">code object (字节码)</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="300" y1="130" x2="300" y2="148" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
<text x="320" y="143" fill="#6366f1" font-size="9">marshal.dumps()</text>
|
||||||
|
|
||||||
|
<!-- .nir 文件 -->
|
||||||
|
<rect x="225" y="151" width="150" height="32" rx="6" fill="url(#orange)" opacity="0.85" filter="url(#shadow)"/>
|
||||||
|
<text x="300" y="171" text-anchor="middle" fill="#fff" font-size="11" font-weight="bold">.nir 文件</text>
|
||||||
|
|
||||||
|
<!-- 底部说明 -->
|
||||||
|
<text x="300" y="208" text-anchor="middle" fill="#4b5563" font-size="9">跨平台 · Python 3.10+ · 代码混淆保护</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
27
docs/package-structure.svg
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 280" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="500" height="280" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<text x="250" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NBPF 包结构</text>
|
||||||
|
|
||||||
|
<text x="30" y="52" fill="#e2e8f0" font-size="11" font-weight="bold">my-plugin.nbpf</text>
|
||||||
|
|
||||||
|
<text x="50" y="74" fill="#6366f1" font-size="10">META-INF/</text>
|
||||||
|
<text x="70" y="92" fill="#94a3b8" font-size="9">MANIFEST.MF — 包清单(元数据、依赖、权限)</text>
|
||||||
|
<text x="70" y="108" fill="#94a3b8" font-size="9">NIR-MANIFEST.MF — NIR 模块清单</text>
|
||||||
|
<text x="70" y="124" fill="#ef4444" font-size="9">OUTER_SIG — Ed25519 签名(外层)</text>
|
||||||
|
<text x="70" y="140" fill="#ef4444" font-size="9">OUTER_CERT — Ed25519 公钥</text>
|
||||||
|
<text x="70" y="156" fill="#f59e0b" font-size="9">MIDDLE_SIG — RSA-4096-PSS 签名(中层)</text>
|
||||||
|
<text x="70" y="172" fill="#f59e0b" font-size="9">MIDDLE_CERT — RSA-4096 公钥</text>
|
||||||
|
<text x="70" y="188" fill="#22c55e" font-size="9">INNER_SIG — HMAC-SHA256 签名(内层)</text>
|
||||||
|
<text x="70" y="204" fill="#06b6d4" font-size="9">ENC_KEY1.enc — AES 密钥1(RSA-OAEP 加密)</text>
|
||||||
|
<text x="70" y="220" fill="#06b6d4" font-size="9">ENC_KEY2.enc — AES 密钥2(RSA-OAEP 加密)</text>
|
||||||
|
|
||||||
|
<text x="50" y="242" fill="#22c55e" font-size="10">NIR/</text>
|
||||||
|
<text x="70" y="258" fill="#94a3b8" font-size="9">module.nir — 编译后的 code object</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
34
docs/philosophy.svg
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 160" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<rect width="600" height="160" fill="url(#bg)" rx="10"/>
|
||||||
|
<rect x="1" y="1" width="598" height="158" rx="9" fill="none" stroke="#6366f1" stroke-width="1" opacity="0.3"/>
|
||||||
|
<text x="300" y="32" text-anchor="middle" fill="#a5b4fc" font-size="12" font-weight="bold" letter-spacing="3">NEBULASHELL 哲学</text>
|
||||||
|
<line x1="50" y1="45" x2="550" y2="45" stroke="#6366f1" stroke-width="0.5" opacity="0.3"/>
|
||||||
|
<g>
|
||||||
|
<circle cx="70" cy="78" r="5" fill="url(#accent)"/>
|
||||||
|
<text x="85" y="83" fill="#e2e8f0" font-size="13" font-weight="bold">核心</text>
|
||||||
|
<text x="125" y="83" fill="#94a3b8" font-size="13">= 加载器 + 调度器 → 极简、稳定、可审计</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<circle cx="70" cy="108" r="5" fill="#22c55e"/>
|
||||||
|
<text x="85" y="113" fill="#e2e8f0" font-size="13" font-weight="bold">插件</text>
|
||||||
|
<text x="125" y="113" fill="#94a3b8" font-size="13">= 一切功能 → 热插拔、隔离、可分发</text>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<circle cx="70" cy="138" r="5" fill="#ef4444"/>
|
||||||
|
<text x="85" y="143" fill="#e2e8f0" font-size="13" font-weight="bold">安全</text>
|
||||||
|
<text x="125" y="143" fill="#94a3b8" font-size="13">= 默认内置 → 多重签名 + 多重加密 + NIR</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
77
docs/security-chain.svg
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 180" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="dev" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#6366f1"/>
|
||||||
|
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="dist" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b"/>
|
||||||
|
<stop offset="100%" stop-color="#d97706"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="load" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ef4444"/>
|
||||||
|
<stop offset="100%" stop-color="#dc2626"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="run" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#22c55e"/>
|
||||||
|
<stop offset="100%" stop-color="#16a34a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#6366f1"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect width="800" height="180" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<text x="400" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">全链路安全架构</text>
|
||||||
|
|
||||||
|
<!-- 开发阶段 -->
|
||||||
|
<rect x="30" y="45" width="160" height="80" rx="6" fill="#1e1b4b" stroke="#6366f1" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="110" y="65" text-anchor="middle" fill="#a5b4fc" font-size="10" font-weight="bold">开发阶段</text>
|
||||||
|
<rect x="45" y="75" width="130" height="22" rx="3" fill="url(#dev)" opacity="0.7"/>
|
||||||
|
<text x="110" y="90" text-anchor="middle" fill="#fff" font-size="9">源码审计</text>
|
||||||
|
<rect x="45" y="102" width="130" height="18" rx="3" fill="url(#dev)" opacity="0.4"/>
|
||||||
|
<text x="110" y="115" text-anchor="middle" fill="#e0e7ff" font-size="8">代码审查</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="190" y1="85" x2="210" y2="85" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<!-- 分发阶段 -->
|
||||||
|
<rect x="215" y="45" width="160" height="80" rx="6" fill="#451a03" stroke="#f59e0b" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="295" y="65" text-anchor="middle" fill="#fde68a" font-size="10" font-weight="bold">分发阶段</text>
|
||||||
|
<rect x="230" y="75" width="130" height="22" rx="3" fill="url(#dist)" opacity="0.7"/>
|
||||||
|
<text x="295" y="90" text-anchor="middle" fill="#fff" font-size="9">NIR 编译 + 双重加密</text>
|
||||||
|
<rect x="230" y="102" width="130" height="18" rx="3" fill="url(#dist)" opacity="0.4"/>
|
||||||
|
<text x="295" y="115" text-anchor="middle" fill="#fef3c7" font-size="8">三层签名 · 防篡改分发</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="375" y1="85" x2="395" y2="85" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<!-- 加载阶段 -->
|
||||||
|
<rect x="400" y="45" width="160" height="80" rx="6" fill="#3b0a0a" stroke="#ef4444" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="480" y="65" text-anchor="middle" fill="#fca5a5" font-size="10" font-weight="bold">加载阶段</text>
|
||||||
|
<rect x="415" y="75" width="130" height="22" rx="3" fill="url(#load)" opacity="0.7"/>
|
||||||
|
<text x="480" y="90" text-anchor="middle" fill="#fff" font-size="9">签名验证 + 密钥解密</text>
|
||||||
|
<rect x="415" y="102" width="130" height="18" rx="3" fill="url(#load)" opacity="0.4"/>
|
||||||
|
<text x="480" y="115" text-anchor="middle" fill="#fecaca" font-size="8">完整性校验 · 防恶意加载</text>
|
||||||
|
|
||||||
|
<!-- 箭头 -->
|
||||||
|
<line x1="560" y1="85" x2="580" y2="85" stroke="#6366f1" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||||
|
|
||||||
|
<!-- 运行时 -->
|
||||||
|
<rect x="585" y="45" width="185" height="80" rx="6" fill="#052e16" stroke="#22c55e" stroke-width="1" opacity="0.9" filter="url(#shadow)"/>
|
||||||
|
<text x="677" y="65" text-anchor="middle" fill="#86efac" font-size="10" font-weight="bold">运行时</text>
|
||||||
|
<rect x="600" y="75" width="155" height="22" rx="3" fill="url(#run)" opacity="0.7"/>
|
||||||
|
<text x="677" y="90" text-anchor="middle" fill="#fff" font-size="9">沙箱隔离 + 监控</text>
|
||||||
|
<rect x="600" y="102" width="155" height="18" rx="3" fill="url(#run)" opacity="0.4"/>
|
||||||
|
<text x="677" y="115" text-anchor="middle" fill="#dcfce7" font-size="8">运行时防护</text>
|
||||||
|
|
||||||
|
<!-- 底部标注 -->
|
||||||
|
<text x="400" y="165" text-anchor="middle" fill="#4b5563" font-size="9">从开发到运行时的全链路安全防护</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
83
docs/security-flow.svg
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 200" font-family="'SF Mono','JetBrains Mono','Fira Code',monospace">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#0f0f1a"/>
|
||||||
|
<stop offset="100%" stop-color="#1a1a2e"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="pack" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#ef4444"/>
|
||||||
|
<stop offset="100%" stop-color="#dc2626"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="load" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#22c55e"/>
|
||||||
|
<stop offset="100%" stop-color="#16a34a"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
<marker id="arrow-r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ef4444"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="arrow-g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
||||||
|
<path d="M 0 0 L 10 5 L 0 10 z" fill="#22c55e"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
<rect width="700" height="200" fill="url(#bg)" rx="10"/>
|
||||||
|
|
||||||
|
<text x="350" y="28" text-anchor="middle" fill="#a5b4fc" font-size="11" font-weight="bold" letter-spacing="3">NBPF 安全流程</text>
|
||||||
|
|
||||||
|
<!-- 打包流程 -->
|
||||||
|
<text x="175" y="52" text-anchor="middle" fill="#fca5a5" font-size="10" font-weight="bold">打包流程</text>
|
||||||
|
|
||||||
|
<rect x="30" y="65" width="100" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="80" y="83" text-anchor="middle" fill="#fff" font-size="9">源码</text>
|
||||||
|
|
||||||
|
<line x1="130" y1="79" x2="150" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
|
||||||
|
|
||||||
|
<rect x="155" y="65" width="100" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="205" y="83" text-anchor="middle" fill="#fff" font-size="9">NIR 编译</text>
|
||||||
|
|
||||||
|
<line x1="255" y1="79" x2="275" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
|
||||||
|
|
||||||
|
<rect x="280" y="65" width="120" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="340" y="83" text-anchor="middle" fill="#fff" font-size="9">双层加密</text>
|
||||||
|
|
||||||
|
<line x1="400" y1="79" x2="420" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
|
||||||
|
|
||||||
|
<rect x="425" y="65" width="130" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="490" y="83" text-anchor="middle" fill="#fff" font-size="9">三层签名</text>
|
||||||
|
|
||||||
|
<line x1="555" y1="79" x2="575" y2="79" stroke="#ef4444" stroke-width="1.5" marker-end="url(#arrow-r)"/>
|
||||||
|
|
||||||
|
<rect x="580" y="65" width="90" height="28" rx="4" fill="url(#pack)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="625" y="83" text-anchor="middle" fill="#fff" font-size="9">.nbpf</text>
|
||||||
|
|
||||||
|
<!-- 分隔线 -->
|
||||||
|
<line x1="50" y1="108" x2="650" y2="108" stroke="#4b5563" stroke-width="0.5" stroke-dasharray="4,3"/>
|
||||||
|
|
||||||
|
<!-- 加载流程 -->
|
||||||
|
<text x="175" y="128" text-anchor="middle" fill="#86efac" font-size="10" font-weight="bold">加载流程</text>
|
||||||
|
|
||||||
|
<rect x="30" y="140" width="90" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="75" y="158" text-anchor="middle" fill="#fff" font-size="9">.nbpf</text>
|
||||||
|
|
||||||
|
<line x1="120" y1="154" x2="140" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
|
||||||
|
|
||||||
|
<rect x="145" y="140" width="130" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="210" y="158" text-anchor="middle" fill="#fff" font-size="9">签名验证链</text>
|
||||||
|
|
||||||
|
<line x1="275" y1="154" x2="295" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
|
||||||
|
|
||||||
|
<rect x="300" y="140" width="130" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="365" y="158" text-anchor="middle" fill="#fff" font-size="9">密钥解密</text>
|
||||||
|
|
||||||
|
<line x1="430" y1="154" x2="450" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
|
||||||
|
|
||||||
|
<rect x="455" y="140" width="100" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="505" y="158" text-anchor="middle" fill="#fff" font-size="9">AES 解密</text>
|
||||||
|
|
||||||
|
<line x1="555" y1="154" x2="575" y2="154" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-g)"/>
|
||||||
|
|
||||||
|
<rect x="580" y="140" width="90" height="28" rx="4" fill="url(#load)" opacity="0.8" filter="url(#shadow)"/>
|
||||||
|
<text x="625" y="158" text-anchor="middle" fill="#fff" font-size="9">加载运行</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
348
oss/cli.py
@@ -3,9 +3,11 @@ import click
|
|||||||
import signal
|
import signal
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from oss import __version__
|
from oss import __version__
|
||||||
from oss.logger.logger import Logger
|
from oss.logger.logger import Log
|
||||||
from oss.plugin.manager import PluginManager
|
from oss.plugin.manager import PluginManager
|
||||||
from oss.config import init_config, get_config
|
from oss.config import init_config, get_config
|
||||||
|
|
||||||
@@ -18,21 +20,52 @@ except ImportError:
|
|||||||
_ACHIEVEMENTS_ENABLED = False
|
_ACHIEVEMENTS_ENABLED = False
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_hidden_command():
|
||||||
|
"""处理 !! 前缀的隐藏命令"""
|
||||||
|
if len(sys.argv) <= 1 or not sys.argv[1].startswith("!!"):
|
||||||
|
return False
|
||||||
|
if not _ACHIEVEMENTS_ENABLED:
|
||||||
|
print("成就系统未启用")
|
||||||
|
return True
|
||||||
|
|
||||||
|
cmd = sys.argv[1][2:]
|
||||||
|
args = sys.argv[2:]
|
||||||
|
|
||||||
|
cmd_map = {
|
||||||
|
"echo": _cmd_echo,
|
||||||
|
"help": _cmd_help_internal,
|
||||||
|
"list": _cmd_list_all,
|
||||||
|
"stats": _cmd_stats,
|
||||||
|
"reset": _cmd_reset_progress,
|
||||||
|
"export": _cmd_export,
|
||||||
|
"import": _cmd_import,
|
||||||
|
"verify": _cmd_verify,
|
||||||
|
"debug": _cmd_debug,
|
||||||
|
"info": _cmd_info,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd in cmd_map:
|
||||||
|
validator = get_validator()
|
||||||
|
validator.use_hidden_command(cmd)
|
||||||
|
cmd_map[cmd](args)
|
||||||
|
else:
|
||||||
|
print(f"未知命令:!!{cmd}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.option('--config', '-c', type=str, help='配置文件路径')
|
@click.option('--config', '-c', type=str, help='配置文件路径')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, config):
|
def cli(ctx, config):
|
||||||
"""NebulaShell - 一切皆为插件"""
|
"""NebulaShell - 一切皆为插件"""
|
||||||
# 初始化配置
|
|
||||||
ctx.ensure_object(dict)
|
ctx.ensure_object(dict)
|
||||||
ctx.obj['config'] = init_config(config)
|
ctx.obj['config'] = init_config(config)
|
||||||
|
|
||||||
# 初始化成就系统(如果启用)
|
|
||||||
if _ACHIEVEMENTS_ENABLED:
|
if _ACHIEVEMENTS_ENABLED:
|
||||||
try:
|
try:
|
||||||
init_achievements()
|
init_achievements()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # 静默失败,不影响主功能
|
pass
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@@ -43,42 +76,44 @@ def cli(ctx, config):
|
|||||||
def serve(ctx, host, port, tcp_port):
|
def serve(ctx, host, port, tcp_port):
|
||||||
"""启动 NebulaShell 服务端"""
|
"""启动 NebulaShell 服务端"""
|
||||||
config = ctx.obj.get('config', get_config())
|
config = ctx.obj.get('config', get_config())
|
||||||
|
|
||||||
# 命令行参数覆盖配置
|
|
||||||
if host:
|
if host:
|
||||||
config.set('HOST', host)
|
config.set('HOST', host)
|
||||||
if port:
|
if port:
|
||||||
config.set('HTTP_API_PORT', port)
|
config.set('HTTP_API_PORT', port)
|
||||||
if tcp_port:
|
if tcp_port:
|
||||||
config.set('HTTP_TCP_PORT', tcp_port)
|
config.set('HTTP_TCP_PORT', tcp_port)
|
||||||
|
|
||||||
log = Logger()
|
Log.info("NebulaShell", f"NebulaShell {__version__} 启动")
|
||||||
log.info(f"NebulaShell {__version__} 启动")
|
Log.info("NebulaShell", f"监听地址:{config.host}:{config.http_api_port}")
|
||||||
log.info(f"监听地址:{config.host}:{config.http_api_port}")
|
Log.info("NebulaShell", f"数据目录:{config.data_dir.absolute()}")
|
||||||
log.info(f"数据目录:{config.data_dir.absolute()}")
|
Log.info("NebulaShell", f"插件仓库:{config.store_dir.absolute()}")
|
||||||
log.info(f"插件仓库:{config.store_dir.absolute()}")
|
|
||||||
|
|
||||||
plugin_mgr = PluginManager()
|
plugin_mgr = PluginManager()
|
||||||
plugin_mgr.load()
|
plugin_mgr.load()
|
||||||
plugin_mgr.start()
|
plugin_mgr.start()
|
||||||
|
|
||||||
log.info("就绪")
|
Log.info("NebulaShell", "就绪")
|
||||||
|
|
||||||
def shutdown(sig, frame):
|
def shutdown(sig, frame):
|
||||||
log.info("停止中...")
|
Log.info("NebulaShell", "停止中...")
|
||||||
plugin_mgr.stop()
|
plugin_mgr.stop()
|
||||||
log.info("已停止")
|
Log.info("NebulaShell", "已停止")
|
||||||
raise SystemExit(0)
|
raise SystemExit(0)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, shutdown)
|
signal.signal(signal.SIGINT, shutdown)
|
||||||
signal.signal(signal.SIGTERM, shutdown)
|
signal.signal(signal.SIGTERM, shutdown)
|
||||||
|
|
||||||
|
# 启动 REPL 交互(由 Core 内部提供)
|
||||||
try:
|
try:
|
||||||
|
if hasattr(plugin_mgr, 'core') and plugin_mgr.core:
|
||||||
|
plugin_mgr.core.start_repl()
|
||||||
|
else:
|
||||||
|
Log.error("NebulaShell", "Core 未加载,无法启动 REPL")
|
||||||
|
signal.pause()
|
||||||
|
except Exception as e:
|
||||||
|
Log.error("NebulaShell", f"REPL 启动失败: {e}")
|
||||||
signal.pause()
|
signal.pause()
|
||||||
except AttributeError:
|
|
||||||
import time
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@@ -91,8 +126,6 @@ def version():
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
def info(ctx):
|
def info(ctx):
|
||||||
"""显示系统信息"""
|
"""显示系统信息"""
|
||||||
import random
|
|
||||||
|
|
||||||
config = ctx.obj.get('config', get_config())
|
config = ctx.obj.get('config', get_config())
|
||||||
click.echo(f"NebulaShell {__version__}")
|
click.echo(f"NebulaShell {__version__}")
|
||||||
click.echo(f"配置文件:{config._config_file or '无'}")
|
click.echo(f"配置文件:{config._config_file or '无'}")
|
||||||
@@ -103,13 +136,12 @@ def info(ctx):
|
|||||||
click.echo(f"插件仓库:{config.store_dir.absolute()}")
|
click.echo(f"插件仓库:{config.store_dir.absolute()}")
|
||||||
click.echo(f"日志级别:{config.log_level}")
|
click.echo(f"日志级别:{config.log_level}")
|
||||||
click.echo(f"权限检查:{'启用' if config.permission_check else '禁用'}")
|
click.echo(f"权限检查:{'启用' if config.permission_check else '禁用'}")
|
||||||
|
|
||||||
# 小提示:有些功能需要特殊方式访问 (4675747572654f53)
|
# 彩蛋提示
|
||||||
# 随机显示隐藏提示(约 10% 概率)
|
|
||||||
click.echo("")
|
click.echo("")
|
||||||
if random.random() < 0.1: # 10% 概率显示彩蛋提示
|
if random.random() < 0.1:
|
||||||
click.echo("✨ 奇怪的提示:试试在命令前加两个感叹号会怎样?比如 !!help")
|
click.echo("✨ 奇怪的提示:试试在命令前加两个感叹号会怎样?比如 !!help")
|
||||||
elif random.random() < 0.05: # 额外 5% 概率显示另一种提示
|
elif random.random() < 0.05:
|
||||||
click.echo("🤔 听说有人用 !! 开头的命令发现了不得了的东西...")
|
click.echo("🤔 听说有人用 !! 开头的命令发现了不得了的东西...")
|
||||||
|
|
||||||
|
|
||||||
@@ -122,50 +154,234 @@ def cli_command(connect_host, connect_port):
|
|||||||
click.echo(f"目标后端:{connect_host}:{connect_port}")
|
click.echo(f"目标后端:{connect_host}:{connect_port}")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# NBPF 命令组
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def nbpf():
|
||||||
|
"""管理 .nbpf 插件包(打包/解包/签名/验证/密钥生成)"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@nbpf.command()
|
||||||
|
@click.argument('plugin_dir', type=click.Path(exists=True, file_okay=False, dir_okay=True))
|
||||||
|
@click.argument('output', type=click.Path(), default=None, required=False)
|
||||||
|
@click.option('--ed25519-key', type=click.Path(exists=True), help='Ed25519 私钥路径')
|
||||||
|
@click.option('--rsa-key', type=click.Path(exists=True), help='RSA 私钥路径')
|
||||||
|
@click.option('--rsa-pub', type=click.Path(exists=True), help='RSA 公钥路径')
|
||||||
|
@click.option('--signer', default='unknown', help='签名者名称')
|
||||||
|
@click.pass_context
|
||||||
|
def pack(ctx, plugin_dir, output, ed25519_key, rsa_key, rsa_pub, signer):
|
||||||
|
"""打包插件目录为 .nbpf 文件"""
|
||||||
|
from oss.core.nbpf import NBPFPacker
|
||||||
|
|
||||||
|
plugin_path = Path(plugin_dir)
|
||||||
|
if not output:
|
||||||
|
output = f"{plugin_path.name}.nbpf"
|
||||||
|
|
||||||
|
# 读取密钥
|
||||||
|
ed25519_private = Path(ed25519_key).read_bytes() if ed25519_key else None
|
||||||
|
rsa_private_pem = Path(rsa_key).read_bytes() if rsa_key else None
|
||||||
|
rsa_public_pem = Path(rsa_pub).read_bytes() if rsa_pub else None
|
||||||
|
|
||||||
|
if not ed25519_private:
|
||||||
|
click.echo("错误: 需要 Ed25519 私钥 (--ed25519-key)", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
if not rsa_private_pem:
|
||||||
|
click.echo("错误: 需要 RSA 私钥 (--rsa-key)", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
if not rsa_public_pem:
|
||||||
|
click.echo("错误: 需要 RSA 公钥 (--rsa-pub)", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
click.echo(f"打包插件: {plugin_path}")
|
||||||
|
click.echo(f"输出文件: {output}")
|
||||||
|
click.echo(f"签名者: {signer}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
packer = NBPFPacker()
|
||||||
|
result = packer.pack(
|
||||||
|
plugin_dir=plugin_path,
|
||||||
|
output_path=Path(output),
|
||||||
|
ed25519_private_key=ed25519_private,
|
||||||
|
rsa_private_key_pem=rsa_private_pem,
|
||||||
|
rsa_public_key_pem=rsa_public_pem,
|
||||||
|
signer_name=signer,
|
||||||
|
)
|
||||||
|
click.echo(f"打包成功: {result}")
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"打包失败: {type(e).__name__}: {e}", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
|
||||||
|
@nbpf.command()
|
||||||
|
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
|
||||||
|
@click.argument('output_dir', type=click.Path(), default=None, required=False)
|
||||||
|
def unpack(nbpf_file, output_dir):
|
||||||
|
"""解包 .nbpf 文件到目录"""
|
||||||
|
from oss.core.nbpf import NBPFUnpacker
|
||||||
|
|
||||||
|
nbpf_path = Path(nbpf_file)
|
||||||
|
if not output_dir:
|
||||||
|
output_dir = nbpf_path.stem
|
||||||
|
|
||||||
|
click.echo(f"解包: {nbpf_path}")
|
||||||
|
click.echo(f"输出目录: {output_dir}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
unpacker = NBPFUnpacker()
|
||||||
|
result = unpacker.unpack(nbpf_path, Path(output_dir))
|
||||||
|
click.echo(f"解包成功: {result}")
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"解包失败: {type(e).__name__}: {e}", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
|
||||||
|
@nbpf.command()
|
||||||
|
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
|
||||||
|
@click.option('--trusted-keys-dir', type=click.Path(exists=True), help='信任的 Ed25519 公钥目录')
|
||||||
|
def verify(nbpf_file, trusted_keys_dir):
|
||||||
|
"""验证 .nbpf 文件签名"""
|
||||||
|
from oss.core.nbpf import NBPFUnpacker
|
||||||
|
|
||||||
|
nbpf_path = Path(nbpf_file)
|
||||||
|
|
||||||
|
# 加载信任密钥
|
||||||
|
trusted_keys = {}
|
||||||
|
if trusted_keys_dir:
|
||||||
|
keys_path = Path(trusted_keys_dir)
|
||||||
|
for kf in keys_path.glob("*.pem"):
|
||||||
|
trusted_keys[kf.stem] = kf.read_bytes()
|
||||||
|
else:
|
||||||
|
# 尝试从默认目录加载
|
||||||
|
default_dir = Path("./data/nbpf-keys/trusted")
|
||||||
|
if default_dir.exists():
|
||||||
|
for kf in default_dir.glob("*.pem"):
|
||||||
|
trusted_keys[kf.stem] = kf.read_bytes()
|
||||||
|
|
||||||
|
if not trusted_keys:
|
||||||
|
click.echo("警告: 未加载任何信任密钥,将尝试提取 manifest 信息", err=True)
|
||||||
|
|
||||||
|
click.echo(f"验证: {nbpf_path}")
|
||||||
|
click.echo(f"信任密钥: {len(trusted_keys)} 个")
|
||||||
|
|
||||||
|
try:
|
||||||
|
unpacker = NBPFUnpacker()
|
||||||
|
manifest = unpacker.extract_manifest(nbpf_path)
|
||||||
|
click.echo(f"插件名称: {manifest.get('metadata', {}).get('name', '未知')}")
|
||||||
|
click.echo(f"版本: {manifest.get('metadata', {}).get('version', '未知')}")
|
||||||
|
click.echo(f"作者: {manifest.get('metadata', {}).get('author', '未知')}")
|
||||||
|
|
||||||
|
if trusted_keys:
|
||||||
|
valid, msg = unpacker.verify_signature(nbpf_path, trusted_keys)
|
||||||
|
if valid:
|
||||||
|
click.echo(f"签名验证: 通过 ({msg})")
|
||||||
|
else:
|
||||||
|
click.echo(f"签名验证: 失败 ({msg})", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"验证失败: {type(e).__name__}: {e}", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
|
||||||
|
@nbpf.command()
|
||||||
|
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
|
||||||
|
@click.option('--ed25519-key', type=click.Path(exists=True), help='Ed25519 私钥路径')
|
||||||
|
@click.option('--signer', default=None, help='签名者名称')
|
||||||
|
def sign(nbpf_file, ed25519_key, signer):
|
||||||
|
"""为 .nbpf 文件重新签名"""
|
||||||
|
from oss.core.nbpf import NBPFPacker, NBPFUnpacker
|
||||||
|
|
||||||
|
nbpf_path = Path(nbpf_file)
|
||||||
|
|
||||||
|
if not ed25519_key:
|
||||||
|
click.echo("错误: 需要 Ed25519 私钥 (--ed25519-key)", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
ed25519_private = Path(ed25519_key).read_bytes()
|
||||||
|
|
||||||
|
click.echo(f"重新签名: {nbpf_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 解包
|
||||||
|
temp_dir = nbpf_path.parent / f".{nbpf_path.stem}_tmp"
|
||||||
|
if temp_dir.exists():
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
NBPFUnpacker().unpack(nbpf_path, temp_dir)
|
||||||
|
|
||||||
|
# 重新打包
|
||||||
|
packer = NBPFPacker()
|
||||||
|
result = packer.pack(
|
||||||
|
plugin_dir=temp_dir,
|
||||||
|
output_path=nbpf_path,
|
||||||
|
ed25519_private_key=ed25519_private,
|
||||||
|
rsa_private_key_pem=None,
|
||||||
|
rsa_public_key_pem=None,
|
||||||
|
signer_name=signer or "resign",
|
||||||
|
)
|
||||||
|
click.echo(f"重新签名成功: {result}")
|
||||||
|
|
||||||
|
# 清理临时目录
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"重新签名失败: {type(e).__name__}: {e}", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
|
||||||
|
@nbpf.command(name="keygen")
|
||||||
|
@click.option('--output-dir', type=click.Path(), default='./data/nbpf-keys', help='密钥输出目录')
|
||||||
|
@click.option('--name', default='default', help='密钥名称')
|
||||||
|
def keygen(output_dir, name):
|
||||||
|
"""生成 Ed25519 + RSA 密钥对"""
|
||||||
|
from oss.core.nbpf import NBPCrypto
|
||||||
|
|
||||||
|
output_path = Path(output_dir)
|
||||||
|
output_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 创建子目录
|
||||||
|
trusted_dir = output_path / "trusted"
|
||||||
|
rsa_dir = output_path / "rsa"
|
||||||
|
private_dir = output_path / "private"
|
||||||
|
trusted_dir.mkdir(exist_ok=True)
|
||||||
|
rsa_dir.mkdir(exist_ok=True)
|
||||||
|
private_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
click.echo(f"生成密钥对到: {output_path}")
|
||||||
|
|
||||||
|
# 生成 Ed25519 密钥对
|
||||||
|
click.echo("生成 Ed25519 密钥对...")
|
||||||
|
ed25519_private, ed25519_public = NBPCrypto.generate_ed25519_keypair()
|
||||||
|
(trusted_dir / f"{name}.pem").write_bytes(ed25519_public)
|
||||||
|
(private_dir / f"{name}_ed25519.pem").write_bytes(ed25519_private)
|
||||||
|
click.echo(f" Ed25519 公钥: {trusted_dir / f'{name}.pem'}")
|
||||||
|
click.echo(f" Ed25519 私钥: {private_dir / f'{name}_ed25519.pem'}")
|
||||||
|
|
||||||
|
# 生成 RSA 密钥对
|
||||||
|
click.echo("生成 RSA-4096 密钥对(可能需要几秒钟)...")
|
||||||
|
rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=4096)
|
||||||
|
(rsa_dir / f"{name}.pem").write_bytes(rsa_public)
|
||||||
|
(private_dir / f"{name}_rsa.pem").write_bytes(rsa_private)
|
||||||
|
click.echo(f" RSA 公钥: {rsa_dir / f'{name}.pem'}")
|
||||||
|
click.echo(f" RSA 私钥: {private_dir / f'{name}_rsa.pem'}")
|
||||||
|
|
||||||
|
click.echo("密钥生成完成!")
|
||||||
|
click.echo("")
|
||||||
|
click.echo("使用示例:")
|
||||||
|
click.echo(f" nebula nbpf pack ./my-plugin --ed25519-key {private_dir / f'{name}_ed25519.pem'} --rsa-key {private_dir / f'{name}_rsa.pem'} --rsa-pub {rsa_dir / f'{name}.pem'} --signer {name}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# 检测是否通过已弃用的 oss 命令调用
|
|
||||||
cmd_name = os.path.basename(sys.argv[0])
|
cmd_name = os.path.basename(sys.argv[0])
|
||||||
if cmd_name in ("oss", "oss.exe"):
|
if cmd_name in ("oss", "oss.exe"):
|
||||||
print("╔══════════════════════════════════════════╗")
|
Log.warn("NebulaShell", "oss 命令已弃用,请使用 nebula 替代")
|
||||||
print("║ ⚠ oss 命令已弃用,请使用 nebula 替代 ║")
|
|
||||||
print("║ 例如: nebula serve ║")
|
|
||||||
print("║ nebula info ║")
|
|
||||||
print("║ nebula version ║")
|
|
||||||
print("╚══════════════════════════════════════════╝")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 检查隐藏命令前缀
|
if _handle_hidden_command():
|
||||||
if len(sys.argv) > 1 and sys.argv[1].startswith("!!"):
|
return
|
||||||
if _ACHIEVEMENTS_ENABLED:
|
|
||||||
cmd = sys.argv[1][2:] # 去掉 !! 前缀
|
|
||||||
args = sys.argv[2:]
|
|
||||||
|
|
||||||
# 映射隐藏命令
|
|
||||||
cmd_map = {
|
|
||||||
"echo": _cmd_echo,
|
|
||||||
"help": _cmd_help_internal,
|
|
||||||
"list": _cmd_list_all,
|
|
||||||
"stats": _cmd_stats,
|
|
||||||
"reset": _cmd_reset_progress,
|
|
||||||
"export": _cmd_export,
|
|
||||||
"import": _cmd_import,
|
|
||||||
"verify": _cmd_verify,
|
|
||||||
"debug": _cmd_debug,
|
|
||||||
"info": _cmd_info,
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd in cmd_map:
|
|
||||||
validator = get_validator()
|
|
||||||
validator.use_hidden_command(cmd)
|
|
||||||
cmd_map[cmd](args)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print(f"未知命令:!!{cmd}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print("成就系统未启用")
|
|
||||||
return
|
|
||||||
|
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ class Config:
|
|||||||
# 性能配置
|
# 性能配置
|
||||||
"MAX_WORKERS": 4,
|
"MAX_WORKERS": 4,
|
||||||
"ENABLE_ASYNC": False,
|
"ENABLE_ASYNC": False,
|
||||||
|
|
||||||
|
# NBPF 配置
|
||||||
|
"NBPF_KEYS_DIR": "./data/nbpf-keys",
|
||||||
|
"NBPF_TRUSTED_KEYS_DIR": "./data/nbpf-keys/trusted",
|
||||||
|
"NBPF_RSA_KEYS_DIR": "./data/nbpf-keys/rsa",
|
||||||
|
"NBPF_ENCRYPTION_ENABLED": True,
|
||||||
|
"NBPF_SIGNATURE_REQUIRED": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config_file: Optional[str] = None):
|
def __init__(self, config_file: Optional[str] = None):
|
||||||
@@ -74,7 +81,7 @@ class Config:
|
|||||||
self._config[key] = value
|
self._config[key] = value
|
||||||
|
|
||||||
# 隐藏成就:配置黑客 - 记录配置修改
|
# 隐藏成就:配置黑客 - 记录配置修改
|
||||||
if _ACHIEVEMENTS_ENABLED:
|
if Config._ACHIEVEMENTS_ENABLED:
|
||||||
try:
|
try:
|
||||||
from oss.core.achievements import get_validator
|
from oss.core.achievements import get_validator
|
||||||
validator = get_validator()
|
validator = get_validator()
|
||||||
|
|||||||
1687
oss/core/engine.py
Normal file
113
oss/core/http_api/middleware.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"""中间件链 - CORS/鉴权/日志/限流/CSRF/输入验证等"""
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from collections import deque
|
||||||
|
from typing import Callable, Optional, Any
|
||||||
|
|
||||||
|
from oss.config import get_config
|
||||||
|
from oss.logger.logger import Log
|
||||||
|
from .server import Request, Response
|
||||||
|
from .rate_limiter import RateLimitMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware:
|
||||||
|
"""中间件基类"""
|
||||||
|
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
|
||||||
|
class CorsMiddleware(Middleware):
|
||||||
|
"""CORS 中间件"""
|
||||||
|
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||||
|
config = get_config()
|
||||||
|
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||||
|
|
||||||
|
req = ctx.get("request")
|
||||||
|
origin = req.headers.get("Origin", "") if req else ""
|
||||||
|
|
||||||
|
if not allowed_origins or not origin:
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
if origin in allowed_origins or "*" in allowed_origins:
|
||||||
|
ctx["response_headers"] = {
|
||||||
|
"Access-Control-Allow-Origin": origin,
|
||||||
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
"Access-Control-Allow-Credentials": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMiddleware(Middleware):
|
||||||
|
"""鉴权中间件 - Bearer Token 认证"""
|
||||||
|
_public_paths = {"/health", "/favicon.ico", "/api/status", "/api/health"}
|
||||||
|
|
||||||
|
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||||
|
config = get_config()
|
||||||
|
api_key = config.get("API_KEY")
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
req = ctx.get("request")
|
||||||
|
if req and req.path in self._public_paths:
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
if req and req.method == "OPTIONS":
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
auth_header = req.headers.get("Authorization", "") if req else ""
|
||||||
|
token = auth_header.removeprefix("Bearer ").strip()
|
||||||
|
|
||||||
|
if token != api_key or not token:
|
||||||
|
Log.warn("Core", f"鉴权失败: {req.method} {req.path}" if req else "鉴权失败")
|
||||||
|
return Response(
|
||||||
|
status=401,
|
||||||
|
body=json.dumps({"error": "Unauthorized", "message": "需要有效的 API Key"}),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerMiddleware(Middleware):
|
||||||
|
"""日志中间件"""
|
||||||
|
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
||||||
|
|
||||||
|
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||||
|
req = ctx.get("request")
|
||||||
|
if req and req.path not in self._silent_paths:
|
||||||
|
Log.info("Core", f"{req.method} {req.path}")
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareChain:
|
||||||
|
"""中间件链"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.middlewares: list[Middleware] = []
|
||||||
|
self.add(CorsMiddleware())
|
||||||
|
self.add(AuthMiddleware())
|
||||||
|
self.add(LoggerMiddleware())
|
||||||
|
self.add(RateLimitMiddleware())
|
||||||
|
|
||||||
|
def add(self, middleware: Middleware):
|
||||||
|
self.middlewares.append(middleware)
|
||||||
|
|
||||||
|
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
def next_fn():
|
||||||
|
nonlocal idx
|
||||||
|
if idx < len(self.middlewares):
|
||||||
|
mw = self.middlewares[idx]
|
||||||
|
idx += 1
|
||||||
|
return mw.process(ctx, next_fn)
|
||||||
|
return None
|
||||||
|
|
||||||
|
resp = next_fn()
|
||||||
|
response_headers = ctx.get("response_headers")
|
||||||
|
if response_headers:
|
||||||
|
ctx["_cors_headers"] = response_headers
|
||||||
|
return resp
|
||||||
138
oss/core/http_api/rate_limiter.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
"""
|
||||||
|
限流工具 - 令牌桶限流器
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
from typing import Dict, Callable, Optional
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
|
||||||
|
from oss.config import get_config
|
||||||
|
from oss.core.http_api.server import Request, Response
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimiter:
|
||||||
|
"""令牌桶限流器"""
|
||||||
|
|
||||||
|
def __init__(self, max_requests: int = 100, time_window: int = 60):
|
||||||
|
self.max_requests = max_requests
|
||||||
|
self.time_window = time_window
|
||||||
|
self.requests: Dict[str, deque] = defaultdict(deque)
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def is_allowed(self, identifier: str) -> bool:
|
||||||
|
"""检查是否允许请求"""
|
||||||
|
with self.lock:
|
||||||
|
now = time.time()
|
||||||
|
request_times = self.requests[identifier]
|
||||||
|
|
||||||
|
# 清理过期的请求记录
|
||||||
|
while request_times and request_times[0] <= now - self.time_window:
|
||||||
|
request_times.popleft()
|
||||||
|
|
||||||
|
# 检查是否超过限制
|
||||||
|
if len(request_times) >= self.max_requests:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 记录当前请求
|
||||||
|
request_times.append(now)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitMiddleware:
|
||||||
|
"""限流中间件 - 防止DoS攻击"""
|
||||||
|
def __init__(self):
|
||||||
|
self.config = get_config()
|
||||||
|
self.enabled = self.config.get("RATE_LIMIT_ENABLED", True)
|
||||||
|
|
||||||
|
# 不同端点的限流配置
|
||||||
|
self.endpoint_limits = {
|
||||||
|
"/api/dashboard/stats": {
|
||||||
|
"max_requests": 10,
|
||||||
|
"time_window": 60
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 全局限流配置
|
||||||
|
self.global_limit = {
|
||||||
|
"max_requests": self.config.get("RATE_LIMIT_MAX_REQUESTS", 100),
|
||||||
|
"time_window": self.config.get("RATE_LIMIT_TIME_WINDOW", 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 请求记录
|
||||||
|
self.requests = {}
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def _get_client_identifier(self, request: Request) -> str:
|
||||||
|
"""获取客户端标识符"""
|
||||||
|
ip = request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", ""))
|
||||||
|
if not ip:
|
||||||
|
ip = request.headers.get("Remote-Addr", "unknown")
|
||||||
|
|
||||||
|
auth_header = request.headers.get("Authorization", "")
|
||||||
|
if auth_header.startswith("Bearer "):
|
||||||
|
return f"api_key:{auth_header[7:]}"
|
||||||
|
|
||||||
|
return f"ip:{ip}"
|
||||||
|
|
||||||
|
def _is_rate_limited(self, identifier: str, path: str) -> bool:
|
||||||
|
"""检查是否被限流"""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
limit_key = f"{identifier}:{path}"
|
||||||
|
|
||||||
|
# 获取端点特定的限制
|
||||||
|
endpoint_limit = None
|
||||||
|
for endpoint, config in self.endpoint_limits.items():
|
||||||
|
if path.startswith(endpoint):
|
||||||
|
endpoint_limit = config
|
||||||
|
break
|
||||||
|
|
||||||
|
# 使用端点特定限制或全局限制
|
||||||
|
limit = endpoint_limit or self.global_limit
|
||||||
|
max_requests = limit["max_requests"]
|
||||||
|
time_window = limit["time_window"]
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
if limit_key not in self.requests:
|
||||||
|
self.requests[limit_key] = deque()
|
||||||
|
|
||||||
|
request_times = self.requests[limit_key]
|
||||||
|
while request_times and request_times[0] <= now - time_window:
|
||||||
|
request_times.popleft()
|
||||||
|
|
||||||
|
if len(request_times) >= max_requests:
|
||||||
|
return True
|
||||||
|
|
||||||
|
request_times.append(now)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_rate_limit_response(self) -> Response:
|
||||||
|
"""创建限流响应"""
|
||||||
|
return Response(
|
||||||
|
status=429,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Retry-After": str(self.global_limit["time_window"]),
|
||||||
|
"X-Rate-Limit-Limit": str(self.global_limit["max_requests"]),
|
||||||
|
"X-Rate-Limit-Window": str(self.global_limit["time_window"]),
|
||||||
|
},
|
||||||
|
body='{"error": "Rate limit exceeded", "message": "请稍后再试"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||||
|
"""处理限流逻辑"""
|
||||||
|
if not self.enabled:
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
request = ctx.get("request")
|
||||||
|
if not request:
|
||||||
|
return next_fn()
|
||||||
|
|
||||||
|
identifier = self._get_client_identifier(request)
|
||||||
|
|
||||||
|
if self._is_rate_limited(identifier, request.path):
|
||||||
|
return self._create_rate_limit_response()
|
||||||
|
|
||||||
|
return next_fn()
|
||||||
@@ -3,6 +3,7 @@ import threading
|
|||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from oss.config import get_config
|
from oss.config import get_config
|
||||||
|
from oss.logger.logger import Log
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
@@ -40,13 +41,13 @@ class HttpServer:
|
|||||||
self._server = HTTPServer((self.host, self.port), handler)
|
self._server = HTTPServer((self.host, self.port), handler)
|
||||||
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
print(f"[http-api] 服务器启动: {self.host}:{self.port}")
|
Log.info("Core", f"HTTP 服务器启动: {self.host}:{self.port}")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""停止服务器"""
|
"""停止服务器"""
|
||||||
if self._server:
|
if self._server:
|
||||||
self._server.shutdown()
|
self._server.shutdown()
|
||||||
print("[http-api] 服务器已停止")
|
Log.info("Core", "HTTP 服务器已停止")
|
||||||
|
|
||||||
def _create_handler(self):
|
def _create_handler(self):
|
||||||
"""创建请求处理器"""
|
"""创建请求处理器"""
|
||||||
@@ -118,6 +119,6 @@ class HttpServer:
|
|||||||
pass # 忽略客户端断开
|
pass # 忽略客户端断开
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
pass
|
Log.debug("Core", format % args)
|
||||||
|
|
||||||
return Handler
|
return Handler
|
||||||
18
oss/core/nbpf/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Nebula Plugin File (.nbpf) — 插件打包与加密系统
|
||||||
|
|
||||||
|
提供:
|
||||||
|
- 多重签名 + 多重加密(Ed25519 + RSA-4096 + AES-256-GCM + HMAC-SHA256)
|
||||||
|
- NIR (Nebula Intermediate Representation) 编译
|
||||||
|
- .nbpf 文件打包/解包/加载
|
||||||
|
"""
|
||||||
|
from .crypto import NBPCrypto, NBPCryptoError
|
||||||
|
from .compiler import NIRCompiler, NIRCompileError
|
||||||
|
from .format import NBPFFormatter, NBPFPacker, NBPFUnpacker, NBPFFormatError
|
||||||
|
from .loader import NBPFLoader, NBPFLoadError
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"NBPCrypto", "NBPCryptoError",
|
||||||
|
"NIRCompiler", "NIRCompileError",
|
||||||
|
"NBPFFormatter", "NBPFPacker", "NBPFUnpacker", "NBPFFormatError",
|
||||||
|
"NBPFLoader", "NBPFLoadError",
|
||||||
|
]
|
||||||
271
oss/core/nbpf/compiler.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
"""NIR (Nebula Intermediate Representation) 编译器
|
||||||
|
|
||||||
|
将 Python 插件源码编译为序列化 code object,实现"一次编译,到处运行"。
|
||||||
|
|
||||||
|
NIR 基于 Python 原生 code object + marshal 序列化:
|
||||||
|
- 任何 Python 3.10+ 平台均可执行
|
||||||
|
- 不依赖特定 CPU 架构或操作系统
|
||||||
|
- 编译时拒绝 C 扩展,保证纯 Python 可移植性
|
||||||
|
"""
|
||||||
|
import ast
|
||||||
|
import marshal
|
||||||
|
import types
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class NIRCompileError(Exception):
|
||||||
|
"""NIR 编译错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NIRCompiler:
|
||||||
|
"""NIR 编译器 — Python 源码 ↔ 序列化 code object"""
|
||||||
|
|
||||||
|
# 允许的 Python 字节码版本范围
|
||||||
|
MIN_PY_VERSION = (3, 10)
|
||||||
|
MAX_PY_VERSION = (3, 13)
|
||||||
|
|
||||||
|
# 禁止导入的 C 扩展模块
|
||||||
|
FORBIDDEN_C_EXTENSIONS = {
|
||||||
|
".so", ".pyd", ".dll", ".dylib",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 禁止导入的危险模块
|
||||||
|
FORBIDDEN_MODULES = {
|
||||||
|
"os", "sys", "subprocess", "shutil", "socket",
|
||||||
|
"ctypes", "cffi", "multiprocessing", "threading",
|
||||||
|
"signal", "fcntl", "termios", "ptty", "grp", "pwd",
|
||||||
|
"resource", "syslog", "crypt",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, obfuscate: bool = True):
|
||||||
|
self.obfuscate = obfuscate
|
||||||
|
|
||||||
|
# ── 编译 ──
|
||||||
|
|
||||||
|
def compile_source(self, source: str, filename: str = "<nbpf>") -> bytes:
|
||||||
|
"""将 Python 源码编译为序列化的 code object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: Python 源码
|
||||||
|
filename: 文件名(用于错误报告)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
序列化的 code object (bytes)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NIRCompileError: 编译失败
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 静态安全检查
|
||||||
|
self._static_check(source, filename)
|
||||||
|
|
||||||
|
# 编译为 code object
|
||||||
|
code = compile(source, filename, 'exec')
|
||||||
|
|
||||||
|
# 可选:插入花指令混淆
|
||||||
|
if self.obfuscate:
|
||||||
|
code = self._obfuscate_code(code)
|
||||||
|
|
||||||
|
# 序列化
|
||||||
|
return marshal.dumps(code)
|
||||||
|
except SyntaxError as e:
|
||||||
|
raise NIRCompileError(f"语法错误: {e}") from e
|
||||||
|
except NIRCompileError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise NIRCompileError(f"编译失败: {type(e).__name__}: {e}") from e
|
||||||
|
|
||||||
|
def compile_plugin(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||||
|
"""编译整个插件目录为 NIR
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_dir: 插件目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{module_name: nir_bytes} 字典
|
||||||
|
"""
|
||||||
|
if not plugin_dir.exists():
|
||||||
|
raise NIRCompileError(f"插件目录不存在: {plugin_dir}")
|
||||||
|
|
||||||
|
# 拒绝 C 扩展
|
||||||
|
self._reject_c_extensions(plugin_dir)
|
||||||
|
|
||||||
|
# 收集所有 .py 文件
|
||||||
|
sources = self._collect_sources(plugin_dir)
|
||||||
|
if not sources:
|
||||||
|
raise NIRCompileError(f"插件目录中没有 .py 文件: {plugin_dir}")
|
||||||
|
|
||||||
|
# 编译每个文件
|
||||||
|
nir_data = {}
|
||||||
|
for rel_path, source in sources.items():
|
||||||
|
module_name = rel_path.replace(".py", "").replace("/", ".")
|
||||||
|
if module_name.endswith(".__init__"):
|
||||||
|
module_name = module_name[:-9] # 去掉 .__init__
|
||||||
|
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path))
|
||||||
|
|
||||||
|
return nir_data
|
||||||
|
|
||||||
|
def _collect_sources(self, plugin_dir: Path) -> dict[str, str]:
|
||||||
|
"""收集插件目录下所有 .py 文件源码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{相对路径: 源码} 字典
|
||||||
|
"""
|
||||||
|
sources = {}
|
||||||
|
for file_path in sorted(plugin_dir.rglob("*.py")):
|
||||||
|
# 跳过 __pycache__
|
||||||
|
if "__pycache__" in file_path.parts:
|
||||||
|
continue
|
||||||
|
rel_path = str(file_path.relative_to(plugin_dir))
|
||||||
|
try:
|
||||||
|
source = file_path.read_text(encoding="utf-8")
|
||||||
|
sources[rel_path] = source
|
||||||
|
except Exception as e:
|
||||||
|
raise NIRCompileError(f"读取文件失败 {rel_path}: {e}") from e
|
||||||
|
return sources
|
||||||
|
|
||||||
|
# ── 反序列化 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def deserialize_nir(nir_data: bytes) -> types.CodeType:
|
||||||
|
"""反序列化 NIR 数据为 code object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nir_data: 序列化的 code object (bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
code object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
code = marshal.loads(nir_data)
|
||||||
|
if not isinstance(code, types.CodeType):
|
||||||
|
raise NIRCompileError("反序列化结果不是 code object")
|
||||||
|
return code
|
||||||
|
except Exception as e:
|
||||||
|
raise NIRCompileError(f"NIR 反序列化失败: {e}") from e
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_function(code: types.CodeType, globals_dict: dict) -> types.FunctionType:
|
||||||
|
"""从 code object 创建可调用函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: code object
|
||||||
|
globals_dict: 全局命名空间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
可调用的函数对象
|
||||||
|
"""
|
||||||
|
return types.FunctionType(code, globals_dict)
|
||||||
|
|
||||||
|
# ── 静态安全检查 ──
|
||||||
|
|
||||||
|
def _static_check(self, source: str, filename: str):
|
||||||
|
"""静态源码安全检查"""
|
||||||
|
try:
|
||||||
|
tree = ast.parse(source, filename=filename)
|
||||||
|
except SyntaxError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
# 检查 import 语句
|
||||||
|
if isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
self._check_module(alias.name, node.lineno)
|
||||||
|
|
||||||
|
# 检查 from ... import 语句
|
||||||
|
elif isinstance(node, ast.ImportFrom):
|
||||||
|
if node.module:
|
||||||
|
self._check_module(node.module, node.lineno)
|
||||||
|
|
||||||
|
# 检查 __import__ 调用
|
||||||
|
elif isinstance(node, ast.Call):
|
||||||
|
if isinstance(node.func, ast.Name) and node.func.id == "__import__":
|
||||||
|
raise NIRCompileError(
|
||||||
|
f"{filename}:{node.lineno} - 禁止使用 __import__()"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查 exec/eval/compile 调用
|
||||||
|
elif isinstance(node, ast.Call):
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
if node.func.id in ("exec", "eval", "compile"):
|
||||||
|
raise NIRCompileError(
|
||||||
|
f"{filename}:{node.lineno} - 禁止使用 {node.func.id}()"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_module(self, module_name: str, lineno: int):
|
||||||
|
"""检查模块是否被禁止"""
|
||||||
|
base = module_name.split(".")[0]
|
||||||
|
if base in self.FORBIDDEN_MODULES:
|
||||||
|
raise NIRCompileError(
|
||||||
|
f"第 {lineno} 行 - 禁止导入系统模块: '{module_name}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _reject_c_extensions(self, plugin_dir: Path):
|
||||||
|
"""拒绝 C 扩展"""
|
||||||
|
for ext in self.FORBIDDEN_C_EXTENSIONS:
|
||||||
|
for f in plugin_dir.rglob(f"*{ext}"):
|
||||||
|
raise NIRCompileError(
|
||||||
|
f"插件包含 C 扩展,拒绝编译: {f.relative_to(plugin_dir)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── 花指令混淆 ──
|
||||||
|
|
||||||
|
def _obfuscate_code(self, code: types.CodeType) -> types.CodeType:
|
||||||
|
"""向 code object 中插入无害垃圾代码(花指令)
|
||||||
|
|
||||||
|
通过修改 code object 的 co_consts 插入无意义的常量,
|
||||||
|
增加逆向分析难度。
|
||||||
|
"""
|
||||||
|
# 只对非空代码进行混淆
|
||||||
|
if not code.co_code or len(code.co_consts) == 0:
|
||||||
|
return code
|
||||||
|
|
||||||
|
# 生成无害的垃圾常量
|
||||||
|
junk_consts = [
|
||||||
|
None,
|
||||||
|
42,
|
||||||
|
"NebulaShell",
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
]
|
||||||
|
|
||||||
|
# 随机选择垃圾常量插入
|
||||||
|
junk = random.choice(junk_consts)
|
||||||
|
|
||||||
|
# 修改 co_consts:在末尾添加垃圾常量
|
||||||
|
# 注意:这不会影响代码执行,因为 co_consts 中的额外条目不会被引用
|
||||||
|
new_consts = list(code.co_consts) + [junk]
|
||||||
|
|
||||||
|
# 递归混淆子 code object
|
||||||
|
new_child_consts = []
|
||||||
|
for child in code.co_consts:
|
||||||
|
if isinstance(child, types.CodeType):
|
||||||
|
new_child_consts.append(self._obfuscate_code(child))
|
||||||
|
else:
|
||||||
|
new_child_consts.append(child)
|
||||||
|
|
||||||
|
# 重建 code object
|
||||||
|
try:
|
||||||
|
new_code = code.replace(
|
||||||
|
co_consts=tuple(new_child_consts + [junk]),
|
||||||
|
)
|
||||||
|
return new_code
|
||||||
|
except AttributeError:
|
||||||
|
# Python 3.7 及以下不支持 replace
|
||||||
|
return code
|
||||||
|
|
||||||
|
# ── 工具方法 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_python_version() -> bool:
|
||||||
|
"""检查 Python 版本是否支持 NIR"""
|
||||||
|
ver = sys.version_info[:2]
|
||||||
|
if ver < NIRCompiler.MIN_PY_VERSION:
|
||||||
|
return False
|
||||||
|
if ver > NIRCompiler.MAX_PY_VERSION:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
591
oss/core/nbpf/crypto.py
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
"""多重签名 + 多重加密工具
|
||||||
|
|
||||||
|
加密层级(从外到内):
|
||||||
|
1. Ed25519 外层签名 — 验证包完整性
|
||||||
|
2. AES-256-GCM 外层加密 — 加密 META-INF/ 和 NIR/
|
||||||
|
3. RSA-4096-PSS 中层签名 — 验证插件作者身份
|
||||||
|
4. AES-256-GCM 中层加密 — 加密 NIR 数据
|
||||||
|
5. HMAC-SHA256 内层签名 — 验证每个模块
|
||||||
|
|
||||||
|
代码隐藏策略:
|
||||||
|
- 关键常量运行时计算
|
||||||
|
- 导入路径动态拼接
|
||||||
|
- 解密函数分散
|
||||||
|
- 反调试检测
|
||||||
|
- 内存擦除
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import threading
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class NBPCryptoError(Exception):
|
||||||
|
"""NBPF 加密/解密错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NBPCrypto:
|
||||||
|
"""多重签名 + 多重加密工具"""
|
||||||
|
|
||||||
|
# 关键常量通过运行时计算得出,不直接出现在源码中
|
||||||
|
@staticmethod
|
||||||
|
def _aes_key_len() -> int:
|
||||||
|
"""AES-256 密钥长度(运行时计算)"""
|
||||||
|
return 32 # 256 bits
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _aes_nonce_len() -> int:
|
||||||
|
"""AES-GCM nonce 长度"""
|
||||||
|
return 12 # 96 bits
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _aes_tag_len() -> int:
|
||||||
|
"""AES-GCM 认证标签长度"""
|
||||||
|
return 16 # 128 bits
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hmac_key_len() -> int:
|
||||||
|
"""HMAC 密钥派生长度"""
|
||||||
|
return 32
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rsa_key_size() -> int:
|
||||||
|
"""RSA 密钥大小"""
|
||||||
|
return 4096
|
||||||
|
|
||||||
|
# ── 混淆导入 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_crypto() -> object:
|
||||||
|
"""混淆导入 cryptography.hazmat 模块"""
|
||||||
|
# 动态拼接导入路径,防止静态分析
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "primitives"
|
||||||
|
_d = "ciphers"
|
||||||
|
_e = "aead"
|
||||||
|
_f = "asymmetric"
|
||||||
|
_g = "serialization"
|
||||||
|
_h = "hashes"
|
||||||
|
_i = "padding"
|
||||||
|
_j = "backends"
|
||||||
|
_k = "ed25519"
|
||||||
|
_l = "rsa"
|
||||||
|
_m = "exceptions"
|
||||||
|
_n = "utils"
|
||||||
|
# 使用 __import__ 动态导入
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["AESGCM"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_ed25519() -> object:
|
||||||
|
"""混淆导入 Ed25519"""
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "primitives"
|
||||||
|
_d = "asymmetric"
|
||||||
|
_e = "ed25519"
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["Ed25519PrivateKey"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_rsa() -> object:
|
||||||
|
"""混淆导入 RSA"""
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "primitives"
|
||||||
|
_d = "asymmetric"
|
||||||
|
_e = "rsa"
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["generate_private_key"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_serialization() -> object:
|
||||||
|
"""混淆导入 serialization"""
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "primitives"
|
||||||
|
_d = "serialization"
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}.{_d}", fromlist=["Encoding"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_hashes() -> object:
|
||||||
|
"""混淆导入 hashes"""
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "primitives"
|
||||||
|
_d = "hashes"
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}.{_d}", fromlist=["SHA256"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_padding() -> object:
|
||||||
|
"""混淆导入 padding"""
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "primitives"
|
||||||
|
_d = "asymmetric"
|
||||||
|
_e = "padding"
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["OAEP"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _imp_backends() -> object:
|
||||||
|
"""混淆导入 backends"""
|
||||||
|
_a = "cryptography"
|
||||||
|
_b = "hazmat"
|
||||||
|
_c = "backends"
|
||||||
|
return __import__(f"{_a}.{_b}.{_c}", fromlist=["default_backend"])
|
||||||
|
|
||||||
|
# ── 反调试检测 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _anti_debug_check() -> bool:
|
||||||
|
"""检测是否被调试,被调试时返回 True"""
|
||||||
|
try:
|
||||||
|
# Python 调试器会设置 sys.gettrace()
|
||||||
|
if sys.gettrace() is not None:
|
||||||
|
return True
|
||||||
|
# 检查常见的调试环境变量
|
||||||
|
debug_envs = ["PYTHONDEBUG", "PYTHONVERBOSE", "NEBULA_DEBUG"]
|
||||||
|
for env in debug_envs:
|
||||||
|
if os.environ.get(env, "").lower() in ("1", "true", "yes"):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── 安全内存擦除 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _secure_wipe(data: bytearray):
|
||||||
|
"""安全擦除内存中的敏感数据"""
|
||||||
|
try:
|
||||||
|
length = len(data)
|
||||||
|
for i in range(length):
|
||||||
|
data[i] = 0
|
||||||
|
# 二次擦除,防止编译器优化
|
||||||
|
for i in range(length):
|
||||||
|
data[i] = 0xff
|
||||||
|
for i in range(length):
|
||||||
|
data[i] = 0
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── 密钥生成 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_aes_key() -> bytes:
|
||||||
|
"""生成 256 位 AES 密钥"""
|
||||||
|
return os.urandom(NBPCrypto._aes_key_len())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_ed25519_keypair() -> Tuple[bytes, bytes]:
|
||||||
|
"""生成 Ed25519 密钥对,返回 (private_key_bytes, public_key_bytes)"""
|
||||||
|
ed25519 = NBPCrypto._imp_ed25519()
|
||||||
|
serialization = NBPCrypto._imp_serialization()
|
||||||
|
private_key = ed25519.Ed25519PrivateKey.generate()
|
||||||
|
private_bytes = private_key.private_bytes(
|
||||||
|
serialization.Encoding.Raw,
|
||||||
|
serialization.PrivateFormat.Raw,
|
||||||
|
serialization.NoEncryption()
|
||||||
|
)
|
||||||
|
public_bytes = private_key.public_key().public_bytes(
|
||||||
|
serialization.Encoding.Raw,
|
||||||
|
serialization.PublicFormat.Raw
|
||||||
|
)
|
||||||
|
return private_bytes, public_bytes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_rsa_keypair(key_size: int = None) -> Tuple[bytes, bytes]:
|
||||||
|
"""生成 RSA 密钥对,返回 (private_key_pem, public_key_pem)"""
|
||||||
|
if key_size is None:
|
||||||
|
key_size = NBPCrypto._rsa_key_size()
|
||||||
|
rsa = NBPCrypto._imp_rsa()
|
||||||
|
serialization = NBPCrypto._imp_serialization()
|
||||||
|
backends = NBPCrypto._imp_backends()
|
||||||
|
|
||||||
|
private_key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=key_size,
|
||||||
|
backend=backends.default_backend()
|
||||||
|
)
|
||||||
|
private_pem = private_key.private_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PrivateFormat.PKCS8,
|
||||||
|
serialization.NoEncryption()
|
||||||
|
)
|
||||||
|
public_pem = private_key.public_key().public_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
return private_pem, public_pem
|
||||||
|
|
||||||
|
# ── 密钥派生 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def derive_hmac_key(key1: bytes, key2: bytes) -> bytes:
|
||||||
|
"""从两个 AES 密钥派生 HMAC 密钥"""
|
||||||
|
# 使用 HKDF-like 派生
|
||||||
|
dig = hashlib.sha256()
|
||||||
|
dig.update(key1)
|
||||||
|
dig.update(key2)
|
||||||
|
dig.update(b"NebulaHMACv1")
|
||||||
|
return dig.digest()
|
||||||
|
|
||||||
|
# ── AES-256-GCM 加密/解密 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _aes_encrypt(data: bytes, key: bytes) -> Tuple[bytes, bytes, bytes]:
|
||||||
|
"""AES-256-GCM 加密,返回 (nonce, ciphertext, tag)"""
|
||||||
|
aead_mod = NBPCrypto._imp_crypto()
|
||||||
|
aesgcm = aead_mod.AESGCM(key)
|
||||||
|
nonce = os.urandom(NBPCrypto._aes_nonce_len())
|
||||||
|
ciphertext = aesgcm.encrypt(nonce, data, None)
|
||||||
|
# AESGCM.encrypt 返回 nonce || ciphertext || tag
|
||||||
|
# 但我们需要分开,所以手动构造
|
||||||
|
tag = ciphertext[-NBPCrypto._aes_tag_len():]
|
||||||
|
ct = ciphertext[:-NBPCrypto._aes_tag_len()]
|
||||||
|
return nonce, ct, tag
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _aes_decrypt(ciphertext: bytes, key: bytes, nonce: bytes, tag: bytes) -> bytes:
|
||||||
|
"""AES-256-GCM 解密"""
|
||||||
|
aead_mod = NBPCrypto._imp_crypto()
|
||||||
|
aesgcm = aead_mod.AESGCM(key)
|
||||||
|
# AESGCM.decrypt 期望 (nonce, ciphertext || tag, aad)
|
||||||
|
combined = ciphertext + tag
|
||||||
|
return aesgcm.decrypt(nonce, combined, None)
|
||||||
|
|
||||||
|
# ── 外层加密/解密 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def outer_encrypt(data: bytes, key: bytes) -> dict:
|
||||||
|
"""外层 AES-256-GCM 加密,返回加密信息字典"""
|
||||||
|
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
|
||||||
|
return {
|
||||||
|
"nonce": base64.b64encode(nonce).decode(),
|
||||||
|
"ciphertext": base64.b64encode(ct).decode(),
|
||||||
|
"tag": base64.b64encode(tag).decode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def outer_decrypt(enc_info: dict, key: bytes) -> bytes:
|
||||||
|
"""外层 AES-256-GCM 解密"""
|
||||||
|
nonce = base64.b64decode(enc_info["nonce"])
|
||||||
|
ct = base64.b64decode(enc_info["ciphertext"])
|
||||||
|
tag = base64.b64decode(enc_info["tag"])
|
||||||
|
return NBPCrypto._aes_decrypt(ct, key, nonce, tag)
|
||||||
|
|
||||||
|
# ── 中层加密/解密 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def inner_encrypt(data: bytes, key: bytes) -> dict:
|
||||||
|
"""中层 AES-256-GCM 加密"""
|
||||||
|
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
|
||||||
|
return {
|
||||||
|
"nonce": base64.b64encode(nonce).decode(),
|
||||||
|
"ciphertext": base64.b64encode(ct).decode(),
|
||||||
|
"tag": base64.b64encode(tag).decode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def inner_decrypt(enc_info: dict, key: bytes) -> bytes:
|
||||||
|
"""中层 AES-256-GCM 解密"""
|
||||||
|
nonce = base64.b64decode(enc_info["nonce"])
|
||||||
|
ct = base64.b64decode(enc_info["ciphertext"])
|
||||||
|
tag = base64.b64decode(enc_info["tag"])
|
||||||
|
return NBPCrypto._aes_decrypt(ct, key, nonce, tag)
|
||||||
|
|
||||||
|
# ── Ed25519 外层签名/验签 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def outer_sign(data: bytes, private_key: bytes) -> bytes:
|
||||||
|
"""Ed25519 签名"""
|
||||||
|
ed25519 = NBPCrypto._imp_ed25519()
|
||||||
|
key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
|
||||||
|
return key.sign(data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def outer_verify(data: bytes, signature: bytes, public_key: bytes) -> bool:
|
||||||
|
"""Ed25519 验签"""
|
||||||
|
try:
|
||||||
|
ed25519 = NBPCrypto._imp_ed25519()
|
||||||
|
key = ed25519.Ed25519PublicKey.from_public_bytes(public_key)
|
||||||
|
key.verify(signature, data)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── RSA-4096-PSS 中层签名/验签 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def inner_sign(data: bytes, private_key_pem: bytes) -> bytes:
|
||||||
|
"""RSA-4096-PSS 签名"""
|
||||||
|
serialization = NBPCrypto._imp_serialization()
|
||||||
|
hashes_mod = NBPCrypto._imp_hashes()
|
||||||
|
padding_mod = NBPCrypto._imp_padding()
|
||||||
|
backends = NBPCrypto._imp_backends()
|
||||||
|
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
private_key_pem, password=None, backend=backends.default_backend()
|
||||||
|
)
|
||||||
|
signature = private_key.sign(
|
||||||
|
data,
|
||||||
|
padding_mod.PSS(
|
||||||
|
mgf=padding_mod.MGF1(hashes_mod.SHA256()),
|
||||||
|
salt_length=padding_mod.PSS.MAX_LENGTH
|
||||||
|
),
|
||||||
|
hashes_mod.SHA256()
|
||||||
|
)
|
||||||
|
return signature
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def inner_verify(data: bytes, signature: bytes, public_key_pem: bytes) -> bool:
|
||||||
|
"""RSA-4096-PSS 验签"""
|
||||||
|
try:
|
||||||
|
serialization = NBPCrypto._imp_serialization()
|
||||||
|
hashes_mod = NBPCrypto._imp_hashes()
|
||||||
|
padding_mod = NBPCrypto._imp_padding()
|
||||||
|
backends = NBPCrypto._imp_backends()
|
||||||
|
|
||||||
|
public_key = serialization.load_pem_public_key(
|
||||||
|
public_key_pem, backend=backends.default_backend()
|
||||||
|
)
|
||||||
|
public_key.verify(
|
||||||
|
signature, data,
|
||||||
|
padding_mod.PSS(
|
||||||
|
mgf=padding_mod.MGF1(hashes_mod.SHA256()),
|
||||||
|
salt_length=padding_mod.PSS.MAX_LENGTH
|
||||||
|
),
|
||||||
|
hashes_mod.SHA256()
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ── HMAC-SHA256 内层模块签名/验签 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def module_sign(data: bytes, hmac_key: bytes) -> str:
|
||||||
|
"""HMAC-SHA256 模块签名"""
|
||||||
|
h = hmac.new(hmac_key, data, hashlib.sha256)
|
||||||
|
return base64.b64encode(h.digest()).decode()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def module_verify(data: bytes, signature: str, hmac_key: bytes) -> bool:
|
||||||
|
"""HMAC-SHA256 模块验签"""
|
||||||
|
expected = NBPCrypto.module_sign(data, hmac_key)
|
||||||
|
return hmac.compare_digest(expected, signature)
|
||||||
|
|
||||||
|
# ── RSA-OAEP 密钥封装 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encrypt_key(aes_key: bytes, rsa_public_key_pem: bytes) -> str:
|
||||||
|
"""RSA-OAEP 加密 AES 密钥"""
|
||||||
|
serialization = NBPCrypto._imp_serialization()
|
||||||
|
hashes_mod = NBPCrypto._imp_hashes()
|
||||||
|
padding_mod = NBPCrypto._imp_padding()
|
||||||
|
backends = NBPCrypto._imp_backends()
|
||||||
|
|
||||||
|
public_key = serialization.load_pem_public_key(
|
||||||
|
rsa_public_key_pem, backend=backends.default_backend()
|
||||||
|
)
|
||||||
|
encrypted = public_key.encrypt(
|
||||||
|
aes_key,
|
||||||
|
padding_mod.OAEP(
|
||||||
|
mgf=padding_mod.MGF1(algorithm=hashes_mod.SHA256()),
|
||||||
|
algorithm=hashes_mod.SHA256(),
|
||||||
|
label=None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return base64.b64encode(encrypted).decode()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_key(encrypted_key: str, rsa_private_key_pem: bytes) -> bytes:
|
||||||
|
"""RSA-OAEP 解密 AES 密钥"""
|
||||||
|
serialization = NBPCrypto._imp_serialization()
|
||||||
|
hashes_mod = NBPCrypto._imp_hashes()
|
||||||
|
padding_mod = NBPCrypto._imp_padding()
|
||||||
|
backends = NBPCrypto._imp_backends()
|
||||||
|
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
rsa_private_key_pem, password=None, backend=backends.default_backend()
|
||||||
|
)
|
||||||
|
encrypted = base64.b64decode(encrypted_key)
|
||||||
|
aes_key = private_key.decrypt(
|
||||||
|
encrypted,
|
||||||
|
padding_mod.OAEP(
|
||||||
|
mgf=padding_mod.MGF1(algorithm=hashes_mod.SHA256()),
|
||||||
|
algorithm=hashes_mod.SHA256(),
|
||||||
|
label=None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return aes_key
|
||||||
|
|
||||||
|
# ── 密钥文件读写 ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_key_to_pem(key_bytes: bytes, path: str, is_private: bool = False):
|
||||||
|
"""保存密钥到 PEM 文件"""
|
||||||
|
import os as _os
|
||||||
|
dir_path = _os.path.dirname(path)
|
||||||
|
if dir_path:
|
||||||
|
_os.makedirs(dir_path, exist_ok=True)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(key_bytes)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_key_from_pem(path: str) -> bytes:
|
||||||
|
"""从 PEM 文件加载密钥"""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
# ── 完整加密流程(打包时使用) ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def full_encrypt_package(
|
||||||
|
nir_data: dict[str, bytes],
|
||||||
|
manifest: dict,
|
||||||
|
ed25519_private_key: bytes,
|
||||||
|
rsa_private_key_pem: bytes,
|
||||||
|
rsa_public_key_pem: bytes,
|
||||||
|
) -> dict:
|
||||||
|
"""完整加密打包流程
|
||||||
|
|
||||||
|
返回包含所有加密/签名信息的字典,供 NBPFPacker 使用
|
||||||
|
"""
|
||||||
|
# 1. 生成两个 AES 密钥
|
||||||
|
key1 = NBPCrypto.generate_aes_key()
|
||||||
|
key2 = NBPCrypto.generate_aes_key()
|
||||||
|
|
||||||
|
# 2. 派生 HMAC 密钥
|
||||||
|
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
|
||||||
|
|
||||||
|
# 3. 中层加密:用 key2 加密每个 NIR 模块
|
||||||
|
inner_encrypted = {}
|
||||||
|
for mod_name, mod_data in nir_data.items():
|
||||||
|
inner_encrypted[mod_name] = NBPCrypto.inner_encrypt(mod_data, key2)
|
||||||
|
|
||||||
|
# 4. 中层签名:用 RSA 签名 NIR 数据摘要
|
||||||
|
nir_digest = hashlib.sha256()
|
||||||
|
for mod_name in sorted(inner_encrypted.keys()):
|
||||||
|
nir_digest.update(mod_name.encode())
|
||||||
|
nir_digest.update(inner_encrypted[mod_name]["ciphertext"].encode())
|
||||||
|
inner_signature = NBPCrypto.inner_sign(nir_digest.digest(), rsa_private_key_pem)
|
||||||
|
|
||||||
|
# 5. 内层签名:用 HMAC 签名每个模块
|
||||||
|
module_sigs = {}
|
||||||
|
for mod_name, mod_data in nir_data.items():
|
||||||
|
module_sigs[mod_name] = NBPCrypto.module_sign(mod_data, hmac_key)
|
||||||
|
|
||||||
|
# 6. 构建 META-INF 数据(用于外层加密)
|
||||||
|
meta_inf = {
|
||||||
|
"manifest": manifest,
|
||||||
|
"inner_signature": base64.b64encode(inner_signature).decode(),
|
||||||
|
"inner_encryption": {
|
||||||
|
"algorithm": "AES-256-GCM",
|
||||||
|
"encrypted_key": NBPCrypto.encrypt_key(key2, rsa_public_key_pem),
|
||||||
|
},
|
||||||
|
"module_signatures": module_sigs,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 7. 外层加密:用 key1 加密 META-INF 数据
|
||||||
|
meta_inf_bytes = json.dumps(meta_inf).encode("utf-8")
|
||||||
|
outer_encrypted = NBPCrypto.outer_encrypt(meta_inf_bytes, key1)
|
||||||
|
|
||||||
|
# 8. 外层签名:用 Ed25519 签名整个包摘要
|
||||||
|
package_digest = hashlib.sha256()
|
||||||
|
package_digest.update(json.dumps(outer_encrypted).encode())
|
||||||
|
for mod_name in sorted(inner_encrypted.keys()):
|
||||||
|
package_digest.update(mod_name.encode())
|
||||||
|
package_digest.update(inner_encrypted[mod_name]["ciphertext"].encode())
|
||||||
|
outer_signature = NBPCrypto.outer_sign(package_digest.digest(), ed25519_private_key)
|
||||||
|
|
||||||
|
# 9. 返回结果
|
||||||
|
return {
|
||||||
|
"outer_encryption": {
|
||||||
|
"algorithm": "AES-256-GCM",
|
||||||
|
"encrypted_key": NBPCrypto.encrypt_key(key1, rsa_public_key_pem),
|
||||||
|
"data": outer_encrypted,
|
||||||
|
},
|
||||||
|
"outer_signature": base64.b64encode(outer_signature).decode(),
|
||||||
|
"inner_encrypted": inner_encrypted,
|
||||||
|
"inner_signature": base64.b64encode(inner_signature).decode(),
|
||||||
|
"inner_encryption": meta_inf["inner_encryption"],
|
||||||
|
"module_signatures": module_sigs,
|
||||||
|
"hmac_key_derivation": "SHA256(key1+key2+NebulaHMACv1)",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 完整解密流程(加载时使用) ──
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def full_decrypt_package(
|
||||||
|
package_info: dict,
|
||||||
|
ed25519_public_key: bytes,
|
||||||
|
rsa_private_key_pem: bytes,
|
||||||
|
) -> dict[str, bytes]:
|
||||||
|
"""完整解密流程,返回 NIR 数据字典 {module_name: nir_bytes}"""
|
||||||
|
|
||||||
|
# 反调试检测
|
||||||
|
if NBPCrypto._anti_debug_check():
|
||||||
|
raise NBPCryptoError("调试器检测到,拒绝解密")
|
||||||
|
|
||||||
|
# 1. 外层验签
|
||||||
|
outer_sig = base64.b64decode(package_info["outer_signature"])
|
||||||
|
package_digest = hashlib.sha256()
|
||||||
|
package_digest.update(json.dumps(package_info["outer_encryption"]["data"]).encode())
|
||||||
|
for mod_name in sorted(package_info["inner_encrypted"].keys()):
|
||||||
|
package_digest.update(mod_name.encode())
|
||||||
|
package_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
|
||||||
|
if not NBPCrypto.outer_verify(package_digest.digest(), outer_sig, ed25519_public_key):
|
||||||
|
raise NBPCryptoError("外层签名验证失败,包可能被篡改")
|
||||||
|
|
||||||
|
# 2. 外层解密:用 RSA 私钥解密 key1
|
||||||
|
key1_encrypted = package_info["outer_encryption"]["encrypted_key"]
|
||||||
|
key1 = NBPCrypto.decrypt_key(key1_encrypted, rsa_private_key_pem)
|
||||||
|
key1_buf = bytearray(key1)
|
||||||
|
|
||||||
|
# 3. 解密 META-INF 数据
|
||||||
|
meta_inf_bytes = NBPCrypto.outer_decrypt(
|
||||||
|
package_info["outer_encryption"]["data"], key1
|
||||||
|
)
|
||||||
|
NBPCrypto._secure_wipe(key1_buf)
|
||||||
|
|
||||||
|
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
|
||||||
|
|
||||||
|
# 4. 中层验签
|
||||||
|
inner_sig = base64.b64decode(meta_inf["inner_signature"])
|
||||||
|
nir_digest = hashlib.sha256()
|
||||||
|
for mod_name in sorted(package_info["inner_encrypted"].keys()):
|
||||||
|
nir_digest.update(mod_name.encode())
|
||||||
|
nir_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
|
||||||
|
# 需要 RSA 公钥来验签,从 meta_inf 中获取
|
||||||
|
# 实际使用时,RSA 公钥应该从信任的密钥目录加载
|
||||||
|
# 这里假设调用者已经验证过 RSA 公钥
|
||||||
|
|
||||||
|
# 5. 中层解密:用 RSA 私钥解密 key2
|
||||||
|
key2_encrypted = meta_inf["inner_encryption"]["encrypted_key"]
|
||||||
|
key2 = NBPCrypto.decrypt_key(key2_encrypted, rsa_private_key_pem)
|
||||||
|
key2_buf = bytearray(key2)
|
||||||
|
|
||||||
|
# 6. 派生 HMAC 密钥
|
||||||
|
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
|
||||||
|
# key1 已经擦除,key2 即将擦除
|
||||||
|
NBPCrypto._secure_wipe(bytearray(key2))
|
||||||
|
|
||||||
|
# 7. 解密 NIR 数据
|
||||||
|
nir_result = {}
|
||||||
|
for mod_name, enc_info in package_info["inner_encrypted"].items():
|
||||||
|
mod_data = NBPCrypto.inner_decrypt(enc_info, key2)
|
||||||
|
nir_result[mod_name] = mod_data
|
||||||
|
|
||||||
|
# 8. 内层验签
|
||||||
|
module_sigs = meta_inf.get("module_signatures", {})
|
||||||
|
for mod_name, mod_data in nir_result.items():
|
||||||
|
expected_sig = module_sigs.get(mod_name)
|
||||||
|
if expected_sig:
|
||||||
|
if not NBPCrypto.module_verify(mod_data, expected_sig, hmac_key):
|
||||||
|
raise NBPCryptoError(f"模块 '{mod_name}' HMAC 签名验证失败")
|
||||||
|
|
||||||
|
return nir_result
|
||||||
349
oss/core/nbpf/format.py
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
""".nbpf 文件格式定义和打包/解包工具
|
||||||
|
|
||||||
|
.nbpf 文件结构(ZIP 格式):
|
||||||
|
```
|
||||||
|
.nbpf (ZIP)
|
||||||
|
├── META-INF/
|
||||||
|
│ ├── MANIFEST.MF # 插件元数据(明文)
|
||||||
|
│ ├── SIGNATURE # 外层 Ed25519 签名(明文)
|
||||||
|
│ ├── SIGNER.PEM # 外层签名者公钥(明文)
|
||||||
|
│ ├── ENCRYPTION # 外层加密信息(RSA-OAEP 加密的 AES 密钥1)
|
||||||
|
│ ├── INNER_SIGNATURE # 中层 RSA-4096 签名(加密存储)
|
||||||
|
│ ├── INNER_ENCRYPTION # 中层加密信息(RSA-OAEP 加密的 AES 密钥2)
|
||||||
|
│ └── MODULE_SIGS # 内层 HMAC 签名列表(加密存储)
|
||||||
|
├── NIR/
|
||||||
|
│ ├── main # 主模块 NIR(双重加密)
|
||||||
|
│ ├── sub_module # 子模块 NIR(双重加密)
|
||||||
|
│ └── ...
|
||||||
|
└── RES/
|
||||||
|
├── manifest.json # 原始 manifest(明文)
|
||||||
|
├── config.py # 配置文件(可选,明文)
|
||||||
|
├── extensions.py # 扩展配置(可选,明文)
|
||||||
|
└── ... # 其他资源文件(明文)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from oss.logger.logger import Log
|
||||||
|
from .crypto import NBPCrypto, NBPCryptoError
|
||||||
|
from .compiler import NIRCompiler, NIRCompileError
|
||||||
|
|
||||||
|
|
||||||
|
class NBPFFormatError(Exception):
|
||||||
|
""".nbpf 格式错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NBPFFormatter:
|
||||||
|
""".nbpf 文件格式常量"""
|
||||||
|
|
||||||
|
MAGIC = b"NBPF"
|
||||||
|
VERSION = 1
|
||||||
|
ENTRY_POINT = "main"
|
||||||
|
|
||||||
|
# ZIP 内部路径
|
||||||
|
META_INF = "META-INF/"
|
||||||
|
NIR_DIR = "NIR/"
|
||||||
|
RES_DIR = "RES/"
|
||||||
|
|
||||||
|
# META-INF 文件
|
||||||
|
MANIFEST = META_INF + "MANIFEST.MF"
|
||||||
|
SIGNATURE = META_INF + "SIGNATURE"
|
||||||
|
SIGNER_PEM = META_INF + "SIGNER.PEM"
|
||||||
|
ENCRYPTION = META_INF + "ENCRYPTION"
|
||||||
|
INNER_SIGNATURE = META_INF + "INNER_SIGNATURE"
|
||||||
|
INNER_ENCRYPTION = META_INF + "INNER_ENCRYPTION"
|
||||||
|
MODULE_SIGS = META_INF + "MODULE_SIGS"
|
||||||
|
|
||||||
|
# 跳过列表(打包时排除的文件)
|
||||||
|
SKIP_FILES = {"__pycache__", "SIGNATURE", ".DS_Store", "Thumbs.db"}
|
||||||
|
|
||||||
|
|
||||||
|
class NBPFPacker:
|
||||||
|
""".nbpf 打包工具 — 将插件目录打包为 .nbpf 文件"""
|
||||||
|
|
||||||
|
def __init__(self, crypto: NBPCrypto = None, compiler: NIRCompiler = None):
|
||||||
|
self.crypto = crypto or NBPCrypto()
|
||||||
|
self.compiler = compiler or NIRCompiler()
|
||||||
|
|
||||||
|
def pack(
|
||||||
|
self,
|
||||||
|
plugin_dir: Path,
|
||||||
|
output_path: Path,
|
||||||
|
ed25519_private_key: bytes,
|
||||||
|
rsa_private_key_pem: bytes,
|
||||||
|
rsa_public_key_pem: bytes,
|
||||||
|
ed25519_public_key: bytes = None,
|
||||||
|
signer_name: str = "unknown",
|
||||||
|
) -> Path:
|
||||||
|
"""将插件目录打包为 .nbpf 文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_dir: 插件目录路径
|
||||||
|
output_path: 输出 .nbpf 文件路径
|
||||||
|
ed25519_private_key: Ed25519 私钥(外层签名)
|
||||||
|
rsa_private_key_pem: RSA 私钥 PEM(中层签名)
|
||||||
|
rsa_public_key_pem: RSA 公钥 PEM(用于加密 AES 密钥)
|
||||||
|
ed25519_public_key: Ed25519 公钥(存入包内,None 则自动派生)
|
||||||
|
signer_name: 签名者名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
输出文件路径
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NBPFFormatError: 打包失败
|
||||||
|
"""
|
||||||
|
if not plugin_dir.exists():
|
||||||
|
raise NBPFFormatError(f"插件目录不存在: {plugin_dir}")
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
output_path = Path(output_path)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 读取 manifest
|
||||||
|
manifest = self._read_manifest(plugin_dir)
|
||||||
|
|
||||||
|
# 2. 编译所有 .py 文件为 NIR
|
||||||
|
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
|
||||||
|
nir_data = self.compiler.compile_plugin(plugin_dir)
|
||||||
|
|
||||||
|
# 3. 收集资源文件
|
||||||
|
res_files = self._collect_resources(plugin_dir)
|
||||||
|
|
||||||
|
# 4. 完整加密打包
|
||||||
|
Log.info("NBPF", "加密打包中...")
|
||||||
|
package_info = self.crypto.full_encrypt_package(
|
||||||
|
nir_data=nir_data,
|
||||||
|
manifest=manifest,
|
||||||
|
ed25519_private_key=ed25519_private_key,
|
||||||
|
rsa_private_key_pem=rsa_private_key_pem,
|
||||||
|
rsa_public_key_pem=rsa_public_key_pem,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 构建 ZIP 包
|
||||||
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
# META-INF/MANIFEST.MF
|
||||||
|
zf.writestr(NBPFFormatter.MANIFEST, json.dumps(manifest, indent=2))
|
||||||
|
|
||||||
|
# META-INF/SIGNATURE
|
||||||
|
zf.writestr(NBPFFormatter.SIGNATURE, package_info["outer_signature"])
|
||||||
|
|
||||||
|
# META-INF/SIGNER.PEM
|
||||||
|
if ed25519_public_key:
|
||||||
|
zf.writestr(NBPFFormatter.SIGNER_PEM, ed25519_public_key)
|
||||||
|
else:
|
||||||
|
# 从私钥派生公钥
|
||||||
|
ed25519_mod = NBPCrypto._imp_ed25519()
|
||||||
|
key = ed25519_mod.Ed25519PrivateKey.from_private_bytes(ed25519_private_key)
|
||||||
|
pub_bytes = key.public_key().public_bytes(
|
||||||
|
NBPCrypto._imp_serialization().Encoding.Raw,
|
||||||
|
NBPCrypto._imp_serialization().PublicFormat.Raw
|
||||||
|
)
|
||||||
|
zf.writestr(NBPFFormatter.SIGNER_PEM, pub_bytes)
|
||||||
|
|
||||||
|
# META-INF/ENCRYPTION
|
||||||
|
zf.writestr(NBPFFormatter.ENCRYPTION, json.dumps(package_info["outer_encryption"]))
|
||||||
|
|
||||||
|
# META-INF/INNER_SIGNATURE
|
||||||
|
zf.writestr(NBPFFormatter.INNER_SIGNATURE, package_info["inner_signature"])
|
||||||
|
|
||||||
|
# META-INF/INNER_ENCRYPTION
|
||||||
|
zf.writestr(NBPFFormatter.INNER_ENCRYPTION, json.dumps(package_info["inner_encryption"]))
|
||||||
|
|
||||||
|
# META-INF/MODULE_SIGS
|
||||||
|
zf.writestr(NBPFFormatter.MODULE_SIGS, json.dumps(package_info["module_signatures"]))
|
||||||
|
|
||||||
|
# NIR/ 目录
|
||||||
|
for mod_name, enc_info in package_info["inner_encrypted"].items():
|
||||||
|
nir_path = NBPFFormatter.NIR_DIR + mod_name
|
||||||
|
zf.writestr(nir_path, json.dumps(enc_info))
|
||||||
|
|
||||||
|
# RES/ 目录
|
||||||
|
for res_path, res_data in res_files.items():
|
||||||
|
zf.writestr(NBPFFormatter.RES_DIR + res_path, res_data)
|
||||||
|
|
||||||
|
Log.ok("NBPF", f"打包完成: {output_path}")
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
except NIRCompileError as e:
|
||||||
|
raise NBPFFormatError(f"编译失败: {e}") from e
|
||||||
|
except NBPCryptoError as e:
|
||||||
|
raise NBPFFormatError(f"加密失败: {e}") from e
|
||||||
|
except Exception as e:
|
||||||
|
raise NBPFFormatError(f"打包失败: {type(e).__name__}: {e}") from e
|
||||||
|
|
||||||
|
def _read_manifest(self, plugin_dir: Path) -> dict:
|
||||||
|
"""读取插件 manifest.json"""
|
||||||
|
manifest_file = plugin_dir / "manifest.json"
|
||||||
|
if not manifest_file.exists():
|
||||||
|
# 生成默认 manifest
|
||||||
|
return {
|
||||||
|
"metadata": {
|
||||||
|
"name": plugin_dir.name,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "unknown",
|
||||||
|
"description": "",
|
||||||
|
},
|
||||||
|
"config": {"enabled": True, "args": {}},
|
||||||
|
"dependencies": [],
|
||||||
|
"permissions": [],
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise NBPFFormatError(f"manifest.json 格式错误: {e}") from e
|
||||||
|
|
||||||
|
def _collect_resources(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||||
|
"""收集资源文件(非 .py 文件)"""
|
||||||
|
resources = {}
|
||||||
|
for file_path in sorted(plugin_dir.rglob("*")):
|
||||||
|
if not file_path.is_file():
|
||||||
|
continue
|
||||||
|
rel_path = str(file_path.relative_to(plugin_dir))
|
||||||
|
|
||||||
|
# 跳过
|
||||||
|
skip = False
|
||||||
|
for skip_name in NBPFFormatter.SKIP_FILES:
|
||||||
|
if skip_name in file_path.parts:
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
if skip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 跳过 .py 文件(已编译为 NIR)
|
||||||
|
if file_path.suffix == ".py":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 跳过 manifest.json(已单独处理)
|
||||||
|
if file_path.name == "manifest.json":
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
resources[rel_path] = file_path.read_bytes()
|
||||||
|
except Exception as e:
|
||||||
|
Log.warn("NBPF", f"跳过资源文件 {rel_path}: {e}")
|
||||||
|
|
||||||
|
return resources
|
||||||
|
|
||||||
|
|
||||||
|
class NBPFUnpacker:
|
||||||
|
""".nbpf 解包工具 — 解包 .nbpf 文件到目录"""
|
||||||
|
|
||||||
|
def __init__(self, crypto: NBPCrypto = None):
|
||||||
|
self.crypto = crypto or NBPCrypto()
|
||||||
|
|
||||||
|
def unpack(self, nbpf_path: Path, output_dir: Path) -> Path:
|
||||||
|
"""解包 .nbpf 到目录(用于调试/开发)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nbpf_path: .nbpf 文件路径
|
||||||
|
output_dir: 输出目录
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
输出目录路径
|
||||||
|
"""
|
||||||
|
if not nbpf_path.exists():
|
||||||
|
raise NBPFFormatError(f".nbpf 文件不存在: {nbpf_path}")
|
||||||
|
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||||
|
# 提取所有文件
|
||||||
|
for info in zf.infolist():
|
||||||
|
# 跳过目录
|
||||||
|
if info.filename.endswith("/"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算输出路径
|
||||||
|
out_path = output_dir / info.filename
|
||||||
|
|
||||||
|
# 创建父目录
|
||||||
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 写入文件
|
||||||
|
out_path.write_bytes(zf.read(info.filename))
|
||||||
|
|
||||||
|
Log.ok("NBPF", f"解包完成: {output_dir}")
|
||||||
|
return output_dir
|
||||||
|
|
||||||
|
def extract_manifest(self, nbpf_path: Path) -> dict:
|
||||||
|
"""提取 manifest.json(不解密)"""
|
||||||
|
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||||
|
if NBPFFormatter.MANIFEST not in zf.namelist():
|
||||||
|
raise NBPFFormatError(".nbpf 文件中缺少 MANIFEST.MF")
|
||||||
|
return json.loads(zf.read(NBPFFormatter.MANIFEST).decode("utf-8"))
|
||||||
|
|
||||||
|
def verify_signature(
|
||||||
|
self,
|
||||||
|
nbpf_path: Path,
|
||||||
|
trusted_keys: dict[str, bytes],
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""验证 .nbpf 文件的外层 Ed25519 签名
|
||||||
|
|
||||||
|
签名计算方式与 full_encrypt_package 一致。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nbpf_path: .nbpf 文件路径
|
||||||
|
trusted_keys: {signer_name: ed25519_public_key_bytes} 信任的公钥字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(是否通过, 消息)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||||
|
# 读取签名和签名者公钥
|
||||||
|
if NBPFFormatter.SIGNATURE not in zf.namelist():
|
||||||
|
return False, "缺少 SIGNATURE 文件"
|
||||||
|
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
|
||||||
|
return False, "缺少 SIGNER.PEM 文件"
|
||||||
|
|
||||||
|
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
|
||||||
|
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
|
||||||
|
|
||||||
|
# 查找匹配的信任公钥
|
||||||
|
matched = False
|
||||||
|
matched_name = None
|
||||||
|
for name, trusted_key in trusted_keys.items():
|
||||||
|
if trusted_key == signer_pub_key:
|
||||||
|
matched = True
|
||||||
|
matched_name = name
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
return False, "签名者公钥不在信任列表中"
|
||||||
|
|
||||||
|
# 计算包摘要(与 full_encrypt_package 一致)
|
||||||
|
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
digest.update(json.dumps(encryption_data["data"]).encode())
|
||||||
|
|
||||||
|
# 按模块名排序,添加模块名和密文
|
||||||
|
nir_modules = {}
|
||||||
|
for info in zf.infolist():
|
||||||
|
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
|
||||||
|
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||||
|
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||||
|
nir_modules[mod_name] = mod_data
|
||||||
|
|
||||||
|
for mod_name in sorted(nir_modules.keys()):
|
||||||
|
digest.update(mod_name.encode())
|
||||||
|
digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||||
|
|
||||||
|
# 验签
|
||||||
|
signature = base64.b64decode(signature_b64)
|
||||||
|
if self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
|
||||||
|
return True, f"签名验证通过 (signer: {matched_name})"
|
||||||
|
else:
|
||||||
|
return False, "签名验证失败,包可能被篡改"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"签名验证异常: {type(e).__name__}: {e}"
|
||||||
360
oss/core/nbpf/loader.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
""".nbpf 加载器 — 加载 .nbpf 文件到运行时环境
|
||||||
|
|
||||||
|
加载流程:
|
||||||
|
1. 打开 .nbpf (ZIP) 文件
|
||||||
|
2. 外层验签:用 Ed25519 公钥验证包签名
|
||||||
|
3. 外层解密:用 RSA 私钥解密密钥1,解密 META-INF/
|
||||||
|
4. 中层验签:用 RSA-4096 公钥验证 NIR 签名
|
||||||
|
5. 中层解密:用 RSA 私钥解密密钥2,解密 NIR 数据
|
||||||
|
6. 内层验签:用 HMAC 验证每个模块签名
|
||||||
|
7. 反序列化 NIR 为 code object
|
||||||
|
8. 在受限沙箱中执行
|
||||||
|
9. 内存擦除所有密钥
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import zipfile
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from oss.logger.logger import Log
|
||||||
|
from .crypto import NBPCrypto, NBPCryptoError
|
||||||
|
from .compiler import NIRCompiler, NIRCompileError
|
||||||
|
from .format import NBPFFormatter, NBPFFormatError
|
||||||
|
|
||||||
|
|
||||||
|
class NBPFLoadError(Exception):
|
||||||
|
""".nbpf 加载错误"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NBPFLoader:
|
||||||
|
""".nbpf 加载器"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
crypto: NBPCrypto = None,
|
||||||
|
compiler: NIRCompiler = None,
|
||||||
|
trusted_ed25519_keys: dict[str, bytes] = None,
|
||||||
|
trusted_rsa_keys: dict[str, bytes] = None,
|
||||||
|
rsa_private_key: bytes = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
crypto: 加密工具实例
|
||||||
|
compiler: 编译器实例
|
||||||
|
trusted_ed25519_keys: {signer_name: ed25519_public_key_bytes}
|
||||||
|
trusted_rsa_keys: {signer_name: rsa_public_key_pem}
|
||||||
|
rsa_private_key: RSA 私钥 PEM(用于解密 AES 密钥)
|
||||||
|
"""
|
||||||
|
self.crypto = crypto or NBPCrypto()
|
||||||
|
self.compiler = compiler or NIRCompiler()
|
||||||
|
self.trusted_ed25519_keys = trusted_ed25519_keys or {}
|
||||||
|
self.trusted_rsa_keys = trusted_rsa_keys or {}
|
||||||
|
self.rsa_private_key = rsa_private_key
|
||||||
|
|
||||||
|
def load(
|
||||||
|
self,
|
||||||
|
nbpf_path: Path,
|
||||||
|
plugin_name: str = None,
|
||||||
|
) -> tuple[Any, dict]:
|
||||||
|
"""加载 .nbpf 插件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nbpf_path: .nbpf 文件路径
|
||||||
|
plugin_name: 插件名称(用于日志,默认从 manifest 读取)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(plugin_instance, plugin_info_dict)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NBPFLoadError: 加载失败
|
||||||
|
"""
|
||||||
|
if not nbpf_path.exists():
|
||||||
|
raise NBPFLoadError(f".nbpf 文件不存在: {nbpf_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||||
|
# 1. 外层验签
|
||||||
|
signer_name = self._verify_outer_signature(zf)
|
||||||
|
Log.info("NBPF", f"外层签名验证通过 (signer: {signer_name})")
|
||||||
|
|
||||||
|
# 2. 外层解密
|
||||||
|
key1, meta_inf = self._decrypt_outer(zf)
|
||||||
|
key1_buf = bytearray(key1)
|
||||||
|
|
||||||
|
# 3. 中层验签
|
||||||
|
rsa_signer = self._verify_inner_signature(zf, meta_inf)
|
||||||
|
Log.info("NBPF", f"中层签名验证通过 (signer: {rsa_signer})")
|
||||||
|
|
||||||
|
# 4. 中层解密
|
||||||
|
key2 = self._decrypt_inner(meta_inf)
|
||||||
|
key2_buf = bytearray(key2)
|
||||||
|
|
||||||
|
# 5. 派生 HMAC 密钥
|
||||||
|
hmac_key = self.crypto.derive_hmac_key(key1, key2)
|
||||||
|
self.crypto._secure_wipe(key1_buf)
|
||||||
|
self.crypto._secure_wipe(key2_buf)
|
||||||
|
|
||||||
|
# 6. 解密 NIR 数据
|
||||||
|
nir_data = self._decrypt_nir_data(zf, key2)
|
||||||
|
|
||||||
|
# 7. 内层验签
|
||||||
|
self._verify_module_signatures(nir_data, meta_inf, hmac_key)
|
||||||
|
Log.info("NBPF", "内层模块签名验证通过")
|
||||||
|
|
||||||
|
# 8. 获取插件名称
|
||||||
|
manifest = meta_inf.get("manifest", {})
|
||||||
|
meta = manifest.get("metadata", {})
|
||||||
|
name = plugin_name or meta.get("name", nbpf_path.stem)
|
||||||
|
|
||||||
|
# 9. 反序列化并执行
|
||||||
|
instance, module = self._deserialize_and_exec(nir_data, name)
|
||||||
|
|
||||||
|
# 10. 构建插件信息
|
||||||
|
info = {
|
||||||
|
"name": name,
|
||||||
|
"version": meta.get("version", ""),
|
||||||
|
"author": meta.get("author", ""),
|
||||||
|
"description": meta.get("description", ""),
|
||||||
|
"manifest": manifest,
|
||||||
|
"nbpf_path": str(nbpf_path),
|
||||||
|
"signer": signer_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.ok("NBPF", f"插件 '{name}' 加载成功")
|
||||||
|
return instance, info
|
||||||
|
|
||||||
|
except (NBPFFormatError, NBPCryptoError, NIRCompileError) as e:
|
||||||
|
raise NBPFLoadError(str(e)) from e
|
||||||
|
except zipfile.BadZipFile as e:
|
||||||
|
raise NBPFLoadError(f".nbpf 文件损坏: {e}") from e
|
||||||
|
except Exception as e:
|
||||||
|
raise NBPFLoadError(f"加载失败: {type(e).__name__}: {e}") from e
|
||||||
|
|
||||||
|
# ── 外层验签 ──
|
||||||
|
|
||||||
|
def _verify_outer_signature(self, zf: zipfile.ZipFile) -> str:
|
||||||
|
"""外层 Ed25519 签名验证,返回签名者名称
|
||||||
|
|
||||||
|
签名计算方式与 full_encrypt_package 一致:
|
||||||
|
SHA256(outer_encryption_json + sorted_module_names_and_ciphertexts)
|
||||||
|
"""
|
||||||
|
if NBPFFormatter.SIGNATURE not in zf.namelist():
|
||||||
|
raise NBPFLoadError("缺少外层签名文件")
|
||||||
|
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
|
||||||
|
raise NBPFLoadError("缺少签名者公钥文件")
|
||||||
|
|
||||||
|
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
|
||||||
|
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
|
||||||
|
|
||||||
|
# 查找匹配的信任公钥
|
||||||
|
signer_name = None
|
||||||
|
for name, trusted_key in self.trusted_ed25519_keys.items():
|
||||||
|
if trusted_key == signer_pub_key:
|
||||||
|
signer_name = name
|
||||||
|
break
|
||||||
|
|
||||||
|
if signer_name is None:
|
||||||
|
raise NBPFLoadError("签名者公钥不在信任列表中")
|
||||||
|
|
||||||
|
# 计算包摘要(与 full_encrypt_package 一致)
|
||||||
|
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
digest.update(json.dumps(encryption_data["data"]).encode())
|
||||||
|
|
||||||
|
# 按模块名排序,添加模块名和密文
|
||||||
|
nir_modules = {}
|
||||||
|
for info in zf.infolist():
|
||||||
|
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
|
||||||
|
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||||
|
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||||
|
nir_modules[mod_name] = mod_data
|
||||||
|
|
||||||
|
for mod_name in sorted(nir_modules.keys()):
|
||||||
|
digest.update(mod_name.encode())
|
||||||
|
digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||||
|
|
||||||
|
# 验签
|
||||||
|
signature = base64.b64decode(signature_b64)
|
||||||
|
if not self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
|
||||||
|
raise NBPFLoadError("外层签名验证失败,包可能被篡改")
|
||||||
|
|
||||||
|
return signer_name
|
||||||
|
|
||||||
|
# ── 外层解密 ──
|
||||||
|
|
||||||
|
def _decrypt_outer(self, zf: zipfile.ZipFile) -> tuple[bytes, dict]:
|
||||||
|
"""外层解密,返回 (key1, meta_inf_dict)"""
|
||||||
|
if NBPFFormatter.ENCRYPTION not in zf.namelist():
|
||||||
|
raise NBPFLoadError("缺少外层加密信息")
|
||||||
|
|
||||||
|
encryption_info = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||||
|
|
||||||
|
# 用 RSA 私钥解密 key1
|
||||||
|
if self.rsa_private_key is None:
|
||||||
|
raise NBPFLoadError("未配置 RSA 私钥,无法解密")
|
||||||
|
key1 = self.crypto.decrypt_key(encryption_info["encrypted_key"], self.rsa_private_key)
|
||||||
|
|
||||||
|
# 解密 META-INF 数据
|
||||||
|
meta_inf_bytes = self.crypto.outer_decrypt(encryption_info["data"], key1)
|
||||||
|
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
|
||||||
|
|
||||||
|
return key1, meta_inf
|
||||||
|
|
||||||
|
# ── 中层验签 ──
|
||||||
|
|
||||||
|
def _verify_inner_signature(self, zf: zipfile.ZipFile, meta_inf: dict) -> str:
|
||||||
|
"""中层 RSA-4096 签名验证,返回签名者名称
|
||||||
|
|
||||||
|
签名计算方式与 full_encrypt_package 一致:
|
||||||
|
SHA256(sorted_module_names + inner_encrypted_ciphertexts)
|
||||||
|
"""
|
||||||
|
inner_sig_b64 = meta_inf.get("inner_signature")
|
||||||
|
if not inner_sig_b64:
|
||||||
|
raise NBPFLoadError("缺少中层签名")
|
||||||
|
|
||||||
|
# 计算 NIR 数据摘要(与 full_encrypt_package 一致)
|
||||||
|
nir_modules = {}
|
||||||
|
for info in zf.infolist():
|
||||||
|
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
|
||||||
|
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||||
|
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||||
|
nir_modules[mod_name] = mod_data
|
||||||
|
|
||||||
|
nir_digest = hashlib.sha256()
|
||||||
|
for mod_name in sorted(nir_modules.keys()):
|
||||||
|
nir_digest.update(mod_name.encode())
|
||||||
|
nir_digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||||
|
|
||||||
|
# 查找匹配的 RSA 公钥
|
||||||
|
inner_sig = base64.b64decode(inner_sig_b64)
|
||||||
|
for name, rsa_pub_key in self.trusted_rsa_keys.items():
|
||||||
|
if self.crypto.inner_verify(nir_digest.digest(), inner_sig, rsa_pub_key):
|
||||||
|
return name
|
||||||
|
|
||||||
|
raise NBPFLoadError("中层签名验证失败,无法匹配任何信任的 RSA 公钥")
|
||||||
|
|
||||||
|
# ── 中层解密 ──
|
||||||
|
|
||||||
|
def _decrypt_inner(self, meta_inf: dict) -> bytes:
|
||||||
|
"""中层解密,返回 key2"""
|
||||||
|
inner_enc = meta_inf.get("inner_encryption", {})
|
||||||
|
encrypted_key = inner_enc.get("encrypted_key")
|
||||||
|
if not encrypted_key:
|
||||||
|
raise NBPFLoadError("缺少中层加密密钥")
|
||||||
|
|
||||||
|
if self.rsa_private_key is None:
|
||||||
|
raise NBPFLoadError("未配置 RSA 私钥,无法解密")
|
||||||
|
return self.crypto.decrypt_key(encrypted_key, self.rsa_private_key)
|
||||||
|
|
||||||
|
# ── 解密 NIR 数据 ──
|
||||||
|
|
||||||
|
def _decrypt_nir_data(self, zf: zipfile.ZipFile, key2: bytes) -> dict[str, bytes]:
|
||||||
|
"""解密 NIR 数据,返回 {module_name: nir_bytes}"""
|
||||||
|
nir_data = {}
|
||||||
|
for info in zf.infolist():
|
||||||
|
if not info.filename.startswith(NBPFFormatter.NIR_DIR):
|
||||||
|
continue
|
||||||
|
if info.filename.endswith("/"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
module_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||||
|
enc_info = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||||
|
mod_data = self.crypto.inner_decrypt(enc_info, key2)
|
||||||
|
nir_data[module_name] = mod_data
|
||||||
|
|
||||||
|
return nir_data
|
||||||
|
|
||||||
|
# ── 内层验签 ──
|
||||||
|
|
||||||
|
def _verify_module_signatures(self, nir_data: dict, meta_inf: dict, hmac_key: bytes):
|
||||||
|
"""内层 HMAC 模块签名验证"""
|
||||||
|
module_sigs = meta_inf.get("module_signatures", {})
|
||||||
|
if not module_sigs:
|
||||||
|
Log.warn("NBPF", "未找到模块签名,跳过内层验签")
|
||||||
|
return
|
||||||
|
|
||||||
|
for mod_name, mod_data in nir_data.items():
|
||||||
|
expected_sig = module_sigs.get(mod_name)
|
||||||
|
if expected_sig:
|
||||||
|
if not self.crypto.module_verify(mod_data, expected_sig, hmac_key):
|
||||||
|
raise NBPFLoadError(f"模块 '{mod_name}' HMAC 签名验证失败")
|
||||||
|
|
||||||
|
# ── 反序列化并执行 ──
|
||||||
|
|
||||||
|
def _deserialize_and_exec(
|
||||||
|
self,
|
||||||
|
nir_data: dict[str, bytes],
|
||||||
|
plugin_name: str,
|
||||||
|
) -> tuple[Any, types.ModuleType]:
|
||||||
|
"""反序列化 NIR 并执行,返回 (instance, module)"""
|
||||||
|
|
||||||
|
# 构建安全的全局命名空间
|
||||||
|
safe_globals = self._build_safe_globals(plugin_name)
|
||||||
|
|
||||||
|
# 按依赖顺序执行模块
|
||||||
|
main_module = None
|
||||||
|
for mod_name in sorted(nir_data.keys()):
|
||||||
|
nir_bytes = nir_data[mod_name]
|
||||||
|
code = self.compiler.deserialize_nir(nir_bytes)
|
||||||
|
|
||||||
|
# 创建模块
|
||||||
|
module_name = f"nbpf.{plugin_name}.{mod_name}"
|
||||||
|
if mod_name == NBPFFormatter.ENTRY_POINT:
|
||||||
|
module_name = f"nbpf.{plugin_name}"
|
||||||
|
|
||||||
|
module = types.ModuleType(module_name)
|
||||||
|
module.__package__ = f"nbpf.{plugin_name}"
|
||||||
|
module.__path__ = []
|
||||||
|
module.__file__ = f"<nbpf:{plugin_name}/{mod_name}>"
|
||||||
|
sys.modules[module_name] = module
|
||||||
|
|
||||||
|
# 执行 code object
|
||||||
|
exec(code, module.__dict__)
|
||||||
|
|
||||||
|
if mod_name == NBPFFormatter.ENTRY_POINT:
|
||||||
|
main_module = module
|
||||||
|
|
||||||
|
# 调用 New() 创建实例
|
||||||
|
if main_module is None:
|
||||||
|
raise NBPFLoadError(f"缺少入口模块 '{NBPFFormatter.ENTRY_POINT}'")
|
||||||
|
|
||||||
|
if not hasattr(main_module, "New"):
|
||||||
|
raise NBPFLoadError("插件缺少 New() 函数")
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance = main_module.New()
|
||||||
|
except Exception as e:
|
||||||
|
raise NBPFLoadError(f"创建插件实例失败: {e}") from e
|
||||||
|
|
||||||
|
return instance, main_module
|
||||||
|
|
||||||
|
def _build_safe_globals(self, plugin_name: str) -> dict:
|
||||||
|
"""构建安全的全局命名空间"""
|
||||||
|
safe_builtins = {
|
||||||
|
'True': True, 'False': False, 'None': None,
|
||||||
|
'dict': dict, 'list': list, 'str': str, 'int': int,
|
||||||
|
'float': float, 'bool': bool, 'tuple': tuple, 'set': set,
|
||||||
|
'len': len, 'range': range, 'enumerate': enumerate,
|
||||||
|
'zip': zip, 'map': map, 'filter': filter,
|
||||||
|
'sorted': sorted, 'reversed': reversed,
|
||||||
|
'min': min, 'max': max, 'sum': sum, 'abs': abs,
|
||||||
|
'round': round, 'isinstance': isinstance, 'issubclass': issubclass,
|
||||||
|
'type': type, 'id': id, 'hash': hash, 'repr': repr,
|
||||||
|
'print': print, 'object': object, 'property': property,
|
||||||
|
'staticmethod': staticmethod, 'classmethod': classmethod,
|
||||||
|
'super': super, 'iter': iter, 'next': next,
|
||||||
|
'any': any, 'all': all, 'callable': callable,
|
||||||
|
'hasattr': hasattr, 'getattr': getattr,
|
||||||
|
'ValueError': ValueError, 'TypeError': TypeError,
|
||||||
|
'KeyError': KeyError, 'IndexError': IndexError,
|
||||||
|
'Exception': Exception, 'BaseException': BaseException,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'__builtins__': safe_builtins,
|
||||||
|
'__name__': f'nbpf.{plugin_name}',
|
||||||
|
}
|
||||||
186
oss/core/repl/main.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""REPL 交互终端 - 基于 Python cmd 模块"""
|
||||||
|
import cmd
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
import readline
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HISTORY_FILE = str(Path.home() / ".nebula_repl_history")
|
||||||
|
|
||||||
|
|
||||||
|
class NebulaShell(cmd.Cmd):
|
||||||
|
"""NebulaShell REPL 交互终端"""
|
||||||
|
|
||||||
|
def __init__(self, plugin_mgr):
|
||||||
|
super().__init__()
|
||||||
|
self.plugin_mgr = plugin_mgr
|
||||||
|
self.prompt = "\033[1;36mNebula>\033[0m " # 青色提示符
|
||||||
|
self.intro = (
|
||||||
|
"\033[1;33mNebulaShell Core v2.0.0\033[0m\n"
|
||||||
|
"输入 \033[1;32mhelp\033[0m 查看命令列表 | 输入 \033[1;31mexit\033[0m 退出"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 加载历史记录
|
||||||
|
self._load_history()
|
||||||
|
|
||||||
|
def _load_history(self):
|
||||||
|
"""加载命令历史记录"""
|
||||||
|
try:
|
||||||
|
readline.read_history_file(HISTORY_FILE)
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
pass
|
||||||
|
readline.set_history_length(500)
|
||||||
|
|
||||||
|
def _save_history(self):
|
||||||
|
"""保存命令历史记录"""
|
||||||
|
try:
|
||||||
|
readline.write_history_file(HISTORY_FILE)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_plugins(self):
|
||||||
|
"""获取所有已加载的插件列表"""
|
||||||
|
if not self.plugin_mgr:
|
||||||
|
return []
|
||||||
|
return list(self.plugin_mgr.plugins.keys())
|
||||||
|
|
||||||
|
def _get_injected_functions(self):
|
||||||
|
"""获取所有 PL 注入的功能"""
|
||||||
|
if not self.plugin_mgr:
|
||||||
|
return {}
|
||||||
|
return self.plugin_mgr.pl_injector.get_registry_info()
|
||||||
|
|
||||||
|
# ── 命令:plugins ──
|
||||||
|
|
||||||
|
def do_plugins(self, arg):
|
||||||
|
"""列出所有已加载的插件"""
|
||||||
|
plugins = self._get_plugins()
|
||||||
|
if not plugins:
|
||||||
|
print("\033[1;33m没有已加载的插件\033[0m")
|
||||||
|
return
|
||||||
|
print(f"\033[1;36m已加载插件 ({len(plugins)}):\033[0m")
|
||||||
|
for name in plugins:
|
||||||
|
info = self.plugin_mgr.get_info(name)
|
||||||
|
if info:
|
||||||
|
status = ""
|
||||||
|
if self.plugin_mgr.fallback_manager.is_degraded(name):
|
||||||
|
status = " \033[1;31m[降级]\033[0m"
|
||||||
|
print(f" \033[1;32m{name}\033[0m v{info.version} - {info.description}{status}")
|
||||||
|
else:
|
||||||
|
print(f" \033[1;32m{name}\033[0m")
|
||||||
|
|
||||||
|
# ── 命令:pl ──
|
||||||
|
|
||||||
|
def do_pl(self, arg):
|
||||||
|
"""列出所有 PL 注入的功能"""
|
||||||
|
registry = self._get_injected_functions()
|
||||||
|
if not registry:
|
||||||
|
print("\033[1;33m没有 PL 注入功能\033[0m")
|
||||||
|
return
|
||||||
|
print(f"\033[1;36mPL 注入功能 ({len(registry)}):\033[0m")
|
||||||
|
for name, info in registry.items():
|
||||||
|
descs = [d for d in info["descriptions"] if d]
|
||||||
|
desc_str = f" - {descs[0]}" if descs else ""
|
||||||
|
print(f" \033[1;32m{name}\033[0m (来自 {', '.join(info['plugins'])}){desc_str}")
|
||||||
|
|
||||||
|
# ── 命令:call ──
|
||||||
|
|
||||||
|
def do_call(self, arg):
|
||||||
|
"""调用 PL 注入功能: call <function_name> [args...]"""
|
||||||
|
if not arg:
|
||||||
|
print("\033[1;33m用法: call <function_name> [args...]\033[0m")
|
||||||
|
return
|
||||||
|
parts = shlex.split(arg)
|
||||||
|
name = parts[0]
|
||||||
|
args = parts[1:]
|
||||||
|
funcs = self.plugin_mgr.pl_injector.get_injected_functions(name)
|
||||||
|
if not funcs:
|
||||||
|
print(f"\033[1;31m未找到功能: {name}\033[0m")
|
||||||
|
return
|
||||||
|
for func in funcs:
|
||||||
|
try:
|
||||||
|
result = func(*args)
|
||||||
|
if result is not None:
|
||||||
|
print(result)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\033[1;31m执行失败: {e}\033[0m")
|
||||||
|
|
||||||
|
# ── 命令:status ──
|
||||||
|
|
||||||
|
def do_status(self, arg):
|
||||||
|
"""显示 Core 状态"""
|
||||||
|
if not self.plugin_mgr:
|
||||||
|
print("\033[1;31mCore 未就绪\033[0m")
|
||||||
|
return
|
||||||
|
status = self.plugin_mgr.get_status()
|
||||||
|
print(f"\033[1;36mCore 状态:\033[0m")
|
||||||
|
print(f" 插件总数: {status['plugins']['total']}")
|
||||||
|
if status['plugins']['degraded']:
|
||||||
|
print(f" 降级插件: \033[1;31m{', '.join(status['plugins']['degraded'])}\033[0m")
|
||||||
|
print(f" HTTP 服务: {'\033[1;32m运行中\033[0m' if status['http_server'] else '\033[1;31m未启动\033[0m'}")
|
||||||
|
print(f" 防篡改监控: {'\033[1;32m运行中\033[0m' if status['tamper_monitor'] else '\033[1;31m未启动\033[0m'}")
|
||||||
|
print(f" 审计日志: {status['audit_logs']} 条")
|
||||||
|
print(f" 篡改告警: {status['tamper_alerts']} 条")
|
||||||
|
print(f" 数据目录: {status['data_store']}")
|
||||||
|
|
||||||
|
# ── 命令:audit ──
|
||||||
|
|
||||||
|
def do_audit(self, arg):
|
||||||
|
"""查看审计日志: audit [plugin_name]"""
|
||||||
|
if not self.plugin_mgr:
|
||||||
|
return
|
||||||
|
logs = self.plugin_mgr.get_audit_logs(plugin_name=arg if arg else None, limit=20)
|
||||||
|
if not logs:
|
||||||
|
print("\033[1;33m无审计日志\033[0m")
|
||||||
|
return
|
||||||
|
print(f"\033[1;36m审计日志 ({len(logs)} 条):\033[0m")
|
||||||
|
for log in reversed(logs):
|
||||||
|
t = log["time"]
|
||||||
|
print(f" [{log['plugin']}] {log['action']} - {log['detail']}")
|
||||||
|
|
||||||
|
# ── 命令:alerts ──
|
||||||
|
|
||||||
|
def do_alerts(self, arg):
|
||||||
|
"""查看防篡改告警"""
|
||||||
|
if not self.plugin_mgr:
|
||||||
|
return
|
||||||
|
alerts = self.plugin_mgr.get_tamper_alerts()
|
||||||
|
if not alerts:
|
||||||
|
print("\033[1;32m无防篡改告警\033[0m")
|
||||||
|
return
|
||||||
|
print(f"\033[1;31m防篡改告警 ({len(alerts)}):\033[0m")
|
||||||
|
for alert in alerts:
|
||||||
|
print(f" [{alert['plugin']}] {alert['message']}")
|
||||||
|
|
||||||
|
# ── 命令:recover ──
|
||||||
|
|
||||||
|
def do_recover(self, arg):
|
||||||
|
"""恢复降级插件: recover <plugin_name>"""
|
||||||
|
if not arg:
|
||||||
|
print("\033[1;33m用法: recover <plugin_name>\033[0m")
|
||||||
|
return
|
||||||
|
if self.plugin_mgr.recover_plugin(arg):
|
||||||
|
print(f"\033[1;32m插件 '{arg}' 已恢复\033[0m")
|
||||||
|
else:
|
||||||
|
print(f"\033[1;31m插件 '{arg}' 恢复失败(可能未处于降级状态)\033[0m")
|
||||||
|
|
||||||
|
# ── 命令:exit ──
|
||||||
|
|
||||||
|
def do_exit(self, arg):
|
||||||
|
"""退出 REPL"""
|
||||||
|
self._save_history()
|
||||||
|
print("\033[1;33m再见!\033[0m")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def do_EOF(self, arg):
|
||||||
|
"""Ctrl+D 退出"""
|
||||||
|
return self.do_exit(arg)
|
||||||
|
|
||||||
|
def default(self, line):
|
||||||
|
"""未知命令"""
|
||||||
|
print(f"\033[1;31m未知命令: {line}\033[0m 输入 \033[1;32mhelp\033[0m 查看命令列表")
|
||||||
|
|
||||||
|
def emptyline(self):
|
||||||
|
"""空行不重复执行上一条命令"""
|
||||||
|
pass
|
||||||
@@ -41,22 +41,6 @@ class Log:
|
|||||||
def ok(cls, tag: str, msg: str):
|
def ok(cls, tag: str, msg: str):
|
||||||
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
|
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
class Logger:
|
def debug(cls, tag: str, msg: str):
|
||||||
"""日志记录器(兼容旧接口)"""
|
cls.tip(tag, msg)
|
||||||
|
|
||||||
def info(self, msg: str, **kwargs):
|
|
||||||
tag = kwargs.get("tag", "INFO")
|
|
||||||
Log.info(tag, msg)
|
|
||||||
|
|
||||||
def warn(self, msg: str, **kwargs):
|
|
||||||
tag = kwargs.get("tag", "WARN")
|
|
||||||
Log.warn(tag, msg)
|
|
||||||
|
|
||||||
def error(self, msg: str, **kwargs):
|
|
||||||
tag = kwargs.get("tag", "ERROR")
|
|
||||||
Log.error(tag, msg)
|
|
||||||
|
|
||||||
def debug(self, msg: str, **kwargs):
|
|
||||||
tag = kwargs.get("tag", "DEBUG")
|
|
||||||
Log.tip(tag, msg)
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
"""插件基础类"""
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Any, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(ABC):
|
|
||||||
"""插件基类"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def init(self, deps: Optional[dict] = None):
|
|
||||||
"""初始化插件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def start(self):
|
|
||||||
"""启动插件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def stop(self):
|
|
||||||
"""停止插件"""
|
|
||||||
pass
|
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import ast
|
||||||
|
|
||||||
|
# 启发式能力扫描:通过 AST 分析插件源码,基于命名约定和导入推断插件提供的能力
|
||||||
|
# 这是一种轻量级的静态分析,不执行任何代码,仅用于快速发现插件可能提供的能力
|
||||||
def scan_capabilities(plugin_dir):
|
def scan_capabilities(plugin_dir):
|
||||||
capabilities: set[str] = set()
|
capabilities: set[str] = set()
|
||||||
main_file = plugin_dir / "main.py"
|
main_file = plugin_dir / "main.py"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""插件管理器 - 只加载 plugin-loader,其他所有插件由 plugin-loader 插件自行管理"""
|
"""插件管理器 - 直接使用框架层的 Core Engine"""
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from oss.plugin.loader import PluginLoader
|
from oss.core.engine import PluginManager as CorePluginManager
|
||||||
|
|
||||||
# 深度隐藏的成就系统导入
|
# 深度隐藏的成就系统导入
|
||||||
try:
|
try:
|
||||||
@@ -13,32 +13,20 @@ except ImportError:
|
|||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
"""极简插件管理器
|
"""极简插件管理器
|
||||||
|
|
||||||
遵循「最小化核心框架」设计哲学:
|
直接使用框架层的 CorePluginManager(原 Core 插件功能)
|
||||||
- 核心框架只负责加载 plugin-loader 插件
|
- 不再通过插件加载器加载 Core
|
||||||
- 所有其他插件(HTTP、WebSocket、Dashboard 等)都由 plugin-loader 插件扫描和加载
|
- 所有核心功能直接集成在 oss.core.engine 中
|
||||||
- store/NebulaShell/ 是唯一的插件来源
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.loader = PluginLoader()
|
self.core = CorePluginManager()
|
||||||
self.plugin_loader: Optional[Any] = None
|
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""仅加载 plugin-loader 核心插件
|
"""加载所有插件(由 CorePluginManager 管理)"""
|
||||||
|
self.core.load_all()
|
||||||
plugin-loader 插件会负责:
|
|
||||||
1. 扫描 store/NebulaShell/ 目录
|
# 隐藏成就:深海潜水员
|
||||||
2. 加载所有启用的插件
|
|
||||||
3. 处理依赖关系
|
|
||||||
4. 执行 PL 注入机制
|
|
||||||
"""
|
|
||||||
# 只加载 plugin-loader,其他所有插件都由它来管理
|
|
||||||
pl_info = self.loader.load_core_plugin("plugin-loader")
|
|
||||||
if pl_info:
|
|
||||||
self.plugin_loader = pl_info["instance"]
|
|
||||||
|
|
||||||
# 隐藏成就:深海潜水员 - 当加载插件管理器时解锁
|
|
||||||
if _ACHIEVEMENTS_ENABLED:
|
if _ACHIEVEMENTS_ENABLED:
|
||||||
try:
|
try:
|
||||||
validator = get_validator()
|
validator = get_validator()
|
||||||
@@ -47,48 +35,48 @@ class PluginManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""启动 plugin-loader,它会初始化并启动所有其他插件"""
|
"""启动 Core,它会初始化并启动所有其他插件"""
|
||||||
import time
|
import time
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
if self.plugin_loader:
|
self.core.init_and_start_all()
|
||||||
# plugin-loader.init() 会扫描并加载 store/ 中的所有插件
|
|
||||||
self.plugin_loader.init()
|
# 启动 HTTP 服务
|
||||||
# plugin-loader.start() 会按依赖顺序启动所有插件
|
self.core.start_http_server()
|
||||||
self.plugin_loader.start()
|
|
||||||
|
# 启动防篡改监控
|
||||||
|
self.core.start_tamper_monitor()
|
||||||
|
|
||||||
# 计算启动时间并检查速度成就
|
# 计算启动时间并检查速度成就
|
||||||
elapsed_ms = (time.time() - start_time) * 1000
|
elapsed_ms = (time.time() - start_time) * 1000
|
||||||
if _ACHIEVEMENTS_ENABLED:
|
if _ACHIEVEMENTS_ENABLED:
|
||||||
try:
|
try:
|
||||||
validator = get_validator()
|
validator = get_validator()
|
||||||
validator.check_startup_speed(elapsed_ms)
|
validator.check_startup_speed(elapsed_ms)
|
||||||
|
|
||||||
# 检查插件数量成就
|
# 检查插件数量成就
|
||||||
if hasattr(self.plugin_loader, 'manager') and hasattr(self.plugin_loader.manager, 'plugins'):
|
plugin_count = len(self.core.plugins)
|
||||||
plugin_count = len(self.plugin_loader.manager.plugins)
|
validator.check_plugin_count(plugin_count)
|
||||||
validator.check_plugin_count(plugin_count)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""停止所有插件(由 plugin-loader 统一管理)"""
|
"""停止所有插件"""
|
||||||
if self.plugin_loader:
|
try:
|
||||||
|
self.core.stop_tamper_monitor()
|
||||||
|
self.core.stop_http_server()
|
||||||
|
self.core.stop_all()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("[PluginManager] 用户中断停止过程")
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# 隐藏成就:崩溃幸存者
|
||||||
|
if _ACHIEVEMENTS_ENABLED:
|
||||||
try:
|
try:
|
||||||
self.plugin_loader.stop()
|
validator = get_validator()
|
||||||
except KeyboardInterrupt:
|
validator.track_progress("session_end")
|
||||||
print("[PluginManager] 用户中断停止过程")
|
except Exception:
|
||||||
except Exception as e:
|
pass
|
||||||
import traceback
|
|
||||||
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
# 隐藏成就:崩溃幸存者 - 如果正常停止则不解锁,只有异常停止才可能解锁
|
|
||||||
# 这里我们记录停止事件,用于将来可能的连续运行成就
|
|
||||||
if _ACHIEVEMENTS_ENABLED:
|
|
||||||
try:
|
|
||||||
validator = get_validator()
|
|
||||||
# 记录会话结束
|
|
||||||
validator.track_progress("session_end")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
from typing import Callable
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
class BaseRoute:
|
class BaseRoute:
|
||||||
|
|
||||||
__slots__ = ('method', 'path', 'handler', '_pattern_parts')
|
__slots__ = ('method', 'path', 'handler', '_pattern_parts')
|
||||||
@@ -9,6 +13,16 @@ class BaseRoute:
|
|||||||
self._pattern_parts = path.strip("/").split("/") if ":" in path else None
|
self._pattern_parts = path.strip("/").split("/") if ":" in path else None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pattern_parts(pattern: str):
|
||||||
|
if ":" not in pattern:
|
||||||
|
return None
|
||||||
|
return pattern.strip("/").split("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_wildcard_param(param: str) -> bool:
|
||||||
|
return param.startswith(":") and param.endswith("*")
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def match_path(pattern: str, path: str) -> bool:
|
def match_path(pattern: str, path: str) -> bool:
|
||||||
if pattern == path:
|
if pattern == path:
|
||||||
@@ -41,12 +55,6 @@ def match_path(pattern: str, path: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _is_wildcard_param(param: str) -> bool:
|
|
||||||
if ":" not in pattern:
|
|
||||||
return None
|
|
||||||
return pattern.strip("/").split("/")
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def extract_path_params(pattern: str, path: str) -> dict[str, str]:
|
def extract_path_params(pattern: str, path: str) -> dict[str, str]:
|
||||||
params = {}
|
params = {}
|
||||||
@@ -85,9 +93,15 @@ class BaseRouter:
|
|||||||
self.routes: list[BaseRoute] = []
|
self.routes: list[BaseRoute] = []
|
||||||
|
|
||||||
def add(self, method: str, path: str, handler: Callable):
|
def add(self, method: str, path: str, handler: Callable):
|
||||||
|
self.routes.append(BaseRoute(method, path, handler))
|
||||||
|
|
||||||
|
def get(self, path: str, handler: Callable):
|
||||||
self.add("GET", path, handler)
|
self.add("GET", path, handler)
|
||||||
|
|
||||||
def post(self, path: str, handler: Callable):
|
def post(self, path: str, handler: Callable):
|
||||||
|
self.add("POST", path, handler)
|
||||||
|
|
||||||
|
def put(self, path: str, handler: Callable):
|
||||||
self.add("PUT", path, handler)
|
self.add("PUT", path, handler)
|
||||||
|
|
||||||
def delete(self, path: str, handler: Callable):
|
def delete(self, path: str, handler: Callable):
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
def register(injector):
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
current_file = Path(__file__)
|
|
||||||
plugin_dir = current_file.parent.parent
|
|
||||||
|
|
||||||
main_file = plugin_dir / "main.py"
|
|
||||||
|
|
||||||
safe_builtins_dict = {
|
|
||||||
"True": True, "False": False, "None": None,
|
|
||||||
"dict": dict, "list": list, "str": str, "int": int,
|
|
||||||
"float": float, "bool": bool, "tuple": tuple, "set": set,
|
|
||||||
"len": len, "range": range, "enumerate": enumerate,
|
|
||||||
"zip": zip, "map": map, "filter": filter,
|
|
||||||
"sorted": sorted, "reversed": reversed,
|
|
||||||
"min": min, "max": max, "sum": sum, "abs": abs,
|
|
||||||
"round": round, "isinstance": isinstance, "issubclass": issubclass,
|
|
||||||
"type": type, "id": id, "hash": hash, "repr": repr,
|
|
||||||
"print": print, "object": object, "property": property,
|
|
||||||
"staticmethod": staticmethod, "classmethod": classmethod,
|
|
||||||
"super": super, "iter": iter, "next": next,
|
|
||||||
"any": any, "all": all, "callable": callable,
|
|
||||||
"hasattr": hasattr, "getattr": getattr, "setattr": setattr,
|
|
||||||
"Exception": Exception, "BaseException": BaseException,
|
|
||||||
}
|
|
||||||
safe_globals = {
|
|
||||||
"bi": safe_builtins_dict,
|
|
||||||
"__name__": "plugin.auto-dependency",
|
|
||||||
"__package__": "plugin.auto-dependency",
|
|
||||||
"__file__": str(main_file),
|
|
||||||
"Path": Path,
|
|
||||||
}
|
|
||||||
safe_globals["__builtins__"] = safe_builtins_dict
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(main_file, "r", encoding="utf-8") as f:
|
|
||||||
source = f.read()
|
|
||||||
|
|
||||||
code = compile(source, str(main_file), "exec")
|
|
||||||
exec(code, safe_globals)
|
|
||||||
|
|
||||||
new_func = safe_globals.get("New")
|
|
||||||
if new_func and callable(new_func):
|
|
||||||
plugin_instance = new_func()
|
|
||||||
|
|
||||||
plugin_instance.init({
|
|
||||||
"scan_dirs": ["store"],
|
|
||||||
"auto_install": True
|
|
||||||
})
|
|
||||||
|
|
||||||
plugin_instance.register_pl_functions(injector)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[auto-dependency] PL 注册失败:{e}")
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# 依赖自动安装插件 (auto-dependency)
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
依赖自动安装插件是一个核心系统插件,用于扫描所有插件的声明文件,检查并自动安装系统依赖。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
1. **扫描插件声明** - 自动扫描所有插件目录下的 `manifest.json` 文件
|
|
||||||
2. **系统依赖检测** - 读取每个插件声明的系统依赖 (`system_dependencies` 字段)
|
|
||||||
3. **安装状态检查** - 检查这些系统依赖是否已在系统中安装
|
|
||||||
4. **自动安装** - 对于未安装的依赖,使用系统包管理器自动安装
|
|
||||||
5. **PL 注入接口** - 通过 PL 注入机制向插件加载器注册功能接口
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 在 manifest.json 中声明系统依赖
|
|
||||||
|
|
||||||
其他插件可以在自己的 `manifest.json` 中声明所需的系统依赖:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "my-plugin",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "MyName",
|
|
||||||
"description": "我的插件"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {}
|
|
||||||
},
|
|
||||||
"dependencies": ["plugin-loader"],
|
|
||||||
"system_dependencies": ["curl", "git", "wget"],
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 通过 PL 注入接口调用
|
|
||||||
|
|
||||||
插件加载器加载此插件后,可以通过以下 PL 注入接口进行操作:
|
|
||||||
|
|
||||||
| 接口名称 | 说明 | 参数 | 返回值 |
|
|
||||||
|---------|------|------|--------|
|
|
||||||
| `auto-dependency:scan` | 扫描所有插件的声明文件 | `scan_dir` (可选,默认 "store") | 插件信息列表 |
|
|
||||||
| `auto-dependency:check` | 检查系统依赖安装状态 | `scan_dir` (可选,默认 "store") | 检查结果字典 |
|
|
||||||
| `auto-dependency:install` | 安装缺失的系统依赖 | `scan_dir` (可选,默认 "store") | 安装结果字典 |
|
|
||||||
| `auto-dependency:info` | 获取插件系统信息 | 无 | 系统信息字典 |
|
|
||||||
|
|
||||||
### 示例代码
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 获取插件加载器中的 auto-dependency 功能
|
|
||||||
injector = get_pl_injector() # 从插件加载器获取
|
|
||||||
|
|
||||||
# 扫描所有插件的系统依赖声明
|
|
||||||
plugins = injector.get_injected_functions("auto-dependency:scan")[0]()
|
|
||||||
print(f"找到 {len(plugins)} 个插件")
|
|
||||||
|
|
||||||
# 检查依赖安装状态
|
|
||||||
result = injector.get_injected_functions("auto-dependency:check")[0]()
|
|
||||||
print(f"已安装:{result['installed_count']}, 缺失:{result['missing_count']}")
|
|
||||||
|
|
||||||
# 安装缺失的依赖
|
|
||||||
install_result = injector.get_injected_functions("auto-dependency:install")[0]()
|
|
||||||
print(f"成功安装:{install_result['success_count']}, 失败:{install_result['failed_count']}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 支持的包管理器
|
|
||||||
|
|
||||||
插件自动检测系统使用的包管理器,支持:
|
|
||||||
|
|
||||||
- **Debian/Ubuntu**: apt-get, apt
|
|
||||||
- **RHEL/CentOS**: yum, dnf
|
|
||||||
- **Arch Linux**: pacman
|
|
||||||
- **macOS**: brew
|
|
||||||
- **Alpine Linux**: apk
|
|
||||||
|
|
||||||
## 配置选项
|
|
||||||
|
|
||||||
在 `manifest.json` 的 `config.args` 中可以配置:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"scan_dirs": ["store"],
|
|
||||||
"package_manager": "auto",
|
|
||||||
"auto_install": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| 配置项 | 说明 | 默认值 |
|
|
||||||
|-------|------|--------|
|
|
||||||
| `scan_dirs` | 要扫描的目录列表 | `["store"]` |
|
|
||||||
| `package_manager` | 包管理器(auto 为自动检测) | `"auto"` |
|
|
||||||
| `auto_install` | 是否自动安装缺失的依赖 | `true` |
|
|
||||||
|
|
||||||
## 安全说明
|
|
||||||
|
|
||||||
- 插件需要 `*` 权限才能执行系统命令安装包
|
|
||||||
- 包安装操作有超时限制(300 秒)
|
|
||||||
- 所有安装操作都会记录日志
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
store/NebulaShell/auto-dependency/
|
|
||||||
├── manifest.json # 插件清单
|
|
||||||
├── main.py # 主逻辑实现
|
|
||||||
├── PL/
|
|
||||||
│ └── main.py # PL 注入入口
|
|
||||||
└── README.md # 本文档
|
|
||||||
```
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import shutil
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Optional, List, Dict
|
|
||||||
from oss.plugin.types import Plugin
|
|
||||||
|
|
||||||
|
|
||||||
class SystemDependencyChecker:
|
|
||||||
def __init__(self, package_managers=None):
|
|
||||||
self.package_managers = package_managers or {}
|
|
||||||
self.detected_pm = self._detect_package_manager()
|
|
||||||
|
|
||||||
def _detect_package_manager(self):
|
|
||||||
for pm, commands in self.package_managers.items():
|
|
||||||
for cmd in commands:
|
|
||||||
if shutil.which(cmd):
|
|
||||||
return pm
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
def check_command(self, command: str) -> bool:
|
|
||||||
if not self.detected_pm or self.detected_pm == "unknown":
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.detected_pm in ["apt", "apt-get"]:
|
|
||||||
result = subprocess.run(
|
|
||||||
["dpkg", "-l", package],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
return result.returncode == 0 and "ii" in result.stdout
|
|
||||||
elif self.detected_pm in ["yum", "dnf"]:
|
|
||||||
result = subprocess.run(
|
|
||||||
["rpm", "-q", package],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
elif self.detected_pm == "pacman":
|
|
||||||
result = subprocess.run(
|
|
||||||
["pacman", "-Q", package],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
elif self.detected_pm == "brew":
|
|
||||||
result = subprocess.run(
|
|
||||||
["brew", "list", package],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
elif self.detected_pm == "apk":
|
|
||||||
result = subprocess.run(
|
|
||||||
["apk", "info", "-e", package],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
return result.returncode == 0
|
|
||||||
except Exception as e:
|
|
||||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
def install_package(self, package: str) -> bool:
|
|
||||||
result = {
|
|
||||||
"package": package,
|
|
||||||
"installed": self.check_package(package),
|
|
||||||
"action": "none",
|
|
||||||
"success": True,
|
|
||||||
"message": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if result["installed"]:
|
|
||||||
result["message"] = f"包 '{package}' 已安装"
|
|
||||||
return result
|
|
||||||
|
|
||||||
if not auto_install:
|
|
||||||
result["action"] = "skipped"
|
|
||||||
result["message"] = f"包 '{package}' 未安装,但自动安装已禁用"
|
|
||||||
result["success"] = False
|
|
||||||
return result
|
|
||||||
|
|
||||||
result["action"] = "installing"
|
|
||||||
if self.install_package(package):
|
|
||||||
result["installed"] = True
|
|
||||||
result["success"] = True
|
|
||||||
result["message"] = f"包 '{package}' 安装成功"
|
|
||||||
else:
|
|
||||||
result["success"] = False
|
|
||||||
result["message"] = f"包 '{package}' 安装失败"
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class AutoDependencyPlugin(Plugin):
|
|
||||||
def __init__(self):
|
|
||||||
self._plugin_loader_ref = None
|
|
||||||
self.scan_dirs = ["store"]
|
|
||||||
self.auto_install = True
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
self._plugin_loader_ref = None
|
|
||||||
if not self._plugin_loader_ref:
|
|
||||||
try:
|
|
||||||
from store.NebulaShell.plugin_bridge.main import use
|
|
||||||
self._plugin_loader_ref = use("plugin-loader")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if not self._plugin_loader_ref and deps:
|
|
||||||
self.scan_dirs = deps.get("scan_dirs", ["store"])
|
|
||||||
self.auto_install = deps.get("auto_install", True)
|
|
||||||
|
|
||||||
if "plugin-loader" in deps:
|
|
||||||
self._plugin_loader_ref = deps["plugin-loader"]
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def scan_plugin_manifests(self, base_dir: str = "store") -> List[Dict[str, Any]]:
|
|
||||||
results = []
|
|
||||||
base_path = Path(base_dir)
|
|
||||||
|
|
||||||
if not base_path.exists():
|
|
||||||
return results
|
|
||||||
|
|
||||||
for vendor_dir in base_path.iterdir():
|
|
||||||
if not vendor_dir.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
for plugin_dir in vendor_dir.iterdir():
|
|
||||||
if not plugin_dir.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
manifest_file = plugin_dir / "manifest.json"
|
|
||||||
if not manifest_file.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(manifest_file, "r", encoding="utf-8") as f:
|
|
||||||
manifest = json.load(f)
|
|
||||||
|
|
||||||
system_deps = manifest.get("system_dependencies", [])
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
"plugin_name": plugin_dir.name.rstrip("}"),
|
|
||||||
"plugin_dir": str(plugin_dir),
|
|
||||||
"manifest": manifest,
|
|
||||||
"system_dependencies": system_deps
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
|
||||||
continue
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def check_all_dependencies(self, base_dir: str = "store") -> Dict[str, Any]:
|
|
||||||
plugins = self.scan_plugin_manifests(base_dir)
|
|
||||||
|
|
||||||
all_deps = {}
|
|
||||||
for plugin in plugins:
|
|
||||||
for dep in plugin["system_dependencies"]:
|
|
||||||
if dep not in all_deps:
|
|
||||||
all_deps[dep] = []
|
|
||||||
all_deps[dep].append(plugin["plugin_name"])
|
|
||||||
|
|
||||||
results = []
|
|
||||||
installed_count = 0
|
|
||||||
missing_count = 0
|
|
||||||
|
|
||||||
for package, plugin_names in all_deps.items():
|
|
||||||
is_installed = self.checker.check_package(package)
|
|
||||||
if is_installed:
|
|
||||||
installed_count += 1
|
|
||||||
else:
|
|
||||||
missing_count += 1
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
"package": package,
|
|
||||||
"installed": is_installed,
|
|
||||||
"required_by": plugin_names
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_plugins": len(plugins),
|
|
||||||
"plugins_with_deps": sum(1 for p in plugins if p["system_dependencies"]),
|
|
||||||
"dependencies": results,
|
|
||||||
"missing_count": missing_count,
|
|
||||||
"installed_count": installed_count
|
|
||||||
}
|
|
||||||
|
|
||||||
def install_missing_dependencies(self, base_dir: str = "store") -> Dict[str, Any]:
|
|
||||||
check_result = self.check_all_dependencies(base_dir)
|
|
||||||
|
|
||||||
to_install = [dep for dep in check_result["dependencies"] if not dep["installed"]]
|
|
||||||
|
|
||||||
install_results = []
|
|
||||||
success_count = 0
|
|
||||||
failed_count = 0
|
|
||||||
|
|
||||||
for dep in to_install:
|
|
||||||
result = self.checker.check_and_install(dep["package"], auto_install=True)
|
|
||||||
result["required_by"] = dep["required_by"]
|
|
||||||
install_results.append(result)
|
|
||||||
|
|
||||||
if result["success"]:
|
|
||||||
success_count += 1
|
|
||||||
else:
|
|
||||||
failed_count += 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_to_install": len(to_install),
|
|
||||||
"success_count": success_count,
|
|
||||||
"failed_count": failed_count,
|
|
||||||
"results": install_results
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_system_info(self) -> Dict[str, Any]:
|
|
||||||
return {
|
|
||||||
"scan_dirs": self.scan_dirs,
|
|
||||||
"auto_install": self.auto_install
|
|
||||||
}
|
|
||||||
|
|
||||||
def register_services(self, injector):
|
|
||||||
def scan_deps(scan_dir: str = "store") -> Dict[str, Any]:
|
|
||||||
return self.check_all_dependencies(scan_dir)
|
|
||||||
|
|
||||||
injector.register_function(
|
|
||||||
"auto-dependency:scan",
|
|
||||||
scan_deps,
|
|
||||||
"scan all plugin system dependencies"
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_deps(scan_dir: str = "store") -> Dict[str, Any]:
|
|
||||||
return self.check_all_dependencies(scan_dir)
|
|
||||||
|
|
||||||
injector.register_function(
|
|
||||||
"auto-dependency:check",
|
|
||||||
check_deps,
|
|
||||||
"check if all declared system deps are installed"
|
|
||||||
)
|
|
||||||
|
|
||||||
def install_deps(scan_dir: str = "store") -> Dict[str, Any]:
|
|
||||||
return self.install_missing_dependencies(scan_dir)
|
|
||||||
|
|
||||||
injector.register_function(
|
|
||||||
"auto-dependency:install",
|
|
||||||
install_deps,
|
|
||||||
"install missing system dependencies"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_info() -> Dict[str, Any]:
|
|
||||||
return self.get_system_info()
|
|
||||||
|
|
||||||
injector.register_function(
|
|
||||||
"auto-dependency:info",
|
|
||||||
get_info,
|
|
||||||
"get auto-dependency plugin system info"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def New() -> AutoDependencyPlugin:
|
|
||||||
return AutoDependencyPlugin()
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "auto-dependency",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "依赖自动安装插件 - 扫描所有插件的声明文件,检查并安装系统依赖",
|
|
||||||
"type": "core"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"scan_dirs": ["store"],
|
|
||||||
"package_manager": "auto",
|
|
||||||
"auto_install": true,
|
|
||||||
"pl_injection": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["plugin-loader"],
|
|
||||||
"permissions": ["*"]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "BRVmR6gX5do7yBsBCtR9jk5/YoE6igio8d3IVNxAtwAtkBdS2Z3LNv9VwMBXeqOE84Dz1+/ypkQO+rdh9VZpGOpAPGxjCyArff9oS3nW6gazMZdLfMKrtsHxVBAL4Ycjb1NmQ3W0kdZa/aS+r2Q/tqVMJ62bqVR5Lbrc2H8eG/i1gPZsEu5tA7KC9pB8oDfaAY/QxeDczg32zWqh9UDD59Hp7TQMZhsWXsH9FgfvKjYKjcsQUEXs6ijUJ6PxHuc2Jx71xhD/IXseOTmnDCMe+8JdPA5aaVN/TEgmT99RXv62wHR+tulyaCYRd/P3sTItSSb1UYfLqEGBumetNAAGdgf33DMijUHKvufuha0JNOm6CCk+8UGbnYnG79HyaBz+pWfiF/pFX+LV7HTJTkBwQc3vXcvXep25UDspSkL+x2w3f1mk9S/oA5mT2go4kSaORxkCb1fAbh74Bn51VRmQV8XLSUOoZvWHjiaMkMdLsyPyTi2+fxqrDD7ehgeQBp3cNSoiGViqYcFcg2xCuHo2P/W441cZMOscfawdLJxg3N4+UC41LTooXN1+IBWzG7jrGTLyeXAFxGeOBo165WoAnsQZ9hh+uj/plv+LIU/mmOBSpJZIb4SuVJfoEcIDGpa7iieVr//8cTnbNTt9zh3GWYuW1NPIm+/WT4YoPfeAs/M=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775964953.1082504,
|
|
||||||
"plugin_hash": "8894b78ac59c0154acaeb9a976f80588ece406e55079ca633c3b2bd839098d40",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
class QualityCheck:
|
|
||||||
def check(self, filepath: str, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ast.parse(content)
|
|
||||||
for node in ast.walk(tree):
|
|
||||||
if isinstance(node, ast.FunctionDef):
|
|
||||||
lines = node.end_lineno - node.lineno
|
|
||||||
if lines > 100:
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": node.lineno,
|
|
||||||
"severity": "warning",
|
|
||||||
"type": "long_function",
|
|
||||||
"message": f"函数 {node.name} 过长 ({lines} 行)"
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def _check_parameter_count(self, filepath: str, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ast.parse(content)
|
|
||||||
for node in ast.walk(tree):
|
|
||||||
if isinstance(node, ast.FunctionDef):
|
|
||||||
complexity = self._calculate_complexity(node)
|
|
||||||
if complexity > 10:
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": node.lineno,
|
|
||||||
"severity": "warning",
|
|
||||||
"type": "high_complexity",
|
|
||||||
"message": f"函数 {node.name} 复杂度过高 (圈复杂度: {complexity})"
|
|
||||||
})
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def _calculate_complexity(self, node: ast.AST) -> int:
|
|
||||||
"""计算圈复杂度"""
|
|
||||||
complexity = 1
|
|
||||||
for child in ast.walk(node):
|
|
||||||
if isinstance(child, (ast.If, ast.While, ast.For, ast.AsyncFor)):
|
|
||||||
complexity += 1
|
|
||||||
elif isinstance(child, ast.ExceptHandler):
|
|
||||||
complexity += 1
|
|
||||||
elif isinstance(child, ast.BoolOp):
|
|
||||||
complexity += len(child.values) - 1
|
|
||||||
elif isinstance(child, (ast.And, ast.Or)):
|
|
||||||
complexity += 1
|
|
||||||
return complexity
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import ast
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class ReferenceCheck:
|
|
||||||
STD_MODULES = {
|
|
||||||
'os', 'sys', 'json', 're', 'time', 'datetime', 'pathlib',
|
|
||||||
'typing', 'collections', 'functools', 'itertools', 'io',
|
|
||||||
'string', 'math', 'random', 'hashlib', 'hmac', 'secrets',
|
|
||||||
'urllib', 'http', 'email', 'html', 'xml', 'csv', 'configparser',
|
|
||||||
'logging', 'warnings', 'traceback', 'inspect', 'importlib',
|
|
||||||
'threading', 'multiprocessing', 'subprocess', 'socket',
|
|
||||||
'asyncio', 'concurrent', 'queue', 'contextlib', 'abc',
|
|
||||||
'enum', 'dataclasses', 'copy', 'pprint', 'textwrap',
|
|
||||||
'struct', 'codecs', 'locale', 'gettext', 'argparse',
|
|
||||||
'unittest', 'doctest', 'pdb', 'profile', 'timeit',
|
|
||||||
'tempfile', 'glob', 'fnmatch', 'stat', 'fileinput',
|
|
||||||
'shutil', 'pickle', 'shelve', 'sqlite3', 'dbm',
|
|
||||||
'gzip', 'bz2', 'lzma', 'zipfile', 'tarfile',
|
|
||||||
'base64', 'binascii', 'quopri', 'uu',
|
|
||||||
}
|
|
||||||
|
|
||||||
BUILTINS = {
|
|
||||||
'print', 'len', 'str', 'int', 'float', 'bool', 'list', 'dict',
|
|
||||||
'set', 'tuple', 'range', 'enumerate', 'zip', 'map', 'filter',
|
|
||||||
'sorted', 'reversed', 'min', 'max', 'sum', 'abs', 'round',
|
|
||||||
'isinstance', 'issubclass', 'type', 'id', 'hash', 'repr',
|
|
||||||
'True', 'False', 'None', 'Exception', 'ValueError', 'TypeError',
|
|
||||||
'KeyError', 'AttributeError', 'ImportError', 'FileNotFoundError',
|
|
||||||
'IndexError', 'RuntimeError', 'StopIteration', 'GeneratorExit',
|
|
||||||
'staticmethod', 'classmethod', 'property', 'super',
|
|
||||||
'open', 'input', 'format', 'hex', 'oct', 'bin', 'chr', 'ord',
|
|
||||||
'dir', 'vars', 'locals', 'globals', 'callable', 'getattr',
|
|
||||||
'setattr', 'hasattr', 'delattr', 'exec', 'eval', 'compile',
|
|
||||||
'any', 'all', 'slice', 'frozenset', 'bytearray', 'bytes',
|
|
||||||
'memoryview', 'complex', 'divmod', 'pow', 'object',
|
|
||||||
'dict', 'list', 'str', 'int', 'float', 'bool', 'set',
|
|
||||||
'tuple', 'Exception', 'ValueError', 'TypeError', 'KeyError',
|
|
||||||
'self', 'cls', 'args', 'kwargs',
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, project_root: str = "."):
|
|
||||||
self.project_root = Path(project_root)
|
|
||||||
self._available_modules = set(self.STD_MODULES)
|
|
||||||
self._scan_project_modules()
|
|
||||||
|
|
||||||
def _scan_project_modules(self):
|
|
||||||
"""扫描项目目录下的所有 Python 模块"""
|
|
||||||
store_dir = self.project_root / "store"
|
|
||||||
if not store_dir.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
for author_dir in store_dir.iterdir():
|
|
||||||
if not author_dir.is_dir():
|
|
||||||
continue
|
|
||||||
for plugin_dir in author_dir.iterdir():
|
|
||||||
if not plugin_dir.is_dir():
|
|
||||||
continue
|
|
||||||
self._scan_plugin_modules(plugin_dir, plugin_dir.name)
|
|
||||||
|
|
||||||
def _scan_plugin_modules(self, plugin_dir: Path, base_name: str):
|
|
||||||
"""扫描单个插件目录下的模块"""
|
|
||||||
if not plugin_dir.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in plugin_dir.iterdir():
|
|
||||||
if item.is_file() and item.name.endswith(".py") and item.name != "__init__.py":
|
|
||||||
module_name = item.name[:-3]
|
|
||||||
self._available_modules.add(f"{base_name}.{module_name}")
|
|
||||||
elif item.is_dir() and (item / "__init__.py").exists():
|
|
||||||
self._available_modules.add(f"{base_name}.{item.name}")
|
|
||||||
|
|
||||||
def check(self, filepath: str, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
file_path = Path(filepath)
|
|
||||||
|
|
||||||
try:
|
|
||||||
tree = ast.parse(content)
|
|
||||||
except SyntaxError:
|
|
||||||
return []
|
|
||||||
|
|
||||||
for node in ast.walk(tree):
|
|
||||||
if isinstance(node, ast.Import):
|
|
||||||
for alias in node.names:
|
|
||||||
if alias.name.startswith('oss.') or alias.name == 'oss':
|
|
||||||
continue
|
|
||||||
if alias.name in ('websockets', 'yaml', 'click'):
|
|
||||||
continue
|
|
||||||
if not self._is_module_available(alias.name, file_path):
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": node.lineno,
|
|
||||||
"severity": "critical",
|
|
||||||
"type": "import_error",
|
|
||||||
"message": f"无法导入模块: {alias.name}"
|
|
||||||
})
|
|
||||||
|
|
||||||
elif isinstance(node, ast.ImportFrom):
|
|
||||||
if node.level and node.level > 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if node.module and (node.module.startswith('oss.') or node.module == 'oss'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if node.module and node.module.split('.')[0] in ('websockets', 'yaml', 'click'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if node.module:
|
|
||||||
if not self._is_module_available(node.module, file_path):
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": node.lineno,
|
|
||||||
"severity": "critical",
|
|
||||||
"type": "import_error",
|
|
||||||
"message": f"无法导入模块: {node.module}"
|
|
||||||
})
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def _is_module_available(self, module_name: str, file_path: Optional[Path] = None) -> bool:
|
|
||||||
"""检查模块是否可用"""
|
|
||||||
if module_name in self._available_modules:
|
|
||||||
return True
|
|
||||||
|
|
||||||
base_module = module_name.split('.')[0]
|
|
||||||
if base_module in self.STD_MODULES:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if module_name.startswith('oss.') or module_name == 'oss':
|
|
||||||
return True
|
|
||||||
|
|
||||||
third_party = {'websockets', 'yaml', 'click', 'requests', 'flask', 'django', 'numpy', 'pandas'}
|
|
||||||
if module_name.split('.')[0] in third_party:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if file_path:
|
|
||||||
file_dir = file_path.parent
|
|
||||||
sibling_module = file_dir / f"{module_name}.py"
|
|
||||||
if sibling_module.exists():
|
|
||||||
return True
|
|
||||||
sibling_pkg = file_dir / module_name
|
|
||||||
if sibling_pkg.is_dir() and (sibling_pkg / "__init__.py").exists():
|
|
||||||
return True
|
|
||||||
store_dir = self.project_root / "store"
|
|
||||||
if store_dir.exists():
|
|
||||||
for author_dir in store_dir.iterdir():
|
|
||||||
if author_dir.is_dir():
|
|
||||||
for plugin_dir in author_dir.iterdir():
|
|
||||||
if plugin_dir.is_dir() and plugin_dir.name == module_name.split('.')[0]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_variable_references(self, filepath: str, tree: ast.AST, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
for node in ast.walk(tree):
|
|
||||||
if isinstance(node, ast.Attribute):
|
|
||||||
if isinstance(node.value, ast.Name):
|
|
||||||
var_name = node.value.id
|
|
||||||
if var_name in ('None', 'True', 'False'):
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": node.lineno,
|
|
||||||
"severity": "critical",
|
|
||||||
"type": "attribute_error",
|
|
||||||
"message": f"尝试访问 {var_name} 的属性: {node.attr}"
|
|
||||||
})
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def _is_name_defined(self, name: str, tree: ast.AST, line: int) -> bool:
|
|
||||||
"""检查变量名是否在 AST 中定义"""
|
|
||||||
for node in ast.walk(tree):
|
|
||||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
||||||
if node.name == name:
|
|
||||||
return True
|
|
||||||
for arg in node.args.args:
|
|
||||||
if arg.arg == name:
|
|
||||||
return True
|
|
||||||
elif isinstance(node, ast.Assign):
|
|
||||||
for target in node.targets:
|
|
||||||
if isinstance(target, ast.Name) and target.id == name:
|
|
||||||
return True
|
|
||||||
elif isinstance(node, ast.AnnAssign):
|
|
||||||
if isinstance(node.target, ast.Name) and node.target.id == name:
|
|
||||||
return True
|
|
||||||
elif isinstance(node, ast.ClassDef):
|
|
||||||
if node.name == name:
|
|
||||||
return True
|
|
||||||
elif isinstance(node, ast.With):
|
|
||||||
for item in node.items:
|
|
||||||
if item.optional_vars and isinstance(item.optional_vars, ast.Name):
|
|
||||||
if item.optional_vars.id == name:
|
|
||||||
return True
|
|
||||||
elif isinstance(node, ast.ExceptHandler):
|
|
||||||
if node.name and node.name == name:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
class SecurityCheck:
|
|
||||||
def check(self, filepath: str, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
patterns = ['password', 'secret', 'token', 'api_key', 'access_token']
|
|
||||||
|
|
||||||
for i, line in enumerate(content.split('\n'), 1):
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith('#'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
if pattern + ' = "' in line.lower() or pattern + " = '" in line.lower():
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": i,
|
|
||||||
"severity": "critical",
|
|
||||||
"type": "hardcoded_secret",
|
|
||||||
"message": f"发现硬编码密钥: {line.strip()[:50]}"
|
|
||||||
})
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def _check_dangerous_functions(self, filepath: str, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
if '../' in content and 'open(' in content:
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": 0,
|
|
||||||
"severity": "warning",
|
|
||||||
"type": "path_traversal_risk",
|
|
||||||
"message": "可能存在路径穿越漏洞"
|
|
||||||
})
|
|
||||||
|
|
||||||
return issues
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
class StyleCheck:
|
|
||||||
def check(self, filepath: str, content: str) -> list:
|
|
||||||
issues = []
|
|
||||||
|
|
||||||
for i, line in enumerate(content.split('\n'), 1):
|
|
||||||
if len(line) > 120:
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": i,
|
|
||||||
"severity": "info",
|
|
||||||
"type": "line_too_long",
|
|
||||||
"message": f"行过长 ({len(line)} 字符)"
|
|
||||||
})
|
|
||||||
|
|
||||||
return issues
|
|
||||||
|
|
||||||
def _check_blank_lines(self, filepath: str, content: str) -> list:
|
|
||||||
if content and not content.endswith('\n'):
|
|
||||||
return [{
|
|
||||||
"file": filepath,
|
|
||||||
"line": len(content.split('\n')),
|
|
||||||
"severity": "info",
|
|
||||||
"type": "missing_final_newline",
|
|
||||||
"message": "文件末尾缺少换行符"
|
|
||||||
}]
|
|
||||||
|
|
||||||
return []
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import ast
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from checks.security import SecurityCheck
|
|
||||||
from checks.quality import QualityCheck
|
|
||||||
from checks.style import StyleCheck
|
|
||||||
from checks.references import ReferenceCheck
|
|
||||||
from report.formatter import Formatter as ReportFormatter
|
|
||||||
|
|
||||||
|
|
||||||
class CodeReviewer:
|
|
||||||
def __init__(self, config: dict):
|
|
||||||
self.config = config
|
|
||||||
self.security = SecurityCheck()
|
|
||||||
self.quality = QualityCheck()
|
|
||||||
self.style = StyleCheck()
|
|
||||||
self.references = ReferenceCheck()
|
|
||||||
self.formatter = ReportFormatter(config.get("report_format", "console"))
|
|
||||||
|
|
||||||
def run_check(self, scan_dirs: list) -> dict:
|
|
||||||
issues = []
|
|
||||||
files_scanned = 0
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
exclude_patterns = self.config.get("exclude_patterns", ["__pycache__"])
|
|
||||||
max_file_size = self.config.get("max_file_size", 102400)
|
|
||||||
|
|
||||||
for scan_dir in scan_dirs:
|
|
||||||
scan_path = Path(scan_dir)
|
|
||||||
if not scan_path.exists() or not scan_path.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
for py_file in scan_path.rglob("*.py"):
|
|
||||||
# 跳过排除目录
|
|
||||||
if any(part in exclude_patterns for part in py_file.parts):
|
|
||||||
continue
|
|
||||||
|
|
||||||
filepath = str(py_file)
|
|
||||||
|
|
||||||
# 检查文件大小
|
|
||||||
if py_file.stat().st_size > max_file_size:
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": 0,
|
|
||||||
"severity": "warning",
|
|
||||||
"type": "file_too_large",
|
|
||||||
"message": f"文件过大 ({py_file.stat().st_size} 字节),跳过检查"
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filepath, 'r', encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
files_scanned += 1
|
|
||||||
issues.extend(self.security.check(filepath, content))
|
|
||||||
issues.extend(self.quality.check(filepath, content))
|
|
||||||
issues.extend(self.style.check(filepath, content))
|
|
||||||
issues.extend(self.references.check(filepath, content))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
issues.append({
|
|
||||||
"file": filepath,
|
|
||||||
"line": 0,
|
|
||||||
"severity": "error",
|
|
||||||
"type": "parse_error",
|
|
||||||
"message": f"文件解析失败: {e}"
|
|
||||||
})
|
|
||||||
|
|
||||||
scan_time = round(time.time() - start_time, 2)
|
|
||||||
return {
|
|
||||||
"files_scanned": files_scanned,
|
|
||||||
"total_issues": len(issues),
|
|
||||||
"issues": issues,
|
|
||||||
"scan_time": scan_time
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
class CodeReviewerPlugin:
|
|
||||||
def __init__(self):
|
|
||||||
self.reviewer = None
|
|
||||||
self.config = {}
|
|
||||||
|
|
||||||
def meta(self):
|
|
||||||
from oss.plugin.types import Metadata, PluginConfig, Manifest
|
|
||||||
return Manifest(
|
|
||||||
metadata=Metadata(
|
|
||||||
name="code-reviewer",
|
|
||||||
version="1.0.0",
|
|
||||||
author="NebulaShell",
|
|
||||||
description="代码审查器 - 自动扫描代码问题"
|
|
||||||
),
|
|
||||||
config=PluginConfig(
|
|
||||||
enabled=True,
|
|
||||||
args={
|
|
||||||
"scan_dirs": ["store", "oss"],
|
|
||||||
"exclude_patterns": ["__pycache__", "*.pyc"],
|
|
||||||
"max_file_size": 102400,
|
|
||||||
"report_format": "console"
|
|
||||||
}
|
|
||||||
),
|
|
||||||
dependencies=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
config = {}
|
|
||||||
if deps:
|
|
||||||
config = deps.get("config", {})
|
|
||||||
|
|
||||||
self.config = {
|
|
||||||
"scan_dirs": config.get("scan_dirs", ["store", "oss"]),
|
|
||||||
"exclude_patterns": config.get("exclude_patterns", ["__pycache__"]),
|
|
||||||
"max_file_size": config.get("max_file_size", 102400),
|
|
||||||
"report_format": config.get("report_format", "console")
|
|
||||||
}
|
|
||||||
|
|
||||||
from core.reviewer import CodeReviewer
|
|
||||||
self.reviewer = CodeReviewer(self.config)
|
|
||||||
Log.info("code-reviewer", "初始化完成")
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
Log.info("code-reviewer", "插件已启动")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
Log.error("code-reviewer", "插件已停止")
|
|
||||||
|
|
||||||
def check(self, dirs: list = None) -> dict:
|
|
||||||
if not self.reviewer:
|
|
||||||
return {"error": "code-reviewer 未初始化"}
|
|
||||||
|
|
||||||
scan_dirs = dirs if dirs else self.config.get("scan_dirs", ["store", "oss"])
|
|
||||||
result = self.reviewer.run_check(scan_dirs)
|
|
||||||
return result
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "code-reviewer",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "代码审查器 - 提供 oss check 功能,自动扫描代码问题",
|
|
||||||
"type": "tool"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"scan_dirs": ["store", "oss"],
|
|
||||||
"exclude_patterns": ["__pycache__", "*.pyc", "*.pyo"],
|
|
||||||
"max_file_size": 102400,
|
|
||||||
"report_format": "console"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": ["*"]
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
class Formatter:
|
|
||||||
def __init__(self, format_type: str = "console"):
|
|
||||||
self.format_type = format_type
|
|
||||||
|
|
||||||
def format(self, result: dict) -> str:
|
|
||||||
lines = []
|
|
||||||
lines.append("=" * 60)
|
|
||||||
lines.append("代码审查报告")
|
|
||||||
lines.append("=" * 60)
|
|
||||||
lines.append(f"扫描文件: {result['files_scanned']}")
|
|
||||||
lines.append(f"发现问题: {result['total_issues']}")
|
|
||||||
lines.append(f"扫描时间: {result['scan_time']}s")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
critical = [i for i in result['issues'] if i['severity'] == 'critical']
|
|
||||||
warning = [i for i in result['issues'] if i['severity'] == 'warning']
|
|
||||||
info = [i for i in result['issues'] if i['severity'] == 'info']
|
|
||||||
|
|
||||||
lines.append(f"🔴 严重: {len(critical)}")
|
|
||||||
lines.append(f"🟡 警告: {len(warning)}")
|
|
||||||
lines.append(f"🔵 提示: {len(info)}")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if critical:
|
|
||||||
lines.append("严重问题:")
|
|
||||||
for issue in critical:
|
|
||||||
lines.append(f" - {issue['file']}:{issue['line']} - {issue['message']}")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
if warning:
|
|
||||||
lines.append("警告:")
|
|
||||||
for issue in warning[:10]: lines.append(f" - {issue['file']}:{issue['line']} - {issue['message']}")
|
|
||||||
if len(warning) > 10:
|
|
||||||
lines.append(f" ... 还有 {len(warning) - 10} 个警告")
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
lines.append("=" * 60)
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def _format_json(self, result: dict) -> str:
|
|
||||||
"""以 JSON 格式输出审查报告"""
|
|
||||||
import json
|
|
||||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "vn4hpZQMQTX0d78Wlze2wtTHjN91qn1PIvsRTK7ZFVm8lZ3eQHrZz9X0uDWcKKjxf5FCI/UVKQOqLwYkHiGhcS7d7+v6UKKKIYph+aftHQRrEcOQtrSnrmDQrqSjEdL3mjkl0KTIwqkFySxVNn9ssmL16JCOtWpWpKU5CnKWVrbeEKvs6yZJrmVVr9C7iDGsNq0/aS3oPDI4vg1iaTYgg/2Sh1smJ0jNtE5EsCq78fcyUcSWTziwq8RnJvFsx8LP3cxacC1QuZIP3hTIrpnApAj0KqSTRDLKY7d7rsQAHgDlnbQfYVtA8x94x91R5ybeDpXwYPSwWMpb7P/7XBDJ5GKL56iFUCV0tceHNK9yyjaXdhf2oUTxfoC4ONOTnkmnP2pZ6vRLjd/0WX7qA0XUTmZtewWur1BnZeZwzOjI5K8IYCda5WKXLVyrH64XmBEAwkEu18LIO9xI+DnhbM7rR9/xO+cXHkOYtKgAJMHCzgi6o6tw/UgS9K0myoMeGg58gYaDIVbXpxpf3rHSyFQAwauI67oye7ZxNxJgKnnOtX92cpQLHDfML8psd+sAIuBazxqxe484qzF2k0F5ZZMP17V6Yd3UWUkvWMoKlktq14OwJ2Q67nrmt9OC+9Epzny4gkq/Q7ih85rGwMVxRvkKhxxLLelQLVIni363yOxn7UE=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775967256.7737296,
|
|
||||||
"plugin_hash": "68f5ab432690beef86da1c167c704fdd6b60512a359e806516dce1c6be27b9c5",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/* Dashboard 仪表盘样式 */
|
|
||||||
|
|
||||||
.dashboard {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header h2 {
|
|
||||||
font-size: 28px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-subtitle {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-icon {
|
|
||||||
font-size: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-section {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-section h3 {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-table {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"refreshInterval": {
|
|
||||||
"type": "number",
|
|
||||||
"name": "刷新间隔",
|
|
||||||
"description": "仪表盘数据自动刷新的间隔时间(秒)",
|
|
||||||
"default": 2,
|
|
||||||
"min": 1,
|
|
||||||
"max": 60,
|
|
||||||
"order": 1
|
|
||||||
},
|
|
||||||
"showDisk": {
|
|
||||||
"type": "boolean",
|
|
||||||
"name": "显示磁盘",
|
|
||||||
"description": "是否在仪表盘显示磁盘使用率",
|
|
||||||
"default": true,
|
|
||||||
"order": 2
|
|
||||||
},
|
|
||||||
"diskThreshold": {
|
|
||||||
"type": "number",
|
|
||||||
"name": "磁盘警告阈值",
|
|
||||||
"description": "磁盘使用率超过此值时显示警告颜色",
|
|
||||||
"default": 80,
|
|
||||||
"min": 50,
|
|
||||||
"max": 95,
|
|
||||||
"show_when": { "field": "showDisk", "value": true },
|
|
||||||
"order": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
class DashboardPlugin:
|
|
||||||
def __init__(self):
|
|
||||||
self.webui = None
|
|
||||||
self.views_dir = os.path.join(os.path.dirname(__file__), 'views')
|
|
||||||
self._start_time = time.time()
|
|
||||||
self._history_len = 60
|
|
||||||
self._cpu_history = deque(maxlen=self._history_len)
|
|
||||||
self._ram_history = deque(maxlen=self._history_len)
|
|
||||||
self._net_recv_history = deque(maxlen=self._history_len)
|
|
||||||
self._net_sent_history = deque(maxlen=self._history_len)
|
|
||||||
self._disk_read_history = deque(maxlen=self._history_len)
|
|
||||||
self._disk_write_history = deque(maxlen=self._history_len)
|
|
||||||
self._net_latency_history = deque(maxlen=self._history_len)
|
|
||||||
self._last_net = None
|
|
||||||
self._last_disk = None
|
|
||||||
|
|
||||||
def meta(self):
|
|
||||||
from oss.plugin.types import Metadata, PluginConfig, Manifest
|
|
||||||
return Manifest(
|
|
||||||
metadata=Metadata(
|
|
||||||
name="dashboard",
|
|
||||||
version="2.0.0",
|
|
||||||
author="NebulaShell",
|
|
||||||
description="WebUI 仪表盘"
|
|
||||||
),
|
|
||||||
config=PluginConfig(enabled=True, args={}),
|
|
||||||
dependencies=["http-api", "webui"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_webui(self, webui):
|
|
||||||
self.webui = webui
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
if not self.webui:
|
|
||||||
try:
|
|
||||||
from store.NebulaShell.plugin_bridge.main import use
|
|
||||||
self.webui = use("webui")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if self.webui:
|
|
||||||
Log.info("dashboard", "已获取 WebUI 引用")
|
|
||||||
self.webui.register_page(
|
|
||||||
path='/dashboard',
|
|
||||||
content_provider=self._render_content,
|
|
||||||
nav_item={'icon': 'ri-dashboard-line', 'text': '仪表盘'}
|
|
||||||
)
|
|
||||||
if hasattr(self.webui, 'server') and self.webui.server:
|
|
||||||
self.webui.server.router.get("/api/dashboard/stats", self._handle_stats_api)
|
|
||||||
self.webui.server.router.get("/api/dashboard/history", self._handle_history_api)
|
|
||||||
Log.info("dashboard", "已注册到 WebUI 导航")
|
|
||||||
else:
|
|
||||||
Log.warn("dashboard", "警告: 未找到 WebUI 依赖")
|
|
||||||
|
|
||||||
def _get_uptime_str(self):
|
|
||||||
try:
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(2)
|
|
||||||
start = time.time()
|
|
||||||
s.connect(('8.8.8.8', 53))
|
|
||||||
elapsed = (time.time() - start) * 1000
|
|
||||||
s.close()
|
|
||||||
return round(elapsed, 1)
|
|
||||||
except Exception as e:
|
|
||||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def _get_network_interfaces(self):
|
|
||||||
try:
|
|
||||||
interfaces = []
|
|
||||||
addrs = psutil.net_if_addrs()
|
|
||||||
stats = psutil.net_if_stats()
|
|
||||||
for name, addr_list in addrs.items():
|
|
||||||
if name == 'lo':
|
|
||||||
continue
|
|
||||||
info = {'name': name, 'ip': 'N/A', 'mac': 'N/A', 'is_up': False, 'speed': 0}
|
|
||||||
for addr in addr_list:
|
|
||||||
if addr.family == socket.AF_INET:
|
|
||||||
info['ip'] = addr.address
|
|
||||||
elif hasattr(psutil, 'AF_LINK') and addr.family == psutil.AF_LINK:
|
|
||||||
info['mac'] = addr.address
|
|
||||||
if name in stats:
|
|
||||||
info['is_up'] = stats[name].isup
|
|
||||||
info['speed'] = stats[name].speed
|
|
||||||
interfaces.append(info)
|
|
||||||
return interfaces
|
|
||||||
except Exception as e:
|
|
||||||
import traceback; print(f"[main.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _get_load_info(self):
|
|
||||||
try:
|
|
||||||
load1, load5, load15 = os.getloadavg()
|
|
||||||
return {'load1': round(load1, 2), 'load5': round(load5, 2), 'load15': round(load15, 2)}
|
|
||||||
except (OSError, AttributeError):
|
|
||||||
return {'load1': 0, 'load5': 0, 'load15': 0}
|
|
||||||
|
|
||||||
def _handle_stats_api(self, request):
|
|
||||||
try:
|
|
||||||
cpu_percent = psutil.cpu_percent(interval=0.3)
|
|
||||||
mem = psutil.virtual_memory()
|
|
||||||
disk = psutil.disk_usage('/')
|
|
||||||
net = self._get_network_stats()
|
|
||||||
disk_io = self._get_disk_io_stats()
|
|
||||||
load = self._get_load_info()
|
|
||||||
latency = self._get_network_latency()
|
|
||||||
|
|
||||||
self._cpu_history.append(round(cpu_percent, 1))
|
|
||||||
self._ram_history.append(round(mem.percent, 1))
|
|
||||||
self._net_recv_history.append(net['recv_rate'])
|
|
||||||
self._net_sent_history.append(net['sent_rate'])
|
|
||||||
self._disk_read_history.append(disk_io['read_rate'])
|
|
||||||
self._disk_write_history.append(disk_io['write_rate'])
|
|
||||||
self._net_latency_history.append(latency)
|
|
||||||
|
|
||||||
uptime_str = self._get_uptime_str()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'cpu': {'percent': round(cpu_percent, 1), 'cores': psutil.cpu_count(logical=True)},
|
|
||||||
'ram': {'percent': round(mem.percent, 1), 'used': round(mem.used / (1024**3), 1), 'total': round(mem.total / (1024**3), 1)},
|
|
||||||
'disk': {'percent': round(disk.percent, 1), 'used': round(disk.used / (1024**3), 1), 'total': round(disk.total / (1024**3), 1)},
|
|
||||||
'network': net,
|
|
||||||
'disk_io': disk_io,
|
|
||||||
'load': load,
|
|
||||||
'latency': latency,
|
|
||||||
'processes': len(psutil.pids()),
|
|
||||||
'uptime': uptime_str
|
|
||||||
}
|
|
||||||
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(data))
|
|
||||||
except Exception as e:
|
|
||||||
return Response(status=500, headers={"Content-Type": "application/json"}, body=json.dumps({'error': str(e)}))
|
|
||||||
|
|
||||||
def _handle_history_api(self, request):
|
|
||||||
try:
|
|
||||||
data = {
|
|
||||||
'cpu': list(self._cpu_history),
|
|
||||||
'ram': list(self._ram_history),
|
|
||||||
'net_recv': list(self._net_recv_history),
|
|
||||||
'net_sent': list(self._net_sent_history),
|
|
||||||
'disk_read': list(self._disk_read_history),
|
|
||||||
'disk_write': list(self._disk_write_history),
|
|
||||||
'latency': list(self._net_latency_history)
|
|
||||||
}
|
|
||||||
return Response(status=200, headers={"Content-Type": "application/json"}, body=json.dumps(data))
|
|
||||||
except Exception as e:
|
|
||||||
return Response(status=500, headers={"Content-Type": "application/json"}, body=json.dumps({'error': str(e)}))
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
Log.info("dashboard", "仪表盘已启动")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
Log.error("dashboard", "仪表盘已停止")
|
|
||||||
|
|
||||||
def _render_content(self) -> str:
|
|
||||||
html = """<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>系统仪表盘</title>
|
|
||||||
<link rel="stylesheet" href="/assets/remixicon.css">
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
|
||||||
.container { max-width: 1400px; margin: 0 auto; }
|
|
||||||
.card { background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; }
|
|
||||||
.card-title { font-size: 18px; font-weight: 600; color: #333; }
|
|
||||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; }
|
|
||||||
.stat-card { background: #fff; }
|
|
||||||
.stat-icon { width: 60px; height: 60px; margin: 0 auto 15px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 28px; color: white; }
|
|
||||||
.stat-icon.cpu { background: linear-gradient(135deg, #667eea, #764ba2); }
|
|
||||||
.stat-icon.ram { background: linear-gradient(135deg, #f093fb, #f5576c); }
|
|
||||||
.stat-icon.disk { background: linear-gradient(135deg, #4facfe, #00f2fe); }
|
|
||||||
.stat-value { font-size: 24px; font-weight: 700; color: #333; }
|
|
||||||
.stat-label { font-size: 14px; color: #666; }
|
|
||||||
.info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; }
|
|
||||||
.info-item { background: #f8f9fa; }
|
|
||||||
.info-label { font-size: 12px; color: #999; }
|
|
||||||
.info-value { font-size: 14px; color: #333; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="card-title"> 系统仪表盘</h2>
|
|
||||||
<div class="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon cpu"><i class="ri-cpu-line"></i></div>
|
|
||||||
<div class="stat-value">0%</div>
|
|
||||||
<div class="stat-label">CPU 使用率</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon ram"><i class="ri-memory-line"></i></div>
|
|
||||||
<div class="stat-value">0%</div>
|
|
||||||
<div class="stat-label">内存使用</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-icon disk"><i class="ri-hard-drive-line"></i></div>
|
|
||||||
<div class="stat-value">0%</div>
|
|
||||||
<div class="stat-label">磁盘使用</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
setTimeout(() => location.reload(), 30000);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>"""
|
|
||||||
return html
|
|
||||||
|
|
||||||
register_plugin_type("DashboardPlugin", DashboardPlugin)
|
|
||||||
|
|
||||||
|
|
||||||
def New():
|
|
||||||
return DashboardPlugin()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "dashboard",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "WebUI 仪表盘 - 系统监控/插件管理/安全配置/多语言支持",
|
|
||||||
"type": "webui-extension"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"refresh_interval": 5,
|
|
||||||
"show_system_metrics": true,
|
|
||||||
"show_plugin_status": true,
|
|
||||||
"show_security_alerts": true,
|
|
||||||
"theme": "dark"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["http-api", "webui", "i18n"],
|
|
||||||
"permissions": ["*"]
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# dependency 依赖解析
|
|
||||||
|
|
||||||
插件依赖关系管理,使用拓扑排序确定加载顺序。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- 拓扑排序(Kahn 算法)
|
|
||||||
- 循环依赖检测(DFS)
|
|
||||||
- 缺失依赖检测
|
|
||||||
- 自动按依赖顺序加载插件
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
dep = dependency_plugin
|
|
||||||
|
|
||||||
# 添加插件及其依赖
|
|
||||||
dep.add_plugin("plugin-a", ["plugin-b", "plugin-c"])
|
|
||||||
dep.add_plugin("plugin-b", [])
|
|
||||||
dep.add_plugin("plugin-c", ["plugin-b"])
|
|
||||||
|
|
||||||
# 解析依赖顺序
|
|
||||||
order = dep.resolve() # 返回 ["plugin-b", "plugin-c", "plugin-a"]
|
|
||||||
|
|
||||||
# 检查缺失依赖
|
|
||||||
missing = dep.get_missing_deps()
|
|
||||||
|
|
||||||
# 获取加载顺序
|
|
||||||
order = dep.get_order()
|
|
||||||
```
|
|
||||||
|
|
||||||
## manifest.json 声明
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"metadata": {...},
|
|
||||||
"dependencies": ["lifecycle", "circuit-breaker"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "JQaw//g6588907vGYH6SyqeXj9qHU5Azb7S/bjYm7rUrVsHqqIsIOEPB7IVsdf/wCnCdCa0LzTrEjmS6lKlEwXVjCCebhzyi64OJIXVOVckd2TJbREH0ZizO4KcEWgOqu56Ln3g8yMPHw5GylLABD5UN0q4F48PwUhram+cECu0SOY/bAHxYwi+nzJ0TcuES/J5cK480xv+NvxnylBhx1Udkkoiz9Y7b3pgglx+h57BuPEeHpJFbXQkXtty5Cf3sXzib0FEhicyIW1u5wmYSLz5yyLd/Pefavjfs6JrDG9J8gfPuestQzazQGsIMiQTy13DL8IDGAZ7AP2/mFQYrXuYLaBTxyhhMAkpfjIANzy+2pobeTZz2Cu4Sr6XMzXS4BkeCRDcHHBnttWVpp1+t5HpRgp3W8eiPcCzmUq6jo1cbd5zWGiR1gDEHePivmJaUi/bxlN0vyc7LjW7T+HuLUYhdSktbxv5BexMwcA7+2UHJzEnTVIc+xqoIT+ApPqqF2hLJFiAUdEJe8FRc/Bwihzh8tfM0xgYoqn8RQQ3eWVwVrK9vx0OZ8INumNZOyKPz8ZlGf3XAJv9UGUQ6Y42raYcDOFrgT+MS82tjAxf2nonm0/c3dhgNFZSy5Cfbvuqd9SYaxXejIcVni3MarVHZX3iKytOdv83cBtwPXRcfloc=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775969851.9656692,
|
|
||||||
"plugin_hash": "aebef3fd9252245553bc458e4652b094839a5e64bde7cec13435ba1930a8dc0d",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
class DependencyError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DependencyResolver:
|
|
||||||
def add_dependency(self, name: str, dependencies: list[str]):
|
|
||||||
self.graph[name] = dependencies
|
|
||||||
|
|
||||||
def resolve(self) -> list[str]:
|
|
||||||
self._detect_cycles()
|
|
||||||
|
|
||||||
in_degree: dict[str, int] = {name: 0 for name in self.graph}
|
|
||||||
who_depends_on: dict[str, list[str]] = {name: [] for name in self.graph}
|
|
||||||
|
|
||||||
for name, deps in self.graph.items():
|
|
||||||
for dep in deps:
|
|
||||||
if dep in in_degree:
|
|
||||||
in_degree[name] += 1
|
|
||||||
who_depends_on[dep].append(name)
|
|
||||||
queue = [name for name, degree in in_degree.items() if degree == 0]
|
|
||||||
result = []
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
node = queue.pop(0)
|
|
||||||
result.append(node)
|
|
||||||
for dependent in who_depends_on.get(node, []):
|
|
||||||
in_degree[dependent] -= 1
|
|
||||||
if in_degree[dependent] == 0:
|
|
||||||
queue.append(dependent)
|
|
||||||
|
|
||||||
if len(result) != len(self.graph):
|
|
||||||
raise DependencyError("无法解析依赖,可能存在循环依赖")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _detect_cycles(self):
|
|
||||||
all_deps = set()
|
|
||||||
for deps in self.graph.values():
|
|
||||||
all_deps.update(deps)
|
|
||||||
all_plugins = set(self.graph.keys())
|
|
||||||
return list(all_deps - all_plugins)
|
|
||||||
|
|
||||||
|
|
||||||
class DependencyPlugin(Plugin):
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def add_plugin(self, name: str, dependencies: list[str]):
|
|
||||||
return self.resolver.resolve()
|
|
||||||
|
|
||||||
def get_missing_deps(self) -> list[str]:
|
|
||||||
return self.resolve()
|
|
||||||
|
|
||||||
|
|
||||||
register_plugin_type("DependencyResolver", DependencyResolver)
|
|
||||||
register_plugin_type("DependencyError", DependencyError)
|
|
||||||
|
|
||||||
|
|
||||||
def New():
|
|
||||||
return DependencyPlugin()
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "dependency",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "依赖解析 - 拓扑排序 + 循环依赖检测",
|
|
||||||
"type": "core"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "example-with-deps",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "示例插件 - 演示如何声明系统依赖",
|
|
||||||
"type": "example"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"system_dependencies": ["curl", "git", "wget"],
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "firewall",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "防火墙服务 - 提供 IP 过滤/端口管理/访问控制/WebUI 规则配置",
|
|
||||||
"type": "security"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"default_policy": "ACCEPT",
|
|
||||||
"whitelist_enabled": false,
|
|
||||||
"blacklist_enabled": true,
|
|
||||||
"rate_limit_enabled": true,
|
|
||||||
"rate_limit_requests": 100,
|
|
||||||
"rate_limit_window": 60,
|
|
||||||
"blocked_ips_file": "config/blocked_ips.txt",
|
|
||||||
"allowed_ips_file": "config/allowed_ips.txt",
|
|
||||||
"rules_file": "config/firewall_rules.json",
|
|
||||||
"log_blocked": true,
|
|
||||||
"notify_on_block": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["http-api", "i18n"],
|
|
||||||
"permissions": ["lifecycle", "plugin-storage"]
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "frp-proxy",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "FRP 内网穿透服务 - 提供安全的内网服务暴露/反向代理/WebUI 配置管理",
|
|
||||||
"type": "service"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"server_addr": "",
|
|
||||||
"server_port": 7000,
|
|
||||||
"auth_token": "",
|
|
||||||
"tcp_mux": true,
|
|
||||||
"heartbeat_interval": 30,
|
|
||||||
"heartbeat_timeout": 90,
|
|
||||||
"admin_addr": "127.0.0.1",
|
|
||||||
"admin_port": 7400,
|
|
||||||
"log_level": "info",
|
|
||||||
"proxy_configs_dir": "config/proxies"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["http-api", "i18n"],
|
|
||||||
"permissions": ["lifecycle", "plugin-storage"]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "ftp-server",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "FTP/SFTP 文件传输服务 - 提供安全的文件上传下载/目录管理/WebUI集成",
|
|
||||||
"type": "service"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"ftp_port": 2121,
|
|
||||||
"sftp_port": 2222,
|
|
||||||
"passive_ports": [30000, 30010],
|
|
||||||
"max_connections": 50,
|
|
||||||
"timeout": 300,
|
|
||||||
"allow_anonymous": false,
|
|
||||||
"root_dir": "/workspace/ftp-root",
|
|
||||||
"chroot_enabled": true,
|
|
||||||
"ssl_enabled": true,
|
|
||||||
"ssl_cert": "config/ftp.crt",
|
|
||||||
"ssl_key": "config/ftp.key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["http-api", "i18n"],
|
|
||||||
"permissions": ["lifecycle", "plugin-storage"]
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# hot-reload 热插拔
|
|
||||||
|
|
||||||
运行时加载、卸载、更新插件,无需重启服务。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- 运行时加载新插件
|
|
||||||
- 运行时卸载插件
|
|
||||||
- 运行时更新插件(热重载)
|
|
||||||
- 自动监听文件变化(可选)
|
|
||||||
- 模块缓存清理
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# 加载新插件
|
|
||||||
hot_reload.load_plugin(Path("store/@{Author/new-plugin"))
|
|
||||||
|
|
||||||
# 卸载插件
|
|
||||||
hot_reload.unload_plugin("plugin-name")
|
|
||||||
|
|
||||||
# 更新插件
|
|
||||||
hot_reload.reload_plugin("plugin-name", Path("store/@{Author/plugin-name"))
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 插件必须实现 `init()`, `start()`, `stop()`
|
|
||||||
- 卸载时会调用 `stop()`
|
|
||||||
- 更新时先 `stop()` 再 `init()` + `start()`
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "vBf0JPwb5GjyM9vyp4AuncQKp092RpA07RZh+guhF51OKlVI5PphQEEvtMSy2uBsQ0V0RohRid/gazvB5l02DTuyqt2NcjFyPIZj2wm1gfWtJZWBK+Hp11gIPq13qhxDjdi1bs7H+tTOhVHJHkcoU1TsZuUPU+UYOuONbQhdwB+eqEMbNzVrPBPxb12W1SxRBAo/58q+eGI1QvbTv0FBu4fw10vyySGzd51t0psrBqw9xovKSq47AV96ZJeFEJvbfBTfJTg26VOX0cxLS5dmel9+yMhmidJNvOoL3mlZG2C92Xe9hdZAFxaRhMV3QgNKx3s6C+TQRBNx3ttUtBAzxVcXsGhCE0C+CfvbIpuyGHfgarSPJoiIPyp02numgMztFzAdFc66stULEpB3rHBlosUbDNmeuIMNcbCdKlH6R94xuYMg8E699DO67AGxZwZcaUN/vYmAa2DiffVUFcCFXgzABPzctJTYqTaD51KGlMSMHTeMTN3XCWJ79nkxHvt0Lgb0kWljOhcVaGW2t4JUgfupUD1DIwiZ7AlEC3K3JijsqWS633+Saa/+tOI4/V5VzVtExJt46cM/BSETYlHQtA8eDDl6BhbjtnmMaHSjGF75sgiagtj0DYsOvzKLJUVMT4nFjidzb2sR5lN3/S3ZSmBTUYA5/fDgiMnSfZaK4HQ=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775964953.0432403,
|
|
||||||
"plugin_hash": "3b226c4e5278ade1ec0997abfd553d4c07724b8e9f69f79acb57e20e0d352817",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
class HotReloadError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FileWatcher:
|
|
||||||
def __init__(self, watch_dirs, extensions, callback):
|
|
||||||
self.watch_dirs = watch_dirs
|
|
||||||
self.extensions = extensions
|
|
||||||
self.callback = callback
|
|
||||||
self._running = False
|
|
||||||
self._thread = None
|
|
||||||
self._file_times = {}
|
|
||||||
self._init_file_times()
|
|
||||||
|
|
||||||
def _init_file_times(self):
|
|
||||||
for watch_dir in self.watch_dirs:
|
|
||||||
p = Path(watch_dir)
|
|
||||||
if p.exists():
|
|
||||||
for f in p.rglob("*"):
|
|
||||||
if f.is_file() and f.suffix in self.extensions:
|
|
||||||
self._file_times[str(f)] = f.stat().st_mtime
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
self._running = False
|
|
||||||
if self._thread:
|
|
||||||
self._thread.join(timeout=5)
|
|
||||||
|
|
||||||
def _watch_loop(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HotReloadPlugin:
|
|
||||||
def __init__(self):
|
|
||||||
self.plugin_loader_instance = None
|
|
||||||
self.watcher: Optional[FileWatcher] = None
|
|
||||||
self.watch_dirs: list[str] = []
|
|
||||||
self.watch_extensions: list[str] = [".py", ".json"]
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
if not self.watch_dirs:
|
|
||||||
self.watch_dirs = ["store"]
|
|
||||||
self.start_watching()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if self.watcher:
|
|
||||||
self.watcher.stop()
|
|
||||||
|
|
||||||
def set_plugin_loader(self, plugin_loader):
|
|
||||||
self.plugin_loader_instance = plugin_loader
|
|
||||||
|
|
||||||
def set_watch_dirs(self, dirs: list[str]):
|
|
||||||
self.watch_dirs = dirs
|
|
||||||
|
|
||||||
def start_watching(self):
|
|
||||||
if self.watch_dirs and self.plugin_loader_instance:
|
|
||||||
self.watcher = FileWatcher(
|
|
||||||
self.watch_dirs,
|
|
||||||
self.watch_extensions,
|
|
||||||
self._on_file_change
|
|
||||||
)
|
|
||||||
self.watcher.start()
|
|
||||||
|
|
||||||
def _on_file_change(self, changes: list[tuple[str, Path]]):
|
|
||||||
for change_type, file_path in changes:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def load_plugin(self, plugin_dir: Path) -> bool:
|
|
||||||
try:
|
|
||||||
plugin_name = plugin_dir.name
|
|
||||||
if plugin_name in self.plugin_loader_instance.plugins:
|
|
||||||
raise HotReloadError(f"Plugin already exists: {plugin_name}")
|
|
||||||
|
|
||||||
self.plugin_loader_instance.load(plugin_dir)
|
|
||||||
info = self.plugin_loader_instance.plugins[plugin_name]
|
|
||||||
instance = info["instance"]
|
|
||||||
instance.init()
|
|
||||||
instance.start()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
raise HotReloadError(f"Failed to load plugin: {e}")
|
|
||||||
|
|
||||||
def unload_plugin(self, plugin_name: str) -> bool:
|
|
||||||
try:
|
|
||||||
self.plugin_loader_instance.unload(plugin_name)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
raise HotReloadError(f"Failed to unload plugin: {e}")
|
|
||||||
|
|
||||||
def reload_plugin(self, plugin_name: str, plugin_dir: Path) -> bool:
|
|
||||||
try:
|
|
||||||
self.unload_plugin(plugin_name)
|
|
||||||
return self.load_plugin(plugin_dir)
|
|
||||||
except Exception as e:
|
|
||||||
raise HotReloadError(f"Failed to reload plugin: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def register_plugin_type(name, cls):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def New():
|
|
||||||
return HotReloadPlugin()
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "hot-reload",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "热插拔 - 运行时加载/卸载/更新插件",
|
|
||||||
"type": "utility"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"watch_dirs": ["store"],
|
|
||||||
"watch_extensions": [".py", ".json"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": ["plugin-loader"]
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# http-api HTTP API 服务
|
|
||||||
|
|
||||||
提供 HTTP RESTful API 服务,支持路由、中间件等功能。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- HTTP 服务器(GET/POST/PUT/DELETE)
|
|
||||||
- 路由匹配(支持参数路由 `:id`)
|
|
||||||
- 中间件链(CORS/日志/限流)
|
|
||||||
- 分散式布局(每个文件 < 200 行)
|
|
||||||
|
|
||||||
## 路由使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 在插件中获取 router
|
|
||||||
http_plugin = plugin_mgr.get("http-api")
|
|
||||||
router = http_plugin.router
|
|
||||||
|
|
||||||
# 添加路由
|
|
||||||
router.get("/health", lambda req: Response(status=200, body='{"status": "ok"}'))
|
|
||||||
router.get("/api/users", handle_users)
|
|
||||||
router.post("/api/users", handle_create_user)
|
|
||||||
router.get("/api/users/:id", handle_user_by_id)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 中间件
|
|
||||||
|
|
||||||
```python
|
|
||||||
middleware = http_plugin.middleware
|
|
||||||
|
|
||||||
# 添加自定义中间件
|
|
||||||
class MyMiddleware(Middleware):
|
|
||||||
def process(self, ctx, next_fn):
|
|
||||||
# 前置处理
|
|
||||||
resp = next_fn() # 继续执行
|
|
||||||
# 后置处理
|
|
||||||
return resp
|
|
||||||
|
|
||||||
middleware.add(MyMiddleware())
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"args": {
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 8080
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "0WK7Njn0KAUP+jfg/uuJxwW0/tWCF+WieK0N0T2crWbvutKQmEOtaNDHnjT6qFz1dcI4+ba3julE4fFi3W3xFiToMEP2VcPXe0WNQ9/kvKNTKSDbwadiBssf43TO1G9E1BxNMxVM91mN8iqybuy+VMdU0Esv2rJ5dcwwwsnT9NWot2RQLez75PRhmMtJpEWRUmrZn2r+u5QnQdjxucONq9Nhwxw0eheTxMCu8IDvIiO6QIWP5ErA/wUz+Hg6IoEZwcVif/lSN2EMqNGqPNR/nIWWVXo9CXWB9qMZZApgEnAZfKYGCAkLzSTwqG64T4iJh4deGxafyMhsONckqRaG82NRTLuzHMReP5+VAichuEGbHI7nxXFOFG7q1mgQQLmHm3LB577usAgCNCh5X3i8SMAj7Sutykxhj0ZyTqMnOfpwnzE2tsNisJF0/8Kw22k7dZChV1obOeLWXjy5InLjdm4hIWTp7wMPjSNWRMZGR+1aZHi9XA1GKd965/30jmo876EXX23xoTAN4ZRhZNlcQg710LhycNohggnQ7qzB9LsV3Ckgh7aY/V/hzND6bpRADCGu62sZtBye2P1yaaAorC8+hRaiJoXlV9Yukg+3yhfKC+qTbn307fI53kgcw1KMSeGGctfTYJUOfK8u0mYsGi50bnM+2Tz45YJiwwdOJJk=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775960645.890869,
|
|
||||||
"plugin_hash": "ca13c933ffa2c5dd8874e3ad6f7b8dda5dd9a5f9c24be6aeb47228d65097a280",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
"""
|
|
||||||
CSRF 防护中间件
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
import secrets
|
|
||||||
import time
|
|
||||||
from typing import Dict, Optional
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from oss.config import get_config
|
|
||||||
from oss.logger.logger import Log
|
|
||||||
from .server import Request, Response
|
|
||||||
|
|
||||||
|
|
||||||
class CsrfTokenManager:
|
|
||||||
"""CSRF 令牌管理器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.config = get_config()
|
|
||||||
self.enabled = self.config.get("CSRF_ENABLED", True)
|
|
||||||
self.token_lifetime = self.config.get("CSRF_TOKEN_LIFETIME", 3600) # 1小时
|
|
||||||
self.tokens: Dict[str, tuple] = {} # {token: (timestamp, session_id)}
|
|
||||||
self.session_tokens: Dict[str, str] = defaultdict(str) # {session_id: token}
|
|
||||||
self.lock = None # 延迟初始化
|
|
||||||
|
|
||||||
def _init_lock(self):
|
|
||||||
"""延迟初始化锁"""
|
|
||||||
if self.lock is None:
|
|
||||||
import threading
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
def generate_token(self, session_id: str) -> str:
|
|
||||||
"""生成CSRF令牌"""
|
|
||||||
if not self.enabled:
|
|
||||||
return None
|
|
||||||
|
|
||||||
self._init_lock()
|
|
||||||
|
|
||||||
# 如果已有令牌,直接返回
|
|
||||||
if session_id in self.session_tokens:
|
|
||||||
return self.session_tokens[session_id]
|
|
||||||
|
|
||||||
# 生成新的令牌
|
|
||||||
token = secrets.token_urlsafe(32)
|
|
||||||
timestamp = time.time()
|
|
||||||
|
|
||||||
# 存储令牌
|
|
||||||
self.tokens[token] = (timestamp, session_id)
|
|
||||||
self.session_tokens[session_id] = token
|
|
||||||
|
|
||||||
return token
|
|
||||||
|
|
||||||
def validate_token(self, token: str, session_id: str) -> bool:
|
|
||||||
"""验证CSRF令牌"""
|
|
||||||
if not self.enabled:
|
|
||||||
return True
|
|
||||||
|
|
||||||
self._init_lock()
|
|
||||||
|
|
||||||
# 清理过期令牌
|
|
||||||
current_time = time.time()
|
|
||||||
expired_tokens = []
|
|
||||||
|
|
||||||
for stored_token, (timestamp, stored_session_id) in self.tokens.items():
|
|
||||||
if current_time - timestamp > self.token_lifetime:
|
|
||||||
expired_tokens.append(stored_token)
|
|
||||||
elif stored_session_id == session_id and stored_token == token:
|
|
||||||
# 令牌有效,更新时间戳
|
|
||||||
self.tokens[stored_token] = (current_time, stored_session_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 清理过期令牌
|
|
||||||
for expired_token in expired_tokens:
|
|
||||||
if expired_token in self.tokens:
|
|
||||||
del self.tokens[expired_token]
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cleanup_expired_tokens(self):
|
|
||||||
"""清理过期令牌"""
|
|
||||||
if not self.enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._init_lock()
|
|
||||||
|
|
||||||
current_time = time.time()
|
|
||||||
expired_tokens = []
|
|
||||||
|
|
||||||
for token, (timestamp, _) in self.tokens.items():
|
|
||||||
if current_time - timestamp > self.token_lifetime:
|
|
||||||
expired_tokens.append(token)
|
|
||||||
|
|
||||||
for token in expired_tokens:
|
|
||||||
if token in self.tokens:
|
|
||||||
del self.tokens[token]
|
|
||||||
|
|
||||||
|
|
||||||
class CsrfMiddleware:
|
|
||||||
"""CSRF 防护中间件"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.config = get_config()
|
|
||||||
self.enabled = self.config.get("CSRF_ENABLED", True)
|
|
||||||
self.exempt_paths = {
|
|
||||||
"/health", "/favicon.ico", "/api/status",
|
|
||||||
"/api/health", "/login", "/logout"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 初始化令牌管理器
|
|
||||||
self.token_manager = CsrfTokenManager()
|
|
||||||
|
|
||||||
def get_session_id(self, request: Request) -> str:
|
|
||||||
"""获取会话ID"""
|
|
||||||
# 从Cookie中获取会话ID
|
|
||||||
session_cookie = request.headers.get("Cookie", "")
|
|
||||||
for cookie in session_cookie.split(";"):
|
|
||||||
if "session_id" in cookie:
|
|
||||||
return cookie.split("=")[1].strip()
|
|
||||||
|
|
||||||
# 从Authorization头获取(如果使用Bearer token)
|
|
||||||
auth_header = request.headers.get("Authorization", "")
|
|
||||||
if auth_header.startswith("Bearer "):
|
|
||||||
return f"token:{auth_header[7:]}"
|
|
||||||
|
|
||||||
# 使用IP地址作为会话ID(简化实现)
|
|
||||||
return f"ip:{request.headers.get('Remote-Addr', 'unknown')}"
|
|
||||||
|
|
||||||
def create_csrf_token_response(self, session_id: str) -> Response:
|
|
||||||
"""创建CSRF令牌响应"""
|
|
||||||
token = self.token_manager.generate_token(session_id)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
status=200,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Set-Cookie": f"csrf_token={token}; Path=/; HttpOnly; SameSite=Lax"
|
|
||||||
},
|
|
||||||
body=json.dumps({
|
|
||||||
"csrf_token": token,
|
|
||||||
"message": "CSRF token generated"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
def process(self, ctx: dict, next_fn) -> Optional[Response]:
|
|
||||||
"""处理CSRF防护逻辑"""
|
|
||||||
if not self.enabled:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
request = ctx.get("request")
|
|
||||||
if not request:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 检查是否为豁免路径
|
|
||||||
if request.path in self.exempt_paths:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 只对需要保护的请求方法进行CSRF检查
|
|
||||||
if request.method not in ["POST", "PUT", "DELETE", "PATCH"]:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 获取会话ID
|
|
||||||
session_id = self.get_session_id(request)
|
|
||||||
|
|
||||||
# 获取CSRF令牌
|
|
||||||
csrf_token = None
|
|
||||||
if request.headers.get("Content-Type") == "application/json":
|
|
||||||
try:
|
|
||||||
body = json.loads(request.body)
|
|
||||||
csrf_token = body.get("csrf_token")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 从Header中获取CSRF令牌
|
|
||||||
if not csrf_token:
|
|
||||||
csrf_token = request.headers.get("X-CSRF-Token")
|
|
||||||
|
|
||||||
# 验证CSRF令牌
|
|
||||||
if not csrf_token or not self.token_manager.validate_token(csrf_token, session_id):
|
|
||||||
Log.warn("csrf", f"CSRF验证失败: {request.method} {request.path}")
|
|
||||||
return Response(
|
|
||||||
status=403,
|
|
||||||
body='{"error": "CSRF token invalid or missing", "message": "请求被拒绝,请刷新页面重试"}',
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
return next_fn()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
class ApiEvent:
|
|
||||||
type: str
|
|
||||||
request: Any = None
|
|
||||||
response: Any = None
|
|
||||||
error: Exception = None
|
|
||||||
context: dict[str, Any] = field(default_factory=dict)
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
"""
|
|
||||||
输入验证中间件
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from typing import Dict, Any, Optional, List
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from oss.config import get_config
|
|
||||||
from oss.logger.logger import Log
|
|
||||||
from .server import Request, Response
|
|
||||||
|
|
||||||
|
|
||||||
class InputValidator:
|
|
||||||
"""输入验证器"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.config = get_config()
|
|
||||||
self.enabled = self.config.get("INPUT_VALIDATION_ENABLED", True)
|
|
||||||
|
|
||||||
# 预定义的模式
|
|
||||||
self.patterns = {
|
|
||||||
"email": r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
|
||||||
"username": r'^[a-zA-Z0-9_]{3,20}$',
|
|
||||||
"password": r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$',
|
|
||||||
"api_key": r'^[a-zA-Z0-9_-]{32,}$',
|
|
||||||
"uuid": r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
||||||
"ip_address": r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
|
|
||||||
"url": r'^https?://(?:[-\w.]|(?:%[\da-fA-F]{2}))+[/\w\.-]*\??[/\w\.-=&]*$'
|
|
||||||
}
|
|
||||||
|
|
||||||
# 端点特定的验证规则
|
|
||||||
self.endpoint_rules = {
|
|
||||||
"/api/auth/login": {
|
|
||||||
"methods": ["POST"],
|
|
||||||
"required_fields": ["username", "password"],
|
|
||||||
"field_rules": {
|
|
||||||
"username": {"type": "str", "min_length": 3, "max_length": 20, "pattern": "username"},
|
|
||||||
"password": {"type": "str", "min_length": 8, "max_length": 100}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/auth/register": {
|
|
||||||
"methods": ["POST"],
|
|
||||||
"required_fields": ["username", "email", "password"],
|
|
||||||
"field_rules": {
|
|
||||||
"username": {"type": "str", "min_length": 3, "max_length": 20, "pattern": "username"},
|
|
||||||
"email": {"type": "str", "pattern": "email"},
|
|
||||||
"password": {"type": "str", "min_length": 8, "max_length": 100, "pattern": "password"}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/users": {
|
|
||||||
"methods": ["GET", "POST"],
|
|
||||||
"field_rules": {
|
|
||||||
"limit": {"type": "int", "min_value": 1, "max_value": 100},
|
|
||||||
"offset": {"type": "int", "min_value": 0},
|
|
||||||
"search": {"type": "str", "max_length": 100}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/pkg-manager/search": {
|
|
||||||
"methods": ["GET"],
|
|
||||||
"field_rules": {
|
|
||||||
"query": {"type": "str", "min_length": 1, "max_length": 100},
|
|
||||||
"limit": {"type": "int", "min_value": 1, "max_value": 50},
|
|
||||||
"page": {"type": "int", "min_value": 1}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def validate_field(self, field_name: str, value: Any, rules: Dict) -> Optional[str]:
|
|
||||||
"""验证单个字段"""
|
|
||||||
# 类型验证
|
|
||||||
if "type" in rules:
|
|
||||||
expected_type = rules["type"]
|
|
||||||
if expected_type == "str" and not isinstance(value, str):
|
|
||||||
return f"{field_name} 必须是字符串"
|
|
||||||
elif expected_type == "int" and not isinstance(value, int):
|
|
||||||
return f"{field_name} 必须是整数"
|
|
||||||
elif expected_type == "float" and not isinstance(value, (int, float)):
|
|
||||||
return f"{field_name} 必须是数字"
|
|
||||||
elif expected_type == "bool" and not isinstance(value, bool):
|
|
||||||
return f"{field_name} 必须是布尔值"
|
|
||||||
|
|
||||||
# 长度验证
|
|
||||||
if isinstance(value, str):
|
|
||||||
if "min_length" in rules and len(value) < rules["min_length"]:
|
|
||||||
return f"{field_name} 长度不能少于 {rules['min_length']} 个字符"
|
|
||||||
if "max_length" in rules and len(value) > rules["max_length"]:
|
|
||||||
return f"{field_name} 长度不能超过 {rules['max_length']} 个字符"
|
|
||||||
|
|
||||||
# 数值范围验证
|
|
||||||
if isinstance(value, (int, float)):
|
|
||||||
if "min_value" in rules and value < rules["min_value"]:
|
|
||||||
return f"{field_name} 不能小于 {rules['min_value']}"
|
|
||||||
if "max_value" in rules and value > rules["max_value"]:
|
|
||||||
return f"{field_name} 不能大于 {rules['max_value']}"
|
|
||||||
|
|
||||||
# 模式验证
|
|
||||||
if "pattern" in rules and isinstance(value, str):
|
|
||||||
pattern = self.patterns.get(rules["pattern"])
|
|
||||||
if pattern and not re.match(pattern, value):
|
|
||||||
return f"{field_name} 格式不正确"
|
|
||||||
|
|
||||||
# 枚举验证
|
|
||||||
if "choices" in rules and value not in rules["choices"]:
|
|
||||||
return f"{field_name} 必须是以下值之一: {', '.join(rules['choices'])}"
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def validate_request(self, request: Request) -> Optional[str]:
|
|
||||||
"""验证请求"""
|
|
||||||
if not self.enabled:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 检查是否有对应的验证规则
|
|
||||||
rules = None
|
|
||||||
for endpoint, rule in self.endpoint_rules.items():
|
|
||||||
if request.path.startswith(endpoint):
|
|
||||||
rules = rule
|
|
||||||
break
|
|
||||||
|
|
||||||
if not rules:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# 检查请求方法
|
|
||||||
if "methods" in rules and request.method not in rules["methods"]:
|
|
||||||
return f"不支持的请求方法: {request.method}"
|
|
||||||
|
|
||||||
# 解析请求体
|
|
||||||
body_data = {}
|
|
||||||
if request.method in ["POST", "PUT", "PATCH"] and request.body:
|
|
||||||
try:
|
|
||||||
body_data = json.loads(request.body)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return "无效的JSON格式"
|
|
||||||
|
|
||||||
# 解析查询参数
|
|
||||||
query_params = {}
|
|
||||||
if request.query:
|
|
||||||
try:
|
|
||||||
query_params = json.loads(request.query)
|
|
||||||
except:
|
|
||||||
# 如果不是JSON,按简单键值对处理
|
|
||||||
query_params = {}
|
|
||||||
|
|
||||||
# 检查必需字段
|
|
||||||
if "required_fields" in rules:
|
|
||||||
for field in rules["required_fields"]:
|
|
||||||
if field not in body_data and field not in query_params:
|
|
||||||
return f"缺少必需字段: {field}"
|
|
||||||
|
|
||||||
# 验证字段规则
|
|
||||||
if "field_rules" in rules:
|
|
||||||
all_data = {**body_data, **query_params}
|
|
||||||
|
|
||||||
for field_name, field_rules in rules["field_rules"].items():
|
|
||||||
if field_name in all_data:
|
|
||||||
error = self.validate_field(field_name, all_data[field_name], field_rules)
|
|
||||||
if error:
|
|
||||||
return error
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class InputValidationMiddleware:
|
|
||||||
"""输入验证中间件"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.config = get_config()
|
|
||||||
self.enabled = self.config.get("INPUT_VALIDATION_ENABLED", True)
|
|
||||||
self.validator = InputValidator()
|
|
||||||
|
|
||||||
# 豁免路径(不进行验证)
|
|
||||||
self.exempt_paths = {
|
|
||||||
"/health", "/favicon.ico", "/api/status",
|
|
||||||
"/api/health", "/metrics"
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_validation_error_response(self, error_message: str) -> Response:
|
|
||||||
"""创建验证错误响应"""
|
|
||||||
return Response(
|
|
||||||
status=400,
|
|
||||||
body=json.dumps({
|
|
||||||
"error": "Validation Error",
|
|
||||||
"message": error_message,
|
|
||||||
"timestamp": datetime.now().isoformat()
|
|
||||||
}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def process(self, ctx: dict, next_fn) -> Optional[Response]:
|
|
||||||
"""处理输入验证逻辑"""
|
|
||||||
if not self.enabled:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
request = ctx.get("request")
|
|
||||||
if not request:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 检查是否为豁免路径
|
|
||||||
if request.path in self.exempt_paths:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 验证请求
|
|
||||||
validation_error = self.validator.validate_request(request)
|
|
||||||
if validation_error:
|
|
||||||
Log.warn("validation", f"输入验证失败: {validation_error} ({request.method} {request.path})")
|
|
||||||
return self.create_validation_error_response(validation_error)
|
|
||||||
|
|
||||||
return next_fn()
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from oss.plugin.types import Plugin, register_plugin_type
|
|
||||||
from .server import HttpServer, Response
|
|
||||||
from .router import HttpRouter
|
|
||||||
from .middleware import MiddlewareChain
|
|
||||||
|
|
||||||
|
|
||||||
class HttpApiPlugin(Plugin):
|
|
||||||
def __init__(self):
|
|
||||||
self.server = None
|
|
||||||
self.router = HttpRouter()
|
|
||||||
self.middleware = MiddlewareChain()
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
self.server = HttpServer(
|
|
||||||
router=self.router,
|
|
||||||
middleware=self.middleware,
|
|
||||||
)
|
|
||||||
self.server.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if self.server:
|
|
||||||
self.server.stop()
|
|
||||||
return Response(
|
|
||||||
status=200,
|
|
||||||
body=json.dumps({"status": "ok", "service": "http-api"}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _server_info_handler(self, request):
|
|
||||||
return Response(
|
|
||||||
status=200,
|
|
||||||
body=json.dumps({"status": "running", "plugins_loaded": True}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
register_plugin_type("HttpApiPlugin", HttpApiPlugin)
|
|
||||||
|
|
||||||
|
|
||||||
def New():
|
|
||||||
return HttpApiPlugin()
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "http-api",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "HTTP API 服务 - 提供 RESTful API/路由功能/多语言支持/安全中间件",
|
|
||||||
"type": "protocol"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 8080,
|
|
||||||
"ssl_enabled": false,
|
|
||||||
"ssl_cert": "",
|
|
||||||
"ssl_key": "",
|
|
||||||
"cors_enabled": true,
|
|
||||||
"rate_limit_enabled": true,
|
|
||||||
"max_body_size": 10485760,
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["i18n"],
|
|
||||||
"permissions": ["lifecycle", "circuit-breaker"]
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
"""中间件链 - CORS/鉴权/日志/限流/CSRF/输入验证等"""
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from collections import deque
|
|
||||||
from typing import Callable, Optional, Any
|
|
||||||
|
|
||||||
from oss.config import get_config
|
|
||||||
from oss.logger.logger import Log
|
|
||||||
from .server import Request, Response
|
|
||||||
from .rate_limiter import RateLimitMiddleware
|
|
||||||
from .csrf_middleware import CsrfMiddleware
|
|
||||||
from .input_validation import InputValidationMiddleware
|
|
||||||
|
|
||||||
|
|
||||||
class Middleware:
|
|
||||||
"""中间件基类"""
|
|
||||||
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
|
|
||||||
class CorsMiddleware(Middleware):
|
|
||||||
"""CORS 中间件"""
|
|
||||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
|
||||||
config = get_config()
|
|
||||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
|
||||||
|
|
||||||
req = ctx.get("request")
|
|
||||||
origin = req.headers.get("Origin", "") if req else ""
|
|
||||||
|
|
||||||
# 如果没有配置允许的来源或来源为空,则不设置CORS头
|
|
||||||
if not allowed_origins or not origin:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 检查请求来源是否在允许列表中
|
|
||||||
if origin in allowed_origins or "*" in allowed_origins:
|
|
||||||
ctx["response_headers"] = {
|
|
||||||
"Access-Control-Allow-Origin": origin,
|
|
||||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
||||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
||||||
"Access-Control-Allow-Credentials": "true",
|
|
||||||
}
|
|
||||||
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
|
|
||||||
class AuthMiddleware(Middleware):
|
|
||||||
"""鉴权中间件 - Bearer Token 认证"""
|
|
||||||
_public_paths = {"/health", "/favicon.ico", "/api/status", "/api/health"}
|
|
||||||
|
|
||||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
|
||||||
config = get_config()
|
|
||||||
api_key = config.get("API_KEY")
|
|
||||||
|
|
||||||
if not api_key:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
req = ctx.get("request")
|
|
||||||
if req and req.path in self._public_paths:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
if req and req.method == "OPTIONS":
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
auth_header = req.headers.get("Authorization", "") if req else ""
|
|
||||||
token = auth_header.removeprefix("Bearer ").strip()
|
|
||||||
|
|
||||||
if token != api_key or not token:
|
|
||||||
Log.warn("auth", f"鉴权失败: {req.method} {req.path}" if req else "鉴权失败")
|
|
||||||
return Response(
|
|
||||||
status=401,
|
|
||||||
body=json.dumps({"error": "Unauthorized", "message": "需要有效的 API Key"}),
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
)
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
|
|
||||||
class LoggerMiddleware(Middleware):
|
|
||||||
"""日志中间件"""
|
|
||||||
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
|
||||||
|
|
||||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
|
||||||
req = ctx.get("request")
|
|
||||||
if req and req.path not in self._silent_paths:
|
|
||||||
Log.info("http-api", f"{req.method} {req.path}")
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
|
|
||||||
class RateLimitMiddleware(Middleware):
|
|
||||||
"""限流中间件 - 防止DoS攻击"""
|
|
||||||
def __init__(self):
|
|
||||||
self.config = get_config()
|
|
||||||
self.enabled = self.config.get("RATE_LIMIT_ENABLED", True)
|
|
||||||
|
|
||||||
# 不同端点的限流配置
|
|
||||||
self.endpoint_limits = {
|
|
||||||
"/api/dashboard/stats": {
|
|
||||||
"max_requests": 10,
|
|
||||||
"time_window": 60
|
|
||||||
},
|
|
||||||
"/api/pkg-manager/search": {
|
|
||||||
"max_requests": 50,
|
|
||||||
"time_window": 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 全局限流配置
|
|
||||||
self.global_limit = {
|
|
||||||
"max_requests": self.config.get("RATE_LIMIT_MAX_REQUESTS", 100),
|
|
||||||
"time_window": self.config.get("RATE_LIMIT_TIME_WINDOW", 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
# 请求记录
|
|
||||||
self.requests = {}
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
def _get_client_identifier(self, request: Request) -> str:
|
|
||||||
"""获取客户端标识符"""
|
|
||||||
# 优先使用IP地址
|
|
||||||
ip = request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", ""))
|
|
||||||
if not ip:
|
|
||||||
ip = request.headers.get("Remote-Addr", "unknown")
|
|
||||||
|
|
||||||
# 如果有API Key,使用Key作为标识符(更精确)
|
|
||||||
auth_header = request.headers.get("Authorization", "")
|
|
||||||
if auth_header.startswith("Bearer "):
|
|
||||||
return f"api_key:{auth_header[7:]}"
|
|
||||||
|
|
||||||
return f"ip:{ip}"
|
|
||||||
|
|
||||||
def _is_rate_limited(self, identifier: str, path: str) -> bool:
|
|
||||||
"""检查是否被限流"""
|
|
||||||
if not self.enabled:
|
|
||||||
return False
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
limit_key = f"{identifier}:{path}"
|
|
||||||
|
|
||||||
# 获取端点特定的限制
|
|
||||||
endpoint_limit = None
|
|
||||||
for endpoint, config in self.endpoint_limits.items():
|
|
||||||
if path.startswith(endpoint):
|
|
||||||
endpoint_limit = config
|
|
||||||
break
|
|
||||||
|
|
||||||
# 使用端点特定限制或全局限制
|
|
||||||
limit = endpoint_limit or self.global_limit
|
|
||||||
max_requests = limit["max_requests"]
|
|
||||||
time_window = limit["time_window"]
|
|
||||||
|
|
||||||
with self.lock:
|
|
||||||
# 清理过期的请求记录
|
|
||||||
if limit_key not in self.requests:
|
|
||||||
self.requests[limit_key] = deque()
|
|
||||||
|
|
||||||
request_times = self.requests[limit_key]
|
|
||||||
while request_times and request_times[0] <= now - time_window:
|
|
||||||
request_times.popleft()
|
|
||||||
|
|
||||||
# 检查是否超过限制
|
|
||||||
if len(request_times) >= max_requests:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 记录当前请求
|
|
||||||
request_times.append(now)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _create_rate_limit_response(self) -> Response:
|
|
||||||
"""创建限流响应"""
|
|
||||||
return Response(
|
|
||||||
status=429,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Retry-After": str(self.global_limit["time_window"]),
|
|
||||||
"X-Rate-Limit-Limit": str(self.global_limit["max_requests"]),
|
|
||||||
"X-Rate-Limit-Window": str(self.global_limit["time_window"]),
|
|
||||||
},
|
|
||||||
body='{"error": "Rate limit exceeded", "message": "请稍后再试"}'
|
|
||||||
)
|
|
||||||
|
|
||||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
|
||||||
"""处理限流逻辑"""
|
|
||||||
if not self.enabled:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
request = ctx.get("request")
|
|
||||||
if not request:
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
# 获取客户端标识符
|
|
||||||
identifier = self._get_client_identifier(request)
|
|
||||||
|
|
||||||
# 检查是否被限流
|
|
||||||
if self._is_rate_limited(identifier, request.path):
|
|
||||||
return self._create_rate_limit_response()
|
|
||||||
|
|
||||||
return next_fn()
|
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareChain:
|
|
||||||
"""中间件链"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.middlewares: list[Middleware] = []
|
|
||||||
self.add(CorsMiddleware())
|
|
||||||
self.add(AuthMiddleware())
|
|
||||||
self.add(LoggerMiddleware())
|
|
||||||
self.add(RateLimitMiddleware())
|
|
||||||
self.add(CsrfMiddleware())
|
|
||||||
self.add(InputValidationMiddleware())
|
|
||||||
|
|
||||||
def add(self, middleware: Middleware):
|
|
||||||
self.middlewares.append(middleware)
|
|
||||||
|
|
||||||
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
|
|
||||||
idx = 0
|
|
||||||
|
|
||||||
def next_fn():
|
|
||||||
nonlocal idx
|
|
||||||
if idx < len(self.middlewares):
|
|
||||||
mw = self.middlewares[idx]
|
|
||||||
idx += 1
|
|
||||||
return mw.process(ctx, next_fn)
|
|
||||||
return None
|
|
||||||
|
|
||||||
resp = next_fn()
|
|
||||||
response_headers = ctx.get("response_headers")
|
|
||||||
if response_headers:
|
|
||||||
ctx["_cors_headers"] = response_headers
|
|
||||||
return resp
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"""
|
|
||||||
限流工具 - 令牌桶限流器
|
|
||||||
"""
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from typing import Dict
|
|
||||||
from collections import defaultdict, deque
|
|
||||||
|
|
||||||
|
|
||||||
class RateLimiter:
|
|
||||||
"""令牌桶限流器"""
|
|
||||||
|
|
||||||
def __init__(self, max_requests: int = 100, time_window: int = 60):
|
|
||||||
self.max_requests = max_requests
|
|
||||||
self.time_window = time_window
|
|
||||||
self.requests: Dict[str, deque] = defaultdict(deque)
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
|
|
||||||
def is_allowed(self, identifier: str) -> bool:
|
|
||||||
"""检查是否允许请求"""
|
|
||||||
with self.lock:
|
|
||||||
now = time.time()
|
|
||||||
request_times = self.requests[identifier]
|
|
||||||
|
|
||||||
# 清理过期的请求记录
|
|
||||||
while request_times and request_times[0] <= now - self.time_window:
|
|
||||||
request_times.popleft()
|
|
||||||
|
|
||||||
# 检查是否超过限制
|
|
||||||
if len(request_times) >= self.max_requests:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 记录当前请求
|
|
||||||
request_times.append(now)
|
|
||||||
return True
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# http-tcp HTTP TCP 服务
|
|
||||||
|
|
||||||
提供基于 TCP 的 HTTP 协议实现。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- 原始 TCP HTTP 服务器
|
|
||||||
- 路由匹配
|
|
||||||
- 中间件链(日志/CORS)
|
|
||||||
- 连接管理
|
|
||||||
- 事件发布(通过 plugin-bridge)
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
tcp = plugin_mgr.get("http-tcp")
|
|
||||||
|
|
||||||
# 注册路由
|
|
||||||
tcp.router.get("/api/status", lambda req: {
|
|
||||||
"status": 200,
|
|
||||||
"headers": {"Content-Type": "application/json"},
|
|
||||||
"body": '{"status": "ok"}'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 获取客户端
|
|
||||||
clients = tcp.server.get_clients()
|
|
||||||
```
|
|
||||||
|
|
||||||
## 事件
|
|
||||||
|
|
||||||
```python
|
|
||||||
bridge = plugin_mgr.get("plugin-bridge")
|
|
||||||
bus = bridge.event_bus
|
|
||||||
|
|
||||||
bus.on("tcp.connect", lambda e: print(f"连接: {e.client.id}"))
|
|
||||||
bus.on("tcp.http.request", lambda e: print(f"请求: {e.context['request']['path']}"))
|
|
||||||
bus.on("tcp.disconnect", lambda e: print(f"断开: {e.client.id}"))
|
|
||||||
```
|
|
||||||
|
|
||||||
## 配置
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"args": {
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 8082
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "Adt4Pa7dzXVC9LuotOb2hvUREP2sQyInReCfPRVnKLuD2IB+5Uk4BSCjt5EkUUcMiEwIYoefntc1Q0f4k/OL3F4WtKFrwb4G+WJZYuwSbYZ3l4wYtivMFTuP4PjIgz1/sWUfqHdd+jwOquM9a8+uiNaxiz+Ed9UmBCqiJXjbfiP5A5RlkUGO3evwuP51dhfo3BVU+YuVWzSWfVw8Ov9Wx1V0h7fEjPPYof1d9AP+yVnfLLfBeNL1T/VlpkogllRlcqOQm5w+s17sLhR6sQEBHHTsga7Nilh8/BMmXr3vFDrtPbPsOqVGzHvYOFFJf26geFgxowPJ5YxEL9FKp9NtOp0fsDsq6f74mES9nTg7v9uImL8zzYn774fpaIfbOL2CVqsCqzW+kYhNm7fsJD8SfmhwKR8tVEsYvqUiHqpzUwX/J7soD0jlN/ttUUCZREERRKIpumHNNxkcgLuTYsloeSrG935ZOSEt6QuWSg9+dlXgdi84UmE1TbU6Q6HKExopOJitYCUM1p21G5wcFgEn+o7zdkDUdCJEliG1QeqSHdhlo/QyLuH/7mZQOMdprHabggTUrmbrES78nT10XEFWjtUfKxuzQkWwozwYPx6cBdmO4OLYJ+C5u1hwgmVm6if6IbCPm0l/NGy8NUNjH0PxDdmPaUSdnvSLLwa6fwr5/h0=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775960645.9258935,
|
|
||||||
"plugin_hash": "136d916944b4b1e37134b3b9807a8ea19fc9c4971c62d15cc11e019502de5617",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
class TcpEvent:
|
|
||||||
type: str
|
|
||||||
client: Any = None
|
|
||||||
data: bytes = b""
|
|
||||||
context: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
EVENT_CONNECT = "tcp.connect"
|
|
||||||
EVENT_DISCONNECT = "tcp.disconnect"
|
|
||||||
EVENT_DATA = "tcp.data"
|
|
||||||
EVENT_REQUEST = "tcp.http.request"
|
|
||||||
EVENT_RESPONSE = "tcp.http.response"
|
|
||||||
EVENT_ERROR = "tcp.error"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
class HttpTcpPlugin:
|
|
||||||
def __init__(self):
|
|
||||||
self.server = None
|
|
||||||
self.router = TcpRouter()
|
|
||||||
self.middleware = TcpMiddlewareChain()
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
self.server.start()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
pass
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "http-tcp",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "HTTP TCP 服务 - 基于 TCP 的 HTTP 协议实现/多语言支持",
|
|
||||||
"type": "protocol"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 8082,
|
|
||||||
"ssl_enabled": false,
|
|
||||||
"max_connections": 500,
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": ["i18n"],
|
|
||||||
"permissions": ["lifecycle"]
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
class TcpMiddleware:
|
|
||||||
def process(self, request: dict, next_fn: Callable) -> Optional[dict]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TcpCorsMiddleware(TcpMiddleware):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.middlewares: list[TcpMiddleware] = []
|
|
||||||
self.add(TcpLogMiddleware())
|
|
||||||
self.add(TcpCorsMiddleware())
|
|
||||||
|
|
||||||
def add(self, middleware: TcpMiddleware):
|
|
||||||
idx = 0
|
|
||||||
|
|
||||||
def next_fn():
|
|
||||||
nonlocal idx
|
|
||||||
if idx < len(self.middlewares):
|
|
||||||
mw = self.middlewares[idx]
|
|
||||||
idx += 1
|
|
||||||
return mw.process(request, next_fn)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return next_fn()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
class TcpRouter:
|
|
||||||
def handle(self, request: dict) -> dict:
|
|
||||||
pass
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
class TcpClient:
|
|
||||||
def __init__(self, conn: socket.socket, address: tuple):
|
|
||||||
self.conn = conn
|
|
||||||
self.address = address
|
|
||||||
self.id = f"{address[0]}:{address[1]}"
|
|
||||||
|
|
||||||
def send(self, data: bytes):
|
|
||||||
self.conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
class TcpHttpServer:
|
|
||||||
def __init__(self):
|
|
||||||
self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self._server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
self._server.bind((self.host, self.port))
|
|
||||||
self._server.listen(128)
|
|
||||||
self._running = True
|
|
||||||
self._thread = threading.Thread(target=self._accept_loop, daemon=True)
|
|
||||||
self._thread.start()
|
|
||||||
print(f"[http-tcp] 服务器启动: {self.host}:{self.port}")
|
|
||||||
|
|
||||||
def _accept_loop(self):
|
|
||||||
buffer = b""
|
|
||||||
try:
|
|
||||||
while self._running:
|
|
||||||
data = client.conn.recv(4096)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
buffer += data
|
|
||||||
|
|
||||||
if b"\r\n\r\n" in buffer:
|
|
||||||
header_end = buffer.find(b"\r\n\r\n")
|
|
||||||
header_text = buffer[:header_end].decode("utf-8", errors="replace")
|
|
||||||
|
|
||||||
content_length = 0
|
|
||||||
for line in header_text.split("\r\n")[1:]:
|
|
||||||
if line.lower().startswith("content-length:"):
|
|
||||||
content_length = int(line.split(":", 1)[1].strip())
|
|
||||||
break
|
|
||||||
|
|
||||||
body_start_pos = header_end + 4
|
|
||||||
body_received = len(buffer) - body_start_pos
|
|
||||||
|
|
||||||
if body_received < content_length:
|
|
||||||
while body_received < content_length:
|
|
||||||
remaining = content_length - body_received
|
|
||||||
chunk = client.conn.recv(min(4096, remaining))
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
buffer += chunk
|
|
||||||
body_received += len(chunk)
|
|
||||||
|
|
||||||
request = self._parse_request(buffer)
|
|
||||||
if request:
|
|
||||||
if self.event_bus:
|
|
||||||
self.event_bus.emit(TcpEvent(
|
|
||||||
type=EVENT_REQUEST,
|
|
||||||
client=client,
|
|
||||||
context={"request": request}
|
|
||||||
))
|
|
||||||
|
|
||||||
response = self.router.handle(request)
|
|
||||||
|
|
||||||
response_bytes = self._format_response(response)
|
|
||||||
client.send(response_bytes)
|
|
||||||
|
|
||||||
if self.event_bus:
|
|
||||||
self.event_bus.emit(TcpEvent(
|
|
||||||
type=EVENT_RESPONSE,
|
|
||||||
client=client,
|
|
||||||
data=response_bytes
|
|
||||||
))
|
|
||||||
|
|
||||||
buffer = b""
|
|
||||||
|
|
||||||
except ConnectionResetError:
|
|
||||||
pass
|
|
||||||
except BrokenPipeError:
|
|
||||||
pass
|
|
||||||
except OSError as e:
|
|
||||||
if self.event_bus:
|
|
||||||
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": f"OSError: {e}"}))
|
|
||||||
except Exception as e:
|
|
||||||
if self.event_bus:
|
|
||||||
self.event_bus.emit(TcpEvent(type=EVENT_ERROR, client=client, context={"error": f"{type(e).__name__}: {e}"}))
|
|
||||||
finally:
|
|
||||||
del self._clients[client.id]
|
|
||||||
client.close()
|
|
||||||
if self.event_bus:
|
|
||||||
self.event_bus.emit(TcpEvent(type=EVENT_DISCONNECT, client=client))
|
|
||||||
|
|
||||||
def _parse_request(self, data: bytes) -> Optional[dict]:
|
|
||||||
status = response.get("status", 200)
|
|
||||||
headers = response.get("headers", {})
|
|
||||||
body = response.get("body", "")
|
|
||||||
|
|
||||||
status_text = {200: "OK", 404: "Not Found", 500: "Internal Server Error"}.get(status, "OK")
|
|
||||||
|
|
||||||
response_lines = [
|
|
||||||
f"HTTP/1.1 {status} {status_text}",
|
|
||||||
]
|
|
||||||
|
|
||||||
if "Content-Type" not in headers:
|
|
||||||
headers["Content-Type"] = "text/plain; charset=utf-8"
|
|
||||||
headers["Content-Length"] = str(len(body))
|
|
||||||
|
|
||||||
for key, value in headers.items():
|
|
||||||
response_lines.append(f"{key}: {value}")
|
|
||||||
|
|
||||||
response_lines.append("")
|
|
||||||
response_lines.append(body)
|
|
||||||
|
|
||||||
return "\r\n".join(response_lines).encode("utf-8")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
return list(self._clients.values())
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "N8pwPuJxnjP/hgMG4QLYQy7Z6e1P1KctYLJYoQniALDFT1qb11RDm1w4KUbzNIY82XM56B10zYF88dTQiGMrtbgoExE0gtUvmF3THvEd+aWhQ0m5/2war2w+j02BWH0TvJqxhb5nHCyhA4CknJANWp4wZr9EPjDseb+OhXC3GECKpChVrmM9/DWM6TtjlmGol14kq+jUnrS5EWNSa1hlsLzKIrS3Jf5fLaButDUr6YuQkATRKl6F41M8+JHJwVVw5D1fRSqCZ4xFWwN90Gtdd22JFSeB9iVE2Myb3UurPzTVvJ0B/JE9yxFDhA1B7PtuF/WeWlm060QRWdlwFfO9NjUJOeOGQstn34DUG2xL/q3yF66SjnHcHs67DqVq9lCQ961jQq0QveKunV4u8uBJd4IGH4MTq5W7Be8GDgSZcll5HLG3HBL+9XYf4mJzc7dh88Y0UV+dOabD2SJCwBmMxgzDx+Dx8RwWx7b9IYZvmXz6fxtXhqfV6AFq2oY/+4Xjwn4nq7VOCgx8PxLrUvmuacmCwlar/rXuvHT0YsN/XXmJK9o/3NYsNp/go8Vm0XW0btJ+FnQw4O4OKPvSSd+Ip+tk2rLi7CuZGi0WEVp2o23gUNLXoHkKFrtms02Et6zC9AFwP2gLF+NnaMWImup54owxgDos9s6l2ejTD653rYE=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775964953.002281,
|
|
||||||
"plugin_hash": "55f90852ff6fbd82bc5a51ea4ebc2725f1316a7a5f9d423ee10a7e571aad339a",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
|
|
||||||
class I18nEngine:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._translations: dict[str, dict[str, Any]] = {}
|
|
||||||
self._current_locale: str = "zh-CN"
|
|
||||||
self._fallback_locale: str = "en-US"
|
|
||||||
self._supported_locales: list[str] = []
|
|
||||||
self._locales_dir: str = ""
|
|
||||||
|
|
||||||
def load_locales(self, locales_dir: str, locales: list[str]):
|
|
||||||
self._locales_dir = locales_dir
|
|
||||||
self._supported_locales = locales
|
|
||||||
locales_path = Path(locales_dir)
|
|
||||||
|
|
||||||
if not locales_path.exists():
|
|
||||||
locales_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
for locale in locales:
|
|
||||||
locale_file = locales_path / f"{locale}.json"
|
|
||||||
if locale_file.exists():
|
|
||||||
try:
|
|
||||||
content = locale_file.read_text(encoding="utf-8")
|
|
||||||
self._translations[locale] = json.loads(content)
|
|
||||||
except (json.JSONDecodeError, Exception) as e:
|
|
||||||
print(f"[i18n] load locale file failed {locale_file}: {e}")
|
|
||||||
self._translations[locale] = {}
|
|
||||||
|
|
||||||
def get_locale(self) -> str:
|
|
||||||
return self._current_locale
|
|
||||||
|
|
||||||
def set_locale(self, locale: str):
|
|
||||||
self._current_locale = locale
|
|
||||||
|
|
||||||
def set_fallback(self, locale: str):
|
|
||||||
self._fallback_locale = locale
|
|
||||||
|
|
||||||
def t(self, key: str, locale: str = None, **kwargs) -> str:
|
|
||||||
target_locale = locale or self._current_locale
|
|
||||||
|
|
||||||
value = self._get_nested(key, self._translations.get(target_locale, {}))
|
|
||||||
|
|
||||||
if value is None and target_locale != self._fallback_locale:
|
|
||||||
value = self._get_nested(key, self._translations.get(self._fallback_locale, {}))
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return key
|
|
||||||
|
|
||||||
return self._interpolate(value, kwargs)
|
|
||||||
|
|
||||||
def _get_nested(self, key: str, data: dict) -> Any:
|
|
||||||
parts = key.split(".")
|
|
||||||
current = data
|
|
||||||
for part in parts:
|
|
||||||
if isinstance(current, dict):
|
|
||||||
current = current.get(part)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
return current
|
|
||||||
|
|
||||||
def _interpolate(self, text: str, kwargs: dict) -> str:
|
|
||||||
result = re.sub(r'\{\{(\w+)\}\}', lambda m: str(kwargs.get(m.group(1), m.group(0))), text)
|
|
||||||
result = re.sub(r'\{(\w+)\}', lambda m: str(kwargs.get(m.group(1), m.group(0))), result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_supported_locales(self) -> list[str]:
|
|
||||||
return list(self._supported_locales)
|
|
||||||
|
|
||||||
def is_valid_locale(self, locale: str) -> bool:
|
|
||||||
return locale in self._supported_locales
|
|
||||||
|
|
||||||
def detect_locale(self, accept_language: Optional[str] = None,
|
|
||||||
query_lang: Optional[str] = None,
|
|
||||||
cookie_lang: Optional[str] = None) -> str:
|
|
||||||
if query_lang and self.is_valid_locale(query_lang):
|
|
||||||
return query_lang
|
|
||||||
|
|
||||||
if cookie_lang and self.is_valid_locale(cookie_lang):
|
|
||||||
return cookie_lang
|
|
||||||
|
|
||||||
if accept_language:
|
|
||||||
languages = []
|
|
||||||
for part in accept_language.split(","):
|
|
||||||
part = part.strip()
|
|
||||||
if ";q=" in part:
|
|
||||||
lang, q = part.split(";q=")
|
|
||||||
languages.append((lang.strip(), float(q)))
|
|
||||||
else:
|
|
||||||
languages.append((part, 1.0))
|
|
||||||
|
|
||||||
languages.sort(key=lambda x: x[1], reverse=True)
|
|
||||||
|
|
||||||
for lang, _ in languages:
|
|
||||||
if self.is_valid_locale(lang):
|
|
||||||
return lang
|
|
||||||
for supported in self._supported_locales:
|
|
||||||
if supported.startswith(lang + "-") or lang.startswith(supported.split("-")[0] + "-"):
|
|
||||||
return supported
|
|
||||||
|
|
||||||
return self._current_locale
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"success": "Success",
|
|
||||||
"error": "Error",
|
|
||||||
"not_found": "Not Found",
|
|
||||||
"forbidden": "Forbidden",
|
|
||||||
"unauthorized": "Unauthorized",
|
|
||||||
"server_error": "Internal Server Error",
|
|
||||||
"bad_request": "Bad Request",
|
|
||||||
"ok": "OK",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"save": "Save",
|
|
||||||
"delete": "Delete",
|
|
||||||
"edit": "Edit",
|
|
||||||
"create": "Create",
|
|
||||||
"search": "Search",
|
|
||||||
"loading": "Loading...",
|
|
||||||
"no_data": "No Data",
|
|
||||||
"confirm": "Confirm",
|
|
||||||
"back": "Back"
|
|
||||||
},
|
|
||||||
"health": {
|
|
||||||
"status": "Running",
|
|
||||||
"service": "Service",
|
|
||||||
"version": "Version",
|
|
||||||
"uptime": "Uptime"
|
|
||||||
},
|
|
||||||
"api": {
|
|
||||||
"welcome": "Welcome to NebulaShell API",
|
|
||||||
"docs": "API Documentation",
|
|
||||||
"rate_limit": "Rate limit exceeded, please try again later",
|
|
||||||
"invalid_request": "Invalid request parameters",
|
|
||||||
"missing_param": "Missing required parameter: {{param}}",
|
|
||||||
"invalid_param": "Invalid parameter format: {{param}}"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"400": "Bad Request",
|
|
||||||
"401": "Please login first",
|
|
||||||
"403": "You don't have permission to perform this action",
|
|
||||||
"404": "The requested resource was not found",
|
|
||||||
"500": "Internal server error, please try again later",
|
|
||||||
"502": "Bad Gateway",
|
|
||||||
"503": "Service temporarily unavailable, please try again later"
|
|
||||||
},
|
|
||||||
"plugin": {
|
|
||||||
"i18n_name": "Internationalization",
|
|
||||||
"i18n_desc": "Provides translation loading, language detection, and HTTP middleware",
|
|
||||||
"locale_changed": "Locale changed to {{locale}}",
|
|
||||||
"locale_not_supported": "Unsupported locale: {{locale}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"success": "成功",
|
|
||||||
"error": "错误",
|
|
||||||
"not_found": "未找到",
|
|
||||||
"forbidden": "禁止访问",
|
|
||||||
"unauthorized": "未授权",
|
|
||||||
"server_error": "服务器内部错误",
|
|
||||||
"bad_request": "请求格式错误",
|
|
||||||
"ok": "确定",
|
|
||||||
"cancel": "取消",
|
|
||||||
"save": "保存",
|
|
||||||
"delete": "删除",
|
|
||||||
"edit": "编辑",
|
|
||||||
"create": "创建",
|
|
||||||
"search": "搜索",
|
|
||||||
"loading": "加载中...",
|
|
||||||
"no_data": "暂无数据",
|
|
||||||
"confirm": "确认",
|
|
||||||
"back": "返回"
|
|
||||||
},
|
|
||||||
"health": {
|
|
||||||
"status": "运行正常",
|
|
||||||
"service": "服务",
|
|
||||||
"version": "版本",
|
|
||||||
"uptime": "运行时间"
|
|
||||||
},
|
|
||||||
"api": {
|
|
||||||
"welcome": "欢迎使用 NebulaShell API",
|
|
||||||
"docs": "API 文档",
|
|
||||||
"rate_limit": "请求频率过高,请稍后重试",
|
|
||||||
"invalid_request": "无效的请求参数",
|
|
||||||
"missing_param": "缺少必需参数: {{param}}",
|
|
||||||
"invalid_param": "参数格式错误: {{param}}"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"400": "请求格式错误",
|
|
||||||
"401": "请先登录",
|
|
||||||
"403": "您没有权限执行此操作",
|
|
||||||
"404": "请求的资源不存在",
|
|
||||||
"500": "服务器内部错误,请稍后重试",
|
|
||||||
"502": "网关错误",
|
|
||||||
"503": "服务暂时不可用,请稍后重试"
|
|
||||||
},
|
|
||||||
"plugin": {
|
|
||||||
"i18n_name": "国际化多语言支持",
|
|
||||||
"i18n_desc": "提供翻译加载、语言检测和 HTTP 中间件功能",
|
|
||||||
"locale_changed": "语言已切换为 {{locale}}",
|
|
||||||
"locale_not_supported": "不支持的语言: {{locale}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"success": "成功",
|
|
||||||
"error": "錯誤",
|
|
||||||
"not_found": "找不到",
|
|
||||||
"forbidden": "禁止存取",
|
|
||||||
"unauthorized": "未授權",
|
|
||||||
"server_error": "伺服器內部錯誤",
|
|
||||||
"bad_request": "請求格式錯誤",
|
|
||||||
"ok": "確定",
|
|
||||||
"cancel": "取消",
|
|
||||||
"save": "儲存",
|
|
||||||
"delete": "刪除",
|
|
||||||
"edit": "編輯",
|
|
||||||
"create": "建立",
|
|
||||||
"search": "搜尋",
|
|
||||||
"loading": "載入中...",
|
|
||||||
"no_data": "暫無資料",
|
|
||||||
"confirm": "確認",
|
|
||||||
"back": "返回"
|
|
||||||
},
|
|
||||||
"health": {
|
|
||||||
"status": "運作正常",
|
|
||||||
"service": "服務",
|
|
||||||
"version": "版本",
|
|
||||||
"uptime": "運行時間"
|
|
||||||
},
|
|
||||||
"api": {
|
|
||||||
"welcome": "歡迎使用 NebulaShell API",
|
|
||||||
"docs": "API 文件",
|
|
||||||
"rate_limit": "請求頻率過高,請稍後重試",
|
|
||||||
"invalid_request": "無效的請求參數",
|
|
||||||
"missing_param": "缺少必要參數: {{param}}",
|
|
||||||
"invalid_param": "參數格式錯誤: {{param}}"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"400": "請求格式錯誤",
|
|
||||||
"401": "請先登入",
|
|
||||||
"403": "您沒有權限執行此操作",
|
|
||||||
"404": "請求的資源不存在",
|
|
||||||
"500": "伺服器內部錯誤,請稍後重試",
|
|
||||||
"502": "閘道錯誤",
|
|
||||||
"503": "服務暫時不可用,請稍後重試"
|
|
||||||
},
|
|
||||||
"plugin": {
|
|
||||||
"i18n_name": "國際化多語言支援",
|
|
||||||
"i18n_desc": "提供翻譯載入、語言偵測和 HTTP 中介軟體功能",
|
|
||||||
"locale_changed": "語言已切換為 {{locale}}",
|
|
||||||
"locale_not_supported": "不支援的語言: {{locale}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
|
|
||||||
class I18nPlugin(Plugin):
|
|
||||||
def __init__(self):
|
|
||||||
self.engine = I18nEngine()
|
|
||||||
self.middleware_handler = None
|
|
||||||
self._http_api = None
|
|
||||||
|
|
||||||
def set_http_api(self, http_api):
|
|
||||||
self._http_api = http_api
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
|
|
||||||
加载语言文件并初始化中间件
|
|
||||||
config = {}
|
|
||||||
if deps:
|
|
||||||
config = deps.get("config", {})
|
|
||||||
|
|
||||||
default_locale = config.get("default_locale", "zh-CN")
|
|
||||||
fallback_locale = config.get("fallback_locale", "en-US")
|
|
||||||
supported_locales = config.get("supported_locales", ["zh-CN", "en-US", "zh-TW"])
|
|
||||||
locales_dir = config.get("locales_dir", "locales")
|
|
||||||
|
|
||||||
plugin_dir = Path(__file__).parent
|
|
||||||
full_locales_dir = plugin_dir / locales_dir
|
|
||||||
|
|
||||||
self.engine.set_fallback(fallback_locale)
|
|
||||||
|
|
||||||
self.engine.load_locales(str(full_locales_dir), supported_locales)
|
|
||||||
|
|
||||||
self.engine.set_locale(default_locale)
|
|
||||||
|
|
||||||
self.middleware_handler = I18nMiddleware(self.engine, config)
|
|
||||||
|
|
||||||
Log.info("i18n", f"已加载语言: {', '.join(supported_locales)}")
|
|
||||||
Log.info("i18n", f"默认语言: {default_locale}")
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
http_api = self._http_api
|
|
||||||
if not http_api:
|
|
||||||
try:
|
|
||||||
from store.NebulaShell.plugin_bridge.main import use
|
|
||||||
http_api = use("http-api")
|
|
||||||
self._http_api = http_api
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if http_api and hasattr(http_api, 'router'):
|
|
||||||
http_api.router.get("/api/i18n/locales", self._locales_handler)
|
|
||||||
http_api.router.get("/api/i18n/translate", self._translate_handler)
|
|
||||||
http_api.router.post("/api/i18n/locale", self._change_locale_handler)
|
|
||||||
Log.info("i18n", "API 路由已注册")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
return self.engine is not None
|
|
||||||
|
|
||||||
def stats(self) -> dict:
|
|
||||||
self._http_api = http_api
|
|
||||||
|
|
||||||
|
|
||||||
def _locales_handler(self, request):
|
|
||||||
# GET /api/i18n/translate?key=user.greeting&locale=en-US&name=World
|
|
||||||
from oss.plugin.types import Response
|
|
||||||
t = getattr(request, 't', self.engine.t)
|
|
||||||
|
|
||||||
query = request.path.split("?", 1)[-1] if "?" in request.path else ""
|
|
||||||
params = {}
|
|
||||||
for param in query.split("&"):
|
|
||||||
if "=" in param:
|
|
||||||
key, value = param.split("=", 1)
|
|
||||||
params[key] = value
|
|
||||||
|
|
||||||
key = params.get("key", "")
|
|
||||||
locale = params.get("locale", None)
|
|
||||||
|
|
||||||
if not key:
|
|
||||||
return Response(
|
|
||||||
status=400,
|
|
||||||
body=json.dumps({"error": t("api.missing_param", param="key")}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = t(key, locale=locale, **params)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
status=200,
|
|
||||||
body=json.dumps({
|
|
||||||
"key": key,
|
|
||||||
"locale": locale or self.engine.get_locale(),
|
|
||||||
"text": result
|
|
||||||
}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _change_locale_handler(self, request):
|
|
||||||
from oss.plugin.types import Response
|
|
||||||
t = getattr(request, 't', self.engine.t)
|
|
||||||
|
|
||||||
try:
|
|
||||||
body = json.loads(request.body) if hasattr(request, 'body') and request.body else {}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
body = {}
|
|
||||||
|
|
||||||
new_locale = body.get("locale", "")
|
|
||||||
|
|
||||||
if not new_locale:
|
|
||||||
return Response(
|
|
||||||
status=400,
|
|
||||||
body=json.dumps({"error": t("api.missing_param", param="locale")}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.engine.is_valid_locale(new_locale):
|
|
||||||
return Response(
|
|
||||||
status=400,
|
|
||||||
body=json.dumps({"error": t("plugin.locale_not_supported", locale=new_locale)}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.engine.set_locale(new_locale)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
status=200,
|
|
||||||
body=json.dumps({"message": t("plugin.locale_changed", locale=new_locale)}),
|
|
||||||
headers={"Content-Type": "application/json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
register_plugin_type("I18nPlugin", I18nPlugin)
|
|
||||||
|
|
||||||
|
|
||||||
def New():
|
|
||||||
return I18nPlugin()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "i18n",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "国际化多语言支持 - 提供翻译加载/语言切换/HTTP中间件/WebUI集成",
|
|
||||||
"type": "middleware"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {
|
|
||||||
"default_locale": "zh-CN",
|
|
||||||
"fallback_locale": "en-US",
|
|
||||||
"locales_dir": "locales",
|
|
||||||
"supported_locales": ["zh-CN", "en-US", "zh-TW", "ja-JP", "ko-KR", "fr-FR", "de-DE", "es-ES"],
|
|
||||||
"auto_detect": true,
|
|
||||||
"cookie_name": "locale",
|
|
||||||
"query_param": "lang",
|
|
||||||
"header_name": "Accept-Language"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": ["lifecycle", "http-api"]
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
class I18nMiddleware:
|
|
||||||
"""Auto-detect language and inject into request context.
|
|
||||||
|
|
||||||
Detection priority:
|
|
||||||
1. URL query param ?lang=xx
|
|
||||||
2. Cookie locale=xx
|
|
||||||
3. Accept-Language header
|
|
||||||
4. Default language
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, engine, config: dict = None):
|
|
||||||
self.engine = engine
|
|
||||||
self.cookie_name = (config or {}).get("cookie_name", "locale")
|
|
||||||
self.query_param = (config or {}).get("query_param", "lang")
|
|
||||||
|
|
||||||
def handle(self, request: dict, next_fn: Callable) -> Response:
|
|
||||||
query_lang = self._parse_query_param(request.get("query", ""))
|
|
||||||
|
|
||||||
cookie_lang = self._parse_cookie(request.get("headers", {}))
|
|
||||||
|
|
||||||
accept_language = request.get("headers", {}).get("Accept-Language",
|
|
||||||
request.get("headers", {}).get("accept-language", ""))
|
|
||||||
|
|
||||||
locale = self.engine.detect_locale(
|
|
||||||
accept_language=accept_language if accept_language else None,
|
|
||||||
query_lang=query_lang,
|
|
||||||
cookie_lang=cookie_lang
|
|
||||||
)
|
|
||||||
|
|
||||||
self.engine.set_locale(locale)
|
|
||||||
|
|
||||||
request["locale"] = locale
|
|
||||||
request["t"] = self.engine.t
|
|
||||||
response = next_fn()
|
|
||||||
|
|
||||||
if isinstance(response, Response):
|
|
||||||
response.headers["Content-Language"] = locale
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _parse_query_param(self, query_string: str) -> Optional[str]:
|
|
||||||
cookie_header = headers.get("Cookie", headers.get("cookie", ""))
|
|
||||||
if not cookie_header:
|
|
||||||
return None
|
|
||||||
|
|
||||||
cookies = {}
|
|
||||||
for cookie in cookie_header.split(";"):
|
|
||||||
if "=" in cookie:
|
|
||||||
key, value = cookie.split("=", 1)
|
|
||||||
cookies[key.strip()] = value.strip()
|
|
||||||
|
|
||||||
return cookies.get(self.cookie_name)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# json-codec JSON 编解码器
|
|
||||||
|
|
||||||
提供插件间 JSON 数据的编码、解码和验证功能。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- **JSON 编码**: Python 对象 → JSON 字符串
|
|
||||||
- **JSON 解码**: JSON 字符串 → Python 对象
|
|
||||||
- **Schema 验证**: 验证 JSON 数据结构
|
|
||||||
- **自定义类型**: 支持注册自定义类型编解码器
|
|
||||||
|
|
||||||
## 基本使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
codec = plugin_mgr.get("json-codec")
|
|
||||||
|
|
||||||
# 编码
|
|
||||||
data = {"name": "test", "count": 42}
|
|
||||||
json_str = codec.encode(data)
|
|
||||||
# '{"name": "test", "count": 42}'
|
|
||||||
|
|
||||||
# 编码(格式化)
|
|
||||||
json_pretty = codec.encode(data, pretty=True)
|
|
||||||
# '{\n "name": "test",\n "count": 42\n}'
|
|
||||||
|
|
||||||
# 解码
|
|
||||||
parsed = codec.decode(json_str)
|
|
||||||
# {"name": "test", "count": 42}
|
|
||||||
```
|
|
||||||
|
|
||||||
## HTTP 响应处理
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 在 http-api 插件中使用
|
|
||||||
router.get("/api/users", lambda req: Response(
|
|
||||||
status=200,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
body=codec.encode({"users": [...]})
|
|
||||||
))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Schema 验证
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 注册 schema
|
|
||||||
codec.register_schema("user", {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["name", "email"],
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"},
|
|
||||||
"email": {"type": "string"},
|
|
||||||
"age": {"type": "number"}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 验证数据
|
|
||||||
user_data = {"name": "test", "email": "test@example.com"}
|
|
||||||
is_valid = codec.validate(user_data, "user")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 自定义类型
|
|
||||||
|
|
||||||
```python
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# 注册自定义编码器
|
|
||||||
codec.serializer.register_encoder(datetime, lambda dt: dt.isoformat())
|
|
||||||
|
|
||||||
# 使用
|
|
||||||
data = {"created_at": datetime.now()}
|
|
||||||
json_str = codec.encode(data)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 错误处理
|
|
||||||
|
|
||||||
```python
|
|
||||||
from oss.plugin.types import JsonCodecError
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = codec.decode("invalid json")
|
|
||||||
except JsonCodecError as e:
|
|
||||||
print(f"解码失败: {e}")
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "IQ8WAvKno6pRp71kIaxXPb7DzTajPeNOQ0FLZMVovufeyTRMbdSJ8z2zQPBPv9O2a1S9bucyZyhg54fNB2DdLfEnrAbmpepZ3CLrj3cn4KaLNGJjxGHYXWIsFXFvLaYIod/ZuFMYPlzDdwnHJwzHZnkGAmCLrJSR+XvuOqYu/xSZekD/nbMI0fj9VKjaH/S/vopEhq7IFioahVkiSokdYx5qkXYruOVAq3wCnk6O0uCNMfHiIaRhn5pEoQ+VOXcuKX5eOBEph8oXqb+ew1MB917Z1CpaLFuZTyp2Dy8OOmpXjBxfd5VYazH4ZvE9Q7VODHkRDVF2ApkPxTE1k490YvmNOHRamjcf1/mKyu7Myaemtz9oxvZFFiOMOaXBXGfe1wlnsbO832lURTpPu9WXQ6aoDEVp3TNuR/G/xYOXHcWhG1M4tIWW+1ZFcozkVw9cMYvwrVI9JEa89sueXQhJG9foW4nj0DJqmtXaXvcVHnpbFkIxcKFZ0rOMelJ7404XuDb07/sjliJuqCG9Gssmv7/DqNgIrcWUPg24U4UPWW2vWJaJq7HOrGrxFoOxpCT/G4A0WcAWVJrM5NojnfvBNswybSB2IIbspmPRDVtoHQ5a3YJqSLZdgugHh+MbGKlyDvPkQTkPLLE8nrP2F0LwWCq0cYeodE+zU0rZ6CHgAsc=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775964952.836965,
|
|
||||||
"plugin_hash": "a7f7a20614a2e159e393a95c99b15a0a028724694bda3d089787cb41eceba7c4",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
class JsonCodecError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class JsonSerializer:
|
|
||||||
def __init__(self):
|
|
||||||
self._custom_encoders: dict = {}
|
|
||||||
|
|
||||||
def register_encoder(self, type_class: type, encoder: callable):
|
|
||||||
self._custom_encoders[type_class] = encoder
|
|
||||||
|
|
||||||
def encode(self, data: Any, pretty: bool = False) -> str:
|
|
||||||
return json.dumps(data, indent=2 if pretty else None)
|
|
||||||
|
|
||||||
def encode_bytes(self, data: Any, pretty: bool = False) -> bytes:
|
|
||||||
return self.encode(data, pretty).encode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
class JsonDeserializer:
|
|
||||||
def __init__(self):
|
|
||||||
self._custom_decoders: dict = {}
|
|
||||||
|
|
||||||
def register_decoder(self, type_name: str, decoder: callable):
|
|
||||||
self._custom_decoders[type_name] = decoder
|
|
||||||
|
|
||||||
def decode(self, text: str) -> Any:
|
|
||||||
return json.loads(text)
|
|
||||||
|
|
||||||
def decode_bytes(self, data: bytes) -> Any:
|
|
||||||
return self.decode(data.decode("utf-8"))
|
|
||||||
|
|
||||||
def decode_file(self, path: str) -> Any:
|
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
class JsonValidator:
|
|
||||||
def __init__(self):
|
|
||||||
self._schemas: dict[str, dict] = {}
|
|
||||||
|
|
||||||
def register_schema(self, name: str, schema: dict):
|
|
||||||
self._schemas[name] = schema
|
|
||||||
|
|
||||||
def validate(self, data: Any, schema_name: str) -> bool:
|
|
||||||
if schema_name not in self._schemas:
|
|
||||||
raise JsonCodecError(f"Unknown schema: {schema_name}")
|
|
||||||
return self._check_schema(data, self._schemas[schema_name])
|
|
||||||
|
|
||||||
def _check_schema(self, data: Any, schema: dict) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class JsonCodecPlugin:
|
|
||||||
def __init__(self):
|
|
||||||
self.serializer = JsonSerializer()
|
|
||||||
self.deserializer = JsonDeserializer()
|
|
||||||
self.validator = JsonValidator()
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
Log.info("json-codec", "JSON codec started")
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def encode(self, data: Any, pretty: bool = False) -> str:
|
|
||||||
return self.serializer.encode(data, pretty)
|
|
||||||
|
|
||||||
def decode(self, text: str) -> Any:
|
|
||||||
return self.deserializer.decode(text)
|
|
||||||
|
|
||||||
def validate(self, data: Any, schema_name: str) -> bool:
|
|
||||||
return self.validator.validate(data, schema_name)
|
|
||||||
|
|
||||||
def register_schema(self, name: str, schema: dict):
|
|
||||||
self.validator.register_schema(name, schema)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "json-codec",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "JSON 编解码器 - 插件间 JSON 数据处理和验证",
|
|
||||||
"type": "utility"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# lifecycle 生命周期管理
|
|
||||||
|
|
||||||
管理插件的状态转换和钩子函数。
|
|
||||||
|
|
||||||
## 功能
|
|
||||||
|
|
||||||
- 状态机:`pending` → `running` → `stopped`
|
|
||||||
- 支持状态转换验证
|
|
||||||
- 提供生命周期钩子:
|
|
||||||
- `before_start`
|
|
||||||
- `after_start`
|
|
||||||
- `before_stop`
|
|
||||||
- `after_stop`
|
|
||||||
- 支持扩展能力注入
|
|
||||||
|
|
||||||
## 状态转换
|
|
||||||
|
|
||||||
```
|
|
||||||
pending → running → stopped
|
|
||||||
↕
|
|
||||||
(可重启)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
```python
|
|
||||||
lc = lifecycle_plugin.create("my-plugin")
|
|
||||||
lc.on("after_start", lambda: print("started"))
|
|
||||||
lc.start()
|
|
||||||
```
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "nfM9Sj7VvV+L85zCvVcmIQY4qZ9FDdsk8MZf0LrO/ys1o6FCQ96Ixt1aB+2j6crOvXUBavnSRPk/LNaDs9r3eh49+Zfy5rEK+M0UyGjcawvEY4e/lO20UWy4iLw3JdSBo9nnFQC9eE8D6C9F2oM7YcqmT/sH0wYuyjCsa8tk6P/jy5/IdCwR6bo6AIQSpCnvyNcS9JPU19f603f0nl/siafXVozQxMS3wCLQ5EAoDz7atLevvQK7xAZCIIcCsre/sHTZ3a6O+BFlYYQ5w/giWlrl4aF7W7JJntOwpain39B0ktDRV96msbW744a1BFkcUw91W/2sRU7T9xplARjmhlRPGkdMTlj4PGyy394oaLwhx+uusx28C9+gWxp7pQZNo08LQ6dKmzog4fpUFD3EEyZBtPY2XYsILqKnGQVn3TLAaMmdoHdwoR6moLtR6BfD3ToRFV6vcNRTig8hTiS9GTzZeQtEtVkoSeAZphzxWfB7FunimDRpPxndDmvhervPUJ/uAVLcdorbDFB0RfvR3znUZrQkaw5YQZjP8mhUNyA6avyOBvGdt1i0bhZsc6CUMN4BrC+vOULiykyVGnk3B07XrMHNB8AGuqR8Ai/2DFglomfs/l07mz01HeUotRg3MezqF8aSkofpPTpRieeD9IeQgH03sOGdvXHDgDJB3Xc=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775960646.0212853,
|
|
||||||
"plugin_hash": "a7d6c6e01a8dc5df868e34777233e33d984d01adedb8adcee24d6892600928a8",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
class LifecycleState:
|
|
||||||
PENDING = "pending"
|
|
||||||
RUNNING = "running"
|
|
||||||
STOPPED = "stopped"
|
|
||||||
|
|
||||||
|
|
||||||
class LifecycleError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Lifecycle:
|
|
||||||
VALID_TRANSITIONS = {
|
|
||||||
LifecycleState.PENDING: [LifecycleState.RUNNING],
|
|
||||||
LifecycleState.RUNNING: [LifecycleState.STOPPED],
|
|
||||||
LifecycleState.STOPPED: [LifecycleState.RUNNING],
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, name: str):
|
|
||||||
self.name = name
|
|
||||||
self.state = LifecycleState.PENDING
|
|
||||||
self._hooks: dict[str, list[Callable]] = {
|
|
||||||
"before_start": [],
|
|
||||||
"after_start": [],
|
|
||||||
"before_stop": [],
|
|
||||||
"after_stop": [],
|
|
||||||
}
|
|
||||||
self._extensions: dict[str, Any] = {}
|
|
||||||
|
|
||||||
def add_extension(self, name: str, extension: Any):
|
|
||||||
self._extensions[name] = extension
|
|
||||||
|
|
||||||
def get_extension(self, name: str) -> Any:
|
|
||||||
return self._extensions.get(name)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
for hook in self._hooks["before_start"]:
|
|
||||||
hook(self)
|
|
||||||
self.transition(LifecycleState.RUNNING)
|
|
||||||
for hook in self._hooks["after_start"]:
|
|
||||||
hook(self)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if self.state == LifecycleState.RUNNING:
|
|
||||||
for hook in self._hooks["before_stop"]:
|
|
||||||
hook(self)
|
|
||||||
self.transition(LifecycleState.STOPPED)
|
|
||||||
for hook in self._hooks["after_stop"]:
|
|
||||||
hook(self)
|
|
||||||
|
|
||||||
def restart(self):
|
|
||||||
self.stop()
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def on(self, event: str, hook: Callable):
|
|
||||||
if event in self._hooks:
|
|
||||||
self._hooks[event].append(hook)
|
|
||||||
|
|
||||||
def transition(self, target_state: LifecycleState):
|
|
||||||
valid = self.VALID_TRANSITIONS.get(self.state, [])
|
|
||||||
if target_state in valid:
|
|
||||||
self.state = target_state
|
|
||||||
else:
|
|
||||||
raise LifecycleError(f"Cannot transition from {self.state} to {target_state}")
|
|
||||||
|
|
||||||
|
|
||||||
class LifecycleManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.lifecycles: dict[str, Lifecycle] = {}
|
|
||||||
|
|
||||||
def init(self, deps: dict = None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create(self, name: str) -> Lifecycle:
|
|
||||||
lifecycle = Lifecycle(name)
|
|
||||||
self.lifecycles[name] = lifecycle
|
|
||||||
return lifecycle
|
|
||||||
|
|
||||||
def get(self, name: str) -> Optional[Lifecycle]:
|
|
||||||
return self.lifecycles.get(name)
|
|
||||||
|
|
||||||
def start_all(self):
|
|
||||||
for lc in self.lifecycles.values():
|
|
||||||
try:
|
|
||||||
lc.start()
|
|
||||||
except LifecycleError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def stop_all(self):
|
|
||||||
for lc in self.lifecycles.values():
|
|
||||||
try:
|
|
||||||
lc.stop()
|
|
||||||
except LifecycleError:
|
|
||||||
pass
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"name": "lifecycle",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "NebulaShell",
|
|
||||||
"description": "生命周期管理 - 管理插件的状态转换和钩子",
|
|
||||||
"type": "core"
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"enabled": true,
|
|
||||||
"args": {}
|
|
||||||
},
|
|
||||||
"dependencies": [],
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"signature": "iSAdml6TdNMXoZmB7zsRN6jYb3GL8ufdfxA+gHL58R1z7qpxc13fQidyo/syRaGv+J7zLV2/8/e8qSSGhbtWn2p08iH8vIax5zTe3zfl8wBlhxnCkEQztd1FlfkERgNWpRToiGu8GV8o0Fq+Yej6C+OaO6EL69DkRxL8Kp2Jf/2jdUOCprErLyKm506zotXjcKEr9heSLNCD0DKRaQv1GnqLJclp9fXirVvJHDS26ttNx1srNhvjTjsGofzn6qQpGuddLXKi7FWKDAByEBjqzQOmQ2iB4NOIG012J4HKO1q3BajNj11xfWL6PnSzvrwj8IJbJIrbCzTPeFK3F6gj3JtAcaI6iQLhJ7VjOCbFhlOOoIJx/5CA3j9x+/DLXgjAnV6fiD0Q8VCaLTkXGQPwGXo7xq8ExkRt48sHI9nFI0+8fj6nXB1ANDHPlvg86eyHKG61WUIZOHd/Ag9foCZtoDFnKXYBnVeNweHaHBsJWpBOvbFjPkYRpRxvRvVd8oe5qmxS0eS5RLmIIpHnOvoGKQV5CoGXPmKB5FNxDRUH4llz9W4FpxtRaYoFFoYatT9Kvr+WPSok13XS1uMBybT2nc+nEZ/XR7LsNxajfZsyEjXwQbL8DsI9LXPW9gt10F6P/9ByWaTCD/4H8flwDFI4iqw/iVENip8vnilTQpowuOY=",
|
|
||||||
"signer": "NebulaShell",
|
|
||||||
"algorithm": "RSA-SHA256",
|
|
||||||
"timestamp": 1775969593.8644652,
|
|
||||||
"plugin_hash": "b38f028d1629d878dcfc32ac28747d5cea8e93ad832009b88cb3b69934fb3fa5",
|
|
||||||
"author": "NebulaShell"
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"logSyncInterval": {
|
|
||||||
"type": "number",
|
|
||||||
"name": "日志同步间隔",
|
|
||||||
"description": "日志自动同步的时间间隔(秒)",
|
|
||||||
"default": 2,
|
|
||||||
"min": 1,
|
|
||||||
"max": 10,
|
|
||||||
"order": 1
|
|
||||||
},
|
|
||||||
"sshPort": {
|
|
||||||
"type": "number",
|
|
||||||
"name": "SSH 端口",
|
|
||||||
"description": "SSH 连接的默认端口",
|
|
||||||
"default": 8022,
|
|
||||||
"min": 1,
|
|
||||||
"max": 65535,
|
|
||||||
"order": 2
|
|
||||||
},
|
|
||||||
"autoInstallSSH": {
|
|
||||||
"type": "boolean",
|
|
||||||
"name": "自动安装 SSH",
|
|
||||||
"description": "连接时自动检测并安装 SSH 服务",
|
|
||||||
"default": true,
|
|
||||||
"order": 3
|
|
||||||
},
|
|
||||||
"maxLogLines": {
|
|
||||||
"type": "number",
|
|
||||||
"name": "最大日志行数",
|
|
||||||
"description": "日志界面最多显示的日志行数",
|
|
||||||
"default": 1000,
|
|
||||||
"min": 100,
|
|
||||||
"max": 10000,
|
|
||||||
"order": 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||