263 lines
8.0 KiB
Markdown
263 lines
8.0 KiB
Markdown
# 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 (`[email protected]'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:<wsPort>` 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.
|