Files
buzz/desktop/src-tauri/src/linux_media.rs
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

147 lines
5.9 KiB
Rust

//! Linux-only: enable media capture (`getUserMedia`) in the WebKitGTK webview.
//!
//! On macOS (WKWebView) and Windows (WebView2) the media-permission prompt is
//! routed to the OS automatically, so microphone/camera capture "just works".
//! WebKitGTK is different on two counts, and both must be handled or capture
//! fails on Linux only:
//!
//! * `enable-media-stream` is **off by default**, so `navigator.mediaDevices`
//! never exposes a working `getUserMedia`; and
//! * the default `permission-request` handler **denies every request**, so even
//! with media-stream on, the call rejects with `NotAllowedError`.
//!
//! This module reaches the underlying `webkit2gtk::WebView` via
//! [`tauri::Webview::with_webview`], enables media-stream, and installs a
//! `permission-request` handler that is **deny-by-default**: a `UserMedia`
//! request is allowed only when it comes from a trusted app origin and asks for
//! an audio and/or video device. Tauri does not restrict navigation by default,
//! so without the origin check any document that ended up in this webview would
//! inherit silent mic/camera access for the process lifetime.
//!
//! Buzz's AppImage pins `GDK_BACKEND=x11` (see [`crate::webkit_rendering`]),
//! which is the backend WebKitGTK media capture is reliable on.
/// The origin Tauri serves the packaged app from on Linux.
/// Consumed only by linux-gated [`enable_media_capture`]; kept compiling on all
/// platforms so the unit tests run everywhere.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const PROD_ORIGIN: &str = "tauri://localhost";
/// The Vite dev-server origin (`devUrl` in `tauri.conf.json`, `strictPort`
/// 1420 in `vite.config.ts`). Only trusted in debug builds.
#[cfg(debug_assertions)]
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const DEV_ORIGIN: &str = "http://localhost:1420";
/// Whether `uri` (the webview's current document URI) is a trusted app origin
/// allowed to use mic/camera. Matches the origin exactly or as a path prefix so
/// `tauri://localhost.evil.com` and `http://localhost:14200` do not slip
/// through. Pure and platform-independent so it can be unit-tested everywhere.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn is_trusted_media_origin(uri: &str) -> bool {
fn matches(uri: &str, origin: &str) -> bool {
uri == origin
|| uri
.strip_prefix(origin)
.is_some_and(|rest| rest.starts_with('/'))
}
if matches(uri, PROD_ORIGIN) {
return true;
}
#[cfg(debug_assertions)]
if matches(uri, DEV_ORIGIN) {
return true;
}
false
}
/// Enable microphone/camera capture for `webview` if it is running on
/// WebKitGTK. A no-op on every non-Linux target, so callers can invoke it
/// unconditionally from shared startup code.
#[cfg(target_os = "linux")]
pub fn enable_media_capture<R: tauri::Runtime>(webview: &tauri::Webview<R>) {
use webkit2gtk::{
glib::prelude::Cast, PermissionRequestExt, SettingsExt, UserMediaPermissionRequest,
UserMediaPermissionRequestExt, WebViewExt,
};
// `with_webview` runs the closure on the UI thread, which GTK calls
// require. It errors only if the platform webview is unavailable.
let result = webview.with_webview(|platform_webview| {
// On Linux this is the underlying `webkit2gtk::WebView`.
let webview = platform_webview.inner();
if let Some(settings) = WebViewExt::settings(&webview) {
settings.set_enable_media_stream(true);
}
// Deny-by-default: allow only mic/camera requests from a trusted app
// origin; deny everything else (still returning `true` so WebKit's
// auto-deny default does not also run). Non-`UserMedia` requests return
// `false` and keep their default handling.
webview.connect_permission_request(|wv, request| {
let Some(request) = request.downcast_ref::<UserMediaPermissionRequest>() else {
return false;
};
let uri = wv.uri().map(|u| u.to_string()).unwrap_or_default();
let for_device = request.is_for_audio_device() || request.is_for_video_device();
if for_device && is_trusted_media_origin(&uri) {
request.allow();
} else {
request.deny();
}
true
});
});
if let Err(error) = result {
eprintln!("buzz-desktop: could not enable WebKitGTK media capture: {error}");
}
}
/// No-op stub so shared startup code can call [`enable_media_capture`] on every
/// platform. macOS and Windows route media permissions through the OS.
#[cfg(not(target_os = "linux"))]
pub fn enable_media_capture<R: tauri::Runtime>(_webview: &tauri::Webview<R>) {}
#[cfg(test)]
mod tests {
use super::is_trusted_media_origin;
#[test]
fn allows_production_app_origin() {
assert!(is_trusted_media_origin("tauri://localhost"));
assert!(is_trusted_media_origin(
"tauri://localhost/channels/general"
));
}
#[test]
fn denies_untrusted_origins() {
assert!(!is_trusted_media_origin(""));
assert!(!is_trusted_media_origin("https://evil.example.com"));
// Prefix look-alikes must not slip through.
assert!(!is_trusted_media_origin("tauri://localhost.evil.com"));
assert!(!is_trusted_media_origin("tauri://localhostfoo"));
}
#[cfg(debug_assertions)]
#[test]
fn allows_dev_origin_in_debug_only() {
assert!(is_trusted_media_origin("http://localhost:1420"));
assert!(is_trusted_media_origin("http://localhost:1420/"));
// A different localhost port is still untrusted.
assert!(!is_trusted_media_origin("http://localhost:14200"));
assert!(!is_trusted_media_origin("http://localhost:3000"));
}
#[cfg(not(debug_assertions))]
#[test]
fn denies_dev_origin_in_release() {
assert!(!is_trusted_media_origin("http://localhost:1420"));
}
}