frpsctl 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {frpsctl-0.2.1 → frpsctl-0.2.2}/.github/workflows/ci.yml +39 -0
  2. {frpsctl-0.2.1 → frpsctl-0.2.2}/CHANGELOG.md +34 -0
  3. {frpsctl-0.2.1 → frpsctl-0.2.2}/PKG-INFO +62 -11
  4. {frpsctl-0.2.1 → frpsctl-0.2.2}/README.md +61 -10
  5. {frpsctl-0.2.1 → frpsctl-0.2.2}/frpsctl-/350/256/276/350/256/241/346/226/271/346/241/210.md +148 -3
  6. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/__init__.py +1 -1
  7. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/cli/__init__.py +198 -29
  8. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/cli/ui.py +8 -7
  9. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/admin.py +41 -4
  10. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/config.py +91 -13
  11. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/healthcheck.py +30 -0
  12. frpsctl-0.2.2/src/frpsctl/core/logs.py +52 -0
  13. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/systemd.py +238 -0
  14. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/transaction.py +58 -1
  15. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/server.py +6 -12
  16. frpsctl-0.2.2/src/frpsctl/web/__init__.py +35 -0
  17. frpsctl-0.2.2/src/frpsctl/web/api.py +452 -0
  18. frpsctl-0.2.2/src/frpsctl/web/auth.py +142 -0
  19. frpsctl-0.2.2/src/frpsctl/web/server.py +339 -0
  20. frpsctl-0.2.2/src/frpsctl/web/static/index.html +631 -0
  21. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/test_cli.py +66 -2
  22. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/test_facts.py +77 -0
  23. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/test_integration.py +57 -0
  24. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/test_units.py +218 -0
  25. frpsctl-0.2.2/tests/test_web.py +547 -0
  26. {frpsctl-0.2.1 → frpsctl-0.2.2}/.github/workflows/release.yml +0 -0
  27. {frpsctl-0.2.1 → frpsctl-0.2.2}/.gitignore +0 -0
  28. {frpsctl-0.2.1 → frpsctl-0.2.2}/LICENSE +0 -0
  29. {frpsctl-0.2.1 → frpsctl-0.2.2}/NOTICE +0 -0
  30. {frpsctl-0.2.1 → frpsctl-0.2.2}/install.sh +0 -0
  31. {frpsctl-0.2.1 → frpsctl-0.2.2}/pyproject.toml +0 -0
  32. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/__main__.py +0 -0
  33. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/cli/context.py +0 -0
  34. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/__init__.py +0 -0
  35. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/doctor.py +0 -0
  36. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/health.py +0 -0
  37. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/instance.py +0 -0
  38. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/lifecycle.py +0 -0
  39. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/lock.py +0 -0
  40. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/platform.py +0 -0
  41. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/release.py +0 -0
  42. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/schema.py +0 -0
  43. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/core/version.py +0 -0
  44. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/errors.py +0 -0
  45. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/__init__.py +0 -0
  46. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/audit.py +0 -0
  47. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/engine.py +0 -0
  48. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/policy.py +0 -0
  49. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/quota.py +0 -0
  50. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/plugin/types.py +0 -0
  51. {frpsctl-0.2.1 → frpsctl-0.2.2}/src/frpsctl/py.typed +0 -0
  52. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/__init__.py +0 -0
  53. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/conftest.py +0 -0
  54. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/fake_frps.py +0 -0
  55. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/faults.py +0 -0
  56. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/test_faults.py +0 -0
  57. {frpsctl-0.2.1 → frpsctl-0.2.2}/tests/test_plugin.py +0 -0
@@ -85,6 +85,45 @@ jobs:
85
85
  .venv/bin/frpsctl stop
86
86
  .venv/bin/frpsctl status
87
87
 
88
+ - name: Web 管理台冒烟(后台起服务 + HTTP 全流程)
89
+ run: |
90
+ set -euo pipefail
91
+ export FRPSCTL_ROOT="$RUNNER_TEMP/smoke/instances"
92
+ export FRPSCTL_DATA_HOME="$RUNNER_TEMP/smoke/data"
93
+ WP=$(python3 -c "import socket;s=socket.socket();s.bind(('127.0.0.1',0));print(s.getsockname()[1])")
94
+ .venv/bin/frpsctl web serve --bind "127.0.0.1:$WP" --password smoke-pw &
95
+ WEB_PID=$!
96
+ sleep 2
97
+ python3 - "$WP" <<'EOF'
98
+ import json, sys, urllib.request
99
+
100
+ base = f"http://127.0.0.1:{int(sys.argv[1])}"
101
+ login = urllib.request.Request(
102
+ base + "/api/login", method="POST",
103
+ data=json.dumps({"password": "smoke-pw"}).encode(),
104
+ headers={"Content-Type": "application/json"},
105
+ )
106
+ with urllib.request.urlopen(login, timeout=10) as resp:
107
+ cookie = resp.headers["Set-Cookie"].split(";")[0]
108
+ csrf = json.load(resp)["csrf"]
109
+
110
+ def call(path):
111
+ req = urllib.request.Request(base + path, headers={"Cookie": cookie})
112
+ with urllib.request.urlopen(req, timeout=10) as resp:
113
+ return json.load(resp)
114
+
115
+ status = call("/api/status")
116
+ assert status["state"] == "STOPPED", status
117
+ assert call("/api/session")["csrf"] == csrf
118
+ config = call("/api/config")
119
+ assert any(item["key"] == "bindPort" for item in config["entries"]), config
120
+ html = urllib.request.urlopen(base + "/", timeout=10).read().decode("utf-8")
121
+ assert "frpsctl 管理台" in html
122
+ print("web 冒烟通过:login / session / status / config / 静态页")
123
+ EOF
124
+ kill "$WEB_PID"
125
+ wait "$WEB_PID" 2>/dev/null || true
126
+
88
127
  no-binary:
89
128
  name: 无二进制降级路径(Python 3.13)
90
129
  runs-on: ubuntu-latest
@@ -3,6 +3,40 @@
3
3
  格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
4
  版本号遵循[语义化版本](https://semver.org/lang/zh-CN/)。
5
5
 
6
+ ## [0.2.2] - 2026-09-17
7
+
8
+ Web 管理台(内置界面)与 `kick` 语义修正。
9
+
10
+ ### 新增
11
+
12
+ - `frpsctl web serve`:内置 Web 管理台——仪表盘(状态 / 三层健康 / 7 天流量
13
+ 柱状图 / 会话内实时曲线)、客户端与代理列表、日志面板、进程启停、配置编辑
14
+ (预览打码 diff → 应用 → 失败自动回滚 → 一键回滚)。单文件前端、零外部
15
+ 资源(CSP `default-src 'none'`),默认只绑回环。
16
+ - `frpsctl web service install|uninstall|status`:Web 管理台的 systemd 集成
17
+ (生成 0600 口令文件并移交服务用户;口令明文不进 unit)。
18
+ - `frpsctl prune`:清理 dashboard 统计里的离线代理记录。
19
+ - 配置编辑的多键事务 `apply_sets` / `plan_set_many`:一次快照、一次重启,
20
+ 带 CAS(预览之后文件被改 → 拒绝而不是覆盖)。
21
+ - 契约层新增 **C9**(代理写 API 的真实语义)与 **C10**(traffic 端点的"无数据
22
+ = 404"语义,Web 容错的前提),0.70.0 上同样成立。
23
+
24
+ ### 修复
25
+
26
+ - **修正 `kick` 的语义错误**:frp 的 `DELETE /api/proxies` 实际是
27
+ `ClearOfflineProxies()`(只接受 `?status=offline`),**不存在**强制下线在线
28
+ 代理的 API。原 `kick` 按"按 name 下线"实现该端点,真机永远返回 400——一个
29
+ 从未工作过的功能(Web 端到端测试暴露)。已由 `prune` 取代。
30
+
31
+ ### 工程
32
+
33
+ - 新增 78 条测试(357 → 435),覆盖率 82%。含 Web 层的全路由认证扫描(16 条)、
34
+ 浏览器刷新恢复、TOML datetime 序列化、单代理故障注入等回归用例。
35
+ - core 下沉三处共用逻辑(消除重复实现):`parse_bind`(插件服务同时受益)、
36
+ `mask_value`(打码单点)、`core/logs.py`(`frpsctl log` 与 web 共用)。
37
+ - CI 新增 **Web 管理台冒烟**步骤(此前 CI 从不触碰 web):登录 → 会话恢复 →
38
+ 状态 → 配置 → 静态页,全链路验证。
39
+
6
40
  ## [0.2.1] - 2026-09-17
7
41
 
8
42
  第四轮全量迭代:并发根治、策略 fail-open 修复与便捷性命令。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: frpsctl
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: 把 frp 服务端(frps)包装成命令行工具:配置翻译器 + 进程保镖 + 状态聚合器
5
5
  Project-URL: Homepage, https://github.com/ThzxxArt/frpsctl
6
6
  Project-URL: Repository, https://github.com/ThzxxArt/frpsctl
@@ -46,9 +46,10 @@ Description-Content-Type: text/markdown
46
46
  - [看日志](#看日志)
47
47
  - [停下来](#停下来)
48
48
  - [体检](#体检)
49
- - [下线代理](#下线代理)
49
+ - [清理离线记录](#清理离线记录)
50
50
  - [多实例](#多实例)
51
51
  - [服务端插件(多用户鉴权 + 端口白名单)](#服务端插件多用户鉴权-端口白名单)
52
+ - [Web 管理台](#web-管理台)
52
53
  - [用 systemd 托管](#用-systemd-托管)
53
54
  - [升级 frps](#升级-frps)
54
55
  - [退出码(脚本化契约)](#退出码脚本化契约)
@@ -178,7 +179,7 @@ $ ./install.sh
178
179
  注册全局命令
179
180
  ✓ 已注册:/home/u/.local/bin/frpsctl
180
181
  自检
181
- ✓ 命令可用:frpsctl 0.2.1
182
+ ✓ 命令可用:frpsctl 0.2.2
182
183
 
183
184
  frpsctl 安装完成
184
185
  ```
@@ -235,7 +236,7 @@ dashboard 端口(0 = 不启用,将失去状态聚合能力) [7500]:
235
236
  ```
236
237
 
237
238
  **把这三项记下来**——`auth.token` 要填到每个 frpc 客户端,dashboard 口令用于
238
- `kick` 等操作。
239
+ `prune` 与 Web 管理台等操作。
239
240
 
240
241
  ```console
241
242
  $ frpsctl verify
@@ -410,7 +411,8 @@ alice.alice-ssh alice tcp 6000 online 0 0 B / 0 B
410
411
  $ frpsctl proxies --type http # 只看某类型
411
412
  ```
412
413
 
413
- `kick <proxy-name>` 下线指定代理(名字就是 `proxies` 列出的 name)。
414
+ `prune` 清理离线代理记录;在线代理无法从服务端强制下线(frp 没有该 API,
415
+ 见"清理离线记录"一节)。
414
416
 
415
417
  ### 改配置
416
418
 
@@ -546,13 +548,16 @@ $ frpsctl doctor
546
548
 
547
549
  **有 ERROR 时退出码为 1**,可直接接进 CI 或监控。
548
550
 
549
- ### 下线代理
551
+ ### 清理离线记录
550
552
 
551
553
  ```bash
552
- frpsctl kick my-ssh # DELETE /api/proxies
554
+ frpsctl prune # 清理 dashboard 统计里的离线代理记录
553
555
  ```
554
556
 
555
- 需要 dashboard 启用(`webServer.port > 0`);未启用时退出码 7。
557
+ ⚠️ **frp 没有强制下线在线代理的 API**——`DELETE /api/proxies` 的实际语义是
558
+ `ClearOfflineProxies()`(只接受 `?status=offline`,源码与真机均已核实)。
559
+ 要断开某个客户端请停掉它的 frpc。此前版本的 `kick` 基于对该端点的误读,
560
+ 从未真正工作过,已由 `prune` 取代。
556
561
 
557
562
  ---
558
563
 
@@ -732,6 +737,51 @@ $ tail -1 plugin-audit.jsonl
732
737
 
733
738
  ---
734
739
 
740
+ ## Web 管理台
741
+
742
+ 内置的浏览器界面——不是 frp 自带 dashboard 的复刻,而是**含控制面**的管理台:
743
+ 进程启停、配置编辑(预览 diff → 应用 → 失败自动回滚)、日志、7 天流量图。
744
+
745
+ ```bash
746
+ frpsctl web serve # 默认只绑 127.0.0.1:8787
747
+ frpsctl web serve --bind 127.0.0.1:9000 # 换端口
748
+ FRPSCTL_WEB_PASSWORD=my-pw frpsctl web serve # 指定口令(默认自动生成并打印一次)
749
+ ```
750
+
751
+ ```console
752
+ $ frpsctl web serve
753
+ Web 管理台:http://127.0.0.1:8787/
754
+ 登录口令(仅显示这一次):Ih2x...(24 字符)
755
+ Ctrl-C 停止。
756
+ ```
757
+
758
+ 打开浏览器即可——单文件前端(暗色主题),零外部资源(不加载任何 CDN):
759
+
760
+ | 页面 | 内容 |
761
+ |------|------|
762
+ | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图 / 会话内实时流量曲线 / 日志(5 秒自动刷新) |
763
+ | 配置 | 逐字段表单(敏感值不回显,留空表示不改)→ 预览打码 diff → 确认应用(一次事务、一次重启)→ 一键回滚 |
764
+
765
+ **安全设计**(比 frp 自带 dashboard 更严——它正是本项目安全决策的来源):
766
+
767
+ - 默认只绑回环;绑非回环必须显式 `--allow-non-loopback`(建议再套反向代理 + TLS);
768
+ - **不允许空口令**:自动生成(仅打印一次)或显式指定,会话 Cookie 带 `HttpOnly` + `SameSite=Strict`;
769
+ - 一切变更请求要求 `X-CSRF-Token`(登录时下发,仅存浏览器内存);
770
+ - 登录失败按来源限速(防爆破),错误口令与限速的响应完全一致;
771
+ - **配置原文绝不回显**:界面与 API 只返回打码值;要明文用 `config get --reveal`;
772
+ - 响应带 CSP(`default-src 'none'`)与 `no-store`。
773
+
774
+ 用 systemd 托管(生成 0600 口令文件,unit 只引用路径,明文不落 unit):
775
+
776
+ ```bash
777
+ sudo frpsctl web service install # 体检 → 渲染 frpsctl-web@.service → enable
778
+ sudo frpsctl web service status
779
+ sudo frpsctl web service uninstall
780
+ ```
781
+
782
+ > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
783
+ > 挡住)——与插件服务的部署要求一致。
784
+
735
785
  ## 用 systemd 托管
736
786
 
737
787
  ```bash
@@ -850,7 +900,7 @@ frpsctl install --version 0.71.0 && frpsctl restart
850
900
  | 4 | 二进制缺失 / 不可执行 / 版本不受支持 | 未 install,或版本 `< 0.70.0` |
851
901
  | 5 | 实例未运行 | `stop` 时无进程 |
852
902
  | 6 | 实例已在运行 | 重复 `start` |
853
- | 7 | dashboard 不可达 / 未启用 | `kick` 时 `webServer.port = 0`;v2 API 缺失 |
903
+ | 7 | dashboard 不可达 / 未启用 | `clients` / `proxies` / `prune` 时 `webServer.port = 0`;v2 API 缺失 |
854
904
  | 8 | 权限不足 | 需要 root 的操作 |
855
905
  | 9 | 变更已自动回滚 | 配置写入后启动/健康检查失败,已恢复上一版 |
856
906
  | 10 | 启动失败 / 进程停不下来 | 启动即退出(附 frp 原始报错);SIGKILL 后仍存在 |
@@ -883,6 +933,7 @@ esac
883
933
  | `FRPSCTL_ADMIN_PASSWORD` | dashboard 口令(优先于配置文件) |
884
934
  | `FRPSCTL_PLUGIN_POLICY` | 插件策略文件路径 |
885
935
  | `FRPSCTL_MIRROR` | frps 下载镜像(逗号分隔;`install --mirror` 优先于它) |
936
+ | `FRPSCTL_WEB_PASSWORD` | Web 管理台登录口令(`web serve --password` 优先于它) |
886
937
  | `FRPSCTL_TRACEBACK` | 设为 `1` 时打印完整回溯(排查未分类错误用) |
887
938
 
888
939
  全局选项(写在子命令前后都可以):
@@ -1118,8 +1169,8 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1118
1169
 
1119
1170
  ```bash
1120
1171
  uv venv && uv pip install -e ".[dev]"
1121
- .venv/bin/pytest # 全部 357 条(契约层缺二进制时自动 skip)
1122
- .venv/bin/pytest -m "not contract" # 快速回归(332 条)
1172
+ .venv/bin/pytest # 全部 435 条(契约层缺二进制时自动 skip)
1173
+ .venv/bin/pytest -m "not contract" # 快速回归(405 条)
1123
1174
  .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 83%)
1124
1175
  .venv/bin/ruff check src/ tests/ # 静态分析
1125
1176
  ```
@@ -23,9 +23,10 @@
23
23
  - [看日志](#看日志)
24
24
  - [停下来](#停下来)
25
25
  - [体检](#体检)
26
- - [下线代理](#下线代理)
26
+ - [清理离线记录](#清理离线记录)
27
27
  - [多实例](#多实例)
28
28
  - [服务端插件(多用户鉴权 + 端口白名单)](#服务端插件多用户鉴权-端口白名单)
29
+ - [Web 管理台](#web-管理台)
29
30
  - [用 systemd 托管](#用-systemd-托管)
30
31
  - [升级 frps](#升级-frps)
31
32
  - [退出码(脚本化契约)](#退出码脚本化契约)
@@ -155,7 +156,7 @@ $ ./install.sh
155
156
  注册全局命令
156
157
  ✓ 已注册:/home/u/.local/bin/frpsctl
157
158
  自检
158
- ✓ 命令可用:frpsctl 0.2.1
159
+ ✓ 命令可用:frpsctl 0.2.2
159
160
 
160
161
  frpsctl 安装完成
161
162
  ```
@@ -212,7 +213,7 @@ dashboard 端口(0 = 不启用,将失去状态聚合能力) [7500]:
212
213
  ```
213
214
 
214
215
  **把这三项记下来**——`auth.token` 要填到每个 frpc 客户端,dashboard 口令用于
215
- `kick` 等操作。
216
+ `prune` 与 Web 管理台等操作。
216
217
 
217
218
  ```console
218
219
  $ frpsctl verify
@@ -387,7 +388,8 @@ alice.alice-ssh alice tcp 6000 online 0 0 B / 0 B
387
388
  $ frpsctl proxies --type http # 只看某类型
388
389
  ```
389
390
 
390
- `kick <proxy-name>` 下线指定代理(名字就是 `proxies` 列出的 name)。
391
+ `prune` 清理离线代理记录;在线代理无法从服务端强制下线(frp 没有该 API,
392
+ 见"清理离线记录"一节)。
391
393
 
392
394
  ### 改配置
393
395
 
@@ -523,13 +525,16 @@ $ frpsctl doctor
523
525
 
524
526
  **有 ERROR 时退出码为 1**,可直接接进 CI 或监控。
525
527
 
526
- ### 下线代理
528
+ ### 清理离线记录
527
529
 
528
530
  ```bash
529
- frpsctl kick my-ssh # DELETE /api/proxies
531
+ frpsctl prune # 清理 dashboard 统计里的离线代理记录
530
532
  ```
531
533
 
532
- 需要 dashboard 启用(`webServer.port > 0`);未启用时退出码 7。
534
+ ⚠️ **frp 没有强制下线在线代理的 API**——`DELETE /api/proxies` 的实际语义是
535
+ `ClearOfflineProxies()`(只接受 `?status=offline`,源码与真机均已核实)。
536
+ 要断开某个客户端请停掉它的 frpc。此前版本的 `kick` 基于对该端点的误读,
537
+ 从未真正工作过,已由 `prune` 取代。
533
538
 
534
539
  ---
535
540
 
@@ -709,6 +714,51 @@ $ tail -1 plugin-audit.jsonl
709
714
 
710
715
  ---
711
716
 
717
+ ## Web 管理台
718
+
719
+ 内置的浏览器界面——不是 frp 自带 dashboard 的复刻,而是**含控制面**的管理台:
720
+ 进程启停、配置编辑(预览 diff → 应用 → 失败自动回滚)、日志、7 天流量图。
721
+
722
+ ```bash
723
+ frpsctl web serve # 默认只绑 127.0.0.1:8787
724
+ frpsctl web serve --bind 127.0.0.1:9000 # 换端口
725
+ FRPSCTL_WEB_PASSWORD=my-pw frpsctl web serve # 指定口令(默认自动生成并打印一次)
726
+ ```
727
+
728
+ ```console
729
+ $ frpsctl web serve
730
+ Web 管理台:http://127.0.0.1:8787/
731
+ 登录口令(仅显示这一次):Ih2x...(24 字符)
732
+ Ctrl-C 停止。
733
+ ```
734
+
735
+ 打开浏览器即可——单文件前端(暗色主题),零外部资源(不加载任何 CDN):
736
+
737
+ | 页面 | 内容 |
738
+ |------|------|
739
+ | 仪表盘 | 实例状态 / 三层健康 / 客户端与代理列表 / 近 7 天流量柱状图 / 会话内实时流量曲线 / 日志(5 秒自动刷新) |
740
+ | 配置 | 逐字段表单(敏感值不回显,留空表示不改)→ 预览打码 diff → 确认应用(一次事务、一次重启)→ 一键回滚 |
741
+
742
+ **安全设计**(比 frp 自带 dashboard 更严——它正是本项目安全决策的来源):
743
+
744
+ - 默认只绑回环;绑非回环必须显式 `--allow-non-loopback`(建议再套反向代理 + TLS);
745
+ - **不允许空口令**:自动生成(仅打印一次)或显式指定,会话 Cookie 带 `HttpOnly` + `SameSite=Strict`;
746
+ - 一切变更请求要求 `X-CSRF-Token`(登录时下发,仅存浏览器内存);
747
+ - 登录失败按来源限速(防爆破),错误口令与限速的响应完全一致;
748
+ - **配置原文绝不回显**:界面与 API 只返回打码值;要明文用 `config get --reveal`;
749
+ - 响应带 CSP(`default-src 'none'`)与 `no-store`。
750
+
751
+ 用 systemd 托管(生成 0600 口令文件,unit 只引用路径,明文不落 unit):
752
+
753
+ ```bash
754
+ sudo frpsctl web service install # 体检 → 渲染 frpsctl-web@.service → enable
755
+ sudo frpsctl web service status
756
+ sudo frpsctl web service uninstall
757
+ ```
758
+
759
+ > `web service install` 同样需要 `frpsctl` 位于系统路径(不能被 `ProtectHome`
760
+ > 挡住)——与插件服务的部署要求一致。
761
+
712
762
  ## 用 systemd 托管
713
763
 
714
764
  ```bash
@@ -827,7 +877,7 @@ frpsctl install --version 0.71.0 && frpsctl restart
827
877
  | 4 | 二进制缺失 / 不可执行 / 版本不受支持 | 未 install,或版本 `< 0.70.0` |
828
878
  | 5 | 实例未运行 | `stop` 时无进程 |
829
879
  | 6 | 实例已在运行 | 重复 `start` |
830
- | 7 | dashboard 不可达 / 未启用 | `kick` 时 `webServer.port = 0`;v2 API 缺失 |
880
+ | 7 | dashboard 不可达 / 未启用 | `clients` / `proxies` / `prune` 时 `webServer.port = 0`;v2 API 缺失 |
831
881
  | 8 | 权限不足 | 需要 root 的操作 |
832
882
  | 9 | 变更已自动回滚 | 配置写入后启动/健康检查失败,已恢复上一版 |
833
883
  | 10 | 启动失败 / 进程停不下来 | 启动即退出(附 frp 原始报错);SIGKILL 后仍存在 |
@@ -860,6 +910,7 @@ esac
860
910
  | `FRPSCTL_ADMIN_PASSWORD` | dashboard 口令(优先于配置文件) |
861
911
  | `FRPSCTL_PLUGIN_POLICY` | 插件策略文件路径 |
862
912
  | `FRPSCTL_MIRROR` | frps 下载镜像(逗号分隔;`install --mirror` 优先于它) |
913
+ | `FRPSCTL_WEB_PASSWORD` | Web 管理台登录口令(`web serve --password` 优先于它) |
863
914
  | `FRPSCTL_TRACEBACK` | 设为 `1` 时打印完整回溯(排查未分类错误用) |
864
915
 
865
916
  全局选项(写在子命令前后都可以):
@@ -1095,8 +1146,8 @@ FRPSCTL_TRACEBACK=1 frpsctl status
1095
1146
 
1096
1147
  ```bash
1097
1148
  uv venv && uv pip install -e ".[dev]"
1098
- .venv/bin/pytest # 全部 357 条(契约层缺二进制时自动 skip)
1099
- .venv/bin/pytest -m "not contract" # 快速回归(332 条)
1149
+ .venv/bin/pytest # 全部 435 条(契约层缺二进制时自动 skip)
1150
+ .venv/bin/pytest -m "not contract" # 快速回归(405 条)
1100
1151
  .venv/bin/pytest --cov=frpsctl # 覆盖率(CI 门禁 80%,当前 83%)
1101
1152
  .venv/bin/ruff check src/ tests/ # 静态分析
1102
1153
  ```
@@ -36,7 +36,8 @@
36
36
  15. [风险登记表](#15-风险登记表)
37
37
  16. [设计评审变更记录](#16-设计评审变更记录问题-15-闭环)
38
38
  17. [实现验证记录](#17-实现验证记录m0m5-已落地)
39
- - [17.6 第二轮全量 review](#176-第二轮全量-reviewm0m5--文档定稿--真-frpc-契约复核后)
39
+ - [17.6 第二轮全量 review](#176-第二轮全量-reviewm0m5--文档定稿--真-frpc-契约复核后)
40
+ 18. [Web 管理台](#18-web-管理台)
40
41
  - [附录 A:frps 配置键速查表](#附录-afrps-配置键速查表)
41
42
  - [附录 B:事实复核清单](#附录-b事实复核清单)
42
43
  - [附录 B-2:§3.6 版本矩阵的复核命令](#附录-b-236-版本矩阵的复核命令)
@@ -138,7 +139,7 @@ dashboard 仅在 `webServer.port > 0` 时启动;`port = 0` 表示**完全不
138
139
  | `GET /api/v2/proxies`、`/api/v2/proxies/{name}`、`/api/v2/proxies/{name}/traffic` | Basic | ✔ 代理列表 / 详情 / 流量序列(带分页) |
139
140
  | `GET /api/v2/users` | Basic | ✔ 按用户聚合 |
140
141
  | `POST /api/v2/system/prune` | Basic | ✔ 供未来 `stats prune` 使用 |
141
- | `DELETE /api/proxies` | Basic | ✔ `kick` 用(v1 路径,v2 无对应写接口) |
142
+ | `DELETE /api/proxies` | Basic | ✔ `prune` 用——**实际语义是"清理离线代理记录"**(`?status=offline`,源码 `controller.go` 的 `ClearOfflineProxies()`;真机实测无参数返回 400)。frp **没有**强制下线在线代理的 API;旧 `kick` 命令基于误读,从未工作(§18.6) |
142
143
  | `GET /api/proxy/{type}` | Basic | ✔ 逐类型代理列表(`{"proxies":[...]}`,用于 §13.1 C1 的交叉验证) |
143
144
  | `GET /api/serverinfo`、`GET /api/clients` | Basic | ✘ **v1 遗留**,不实现(ADR-3) |
144
145
  | `GET /metrics` | Basic | ✘ 仅当 `enablePrometheus = true`,超出 v1 范围 |
@@ -527,7 +528,7 @@ class Instance:
527
528
  | `frpsctl log [-f] [-n 100]` | 看日志 | tail `log.to`;缺失时回退到 startup 日志 |
528
529
  | `frpsctl service install\|uninstall\|status` | systemd 集成 | 渲染 unit + `daemon-reload` + `enable`(需 root,明确提示) |
529
530
  | `frpsctl doctor [--json]` | 体检 | §8.7 检查项,按 severity 输出 |
530
- | `frpsctl kick <proxy-name>` | 下线指定代理 | `DELETE /api/proxies` |
531
+ | `frpsctl prune` | 清理离线代理记录 | `DELETE /api/proxies?status=offline`;frp 不支持强制下线在线代理 |
531
532
 
532
533
  ### 7.3 退出码
533
534
 
@@ -2412,6 +2413,150 @@ diff。发现 **3 个真实缺陷**——全部集中在"**展示层与业务层
2412
2413
 
2413
2414
  ---
2414
2415
 
2416
+ ## 18. Web 管理台
2417
+
2418
+ ### 18.1 目标:控制面,而不只是仪表盘
2419
+
2420
+ frp 自带 dashboard 只有**数据面**(状态、客户端、代理、流量)且界面陈旧;
2421
+ 本工具的内置管理台补齐**控制面**——进程启停、配置编辑(预览 → 事务 → 自动
2422
+ 回滚)、日志——数据面则复用 v2 Admin API(§3.2)。
2423
+
2424
+ 架构定位:`web/` 是与 `cli/` **平级的又一个前端**,只调 `core/`,不复制任何
2425
+ 业务逻辑;`core/` 不感知 HTTP。技术栈与插件服务同源:标准库
2426
+ `ThreadingHTTPServer`,零新增依赖。
2427
+
2428
+ ```
2429
+ 浏览器(单文件前端,内嵌 CSS/JS,零外部资源)
2430
+ │ JSON API + 会话 Cookie + CSRF 头
2431
+ ▼
2432
+ frpsctl web serve(独立进程,默认只绑 127.0.0.1)
2433
+ ├── core.Lifecycle → 状态 / start / stop / restart(含健康门控)
2434
+ ├── core.AdminClient → clients / proxies / traffic(v2,自动翻页)
2435
+ ├── core.transaction → 配置预览(plan_set_many + CAS)与应用(apply_sets)
2436
+ └── core.logs → 日志 tail
2437
+ ```
2438
+
2439
+ ### 18.2 API 契约
2440
+
2441
+ | 方法 | 路径 | 说明 |
2442
+ |------|------|------|
2443
+ | POST | `/api/login` `/api/logout` | 口令登录(下发会话 Cookie + CSRF)/ 登出;**login 是唯一免认证入口** |
2444
+ | GET | `/api/status` | 进程状态 + 三层健康 + dashboard 统计(统计不可得为 null) |
2445
+ | GET | `/api/clients` `/api/proxies` `/api/traffic` | v2 数据(自动翻页;traffic 为 7 天日粒度) |
2446
+ | GET | `/api/config` | 配置树(**打码值 + masked 标记**,原文永不下发) |
2447
+ | GET | `/api/logs?lines=` | 日志尾部(≤2000 行,路径解析复用 `core/logs`) |
2448
+ | POST | `/api/config/preview` | 多键变更 → 锁内取快照 + 打码 diff,登记 `preview_id`(TTL 10 分钟) |
2449
+ | POST | `/api/config/apply` | 按 `preview_id` 应用;**CAS**:预览后文件被改 → 400 拒绝而不是覆盖 |
2450
+ | POST | `/api/actions/{start,stop,restart,rollback,prune}` | 与 CLI 同一套 core 入口 |
2451
+
2452
+ 错误映射:`FrpsctlError.exit_code` → HTTP(用法/配置 400、未运行/冲突 409、
2453
+ 权限 403、dashboard 不可达/健康未过 502)——响应只含 `message` 与 `hint`,
2454
+ 机密不进错误体(core 已保证)。
2455
+
2456
+ ### 18.3 安全基线(比 frp 自带 dashboard 更严)
2457
+
2458
+ frp 的教训(user/password 双空 = 完全不鉴权,§3.3)是本项目全部安全决策的
2459
+ 来源;管理台能改配置、停服务,是比 dashboard 高得多的价值目标:
2460
+
2461
+ | 风险 | 对策 |
2462
+ |------|------|
2463
+ | 暴露到非回环 | 默认只绑 `127.0.0.1`;`--bind` 非回环必须显式 `--allow-non-loopback` |
2464
+ | 无口令 | **不允许空口令**:自动生成(仅打印一次)或显式提供 |
2465
+ | 会话劫持 | 256 位随机 token;Cookie `HttpOnly` + `SameSite=Strict`;TTL 8h(内存态) |
2466
+ | CSRF | 一切变更请求要求 `X-CSRF-Token`(登录下发,仅存浏览器内存) |
2467
+ | 口令爆破 | 来源级失败限速(60s/5 次);冷却与错口令**响应完全一致** |
2468
+ | 时序侧信道 | `hmac.compare_digest` |
2469
+ | 配置泄露 | 界面/API 只出打码值;欲看明文用 CLI `--reveal` |
2470
+ | 前端供应链 | 单文件、零外部资源;CSP `default-src 'none'` + `connect-src 'self'` |
2471
+
2472
+ ### 18.4 systemd 托管
2473
+
2474
+ `frpsctl web service install` 渲染 `frpsctl-web@.service`:`Restart=on-failure`
2475
+ (交互工具,正常停止不自启)、四项体检与插件一致、安装时生成 0600 口令文件
2476
+ 并移交服务用户——**口令明文绝不进 unit**(unit 是 0644,只引用文件路径)。
2477
+
2478
+ ### 18.5 实现记录:下沉与复用
2479
+
2480
+ 为 web 与 CLI 共用,三处逻辑下沉到 core(消除重复实现):`parse_bind`
2481
+ (bind 解析,插件服务同时受益)、`mask_value`(打码单点,`cli.ui.mask_secret`
2482
+ 委托)、`core/logs.py`(日志路径解析与 tail,`frpsctl log` 改用)。
2483
+
2484
+ ### 18.6 实现期发现:`kick` 是一个从未工作过的功能
2485
+
2486
+ Web 端到端测试(真 frps + frpc)第一次调用"下线代理"就暴露:frp 的
2487
+ `DELETE /api/proxies` 实现是 `ClearOfflineProxies()`,**只接受
2488
+ `?status=offline`**(源码 `server/http/controller.go`),路由表里也没有任何
2489
+ 强制下线在线代理的 API。旧 `kick` 按"按 name 下线"实现该端点——真机**永远**
2490
+ 返回 400,而它从未被真机验证过(契约层是盲区)。
2491
+
2492
+ 修正:`kick` 移除,`prune` 取代(正确语义:清理离线记录);契约层新增 **C9**
2493
+ 锁定该事实(无参数 400 / `?status=offline` 200 / 客户端源码反向断言)。
2494
+
2495
+ ### 18.7 未做与边界
2496
+
2497
+ | 项 | 结论 |
2498
+ |----|------|
2499
+ | WebSocket/SSE 实时推送 | 不做:5 秒轮询足够,且省掉长连接的生命周期管理 |
2500
+ | 多实例切换 | 不做:一个 web 进程服务一个实例(`--instance`),多实例起多个进程 |
2501
+ | HTTPS | 不做:默认回环明文即可;远程访问建议反向代理终结 TLS(文档已说明) |
2502
+ | 配置的"全文编辑器" | 不做:表单式逐键编辑 + 预览 diff 更安全(原文不回传浏览器) |
2503
+ | 强制下线在线代理 | 做不到:frp 没有该 API(§18.6);停掉对端 frpc 是唯一途径 |
2504
+
2505
+ ### 18.8 发布前回归 review(第八轮)
2506
+
2507
+ 对抗性复现(浏览器刷新场景、TOML datetime、全路由认证扫描、单代理故障注入)
2508
+ 发现 **4 个真实缺陷**,全部修复并补 **21 条测试**(410 → 431):
2509
+
2510
+ | # | 缺陷 | 复现方式 | 修复 |
2511
+ |---|------|---------|------|
2512
+ | 1 | 配置含 TOML datetime 时 `json.dumps` 抛 `TypeError`——traceback 后连接被断开,整个配置页不可用 | 配置写 `expires_at = 2026-12-31T23:59:59` 后 `GET /api/config` | `_send_json` 统一 `default=str` 兜底(与 `cli.ui.emit_json` 一致) |
2513
+ | 2 | 浏览器**刷新页面**后 Cookie 还在但前端内存的 CSRF 丢失——所有变更操作 403 且无恢复路径 | 有 Cookie 无 CSRF 的 POST | 新增 `GET /api/session`(有效会话归还 CSRF);前端 boot 时恢复 |
2514
+ | 3 | 趋势接口中单个代理查询失败会拖垮整张图(dashboard 半死不活时全 502) | mock 单代理抛 `AdminUnreachable` | 逐代理容错(记空 history),其余照常返回 |
2515
+ | 4 | 预览条目无上限(已登录用户可持续 preview 堆内存);前端 `localStorage` 在隐私模式下抛异常会中断流量采样 | 代码审查 + 存储注入 | 预览上限 32 条(丢最旧);采样读/写 try 包裹 |
2516
+
2517
+ 另新增**全路由认证扫描**(16 条参数化用例):除 `POST /api/login` 外的每条 API
2518
+ 在未登录时必须 401——**未知路由同样返回 401**(不泄露路由存在性)。前端 425 行
2519
+ JS 经 `node --check` 语法验证通过。
2520
+
2521
+ ### 18.9 发布前最终验收(v0.2.2)
2522
+
2523
+ 方法同 §17.12:**验证发布产物本身 + 复刻 CI 全套**。本轮补齐了 CI 对 Web 的
2524
+ 覆盖缺口(此前 CI 从不触碰 web),结论:**可发布**。
2525
+
2526
+ #### 发布产物验证
2527
+
2528
+ | 项 | 结果 |
2529
+ |----|------|
2530
+ | wheel / sdist | `frpsctl-0.2.2`(41 条目,含 web 模块与 26KB 单文件前端) |
2531
+ | 干净安装(独立 venv) | `frpsctl 0.2.2`;**11/11 命令 help 全部可用**(含 `web`/`prune`) |
2532
+ | 前端资源 | 安装后 `STATIC_INDEX` 可读(26389 字符) |
2533
+ | tag 一致性(模拟 release workflow) | `v0.2.2` == `__version__` |
2534
+ | wheel 自检 | 含 `py.typed` / `web/static/index.html` / 全部模块 |
2535
+
2536
+ #### CI 全套本地复刻
2537
+
2538
+ | 步骤 | 结果 |
2539
+ |------|------|
2540
+ | ruff / 覆盖率门禁(80%) | 全绿 / 82.44% |
2541
+ | 非契约 | 405 passed |
2542
+ | 契约层(0.71.0,含新增 C9/C10) | 26 passed |
2543
+ | 故障注入层 | 23 passed |
2544
+ | 插件契约层(真 frpc) | 4 passed |
2545
+ | 端到端冒烟(CLI 12 步) | 通过 |
2546
+ | **Web 冒烟(本轮新增的 CI 步骤)** | 通过(login / session / status / config / 静态页) |
2547
+ | 无二进制降级路径 | 3 passed / 23 skipped |
2548
+ | 0.70.0 下界契约矩阵 | 26 passed(C9/C10 在 0.70 上同样成立) |
2549
+
2550
+ #### 本轮 review 的修补
2551
+
2552
+ - **CI 覆盖缺口**:此前 CI 从不触碰 Web——新增 web 冒烟步骤(已在本地复刻验证
2553
+ 可上 CI);
2554
+ - **契约补充**:C10 锁定 v2 traffic 端点的"无数据 = 404"语义(Web 前端容错的
2555
+ 前提),0.70.0 上同样成立;`AdminClient.proxy_traffic` 的解析与名称转义补单测;
2556
+ - 交叉核对:命令(39 条路径)/ 环境变量 / 退出码三向一致,无幽灵命令。
2557
+
2558
+ ---
2559
+
2415
2560
  ## 附录 A:frps 配置键速查表
2416
2561
 
2417
2562
  > 全部取自 `v0.71.0` 源码;「默认值」栏是 `Complete()` 之后的**生效值**。合法取值来自校验器。
@@ -11,4 +11,4 @@ __all__ = ["__version__"]
11
11
 
12
12
  #: 版本号唯一来源:pyproject.toml 通过 hatchling 从这里读取(tool.hatch.version)。
13
13
  #: 发布新版本时只改这里,然后打 `v<version>` tag——release workflow 会校验两者一致。
14
- __version__ = "0.2.1"
14
+ __version__ = "0.2.2"