# SSH Holesail Browser includes a full in-browser SSH terminal. It uses a Holesail tunnel to reach the remote host, spawns a real PTY via `tt-native`, and bridges it to an xterm.js terminal in the dashboard over WebSocket. ## Architecture ``` Dashboard (xterm.js) │ WebSocket ws://127.0.0.1:21000+ ▼ ssh-manager.js (WebSocket server) │ PTY I/O (tt-native / forkpty) ▼ ssh binary (system ssh) │ TCP ▼ 127.0.0.1:20000+ (Holesail client tunnel) │ P2P / Noise protocol ▼ Remote SSH daemon ``` The `ssh` binary is the system's own SSH client — all its features (key auth, agent forwarding, etc.) work as normal. The Holesail tunnel provides the transport; `ssh` sees it as a plain TCP connection to `127.0.0.1`. --- ## Authentication SSH authentication is handled entirely by the system `ssh` binary. The following methods are supported, tried in order: ### 1. Public key (automatic, no prompt) If you have a key in `~/.ssh/` (e.g. `id_rsa`, `id_ed25519`) or loaded in `ssh-agent`, it is tried first. If the remote server accepts it, you are logged in with no prompt at all. ### 2. Password — saved on the connection If you enter a password when saving the connection, it is stored as base64 (`passwordB64`) in `state.json` and survives native host restarts. When connecting, the password is delivered to SSH automatically via `SSH_ASKPASS` and a named FIFO — no prompt appears and the password is never passed on the command line or in the environment. ### 3. Password — typed interactively If no public key is available and no password is saved, the terminal displays a password prompt (`username@127.0.0.1's password:`). Type your password and press Enter. The input is masked (no echo). > **Note:** The native messaging host launched by Chrome has no controlling terminal (`/dev/tty`), so SSH cannot read passwords directly. Holesail Browser handles this transparently using `SSH_ASKPASS` and a named FIFO pipe — the password prompt appears in xterm.js and the typed password is securely forwarded to SSH without exposing it in the process environment or on disk. ### Authentication order summary | Scenario | What happens | |----------|-------------| | Public key in `~/.ssh/` or `ssh-agent` | Logged in silently, no prompt | | Password saved on connection | Password delivered automatically, no prompt | | No key, no saved password | Password prompt appears in xterm.js | --- ## Usage ### Saving a connection 1. Open the dashboard → **SSH** 2. Click **Add Connection** 3. Enter a label, the `hs://` key for the remote peer, and the username 4. Optionally enter a password (leave blank to type it each time, or to use a public key) 5. Click **Save** — the connection definition is stored in `state.json`; the password is kept in memory only and is not persisted ### Connecting 1. Click **Connect** next to a saved connection 2. An xterm.js terminal opens in the dashboard and is automatically focused — you can start typing immediately 3. The native host starts a Holesail tunnel to the remote peer, then spawns `ssh` 4. If authentication requires a password and none is saved, a prompt appears in the terminal ### Terminal resize The terminal resizes automatically when you resize the dashboard window. Resize events are sent to the native host, which calls `ioctl(TIOCSWINSZ)` on the PTY and sends an SSH window-change request to the remote sshd. ### Disconnecting Click **Disconnect** or close the terminal tab. The PTY and Holesail tunnel are cleaned up. --- ## Native host commands ### `startSshSession` Start a new SSH session. **Request payload:** ```json { "connectionId": "ssh-abc123", "hsUrl": "hs://abc123...", "username": "root", "passwordB64": "cGFzc3dvcmQ=", "cols": 80, "rows": 24, "label": "My Server" } ``` `passwordB64` is optional. When present it is a base64-encoded UTF-8 password (`btoa(unescape(encodeURIComponent(password)))`). If omitted or empty, SSH will use public key auth or prompt interactively in the terminal. **Response payload:** ```json { "ok": true, "sessionId": "ssh-session-1", "wsPort": 21000, "tunnelPort": 20000 } ``` ### `stopSshSession` Stop a running SSH session. **Request payload:** ```json { "sessionId": "ssh-session-1" } ``` **Response payload:** ```json { "ok": true } ``` ### `resizeSshSession` Resize the terminal. **Request payload:** ```json { "sessionId": "ssh-session-1", "cols": 120, "rows": 40 } ``` **Response payload:** ```json { "ok": true } ``` ### `getSshSessions` List active SSH sessions. **Response payload:** ```json { "ok": true, "sessions": [ { "sessionId": "ssh-session-1", "connectionId": "ssh-abc123", "wsPort": 21000 } ] } ``` ### `setSshConnections` Save SSH connection definitions to `state.json`. **Request payload:** ```json { "connections": [ { "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://abc...", "username": "root", "passwordB64": "cGFzc3dvcmQ=" } ] } ``` `passwordB64` is optional. When present it is a base64-encoded UTF-8 password string (`btoa(unescape(encodeURIComponent(password)))`). It is persisted in `state.json` and delivered to SSH via `SSH_ASKPASS` + a named FIFO on each connection — never via the command line or environment. --- ## WebSocket protocol The dashboard connects to `ws://127.0.0.1:` after receiving the `startSshSession` response. SSH is not spawned until the WebSocket client connects and sends a ready-signal (`\x00`). This ensures the terminal is live before SSH starts, so any prompts (password, host key) are visible immediately. **Browser → native host:** - First byte `\x00`: ready-signal (triggers SSH spawn) - Binary frames: raw terminal input (keystrokes, paste) - Text frames: JSON control messages ```json { "type": "resize", "cols": 120, "rows": 40 } ``` **Native host → browser:** - Binary frames: raw PTY output (terminal data) --- ## Port allocation | Range | Usage | |-------|-------| | 20000+ | Holesail client tunnel (one per session) | | 21000+ | WebSocket server (one per session) | Ports are allocated sequentially and released when the session ends. --- ## Saved connections SSH connections are stored in `state.json` under `sshConnections`. If a password was saved, it is stored as `passwordB64` (base64-encoded) and survives native host restarts. ```json { "sshConnections": [ { "id": "ssh-abc123", "label": "My Server", "hsUrl": "hs://abc123...", "username": "root", "passwordB64": "cGFzc3dvcmQ=" } ] } ``` `passwordB64` is omitted when no password was saved for the connection. --- ## Requirements - The `ssh` binary must be installed on the machine running the native host - macOS: included with the OS - Linux: install `openssh-client` - Windows: install OpenSSH via Windows Optional Features or Git for Windows - The remote host must be running an SSH daemon accessible via a Holesail `hs://` key --- ## Troubleshooting **Connection times out** The remote peer may be offline or the `hs://` key may be wrong. Check `~/.holesail-browser/holesail-browser.log` for tunnel errors. **`ssh: command not found`** The `ssh` binary is not in the PATH seen by the native host. Install OpenSSH or ensure `ssh` is in `/usr/bin/ssh` (macOS/Linux) or available in `PATH` (Windows). **Permission denied (publickey,password)** - If you have a public key but it is not accepted by the server, SSH falls back to password auth and shows a prompt in the terminal - If you enter the wrong password, SSH exits with "Permission denied" — reconnect and try again - If no authentication method works, check that the username is correct and that the server allows password auth **Password prompt does not appear** This should not happen with the current implementation. If it does, check `~/.holesail-browser/holesail-browser.log` for `WARNING: askpass setup failed` — this would indicate that `mkfifo` is not available on the system. **Terminal display is garbled** Try resizing the dashboard window to trigger a resize event, or disconnect and reconnect.