Compare commits

..
5 Commits
Author SHA1 Message Date
qing f214c862d3 docs(bot4cj): 新增 README.robot.md 完整技术手册
- 补充 5 层架构分层图(脚本→解析器→结构构建→硬件实现→系统库)
- 详解 .mt 脚本语法、块类型、占位符规则
- 补充 PCA9685/Servo/MPU6050/Speaker 各硬件寄存器与校准参数
- 说明执行流程:stdin→FSM 解析→链表→build/play/free
- 列出 I2C 总线保护、GC 析构、pthread_cancel 等安全限制
- 补充 11 条已知缺陷清单(D1-D11)
- 新增 NN/CNN 测试命令与 DRL 预留接口说明
2026-07-30 11:54:51 +08:00
qing a8a190571c feat(gateway): add /refresh_token API with grace window + refresh limit
- POST /refresh_token: accept current Bearer token (or Cookie fallback),
  verify RSA signature, check grace window (iat + keep_time * multiplier)
  and refresh count limit, then issue a new token.
- Token payload extended with iat, refresh_count, max_refresh fields.
- iat preserves original issue time; created_at updates on each refresh.
- Rate limited: 5 req / 5 min / IP via RateLimiter.
- Config: Token.refresh_grace_multiplier (default 2), Token.max_refresh (default 10).
- Backward compatible: old tokens without new fields still pass proxy checks.
2026-07-30 11:54:31 +08:00
qing aa3a99825c docs(captcha): 项目结构树和安装指引补全遗漏项
- 安装章节新增 bash _set.sh 初始化步骤
- 项目结构树补全 _set.sh 和 requirements.txt
2026-07-30 11:53:30 +08:00
qing dcfdbcb9f2 docs(waf): 更新 README 文档与代码实现对齐
- 新增「安全响应头」章节,列出 7 个 WAF 注入的响应头及说明
- 修正「已知限制」中响应体检测描述,明确响应头检测已启用
- 澄清拦截日志等级区分(WARNING 命中拦截 / ERROR 加载错误)及 extra 字段类型
- 补充规则 ID 分段表的 CRS 可选说明
- 更新目录结构列表补充 logclient 子包
- 请求处理流程新增 ModifyResponse 步骤
2026-07-30 11:49:10 +08:00
qing 62007e7848 docs(static): README 路由表、架构图、配置、依赖与实际代码同步
- 修正根路径 / 描述:index.html → login.html
- 补全全部 19 个路由(原仅 6 个),分组为核心/内部工具/OAuth/代理
- 架构图补全 waf:443 → gateway:8443 → static:9200 完整链路
- 配置示例新增 [cors] 段
- 依赖列表补全 waitress 和 pymsg
- 页面和功能列表补全遗漏项
2026-07-30 11:48:54 +08:00
6 changed files with 664 additions and 18 deletions

No files matched your search

+384
View File
@@ -0,0 +1,384 @@
# bot4cj — 机器人硬件脚本解析器
基于**仓颉(Cangjie)** 编程语言和 Linux I2C 接口的机器人动作脚本解析器。用一种矩阵式脚本来描述机器人的硬件构成与动作序列,解析后驱动真实硬件执行。
## 架构分层
```
┌──────────────────────────────────────────────────────────────┐
│ .mt 矩阵脚本(硬件定义 + 动作序列) │
├──────────────────────────────────────────────────────────────┤
│ MatrixParser(P.cj) — FSM 逐字符解析 → MatrixObject 链表 │
├──────────────────────────────────────────────────────────────┤
│ RobotStruct(r.cj) — 从矩阵对象构建硬件变量(createHardVars) │
│ MatrixVars_Hardware 基类 + 闭包回调: │
│ · build() → rise 回调(打开硬件、获取资源) │
│ · play() → things 回调(执行动作,参数/返回值均为 JsonValue) │
│ · free() → free 回调(释放资源) │
├──────────────────────────────────────────────────────────────┤
│ 硬件实现:hd/*.cj │
│ PCA9685(I2C 0x40, 50Hz PWM, 角度→脉宽 0.5~2.5ms) │
│ Servo(舵机:max/min/offset 校准 + 角度翻转) │
│ MPU6050(陀螺+加速度,±500°/s ±8g,后台 10ms 采样线程+卡尔曼) │
│ Speaker(正弦波发声,5ms 淡入淡出) │
│ fb.cj(外部硬件扩展口) │
├──────────────────────────────────────────────────────────────┤
│ 系统库:cpp/ + cuda/ │
│ libi2c4cj.so — I2C glibc 封装 + 20 槽锁数组 │
│ libmt4cj.so — FP32 矩阵运算(CPU / CUDA 二选一) │
│ libfft4cj.so — 基-2 Cooley-Tukey FFT │
│ libcnn4cj.so — Conv/Pool/GA-Pool/Flatten + Adam/Momentum │
│ libsnd4cj.so — WaveAPI / ALSA 音频播放 │
├──────────────────────────────────────────────────────────────┤
│ 数学/NN 库:math/*.cj │
│ Matrix1(二维矩阵)+ Tensor + 6 种激活函数 + Xavier 初始化 │
│ Nn1 全连接层(前向/反向/权重更新) │
│ Af(Sigmoid/Tanh/ReLU/Leaky_ReLU/Swish/Softmax) │
└──────────────────────────────────────────────────────────────┘
```
## 技术栈
| 层 | 技术 | 说明 |
|----|------|------|
| 应用层 | **仓颉 Cangjie** | 主程序、解析器、机器人结构、数学库、HTTP 服务 |
| 系统层 | C / C++ | I2C 驱动、矩阵运算、FFT、CNN、音频 |
| 加速层 | CUDA(可选) | `MODE=cpu/cuda` 切换 |
| 脚本层 | `.mt` 矩阵脚本 | 硬件定义 + 动作序列 |
| 依赖包 | ctype4cj / pthread4cj / spi4cj / lua4cj | 仓颉官方扩展包 |
| 构建 | Makefile(C/C++ 共享库)+ cjpm.toml(仓颉包管理) | — |
## 目录结构
```
bot4cj/
├── src/
│ ├── main.cj # 入口:CLI 参数解析(-i/-o/--test)+ stdin 管道执行
│ ├── P.cj # 矩阵脚本解析器(FSM 逐字符解析)
│ ├── r.cj # RobotStruct + MatrixVars_Hardware 基类
│ ├── testCNN.cj # CNN 测试入口
│ ├── testCNN1.cj # 几何 CNN 测试
│ ├── hd/
│ │ ├── I2C.cj # I2C 封装 + 20 槽锁数组
│ │ ├── PCA9685.cj # PCA9685 舵机控制板(已标记由 Lua 替代)
│ │ ├── Servo.cj # 舵机:校准 + 角度翻转
│ │ ├── MPU6050.cj # MPU6050 陀螺+加速度+卡尔曼滤波
│ │ ├── snd.cj # 扬声器
│ │ ├── fb.cj # 外部硬件扩展
│ │ └── extern/
│ │ └── macro_ExternHardware.cj
│ ├── math/
│ │ ├── Matrix.cj # 二维矩阵(+运算符重载)
│ │ ├── Tensor.cj # 张量
│ │ ├── Af.cj # 6 种激活函数
│ │ ├── Ini.cj # Xavier/Kaiming 初始化
│ │ ├── nn.cj # 全连接层 Nn1
│ │ ├── nns.cj # 旧版神经网络(XNOR 门测试)
│ │ ├── cnn.cj # CNN 仓颉绑定
│ │ ├── pooling.cj # 池化
│ │ ├── flatten.cj # Flatten
│ │ ├── fft.cj # FFT 仓颉绑定
│ │ ├── kalman.cj # 卡尔曼滤波
│ │ └── Adam.cj # Adam 优化器
│ ├── cpp/ # C/C++ 系统库源
│ │ ├── I2C.c # I2C glibc 封装
│ │ ├── Matrix.cpp # CPU 矩阵运算
│ │ ├── fft1.c # FFT 单实序列
│ │ ├── fft.cpp # FFT 双实序列 + 基-2 Cooley-Tukey
│ │ ├── kalman.c # 卡尔曼滤波
│ │ ├── snd.cpp # 音频播放(WaveAPI/ALSA)
│ │ └── cnn/ # 纯 C++ CNN(Conv/Pool/GA-Pool/Flatten)
│ └── cuda/
│ ├── matrix.cu # GPU 矩阵运算 kernel
│ └── error.cuh # CUDA 错误检查宏
├── modules/ # Lua 硬件模块(ADS1115/ADS1256/PCA9685/SPI)
├── scripts/ # .mt 动作脚本(std/jump/lie-down/tilt-* 等)
├── test/ # C/Lua 测试脚本
├── docs/Reports/ # 深度分析报告
├── cjpm.toml # 仓颉包配置
├── Makefile # C/C++ 共享库构建
├── _set.sh # 编译(cjpm update + build)
├── _run.sh # 运行(stdin 管道)
├── compile.ps1 # Windows 编译脚本
├── run.ps1 # Windows 运行脚本
├── README.md # 旧版说明
└── README.robot.md # 本文件
```
## 快速开始
```bash
# 1. 编译 C/C++ 共享库(一次性)
make MODE=cpu # CPU 模式(默认)
# 或
make MODE=cuda # GPU 模式(需要 nvcc)
# 2. 编译仓颉程序
bash _set.sh
# 3. 运行动作脚本(stdin 管道)
cat scripts/std.mt | bash _run.sh
```
> **硬件要求**:树莓派(aarch64)或其他 Linux I2C 设备。x86_64 开发板需自行验证 I2C 参数。Windows 暂不支持(依赖 glibc)。
## CLI 参数
```
./bot # 从 stdin 读取 .mt 脚本执行
./bot --help # 打印帮助
./bot --test nn # 运行神经网络与门测试
./bot --test cnn # 运行 CNN 测试
./bot --test cnn1 # 运行几何 CNN 测试
./bot --test matrix_o # 解析 ss/test.mt 并保存为 ss/output.mt
./bot --test matrix_2 # 解析 ss/output.mt → 执行 → 再次保存
./bot -i model.json -o out.json # 加载/保存神经网络模型(DRL 预留)
```
## .mt 脚本语法
行列式表格,每列以空格或制表符分隔,每行以分号 `;` 结尾。
### 块结构
```
@BlockName
column1 column2 column3;
value1 value2 value3;
@
```
每个 `@` 块由 `@Name` 行开始,`@` 行结束。支持的块类型:
| 块 | 说明 |
|----|------|
| `@Alias` | 列名别名(如 `A` → `angle`、`F` → `frequency`) |
| `@I2C` | 定义 I2C 设备(name + path) |
| `@PCA9685` | 定义 PCA9685 舵机控制板(name + I2C 引用 + 地址) |
| `@MPU6050` | 定义 MPU6050 传感器(name + I2C 引用 + 地址) |
| `@Speaker` | 定义扬声器(channels + sample_rate + volume + max_f + min_f) |
| `@Servo` | 定义舵机(name + max + min + offset + controller + channel) |
| `@Action` | **动作矩阵**:列头为硬件变量的属性,行为每帧的参数 |
### 占位符
`-` 表示"沿用上一行同列的值",当前行该列不产生调用。
### 完整示例
```mt
@Alias
name alias;
A angle;
F frequency;
@
@I2C
name path;
i2c-0 /dev/i2c-0;
i2c-1 /dev/i2c-1;
@
@PCA9685
name I2C address;
pca9685-0 i2c-1 0x40;
@
@Speaker
name channels sample_rate volumn max_f min_f;
spk 2 48000 0.6 5000 -;
@
@Servo
name max min offset controller channel;
s1 90 - - pca9685-0 0;
s2 180 - - - 1;
s3 -90 - - - 2;
s4 -180 - - - 3;
@
@Action
s1.A s2.A s3.A s4.A spk.F spk.delay;
45 - 45 - 10 200; # 站立
90 - - - - 20; # 左转
@
```
## 执行流程
```
stdin.read()
↓
P.MatrixParser (FSM 逐字符解析)
↓
LinkedList<MatrixObject> 链表
↓
RobotStruct.createHardVars() # 按 @I2C/@PCA9685/@Servo/@Speaker/@MPU6050 构建硬件变量
↓
robot.active() # build():逐个硬件调用 rise 回调(打开 I2C、初始化 PWM 等)
↓
robot.run() # 逐行遍历 @Action 矩阵,每列调用对应硬件的 play() 回调
↓
robot.deactive() # free():倒序释放资源
```
`play()` 的输入输出均为 `JsonValue`,实现了硬件与脚本之间的统一通信协议。
## 已支持的硬件
### PCA9685(I2C 舵机控制板)
- 地址:默认 `0x40`
- PWM 频率:50Hz(舵机标准 20ms 周期)
- 舵机脉宽映射:0°→0.5ms,180°→2.5ms
- 角度映射到 12 位 PWM 值(0-4095)
- **注意**:PCA9685.cj 代码已标记"由 Lua 脚本取代",保留作备用
### Servo(舵机)
- 支持 `max` / `min` 角度限制与 `offset` 漂移校准
- `max < 0` 时自动翻转角度(反向安装的舵机)
- 通过闭包回调与实际硬件(PCA9685/Lua 模块)解耦
### MPU6050(6 轴传感器)
- 陀螺仪:±500°/s 量程
- 加速度计:±8g 量程
- 后台 10ms 采样线程(pthread_create + pthread_cancel)
- 卡尔曼滤波融合数据
- **已知问题**:`ADDR_0` 与 `ADDR_1` 常量均为 `0x68`([MPU6050.cj:14-15](src/hd/MPU6050.cj#L14-L15)),第二个地址应改为 `0x69`
### Speaker(扬声器)
- 支持正弦波发声(指定频率 `F` + 延时 `delay`)
- 5ms 淡入淡出,避免"咔哒"声
- 通道数/采样率/音量可在 `.mt` 中配置
- **已知问题**:`play()` 仅 sleep 指定延时,真实发声依赖底层 C++ 实现
## 机器人型号适配
### Dashun 蜘蛛机器人(默认)
10 舵机 + 2 云台 + 扬声器,建模文件见 [gopsc/Dashun](https://github.com/gopsc/Dashun):
| 舵机 | 关节 | 舵机 | 关节 |
|------|------|------|------|
| s1 | 左下水平 (rb0) | s6 | 右上垂直 (lf1) |
| s2 | 左下垂直 (rb1) | s7 | 左上水平 (rf0) |
| s3 | 右下水平 (lb0) | s8 | 左上垂直 (rf1) |
| s4 | 右下垂直 (lb1) | s9 | 云台1 (cam0) |
| s5 | 右上水平 (lf0) | s10 | 云台2 (cam1) |
### 可用脚本
`scripts/` 目录下预写了多种动作脚本:
- `std.mt` — 站立姿态
- `jump.mt` — 跳跃
- `lie-down.mt` — 趴下
- `left-hand.mt` / `rght-hand.mt` — 单手动作
- `left-turn.mt` / `tilt-f.mt` / `tilt-b.mt` / `tilt-l.mt` / `tilt-r.mt` — 转向与倾斜
## 构建
### 依赖
```bash
# 仓颉 1.1.3
# 仓颉官方扩展包:ctype4cj / pthread4cj / spi4cj / lua4cj
# libc / libpthread
# liblua-5.4(可选,Lua 硬件模块支持)
sudo ln -s /usr/lib/aarch64-linux-gnu/liblua5.4.so /usr/lib/aarch64-linux-gnu/liblua.so
sudo ln -s /usr/lib/x86_64-linux-gnu/liblua5.4.so /usr/lib/x86_64-linux-gnu/liblua.so
```
### 构建步骤
```bash
# 1. 构建 C/C++ 共享库
make MODE=cpu
# 产出:libi2c4cj.so libmt4cj.so libfft4cj.so libsnd4cj.so
# 2. (可选)构建 CNN
make libcnn4cj.so
# 3. (可选)CUDA 模式需 NVIDIA CUDA Toolkit
make MODE=cuda
# 4. 仓颉主程序
bash _set.sh
```
[_set.sh](_set.sh) 执行 `cjpm update` + `cjpm build`,自动解析 cjpm.toml 依赖。
### 运行
```bash
bash _run.sh scripts/std.mt
# 或
cat scripts/std.mt | bash _run.sh
```
[_run.sh](_run.sh) 以 `cjStackSize=64MB` / `cjHeapSize=64MB` 启动。
## 神经网络支持
项目内置了全连接层 Nn1([math/nn.cj](src/math/nn.cj))与 CNN([math/cnn.cj](src/math/cnn.cj) + [cpp/cnn/](src/cpp/cnn/)):
- **Nn1**:多层感知机,支持前向传播、反向传播、权重更新
- **CNN**:Conv → ReLU → Pool → GA-Pool → Flatten → FC
- **优化器**:Adam / Momentum
- **激活函数**:Sigmoid / Tanh / ReLU / Leaky_ReLU / Swish / Softmax
- **初始化**:Xavier / Kaiming
### 测试
```bash
./bot --test nn # 与门训练(2 输入 → 1 输出)
./bot --test cnn # CNN 前向/反向测试
./bot --test cnn1 # 几何分类 CNN 测试
```
### DRL 预留(未完成)
main.cj 中预留了 DRL 流程(注释状态):加载预训练权重 → 执行动作 → 保存微调模型。通过 `-i` / `-o` 参数指定输入/输出路径。
## 安全与限制
### I2C 总线保护
- [hd/I2C.cj](src/hd/I2C.cj) 内置 **20 槽锁数组**,多硬件共享 I2C 总线时自动互斥
- 每个 I2C fd 独立加锁,避免并发写入冲突
### 硬件关闭时机
- `MatrixVars_Hardware` 析构依赖仓颉 GC,**不保证**在确定性时机调用 `free()`
- 机器人使用 `try/finally` 在 [main.cj:80-87](src/main.cj#L80) 中显式调用 `deactive()`,确保硬件资源正确释放
### 线程安全
- MPU6050 采样通过 `pthread_create` 启动后台线程
- `pthread_cancel` 依赖系统定义的取消点(sleep / 系统调用),可能在非安全点中断
### 平台支持
- ✅ Linux aarch64(树莓派)/ x86_64
- ❌ Windows(依赖 glibc)
## 已知问题
| ID | 描述 | 影响 |
|----|------|------|
| D1 | 仓颉 GC 不保证析构器调用,I2C fd 可能泄漏 | 长时间运行后 I2C 资源耗尽 |
| D2 | Speaker `play()` 仅 sleep,真实发声未接入 C++ | 扬声器无实际声音 |
| D3 | MPU6050 `pthread_cancel` 可能在非安全点中断 | 传感器数据不一致 |
| D4 | PCA9685 初始化被标记为"由 Lua 取代",但仍保留主路径 | 代码存在双实现 |
| D5 | `_set.sh` 会删除 `a.out` | 误删用户产物 |
| D6 | Softmax 雅可比矩阵实现错误(math/Af.cj) | NN 训练收敛异常 |
| D7 | Tensor 乘法仅支持 2D,3D+ 会 panic | 限制模型复杂度 |
| D8 | Tanh 导数实现错误(使用 `1 - y^2` 而非 `1 - tanh^2(x)`) | NN 训练收敛异常 |
| D9 | CNN `testGeometryCNN` 使用随机数据 | 测试无实际意义 |
| D10 | MPU6050 `ADDR_0` 与 `ADDR_1` 均为 `0x68` | 无法同时使用两个 MPU6050 |
| D11 | `@Action` 与 `@Action_Auto` 并存未合并 | 脚本语法冗余 |
## 相关资源
- [Dashun 蜘蛛机器人建模](https://github.com/gopsc/Dashun)
- [深度分析报告](docs/Reports/analysis_20260525_00.md)(28 章节,1280 行)
- [仓颉编程语言](https://cangjie-lang.org/)
- [PCA9685 数据手册](https://www.nxp.com/docs/en/data-sheet/PCA9685.pdf)
- [MPU6050 数据手册](https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf)
## 许可证
MIT License
+6
View File
@@ -23,6 +23,10 @@
## 安装
```bash
# 自动创建 venv 并安装依赖(推荐)
bash _set.sh
# 或手动安装
pip install Pillow
```
@@ -138,7 +142,9 @@ png_b64 = resp["data"]["image"]
```
captcha/
├── captcha_service.py # 服务主程序(pymsg 服务 + 验证码生成与校验)
├── _set.sh # 初始化脚本(创建 venv + 安装依赖)
├── _run.sh # 启动脚本(cd 后 exec python3 captcha_service.py)
├── requirements.txt # Python 依赖(pymsg、Pillow)
└── README.md
```
+4
View File
@@ -14,6 +14,10 @@ opened = http://127.0.0.1:9200;http://127.0.0.1:8006;
[Token]
# Token 有效期(秒),默认 3600 = 1 小时
keep_time = 3600
# Token 刷新宽限期倍数:过期后 iat + keep_time * 倍数 窗口内仍可刷新,默认 2
refresh_grace_multiplier = 2
# Token 生命周期内最大刷新次数,超过后需重新登录,默认 10
max_refresh = 10
[Admin]
# 管理员账号白名单(分号分隔)。登录时命中即在 Token payload 写入 is_admin=true,
+171 -1
View File
@@ -302,6 +302,10 @@ http::response<http::string_body> handleLogout(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
http::response<http::string_body> handleRefreshToken(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params);
/* 代理 captcha 图形验证码:pymsg 调 captcha 服务,返回 PNG 图片 */
http::response<http::string_body> handleCaptchaImage(
const http::request<http::string_body>& req,
@@ -325,6 +329,8 @@ static std::string _DEFT_URL;
static std::vector<std::string> _OPEN_URL;
static std::string _DB_PATH;
static int _TOKEN_KEEP_TIME = 3600; // Token 有效期(秒),可由 [Token] keep_time 配置
static int _TOKEN_REFRESH_GRACE_MULTIPLIER = 2; // Token 刷新宽限期倍数,过期后 iat + keep_time*倍数 窗口内仍可刷新
static int _TOKEN_MAX_REFRESH = 10; // Token 生命周期内最大刷新次数,可由 [Token] max_refresh 配置
static std::vector<std::string> _CORS_ORIGINS; // CORS 允许的 Origin 白名单,可由 [CORS] allowed_origins 配置
static std::vector<std::string> _ADMIN_USERS; // 管理员账号白名单,可由 [Admin] users 配置(分号分隔),登录时写入 Token payload 的 is_admin
@@ -335,6 +341,7 @@ static std::unique_ptr<MyRSA::Generator> _KEYGEN;
static RateLimiter _LOGIN_RATE;
static RateLimiter _REGISTER_RATE;
static RateLimiter _EMAIL_CODE_RATE; // 发送邮箱验证码限流
static RateLimiter _REFRESH_TOKEN_RATE; // Token 刷新限流
/* pymsg 下游调用超时(秒):
* captcha / smtp verify_email 均为本机内存操作,5s 足够;
@@ -499,6 +506,8 @@ int main(int argc, char **argv) {
_OPEN_URL = split(cf.get<std::string>("Proxy.opened", ""), ';');
_DB_PATH = cf.get<std::string>("Database.path", "");
_TOKEN_KEEP_TIME = cf.get<int>("Token.keep_time", 3600);
_TOKEN_REFRESH_GRACE_MULTIPLIER = cf.get<int>("Token.refresh_grace_multiplier", 2);
_TOKEN_MAX_REFRESH = cf.get<int>("Token.max_refresh", 10);
_CORS_ORIGINS = split(cf.get<std::string>("CORS.allowed_origins", ""), ';');
_ADMIN_USERS = split(cf.get<std::string>("Admin.users", ""), ';');
}
@@ -621,6 +630,11 @@ void registerRoutes(HttpServer &server) {
[](const auto& req, const auto& params) { return handleLogout(req, params); });
});
server.post("/refresh_token", [](const auto& req, const auto& params) -> http::response<http::string_body> {
return wrapperHttp(req, params,
[](const auto& req, const auto& params) { return handleRefreshToken(req, params); });
});
/* 代理 captcha 图形验证码(须在 get("*") 通配符之前注册) */
server.get("/captcha/image", [](const auto& req, const auto& params) -> http::response<http::string_body> {
return wrapperHttp(req, params,
@@ -876,9 +890,12 @@ http::response<http::string_body> handleLogin(
json::value jv_t = {
{"user", username},
{"iat", LocalTime::getCurrent()},
{"created_at", LocalTime::getCurrent()},
{"keep_time", _TOKEN_KEEP_TIME},
{"is_admin", is_admin}
{"is_admin", is_admin},
{"refresh_count", 0},
{"max_refresh", _TOKEN_MAX_REFRESH}
};
std::string data = json::serialize(jv_t);
std::string auth = Base64::base64_encode(data);
@@ -929,6 +946,159 @@ http::response<http::string_body> handleLogout(
return res;
}
/* 刷新 Token:POST /refresh_token
*
* 接受当前有效 Token(即使已过期但在宽限期内),签发新 Token。
* 仅更新 created_at 与 refresh_count;iat 保持不变以限制整条链的总生命周期。
*
* 宽限期:iat + keep_time * refresh_grace_multiplier
* 例:keep_time=3600, multiplier=2 → 首次签发后 2 小时内可刷新
* 刷新上限:refresh_count < max_refresh
* 限流:5 次 / 5 分钟 / IP
* Token 来源:Authorization: Bearer 优先,回退 Cookie q4_token
*
* 响应:{ success, data:{ token, refresh_count, refreshes_remaining } }
*/
http::response<http::string_body> handleRefreshToken(
const http::request<http::string_body>& req,
const std::unordered_map<std::string, std::string>& params)
{
http::response<http::string_body> res;
res.version(req.version());
/* IP 速率限制:5 次 / 300 秒 */
std::string client_ip = "direct";
auto it_host = req.base().find("X-Forwarded-For");
if (it_host != req.base().end())
client_ip = std::string(it_host->value());
if (!_REFRESH_TOKEN_RATE.check_and_record(client_ip, 5, 300))
THROW_EX_CUSTOM(HttpException, 4001, "Too many refresh attempts. Please try again later.",
429, "/refresh_token");
auto _PUBL = MyRSA::Public_Key::from_pem(_KEYGEN->get_public_key_pem());
/* 获取 Token:Authorization 头优先,回退 Cookie */
std::string raw;
auto it_auth = req.base().find("Authorization");
if (it_auth != req.base().end()) {
raw = std::string(it_auth->value());
} else {
auto cit = req.base().find("Cookie");
if (cit != req.base().end()) {
std::string cookie_token = get_cookie_value(std::string(cit->value()), "q4_token");
if (!cookie_token.empty())
raw = "Bearer " + cookie_token;
}
}
if (raw.empty())
THROW_EX_CUSTOM(HttpException, 4002, "No token provided", 400, "/refresh_token");
/* 解析 Bearer <base64>_<signature> */
if (raw.substr(0, 7) != "Bearer ")
THROW_EX_CUSTOM(HttpException, 4002, "Invalid token format", 400, "/refresh_token");
raw = raw.substr(7);
size_t idx = raw.find("_");
if (idx == std::string::npos)
THROW_EX_CUSTOM(HttpException, 4002, "Invalid token format", 400, "/refresh_token");
std::string data = raw.substr(0, idx);
std::string sign = raw.substr(idx + 1);
std::string auth = Base64::base64_decode_to_string(data);
/* RSA 验签 */
if (!_PUBL.Verify(auth, sign))
THROW_EX_CUSTOM(HttpException, 4003, "Invalid token signature", 401, "/refresh_token");
/* 解析 Token payload */
json::value jv_t;
try { jv_t = json::parse(auth); }
catch (const std::exception&) {
THROW_EX_CUSTOM(HttpException, 4004, "Bad token payload", 400, "/refresh_token");
}
if (!jv_t.is_object())
THROW_EX_CUSTOM(HttpException, 4004, "Bad token payload", 400, "/refresh_token");
json::object& obj = jv_t.as_object();
/* 提取关键字段 */
std::string username = json::value_to<std::string>(obj.at("user"));
std::string iat_str = json::value_to<std::string>(obj.at("iat"));
int keep_time = json::value_to<int>(obj.at("keep_time"));
int refresh_count = json::value_to<int>(obj.at("refresh_count"));
int max_refresh = json::value_to<int>(obj.at("max_refresh"));
/* 刷新次数检查 */
if (refresh_count >= max_refresh)
THROW_EX_CUSTOM(HttpException, 4005, "Refresh limit exceeded. Please re-login.",
400, "/refresh_token");
/* 宽限期检查:iat + keep_time * grace_multiplier > now */
auto iat_tp = LocalTime::parse(iat_str);
if (!iat_tp)
THROW_EX_CUSTOM(HttpException, 4004, "Bad token iat", 400, "/refresh_token");
auto now = std::chrono::system_clock::now();
auto grace_expire = *iat_tp + std::chrono::seconds(keep_time * _TOKEN_REFRESH_GRACE_MULTIPLIER);
if (now > grace_expire)
THROW_EX_CUSTOM(HttpException, 4006, "Token expired beyond refresh window. Please re-login.",
401, "/refresh_token");
/* 保留 is_admin(旧 Token 未携带时默认为 false) */
bool is_admin = false;
auto it_admin = obj.find("is_admin");
if (it_admin != obj.end() && it_admin->value().is_bool())
is_admin = it_admin->value().as_bool();
/* 签发新 Token:iat 不变,created_at 更新,refresh_count++ */
auto _PRIV = MyRSA::Private_Key::from_pem(_KEYGEN->get_private_key_pem());
json::value jv_new = {
{"user", username},
{"iat", iat_str},
{"created_at", LocalTime::getCurrent()},
{"keep_time", keep_time},
{"is_admin", is_admin},
{"refresh_count", refresh_count + 1},
{"max_refresh", max_refresh}
};
std::string new_data = json::serialize(jv_new);
std::string new_auth = Base64::base64_encode(new_data);
std::string new_sign = _PRIV.Sign(new_data);
std::string new_token = new_auth + "_" + new_sign;
/* Set-Cookie */
{
std::string ck = "q4_token=" + new_token +
"; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=" +
std::to_string(keep_time);
res.set("Set-Cookie", ck);
}
logs().info("gateway", "Token refreshed", std::nullopt,
json::serialize(json::value{
{"user", username},
{"ip", client_ip},
{"refresh_count", refresh_count + 1},
{"refreshes_remaining", max_refresh - (refresh_count + 1)}
}));
json::value jv_rr = {
{"success", true},
{"data", json::object{
{"token", new_token},
{"refresh_count", refresh_count + 1},
{"refreshes_remaining", max_refresh - (refresh_count + 1)}
}}
};
res.set("Connection", "close");
res.result(http::status::ok);
res.set(http::field::content_type, "application/json");
res.body() = json::serialize(jv_rr);
res.prepare_payload();
return res;
}
/* 代理 captcha 图形验证码:GET /captcha/image
*
* captcha 服务为 pymsg/TCP(:8009),浏览器无法直接访问,
+50 -5
View File
@@ -7,18 +7,23 @@
## 架构
```
gateway :443 (反向代理)
waf :443 (TLS 终止 + 安全规则)
│
▼
gateway :8443 (反向代理 + 认证)
│
▼
static :9200 (Flask + CORS)
│
└── public/ 托管目录
├── index.html 社区首页
├── login.html 登录
├── login.html 登录(默认首页)
├── register.html 注册
├── index.html 社区首页
├── forgot.html 忘记密码
├── dnfs.html 树状笔记(调用 dnfs:8006)
├── __view.html 文件浏览器(系统文件,不在列表中显示)
├── allcolor.html 调色板
├── LICENSE.html 许可证页面
├── css/ theme.css(统一主题)+ view.css
├── js/ dnfs.js / login.js / register.js / validation.js / view.js
├── games/ HTML 小游戏合集
@@ -27,15 +32,46 @@ static :9200 (Flask + CORS)
## 路由
### 核心路由
| 路径 | 说明 |
|------|------|
| `GET /` | 社区首页(index.html),不存在时回退 `__view.html` |
| `GET /` | 登录页(login.html),如不存在回退 `__view.html` |
| `GET /<path>` | 静态文件服务;目录返回 `__view.html` 由前端拉取列表 |
| `GET /api/filelist?path=` | 文件列表 API,过滤 `__` 前缀系统文件 |
| `GET /video/<path>` | 视频/音频流媒体(HTTP Range 206 分段) |
| `GET /<path>?download=true` | 强制下载 |
| `GET /health` | 健康检查 |
### 内部工具路由
| 路径 | 说明 |
|------|------|
| `POST /api/hash` | SHA256 密码哈希 |
| `POST /api/check_user` | 检查用户是否已注册(查询 gateway 数据库) |
| `POST /api/reset_password` | 重置用户密码(直接操作 gateway 数据库) |
### yhdet OAuth 路由
| 路径 | 说明 |
|------|------|
| `GET/POST /yhdet_callback` | yhdet.top OAuth 回调,自动注册/登录并跳转 dnfs 页 |
| `POST /api/yhdet_login` | yhdet OAuth 用户名密码登录 |
| `POST /api/yhdet_verify` | yhdet Token 验证 |
| `POST /api/proxy_send_code` | yhdet 验证码代理转发 |
### dnfs 代理路由
| 路径 | 说明 |
|------|------|
| `POST /get_notes` | 获取树状笔记列表(代理至 dnfs:8006) |
| `POST /add_note` | 新增笔记(代理至 dnfs:8006) |
| `POST /delete_note` | 删除笔记(代理至 dnfs:8006) |
| `POST /update_note` | 更新笔记(代理至 dnfs:8006) |
| `POST /whoami` | 当前用户身份查询(代理至 dnfs:8006) |
以上 5 个 dnfs 代理路由均支持 HttpOnly Cookie 模式:当请求携带 `q4_token` Cookie 时,自动转为 `Authorization: Bearer` 头转发。
## 功能
- 文件浏览:目录导航、面包屑、实时搜索(`/` 或 `Ctrl+F`)、文件统计
@@ -43,6 +79,8 @@ static :9200 (Flask + CORS)
- 深色模式:`css/theme.css` 统一主题,自动适配系统偏好
- 路径遍历防护:`safe_path_join` 限制访问范围在 `directory.root` 内
- Nuitka 编译:可打包为独立二进制 `fsys.bin`(`sys.frozen` 检测 + BASE_DIR 锚定)
- 安全头注入:`security_middleware.py` 为所有响应注入 CSP、X-Frame-Options 等安全头
- 集中日志:`log_client.py` 通过 pymsg TCP 异步批量推送日志至 db:8005 的 Logs 表
## 配置
@@ -60,6 +98,11 @@ root = ./public
enabled = false
cert_file = cert.pem
key_file = key.pem
[cors]
# 允许跨域的 Origin 白名单(逗号分隔)
# 注意:生产环境请勿包含 localhost / 127.0.0.1 / 通配符 *
allowed_origins = https://qsont.me,https://yhdet.top
```
## 依赖
@@ -67,7 +110,9 @@ key_file = key.pem
```
flask>=2.3.0,<3.0.0
flask-cors>=4.0.0,<5.0.0
nuitka # 可选,编译为独立二进制
waitress>=2.1.2,<4.0.0 # 生产级 WSGI 服务器(非 SSL 模式默认使用)
pymsg # 集中日志推送(log_client 依赖)
nuitka # 可选,编译为独立二进制
```
## 运行
+49 -12
View File
@@ -22,7 +22,7 @@
| 列出所有规则 | `./waf.bin -list` |
| 测试输入 | `./waf.bin -check "/?id=1' OR '1'='1"` |
| 热加载(不重启) | `kill -HUP $(pgrep waf)` |
| 临时禁用某文件 | `mv rules/local/x.conf x.conf.bak && kill -HUP $(pgrep waf)` |
| 临时禁用某文件 | `mv rules/local/x.conf x.conf.bak && kill -HUP $(pgrep waf)` (引擎仅加载 `.conf` 后缀) |
### 规则文件组织
@@ -32,7 +32,7 @@ rules/
├── local/ # 本地自定义规则(ID 段 100000-199999)
│ ├── custom.conf # 示例:SQL注入/XSS/路径遍历/命令注入/扫描器UA
│ └── exceptions.conf # 误报白名单(SecRuleRemoveById / ctl:ruleRemoveById)
└── crs/ # OWASP CRS(不入库,由 _fetch_crs.sh 拉取)
└── crs/ # OWASP CRS(不入库,由 _fetch_crs.sh 拉取,可选)
├── coraza.conf # CRS 入口(请求体处理器初始化 + tx.crs_setup_version)
├── LICENSE # Apache-2.0
└── rules/*.conf # CRS 规则文件
@@ -54,8 +54,8 @@ rules/
| ID 段 | 用途 |
|-------|------|
| 100000-199999 | 本地自定义规则 |
| 200000-200006 | CRS 请求体处理器初始化(XML/JSON,由 `crs/coraza.conf` 定义) |
| 900000-999999 | OWASP CRS 规则(由 `crs/rules/` 提供,不手改) |
| 200000-200006 | CRS 请求体处理器初始化(XML/JSON,由 `crs/coraza.conf` 定义,仅当启用 CRS 时存在) |
| 900000-999999 | OWASP CRS 规则(由 `crs/rules/` 提供,不手改,仅当启用 CRS 时存在) |
### 规则语法(SecLang)
@@ -99,7 +99,7 @@ rules_dir: "./rules"
|------|--------|------|
| `listen` | `:443` | 监听地址 |
| `tls_cert` / `tls_key` | — | TLS 证书与私钥路径 |
| `upstream` | — | gateway 地址,**必填** |
| `upstream` | — | gateway 地址,**必填**(启动时硬性校验,缺失将导致进程退出) |
| `rules_dir` | `./rules` | 规则根目录 |
## 目录结构
@@ -112,8 +112,10 @@ waf/
├── internal/
│ ├── engine/
│ │ └── engine.go # coraza 封装:加载/热加载/列举/单测/输入检测
│ ├── logclient/
│ │ └── logclient.go # Go 版 pymsg 日志客户端(异步批量推送)
│ └── proxy/
│ └── proxy.go # ReverseProxy + WAF 事务接入
│ └── proxy.go # ReverseProxy + WAF 事务接入 + 安全响应头注入
├── rules/
│ ├── waf.conf
│ └── local/
@@ -156,13 +158,38 @@ bash /opt/micro_servers/waf/_run.sh
并把 `gateway` 改为内部 HTTP 端口(不再监听 443)。
## 安全响应头
WAF 在边缘层统一注入安全响应头([proxy.go:54-77](internal/proxy/proxy.go#L54-L77) `setSecurityHeaders`),覆盖所有出站响应(包括正常转发、命中拦截、上游 502)。下游服务(static/dnfs 等)的同名头会被 WAF 覆盖,避免 CSP 绕过。
| Header | 值 | 说明 |
|--------|----|------|
| X-Frame-Options | SAMEORIGIN | 允许本站 iframe 嵌套(登录页条例弹窗),防跨站点击劫持 |
| X-Content-Type-Options | nosniff | — |
| X-XSS-Protection | 1; mode=block | 现代浏览器已废弃,仅兼容旧浏览器 |
| Referrer-Policy | strict-origin-when-cross-origin | — |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | — |
| Strict-Transport-Security | max-age=15552000 | HSTS 半年期,**不含子域** |
| Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; ... | 暂保留 `'unsafe-inline'` 以兼容 forgot.html 与 games/*.html 的内联脚本 |
## 拦截日志
命中规则时 `block()` 经 logclient(Go 版 pymsg 客户端,见 [internal/logclient/](internal/logclient/))异步批量推送到 db:8005 的 `Logs` 表(与其他服务的集中日志同链路;标准日志已静默,stdout/journalctl 中看不到拦截记录):
- `service=waf`、`level=WARNING`
- `message`:`block rule_id=100001 GET /path`
- `extra`(JSON 字符串):`rule_id` / `ip` / `method` / `path` / `query` / `ua` / `cost_ms`
- **规则命中拦截**(block 触发):`service=waf`、`level=WARNING`
- **规则加载/解析错误**(`LogError` 回调触发,引擎初始化/热加载时的语义错误):`service=waf`、`level=ERROR`
`block()` 推送的日志 `extra` 字段(JSON 字符串):
| 字段 | 类型 | 说明 |
|------|------|------|
| `rule_id` | int | 命中规则 ID |
| `ip` | string | `RemoteAddr` 原始值,含端口号(如 `1.2.3.4:54321`) |
| `method` | string | 请求方法 |
| `path` | string | 请求路径 |
| `query` | string | 查询字符串 |
| `ua` | string | User-Agent |
| `cost_ms` | int64 | 处理耗时(毫秒) |
fail-safe:db 不可用时日志静默丢弃,不影响拦截动作本身。
@@ -181,6 +208,13 @@ sqlite3 /opt/micro_servers/db/database.db \
WHERE service='waf' AND level='WARNING' GROUP BY rule ORDER BY 2 DESC;"
```
查看规则加载/解析错误:
```bash
sqlite3 /opt/micro_servers/db/database.db \
"SELECT ts, message, extra FROM Logs WHERE service='waf' AND level='ERROR' ORDER BY id DESC LIMIT 20;"
```
## 启用 OWASP CRS(可选)
通过 Go module proxy 拉取(不依赖 GitHub 直连,国内可用):
@@ -214,26 +248,29 @@ kill -HUP $(pgrep waf)
## 请求处理流程
[proxy.go:57-117](internal/proxy/proxy.go#L57) `ServeHTTP`:
[proxy.go:81-138](internal/proxy/proxy.go#L81) `ServeHTTP`:
1. `ProcessConnection` + `ProcessURI` + 添加请求头(含 Host)
2. `ProcessRequestHeaders` → 命中即 `block` 返回 403
3. 读全量请求体 → `ReadRequestBodyFrom` → `ProcessRequestBody` → 命中即 block
4. `httputil.ReverseProxy.ServeHTTP` 转发到 gateway
5. 捕获响应头 → `ProcessResponseHeaders`
6. `ModifyResponse` 统一注入安全响应头(覆盖下游同名头)
Director 改写([proxy.go:36-46](internal/proxy/proxy.go#L36)):剥离 `RemoteAddr` 端口,设置 `X-Forwarded-For` 与 `X-Forwarded-Proto: https`,供 gateway 的 RateLimiter 取真实客户端 IP。
ErrorHandler([proxy.go:48-51](internal/proxy/proxy.go#L48)):upstream 不可达时返回 502,不暴露内部堆栈,仅服务端 log。
ErrorHandler([proxy.go:48-53](internal/proxy/proxy.go#L48)):upstream 不可达时返回 502,同时注入安全响应头,不暴露内部堆栈,仅服务端 log。
## 已知限制
- 请求体全量读入内存检测,10MB 上限(`SecRequestBodyLimit`)。大文件上传场景需调大或加白名单。
- **响应体检测未启用**:仅 `ProcessResponseHeaders`,未 `ProcessResponseBody`(缓冲整个响应延迟成本高)。SQL 报错回显等响应体泄漏检测缺失。
- **响应头检测已启用**(`ProcessResponseHeaders`),**响应体检测未启用**(未 `ProcessResponseBody`,缓冲整个响应延迟成本高)。SQL 报错回显等响应体泄漏检测缺失。
- 限流/黑白名单未实现,可后续作为 coraza 规则或独立中间件加。
- `http.Server` 未设 `ReadTimeout` / `WriteTimeout` / `IdleTimeout`,存在慢连接攻击风险。
- TLS 仅设 `MinVersion: TLS1.2`,未限定 cipher suites,允许弱加密套件。
- `X-Forwarded-For` 直接覆盖原值,未按 RFC 7239 追加;当前 gateway 也只取第一个 IP,无实际影响,但语义不严格。
- CSP 仍含 `'unsafe-inline'`(为兼容 forgot.html 与 games/*.html 的内联脚本),待前端清理后可收紧。
- HSTS 已启用(半年期,不含子域),站点全量 HTTPS 覆盖后可按需收紧。
## 依赖