dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH Web 密码门禁 – Cookie 认证 + 首次强制设密 + 登录失败锁定60s + 内置访问日志查看器。零依赖,原生集成设置页。
/api/<endpoint> exact Fetch routes, so it no longer occupies the shared /api interceptor and normal APIs such as the session list work again.ReferenceError (they called a non-existent writePasswordFile).host:port again now reuses the original entry index instead of erroring out.client-connection module rewrite, a __DSH_TRANSPORT__.ownsHost pre-shim is injected as a second line of defence.Existing passwords, upstream entries, and sessions are unchanged.
简体中文 | English
A zero-dependency DeepSeek Harness web plugin that puts a web password gate in front of DSH, with one-password-per-backend routing for guest and tool entries.
browser → (TLS / reverse proxy) → dsh-web-pass :3081 ┬→ 127.0.0.1:3080 (owner DSH, runs this plugin)
├→ 127.0.0.1:3085 (clean guest DSH, optional)
└→ 127.0.0.1:5101 (openclaw, fnOS and other local services, optional)
Auth & sessions
/gate/logout accepts POST only (Logout CSRF protection).Multi-password multi-upstream (guest mode, v0.3.2)
upstreams.json (survives restarts), patch rows keep working.DSH enhancements
dsh web process, it exchanges DSH's launch token for a browser cookie and injects it — visitors just pass this gate, no DSH token/cookie handling needed.Logs & observability
/dsh-logs/: password-verification requests only; size-based rotation (default 1MB × 7 files) with a hard total size cap; IPs are pseudonymized (HMAC-SHA256 + network prefix), raw IPs never stored.Fixed in v0.3.4
X-Frame-Options: DENY and friends (meant to protect the login page), which blanked pages that backends like fnOS embed via same-origin iframes — Docker / suite apps showed "refused to connect" through the gate. These headers now apply only to the gate's own pages (login / logs); proxied responses keep the upstream's original headers, and any security headers the upstream itself sends still take effect.Hardened in v0.3.5
loginLockMs × backoff multiplier, escalating 2× per repeat (up to 16×); the multiplier survives lock expiry and returns to 1× only after 1 hour with no failures; probes during a lock are not counted and do not extend it.text/html responses automatically degrade to streaming pass-through (injection skipped), and a client disconnect aborts the upstream immediately — prevents OOMing the gate that shares the DSH process.dws_session no longer reaches any upstream (it used to be forwarded verbatim to dsh: false upstreams)./gate/setup must carry the HttpOnly cookie issued by GET /gate-setup, so drive-by cross-site form posts cannot hijack the gate.host:port; both must match on every check — reordering patch rows or changing ports invalidates old sessions automatically (no more manual sessions.jsonl cleanup). Upgrade note: log in again once after upgrading to v0.3.5.resolveEntries now de-duplicates patch rows by host:port.lib/ split by domain into 9 cohesive modules with duplicate implementations merged; the exported API is unchanged — pure code moves with zero behavior change.Hardened in v0.3.6 (hardening release, no new features, no data migration)
dws_session / dws_setup are filtered on all three exits (transparent, rewritten, WebSocket handshake) via one shared sanitizeUpstreamSetCookie(); upstreams can only set their own business cookies.Forwarded / X-Forwarded-* / X-Real-IP etc. are dropped and rebuilt canonically per trustProxy (socket peer by default; trusted front-proxy's left-most value when enabled) — upstreams never see forgeable client assertions again.GET /gate/logout returns 405 (Logout CSRF protection); the logout chip now POSTs.Fixed & adapted in v0.3.7 (DSH v0.1.5-rc.1)
/api interceptor: the old build registered its handler as the single interceptor on the shared /api channel, which DSH 0.1.5's API Gateway also depends on — so ordinary /api requests (the session list in particular) could no longer fall through to the Gateway. Each endpoint now owns an exact POST /api/<endpoint> Fetch route (ctx.connection.fetch.register), still passing Connection's Host/Origin and browser authentication, leaving every other API untouched.setPasswordHash called writePasswordFile, which was never defined nor imported (a rename missed during the v0.3.5 module split), so both the settings-page reset and the forced first-time setup threw a ReferenceError that the error handler swallowed into a generic message. Corrected to writePasswordHash.host:port — so re-adding the same address was skipped and reported "runtime upstream not added to the in-memory entry table". The entry index is now reused and the soft delete lifted — order and index (the session binding key) stay put; a clash with a patch row is reported explicitly.client-connection module rewrite, a pre-shim is injected right after <head> setting __DSH_TRANSPORT__.ownsHost to true (before any client module is evaluated); the rewrite matcher also widened to any dsh-client-connection path.No runtime data lives in the plugin repo — everything is under
$DSH_HOME/dsh-web-pass/(password hashes, sessions, logs).
The plugin runs a reverse proxy inside the dsh web process: it rewrites Host/Origin to the loopback address so DSH's browser trust checks pass without any DSH configuration changes, and layers cookie password auth on top. TLS is usually terminated upstream (reverse proxy / tunnel / nginx).
accept-encoding so the upstream replies uncompressed (nginx enables gzip by default, which would skip all injections — polyfill / module rewrite / logout chip all depend on plaintext); compression is done by the reverse proxy in front of the gate.dsh: true backends: DSH auth carrying + targeted client-connection module rewrite (see the two sections below).dsh: false backends (fnOS, openclaw, etc.): pass through as-is, without DSH cookies or forced security headers.Option 1: from npm (recommended)
dsh plugin --profile web add dsh-web-pass
# then restart dsh web
Option 2: from source
git clone https://github.com/linz919/dsh-web-pass.git
dsh plugin --profile web add ./dsh-web-pass -w
# then restart dsh web
Confirm it is loaded:
ss -tln | grep -E ':3081|:3082'
Every option has a built-in default — the plugin works with no configuration at all (log rotation defaults to 1MB × 7 files). Even if cordis.patch.yml gets overwritten or loses custom settings while installing other plugins, nothing breaks: log rotation falls back to the built-in defaults, and the password env var name falls back to the built-in DSH_WEB_PASS_PASSWORD.
From the plugin config (the config section in cordis.patch.yml, optional):
| Option | Default | Description |
|---|---|---|
port | 3081 | Reverse proxy listen port |
logViewerPort | 3082 | Embedded log viewer port |
maxLoginAttempts | 3 | Wrong-password attempts allowed before lockout |
loginLockMs | 60000 | True-lockout base duration (ms, counted from the last failure; repeated lockouts escalate 2× up to 16×) |
passwordEnv | DSH_WEB_PASS_PASSWORD | Name of the env var providing the password |
trustProxy | false | Trust X-Forwarded-For / CF-Connecting-IP headers (for visitor IP identification and login rate limiting) |
clientHostTrust | true | Enable Host settings document on non-loopback pages (IP/domain access); false restores DSH's native behavior (settings only visible on localhost) |
logMaxBytes | 1048576 | Access log per-file size cap (bytes); rotates when reached, minimum 64KB |
logMaxFiles | 7 | Number of rotated history files to keep (access.log.1 … access.log.N); older ones auto-deleted |
upstreams | [] | Multi-password multi-upstream table (see below): each row has label / passwordEnv / host / port / clientHostTrust / dsh / enabled; omit for single-password behavior |
About
trustProxy: when off (default), visitor IPs and login rate limiting rely only on the socket address — forged forwarding headers are never adopted. When on,X-Forwarded-For/CF-Connecting-IPbecome the rate-limit key, so enable it only behind a trusted reverse proxy that overwrites those headers (nginx withproxy_set_header X-Forwarded-For $remote_addr;, Cloudflare tunnel, etc.); a merely pass-through proxy would hand attackers an unlimited set of rate-limit keys.
DSH_WEB_PASS_PASSWORD, set in the dsh web service environment) > file $DSH_HOME/dsh-web-pass/password (admin entry) or password.<i> (entry i, scrypt hash).cordis.patch.yml only references the env var name — never write plaintext passwords into it (the file goes into git / the repo).sessions.jsonl in the same directory too is cleaner) to return to the forced setup page; for env-var passwords, unset and restart.upstreams in cordis.patch.yml and restart dsh web. Rows added from the page persist in $DSH_HOME/dsh-web-pass/upstreams.json (0600, survives restart), coexist with patch rows and are appended after them; password values can be changed live on the settings "Web password" tab without a restart.patch row is reported explicitly.clientHostTrust: false (default) so guest settings pages stay blank and the model cannot be changed. The gate only forwards — it does not run backends; backends run themselves and bind 127.0.0.1 only.dsh: true each hold their own DSH cookie and get the module rewrite; for reverse-proxying non-DSH services like openclaw or fnOS keep dsh: false (default) for pass-through, avoiding leaking DSH cookies to them./dsh-logs/ entry) exists only in the main DSH that runs this plugin; guest clean DSH instances don't have it, so guests cannot touch it.-), filterable by entry.host:port at issue time; both must match on every check — reordering patch rows or changing ports and restarting invalidates old sessions automatically (just log in again; no manual sessions.jsonl cleanup needed). Order remains "admin → patch rows → page-added rows", append-only with stable soft-deleted indexes.DSH Web ships with a browser-auth layer (process launch-token exchanged for a 30-day cookie). This plugin runs inside the dsh web process and can obtain a process-token URL via ctx.connection.authenticatedUrl(), internally exchange it for DSH's persistent cookie, and inject it into every forwarded request and WebSocket handshake — visitors behind the password gate never see DSH's "authentication required".
dshAuthHolding field (whether the DSH cookie is being carried).dsh web provides ctx.connection; falls back to the old behavior otherwise (visitors still need a DSH token).DSH's client only mounts the host settings document when the page URL is localhost/127.x; via LAN IP or domain (including through this gate) settings degrade to memory mode — the "Plugin configuration", "Models" and "General" tabs go completely blank (pure-RPC features like the plugin list and chat are unaffected).
The plugin forces that check to true with two measures: ① one targeted rewrite of DSH's client-connection module at the proxy layer; ② a pre-shim injected right after <head> setting __DSH_TRANSPORT__.ownsHost to true, before any client module is evaluated. The settings page works fully on 3081 (LAN IP / public domain); the browser URL is unchanged, the password gate remains the only entrance, and the server-side /api trust checks are unaffected.
clientHostTrust: false to restore DSH's native behavior.cache-control: no-cache. After upgrading, hard-refresh once per device (Ctrl+F5) — the old module was cached immutable for a year and a normal refresh won't re-fetch; after one hard refresh it stays current automatically./dsh-logs/ (also reachable via the settings page "Access log" button): only requests to /gate-login, /gate/setup, /gate/logout are recorded, with semantics derived from "method + path + status":
$DSH_HOME/dsh-web-pass/access.log — size-based rotation: when a file reaches logMaxBytes (default 1MB) it rolls to access.log.1, older ones shift to access.log.2 …, keeping at most logMaxFiles (default 7) with the oldest auto-deleted. Total log size is therefore hard-capped (≈8MB by default) and never grows unbounded..hmac-key in the same directory (auto-generated, 0600); do not delete it, otherwise the same IP gets new pseudonyms and aggregate analysis breaks.X-Frame-Options: DENY and other security headers; proxied responses get no extra security headers (since v0.3.4), and whatever the upstream itself sends passes through as-is.trustProxy they switch to X-Forwarded-For / CF-Connecting-IP as the rate-limit key — enable only behind a trusted proxy that overwrites those headers, otherwise it hands attackers an unlimited set of rate-limit keys.