35 Commits
Author SHA1 Message Date
Shaun Struwig 665bb20682 Preserve focus when restoring the web terminal
Addresses https://github.com/ClickHouse/ClickHouse/pull/116026#discussion_r3844674108
2026-08-24 17:13:35 +02:00
Shaun Struwig fb68e66393 formatting: style the terminal as a fixed full width bar 2026-08-24 15:57:47 +02:00
Alexey Milovidov db5b79a8b5 Close the embedded terminal panel for any embedding page
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.
2026-08-24 02:07:45 +00:00
Alexey MilovidovandClaude Opus 5 949cbca3bb Web Terminal: connect once when the host page supplies the credentials
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>
2026-08-17 14:10:26 +00:00
Alexey Milovidov 7e9dc1ef36 Merge remote-tracking branch 'origin/master' into play-html-empty-user
# Conflicts:
#	programs/server/webterminal.html
2026-05-19 20:24:10 +00:00
Alexey Milovidov efb358312b Merge pull request #105191 from ClickHouse/webterminal-cross-origin-postmessage
Web Terminal: accept cross-origin postMessage from clickhouse.com/cloud
2026-05-18 23:26:16 +00:00
Alexey MilovidovandClaude Opus 4.7 23fbd3cc04 Web Terminal: gate postMessage listener on e.source === window.parent
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>
2026-05-18 17:24:31 +00:00
Alexey MilovidovandClaude Opus 4.7 0c395a9198 Web pages: empty user input falls back to server default
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>
2026-05-18 18:53:07 +02:00
Alexey Milovidov 6f4cc454eb Web Terminal: accept cross-origin postMessage from clickhouse.com/cloud
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.
2026-05-18 04:46:18 +02:00
Alexey MilovidovandClaude Opus 4.7 6b8a14bb00 Web terminal: pre-arm credentials receiver for iframe parent
`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>
2026-05-17 02:05:31 +00:00
Alexey Milovidov 72bc883fac Web terminal: attempt automatic authentication before prompting
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.
2026-05-15 18:50:04 +02:00
Alexey Milovidov 8138607d43 Address review: drop URL-based password in webterminal.html
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.
2026-05-02 10:39:15 +00:00
Alexey MilovidovandClaude Opus 4.6 47ccd7ae3d Address review: pre-auth timeout, EAGAIN backpressure, auth close handling
- 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>
2026-03-27 00:47:30 +00:00
Alexey MilovidovandClaude Opus 4.6 1210478cd9 Address review: experimental gate, in-band auth, Origin validation
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>
2026-03-25 13:19:30 +00:00
Alexey MilovidovandClaude Opus 4.6 8337200e19 Address review comments: WebSocket protocol hardening and misc fixes
- 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>
2026-03-23 18:50:51 +01:00
Alexey MilovidovandClaude Opus 4.6 28f5d21855 Escape key deactivates the webterminal when opened from the playground
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:05:32 +01:00
Alexey MilovidovandClaude Opus 4.6 a705208707 Dark themed scrollbar for the webterminal
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:58:16 +01:00
Alexey MilovidovandClaude Opus 4.6 d6032c0a5e Do not print "Authentication failed" in the terminal
Just re-prompt for credentials without an error message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 03:02:07 +01:00
Alexey Milovidov 0fba3bf8a9 Revert "Change terminal default foreground to pure white"
This reverts commit 772f8d2fe7a80b58b1a1ad64fb7302430e4aa897.
2026-03-23 02:31:44 +01:00
Alexey Milovidov 46e3886cb3 Revert "Make bold default-foreground text render as pure white in terminal"
This reverts commit 6b98415145631757f0f80f0495ba414d64efd6c6.
2026-03-23 02:31:44 +01:00
Alexey MilovidovandClaude Opus 4.6 969ab05a8e Make bold default-foreground text render as pure white in terminal
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>
2026-03-23 02:30:18 +01:00
Alexey MilovidovandClaude Opus 4.6 35c375bc44 Change terminal default foreground to pure white
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>
2026-03-23 02:27:25 +01:00
Alexey MilovidovandClaude Opus 4.6 b509936f95 Change terminal white color to pure white
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>
2026-03-23 02:21:27 +01:00
Alexey MilovidovandClaude Opus 4.6 1fbd011cba Fix terminal resize glitch when restoring hidden terminal panel
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>
2026-03-23 02:16:39 +01:00
Alexey MilovidovandClaude Opus 4.6 2dc916eb83 Focus terminal when activating it in Play UI
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>
2026-03-23 02:04:58 +01:00
Alexey MilovidovandClaude Opus 4.6 2ababdf235 Pass credentials from play.html to webterminal iframe via postMessage
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>
2026-03-21 22:11:37 +01:00
Alexey MilovidovandClaude Opus 4.6 ecae9d5fd0 Refit terminal after showing hidden panel
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>
2026-03-21 15:55:08 +01:00
Alexey MilovidovandClaude Opus 4.6 4d78dd5f47 Deactivate terminal panel in play.html when session ends
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>
2026-03-21 15:44:45 +01:00
Alexey MilovidovandClaude Opus 4.6 eeab537a7b Support query URL parameter in web terminal
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>
2026-03-21 14:37:27 +01:00
Alexey MilovidovandClaude Opus 4.6 4500d55cfc Redirect to ClickHouse GitHub repo when client session ends
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:28:28 +01:00
Alexey MilovidovandClaude Opus 4.6 07c8bcd433 Bundle xterm.js libraries as embedded static files
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>
2026-03-21 14:28:05 +01:00
Alexey MilovidovandClaude Opus 4.6 96a404dae8 Change web terminal background to black
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:16:57 +01:00
Alexey MilovidovandClaude Opus 4.6 c8a535ccc0 Clean up "Connecting..." message on successful connection
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>
2026-03-21 14:15:14 +01:00
Alexey MilovidovandClaude Opus 4.6 49b480b603 Rework web terminal to use in-terminal authentication prompts
- 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>
2026-03-21 13:29:15 +01:00
Alexey MilovidovandClaude Opus 4.6 4a12d6c663 Add web terminal interface to clickhouse-server
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>
2026-03-21 13:11:13 +01:00