Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 12 additions & 3 deletions crates/tauri-plugin-hc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ pub struct HolochainPlugin<R: TauriRuntime> {
/// 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
Expand Down Expand Up @@ -667,10 +667,19 @@ fn plugin_builder<R: TauriRuntime>(
}
})
// 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;
})
Expand Down
52 changes: 37 additions & 15 deletions crates/tauri-plugin-hc/src/linux_media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R: Runtime>(webview: &Webview<R>) {
/// 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<R: Runtime>(
webview: &Webview<R>,
origins: Arc<Mutex<HashMap<String, Url>>>,
) {
let label = webview.label().to_string();
let result = webview.with_webview(move |platform| {
use webkit2gtk::glib::prelude::*;
Expand All @@ -31,15 +40,28 @@ pub(crate) fn allow_media_capture<R: Runtime>(webview: &Webview<R>) {
if let Some(settings) = wv.settings() {
settings.set_enable_media_stream(true);
}
wv.connect_permission_request(move |_, request| {
if request.is::<UserMediaPermissionRequest>() {
log::debug!("granting user-media permission to webview {label}");
request.allow();
true
} else {
wv.connect_permission_request(move |wv, request| {
if !request.is::<UserMediaPermissionRequest>() {
// 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(|| "<unknown>".into()),
origin.map(|o| o.to_string()).unwrap_or_else(|| "<none recorded>".into())
);
request.deny();
}
}
true
});
});
if let Err(e) = result {
Expand Down
Loading