diff --git a/CHANGELOG.md b/CHANGELOG.md index ebdf15e..4bfec16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,9 @@ `GST_PLUGIN_PATH_1_0`: the nix webkit's own GStreamer closure carries no capture device provider, so it enumerated zero cameras and `getUserMedia` failed with `OverconstrainedError: Invalid constraint` whatever the constraints. +- The Linux camera/microphone grant is answered per request and only while + the page asking is on the origin the webview first loaded; any other page + is denied. - Android: `tauri-plugin-hc` initializes `ndk_context` in `JNI_OnLoad` with the process's `Application`. tao 0.35 (Tauri 2.11) stopped doing this, so the first `ndk_context::android_context()` call panicked and the app aborted on launch; diff --git a/crates/tauri-plugin-hc/src/lib.rs b/crates/tauri-plugin-hc/src/lib.rs index a7e2505..b4d5f5d 100644 --- a/crates/tauri-plugin-hc/src/lib.rs +++ b/crates/tauri-plugin-hc/src/lib.rs @@ -212,7 +212,7 @@ pub struct HolochainPlugin { /// The origin each webview first loaded from, by label: recorded on its /// first navigation or page load, kept until the window is destroyed (see /// `origin.rs`). Windows from [`HolochainPlugin::lock_navigation`] refuse - /// to leave it. + /// to leave it, and the Linux media grant is answered against it. window_origins: WindowOrigins, /// Monotonic rebind counter — rides each [`EVENT_REBOUND`] as its `seq` so the /// injected env can drop a stale (out-of-order) rebound rather than leave the @@ -667,10 +667,19 @@ fn plugin_builder( } }) // Linux: WebKitGTK denies camera/microphone access unless the embedder - // answers its permission-request signal (see linux_media.rs). + // answers its permission-request signal (see linux_media.rs). Each + // request is answered against the origin the webview first loaded. .on_webview_ready(|webview| { #[cfg(target_os = "linux")] - linux_media::allow_media_capture(&webview); + match webview.app_handle().holochain() { + Ok(plugin) => { + linux_media::allow_media_capture(&webview, plugin.window_origins.clone()) + } + Err(e) => log::warn!( + "not granting media capture to webview {}: {e}", + webview.label() + ), + } #[cfg(not(target_os = "linux"))] let _ = webview; }) diff --git a/crates/tauri-plugin-hc/src/linux_media.rs b/crates/tauri-plugin-hc/src/linux_media.rs index 9508635..3dc97f1 100644 --- a/crates/tauri-plugin-hc/src/linux_media.rs +++ b/crates/tauri-plugin-hc/src/linux_media.rs @@ -4,20 +4,29 @@ //! webview's `permission-request` signal; the default handler denies, and //! neither wry nor Tauri connects one (they do on Android). So a hApp UI that //! opens the camera, e.g. to scan a QR joining code, gets `NotAllowedError` on -//! Linux and works everywhere else. This grants media capture for every -//! webview the plugin sees, which is the app's own bundled UI, not arbitrary -//! web content. The user still gets no OS-level prompt, matching how the same -//! UI behaves in the other webviews. +//! Linux and works everywhere else. This grants media capture to the app's own +//! UI: a request is allowed only while the page making it is on the origin the +//! webview first loaded (see `origin.rs`), so a webview showing anything else +//! is denied. Plugin-built windows cannot leave that origin anyway; this covers +//! windows the app builds itself. The user still gets no OS-level prompt, +//! matching how the same UI behaves in the other webviews. //! //! The devices must also be visible to WebKit's GStreamer; the runtime-tauri //! dev shell provides the capture plugins (see `GST_PLUGIN_PATH_1_0` in //! flake.nix). -use tauri::{Runtime, Webview}; +use crate::origin::same_origin; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tauri::{Runtime, Url, Webview}; -/// Allow user-media (camera/microphone) permission requests on `webview`. -/// Other permission requests keep WebKit's default handling. -pub(crate) fn allow_media_capture(webview: &Webview) { +/// Allow user-media (camera/microphone) permission requests on `webview` while +/// the page asking is on the origin recorded for it in `origins`. Other +/// permission requests keep WebKit's default handling. +pub(crate) fn allow_media_capture( + webview: &Webview, + origins: Arc>>, +) { let label = webview.label().to_string(); let result = webview.with_webview(move |platform| { use webkit2gtk::glib::prelude::*; @@ -31,15 +40,28 @@ pub(crate) fn allow_media_capture(webview: &Webview) { if let Some(settings) = wv.settings() { settings.set_enable_media_stream(true); } - wv.connect_permission_request(move |_, request| { - if request.is::() { - log::debug!("granting user-media permission to webview {label}"); - request.allow(); - true - } else { + wv.connect_permission_request(move |wv, request| { + if !request.is::() { // Let WebKit's default handler decide (it denies). - false + return false; } + let page = wv.uri().and_then(|uri| Url::parse(&uri).ok()); + let origin = origins.lock().unwrap().get(&label).cloned(); + match (&page, &origin) { + (Some(page), Some(origin)) if same_origin(page, origin) => { + log::debug!("granting user-media permission to webview {label}"); + request.allow(); + } + _ => { + log::warn!( + "denying user-media permission to webview {label}: page {} is not on its origin {}", + page.map(|p| p.to_string()).unwrap_or_else(|| "".into()), + origin.map(|o| o.to_string()).unwrap_or_else(|| "".into()) + ); + request.deny(); + } + } + true }); }); if let Err(e) = result {