🎉 Sync 2026-09-27 21:03:26

This commit is contained in:
github-actions[bot] committed 2026-09-27 21:03:26 +08:00
1 parent f4559bbed1
commit 065763f7fd
673 files changed
+261285 -34160

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
# 架构设计 — luci-app-substore
OpenWrt / ImmortalWrt 原生 LuCI 机场订阅管理工具。参考 Sub-Store 功能体验,独立设计与实现,不复制上游代码。
## 1. 目标与范围
原生 OpenWrt 软件包,不使用 Docker,不依赖外部云端服务。在低性能路由器(内存 ≤ 512MB,存储紧张)上运行。
功能范围:
- 订阅管理:多订阅源增删改查、手动/定时更新、更新时间/节点数/状态展示
- 节点管理:解析常见代理协议、查看/搜索/筛选/排序/重命名/去重
- 订阅处理:按条件筛选、去重、重命名、排序、多订阅合并、生成客户端订阅
- 输出格式:13 种全量实现(Plain JSON / Stash / Clash.Meta / Surfboard / Surge / SurgeMac / Loon / Egern / Shadowrocket / QX / sing-box / V2Ray / V2Ray URI)
- 订阅链接:转换结果以订阅链接形式下发,供 Passwall / OpenClash 等拉取
- LuCI 界面:订阅列表、编辑更新、节点查看、规则配置、输出复制、状态日志
## 2. 技术选型
| 层 | 选型 | 理由 |
|----|------|------|
| 后端核心 | **Lua 5.1 纯脚本库** | 用户选定;不依赖 luci.*,可独立测试;跨 LuCI 版本稳定 |
| LuCI 前端 | **Lua 兼容模式 (luci-lua-runtime)** | 用户选定;在 23.05+/24.10+/25.12 以 Lua 兼容层运行 |
| 数据存储 | 独立数据文件 `/etc/substore/*.json` | CLAUDE.md 要求订阅/状态用独立文件,不用 UCI 存节点数据 |
| 配置 | UCI `/etc/config/substore` | 仅存订阅源元数据、处理规则、输出设置 |
| 后台服务 | procd(仅当需要常驻/定时时) | 定时更新可走 LuCI 页面触发 + cron,避免常驻 |
| 输出接口 | LuCI 路由 / CGI,带随机 Token 鉴权 | CLAUDE.md 安全要求 |
## 3. 模块划分
```
luci-app-substore/
├── Makefile # 软件包构建
├── root/
│ ├── etc/config/substore # UCI 配置
│ ├── usr/lib/lua/luci/ # LuCI 前端(controller / model / view)
│ └── usr/share/substore/ # 纯 Lua 核心库 + 数据目录
├── src/ # 可选:需编译的辅助(暂无)
└── docs/ # 文档
```
核心库(`/usr/share/substore/`,纯 Lua,不依赖 luci.*):
- `core.lua` — 订阅源元数据读写、状态管理、订阅链接生成(token + generate_link)
- `parser.lua` — 订阅格式解析(Base64/URI/Clash YAML/JSON)
- `node.lua` — 节点模型、筛选、去重、重命名、排序
- `node_converter.lua` — 协议间字段映射与协议转换
- `converter.lua` — 通用转换器(字段映射、URL 模板渲染)
- `output.lua` — 统一格式分发(FORMAT_ALIASES 别名映射、content-type/扩展名)
- `output_clash_meta.lua` — Clash.Meta / Mihomo / Stash YAML
- `output_uri.lua` — 分享链接 URI / Shadowrocket / V2Ray URI
- `output_singbox.lua` — sing-box JSON
- `output_v2ray.lua` — V2Ray/Xray JSON
- `output_formats.lua` — Surge 系 / Loon / QX / Egern / Plain JSON
- `http.lua` — 订阅下载(SSRF 防护、超时、大小限制)
- `util.lua` — 通用工具(原子写、时间、日志、Base64、JSON)
LuCI 前端(`/usr/lib/lua/luci/`):
- `controller/admin/substore.lua` — 路由注册
- `model/cbi/substore/*.lua` — 配置界面
- `view/substore/*.htm` — 页面模板
## 4. 数据流
订阅 URL / 本地文本 → `http.lua` 下载(SSRF 防护)→ `parser.lua` 解析 → `node.lua` 统一节点模型 → 去重/筛选/重命名/排序 → `output.lua` 按目标格式生成 → LuCI 界面 / 订阅下载端点
**订阅链接下发流程**:每个订阅在创建时生成随机 16 位十六进制 `token`,存入元数据。客户端请求公开端点
`GET /substore/download?token=<token>&target=<format>`(如 `target=ClashMeta`),无需登录态、靠不可猜测的 token 鉴权,
按 `target` 实时转换节点并返回(`node_converter.lua` 负责协议间转换,`output.*` 负责格式生成)。
节点统一内部模型(协议无关):
```lua
{
proto = "vmess"|"vless"|"trojan"|"shadowsocks"|"hysteria2"|"tuic"|"ss"|...,
name = "节点名",
server = "host",
port = 443,
uuid = "uuid", password = "...", cipher = "...", -- 协议特有字段
raw = "原始URI串" -- 保留原始,便于无损转发
}
```
## 5. 安全设计(CLAUDE.md 第 6 节)
- **SSRF 防护**:`http.lua` 下载前解析主机名,拒绝 localhost/内网/链路本地/保留地址;DNS 解析后二次校验;处理重定向
- **资源限制**:订阅响应体最大大小(默认 10MB 可配)、连接/总超时、并发限制
- **路径穿越**:订阅名/文件名做白名单校验,禁止 `..`、`/`
- **命令注入**:不将用户输入拼入 shell;需要时用参数化执行
- **凭据保护**:订阅 URL 中的 token/密码不写入普通日志;输出接口用随机 Token 鉴权
- **导入校验**:解析时对结构/大小/嵌套深度做限制,防恶意 YAML/JSON 资源耗尽
- 所有安全策略结合 OpenWrt 实际网络环境实现并编写测试
## 6. 兼容性约束
- 目标 LuCI 使用 Lua 兼容模式,需依赖 `luci-lua-runtime` + `luci-compat`(23.05+)
- Lua 控制器沿用 `module()` + `entry()` 风格(24.10 有效,活跃插件仍在使用)
- **25.12 风险**:LuCI 模板优先级已转向 `.ut`(ucode),`.htm` 为 Legacy。阶段4 已评估迁移方案,详见 `docs/UCODE_MIGRATION.md`(含 `.htm`→`.ut` 语法对照与逐模板映射;核心库纯 Lua 可通过 `include()` 复用,迁移成本集中在 5 个模板的胶水代码)
- 不假设所有 OpenWrt 版本依赖一致,分版本适配
- 纯 Lua 核心库优先用 Lua 5.1 标准库(luci 自带 lua5.1),避免 luarocks 依赖
- 如需要 YAML 解析:优先评估纯 Lua 实现或 luci 内置,避免重运行时
- `/lib/functions/luci.sh` 已移除,postinst 用 `rm -f /tmp/luci-indexcache` + `/etc/init.d/luci reload` 刷新缓存
## 7. 文档清单
README.md、docs/ARCHITECTURE.md、docs/BUILD.md、docs/INSTALL.md、docs/SECURITY.md、docs/TESTING.md、CHANGELOG.md
+22
View File
@@ -0,0 +1,22 @@
# Building — luci-app-substore
## Prerequisites
An OpenWrt / ImmortalWrt SDK or full source tree matching the target firmware version.
## Build
Place this package under the OpenWrt tree:
```bash
cd <openwrt-sdk-or-source>
# copy or symlink the package
cp -r <path>/luci-app-substore package/
make package/luci-app-substore/compile V=s
```
The `.ipk` (or `.apk` on apk-based builds) is produced under
`bin/packages/.../luci-app-substore_*.ipk`.
> NOTE: stage-0 skeleton has not yet been built/verified on a target device.
> This document will be updated once the build chain is validated (see docs/PLAN.md stage 0).
+27
View File
@@ -0,0 +1,27 @@
# Installation — luci-app-substore
> NOTE: stage-0 skeleton has not yet been installed/verified on a target device.
> This document will be updated once installation is validated.
>
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.1.3-r6)。
## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早)
```bash
opkg install luci-app-substore-2.1.3-r6.ipk
```
## Install (apk — OpenWrt / ImmortalWrt 25.12+)
```bash
apk add --allow-untrusted luci-app-substore-2.1.3-r6.apk
```
Refresh LuCI:
```bash
rm -f /tmp/luci-indexcache
/etc/init.d/luci reload
```
Then open LuCI menu: **Services → Subscriptions** (or **System → Subscriptions** depending on firmware).
+99
View File
@@ -0,0 +1,99 @@
# 分阶段开发计划 — luci-app-substore
架构覆盖完整功能,交付分阶段进行。每阶段产出可编译、可安装、可验证的产物。每阶段遵循 CLAUDE.md 流程:说明目标 → 检查现有代码 → 列改动文件 → 小范围实施 → 静态检查/测试 → 检查 diff → 更新文档 → 汇报。
## 阶段 0 — 骨架与构建链路验证
**目标**:最小可编译、可安装的包骨架,跑通 OpenWrt 构建与安装全链路。
**产出**:
- Makefile(含 DEPENDS、安装路径、postinst)
- LuCI 菜单注册 + 一个占位页面(订阅列表空态)
- UCI 配置文件 `/etc/config/substore` + uci-defaults
- docs/BUILD.md、docs/INSTALL.md、README.md(骨架版)
**验收**:在目标 OpenWrt 设备上 `make package/luci-app-substore/compile` 成功、安装后 LuCI 出现菜单并可打开页面。
## 阶段 1 — 订阅源管理与下载解析
**目标**:订阅源 CRUD + URL 下载 + 本地粘贴导入 + 基础解析。
**产出**:
- `core.lua`:订阅源元数据读写(独立 JSON 数据文件)
- `http.lua`:SSRF 防护、超时、大小限制的下载器
- `parser.lua`:Base64/URI 订阅解析 → 统一节点模型(首批协议:vmess/vless/trojan/shadowsocks/ss)
- `node.lua`:节点模型基础
- LuCI 订阅列表页 + 添加/编辑/更新/删除
- UCI 存订阅源元数据;节点数据存独立文件
**验收**:添加真实订阅 → 手动更新 → 列表显示节点数、更新时间、解析成功状态。
## 阶段 2 — 节点处理与筛选
**目标**:节点查看、搜索、筛选、排序、重命名、去重。
**产出**:
- `node.lua`:筛选(协议/名称/关键词)、去重(同 server+port+proto)、重命名、排序
- LuCI 节点查看页(列表/搜索/筛选)
- 解析器测试样例
**验收**:对含重复、多协议的真实订阅,去重/筛选/重命名/排序结果正确。
## 阶段 3 — 多订阅合并与输出(订阅转换 + 订阅链接)
**目标**:任意类型节点转换为任意类型,13 种输出格式全量实现,转换结果以订阅链接形式下发。
**产出**:
- `output.lua`:统一格式分发(FORMAT_ALIASES 别名映射、content-type / 扩展名)
- `output_clash_meta.lua`:Clash.Meta / Mihomo / Stash YAML
- `output_uri.lua`:分享链接 URI / Shadowrocket(base64) / V2Ray URI
- `output_singbox.lua`:sing-box JSON
- `output_v2ray.lua`:V2Ray/Xray JSON
- `output_formats.lua`:Surge / Surfboard / SurgeMac / Loon / QX / Egern / Plain JSON
- `node_converter.lua` + `converter.lua`:协议间转换、URL 模板渲染
- `parser_surge.lua`:Surge / Loon / QX 客户端配置导入
- `parser.lua` 接线:sing-box / V2Ray / Clash JSON / Clash YAML / Surge / QX 输入源
- `core.generate_link(token, target)` + 每订阅随机 token
- LuCI 公开下载端点 `/substore/download?token=<token>&target=<format>`
- LuCI:output 页 13 格式下拉、subscriptions 页展示可复制的订阅链接
**验收**:合并多订阅生成 13 种格式,Passwall / OpenClash 可通过订阅链接拉取。
## 阶段 4 — 定时更新、规则、完善
**目标**:定时更新(cron)、规则配置、错误处理、安全加固、多版本兼容。
**产出**:
- 定时更新(cron / procd)
- 规则配置页
- 完整错误处理、日志
- 分版本依赖适配(23.05 / 24.10 / 25.12)
- **模板迁移:`.htm` → `.ut`(ucode),确保 25.12 兼容**(调研:25.12 模板优先级 `.ut` only)
- 订阅下载方式定案:curl vs Lua socket(影响 SSRF 防护实现,阶段1评估)
- docs/SECURITY.md、docs/TESTING.md、CHANGELOG.md
**验收**:定时更新生效、错误日志可读、各 OpenWrt 版本兼容、安全测试通过。
---
## 依赖关系
阶段 0 → 1 → 2 → 3 → 4(顺序推进,每阶段验证通过再进下一阶段)
## 当前状态
- [x] 调研:OpenWrt LuCI 24.10 有 luci-lua-runtime(Lua 兼容层),Lua 方案可行
- [x] 架构设计:docs/ARCHITECTURE.md
- [x] 分阶段计划:本文档
- [x] 阶段0:骨架构建链路验证通过(菜单位于 服务 → Subscriptions)
- [x] 阶段1:订阅 CRUD + 下载解析核心实现(core/util/node/parser/http + LuCI 列表/表单)
- [ ] 阶段1验证:用户在设备上添加真实订阅、手动更新、查看节点数/更新时间
- [x] 阶段2:节点处理与筛选(node.lua 筛选/去重/重命名/排序 + 分组/标签/重命名规则)
- [x] 阶段3:订阅转换 + 订阅链接(13 种输出格式、协议转换、输入源扩展、下载端点)
- [x] 阶段4:定时更新(cron)、规则配置、错误处理、安全加固、多版本兼容
- [x] cron 定时更新(substore-cron.sh 独立可运行、设置页 cron 配置)
- [x] 规则配置页(协议过滤/关键词/去重/重命名规则,含精确·正则·模板三形式)
- [x] 错误处理(core.sync 日志、action_update pcall 兜底、列表状态列展示错误)
- [x] 安全加固(下载文件名白名单防头注入;SSRF/大小/超时此前已实现)
- [x] 多版本兼容(LUCI_DEPENDS luci-lua-runtime + luci-compat)
- [ ] 模板迁移 `.htm` → `.ut`(ucode):已评估并写 docs/UCODE_MIGRATION.md,未执行——需 25.12 构建环境验证
+38
View File
@@ -0,0 +1,38 @@
# Security — luci-app-substore
## SSRF 防护
- `http.lua` 下载前解析主机名,拒绝 localhost/内网/链路本地/保留地址
- DNS 解析后二次校验,处理重定向
- 仅允许 http/https 协议
## 资源限制
- 订阅响应体最大 10MB,可配置
- 连接/总超时 20s
- 并发限制由系统调度
## 输入校验
- 订阅名/文件名白名单校验,禁止 `..`、`/`
- 解析时限制结构/大小/嵌套深度,防 YAML/JSON 资源耗尽
- URL 参数解码后二次校验
## 凭据保护
- 订阅 URL 中的 token 不写入普通日志
- 输出/下载接口使用**每订阅随机 16 位十六进制 token** 鉴权(`core.ensure_token`),不可猜测;`/substore/download` 无登录态
- 日志使用 `logger -t luci-app-substore`,不记录敏感信息
## 安全测试
- 已验证 SSRF 私网拒绝
- Base64/JSON 解析异常处理
- 超大响应体截断测试
参见 docs/ARCHITECTURE.md 第 5 节安全设计。
Key requirements (from CLAUDE.md):
- Prevent SSRF (reject localhost / private / link-local / reserved addresses)
- DNS rebinding protection
- Limit subscription response body size and set request timeouts
- Prevent path traversal and command injection
- Never log subscription credentials / tokens
- Output interface requires access control or a random token
- Validate imported data; prevent malicious YAML/JSON resource exhaustion
+37
View File
@@ -0,0 +1,37 @@
# Testing — luci-app-substore
## 单元测试
`tests/` 下每个测试文件均可独立运行,全部为纯 Lua 5.1:
| 测试文件 | 覆盖内容 |
|---------|---------|
| `run_tests.lua` | util(base64/json/url/hostport)、node、parser 基础 |
| `converter_*_test.lua` | 协议转换、URL 模板渲染 |
| `node_*_test.lua` | 节点模型扩展、分组、重命名 |
| `output_clash_meta_test.lua` | Clash.Meta / Mihomo YAML 生成 |
| `output_formats_test.lua` | 13 种目标格式统一分发 |
| `core_link_test.lua` | 订阅 token + generate_link 链接生成 |
| `parser_clash_yaml_test.lua` | Clash YAML 解析 |
| `parser_json_config_test.lua` | sing-box / V2Ray / Clash JSON 解析 |
| `parser_input_test.lua` | 多客户端配置导入(sing-box/V2Ray/Surge/QX) |
| `parser_local_link_test.lua` | 局域网订阅链接检测与解析 |
运行全部:`for f in tests/*.lua; do lua5.1 "$f" || exit 1; done`
## 集成测试
1. 编译安装到目标设备
2. 添加订阅 URL,手动更新,验证节点数
3. 节点浏览:筛选、排序
4. 输出生成:13 种格式下拉均可生成
5. 订阅链接:复制 `/substore/download?token=...&target=ClashMeta` 到 Passwall/OpenClash 验证可拉取
6. 输入源:导入 Clash YAML / sing-box JSON / V2Ray JSON / Surge / QX 配置验证解析
7. 定时更新:修改 Settings cron,验证 `/etc/cron.d/substore` 生成
8. 规则应用:设置协议过滤、关键词,验证节点列表/输出受影响
## 安全测试
- SSRF:尝试内网 URL,被拒绝
- 大响应:>10MB 被截断
- 恶意 Base64/JSON:解析失败不崩溃
## 回归测试
每次阶段发布前执行 `tests/run_tests.lua` 并在目标设备手动验证菜单/功能。
+90
View File
@@ -0,0 +1,90 @@
# 模板迁移评估:`.htm` (Lua) → `.ut` (ucode)
> **状态**:评估完成,未执行迁移。原因:本机无 OpenWrt 构建/运行环境,`.ut` 模板无法编译验证,冒然上线未经验证的 `.ut` 比保留 `.htm` 更危险(若 25.12 模板加载器优先 `.ut`,带 bug 的 `.ut` 会直接覆盖原本可用的 `.htm`)。
## 1. 背景
本项目的 LuCI 前端采用 **Lua 兼容模式**(`luci-lua-runtime` + `luci-compat`),模板为 `.htm`(Lua 代码 + `<% %>` / `<%= %>` / `<%: %>` 标签)。
| LuCI 版本 | 模板体系 | 本项目的 `.htm` |
|-----------|----------|------------------|
| 23.05 | Lua(`.htm`) | ✅ 原生支持 |
| 24.10 | ucode(`.ut`)为主,Lua 兼容层保留 `.htm` | ✅ 经 `luci-lua-runtime` |
| 25.12+ | 趋向 ucode(`.ut`),Lua 兼容层可能移除 | ⚠️ 风险点 |
调研结论(来自 LuCI 上游变更):24.10 起 `luci-base` 重写为 ucode,模板加载器优先查找 `.ut`;`.htm` 通过 Lua 兼容层继续工作。25.12 若彻底移除 `luci-lua-runtime`,则 `.htm` 失效,需 `.ut` 模板。
## 2. 涉及文件(5 个模板)
`root/usr/lib/lua/luci/view/substore/` 下:
- `subscriptions.htm` — 订阅列表(表格、token 链接、复制按钮、多表单)
- `form.htm` — 添加/编辑订阅
- `nodes.htm` — 节点查看(GET 表单筛选/排序)
- `output.htm` — 输出页(格式下拉、textarea)
- `settings.htm` — 设置页(cron 选择器、规则)
## 3. `.htm` → `.ut` 语法对照
| 事项 | Lua `.htm` | ucode `.ut` |
|------|-----------|-------------|
| 输出标签 | `<%=expr%>` | `<%= expr %>`(ucode 表达式)或 `{{= expr }}` |
| 翻译标签 | `<%:Text%>` | `<%:Text%>`(保留,语义一致) |
| 引入其它模板 | `<%+header%>` / `<%+footer%>` | `<% include("header") %>` / `<% include("footer") %>` |
| 取 HTTP 参数 | `require("luci.http")` + `http.formvalue(...)` | 预绑定全局 `http`,`http.formvalue(...)` |
| 取查询参数 | `luci.dispatcher.context.query` | 预绑定全局 `REQUEST`,`REQUEST.query` |
| 迭代数组 | `for _, v in ipairs(t) do ... end` | `for (let v in t) { ... }` |
| 条件 | `if cond then ... end` | `if (cond) { ... }` |
| 字符串拼接 | `..` | `+` |
| 三元 | `a and b or c`(有陷阱) | `cond ? a : b` |
| 取长度 | `#t` | `length(t)` |
| 索引 | `t[k]` / `t.k` | `t[k]` / `t.k` |
| 核心库调用 | `require("substore.core")` 后 `core.xxx()` | `core = include("substore/core.lua")` 后 `core.xxx()` |
## 4. 关键决策点
1. **核心库无需改写**:`/usr/share/substore/*.lua` 是纯 Lua,ucode 通过 `include()` 加载 Lua 文件并调用其函数(ucode 内置 Lua 互操作)。这是本方案最有利的一点——迁移成本集中在 5 个模板文件的“胶水代码”,核心逻辑(解析/转换/输出)不受影响。
2. **控制器**:`controller/admin/substore.lua` 目前用 `module()` + `entry()`。24.10 仍支持;若 25.12 要求 ucode 控制器(`.uc`),则需另写 `controller/admin/substore.uc`。与模板一样,属胶水层。
3. **数据存储与下载端点**:`core.lua`、`action_download` 逻辑与模板语言无关,无需迁移。
## 5. 建议的落地策略
按“能验证再迁移”原则,分步:
- **第 1 步(当前)**:保持 `.htm` + `luci-lua-runtime` 依赖,确保 23.05 / 24.10 可用。
- **第 2 步**:在真实 24.10 设备上安装,验证 `.htm` 经兼容层正常渲染。
- **第 3 步**:拿到 25.12 构建环境后,逐模板迁移为 `.ut`(可 `.htm` 与 `.ut` 并存,LuCI 优先 `.ut`),每个模板迁完即在设备上验证页面。
- **第 4 步**:25.12 若移除 Lua 运行时,同步评估控制器 `.uc` 迁移。
## 6. 示例迁移(subscriptions.htm 片段)
Lua 现状:
```html
<%
local core = require("substore.core")
local items = core.list()
for _, it in ipairs(items) do
%>
<tr><td><%=it.name%></td></tr>
<%
end
%>
```
ucode 等价:
```html
<%
let core = include("substore/core.lua");
let items = core.list();
for (let it in items) {
%>
<tr><td><%= it.name %></td></tr>
<%
}
%>
```
> 注:`include("substore/core.lua")` 的路径与 ucode 的 `LUCI_PATH` / `package.path` 解析规则有关,具体以 25.12 目标机的 `include` 路径解析为准,迁移时需在设备上确认。