Enabled WebKit permission handling for Linux audio capture (#854)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-07-13 17:15:55 +02:00 committed by GitHub
parent 50d7b56ac0
commit 5dc6282908
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 114 additions and 1 deletions

View File

@ -1,7 +1,6 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@using AIStudio.Tools.Rust
@using AIStudio.Tools.Services
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="@T("App Options")">

View File

@ -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.

1
runtime/Cargo.lock generated
View File

@ -4042,6 +4042,7 @@ dependencies = [
"tempfile",
"tokio",
"tokio-stream",
"webkit2gtk",
"whoami",
"windows-native-keyring-store",
"windows-registry",

View File

@ -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"

View File

@ -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<Mutex<Option<WebviewWindow>>> = 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::<UserMediaPermissionRequest>() 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<R: tauri::Runtime>(webview: &tauri::Webview<R>, 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(&current), 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(&current), 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(&current), 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));
}
}