Skip to content

GUACAMOLE-261: Add client-side support for the SPICE protocol#1227

Open
ciroiriarte wants to merge 33 commits into
apache:mainfrom
ciroiriarte:feature/spice-protocol
Open

GUACAMOLE-261: Add client-side support for the SPICE protocol#1227
ciroiriarte wants to merge 33 commits into
apache:mainfrom
ciroiriarte:feature/spice-protocol

Conversation

@ciroiriarte

Copy link
Copy Markdown

Implements the client-side half of GUACAMOLE-261: SPICE protocol support in guacamole-client / guacamole-common-js.

Contents

  • SPICE connection form and parameters (audio, disable-audio-opus, preferred-video-codec, file-transfer-mode, secondary-monitors, recording-include-clipboard, etc.).
  • Clipboard cockpit for recording playback: a Clipboard Activity panel surfacing text/image transfers with direction, image thumbnails, timeline ticks, clustering, and truncation handling.
  • multimon-layout parsing hardening and suppression of sensitive argv parameters in the UI.

Scope note

This branch also carries the GUACAMOLE-288 browser multi-monitor redesign (ported from #1061 by @corentin-soriano, original authorship preserved), with the multi-monitor UI extended to SPICE connections and per-connection BroadcastChannel namespacing.

Testing

Validated end-to-end (browser → guacamole-client → guacd → SPICE guest) as part of the GUACAMOLE-261 QA battery (14/14): SPICE render, keyboard input with layout mapping, multi-monitor extend, and clipboard playback. Frontend builds clean.

Companion PRs: server (apache/guacamole-server) and documentation (apache/guacamole-manual).

ciroiriarte and others added 30 commits July 2, 2026 08:42
Ports necouchman's apache#761, extended to match the
full guacamole-server SPICE parameter set: adds pubkey, proxy,
tls-port, swap-red-blue, preferred-compression, enable-audio-input,
full file-transfer group, and the complete keyboard layout list
(da-dk-qwerty through tr-tr-qwerty, 20 layouts).  Registers "spice"
in LocalEnvironment.KNOWN_PROTOCOLS (alphabetically between rdp and
ssh), adds spice.json protocol form definition with 11 connection
form groups (network, authentication, security, display, keyboard,
clipboard, audio, file-transfer, sftp, recording, wol), and adds all
corresponding PROTOCOL_SPICE translation keys to en.json.

Co-authored-by: Virtually Nick <vnick@apache.org>
Adds the disable-audio-opus boolean (audio section) and its translation,
matching the guacamole-server parameter that forces the legacy CELT audio
codec instead of Opus.
On modern SPICE servers (which dropped CELT) disabling Opus yields raw/lossless
audio, not CELT; reword the label to reflect that rather than implying CELT.
Adds the "secondary-monitors" parameter to the SPICE protocol so the
maximum number of secondary monitors can be configured per connection.
This is the SPICE counterpart to the parameter added for RDP in the
upstream multi-monitor work (apache#1061 by
@corentin-soriano); the browser-side per-monitor window handling from
that PR is protocol-agnostic and applies to SPICE unchanged.
The browser-side multi-monitor support ported from
apache#1061 (by @corentin-soriano) gated the "add
monitor" UI to the RDP protocol. SPICE now implements the same
server-side multi-monitor protocol (secondary-monitors advertisement +
multimon-layout), so allow the multi-monitor UI for SPICE connections as
well. All remaining multi-monitor logic is protocol-agnostic and driven
by the secondary-monitors connection parameter.
…t layout absence

getOffsetX now returns the monitor's absolute left offset (symmetric to
getOffsetY) instead of summing prior monitors' rendered widths, so draw
and mouse offsets are correct for vertical stacks, gaps, and non-tiled
arrangements. onmultimonlayout no longer closes a user-opened monitor
window the moment it is absent from the layout: it tolerates a short run
of transient absences (guest thrash) before closing, so a momentary
1-monitor layout no longer tears down the secondary window. Adds
getMonitorsInfos() for the resize handlers.
Coalesce rapid element-resize events into a single size request (250ms
debounce), cap the effective devicePixelRatio so a single monitor and
the combined width stay within a sane bound, and ignore the element
resize echoes that follow a server-driven display resize for a short
window. Together these break the resize feedback loop that froze the
main window and let the combined display settle instead of oscillating.
Expose the SPICE recording-include-clipboard parameter (server #7) in the
connection schema and add its English field label, matching RDP and VNC.
…oads (#14)

Expose the new file-transfer-mode connection parameter as an ENUM
(none | agent | drive | both) in the SPICE connection form, with translated
labels distinguishing the bidirectional shared folder from the upload-only
direct-to-guest SPICE agent transfer.
…ity)

Fixes finding M1 (Medium) + L2 (Low) from the SPICE security assessment
(ciroiriarte/guacamole-server#21, tracked as #29).

The multi-monitor windows shared a single hard-coded same-origin
BroadcastChannel('guac_monitors') with no per-message provenance. With two
concurrent connections open, a secondary window's keystrokes (possibly typed
credentials) were injected into the other connection's guacd session and its
display stream was mirrored across the connection boundary.

The primary now generates a per-connection channel id, passes it to secondary
windows via their route (/secondaryMonitor/:id/:channel?), and namespaces the
channel as 'guac_monitors_<id>'. Every message is tagged with the id and
inbound messages carrying a foreign id are dropped before dispatch (also closes
L2: unauthenticated broadcast -> client.runHandler).
Image clipboard events in a session recording previously rendered as the
literal text "[Clipboard: [Binary data]]": ClipboardEventInterpreter ran a
UTF-8 text decode on every stream regardless of mimetype, and the display
service discarded the mimetype before rendering.

Handle image/* clipboard streams distinctly: keep the base64 payload and
expose it as a (lowercased-scheme) data: URL plus a byte size, instead of
forcing it through the text decode. The player now renders image clipboard
events as an inline thumbnail chip showing mimetype and size, with a
click-to-enlarge lightbox (Escape or click-out to dismiss). A searchable
synthetic label ("[Clipboard image: mimetype size]") keeps image events
matchable by the existing key-log text filter. Text clipboard events are
unchanged.

Also document, in SessionRecording.js, that onkeyevents/onclipboardevents
are delivered only on the tunnel-CLOSED path; a Blob-sourced recording
never fires them (the bundled player feeds a StaticHTTPTunnel).

Direction annotation (guest-to-client vs client-to-guest), recorded by the
server as a log instruction, is not yet surfaced during playback and
remains a follow-up.
…playback

Two follow-ups to the image-clipboard playback support.

Direction annotation: the server emits a "log" instruction immediately
before each clipboard stream recording its direction ("clipboard stream=N
direction=guest-to-client|client-to-guest ..."). SessionRecording now routes
the log opcode to the clipboard interpreter, which buffers the direction by
stream index and attaches it to the resulting clipboard event. The player
renders it as a colour-coded badge before each clipboard event (both text
and image) - amber "from guest" for data leaving the guest (the
exfiltration-relevant case) and neutral "to guest" for pastes into it.
Unannotated recordings render exactly as before.

Thumbnails: the inline image chip previously embedded the full-resolution
data URL and merely scaled it down with CSS, inflating the key-log DOM for
recordings with many or large clipboard images. The chip now renders a
canvas-downscaled thumbnail (max 96px) and shows the image's natural
dimensions, while the click-to-enlarge lightbox keeps the full-resolution
image. Thumbnail generation is idempotent, uses $evalAsync from the async
image load, and aborts in-flight loads on scope destroy.
Two Low-severity fixes from the SPICE security assessment
(ciroiriarte/guacamole-server#21).

L3 (CWE-20): the server-supplied `multimon-layout` JSON was fed straight
through `JSON.parse` into offset/size math. A malformed payload threw out of
the instruction handler loop (client teardown) and non-finite geometry
poisoned the offsets with NaN. Client.js now parses and dispatches inside a
try/catch and requires a non-null object; guacManageMonitor validates each
entry's geometry (finite width/height/top/left, positive dimensions) and
treats a malformed entry as an absent monitor. Closes #30.

L4 (CWE-1230): values streamed back via `argv` for immutable arguments were
reflected into the UI-visible arguments model, which could surface a
sensitive parameter (e.g. a password) in the connection-parameters panel.
ManagedClient now skips reflecting parameters whose name matches
password/passphrase/private-key/client-key/secret/token, keeping secret
material out of the UI while leaving public material (host-key, public-key,
pubkey) visible. Mutation toward guacd was already rejected server-side.
Closes #31.
First increment of the clipboard audit cockpit (guacclip Phase 2). Adds a
dedicated "Clipboard Activity" panel to the session-recording player,
alongside the existing key-log, toggled from the player controls.

The panel lists clipboard transfers as chronological cards: click (or
keyboard Enter/Space) seeks to the event; a text-labelled, colour-coded
direction badge marks each as copied-out-of-guest (exfiltration) or
pasted-in; text is shown in a code block with show-more, images as a lazy
downscaled thumbnail with a click-to-enlarge lightbox; each card offers a
Download of the original artifact. A header button exports the whole
activity list to CSV (formula-injection-neutralized and UTF-8-BOM-prefixed
for safe spreadsheet import). An empty state explains when no clipboard was
recorded.

The canvas-thumbnail logic is factored out of textView into a shared
clipboardMediaService used by both the key-log and the new panel, with
textView behaviour preserved (pending-image tracking + $destroy abort).

Reviewed (0 critical/high); CSV formula-injection + BOM and a download
decode guard applied from the review. Builds clean (webpack). Deferred to
#34 follow-ups: seek-bar timeline tick-marks and rapid-repeat event
clustering.
Second increment of the clipboard audit cockpit, completing the remaining
Phase 2 items.

Timeline tick-marks: each clipboard transfer is marked on the player's
seek/heatmap bar, colour-coded by direction (amber copied-out / slate
pasted-in), positioned by timestamp. Clicking a tick seeks to ~5s before the
transfer (context lead-in); hovering shows the direction plus a thumbnail
(images) or text preview. The tick layer ignores pointer events except on the
2px marks, so the seek slider stays draggable across its full width; ticks are
a mouse affordance only (the panel cards remain the keyboard-accessible seek
path), and positions are clamped to the bar.

Event clustering: the Clipboard Activity panel collapses runs of consecutive
same-direction, identical-content transfers into one card with a repeat count,
time range, and expand/collapse. The flat event list is preserved, so CSV
export and per-item download remain un-clustered.

Truncation banner: ClipboardEventInterpreter.getIncompleteCount() reports
streams that opened but never ended (truncated recording); SessionRecording
passes it via a new, backward-compatible second onclipboardevents argument,
and the panel shows a warning banner when transfers were recorded
incompletely.

Reviewed (0 critical/high); accessibility (tick tab-stop flood) and overflow
fixes applied. Builds clean (webpack). Closes the timeline-ticks and
clustering follow-ups on #34.
Follow-ups from a quality/security assessment of the clipboard audit cockpit.

Security: image Download no longer echoes the attacker-controlled recording
mimetype. imageExtension is now a closed allowlist (unknown subtypes, e.g. a
forged image/svg, map to an inert .bin), and the downloaded Blob is typed
application/octet-stream rather than the recording mimetype - so a hostile
recording can no longer produce a script-capable file that runs when the
auditor opens it.

Performance: seek-bar tick tooltips now use the downscaled thumbnail
(tick.raw.thumbURL) instead of the full-resolution data URL, and ticks are
capped at 500 (the panel and CSV still retain every event) so a recording
with thousands of clipboard transfers can't bloat the transport bar.

De-duplication: the byte-identical direction-metadata maps in clipboardView
and player were hoisted into clipboardMediaService.getDirectionMeta, and the
verbatim-duplicated lightbox controller (open/close/Escape) was extracted into
clipboardMediaService.attachLightbox, now shared by the key-log and clipboard
viewers. attachLightbox also adds focus management - focus moves into the
dialog on open (via $timeout, after the ng-if overlay renders) and is restored
on close - closing an accessibility gap. Hardcoded English alt/aria-label
strings moved into en.json.

Reviewed (APPROVE, 0 critical/high/medium); builds clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants