When the session ends, the terminal tells the page that embeds it, which is how
the panel around it closes by itself after `exit`. That notification, and the
one for `Escape`, were sent only to `clickhouse.com` and `clickhouse.cloud`
parents, so on the documentation preview deployments the panel stayed open
around a dead terminal.
Both notifications carry no data — an embedder only learns that its own iframe
is done — so send them to the parent whatever its origin is. Credentials keep
travelling only between the terminal and a trusted parent.
An embedded `/webterminal` page is loaded without credentials in its URL and
receives them from the host page (`play.html`, or the ClickHouse Cloud console)
over `postMessage`. It nevertheless opened a connection of its own at startup,
before that message could arrive: an attempt with an empty password that could
only authenticate as a passwordless user and was torn down and replaced by the
credentialed connection a moment later. The user saw `Connecting...` twice.
Wait for the credentials instead of connecting. A host page that never sends
them - it does not implement the protocol, or its origin is not trusted - would
leave a terminal that neither connects nor prompts, so the page now says that it
is waiting, and Enter hands the login over to the interactive prompt.
The wait became a state of the page's own state machine, which makes the
`waitingForCredentials` and `initialAutoAuth` flags and the `ws.onclose` branch
they served unnecessary.
The pages opened as a normal tab, or with a `user` URL parameter, keep
connecting immediately.
Add `tests/integration/test_webterminal_startup`, which runs the real script of
the served page in a Node `vm` with a stubbed terminal and `WebSocket` and
asserts the startup and authentication flows, following the existing
`test_play_reconcile_startup`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewer pointed out that validating `e.origin` alone allows any window
from a trusted origin (e.g. one obtained via `window.open` by the parent)
to drive `webterminal-credentials` and `webterminal-refit`. Tighten the
check so only messages from the embedding parent are accepted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "user" input on `play.html` and on the other web pages
(`dashboard.html`, `jemalloc.html`, `merges.html`, `binary.html`,
`webterminal.html`) previously came pre-filled with the literal text
`default`. Replace that with an empty value and a `user` placeholder
hint. When the value is empty, the page does not send a `user=` query
parameter (or, in the WebTerminal protocol, omits the `user` field
from the auth JSON), letting the server fall back to its configured
default user — which is not necessarily named `default`.
For the WebTerminal, `parseAuthMessage` now treats the `user` field as
optional: when omitted or empty, the caller-provided default is
preserved. The call site seeds it with `default`, mirroring the HTTP
fallback in `authenticateUserByHTTP`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The web terminal page exchanges postMessage with its embedding parent for
three things: receiving credentials, receiving refit requests, and
notifying the parent on Escape / WebSocket close. Both directions
previously required the parent to be same-origin: the inbound check
rejected anything where e.origin !== window.location.origin, and the
outbound calls used window.location.origin as targetOrigin, which the
browser silently drops when the parent is on a different origin.
That makes the terminal effectively non-embeddable from clickhouse.com
or any other site outside play.clickhouse.com. Loosen the policy:
- Inbound: accept messages from same-origin and from any https origin
whose hostname is (or is a subdomain of) clickhouse.com /
clickhouse.cloud.
- Outbound: remember the parent origin from the first valid message
(including an optional 'webterminal-hello' handshake) and post back
to that exact origin, so the browser delivers it.
Credentials and other messages from untrusted origins are still
rejected. Apex domains are matched explicitly; URL parsing anchors on
hostname so suffixes like evil.clickhouse.com.attacker.tld are not
matched.
`play.html` posts `webterminal-credentials` exactly once on iframe
`load`. With auto-auth running on startup, that message could arrive
while `waitingForCredentials` was still `false` and be silently
dropped; the subsequent `ws.onclose` flipped the flag to `true`, but
no second message ever came, leaving the embedded terminal stuck.
Pre-arm `waitingForCredentials` before the initial auto-auth attempt
when embedded without a URL-supplied user, so a one-shot parent
message is consumed regardless of timing (the in-flight connection
is replaced). Also clear `initialAutoAuth` when parent credentials
arrive, so a subsequent failure prompts interactively instead of
waiting for another (non-existent) parent message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the web terminal always opened with a 'Username:' / 'Password:'
prompt (or, when embedded in an iframe without a URL-supplied user, waited
silently for credentials via postMessage). For embedding scenarios where
the default user has an empty password, this is unnecessary friction.
Now the web terminal performs an initial connection attempt using the
URL-supplied user (or 'default') with an empty password. If the server
rejects it, the existing fallback runs: interactive prompt, or postMessage
when embedded without a URL user.
Remove support for passing the password via URL query string and always
prompt for it interactively (or accept it via `postMessage` when embedded
in an iframe). URLs end up in browser history, referrer headers, server
access logs, and reverse-proxy logs, so using a URL parameter for the
password defeats the goal of in-band WebSocket authentication.
Username can still be supplied via URL for convenience.
- Add 5-second `setReceiveTimeout` before reading the auth WebSocket
frame to prevent indefinite blocking by unauthenticated clients.
Cleared before entering the main I/O loop where poll() is used.
- Replace bare `continue` on `EAGAIN`/`EWOULDBLOCK` in PTY write loop
with a `poll` wait (up to 100ms) to avoid busy-spinning the CPU
when the PTY buffer is full.
- Handle WebSocket close code 1008 explicitly in the client-side
`onclose` handler so that authentication failures after `onopen`
re-prompt credentials instead of redirecting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three security/architecture issues from the bot review:
1. Guard `/webterminal` behind `allow_experimental_webterminal` server
setting (default `false`). The handler is no longer registered unless
explicitly enabled. Remove the link from `index.html` since users who
enable the experimental setting will know about the endpoint.
2. Move credentials out of the WebSocket URL query string to prevent
leaking passwords via server/proxy logs and browser history.
Authentication now happens in-band: the client sends a JSON auth
message (`{"type":"auth","user":"...","password":"..."}`) as the
first WebSocket text frame after the handshake completes.
3. Add Origin header validation on WebSocket upgrade to prevent
cross-site WebSocket hijacking (CSWSH). The Origin host must match
the request Host header.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use `ptsname_r` on Linux for thread safety in `openpty`
- Validate `Sec-WebSocket-Key` as base64-encoded 16-byte nonce
- Require GET method for WebSocket upgrade and GET/HEAD for HTML page
- Reject unmasked client frames per RFC 6455
- Validate RSV bits are zero (no extensions negotiated)
- Enforce control frame invariants (FIN required, max 125 bytes)
- Reject protocol violations during WebSocket fragmentation
- Bound total reassembled message size (close 1009 on overflow)
- Handle partial socket writes via `sendAllBytes` loop
- Handle EINTR and EAGAIN in PTY write loop
- Set 30s send timeout to prevent hangs on slow clients
- Remove receive timeout to prevent frame parsing desync
- Fix signed integer overflow in `parseResizeMessage`
- Remove `query` URL parameter auto-execution for security
- Set `application/javascript` MIME type for JS resources
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
xterm.js does not brighten the default foreground color for bold text
(only indexed colors 0-7 are affected by drawBoldTextInBrightColors).
Preprocess incoming data to inject SGR 97 (bright white) when bold is
set without an explicit foreground color, so bold text with the default
foreground renders as #ffffff instead of #d4d4d4.
Revert foreground/cursor back to #d4d4d4 for non-bold text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bold text using the default foreground color was still #d4d4d4
because xterm.js `drawBoldTextInBrightColors` only applies to
indexed colors, not the default foreground. Set foreground and
cursor to #ffffff.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set the indexed white color to #ffffff so that bold white text
is fully white rather than the muted #d4d4d4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the terminal panel transitions from hidden to visible,
`fitAddon.fit` could compute incorrect dimensions from stale layout,
sending a bogus resize (e.g. 10 columns) to the PTY and causing
the shell to redraw incorrectly.
Fix by:
- Deferring `fitAddon.fit` in the refit handler to `requestAnimationFrame`
so the browser layout is settled first.
- Skipping `ResizeObserver` callbacks when the container has zero size.
- Removing redundant `sendResize` calls (already triggered by
`term.onResize`).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the terminal panel is activated (via icon click or ~ key),
focus the iframe and the xterm.js terminal inside it so the user
can start typing immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of putting user/password in the iframe src URL (where they
could leak via browser history, logs, or Referer headers), play.html
now loads /webterminal without credentials and sends them via
postMessage after the iframe loads.
When embedded in an iframe without URL credentials, webterminal.html
waits for a `webterminal-credentials` message before opening the
WebSocket connection. When opened standalone, the existing interactive
prompt behavior is preserved.
Also tighten postMessage security by using window.location.origin
instead of '*' for the target origin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the terminal panel is hidden, xterm.js cannot measure its
dimensions. On re-show, send a postMessage to the iframe which
triggers fitAddon.fit() and a resize message to the PTY, fixing
the cursor position and partial line rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the embedded client exits inside the iframe, webterminal.html
sends a postMessage to the parent window instead of redirecting.
play.html listens for this message and deactivates the terminal
panel. When opened standalone (not in an iframe), the redirect
to the ClickHouse GitHub repo is preserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the `query` URL parameter is present (e.g.,
`/webterminal?query=SELECT 1`), paste it into the terminal
after a successful connection, similar to how `play.html`
handles the query parameter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Embed xterm.js 5.5.0, addon-fit 0.10.0, and addon-web-links 0.11.0
(minified JS and CSS) into the server binary, served at /js/*.
This removes the dependency on CDN and makes the web terminal
page fully self-contained, like the dashboard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Erase the "Connecting..." line before PTY output starts, so the
terminal content renders from a clean line. Also erase it on
auth failure before showing the error prompt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove the overlay login form; the terminal fills the entire page
- Auto-connect with default credentials (or credentials from URL params)
- On auth failure, prompt for username/password directly in the terminal
- If user was provided in the URL, only prompt for password
- After session ends, prompt for credentials again to reconnect
- Hide the scrollbar while keeping the terminal scrollable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a WebSocket-based terminal protocol (webterminal) that runs
a pseudoterminal with embedded clickhouse-client functionality,
accessible via a browser at /webterminal.
The implementation reuses the same `PtyClientDescriptorSet` and
`ClientEmbeddedRunner` infrastructure as the SSH protocol, but
connects via WebSocket instead of SSH. Authentication works the
same way as in the HTTP protocol (URL params, Basic auth,
X-ClickHouse headers).
The HTML page uses xterm.js for terminal emulation and connects
to the server via WebSocket, bridging data bidirectionally
between the browser and the PTY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>