From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 62F6B1FF0B7 for ; Tue, 25 Aug 2026 13:10:26 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 22ECB21726; Tue, 25 Aug 2026 13:09:17 +0200 (CEST) From: Alexandre Derumier To: pve-devel@lists.proxmox.com subject: SPAM: [RFC pve-kyber-web 10/13] add pve-kyber-web: console's webassembly client Date: Tue, 25 Aug 2026 13:08:42 +0200 Message-ID: <20260825110849.2967694-11-alexandre.derumier@groupe-cyllene.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260825110849.2967694-1-alexandre.derumier@groupe-cyllene.com> References: <20260825110849.2967694-1-alexandre.derumier@groupe-cyllene.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 7 AWL -0.550 Adjusted score from AWL reputation of From: address DMARC_QUAR 0.1 DMARC quarantine policy HEADER_FROM_DIFFERENT_DOMAINS 0.25 From and EnvelopeFrom 2nd level mail domains are different KAM_ASCII_DIVIDERS 0.8 Email that uses ascii formatting dividers and possible spam tricks KAM_DMARC_QUARANTINE 4 DKIM has Failed or SPF has failed on the message and the domain has a DMARC quarantine policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods KAM_SHORT 0.001 Use of a URL Shortener for very short URL POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_1 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_3 0.1 random spam to be learned in bayes PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: CRQL4ASBG4LKX5FVAGOHHSKLXVA6C6XF X-Message-ID-Hash: CRQL4ASBG4LKX5FVAGOHHSKLXVA6C6XF X-MailFrom: root@formationkvm1.odiso.net X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Alexandre Derumier X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Alexandre Derumier The console page use Kyber's WASM SDK directly, with some modifications to make it work behind pveproxy: - 0001 gives kyclient a base path, so the controller can be reached under /api2/json/nodes//qemu// rather than owning an origin. - 0002 resolves the renderer worker's wasm glue against baseURI instead of window.location.pathname. - 0003 puts the kymux token in the WebTransport path, giving a proxy in front of several VMs something to route on. Signed-off-by: Alexandre Derumier --- .gitignore | 4 + .gitmodules | 3 + Makefile | 118 +++++++++ debian/changelog | 7 + debian/control | 21 ++ debian/copyright | 20 ++ debian/install | 1 + debian/rules | 9 + debian/source/format | 1 + kyber-web | 1 + .../0001-kyclient-support-a-base-path.patch | 248 ++++++++++++++++++ ...renderer-worker-against-the-base-url.patch | 72 +++++ ...kymux-token-in-the-webtransport-path.patch | 37 +++ 13 files changed, 542 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/install create mode 100755 debian/rules create mode 100644 debian/source/format create mode 160000 kyber-web create mode 100644 patches/0001-kyclient-support-a-base-path.patch create mode 100644 patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch create mode 100644 patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dea6af5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/sdk/ +*.deb +*.buildinfo +*.changes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b7a917b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "kyber-web"] + path = kyber-web + url = https://gitlab.com/kyber/apps/kyber-web.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ed833c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,118 @@ +include /usr/share/dpkg/architecture.mk +include /usr/share/dpkg/pkg-info.mk + +PACKAGE=pve-kyber-web +DEB=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION)_all.deb +DSC=$(PACKAGE)_$(DEB_VERSION).dsc +BUILDDIR=$(PACKAGE)-$(DEB_VERSION_UPSTREAM) + +# Kyber's web client: AGPL-3.0-or-later like this package, but a separate +# upstream with its own cadence, so it is cloned at build time rather than +# vendored or carried as a submodule. This repo then holds nothing but +# packaging, and the revision it builds is one greppable line rather than a +# gitlink. +# +# Pinned by commit, so a moved tag cannot change what this builds. +KYBER_WEB_DIR = kyber-web + +# The four files the console page loads. Not the demo client's bundle, its +# sidebar or its metrics UI: the page drives the SDK directly, so none of that +# is built and neither pnpm nor esbuild is needed. +SDK_FILES = kyclient_wasm.js kyclient_wasm_bg.wasm audio_worklet.js spinlock.js + +all: $(DEB) + +# --- upstream --------------------------------------------------------------- +# Fetched once, and left alone after that, following pve-qemu: a build must not +# depend on re-fetching, and a checkout someone has been working in is theirs. +# +# The patch below is what needs the tree pristine, so it resets just the files +# it touches rather than the whole submodule - which would discard the build +# output beside them for nothing. +.PHONY: submodule +submodule: +ifeq ($(shell test -f "$(KYBER_WEB_DIR)/Cargo.toml" && echo 1 || echo 0), 0) + git submodule update --init --recursive $(KYBER_WEB_DIR) +endif + +# Applied from the kyber-web root: they span it and its nested kysdk submodule. +.PHONY: patch +patch: submodule + git -C $(KYBER_WEB_DIR) checkout --force -- . + git -C $(KYBER_WEB_DIR) submodule foreach --recursive --quiet 'git checkout --force -- .' + set -e; for p in $(CURDIR)/patches/000*.patch; do \ + git -C $(KYBER_WEB_DIR) apply "$$p"; \ + done + +# --- build ------------------------------------------------------------------ +# The version Cargo.lock pins, read at use rather than at parse: the checkout +# does not exist yet when make reads this file. +WASM_BINDGEN_VERSION = $(shell sed -n '/^name = "wasm-bindgen"$$/{n;s/^version = "\(.*\)"/\1/p;q}' $(KYBER_WEB_DIR)/Cargo.lock) + +# web_sys_unstable_apis is required for WebTransport, which is the whole data +# plane. A wasm-bindgen CLI older or newer than the crate emits glue the wasm +# rejects at instantiation, with no error until the console is opened. +.PHONY: wasm +wasm: patch + have=$$(wasm-bindgen --version | awk '{print $$2}'); \ + want="$(WASM_BINDGEN_VERSION)"; \ + if [ "$$have" != "$$want" ]; then \ + echo "wasm-bindgen $$have found, Cargo.lock pins $$want" >&2; \ + echo "install it with: cargo install --locked wasm-bindgen-cli@$$want" >&2; \ + exit 1; \ + fi + cd $(KYBER_WEB_DIR) && RUSTFLAGS=--cfg=web_sys_unstable_apis \ + cargo build --target wasm32-unknown-unknown -p kyclient-wasm --release + cd $(KYBER_WEB_DIR) && wasm-bindgen --target web --no-typescript --out-dir html \ + target/wasm32-unknown-unknown/release/kyclient_wasm.wasm + cd $(KYBER_WEB_DIR) && wasm-opt html/kyclient_wasm_bg.wasm \ + -o html/kyclient_wasm_bg.wasm -Os -g + # The JS glue: worker entry points and the audio worklet. + cd $(KYBER_WEB_DIR)/kysdk/kyctl && ./build-wasm.sh -j $(CURDIR)/$(KYBER_WEB_DIR)/html + +# --- packaging -------------------------------------------------------------- +.PHONY: sdk +sdk: wasm + rm -rf sdk && mkdir sdk + for f in $(SDK_FILES); do install -m 0644 "$(KYBER_WEB_DIR)/html/$$f" sdk/; done + +.PHONY: builddir +builddir: + rm -rf $(BUILDDIR) + $(MAKE) $(BUILDDIR) + +$(BUILDDIR): sdk + rm -rf $@ $@.tmp + mkdir $@.tmp + cp -a sdk debian Makefile $@.tmp/ + mv $@.tmp $@ + +deb: $(DEB) +$(DEB): $(BUILDDIR) + cd $(BUILDDIR); dpkg-buildpackage -b -us -uc + lintian $(DEB) || true + +# A source package, for sbuild and for review: Proxmox builds every package +# this way, so it has to work even when the binary path is what gets used. +.PHONY: dsc +dsc: + rm -rf $(BUILDDIR) $(DSC) + $(MAKE) $(DSC) + lintian $(DSC) + +$(DSC): $(BUILDDIR) + cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d + +sbuild: $(DSC) + sbuild $< + +.PHONY: dinstall +dinstall: deb + dpkg -i $(DEB) + +.PHONY: clean +clean: + rm -rf *.deb *.changes *.dsc *.buildinfo *.build $(PACKAGE)-[0-9]*/ sdk/ + +.PHONY: distclean +distclean: clean diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..ff9047b --- /dev/null +++ b/debian/changelog @@ -0,0 +1,7 @@ +pve-kyber-web (0.27.0) trixie; urgency=medium + + * Initial release: the Kyber WebAssembly client. + * Carries the fix for resolving the renderer worker against the document + base URL, without which the console renders black behind a path prefix. + + -- Proxmox Support Team Wed, 19 Aug 2026 07:00:00 +0200 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..2be348f --- /dev/null +++ b/debian/control @@ -0,0 +1,21 @@ +Source: pve-kyber-web +Section: admin +Priority: optional +Maintainer: Proxmox Support Team +Uploaders: Alexandre Derumier +Build-Depends: debhelper-compat (= 13), + binaryen, + git, +Standards-Version: 4.7.0.0 + +Package: pve-kyber-web +Architecture: all +Depends: ${misc:Depends}, +Description: Kyber streaming client for the Proxmox VE console + The WebAssembly client the Kyber console page drives: the wasm module, its + JavaScript glue and the audio worklet. + . + Built from kyber-web, which is a separate upstream with its own release + cadence and a wasm toolchain that has no business in the pve-manager build, + so it is packaged on its own and the console page in pve-manager loads it + from here. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..702a252 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,20 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: pve-kyber-web + +Files: * +Copyright: 2026 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..701f265 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +sdk/* usr/share/pve-kyber-web/ diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..2a8f910 --- /dev/null +++ b/debian/rules @@ -0,0 +1,9 @@ +#!/usr/bin/make -f +%: + dh $@ +# The sdk/ directory is staged into the build dir before dpkg-buildpackage +# runs, and the Makefile carried along with it has a distclean that removes +# it. Nothing here is built or cleaned by dh. +override_dh_auto_clean: +override_dh_auto_build: +override_dh_auto_test: diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/kyber-web b/kyber-web new file mode 160000 index 0000000..ad610dc --- /dev/null +++ b/kyber-web @@ -0,0 +1 @@ +Subproject commit ad610dc64b5dd5d88aecdbf5e56a21b49572307a diff --git a/patches/0001-kyclient-support-a-base-path.patch b/patches/0001-kyclient-support-a-base-path.patch new file mode 100644 index 0000000..4b9fb1d --- /dev/null +++ b/patches/0001-kyclient-support-a-base-path.patch @@ -0,0 +1,248 @@ +From: Kyber/QEMU integration +Subject: [PATCH] kyclient: let the controller be reached under a base path + +Client::new took a host and a port and built /session/login, +/kymux/start_session and /websocket/ from them, so the controller had to +own the origin it was served on. Behind a reverse proxy it does not: Proxmox +exposes it under /api2/json/nodes//qemu//, and there was no way to +say so. + +Add an optional base_path, threaded through ConnectConfig and ConnectionParams. +Every HTTP URL already derives from HttpClient::base_url, so those come along +with one change; the websocket URLs are built from the host instead and are +prefixed explicitly. + +That includes /ws, the control plane's own websocket in host_events - which is +easy to miss, because it lives on the other side of the platform split from the +three data-plane sockets and is opened later. Missing it does not fail at +startup: the client logs in, fetches /capabilities and only then tries to open +a websocket at the origin root, which behind a proxy names no VM. Found by +running the client through pvekyberproxy. + +normalize_base_path accepts "/kyber", "kyber/" and "/kyber/" alike - a doubled +or missing slash would otherwise show up as a 404 a long way from the setting +that caused it. + +This is not Proxmox-specific: any reverse-proxied deployment needs it, which is +why it is worth sending upstream rather than carrying here. +--- +diff --git a/kyclient/src/http.rs b/kyclient/src/http.rs +index bdf88b6..17d52f2 100644 +--- a/kysdk/kyctl/kyclient/src/http.rs ++++ b/kysdk/kyctl/kyclient/src/http.rs +@@ -38,8 +38,20 @@ pub(crate) fn format_host_for_url(host: &str, port: u16) -> String { + } + } + ++/// Trim a base path to the form the URL builders expect: a leading slash and ++/// no trailing one, or empty for the origin root. Accepting "/kyber", ++/// "kyber/" and "/kyber/" alike avoids a double or missing slash showing up ++/// as a 404 far from the setting that caused it. ++pub(crate) fn normalize_base_path(base_path: Option<&str>) -> String { ++ match base_path.map(str::trim).filter(|p| !p.is_empty() && *p != "/") { ++ None => String::new(), ++ Some(path) => format!("/{}", path.trim_matches('/')), ++ } ++} ++ + pub(crate) struct HttpClient { + base_url: String, ++ base_path: String, + client: reqwest::Client, + host: String, + } +@@ -47,12 +59,17 @@ pub(crate) struct HttpClient { + impl HttpClient { + pub(crate) fn new(conn_params: ConnectionParams) -> Result { + let host = platform_http::create_host(&conn_params); +- let base_url = format!("https://{host}"); ++ // Behind a reverse proxy the controller has no origin of its own, so ++ // every URL has to be built under the path it is exposed at rather ++ // than at the root. ++ let base_path = normalize_base_path(conn_params.base_path.as_deref()); ++ let base_url = format!("https://{host}{base_path}"); + + let client = platform_http::create_reqwest(conn_params)?; + + Ok(Self { + base_url, ++ base_path, + client, + host, + }) +@@ -74,6 +91,12 @@ impl HttpClient { + &self.base_url + } + ++ /// The path prefix on its own, for the websocket URLs - those are built ++ /// from the host rather than from base_url, so they cannot reuse it. ++ pub(crate) fn base_path(&self) -> &str { ++ &self.base_path ++ } ++ + pub(crate) fn peer_addr_from_response( + &self, + response: &reqwest::Response, +diff --git a/kyclient/src/platform/desktop/host_events.rs b/kyclient/src/platform/desktop/host_events.rs +index f8fd9e1..5345ff0 100644 +--- a/kysdk/kyctl/kyclient/src/platform/desktop/host_events.rs ++++ b/kysdk/kyctl/kyclient/src/platform/desktop/host_events.rs +@@ -53,7 +53,11 @@ impl WsClient { + auth_token: &str, + ) -> Result { + let host = platform::http::create_host(conn_params); +- let url = format!("wss://{host}/ws"); ++ // The control plane's own websocket sits under the base path too. It ++ // is built from the host rather than from base_url, so like the three ++ // data-plane sockets it has to be prefixed explicitly. ++ let base_path = crate::http::normalize_base_path(conn_params.base_path.as_deref()); ++ let url = format!("wss://{host}{base_path}/ws"); + + // Connect with appropriate TLS verification + let mut stream = if let Some(verify_mode) = &conn_params.verify_mode { +diff --git a/kyclient/src/platform/desktop/mod.rs b/kyclient/src/platform/desktop/mod.rs +index fadb052..44681a0 100644 +--- a/kysdk/kyctl/kyclient/src/platform/desktop/mod.rs ++++ b/kysdk/kyctl/kyclient/src/platform/desktop/mod.rs +@@ -46,6 +46,10 @@ pub use tls::{TofuPrompt, TofuVerifier, VerifyMode}; + pub struct ConnectConfig { + pub host: String, + pub port: u16, ++ /// Path the controller is reached under, when it sits behind a reverse ++ /// proxy that cannot give it an origin of its own. Every HTTP and ++ /// websocket URL is built beneath it. Default: the origin root. ++ pub base_path: Option, + pub tls_host: Option, + /// TLS verification mode. + pub verify_mode: Option, +@@ -126,6 +130,7 @@ impl VideoPlayerConfig { + pub(crate) struct ConnectionParams { + pub(crate) host: String, + pub(crate) port: u16, ++ pub(crate) base_path: Option, + pub(crate) tls_host: Option, + pub(crate) verify_mode: Option, + pub(crate) credentials: AuthCredentials, +@@ -136,6 +141,7 @@ impl ConnectionParams { + Ok(Self { + host: connect_config.host, + port: connect_config.port, ++ base_path: connect_config.base_path.clone(), + tls_host: connect_config.tls_host.clone(), + verify_mode: connect_config.verify_mode, + credentials: connect_config.credentials, +diff --git a/kyclient/src/platform/web/host_events.rs b/kyclient/src/platform/web/host_events.rs +index 425e76c..a6f2415 100644 +--- a/kysdk/kyctl/kyclient/src/platform/web/host_events.rs ++++ b/kysdk/kyctl/kyclient/src/platform/web/host_events.rs +@@ -315,7 +315,11 @@ impl WsClient { + auth_token: &str, + ) -> Result { + let host = platform::http::create_host(conn_params); +- let url = format!("wss://{host}/ws"); ++ // The control plane's own websocket sits under the base path too. It ++ // is built from the host rather than from base_url, so like the three ++ // data-plane sockets it has to be prefixed explicitly. ++ let base_path = crate::http::normalize_base_path(conn_params.base_path.as_deref()); ++ let url = format!("wss://{host}{base_path}/ws"); + + let inner = InnerRef(Inner::new(url, capabilities, event_sink, auth_token).await?); + +diff --git a/kyclient/src/platform/web/mod.rs b/kyclient/src/platform/web/mod.rs +index bec764b..3cfbb9d 100644 +--- a/kysdk/kyctl/kyclient/src/platform/web/mod.rs ++++ b/kysdk/kyctl/kyclient/src/platform/web/mod.rs +@@ -41,6 +41,10 @@ pub(crate) use websocket::{WebSocket, WebSocketHandler}; + pub struct ConnectConfig { + pub host: String, + pub port: u16, ++ /// Path the controller is reached under, when it sits behind a reverse ++ /// proxy that cannot give it an origin of its own. Every HTTP and ++ /// websocket URL is built beneath it. Default: the origin root. ++ pub base_path: Option, + pub credentials: AuthCredentials, + /// Enable automatic reconnection on connection loss. Default: false. + /// When enabled, the client will emit `Reconnecting`, `Reconnected`, and +@@ -95,6 +99,7 @@ impl From for player::VideoCodec { + pub(crate) struct ConnectionParams { + pub(crate) host: String, + pub(crate) port: u16, ++ pub(crate) base_path: Option, + pub(crate) credentials: AuthCredentials, + } + +@@ -103,6 +108,7 @@ impl ConnectionParams { + Ok(Self { + host: connect_config.host.clone(), + port: connect_config.port, ++ base_path: connect_config.base_path.clone(), + credentials: connect_config.credentials, + }) + } +diff --git a/kyclient/src/ws_backend/mod.rs b/kyclient/src/ws_backend/mod.rs +index c775c98..009612c 100644 +--- a/kysdk/kyctl/kyclient/src/ws_backend/mod.rs ++++ b/kysdk/kyctl/kyclient/src/ws_backend/mod.rs +@@ -163,8 +163,9 @@ impl WsBackend { + VideoPlayer::create(listener, &player_config, metrics).await?; + + let url = format!( +- "wss://{host}/websocket/video", +- host = backend_config.http_client.host() ++ "wss://{host}{base_path}/websocket/video", ++ host = backend_config.http_client.host(), ++ base_path = backend_config.http_client.base_path() + ); + let ws = VideoWsSocketHandler::new(&url, player)?; + +@@ -194,8 +195,9 @@ impl WsBackend { + }; + + let url = format!( +- "wss://{host}/websocket/audio", +- host = backend_config.http_client.host() ++ "wss://{host}{base_path}/websocket/audio", ++ host = backend_config.http_client.host(), ++ base_path = backend_config.http_client.base_path() + ); + let ws = AudioWsSocketHandler::new(&url, player)?; + +@@ -220,8 +222,9 @@ impl WsBackend { + #[allow(clippy::arc_with_non_send_sync)] + let msg_sink = Arc::new(InputMsgSink::new(backend_config.msg_sender.clone())); + let inputs_url = format!( +- "wss://{host}/websocket/inputs", +- host = backend_config.http_client.host() ++ "wss://{host}{base_path}/websocket/inputs", ++ host = backend_config.http_client.host(), ++ base_path = backend_config.http_client.base_path() + ); + let input_websocket = + InputWebSocketHandler::new(&inputs_url, kynput_tx, kynput_rx, msg_sink)?; +diff --git a/src/lib.rs b/src/lib.rs +index e692168..90b5d7c 100644 +--- a/src/lib.rs ++++ b/src/lib.rs +@@ -496,6 +496,9 @@ impl Client { + /// `"decoded"`, `"skipped"`, `"prepared"`, `"displayed"`. + /// All timestamps are in microseconds (µs). The server timestamps are already compensated + /// to match the client clock. ++ /// `base_path` is the path the controller is reached under when it sits ++ /// behind a reverse proxy that cannot give it an origin of its own; pass ++ /// `null` when it has the origin to itself. + pub async fn new( + host: &str, + port: u16, +@@ -505,10 +508,12 @@ impl Client { + metrics_mode_str: String, + live_metrics_callback: Option, + auto_reconnection: bool, ++ base_path: Option, + ) -> JsResult { + let connect_config = kyclient::ConnectConfig { + host: host.into(), + port, ++ base_path, + credentials: credentials.0, + auto_reconnection, + }; diff --git a/patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch b/patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch new file mode 100644 index 0000000..f906736 --- /dev/null +++ b/patches/0002-kywebplayer-resolve-the-renderer-worker-against-the-base-url.patch @@ -0,0 +1,72 @@ +From: Kyber/QEMU integration +Subject: [PATCH] kywebplayer: resolve the renderer worker against the base URL + +RendererWorker::create built the URL of the wasm glue from +window.location.pathname, so the glue had to sit in the same directory as the +HTML that loaded it. That holds for the demo client, whose page and SDK ship +side by side, and does not hold for an application that says otherwise with a + - Proxmox serves its console page at / and the SDK under /kyber/. + +The failure is expensive to find. The worker is created, so the canvas is +transferred to it and the decoder runs happily; only the module fetch 404s, +inside a worker, where nothing surfaces it. Frames are then decoded and +dropped - the browser reports them garbage collected without being closed - and +the console shows a black canvas with no error in the page, no exception, and +no failed request that a page-level capture would see. Found by attaching to +the worker target and watching its network. + +baseURI is the document URL wherever no tag exists, so this changes +nothing for the demo client and every other existing caller. +--- +diff --git a/kywebplayer/src/video/renderer_worker.rs b/kywebplayer/src/video/renderer_worker.rs +index 8aa7fb2..8224d25 100644 +--- a/kysdk/kyctl/kywebplayer/src/video/renderer_worker.rs ++++ b/kysdk/kyctl/kywebplayer/src/video/renderer_worker.rs +@@ -264,20 +264,34 @@ impl RendererWorker { + // This is a library, but the path of the wasm-bindings JavaScript file + // depends on the top-level crate name, so the worker code must be + // generated dynamically. +- let location = web_sys::window() +- .ok_or_else(|| JsValue::from(js_sys::Error::new("No window")))? +- .location(); +- let origin = location.origin()?; +- let pathname = location.pathname()?; +- // pathname can be a directory (".../") or include a filename +- // (".../index.html"); strip back to the last "/" so we resolve the wasm +- // glue next to the HTML rather than concatenating onto the filename. +- let dir = match pathname.rfind('/') { +- Some(i) => &pathname[..=i], +- None => "/", +- }; ++ let window = web_sys::window() ++ .ok_or_else(|| JsValue::from(js_sys::Error::new("No window")))?; + let sanitized_crate_name = app_crate_name.replace("-", "_"); +- let wasm_js_url = format!("{origin}{dir}{sanitized_crate_name}.js"); ++ ++ // Resolved against the document's base URL, not its path. The two are ++ // the same until a page carries a , and then they are not: ++ // an application whose HTML is served from one place and whose wasm ++ // glue lives in another says so with that tag, and resolving against ++ // the path instead sends this worker somewhere the script is not. ++ // ++ // It fails in a way that takes a long time to find. The worker is ++ // created, so the canvas is transferred to it and the decoder runs; ++ // only the module fetch 404s, inside a worker, where nothing surfaces ++ // it. Frames are then decoded and dropped, and the console shows a ++ // black canvas with no error anywhere. ++ // ++ // baseURI is the document URL when there is no , so this is the ++ // old behaviour wherever the tag is absent. ++ let base = window ++ .document() ++ .ok_or_else(|| JsValue::from(js_sys::Error::new("No document")))? ++ .base_uri()? ++ .ok_or_else(|| JsValue::from(js_sys::Error::new("No base URI")))?; ++ let wasm_js_url = web_sys::Url::new_with_base( ++ &format!("{sanitized_crate_name}.js"), ++ &base, ++ )? ++ .href(); + let worker_code = format!( + r#"import init, {{ VideoRendererWorkerCtx }} from "{wasm_js_url}"; + let ctx; diff --git a/patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch b/patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch new file mode 100644 index 0000000..5008623 --- /dev/null +++ b/patches/0003-kyclient-put-the-kymux-token-in-the-webtransport-path.patch @@ -0,0 +1,37 @@ +From: Kyber/QEMU integration +Subject: [PATCH] kyclient: put the kymux token in the WebTransport path + +The client opened WebTransport at the origin root, so the only thing +distinguishing one console from another was the port it connected to. That is +fine against a controller, which serves one VM, and it forces a proxy in front +of several to spend a UDP port per session: nothing in an encrypted datagram +says which VM it belongs to, so the port has to be the routing key. + +Putting the token in the path gives such a proxy something to route on, and it +costs nothing to either end. The token already exists, both sides already agree +on it, and it is already carried in this connection - it authenticates to the +controller a moment later. It does not travel in the clear either: a path is +inside the encrypted session, unlike a port. + +A controller reached directly ignores the path, so this changes nothing for a +client that is not behind a proxy. +--- +diff --git a/kyclient/src/kymux_backend/mod.rs b/kyclient/src/kymux_backend/mod.rs +index db7b13a..b56e888 100644 +--- a/kysdk/kyctl/kyclient/src/kymux_backend/mod.rs ++++ b/kysdk/kyctl/kyclient/src/kymux_backend/mod.rs +@@ -265,7 +265,13 @@ async fn start_kymux(backend_config: &KymuxBackendConfig<'_>) -> Result