@lvxianchao/dsh-web-guard 0.0.0-stage → 2.0.1
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.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +244 -2
- package/README.zh.md +248 -0
- package/cordis.patch.yml +12 -0
- package/docs/logo.svg +6 -0
- package/lib/admin.js +190 -0
- package/lib/auth.js +178 -0
- package/lib/i18n.js +273 -0
- package/lib/index.js +505 -0
- package/lib/inject.js +77 -0
- package/lib/page.js +119 -0
- package/lib/polyfill.js +34 -0
- package/lib/proxy.js +181 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +55 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.1 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
### Internal
|
|
6
|
+
|
|
7
|
+
- Publishing is automatic: pushing a `v*` tag publishes to npm from GitHub Actions through an OIDC trusted publisher, with provenance. A mismatched tag/version fails the run instead of shipping the wrong thing.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## 2.0.0 — 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- Renamed the package to `@lvxianchao/dsh-web-guard` (was `dsh-guard`) and the on-disk state file to `dsh-web-guard.json`. An old `dsh-guard.json` is **not** migrated: the gate generates a fresh password on first start and logs it once.
|
|
15
|
+
- The display name is now **DSH Web Guard** everywhere — login page, sign-out page, admin page, log prefix and health payload.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Localised plugin metadata via `locale/en.json` and `locale/zh.json`, so the DSH plugin list shows a translated title and description.
|
|
20
|
+
- The settings page opens in an overlay instead of navigating away, and closes only from its own Close button.
|
|
21
|
+
- Header actions on the settings page are real buttons; the repository link moved into the title.
|
|
22
|
+
- README (both languages): an explicit warning against disabling or uninstalling the gate, and recovery steps for when it has already happened.
|
|
23
|
+
|
|
24
|
+
## 1.0.0 — 2026-11-04
|
|
25
|
+
|
|
26
|
+
First stable release.
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
- **Password gate** — scrypt-hashed password, revocable sessions with sliding expiry, per-IP lockout after repeated failures.
|
|
31
|
+
- **Reverse proxy that removes the 403** — forwards to the harness listener with `Host`/`Origin` rewritten to the loopback authority, which the `/api` browser-trust fence accepts by design.
|
|
32
|
+
- **Automatic launch-token exchange** — the harness session cookie is minted in-process and injected upstream; the browser never sees a token and there is nothing to dig out of the journal.
|
|
33
|
+
- **Secure-context polyfill** — `crypto.randomUUID` shim for plain-HTTP deployments, where the real API is unavailable outside a secure context.
|
|
34
|
+
- **Admin page** at `/__guard/admin` — change the password (current password required), session lifetime, lockout threshold and duration, listener address/port, view and kick live sessions, sign out everywhere. Reachable from the shield button in the harness UI.
|
|
35
|
+
- **Localisation** — login, admin and sign-out pages follow the harness language setting (Chinese and English).
|
|
36
|
+
- **Optional native TLS** — `tls.certFile` / `tls.keyFile`; otherwise run behind NPM, Caddy or any TLS-terminating proxy.
|
|
37
|
+
- **Zero runtime dependencies** — Node standard library only, no build step.
|
|
38
|
+
|
|
39
|
+
### Configuration precedence
|
|
40
|
+
|
|
41
|
+
`$DSH_HOME/dsh-guard.json` (written by the admin page) → plugin config in `cordis.patch.yml` → built-in defaults.
|
|
42
|
+
|
|
43
|
+
### Known limitations
|
|
44
|
+
|
|
45
|
+
- Single password, single privilege level — not a multi-user system.
|
|
46
|
+
- Listener address/port changes require a DSH restart.
|
|
47
|
+
- Sessions live in memory; a DSH restart signs everyone out.
|
|
48
|
+
- The harness itself must listen on loopback, otherwise the gate can be bypassed by connecting to its port directly.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lvxianchao
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,245 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/logo.svg" width="96" height="96" alt="DSH Web Guard">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">DSH Web Guard</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
A password gate for the <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> Web UI —
|
|
9
|
+
one plugin that fixes four deployment problems at once.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center"><a href="README.zh.md">中文说明</a></p>
|
|
13
|
+
|
|
14
|
+
| Problem | Vanilla behaviour | dsh-web-guard |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| No human login | A per-process `?token=` printed to the terminal, new on every restart | One password + revocable sessions |
|
|
17
|
+
| `/api` returns 403 behind a proxy | The browser-trust fence requires `Host` to arrive unrewritten; NPM/Caddy rewrite it | The proxy rewrites `Host`/`Origin` to the loopback authority, which the fence accepts |
|
|
18
|
+
| Token lives in the journal | `journalctl \| grep token` before you can get in | Exchanged in-process; the browser never sees a token |
|
|
19
|
+
| UI breaks on plain HTTP | `crypto.randomUUID is not a function` outside a secure context | Injects a `getRandomValues` polyfill |
|
|
20
|
+
|
|
21
|
+
Zero runtime dependencies (Node standard library only), no build step — `dsh plugin add` and it runs.
|
|
22
|
+
|
|
23
|
+
## Screenshots
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/login-en.png" width="47%" alt="Login page">
|
|
27
|
+
|
|
28
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/logout-en.png" width="47%" alt="Signed-out page">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/admin-en.png" width="92%" alt="Admin page">
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center"><sub>Login page · sign-out confirmation · admin page (password, session and lockout policy, listener, live sessions)</sub></p>
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
browser ──HTTP(S)──> dsh-web-guard :3081 ──> DSH listener (loopback)
|
|
41
|
+
│
|
|
42
|
+
├─ not signed in → /login
|
|
43
|
+
├─ signed in → forward, Host/Origin rewritten to loopback
|
|
44
|
+
└─ injects the harness cookie (never exposed to the browser)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The harness fence accepts a loopback authority, so making every upstream request look like one removes the 403 entirely. Authentication happens **in the proxy**, not as a front-end overlay — a request without a session cookie never reaches the harness.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# npm (once published)
|
|
53
|
+
dsh plugin --profile web add @lvxianchao/dsh-web-guard
|
|
54
|
+
|
|
55
|
+
# from GitHub
|
|
56
|
+
dsh plugin --profile web add "github:lvxianchao/dsh-web-guard"
|
|
57
|
+
|
|
58
|
+
# from a local checkout
|
|
59
|
+
dsh plugin --profile web add /path/to/dsh-web-guard
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`main` only ever carries released code (development happens on `dev`), so this
|
|
63
|
+
installs the latest stable release. Installing is enough — the package declares `dsh.bundle`, so the guard row is
|
|
64
|
+
mounted automatically. Restart `dsh`.
|
|
65
|
+
|
|
66
|
+
**On first start a password is generated**, printed to the terminal and stored
|
|
67
|
+
(hashed) in `<profile>/data/dsh-web-guard.json` with mode `0600`. To pin your own,
|
|
68
|
+
add a row to `~/.dsh/profiles/web/cordis.patch.yml` (it overrides the bundle
|
|
69
|
+
defaults):
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- id: guard
|
|
73
|
+
config:
|
|
74
|
+
listenHost: '0.0.0.0'
|
|
75
|
+
listenPort: 3081
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Note that a profile patch replaces the whole `config` object, so restate every
|
|
79
|
+
key you care about.
|
|
80
|
+
|
|
81
|
+
> The harness itself must listen on loopback (its default). If it is bound to a LAN address, clients can bypass the gate by connecting there directly — the plugin detects this at startup and warns.
|
|
82
|
+
|
|
83
|
+
## ⚠️ This is the front door, not an optional plugin
|
|
84
|
+
|
|
85
|
+
Once installed, **dsh-web-guard is the only way in**: it listens on 3081 and forwards to a DSH that is bound to loopback only (3080). If the plugin stops loading, 3081 stops listening and **both your LAN and the internet lose access at once** — while DSH itself keeps running, with your sessions and data intact. That is the dangerous shape: the door is welded shut and you are standing outside it.
|
|
86
|
+
|
|
87
|
+
**If you exposed this address to the internet, take this seriously.**
|
|
88
|
+
|
|
89
|
+
Three rules:
|
|
90
|
+
|
|
91
|
+
1. **Do not uninstall it and do not disable it.** Concretely: don't run `dsh plugin --profile web remove @lvxianchao/dsh-web-guard`, don't drop the entry from `dsh.profile.bundles` in the profile's `package.json`, and don't add `disabled: true` to it in `cordis.patch.yml`.
|
|
92
|
+
2. **DSH's Settings → Plugins page is a read-only inventory** — status tags only (enabled / disabled / failed), no toggles. Don't go looking for an off switch there; accidents happen on the command line instead.
|
|
93
|
+
3. Upgrading, editing config and `systemctl restart dsh-web` are all safe. Only "making it not load" is not.
|
|
94
|
+
|
|
95
|
+
### If you closed it by accident
|
|
96
|
+
|
|
97
|
+
Relax: **DSH is still running on `127.0.0.1:3080`; sessions, workspace and password are all intact.** Only the gate is gone. Recovery takes two commands and **does not depend on the plugin being loaded**:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# 1) install it again (use file: if you installed from a checkout)
|
|
101
|
+
dsh plugin --profile web add file:/path/to/dsh-web-guard
|
|
102
|
+
# or: dsh plugin --profile web add github:lvxianchao/dsh-web-guard
|
|
103
|
+
|
|
104
|
+
# 2) restart, and 3081 starts listening again
|
|
105
|
+
sudo systemctl restart dsh-web
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If you removed it by adding `disabled: true`, delete that line and restart. **Your password is not lost** — the hash lives in `<profile>/data/dsh-web-guard.json`; leave that file alone.
|
|
109
|
+
|
|
110
|
+
**The real trap is not these two commands — it is that you can no longer reach the machine.**
|
|
111
|
+
|
|
112
|
+
The gate only proxies DSH's HTTP entry point; it does not touch SSH. But if all your remote access funnels through this one hostname, losing the plugin leaves you exactly one option: **a path that does not go through it.** Have one ready, and **verify it now**, not after the incident:
|
|
113
|
+
|
|
114
|
+
- a hypervisor console (PVE / ESXi / cloud VNC) into the container or VM, then run the two commands;
|
|
115
|
+
- or an independent SSH path (direct LAN, bastion, or VPN).
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
One last thing: **do not keep these recovery commands only on the machine that hosts the gate.** Copy them somewhere you can open from outside.
|
|
119
|
+
|
|
120
|
+
## Deployment topologies
|
|
121
|
+
|
|
122
|
+
**1. Direct, no reverse proxy** — let the plugin terminate TLS:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
config:
|
|
126
|
+
listenHost: '0.0.0.0'
|
|
127
|
+
listenPort: 8443
|
|
128
|
+
tls:
|
|
129
|
+
certFile: /path/fullchain.pem
|
|
130
|
+
keyFile: /path/privkey.pem
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The session cookie is issued with `Secure`. Point DNS at the host and open one port.
|
|
134
|
+
|
|
135
|
+
**2. Behind a reverse proxy (NPM / Caddy / Traefik)** — the proxy does TLS, the plugin does auth:
|
|
136
|
+
|
|
137
|
+
Set the upstream to `http://<dsh-host>:3081` and enable WebSocket support. **No Host-rewrite configuration is needed** — that is the part this plugin removes.
|
|
138
|
+
|
|
139
|
+
**3. Plain LAN** — HTTP plus password. Simplest, least safe: the session cookie can be sniffed on a shared network.
|
|
140
|
+
|
|
141
|
+
## Language
|
|
142
|
+
|
|
143
|
+
The login and admin pages follow the harness language setting: the plugin reads
|
|
144
|
+
`preference` from the `locale` row in the profile patch file (`~/.dsh/profiles/web/cordis.patch.yml`),
|
|
145
|
+
which is exactly where the harness settings UI writes it. Chinese and English are
|
|
146
|
+
supported. With no explicit preference the pages fall back to the browser's
|
|
147
|
+
`Accept-Language`.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
| Key | Default | Meaning |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `listenHost` | `0.0.0.0` | Interface the gate listens on |
|
|
154
|
+
| `listenPort` | `3081` | Gate port; must differ from the harness port |
|
|
155
|
+
| `password` | — | Plaintext password, hashed at startup |
|
|
156
|
+
| `passwordHash` | — | `scrypt$N$r$p$salt$key`; takes precedence over `password` |
|
|
157
|
+
| `sessionDays` | `7` | Session lifetime; `0` = expires when the browser closes |
|
|
158
|
+
| `maxAttempts` | `5` | Consecutive failures before a temporary lock |
|
|
159
|
+
| `lockMinutes` | `5` | Lock duration |
|
|
160
|
+
| `polyfill` | `true` | Inject the `crypto.randomUUID` shim |
|
|
161
|
+
| `title` | `DeepSeek Harness` | Heading on the login page |
|
|
162
|
+
| `tls` | `null` | `{certFile, keyFile}` to serve HTTPS |
|
|
163
|
+
|
|
164
|
+
Generate a hash for the config:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv[1])))" 'your-password'
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Security model
|
|
171
|
+
|
|
172
|
+
- Passwords are hashed with scrypt (random salt). The plaintext appears exactly once, in the generated-password log line.
|
|
173
|
+
- Session tokens are stored server-side as SHA-256 digests; cookies are `HttpOnly`, `SameSite=Lax`, and `Secure` when TLS is on.
|
|
174
|
+
- Login POSTs are checked for a same-hostname `Origin`. The session cookie's `SameSite` attribute is the primary CSRF defence.
|
|
175
|
+
- Repeated failures from one IP trigger a temporary lock (in-memory, cleared on restart).
|
|
176
|
+
- `/logout` ends the current browser session. To revoke everything, change the password and restart, or delete `$DSH_HOME/dsh-web-guard.json`.
|
|
177
|
+
|
|
178
|
+
**This is not multi-user.** One password, one privilege level. The harness agent can execute code, so anyone who gets in has code execution on that host — which is the product's design, not a bug in the gate.
|
|
179
|
+
|
|
180
|
+
## Admin page
|
|
181
|
+
|
|
182
|
+
A shield button appears at the bottom-right of the harness UI once you are signed in. It opens `/__guard/admin` (bookmark the URL if you prefer):
|
|
183
|
+
|
|
184
|
+
- change the password (the current one is required)
|
|
185
|
+
- session lifetime, lockout threshold/duration, page title, repository link — **applied on save**
|
|
186
|
+
- listen host/port — marked "takes effect after restart"
|
|
187
|
+
- live session list (source IP, browser, last activity): kick one device, or sign out everywhere
|
|
188
|
+
|
|
189
|
+
Anonymous visitors are redirected to the login page with no `next` echo, and the login page does not advertise the admin path.
|
|
190
|
+
|
|
191
|
+
**Why not a harness settings page instead:** that requires a `dsh.client` bundle (React plus a build step), which takes the plugin from ~30 KB to hundreds of KB, adds a build toolchain to a project that otherwise has none, and ties it to the harness slot API while that API is still moving — a version mismatch can blank the whole settings screen. A standalone page stays build-free and survives harness upgrades.
|
|
192
|
+
|
|
193
|
+
## Troubleshooting
|
|
194
|
+
|
|
195
|
+
| Symptom | Cause |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `502 upstream unreachable` | The harness is down, or `listenPort` collides with it |
|
|
198
|
+
| Keeps bouncing to `/login` | No session cookie; make sure you use one address (an HTTP cookie is not shared with HTTPS) |
|
|
199
|
+
| Login succeeds then bounces back | Clock skew between the client and the host |
|
|
200
|
+
| `randomUUID is not a function` | `polyfill` is disabled |
|
|
201
|
+
| LAN can still reach the harness port | The harness is not bound to loopback — see the startup warning |
|
|
202
|
+
|
|
203
|
+
All log lines are prefixed `[dsh-web-guard]` and share the harness's journal stream. Successful requests are not logged (the Web UI polls constantly); only failures and slow requests are.
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# shadow instance: serves the real profile without touching it
|
|
209
|
+
dsh --profile web --patch ./dev.yml --host 127.0.0.1 --port 3099 --no-open
|
|
210
|
+
|
|
211
|
+
# e2e against a running gate
|
|
212
|
+
GUARD_URL=http://127.0.0.1:3081 GUARD_PASSWORD=... node test/e2e.mjs
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`dev.yml` only needs the `insert` row shown above with `listenHost: 127.0.0.1` and a distinct port.
|
|
216
|
+
|
|
217
|
+
The plugin uses only public harness APIs: `webServer.register`, `webServer.tapIndex`, `webServer.getOrigin`, and `connection.authenticatedUrl`. No monkey-patching, no source modification.
|
|
218
|
+
|
|
219
|
+
## Companion plugin
|
|
220
|
+
|
|
221
|
+
DSH's built-in document preview cannot play video — opening an `.mp4` in the sidebar says the format is unsupported. [dsh-video-preview](https://github.com/lvxianchao/dsh-video-preview) adds a streaming video renderer for the same sidebar: `mp4` / `webm` / `mov`, byte-range seeking, no size cap.
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
dsh plugin --profile web add "github:lvxianchao/dsh-video-preview"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The two plugins are independent — install either or both.
|
|
228
|
+
|
|
229
|
+
## Sponsor
|
|
230
|
+
|
|
231
|
+
If dsh-web-guard saved you some setup time, a coffee is welcome:
|
|
232
|
+
|
|
233
|
+
- ☕ **[Afdian](https://afdian.com/a/lvxianchao)** — mainland China, WeChat / Alipay
|
|
234
|
+
- 🌍 **[Ko-fi](https://ko-fi.com/lvxianchao)** — international, PayPal
|
|
235
|
+
|
|
236
|
+
<details>
|
|
237
|
+
<summary>WeChat tip QR</summary>
|
|
238
|
+
|
|
239
|
+
<p align="center"><img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/wechat.jpg" width="220" alt="WeChat tip QR"></p>
|
|
240
|
+
|
|
241
|
+
</details>
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/logo.svg" width="96" height="96" alt="DSH Web Guard">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">DSH Web Guard</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
给 <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a>(DSH)Web UI 加一道<strong>密码闸门</strong>——
|
|
9
|
+
一个插件同时解决四件部署上的麻烦事。
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center"><a href="README.md">English</a></p>
|
|
13
|
+
|
|
14
|
+
| 麻烦 | 原生表现 | dsh-web-guard |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 没有人类登录 | 只有进程启动时打印的 `?token=`,重启就换 | 一个访问密码 + 可吊销会话 |
|
|
17
|
+
| 反代一律 403 | `/api` 栅栏要求 `Host` 原样转发,NPM/Caddy 通常会改写 | 代理把 `Host`/`Origin` 改写成回环 authority,天然放行 |
|
|
18
|
+
| token 要挖日志 | `journalctl \| grep token` 才能进站 | 进程内自动兑换,浏览器永远见不到 token |
|
|
19
|
+
| 明文 HTTP 功能报错 | 非安全上下文下 `crypto.randomUUID is not a function` | 注入 polyfill 兜底 |
|
|
20
|
+
|
|
21
|
+
零运行时依赖(只用 Node 标准库),没有构建步骤,`dsh plugin add` 装完即跑。
|
|
22
|
+
|
|
23
|
+
## 截图
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/login.png" width="47%" alt="登录页">
|
|
27
|
+
|
|
28
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/logout.png" width="47%" alt="退出页">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/admin.png" width="92%" alt="设置页">
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center"><sub>登录页 · 退出确认页 · 设置页(改密码、会话与锁定策略、监听地址、在线设备)</sub></p>
|
|
36
|
+
|
|
37
|
+
## 它怎么工作
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
浏览器 ──HTTP(S)──> dsh-web-guard :3081 ──> DSH 监听器 (回环)
|
|
41
|
+
│
|
|
42
|
+
├─ 未登录:/login
|
|
43
|
+
├─ 已登录:转发,Host/Origin 改写为回环 authority
|
|
44
|
+
└─ 注入 DSH cookie(浏览器不可见)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
DSH 的 `/api` 栅栏接受回环 authority;代理让每个上游请求都看起来来自回环,所以不再 403。密码验证发生在代理里,**不是前端遮罩**——没有 cookie 的请求在进入 DSH 之前就被挡下。
|
|
48
|
+
|
|
49
|
+
## 安装
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# npm(发布后)
|
|
53
|
+
dsh plugin --profile web add @lvxianchao/dsh-web-guard
|
|
54
|
+
|
|
55
|
+
# 从 GitHub 安装
|
|
56
|
+
dsh plugin --profile web add "github:lvxianchao/dsh-web-guard"
|
|
57
|
+
|
|
58
|
+
# 从本地目录安装
|
|
59
|
+
dsh plugin --profile web add /path/to/dsh-web-guard
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`main` 只放已发布的代码(开发在 `dev` 分支),所以这条命令装到的就是最新稳定版。装完即挂载——包内声明了 `dsh.bundle`,guard 行会自动叠加。重启 `dsh`。
|
|
63
|
+
|
|
64
|
+
**首次启动会自动生成一个密码**,打印在终端并保存到 `<profile>/data/dsh-web-guard.json`(0600)。
|
|
65
|
+
想固定密码就在 `~/.dsh/profiles/web/cordis.patch.yml` 里加一行覆盖(profile patch 优先于 bundle 默认值):
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
- id: guard
|
|
69
|
+
config:
|
|
70
|
+
listenHost: '0.0.0.0'
|
|
71
|
+
listenPort: 3081
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
注意 profile patch 会整体替换 `config`,要改的键都得重写一遍。
|
|
75
|
+
|
|
76
|
+
> 前提:DSH 自己必须绑回环(默认就是 `127.0.0.1`)。如果它绑在局域网 IP 上,客户端可以绕过闸门直连——插件启动时会检测并告警。
|
|
77
|
+
|
|
78
|
+
## ⚠️ 这是入口,不是可以随手关掉的插件
|
|
79
|
+
|
|
80
|
+
装上之后,**dsh-web-guard 就是唯一入口**:它监听 3081,把请求转给只听回环的 DSH(3080)。插件一旦不加载,3081 就不再监听,**内网和外网会同时打不开**——而 DSH 本体还活着,会话和数据都在,只是你够不着它。
|
|
81
|
+
|
|
82
|
+
特别提醒:**如果你把这个地址开放到了外网,这一条要格外当心。** 关掉插件等于把门焊死,而你正站在门外。
|
|
83
|
+
|
|
84
|
+
所以请记住三件事:
|
|
85
|
+
|
|
86
|
+
1. **不要卸载它,也不要让它被禁用。** 具体是:不要执行 `dsh plugin --profile web remove @lvxianchao/dsh-web-guard`;不要从 profile 的 `package.json` 里删掉 `dsh.profile.bundles` 中的这一项;也不要在 `cordis.patch.yml` 里给它加 `disabled: true`。
|
|
87
|
+
2. **DSH 的「设置 → 插件」页是只读清单**,只有状态标签(已启用 / 未启用 / 加载失败),没有开关。别去那里找关闭按钮——也正因为没有开关,误关基本只发生在命令行里。
|
|
88
|
+
3. 升级、改配置、`systemctl restart dsh-web` 都是安全的;只有"让它不加载"才危险。
|
|
89
|
+
|
|
90
|
+
### 不小心关掉了,怎么救
|
|
91
|
+
|
|
92
|
+
先别慌:**DSH 本体一直在 `127.0.0.1:3080` 跑着,会话、工作区、密码都没丢**,丢的只是门口那一层。恢复只要两条命令,而且**完全不依赖这个插件本身**:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 1) 重新装回(本地目录装的就用 file:,没装过就用 GitHub)
|
|
96
|
+
dsh plugin --profile web add file:/path/to/dsh-web-guard
|
|
97
|
+
# 或:dsh plugin --profile web add github:lvxianchao/dsh-web-guard
|
|
98
|
+
|
|
99
|
+
# 2) 重启,3081 会重新监听
|
|
100
|
+
sudo systemctl restart dsh-web
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
如果当初是加了 `disabled: true` 弄没的,把那一行删掉再重启即可。**密码不用重设**——哈希存在 `<profile>/data/dsh-web-guard.json`,只要没删这个文件就还在。
|
|
104
|
+
|
|
105
|
+
**真正的坑不在这两条命令,而在于:此刻你已经连不上这台机器了。**
|
|
106
|
+
|
|
107
|
+
闸门只代理 DSH 的 HTTP 入口,不碰 SSH;但如果你把远程访问都收在这一个域名上,那插件一没,你就只剩一条路——**一条不经过这个插件的后路**。至少准备一条,并且**现在就验证它真的能用**(别等出事才发现自己没配):
|
|
108
|
+
|
|
109
|
+
- 宿主控制台(PVE / ESXi / 云厂商 VNC)→ 进容器或虚机 → 执行上面两条命令;
|
|
110
|
+
- 或者一条独立的 SSH 通道(内网直连、跳板机、VPN)。
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
最后一句:**别把这两条恢复命令只存在这台机器上。** 它挂的时候你也看不到它,抄一份到你能从外面打开的地方。
|
|
114
|
+
|
|
115
|
+
## 三种部署拓扑
|
|
116
|
+
|
|
117
|
+
**1. 直接暴露(没有反代)** —— 让插件自己终结 TLS:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
config:
|
|
121
|
+
listenHost: '0.0.0.0'
|
|
122
|
+
listenPort: 8443
|
|
123
|
+
tls:
|
|
124
|
+
certFile: /path/fullchain.pem
|
|
125
|
+
keyFile: /path/privkey.pem
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
浏览器访问 `https://<host>:8443`,会话 cookie 自动带 `Secure`。DNS 指过来、只开一个端口即可。
|
|
129
|
+
|
|
130
|
+
**2. 反代之后(NPM / Caddy / Traefik)** —— 反代只做 TLS,认证交给插件:
|
|
131
|
+
|
|
132
|
+
反代上游填 `http://<dsh-host>:3081`,开启 WebSocket。**不需要**任何 Host 改写配置——这正是本插件要消掉的那部分。
|
|
133
|
+
|
|
134
|
+
**3. 纯局域网** —— 明文 HTTP + 密码,最简单,也最不安全(会话 cookie 可被嗅探)。
|
|
135
|
+
|
|
136
|
+
## 语言
|
|
137
|
+
|
|
138
|
+
登录页和管理页跟随 DSH 的语言设置:插件读取 profile patch 文件
|
|
139
|
+
(`~/.dsh/profiles/web/cordis.patch.yml`)里 `locale` 行的 `preference`,
|
|
140
|
+
也就是 DSH 设置界面写入的同一个位置。目前支持中文和英文。没有显式偏好时,
|
|
141
|
+
按浏览器的 `Accept-Language` 回退。
|
|
142
|
+
|
|
143
|
+
## 配置
|
|
144
|
+
|
|
145
|
+
| 键 | 默认 | 说明 |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `listenHost` | `0.0.0.0` | 闸门监听地址 |
|
|
148
|
+
| `listenPort` | `3081` | 闸门端口,不能与 DSH 端口相同 |
|
|
149
|
+
| `password` | — | 明文密码(启动时哈希) |
|
|
150
|
+
| `passwordHash` | — | `scrypt$N$r$p$salt$key`,优先于 `password` |
|
|
151
|
+
| `sessionDays` | `7` | 会话时长;`0` = 关浏览器即失效 |
|
|
152
|
+
| `maxAttempts` | `5` | 连续失败次数,超过临时锁定 |
|
|
153
|
+
| `lockMinutes` | `5` | 锁定时长 |
|
|
154
|
+
| `polyfill` | `true` | 注入 `crypto.randomUUID` 兜底 |
|
|
155
|
+
| `title` | `DeepSeek Harness` | 登录页标题 |
|
|
156
|
+
| `tls` | `null` | `{certFile, keyFile}`,启用 HTTPS |
|
|
157
|
+
|
|
158
|
+
生成密码哈希:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
node -e "import('./lib/auth.js').then(m=>console.log(m.hashPassword(process.argv[1])))" 'your-password'
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 安全模型
|
|
165
|
+
|
|
166
|
+
- 密码用 scrypt(随机盐)哈希;明文只在"自动生成密码"那一行日志里出现一次。
|
|
167
|
+
- 会话 token 服务端只存 SHA-256 摘要;cookie 是 `HttpOnly` + `SameSite=Lax`,启用 TLS 时加 `Secure`。
|
|
168
|
+
- 登录 POST 校验 `Origin` 的 hostname 与请求 `Host` 一致(只比 hostname,因为反代常剥端口)。CSRF 的第一道防线仍是 `SameSite`。
|
|
169
|
+
- 同一 IP 连续失败会临时锁定(内存计数,重启清零)。
|
|
170
|
+
- `/logout` 只退当前浏览器;要全部吊销就改密码并重启,或删掉 `$DSH_HOME/dsh-web-guard.json`。
|
|
171
|
+
|
|
172
|
+
**这不是多用户系统**:一个密码,一个权限级别。DSH 的 agent 本身能执行代码,任何登录者都拥有该机器的执行权限——这是产品设计,不是闸门的缺陷。
|
|
173
|
+
|
|
174
|
+
## 管理页
|
|
175
|
+
|
|
176
|
+
登录后 DSH 界面右下角会出现一个盾牌按钮,点开就是 `/__guard/admin`(也可以直接把地址收藏起来):
|
|
177
|
+
|
|
178
|
+
- 修改登录密码(必须验证当前密码)
|
|
179
|
+
- 会话有效期、失败锁定次数与时长、页面标题、GitHub 链接 —— **保存即生效**
|
|
180
|
+
- 监听地址/端口 —— 标注「重启后生效」
|
|
181
|
+
- 活跃会话列表(来源 IP、浏览器、最后活动):可以踢名单个设备,或一键退出所有设备
|
|
182
|
+
|
|
183
|
+
未登录访问管理页只会被弹回登录页,且不保留跳转路径;登录页上不出现任何管理入口。
|
|
184
|
+
|
|
185
|
+
**为什么不做成 DSH 设置页里的一项**:那需要 `dsh.client` 前端模块和构建产物(React + 打包),插件会从约 30 KB 涨到几百 KB,给一个本来没有构建链的项目引入工具链,还会绑死仍在变动的 slot API —— 版本对不上时整个设置页可能白屏。独立页面零构建、零依赖,DSH 升级也不受影响。
|
|
186
|
+
|
|
187
|
+
## 排障
|
|
188
|
+
|
|
189
|
+
| 现象 | 原因 |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `502 upstream unreachable` | DSH 没起,或 `listenPort` 撞了 |
|
|
192
|
+
| 一直跳 `/login` | 浏览器没带 cookie;检查是否混用了 HTTP/HTTPS 两个地址(cookie 不共享) |
|
|
193
|
+
| 登录后立刻弹回 | 客户端与主机时间偏差导致会话判定过期 |
|
|
194
|
+
| 界面报 `randomUUID` | 把 `polyfill` 关掉了 |
|
|
195
|
+
| 局域网能直连 DSH 端口 | DSH 绑的不是回环,见启动告警 |
|
|
196
|
+
|
|
197
|
+
日志统一带 `[dsh-web-guard]` 前缀,与 DSH 自身日志在同一个 journal 里。成功请求不记录(Web UI 会持续轮询),只记失败和慢请求。
|
|
198
|
+
|
|
199
|
+
## 开发
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# 影子实例:复用真实 profile,但不改动它
|
|
203
|
+
dsh --profile web --patch ./dev.yml --host 127.0.0.1 --port 3099 --no-open
|
|
204
|
+
|
|
205
|
+
# 对运行中的闸门跑 e2e
|
|
206
|
+
GUARD_URL=http://127.0.0.1:3081 GUARD_PASSWORD=... node test/e2e.mjs
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`dev.yml` 只需要上面那段 `insert`,把 `listenHost` 改成 `127.0.0.1`、端口换开即可。
|
|
210
|
+
|
|
211
|
+
`main` 是发布分支,只放已发布并打过 tag 的代码;日常开发在 `dev` 分支,确认发版时才合并回 `main`。
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
git checkout dev
|
|
215
|
+
# … 修改、提交、测试 …
|
|
216
|
+
git checkout main && git merge --ff-only dev
|
|
217
|
+
git tag -a vX.Y.Z && git push origin main --tags
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
插件只用了公开的 harness API:`webServer.register`、`webServer.tapIndex`、`webServer.getOrigin`、`connection.authenticatedUrl`。没有猴子补丁,没有改源码。
|
|
221
|
+
|
|
222
|
+
## 配套插件
|
|
223
|
+
|
|
224
|
+
DSH 自带的文档预览放不了视频——在侧边栏点开 `.mp4` 只会提示格式不支持。[dsh-video-preview](https://github.com/lvxianchao/dsh-video-preview) 给同一个侧边栏补上流式视频渲染器:`mp4` / `webm` / `mov`,支持字节范围拖动、没有大小上限。
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
dsh plugin --profile web add "github:lvxianchao/dsh-video-preview"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
两个插件互相独立,可以只装一个,也可以都装。
|
|
231
|
+
|
|
232
|
+
## 赞助
|
|
233
|
+
|
|
234
|
+
如果这个插件帮你省了点折腾的时间,欢迎请我喝杯咖啡:
|
|
235
|
+
|
|
236
|
+
- ☕ **[爱发电](https://afdian.com/a/lvxianchao)** —— 国内,微信 / 支付宝
|
|
237
|
+
- 🌍 **[Ko-fi](https://ko-fi.com/lvxianchao)** —— 国际,PayPal
|
|
238
|
+
|
|
239
|
+
<details>
|
|
240
|
+
<summary>微信赞赏码</summary>
|
|
241
|
+
|
|
242
|
+
<p align="center"><img src="https://raw.githubusercontent.com/lvxianchao/dsh-web-guard/main/docs/wechat.jpg" width="220" alt="微信赞赏码"></p>
|
|
243
|
+
|
|
244
|
+
</details>
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Bundle patch: mounted automatically when this package is installed as a
|
|
2
|
+
# profile bundle (`dsh plugin --profile web add @lvxianchao/dsh-web-guard`).
|
|
3
|
+
#
|
|
4
|
+
# Installing the package therefore needs no manual `cordis.patch.yml` edit.
|
|
5
|
+
# Anything set in the profile's own patch file still wins, because profile
|
|
6
|
+
# patches are applied after bundle patches.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: guard
|
|
9
|
+
name: '@lvxianchao/dsh-web-guard'
|
|
10
|
+
config:
|
|
11
|
+
listenHost: '0.0.0.0'
|
|
12
|
+
listenPort: 3081
|
package/docs/logo.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="dsh-guard">
|
|
2
|
+
<rect width="96" height="96" rx="22" fill="#12151a"/>
|
|
3
|
+
<rect x="0.5" y="0.5" width="95" height="95" rx="21.5" fill="none" stroke="#24282e"/>
|
|
4
|
+
<path d="M48 18 26 27.5v20.2c0 13.8 9.7 26.6 22 29.3 12.3-2.7 22-15.5 22-29.3V27.5L48 18Z"
|
|
5
|
+
fill="none" stroke="#3f6df0" stroke-width="5" stroke-linejoin="round"/>
|
|
6
|
+
</svg>
|