Releases: wushilin/webshell
Release list
v0.4.0
Server-driven redraw of the visible screen
An app's rarely-rewritten bottom row (Claude Code's status line, say) could still go blank while the tab was in front, and only a resize repaired it. A client-side repaint cannot fix that: the browser's own terminal buffer may already be the thing that is wrong, so redrawing it just redraws the error. The repair now comes from the server.
- The server keeps its own rendering of each slot's visible screen, fed every byte the browsers get, under the same lock the output is broadcast under, so it tracks the stream exactly. No scrollback is kept there — the ring is the history; this is only the grid.
- On a fixed period (
[terminals] redraw_interval_secs, default 30,0disables) every attached browser gets aredrawcontrol frame carrying that screen: every visible row with its attributes and the cursor, as escape sequences the page writes straight into its terminal. It repaints from what the server knows the screen holds, not from its own memory of it. - Lightweight and scoped to the visible screen. The sequence clears and rewrites the display in place and never switches buffers or modes; erasing the display never pushes rows into history, and a full bottom row is left in pending-wrap rather than scrolled. The scrollback and the scroll position are untouched — verified with the view scrolled into history and the page's buffer deliberately corrupted: one period later the screen was byte-identical to the original, viewport and history unchanged.
- Ordered with the stream. The frame is queued by the same task that forwards a slot's output, after everything it already reflects, and rides as a control frame rather than tagged output, so it never moves the client's stream offset. Browsers refuse a frame whose size disagrees with theirs (a resize in flight), one arriving mid-replay, or one from a replaced attachment.
- Resize. The server model is rebuilt from the ring at the next tick after a resize — the same replay a reconnecting browser performs — rather than truncated in place.
Read-only share viewers get the same frame on their own socket.
Compatibility
The wire protocol gains one server→client control frame, {"type":"redraw","term":N,"cols":C,"rows":R,"screen":"<base64>"} (no term on a viewer socket). An older browser page that ignores unknown control frames keeps working. New config key: redraw_interval_secs under [terminals].
Static binaries
Statically linked musl binaries for x86_64 and aarch64, as before.
VER=0.4.0
ARCH=x86_64 # or aarch64
BASE=https://github.com/wushilin/webshell/releases/download/v$VER
curl -fsSLO $BASE/webshell-$VER-linux-$ARCH-musl
curl -fsSLO $BASE/SHA256SUMS
sha256sum -c --ignore-missing SHA256SUMS
chmod +x webshell-$VER-linux-$ARCH-muslv0.3.0
Remember this device (opt-in)
A browser that has already passed a TOTP challenge can be trusted for a bounded window, so it is asked for a password but not a code. Off by default — set remember_device = true under [mfa].
The bypass is the second factor and nothing else. The local password or the Google sign-in is verified on every login regardless, and enrollment is never skipped. A trust record is not a credential that logs anyone in; it is evidence that this browser has already proved possession of the authenticator for one identity.
- Trust is bound to the identity and to the enrolment's
enrolled_at, so re-enrolling a TOTP secret strands every older device with no separate revocation step. That is also the recovery path: drop someone'smfa_secretfromenrollment.tomland their devices stop working. - The window is absolute from the moment the box was ticked and is never extended by use — a sliding window would let a stolen cookie refresh its own lifetime indefinitely.
- Not bound to IP or User-Agent. Phones roam and browsers auto-update, so comparing either would un-trust honest users at random. Both are recorded for display only.
- Turning
remember_deviceback off revokes trust immediately rather than merely stopping new grants. - Manage your own devices from the devices button in the terminal toolbar: which browser you are on, when each was last used, revoke one or all. Self-service and scoped to you, exactly like share links.
New [mfa] options: remember_device, remember_device_days (default 30, clamped 1–90), device_path, max_devices_per_identity.
Fix: a lost-update race in all three state stores
The device, enrolment and session stores all mutated under a lock, cloned a snapshot, released the lock, and then wrote. That is not equivalent to writing under the lock: two writers interleave, and the slower one lands a snapshot taken before the faster one's change — reverting it on disk while memory still looks correct. It only surfaced after a restart, as a revoked device or a logged-out session coming back.
All three now hold the lock across the write, which is what their doc comments already claimed. Covered by concurrency tests that fail against the old shape. The rename is now durable too — the parent directory is fsynced, so a crash cannot lose the swap even though the bytes it points at were safely on disk.
Static binaries for both architectures
Prebuilt statically linked musl binaries for x86_64 and aarch64, verified to carry no PT_INTERP and to run on both musl and glibc userlands. Nothing here touches the host auth stack any more — no PAM, no dlopen, no setuid helper — so one file per architecture runs on any Linux.
VER=0.3.0
ARCH=x86_64 # or aarch64
BASE=https://github.com/wushilin/webshell/releases/download/v$VER
curl -fsSLO $BASE/webshell-$VER-linux-$ARCH-musl
curl -fsSLO $BASE/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing
sudo install -m 0755 webshell-$VER-linux-$ARCH-musl /usr/local/bin/webshellThe one thing a static build gives up is NSS: getpwuid reads /etc/passwd directly rather than LDAP/SSSD. Set [terminals] login_cmd if the account webshell runs as is not local.
Also
- Dropped stale "PAM-authenticated" claims from the crate description (visible in
--help) and from several doc comments; PAM was replaced by self-managed argon2id hashes some releases ago. - README: an install section for the prebuilt binaries, the new
[mfa]options, and how to recover if Google-only sign-in locks you out.
Upgrading is a drop-in binary swap. No config change is required — the new options default to off, and existing config files keep parsing unchanged.
v0.2.0
Share links: expiry and revocation are enforced by the server, with no help from the viewer
Read-only share links were already force-closed by a timer at expiry, but the enforcement had gaps that let a viewer socket outlive its token:
- the force-close deadline came from a second lookup after the lease check, so a clock tick between the two left the socket with no deadline at all
- the streaming loop was unbiased, so a busy output stream could keep winning the race against a deadline that had already fired
- the deadline ran on the monotonic clock while expiry is wall-clock time, so a clock step could stretch a link
- the minute sweep only dropped map entries; live viewers on an expired grant were never told
What changed
The write half of every viewer socket now lives inside a lease guard that is the only way to emit a frame. Every send re-checks the lease — wall-clock expiry and revocation, one predicate — and refuses once it is invalid. The termination paths consume the guard by value, so streaming after that is a compile error, not a runtime check. Validity is one-way: a lease that has expired or been revoked can never become valid again, and every way of invalidating a link fails the viewer identically (Close frame, then reconnects refused at the upgrade).
Layered on top so that no single mechanism is load-bearing:
- the lease carries its own expiry, and the deadline is derived from it with sub-second precision
- the select loop is biased toward the expiry and revocation arms
- every viewer socket audits its own lease on a fixed 15 s tick
- the scheduled sweep (every minute) explicitly signals viewers of expired grants before pruning them
- resolve / list / create prune through the same signalling path
Nothing here depends on the viewer page polling status or disconnecting on its own; a client that disables that polling is closed by the server all the same.
Tests
New integration tests cover a viewer that never polls status being closed by the server within the token's lifetime, and a viewer being disconnected the moment its link is revoked, with reconnects refused in both cases. Unit tests cover the one-way validity of a lease across expiry, revocation and sweep.
Compatibility
No configuration changes. Token format is unchanged; links minted by 0.1.x keep working until they expire. Because the enforcement moved into the server, this release is a drop-in upgrade for anyone relying on share-link expiry.
Assets
Fully static musl builds, no shared-library dependencies:
| File | Platform |
|---|---|
webshell-0.2.0-linux-x86_64-musl |
Linux x86_64 |
webshell-0.2.0-linux-aarch64-musl |
Linux aarch64 |
SHA-256 checksums are in SHA256SUMS.
Full Changelog: v0.1.6...v0.2.0
v0.1.6
v0.1.5
v0.1.4
Terminal UX improvements:
- Mouse reporting from fullscreen apps (vim, Claude Code, htop) is no longer honored — drag always selects text, no Shift needed.
- Select-to-copy: any finished selection is automatically copied to the clipboard.
- Shift+Enter now sends ESC CR (insert-newline in Claude Code and other TUIs); the mobile key-bar's armed shift does the same for return.
Static musl binary attached — no runtime dependencies.
Full Changelog: v0.1.3...v0.1.4
v0.1.3
Highlights
- Adds optional TOML-backed login session persistence with generated-and-persisted cookie secrets in config-backed mode.
- Keeps only meaningful session state on disk: authenticated sessions, MFA-pending sessions, and in-progress Google OAuth flows.
- Adds session cleanup for expired entries while keeping anonymous login CSRF state memory-only and bounded.
- Redirects MFA-pending users back to the MFA screen, with a Cancel action to clear pending login state.
- Improves mobile terminal controls: collapsible translucent keybar, drawer-style callout keys, Alt/Shift/Ctrl support, F-key/navigation callout, backtick entry, and press feedback.
- Resizes the terminal around expanded keybar and iPhone soft keyboard show/hide instead of hiding terminal content behind visual scrolling.
- Reorders README to put Quick Start first and documents session persistence behavior and configuration.
Validation
- cargo test: 54 passed
- cargo fmt --check
- terminal embedded script syntax check
- deployed locally via processmaster and verified /webshell/login returns 200
v0.1.2
Adds configurable shell command and environment: [terminals] login_cmd (argv array, default = your passwd login shell + -l) and [terminals.envs] (seeded into every shell, overrides built-ins). Unset = previous behavior. Static musl binary attached — no runtime dependencies.
Full Changelog: v0.1.1...v0.1.2
webshell 0.1.1
A browser-based login shell with persistent, resumable terminal slots.
New in 0.1.1
Zero-config simple mode. Run the full server with no config file, no MFA
and no Google — a single local user straight from the environment:
WEBSHELL_USER=alice WEBSHELL_PASSWORD=hunter2 webshell simple
# Web Shell listening on http://127.0.0.1:9023/webshell/WEBSHELL_USER/WEBSHELL_PASSWORD— the single login (required).WEBSHELL_BIND— listen address (default127.0.0.1:9023); set0.0.0.0:PORTto expose it.
Nothing is written to disk. Meant for quick, local, trusted sharing — the
password lives in the process environment and there is no second factor. For
anything internet-facing, use the full TOML config with MFA behind TLS.
The config-file (webshell run) path is unchanged.
Binary
webshell-0.1.1-x86_64-linux-musl is a statically linked (static-pie) x86_64
Linux binary with no shared-library dependencies. Verify with the accompanying
.sha256.
webshell 0.1.0
webshell 0.1.0
A browser-based login shell with persistent, resumable terminal slots.
Sign in with Google (OpenID Connect) or a webshell-managed password, with
an optional TOTP second factor enrolled per identity. Each identity gets
its own slots, scrollback and read-only share links.
Links to no PAM library, execs no setuid helper, and depends on nothing in
the host's auth stack — so it ships as a single static binary.