Skip to content

hisa110/moshweb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

moshweb

A full mosh client that runs entirely in your browser.

Pure JavaScript. No server-side terminal, no agents, nothing to install on the remote host. Falls back to a built-in SSH-2 client when mosh isn't available.

English · 日本語

moshweb terminal session over mosh

Why

This project was born on the Shinkansen between Tokyo and Kyoto, watching an SSH session die every few minutes as the train hopped between cell towers and tunnels at 285 km/h.

SSH from a phone or laptop is miserable: switch from Wi-Fi to LTE, close the lid, walk out of range — your session is gone. mosh fixed this in 2012 with a UDP state-sync protocol that survives roaming and sleep, but it has always required a native client — and on an iPhone, "a native client" means whatever the App Store happens to offer.

moshweb implements the entire mosh protocol in browser JavaScript — the OCB3-AES128 AEAD cipher, the State Synchronization Protocol datagram layer, the protobuf-based screen/keystroke sync, all of it. Open a tab, connect, put your phone in your pocket, and your session is still there an hour later.

Features

  • 🖥️ Real mosh, not a lookalike — interoperates with stock mosh-server. SSP datagrams, OCB3-AES128 (RFC 7253, verified against the RFC test vectors), hand-rolled protobuf, zlib, ACK/retransmit/heartbeat.
  • 🔐 Built-in SSH-2 client for bootstrapping and fallback — curve25519-sha256 key exchange, aes128-ctr + hmac-sha2-256, ed25519 public-key / password / keyboard-interactive auth, host-key pinning (TOFU). All crypto is pure JS (noble), so it even works on plain http:// origins where WebCrypto is unavailable.
  • 🔄 Automatic fallback chain: mosh → ssh → ssh+tmux. If mosh-server is missing or UDP is blocked, you still get a shell, and tmux keeps it alive across reconnects.
  • 📱 Made for phones:
    • Touch scrolling that does the right thing — wheel events to tmux/vim when the app owns the mouse, arrow keys on alternate screens, local scrollback otherwise
    • A key bar (esc / tab / ctrl / arrows) above the soft keyboard, with sticky-Ctrl
    • Layout that tracks the iOS visual viewport, so the input line is never hidden behind the keyboard
  • 🇯🇵 First-class CJK & IME support — Unicode 11 width tables (wide chars and emoji measured correctly), native browser IME composition, deduplication for iOS Safari's double-fired IME commits, CJK font stack, mosh-server launched with a UTF-8 locale.
  • 🔌 Survives almost anything (see resilience) — WebSocket drops, full bridge restarts, network roaming, laptop sleep.
  • 🧪 Tested against the real thing — Node interop tests against genuine mosh-server/sshd, plus browser E2E tests that restart the bridge mid-session and verify Japanese round-trips.

Screenshots

Connect On a phone

How it works

architecture

Browsers can't open raw UDP or TCP sockets, so a ~250-line Node.js process relays bytes between a WebSocket and the network. That's the only thing that runs outside the browser. It never sees keys or plaintext — mosh datagrams are end-to-end encrypted from the JS client to mosh-server, and the SSH transport is likewise encrypted in the browser.

The connection flow:

  1. The built-in SSH client connects and runs mosh-server new
  2. The reply MOSH CONNECT <port> <key> seeds the mosh engine, which takes over on UDP; the bootstrap SSH connection closes
  3. If anything in step 1–2 fails (no mosh-server, UDP filtered), you transparently land in an SSH PTY shell — optionally inside tmux new -A so the session itself becomes durable

Quick start

git clone https://github.com/hisa110/moshweb && cd moshweb
npm install
npm run build      # bundle the browser app  -> public/app.js
npm start          # start the bridge        -> open http://127.0.0.1:8022/

Fill in host / user / auth (password or an OpenSSH ed25519 private key) and connect. Done.

📖 The recommended deployment — bridge on a home server, phone connecting over Tailscale — has its own step-by-step guide: docs/setup.md. It covers the slightly unusual topology (the "host" field is relative to the bridge!), systemd, tmux mouse mode, and troubleshooting.

Use it from your phone (Tailscale & friends)

The bridge binds loopback only by default. To reach it over a VPN, list the addresses explicitly:

MOSHWEB_HOST=127.0.0.1,$(tailscale ip -4) node bridge.js
# then open http://<machine>.your-tailnet.ts.net:8022/ on your phone

Because the crypto is pure JS, everything works on plain http:// — no certificate setup required. If you want HTTPS anyway, tailscale serve --bg 8022 gives you a trusted cert in one command.

⚠️ Never bind 0.0.0.0. The bridge is a TCP/UDP relay; exposing it to an untrusted network lets anyone on it relay traffic to arbitrary hosts. Bind loopback + VPN addresses only.

Direct mosh connection

Already have a running mosh-server? Open 詳細設定 / Advanced, paste <port> <key> from its MOSH CONNECT line, and moshweb connects over UDP without touching SSH.

Resilience

The browser ⇔ bridge WebSocket is treated as unreliable by design:

Failure What happens
WebSocket drops (mosh) Auto-reconnect. SSP is stateless over the wire — nothing is lost
WebSocket drops (ssh) Both directions are buffered with byte offsets + ACKs and replayed on reattach — not a single byte lost
Bridge process restarts mosh: a fresh UDP pipe is created, same session continues. ssh: auto re-dial with kept credentials (tmux restores your screen)
Wi-Fi → LTE, sleep, dead zones Classic mosh behavior: the screen syncs the instant connectivity returns

Japanese / CJK support

  • Unicode 11 width tables via @xterm/addon-unicode11 — full-width characters and emoji occupy the correct number of cells
  • IME composition uses the browser's native composition events; the in-progress text renders at the cursor
  • iOS Safari fires some IME commits twice through different event paths — moshweb deduplicates identical non-ASCII chunks arriving within 50 ms
  • Keystrokes travel as UTF-8 inside SSP UserStream messages, exactly like native mosh
  • mosh-server is started with LANG=ja_JP.UTF-8 (falling back to C.UTF-8)

Security model

  • Bridge: binds 127.0.0.1 (plus explicitly listed VPN addresses), rejects cross-origin WebSockets, relays ciphertext only
  • SSH host keys: trust-on-first-use, pinned in localStorage, connection refused if the key changes
  • Credentials: used in-memory for the session; only host/user/options are persisted, never passwords or keys
  • mosh key: the AES key from MOSH CONNECT lives only in the page's memory

Project layout

Path What it is
src/mosh/ocb.js OCB3 AES-128 AEAD (RFC 7253) — passes the RFC test vectors
src/mosh/ssp.js mosh datagram layer: nonces, direction bit, timestamps, fragmentation, zlib
src/mosh/sync.js state synchronization: UserStream diffs, ACKs, retransmit, heartbeat, shutdown
src/mosh/pb.js minimal hand-written protobuf for mosh's Transport/Client/HostBuffers messages
src/ssh/ complete SSH-2 client: transport, kex, auth, channels, PTY
src/term.js xterm.js setup: CJK fonts, Unicode 11, touch scrolling, viewport tracking
src/relay.js reconnecting WebSocket session with byte-offset replay
bridge.js the local WS ↔ UDP/TCP relay

Tests

node test/ssp-node.mjs <port> <key>   # mosh protocol interop against a real mosh-server
node test/ssh-node.mjs <keyfile>      # SSH interop against a local sshd
node test/e2e-mosh.mjs <port> <key>   # browser E2E, direct mosh
node test/e2e-full.mjs                # full E2E: ssh→mosh, bridge restart, fallback, 日本語

Limitations & roadmap

  • Predictive local echo (mosh's speculative typing) — not yet implemented
  • Encrypted (passphrase-protected) private keys
  • RSA / ECDSA client keys (ed25519 only today; host keys support all three)
  • Connection profiles for one-tap access to multiple servers
  • PWA packaging for a home-screen app feel

PRs welcome — see CONTRIBUTING.md.

Acknowledgments

  • mosh by Keith Winstein and contributors — the protocol this project reimplements for the browser
  • xterm.js — the terminal emulator
  • noble cryptography — auditable pure-JS crypto primitives

License

MIT


If moshweb keeps your session alive on a train, consider leaving a ⭐

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages