From 5dc62829082bc021a3c6344de984995584ecd868 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 13 Jul 2026 17:15:55 +0200 Subject: [PATCH] Enabled WebKit permission handling for Linux audio capture (#854) --- .../Settings/SettingsPanelApp.razor | 1 - .../wwwroot/changelog/v26.7.3.md | 1 + runtime/Cargo.lock | 1 + runtime/Cargo.toml | 1 + runtime/src/app_window.rs | 111 ++++++++++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index d26a810e..cb8ab7b5 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -1,7 +1,6 @@ @using AIStudio.Settings @using AIStudio.Settings.DataModel @using AIStudio.Tools.Rust -@using AIStudio.Tools.Services @inherits SettingsPanelBase diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 79e37a5c..346091e4 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -4,6 +4,7 @@ - Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department. - Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. - Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. +- Fixed voice recording not starting on Linux. - Upgraded Rust to v1.97.0. - Upgraded Tauri to v2.11.5. - Upgraded common dependencies. \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index fde1cf0e..f0956ce7 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -4042,6 +4042,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", + "webkit2gtk", "whoami", "windows-native-keyring-store", "windows-registry", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 29554c35..ec61e3d2 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -69,6 +69,7 @@ apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] } [target.'cfg(target_os = "linux")'.dependencies] dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rust"] } +webkit2gtk = { version = "2.0.2", features = ["v2_8"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 7db9925e..fdac344d 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -30,9 +30,16 @@ use crate::environment::{ use crate::log::switch_to_file_logging; use crate::pdfium::PDFIUM_LIB_PATH; use crate::qdrant_edge_database::{start_qdrant_edge_database, stop_qdrant_edge_database}; + #[cfg(debug_assertions)] use crate::dotnet::create_startup_env_file; +#[cfg(target_os = "linux")] +use webkit2gtk::glib::Cast; + +#[cfg(target_os = "linux")] +use webkit2gtk::{PermissionRequestExt, UserMediaPermissionRequestExt}; + /// The Tauri main window. pub static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -138,6 +145,9 @@ pub fn start_tauri() { // Get the main window: let window = app.get_webview_window("main").expect("Failed to get main window."); + #[cfg(target_os = "linux")] + register_linux_permission_request_handler(&window); + // Register a callback for window events, such as file drops. We have to use // this handler in addition to the app event handler, because file drop events // are only available in the window event handler (is a bug, cf. https://github.com/tauri-apps/tauri/issues/14338): @@ -247,6 +257,69 @@ fn same_origin(left: &tauri::Url, right: &tauri::Url) -> bool { && left.port_or_known_default() == right.port_or_known_default() } +#[cfg(any(target_os = "linux", test))] +fn should_allow_audio_capture( + approved_app_url: Option<&tauri::Url>, + current_webview_url: Option<&tauri::Url>, + requests_audio: bool, + requests_video: bool, +) -> bool { + requests_audio + && !requests_video + && approved_app_url.is_some_and(is_local_http_url) + && approved_app_url + .zip(current_webview_url) + .is_some_and(|(approved, current)| same_origin(approved, current)) +} + +#[cfg(target_os = "linux")] +fn register_linux_permission_request_handler(window: &WebviewWindow) { + if let Err(error) = window.with_webview(|platform_webview| { + use webkit2gtk::WebViewExt; + use webkit2gtk::UserMediaPermissionRequest; + + let webview = platform_webview.inner(); + webview.connect_permission_request(|webview, request| { + let Some(user_media_request) = request.downcast_ref::() else { + request.deny(); + info!(Source = "Tauri"; "Denied a non-user-media WebKit permission request."); + return true; + }; + + let current_webview_url = webview + .uri() + .and_then(|uri| tauri::Url::parse(uri.as_str()).ok()); + let approved_app_url = APPROVED_APP_URL.lock().unwrap().clone(); + let origin_matches = approved_app_url + .as_ref() + .zip(current_webview_url.as_ref()) + .is_some_and(|(approved, current)| same_origin(approved, current)); + let requests_audio = user_media_request.is_for_audio_device(); + let requests_video = user_media_request.is_for_video_device(); + let allow = should_allow_audio_capture( + approved_app_url.as_ref(), + current_webview_url.as_ref(), + requests_audio, + requests_video, + ); + + if allow { + request.allow(); + } else { + request.deny(); + } + + info!( + Source = "Tauri"; + "Handled WebKit user-media permission request: allowed={allow}, origin_matches={origin_matches}, audio={requests_audio}, video={requests_video}." + ); + true + }); + }) { + error!(Source = "Tauri"; "Failed to register the Linux WebKit permission request handler: {error}"); + } +} + fn should_open_in_system_browser(webview: &tauri::Webview, url: &tauri::Url) -> bool { match url.scheme() { "mailto" | "tel" => return true, @@ -1139,4 +1212,42 @@ mod tests { assert!(!is_tauri_asset_url(&url)); assert!(!is_local_http_url(&url)); } + + #[test] + fn audio_capture_is_allowed_for_exact_approved_app_origin() { + let approved = tauri::Url::parse("http://localhost:12345/").unwrap(); + let current = tauri::Url::parse("http://localhost:12345/voice-recorder").unwrap(); + + assert!(should_allow_audio_capture(Some(&approved), Some(¤t), true, false)); + } + + #[test] + fn audio_capture_is_denied_for_wrong_port() { + let approved = tauri::Url::parse("http://localhost:12345/").unwrap(); + let current = tauri::Url::parse("http://localhost:54321/").unwrap(); + + assert!(!should_allow_audio_capture(Some(&approved), Some(¤t), true, false)); + } + + #[test] + fn audio_capture_is_denied_for_external_origin() { + let approved = tauri::Url::parse("http://localhost:12345/").unwrap(); + let current = tauri::Url::parse("https://example.com/").unwrap(); + + assert!(!should_allow_audio_capture(Some(&approved), Some(¤t), true, false)); + } + + #[test] + fn video_capture_is_denied() { + let approved = tauri::Url::parse("http://localhost:12345/").unwrap(); + + assert!(!should_allow_audio_capture(Some(&approved), Some(&approved), false, true)); + } + + #[test] + fn combined_audio_and_video_capture_is_denied() { + let approved = tauri::Url::parse("http://localhost:12345/").unwrap(); + + assert!(!should_allow_audio_capture(Some(&approved), Some(&approved), true, true)); + } }