重构:核心迁移至 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
|
||||
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");
|
||||
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)
|
||||
[](LICENSE)
|
||||
[]()
|
||||
<p align="center">
|
||||
<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
|
||||
# 克隆
|
||||
# 克隆仓库
|
||||
git clone https://github.com/Starlight-apk/NebulaShell.git
|
||||
cd NebulaShell
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动
|
||||
# 启动 NebulaShell
|
||||
python main.py
|
||||
```
|
||||
|
||||
启动后访问 [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
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| `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` | 多语言部署 |
|
||||
# 3. 验证包完整性
|
||||
nebula nbpf verify my-plugin.nbpf
|
||||
|
||||
---
|
||||
# 4. 将密钥放入信任目录
|
||||
cp ./nbpf-keys/trusted/* ./data/nbpf-keys/trusted/
|
||||
cp ./nbpf-keys/rsa/* ./data/nbpf-keys/rsa/
|
||||
|
||||
## 开发一个插件
|
||||
|
||||
在 `store/NebulaShell/` 下创建目录,包含 `manifest.json` 和 `main.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "my-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "我的插件"
|
||||
},
|
||||
"config": { "enabled": true, "args": {} },
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
# 5. 重启 NebulaShell,插件自动加载
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
from oss.plugin.types import Plugin
|
||||
|
||||
class MyPlugin(Plugin):
|
||||
class HelloPlugin(Plugin):
|
||||
def init(self, deps=None):
|
||||
pass
|
||||
self.name = "hello"
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
print(f"{self.name} started")
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
print(f"{self.name} stopped")
|
||||
|
||||
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
|
||||
from store.NebulaShell.plugin_bridge.main import 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
|
||||
# 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/*" | \
|
||||
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).
|
||||
|
||||
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
|
||||
- **P0 修复完成**:修复 40+ 损坏 Python 文件的 class 定义头和语法错误
|
||||
@@ -805,3 +889,15 @@ Phase 4 (长期) — K8s部署、ADR、类型检查、pre-commit、异步I/O
|
||||
- **README 重写**:805 行 → 283 行,企业级开源项目风格
|
||||
- **分支清理**:删除 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 |
338
oss/cli.py
@@ -3,9 +3,11 @@ import click
|
||||
import signal
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
from oss import __version__
|
||||
from oss.logger.logger import Logger
|
||||
from oss.logger.logger import Log
|
||||
from oss.plugin.manager import PluginManager
|
||||
from oss.config import init_config, get_config
|
||||
|
||||
@@ -18,21 +20,52 @@ except ImportError:
|
||||
_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.option('--config', '-c', type=str, help='配置文件路径')
|
||||
@click.pass_context
|
||||
def cli(ctx, config):
|
||||
"""NebulaShell - 一切皆为插件"""
|
||||
# 初始化配置
|
||||
ctx.ensure_object(dict)
|
||||
ctx.obj['config'] = init_config(config)
|
||||
|
||||
# 初始化成就系统(如果启用)
|
||||
if _ACHIEVEMENTS_ENABLED:
|
||||
try:
|
||||
init_achievements()
|
||||
except Exception:
|
||||
pass # 静默失败,不影响主功能
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@@ -44,7 +77,6 @@ def serve(ctx, host, port, tcp_port):
|
||||
"""启动 NebulaShell 服务端"""
|
||||
config = ctx.obj.get('config', get_config())
|
||||
|
||||
# 命令行参数覆盖配置
|
||||
if host:
|
||||
config.set('HOST', host)
|
||||
if port:
|
||||
@@ -52,33 +84,36 @@ def serve(ctx, host, port, tcp_port):
|
||||
if tcp_port:
|
||||
config.set('HTTP_TCP_PORT', tcp_port)
|
||||
|
||||
log = Logger()
|
||||
log.info(f"NebulaShell {__version__} 启动")
|
||||
log.info(f"监听地址:{config.host}:{config.http_api_port}")
|
||||
log.info(f"数据目录:{config.data_dir.absolute()}")
|
||||
log.info(f"插件仓库:{config.store_dir.absolute()}")
|
||||
Log.info("NebulaShell", f"NebulaShell {__version__} 启动")
|
||||
Log.info("NebulaShell", f"监听地址:{config.host}:{config.http_api_port}")
|
||||
Log.info("NebulaShell", f"数据目录:{config.data_dir.absolute()}")
|
||||
Log.info("NebulaShell", f"插件仓库:{config.store_dir.absolute()}")
|
||||
|
||||
plugin_mgr = PluginManager()
|
||||
plugin_mgr.load()
|
||||
plugin_mgr.start()
|
||||
|
||||
log.info("就绪")
|
||||
Log.info("NebulaShell", "就绪")
|
||||
|
||||
def shutdown(sig, frame):
|
||||
log.info("停止中...")
|
||||
Log.info("NebulaShell", "停止中...")
|
||||
plugin_mgr.stop()
|
||||
log.info("已停止")
|
||||
Log.info("NebulaShell", "已停止")
|
||||
raise SystemExit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
|
||||
# 启动 REPL 交互(由 Core 内部提供)
|
||||
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()
|
||||
except AttributeError:
|
||||
import time
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@@ -91,8 +126,6 @@ def version():
|
||||
@click.pass_context
|
||||
def info(ctx):
|
||||
"""显示系统信息"""
|
||||
import random
|
||||
|
||||
config = ctx.obj.get('config', get_config())
|
||||
click.echo(f"NebulaShell {__version__}")
|
||||
click.echo(f"配置文件:{config._config_file or '无'}")
|
||||
@@ -104,12 +137,11 @@ def info(ctx):
|
||||
click.echo(f"日志级别:{config.log_level}")
|
||||
click.echo(f"权限检查:{'启用' if config.permission_check else '禁用'}")
|
||||
|
||||
# 小提示:有些功能需要特殊方式访问 (4675747572654f53)
|
||||
# 随机显示隐藏提示(约 10% 概率)
|
||||
# 彩蛋提示
|
||||
click.echo("")
|
||||
if random.random() < 0.1: # 10% 概率显示彩蛋提示
|
||||
if random.random() < 0.1:
|
||||
click.echo("✨ 奇怪的提示:试试在命令前加两个感叹号会怎样?比如 !!help")
|
||||
elif random.random() < 0.05: # 额外 5% 概率显示另一种提示
|
||||
elif random.random() < 0.05:
|
||||
click.echo("🤔 听说有人用 !! 开头的命令发现了不得了的东西...")
|
||||
|
||||
|
||||
@@ -122,49 +154,233 @@ def cli_command(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():
|
||||
# 检测是否通过已弃用的 oss 命令调用
|
||||
cmd_name = os.path.basename(sys.argv[0])
|
||||
if cmd_name in ("oss", "oss.exe"):
|
||||
print("╔══════════════════════════════════════════╗")
|
||||
print("║ ⚠ oss 命令已弃用,请使用 nebula 替代 ║")
|
||||
print("║ 例如: nebula serve ║")
|
||||
print("║ nebula info ║")
|
||||
print("║ nebula version ║")
|
||||
print("╚══════════════════════════════════════════╝")
|
||||
Log.warn("NebulaShell", "oss 命令已弃用,请使用 nebula 替代")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查隐藏命令前缀
|
||||
if len(sys.argv) > 1 and sys.argv[1].startswith("!!"):
|
||||
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
|
||||
if _handle_hidden_command():
|
||||
return
|
||||
|
||||
cli()
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ class Config:
|
||||
# 性能配置
|
||||
"MAX_WORKERS": 4,
|
||||
"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):
|
||||
@@ -74,7 +81,7 @@ class Config:
|
||||
self._config[key] = value
|
||||
|
||||
# 隐藏成就:配置黑客 - 记录配置修改
|
||||
if _ACHIEVEMENTS_ENABLED:
|
||||
if Config._ACHIEVEMENTS_ENABLED:
|
||||
try:
|
||||
from oss.core.achievements import 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 typing import Any
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
|
||||
|
||||
class Request:
|
||||
@@ -40,13 +41,13 @@ class HttpServer:
|
||||
self._server = HTTPServer((self.host, self.port), handler)
|
||||
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
||||
self._thread.start()
|
||||
print(f"[http-api] 服务器启动: {self.host}:{self.port}")
|
||||
Log.info("Core", f"HTTP 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self._server:
|
||||
self._server.shutdown()
|
||||
print("[http-api] 服务器已停止")
|
||||
Log.info("Core", "HTTP 服务器已停止")
|
||||
|
||||
def _create_handler(self):
|
||||
"""创建请求处理器"""
|
||||
@@ -118,6 +119,6 @@ class HttpServer:
|
||||
pass # 忽略客户端断开
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
Log.debug("Core", format % args)
|
||||
|
||||
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):
|
||||
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
|
||||
|
||||
|
||||
class Logger:
|
||||
"""日志记录器(兼容旧接口)"""
|
||||
|
||||
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)
|
||||
@classmethod
|
||||
def debug(cls, tag: str, msg: str):
|
||||
cls.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):
|
||||
capabilities: set[str] = set()
|
||||
main_file = plugin_dir / "main.py"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""插件管理器 - 只加载 plugin-loader,其他所有插件由 plugin-loader 插件自行管理"""
|
||||
"""插件管理器 - 直接使用框架层的 Core Engine"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from oss.plugin.loader import PluginLoader
|
||||
from oss.core.engine import PluginManager as CorePluginManager
|
||||
|
||||
# 深度隐藏的成就系统导入
|
||||
try:
|
||||
@@ -14,31 +14,19 @@ except ImportError:
|
||||
class PluginManager:
|
||||
"""极简插件管理器
|
||||
|
||||
遵循「最小化核心框架」设计哲学:
|
||||
- 核心框架只负责加载 plugin-loader 插件
|
||||
- 所有其他插件(HTTP、WebSocket、Dashboard 等)都由 plugin-loader 插件扫描和加载
|
||||
- store/NebulaShell/ 是唯一的插件来源
|
||||
直接使用框架层的 CorePluginManager(原 Core 插件功能)
|
||||
- 不再通过插件加载器加载 Core
|
||||
- 所有核心功能直接集成在 oss.core.engine 中
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.loader = PluginLoader()
|
||||
self.plugin_loader: Optional[Any] = None
|
||||
self.core = CorePluginManager()
|
||||
|
||||
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:
|
||||
try:
|
||||
validator = get_validator()
|
||||
@@ -47,15 +35,17 @@ class PluginManager:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
"""启动 plugin-loader,它会初始化并启动所有其他插件"""
|
||||
"""启动 Core,它会初始化并启动所有其他插件"""
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
if self.plugin_loader:
|
||||
# plugin-loader.init() 会扫描并加载 store/ 中的所有插件
|
||||
self.plugin_loader.init()
|
||||
# plugin-loader.start() 会按依赖顺序启动所有插件
|
||||
self.plugin_loader.start()
|
||||
self.core.init_and_start_all()
|
||||
|
||||
# 启动 HTTP 服务
|
||||
self.core.start_http_server()
|
||||
|
||||
# 启动防篡改监控
|
||||
self.core.start_tamper_monitor()
|
||||
|
||||
# 计算启动时间并检查速度成就
|
||||
elapsed_ms = (time.time() - start_time) * 1000
|
||||
@@ -65,30 +55,28 @@ class PluginManager:
|
||||
validator.check_startup_speed(elapsed_ms)
|
||||
|
||||
# 检查插件数量成就
|
||||
if hasattr(self.plugin_loader, 'manager') and hasattr(self.plugin_loader.manager, 'plugins'):
|
||||
plugin_count = len(self.plugin_loader.manager.plugins)
|
||||
validator.check_plugin_count(plugin_count)
|
||||
plugin_count = len(self.core.plugins)
|
||||
validator.check_plugin_count(plugin_count)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""停止所有插件(由 plugin-loader 统一管理)"""
|
||||
if self.plugin_loader:
|
||||
try:
|
||||
self.plugin_loader.stop()
|
||||
except KeyboardInterrupt:
|
||||
print("[PluginManager] 用户中断停止过程")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
|
||||
traceback.print_exc()
|
||||
"""停止所有插件"""
|
||||
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:
|
||||
validator = get_validator()
|
||||
# 记录会话结束
|
||||
validator.track_progress("session_end")
|
||||
except Exception:
|
||||
pass
|
||||
# 隐藏成就:崩溃幸存者
|
||||
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:
|
||||
|
||||
__slots__ = ('method', 'path', 'handler', '_pattern_parts')
|
||||
@@ -9,6 +13,16 @@ class BaseRoute:
|
||||
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)
|
||||
def match_path(pattern: str, path: str) -> bool:
|
||||
if pattern == path:
|
||||
@@ -41,12 +55,6 @@ def match_path(pattern: str, path: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _is_wildcard_param(param: str) -> bool:
|
||||
if ":" not in pattern:
|
||||
return None
|
||||
return pattern.strip("/").split("/")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def extract_path_params(pattern: str, path: str) -> dict[str, str]:
|
||||
params = {}
|
||||
@@ -85,9 +93,15 @@ class BaseRouter:
|
||||
self.routes: list[BaseRoute] = []
|
||||
|
||||
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)
|
||||
|
||||
def post(self, path: str, handler: Callable):
|
||||
self.add("POST", path, handler)
|
||||
|
||||
def put(self, path: str, handler: Callable):
|
||||
self.add("PUT", path, handler)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||