wsctl 0.1.0__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 (75) hide show
  1. wsctl-0.1.0/.gitignore +35 -0
  2. wsctl-0.1.0/CHANGELOG.md +154 -0
  3. wsctl-0.1.0/DESIGN.md +254 -0
  4. wsctl-0.1.0/LICENSE +21 -0
  5. wsctl-0.1.0/PKG-INFO +565 -0
  6. wsctl-0.1.0/README.md +491 -0
  7. wsctl-0.1.0/SECURITY.md +39 -0
  8. wsctl-0.1.0/THIRD_PARTY_NOTICES.md +19 -0
  9. wsctl-0.1.0/pyproject.toml +109 -0
  10. wsctl-0.1.0/scripts/e2e/run_all.py +341 -0
  11. wsctl-0.1.0/src/wsctl/__init__.py +7 -0
  12. wsctl-0.1.0/src/wsctl/__main__.py +6 -0
  13. wsctl-0.1.0/src/wsctl/cli/__init__.py +3 -0
  14. wsctl-0.1.0/src/wsctl/cli/client.py +135 -0
  15. wsctl-0.1.0/src/wsctl/cli/connect.py +158 -0
  16. wsctl-0.1.0/src/wsctl/cli/main.py +655 -0
  17. wsctl-0.1.0/src/wsctl/core/__init__.py +3 -0
  18. wsctl-0.1.0/src/wsctl/core/_winpty.py +79 -0
  19. wsctl-0.1.0/src/wsctl/core/config.py +221 -0
  20. wsctl-0.1.0/src/wsctl/core/fs.py +58 -0
  21. wsctl-0.1.0/src/wsctl/core/logging.py +36 -0
  22. wsctl-0.1.0/src/wsctl/core/metrics.py +67 -0
  23. wsctl-0.1.0/src/wsctl/core/net.py +30 -0
  24. wsctl-0.1.0/src/wsctl/core/passwords.py +26 -0
  25. wsctl-0.1.0/src/wsctl/core/pty.py +249 -0
  26. wsctl-0.1.0/src/wsctl/core/ratelimit.py +69 -0
  27. wsctl-0.1.0/src/wsctl/core/recording.py +73 -0
  28. wsctl-0.1.0/src/wsctl/core/scrollback.py +60 -0
  29. wsctl-0.1.0/src/wsctl/core/session.py +467 -0
  30. wsctl-0.1.0/src/wsctl/core/ssh.py +53 -0
  31. wsctl-0.1.0/src/wsctl/core/store.py +462 -0
  32. wsctl-0.1.0/src/wsctl/core/tmux.py +82 -0
  33. wsctl-0.1.0/src/wsctl/core/totp.py +21 -0
  34. wsctl-0.1.0/src/wsctl/core/webhook.py +62 -0
  35. wsctl-0.1.0/src/wsctl/py.typed +0 -0
  36. wsctl-0.1.0/src/wsctl/server/__init__.py +3 -0
  37. wsctl-0.1.0/src/wsctl/server/app.py +773 -0
  38. wsctl-0.1.0/src/wsctl/server/client.py +82 -0
  39. wsctl-0.1.0/src/wsctl/server/security.py +75 -0
  40. wsctl-0.1.0/src/wsctl/server/ws.py +376 -0
  41. wsctl-0.1.0/src/wsctl/static/app.css +309 -0
  42. wsctl-0.1.0/src/wsctl/static/app.js +960 -0
  43. wsctl-0.1.0/src/wsctl/static/index.html +125 -0
  44. wsctl-0.1.0/src/wsctl/static/vendor/THIRD_PARTY_NOTICES.txt +250 -0
  45. wsctl-0.1.0/src/wsctl/static/vendor/addon-fit.js +2 -0
  46. wsctl-0.1.0/src/wsctl/static/vendor/addon-image.js +3 -0
  47. wsctl-0.1.0/src/wsctl/static/vendor/addon-image.js.LICENSE.txt +21 -0
  48. wsctl-0.1.0/src/wsctl/static/vendor/addon-web-links.js +2 -0
  49. wsctl-0.1.0/src/wsctl/static/vendor/asciinema-player.css +762 -0
  50. wsctl-0.1.0/src/wsctl/static/vendor/asciinema-player.min.js +3 -0
  51. wsctl-0.1.0/src/wsctl/static/vendor/xterm.css +218 -0
  52. wsctl-0.1.0/src/wsctl/static/vendor/xterm.js +2 -0
  53. wsctl-0.1.0/src/wsctl/static/vendor/zmodem.js +1 -0
  54. wsctl-0.1.0/tests/conftest.py +36 -0
  55. wsctl-0.1.0/tests/test_browser.py +235 -0
  56. wsctl-0.1.0/tests/test_cli.py +48 -0
  57. wsctl-0.1.0/tests/test_client.py +44 -0
  58. wsctl-0.1.0/tests/test_config.py +96 -0
  59. wsctl-0.1.0/tests/test_connect.py +22 -0
  60. wsctl-0.1.0/tests/test_fs.py +57 -0
  61. wsctl-0.1.0/tests/test_load.py +89 -0
  62. wsctl-0.1.0/tests/test_metrics.py +35 -0
  63. wsctl-0.1.0/tests/test_net.py +29 -0
  64. wsctl-0.1.0/tests/test_pty.py +38 -0
  65. wsctl-0.1.0/tests/test_ratelimit.py +54 -0
  66. wsctl-0.1.0/tests/test_recording.py +41 -0
  67. wsctl-0.1.0/tests/test_scrollback.py +33 -0
  68. wsctl-0.1.0/tests/test_security.py +46 -0
  69. wsctl-0.1.0/tests/test_server.py +649 -0
  70. wsctl-0.1.0/tests/test_session.py +260 -0
  71. wsctl-0.1.0/tests/test_ssh.py +48 -0
  72. wsctl-0.1.0/tests/test_store.py +152 -0
  73. wsctl-0.1.0/tests/test_tmux.py +86 -0
  74. wsctl-0.1.0/tests/test_totp.py +25 -0
  75. wsctl-0.1.0/tests/test_webhook.py +52 -0
wsctl-0.1.0/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Test / coverage
18
+ .pytest_cache/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ .mypy_cache/
23
+ .ruff_cache/
24
+
25
+ # Runtime data
26
+ *.db
27
+ *.sqlite
28
+ *.sqlite3
29
+ wsctl-data/
30
+
31
+ # Editors / OS
32
+ .idea/
33
+ .vscode/
34
+ .DS_Store
35
+ *.swp
@@ -0,0 +1,154 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-09-21
9
+
10
+ First release. Single-server web terminal with persistent sessions, multi-user
11
+ RBAC, auditing, sharing, a file panel and observability.
12
+
13
+ ### Added
14
+
15
+ **Sessions & server**
16
+ - `SessionManager` with connection-independent PTY sessions, attach/detach,
17
+ scrollback replay on reconnect and bounded output backpressure.
18
+ - POSIX PTY backend; optional Windows support via `pywinpty` (`wsctl[win]`).
19
+ - Binary WebSocket protocol (raw terminal bytes) with a JSON control channel.
20
+ - Multi-tab web UI (vendored xterm.js, zero build step) with auto-reconnect.
21
+ - Session renaming, idle/max-lifetime enforcement and metadata reconciliation.
22
+ - Per-session client limit (`session_max_clients`), per-session memory hard limit
23
+ (`session_memory_limit`) and per-client byte cap (`client_max_bytes`).
24
+ - Per-connection input rate limiting via a token bucket
25
+ (`input_rate_limit` / `input_rate_burst`).
26
+
27
+ **Backends**
28
+ - Optional `tmux` backend (`default_backend`, `--backend tmux`): the shell runs
29
+ inside a tmux session and survives a full wsctl restart, then is reattached
30
+ automatically with the same id and a redrawn screen; graceful shutdown
31
+ preserves tmux sessions (`tmux_preserve_on_shutdown`).
32
+ - SSH backend: `backend="ssh"` builds a safe `ssh` argv from a structured target
33
+ (host/user/port/identity/options/remote command) with no shell interpolation.
34
+
35
+ **Users & security**
36
+ - Multi-user accounts with Argon2 hashing and server-side session tokens.
37
+ - Role-based access control (admin / user) on the REST API and WebSocket attach.
38
+ - TOTP two-factor authentication (`wsctl user totp`).
39
+ - Login rate limiting (per IP + username) and a CIDR IP allowlist.
40
+ - Origin allowlist on WebSocket handshakes (anti-CSWSH).
41
+ - Security response headers (nosniff, frame DENY, referrer policy, CSP).
42
+ - Optional submitted-input auditing (`audit_input`).
43
+
44
+ **Auditing & events**
45
+ - Full audit trail with an admin `GET /api/audit` endpoint and `wsctl audit`.
46
+ - Optional webhooks (`webhook_url`): every audit event is POSTed as JSON by a
47
+ background dispatcher.
48
+
49
+ **Sharing**
50
+ - Read-only and read-write share tokens (`POST/DELETE /api/sessions/{id}/share`),
51
+ unguessable, optionally time-limited and revocable.
52
+ - Anonymous viewers attach with a share link (`?session=&share=`) without an
53
+ account; input is refused for read-only links.
54
+ - QR code for a share link (`GET /api/sessions/{id}/qr.svg`) and a share dialog
55
+ in the web UI.
56
+
57
+ **Recording**
58
+ - asciinema cast v2 recording (`core/recording.py`), optionally including input,
59
+ with `auto_record` / `record_input` settings and per-session start/stop.
60
+ - In-browser replay via a vendored asciinema player, plus `wsctl session
61
+ record|record-stop|recording`.
62
+
63
+ **Files & terminal features**
64
+ - Web file panel API (list / download / upload) rooted at a configurable
65
+ `file_root`, with traversal-proof resolution and upload size limits.
66
+ - Sixel image rendering via `@xterm/addon-image`.
67
+ - Opt-in ZMODEM (`sz`/`rz`) file transfer in the browser via `zmodem.js`.
68
+
69
+ **Observability & operations**
70
+ - `/healthz` and Prometheus metrics at `/metrics` (incl. `wsctl_session_bytes`).
71
+ - Structured JSON logging (`--log-json`).
72
+ - Zero-downtime restarts: `reuse_port` / `--reuse-port` binds with `SO_REUSEPORT`
73
+ so a new instance takes over the port before the old one exits.
74
+ - Runtime config hot-reload: file-mtime watcher, `POST /api/config/reload` and
75
+ `wsctl config reload` (restart-only fields are ignored).
76
+
77
+ **Web UI**
78
+ - Dark/light page theme, font size, a terminal theme gallery (10 built-in themes
79
+ plus custom JSON themes) and configurable keyboard shortcuts, persisted locally.
80
+ - Responsive/mobile layout adjustments.
81
+
82
+ **Persistence**
83
+ - SQLite storage for users, auth sessions, terminal sessions, audit logs and
84
+ settings; full `term_sessions` columns (`argv`, `env`, `idle_timeout`,
85
+ `max_life`) with an idempotent schema migration.
86
+
87
+ **CLI**
88
+ - `serve` (`--new`, `--backend`, `--reuse-port`), `connect`, `login`, `logout`,
89
+ `session list|new|kill|attach|record|record-stop|recording`,
90
+ `user add|list|del|passwd|role|totp`, `audit`, `config show|path|edit|set|reload`,
91
+ `version`.
92
+
93
+ **Packaging & CI**
94
+ - Hatchling packaging (PyPI: `wsctl`), MIT license, `py.typed`.
95
+ - Third-party front-end assets ship with their license texts
96
+ (`src/wsctl/static/vendor/THIRD_PARTY_NOTICES.txt`, `THIRD_PARTY_NOTICES.md`).
97
+ - GitHub Actions CI (ruff, mypy, pytest on Python 3.11–3.13), a dedicated
98
+ browser-test job and a backend end-to-end job, plus a release workflow using
99
+ PyPI Trusted Publishing that also creates a GitHub Release.
100
+
101
+ ### Fixed
102
+
103
+ - tmux-backed sessions now default `TERM` (xterm-256color), so they work in
104
+ headless environments where `TERM` is unset (previously the tmux client
105
+ exited immediately and restart recovery failed).
106
+ - A tmux client is never signalled as a process group on shutdown, so the
107
+ freshly forked tmux server (and the preserved session) cannot be killed.
108
+ - Revoked/expired share tokens are now enforced promptly: input is rejected
109
+ immediately and the connection is closed, and a periodic re-check detaches
110
+ viewers even on sessions that produce no output.
111
+ - Read-only clients can no longer resize the shared terminal at attach time.
112
+ - Authorization failures during the WebSocket handshake are delivered as close
113
+ codes (4401), so clients stop reconnecting instead of looping forever.
114
+ - File upload opens the destination with `O_NOFOLLOW` (race-free symlink guard).
115
+ - Config hot-reload treats an explicitly-empty environment variable as set.
116
+ - Web UI: `localStorage.setItem` is guarded, and the record button reflects the
117
+ actual recording state on load and when switching tabs.
118
+ - PTY no longer leaks the master fd or the child process when a spawn fails,
119
+ and out-of-range terminal dimensions are clamped instead of raising.
120
+ - The PTY write buffer is bounded, so a child that stops reading stdin cannot
121
+ grow server memory without limit.
122
+ - Sessions are stopped if post-create setup fails (REST and WebSocket paths),
123
+ instead of leaking a live process with no metadata.
124
+ - Disabled users' existing sessions are invalidated.
125
+ - Empty commands (400), out-of-range dimensions (422) and duplicate users (409)
126
+ are rejected cleanly instead of erroring out.
127
+ - CSP allows `blob:` so the recording player actually loads its cast, and
128
+ `connect-src` was tightened to `'self'`.
129
+ - Web UI: guarded `localStorage` parsing, reset the screen on reconnect (no
130
+ duplicated scrollback), share "revoke" targets the session the dialog was
131
+ opened for, read-only viewers can no longer trigger the login modal, and the
132
+ replay player/blob URL is disposed.
133
+ - WebSocket teardown now drains queued control messages before closing.
134
+ - WebSocket auto-created sessions record `owner_id` and persist metadata.
135
+ - `safe_resolve` rejects absolute paths instead of silently re-rooting them.
136
+ - The share QR endpoint emits a standalone SVG (with `xmlns`) so it renders
137
+ inside an `<img>`; the previous inline SVG was blank in browsers.
138
+ - CSP allows `'wasm-unsafe-eval'` and `worker-src blob:` so the recording player
139
+ can run.
140
+
141
+ ### Testing
142
+
143
+ - 149 unit/integration tests; concurrency and large-output load tests are opt-in
144
+ (`pytest -m slow`), browser tests are opt-in (`pytest -m browser`), and a
145
+ 120-second per-test timeout guards against hangs.
146
+ - Browser-level end-to-end tests with Playwright (`wsctl[e2e]`) covering login,
147
+ terminal I/O, multi-tab, hotkeys, file panel, sharing with QR, recording
148
+ replay, theme switching, read-only input blocking and anonymous share links;
149
+ run in CI in a dedicated job.
150
+ - `scripts/e2e/run_all.py` runs five backend end-to-end scenarios (server, CLI,
151
+ `connect`, tmux restart recovery, SO_REUSEPORT graceful restart); it runs in
152
+ CI in a dedicated job.
153
+
154
+ [0.1.0]: https://github.com/ThzxxArt/wsctl/releases/tag/v0.1.0
wsctl-0.1.0/DESIGN.md ADDED
@@ -0,0 +1,254 @@
1
+ # wsctl 设计文档
2
+
3
+ > 版本:0.1.0 · 状态:已发布(0.1.0)
4
+ > 作者:ThzxxArt · 许可:MIT
5
+
6
+ ## 1. 定位
7
+
8
+ **wsctl 是 ttyd 的现代 Python 超集。**
9
+
10
+ 单机部署的 Web 在线终端。一条命令起服务,浏览器或 CLI 得到完整 shell;终端
11
+ **会话独立于连接**,断线重连即恢复。
12
+
13
+ - 目标场景:个人 / 小团队的远程运维、跳板、演示、受限环境终端访问
14
+ - 部署形态:单进程单机,`pip install wsctl` 零编译即用
15
+ - 对照参照:`ttyd`(C + libuv/libwebsockets)
16
+
17
+ ### 1.1 明确边界(诚实声明)
18
+
19
+ wsctl **不在原始吞吐上对标 ttyd**。纯 Python 无法在 `cat` 大文件、`yes` 刷屏
20
+ 这类高吞吐场景打赢 C+libuv。wsctl 的「稳定」来自**架构**——会话与连接解耦、
21
+ 输出背压、进程隔离——而非单连接速度。请勿用不公平 benchmark 比较两者。
22
+
23
+ ## 2. 相对 ttyd 的差异化
24
+
25
+ | 维度 | ttyd | wsctl |
26
+ |---|---|---|
27
+ | 会话模型 | 一进程 = 一会话 = 一端口 | 单服务托管 N 个会话 |
28
+ | 多终端 | 起多进程 / 依赖 tmux | 原生多会话 + 多标签 UI |
29
+ | 用户体系 | 单一 basic-auth 凭据 | 多用户 + RBAC(admin/user) |
30
+ | 审计 | 无 | 结构化审计日志(可扩展录制) |
31
+ | 连接语义 | 连接与会话耦合 | 解耦,断线重连回放屏幕 |
32
+ | 配置 | 命令行参数 | 配置文件 + CLI + 热更新 |
33
+ | 可观测 | 基础日志 | `/healthz` · `/metrics` · JSON 日志 |
34
+ | 管理面 | 无 | REST API(可扩展 Webhook) |
35
+ | 扩展 | C | Python |
36
+ | 安装 | 各平台二进制 | `pip install` 零编译 |
37
+
38
+ **主动选择放弃的项**:不兼容 ttyd 的命令行参数,另起炉灶使用子命令式 CLI。
39
+
40
+ ## 3. 核心架构:会话与连接解耦
41
+
42
+ ```
43
+ ┌────────────── SessionManager (asyncio) ──────────────┐
44
+ Browser WS ─┐ │ TermSession#a1 │
45
+ Browser WS ─┼─►│ ├ attached[] ──► broadcast(out) │
46
+ CLI WS ─┘ │ ├ PtyMaster(fd) ──┬──┐ │
47
+ │ └ meta │ │ │
48
+ │ ┌────▼──┴───────┐ │
49
+ │ │ Scrollback │ 环形缓冲(原始ANSI流) │
50
+ │ └───────────────┘ │
51
+ │ fork/exec → [bash | ssh | 命令] ⟂连接独立 │
52
+ │ TermSession#b2 ... ⟂生命周期独立 │
53
+ └───────────────────────────────────────────────────────┘
54
+ ```
55
+
56
+ - **attach / detach**:WebSocket 连接进入 / 离开 `attached[]` 集合,PTY 子进程
57
+ 生命周期不受影响。
58
+ - **输出路径**:PTY master → 写入 scrollback → 广播给所有 attached 客户端。
59
+ - **重连回放**:attach 时先发送 `resize`,再 replay scrollback 原始字节流;
60
+ xterm.js 会依据 ANSI 转义序列自动重建屏幕状态。
61
+ - **背压**:每个客户端维护有界发送队列;慢消费者达到上限时丢弃旧帧或断开,
62
+ 防止大输出导致服务端内存暴涨。
63
+ - **回收策略**:空闲超时 / 最大生命周期 / 管理员显式 kill,三者之一触发回收。
64
+ - **进程治理**:`start_new_session` + `killpg`,并在会话结束时 `wait()` 回收;未安装
65
+ 全局 SIGCHLD handler(避免与 `subprocess` 争抢 PID)。已跟踪会话不残留僵尸进程。
66
+
67
+ ### 3.1 可选 tmux 后端(跨重启恢复)
68
+
69
+ `backend="local"`(默认)时 shell 是服务进程的直接子进程,服务退出即随之结束。
70
+ `backend="tmux"` 时,PTY 里跑的是 `tmux new-session -A -s wsctl-<sid> <cmd>`,即一个
71
+ tmux 客户端;真正的 shell 活在 tmux server 内。于是:
72
+
73
+ - 服务关闭时以 `preserve=True` 停止会话——只断开 tmux 客户端,shell 继续运行;
74
+ - 下次启动读取 `term_sessions` 中 `backend='tmux'` 的行,用相同 id 重新 attach,
75
+ tmux 负责重绘屏幕;
76
+ - 管理员 kill / 空闲回收 / 寿命到期则 `preserve=False`,真正 `tmux kill-session`。
77
+
78
+ tmux 为可选的系统依赖(需在主机安装,非 pip 依赖),默认路径不依赖它。
79
+
80
+ ## 4. WebSocket 协议
81
+
82
+ 三端(浏览器 / CLI 瘦客户端)共用同一协议:**二进制帧承载原始终端字节**,
83
+ **文本帧承载 JSON 控制消息**。二进制可避免多字节 UTF-8 字符在分片边界被截断。
84
+
85
+ ```
86
+ 文本帧(JSON 控制)
87
+ C→S {"type":"attach","session":"a1","cols":120,"rows":30}
88
+ C→S {"type":"input","data":"..."} # 备用:文本输入通道
89
+ C→S {"type":"resize","cols":120,"rows":30}
90
+ C→S {"type":"ping"}
91
+
92
+ S→C {"type":"attached","session":"a1","name":"..."}
93
+ S→C {"type":"exit","code":0}
94
+ S→C {"type":"error","msg":"..."}
95
+ S→C {"type":"pong"}
96
+
97
+ 二进制帧(原始终端字节)
98
+ C→S <按键 / 粘贴内容>
99
+ S→C <输出,含重连时的 scrollback 回放>
100
+ ```
101
+
102
+ **顺序保证**:`attach` 时在会话锁内先入队回放、再注册客户端,广播同样持锁,
103
+ 从而保证「回放 → 实时输出」的顺序,且回放不会漏掉锁切换窗口内的输出。
104
+
105
+ **认证**
106
+
107
+ - 浏览器:登录后签发 HttpOnly / Secure / SameSite Cookie,握手时校验
108
+ - CLI:`Authorization: Bearer <token>`
109
+ - 两种方式均校验 **Origin 白名单**(防 CSWSH)
110
+
111
+ ## 5. 数据模型(SQLite)
112
+
113
+ ```sql
114
+ users(id, username, password_hash, role[admin|user], totp_secret, disabled, created_at)
115
+
116
+ auth_sessions(id, user_id, token_hash, ip, user_agent, exp, last_seen) -- 登录态
117
+
118
+ term_sessions(id, name, owner_id, backend[local|tmux|ssh], command, argv, env,
119
+ cwd, idle_timeout, max_life, status, created_at, last_active)
120
+
121
+ audit_logs(id, user_id, term_session_id, event, payload, ip, ts)
122
+
123
+ settings(key, value)
124
+ ```
125
+
126
+ > 命名约定:`auth_sessions` 指**登录态**,`term_sessions` 指**终端会话**。
127
+ > 模式版本 2;旧库通过 `PRAGMA table_info` + `ALTER TABLE` 幂等迁移补齐新列。
128
+
129
+ ## 6. CLI 命令树(已实现)
130
+
131
+ ```
132
+ wsctl serve # 起控制面,默认配置开箱即用
133
+ wsctl serve --new "bash" # 启动时顺带开一个会话
134
+ wsctl serve --backend tmux # 默认使用 tmux 后端(跨重启恢复)
135
+ wsctl session list|new|kill|attach # new 支持 --backend local|tmux|ssh
136
+ wsctl session record|record-stop|recording
137
+ wsctl connect [url] [-s id] # 瘦客户端:本地 raw 终端直连
138
+ wsctl login|logout # 缓存/清除服务端凭据
139
+ wsctl user add|list|del|passwd|role|totp
140
+ wsctl audit # 查看审计日志(admin)
141
+ wsctl config show|path|edit|set|reload
142
+ wsctl version
143
+ ```
144
+
145
+ > `config set` 写入配置文件(校验 TOML),`config reload` 让运行中的服务热加载。
146
+
147
+
148
+ ## 7. 目录结构
149
+
150
+ ```
151
+ wsctl/
152
+ ├── pyproject.toml # hatchling · PyPI: wsctl · MIT
153
+ ├── DESIGN.md README.md LICENSE CHANGELOG.md
154
+ ├── src/wsctl/
155
+ │ ├── __init__.py # __version__
156
+ │ ├── __main__.py
157
+ │ ├── cli/ # serve/session/connect/user/config
158
+ │ ├── server/ # FastAPI: routes, ws, auth, security, deps
159
+ │ ├── core/
160
+ │ │ ├── session.py # TermSession + SessionManager ★核心
161
+ │ │ ├── pty.py # PTY 抽象(posix / win extra)
162
+ │ │ ├── scrollback.py # 环形缓冲
163
+ │ │ ├── store.py # SQLite
164
+ │ │ └── config.py
165
+ │ └── static/ # xterm.js + 前端(内嵌 wheel)
166
+ └── tests/
167
+ ```
168
+
169
+ ## 8. 依赖
170
+
171
+ ```
172
+ fastapi uvicorn[standard] typer rich
173
+ pydantic pydantic-settings argon2-cffi python-multipart
174
+ pyotp segno websockets
175
+
176
+ [win] → pywinpty
177
+ [e2e] → playwright
178
+ [dev] → ruff mypy pytest pytest-asyncio httpx build twine
179
+ ```
180
+
181
+ > tmux 后端需要系统安装 `tmux`(非 pip 依赖),未安装时自动回退 local。
182
+
183
+ ## 9. 安全清单(内建,非可选)
184
+
185
+ 1. 默认强制认证,`argon2` 哈希
186
+ 2. 会话 token 存 HttpOnly / Secure / SameSite Cookie;CLI 用 Bearer
187
+ 3. WebSocket 握手 Origin 白名单校验(防 CSWSH)
188
+ 4. 登录失败限速、IP allowlist
189
+ 5. 可选 TOTP 二次验证
190
+ 6. 全量审计日志
191
+ 7. 会话数 / 空闲超时 / 速率上限
192
+ 8. HTTPS/WSS:文档首选反向代理,内置 `--ssl` 兜底
193
+
194
+ ## 10. 里程碑
195
+
196
+ | 阶段 | 交付 |
197
+ |---|---|
198
+ | **M0** | 脚手架 + pyproject + ruff/mypy + CI + MIT |
199
+ | **M1** | SessionManager + PTY 保活 + WS attach/detach + 重连回放 + 最小前端,跑通单会话 |
200
+ | **M2** | 多会话 + 多标签 UI + 多用户/RBAC |
201
+ | **M3** | 审计日志 + 安全加固(限速 / allowlist / TOTP / Origin)|
202
+ | **M4** | Web 文件面板 + 可观测(/healthz · /metrics · JSON 日志)|
203
+ | **M5** | CLI `connect` 瘦客户端 + 文档 + PyPI Trusted Publishing |
204
+ | **M6** | 缺口补齐:`session new/attach`、`serve --new`、`config edit`、会话重命名、每会话连接上限、输入速率限制、并发压测 |
205
+ | **M7** | 可选 tmux 后端:会话跨服务重启恢复;优雅关闭时保留 tmux 会话;启动自动 reattach |
206
+ | **M8** | 会话只读分享:share token(可限时/可撤销)+ 二维码 + 匿名只读观看(输入被拒) |
207
+ | **M9** | SSH 后端:`backend=ssh` 结构化目标(host/user/port/identity/options/远端命令) |
208
+ | **M10** | 录制回放:asciinema cast v2 录制(可含输入)+ 自动录制 + 下载 API/CLI |
209
+ | **M11** | 优雅重启:SO_REUSEPORT 监听 socket,新实例先接管再停旧实例(零停机) |
210
+ | **M12** | 杂项:`settings` 表 + `term_sessions` 完整字段(含迁移)、Webhook、`config set`、主题/字体可配、移动端适配 |
211
+ | **M13** | 前端浏览器级验证:Playwright 无头 Chromium 跑通登录/终端/多标签/文件面板/分享+二维码/主题/只读分享 |
212
+ | **M14** | 可写分享(share writable) |
213
+ | **M15** | 录制前端回放 UI(vendored asciinema-player)+ 录制开关 |
214
+ | **M16** | 自定义快捷键(可编辑、本地持久化) |
215
+ | **M17** | 配置运行时热更新(文件监听 + 管理端点 + CLI) |
216
+ | **M18** | 每会话内存硬上限 + 每客户端字节上限 |
217
+ | **M19** | Sixel 图像渲染(addon-image)+ 可选 ZMODEM 传输(zmodem.js) |
218
+ | **M20** | 终端主题市场(10 内置主题 + 自定义 JSON 主题) |
219
+
220
+ ## 10.1 实现状态(截至 0.1.0)
221
+
222
+ **已实现**:M0–M20 全部交付项;二进制 WS 协议、会话与连接解耦、重连回放、
223
+ 多用户 RBAC、审计 + `/api/audit`、登录限速、IP allowlist、TOTP、安全响应头、
224
+ 文件面板(防穿越 + 上传限流)、`/metrics`、JSON 日志、`connect` 瘦客户端、
225
+ 会话重命名、每会话连接上限、输入令牌桶限速、并发/大输出压测、可选 tmux 后端
226
+ (跨重启恢复)、只读/可写分享(share token + 二维码 + 匿名观看)、SSH 后端、
227
+ asciinema 录制 + 前端回放、SO_REUSEPORT 零停机重启、Webhook、
228
+ `settings` 表与完整 `term_sessions` 字段、主题/字体/快捷键可配 + 移动端适配、
229
+ `config set`/`reload`、前端浏览器级自动化测试、每会话内存硬上限、
230
+ Sixel 渲染、可选 ZMODEM、终端主题市场。
231
+
232
+ **尚未实现**:无。唯一验证缺口:ZMODEM 的真实 `rz`/`sz` 传输未在 CI 端到端跑通
233
+ (环境无 lrzsz),仅验证了集成不破坏常规终端 I/O。
234
+
235
+ > 回归测试:`scripts/e2e/run_all.py` 提供 5 个后端端到端场景(server / CLI /
236
+ > connect / tmux 重启恢复 / SO_REUSEPORT 优雅重启);`pytest -m browser` 为浏览器级
237
+ > 测试;`pytest -m slow` 为并发/大输出压测。CI 在独立 job 中运行浏览器测试。
238
+
239
+ > 说明:进程回收依赖 `start_new_session` + `killpg` 与 `TermSession` 结束时的
240
+ > `wait()`,未安装全局 SIGCHLD handler(避免与 `subprocess` 争抢 PID);已跟踪
241
+ > 会话不会残留僵尸进程。
242
+
243
+ ## 11. Backlog(后续)
244
+
245
+ 无(v0.1.0 计划项已全部落地)。后续可考虑:ZMODEM 真机端到端测试、
246
+ 更多终端协议(Kitty graphics 等)。
247
+
248
+ ## 12. 发布
249
+
250
+ - 包名 / PyPI:`wsctl`(已验证可用)
251
+ - GitHub:`ThzxxArt/wsctl`
252
+ - 版本:语义化;初始 `0.1.0`
253
+ - CI:ruff + mypy + pytest(py3.11 / 3.12 / 3.13)
254
+ - 发布:tag 触发 PyPI Trusted Publishing(OIDC,无需存 token)
wsctl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ThzxxArt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.