Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e136d3729 | ||
|
|
11e102f7da | ||
|
|
8ee90444ee | ||
|
|
bb5364b53c | ||
|
|
5b4528e4e5 |
+1
-1
Submodule cinny updated: e2b23397bd...be8e49a2bb
@@ -1,17 +1,22 @@
|
|||||||
//! WebKitGTK workarounds for NVIDIA's proprietary driver on Linux.
|
//! WebKitGTK workarounds for NVIDIA's proprietary driver on Linux.
|
||||||
//!
|
//!
|
||||||
//! On NVIDIA + Wayland (reported on CachyOS/KDE, driver 615, webkit2gtk 2.52)
|
//! Diagnosed on CachyOS/KDE Wayland, RTX 3070, driver 615.71.09, webkit2gtk
|
||||||
//! the app never shows a window: GDK dies with "Error 71 (Protocol error)
|
//! 2.52.6 (reported 2026-09-27, WAYLAND_DEBUG trace in the PR):
|
||||||
//! dispatching to Wayland display", and under XWayland WebKit's DMA-BUF
|
//! - Native Wayland: the compositor kills the connection with
|
||||||
//! renderer then fails with "Failed to create GBM buffer … Invalid argument".
|
//! `wp_linux_drm_syncobj_surface_v1 … "explicit sync is used, but no acquire
|
||||||
//! `WEBKIT_DISABLE_DMABUF_RENDERER=1 GDK_BACKEND=x11` makes it run normally.
|
//! point is set"` (GDK: "Error 71 (Protocol error)"), i.e. an explicit-sync
|
||||||
|
//! bug between WebKit and the driver, not a buffer-format problem.
|
||||||
|
//! `__NV_DISABLE_EXPLICIT_SYNC=1` fixes it and keeps WebKit's GPU (DMA-BUF)
|
||||||
|
//! renderer, so that is the default on native Wayland.
|
||||||
|
//! - X11 / XWayland: the DMA-BUF renderer can't allocate or import buffers
|
||||||
|
//! ("Failed to create GBM buffer … Invalid argument", "Failed to import
|
||||||
|
//! DMABuf"), so there it is disabled (`WEBKIT_DISABLE_DMABUF_RENDERER=1`,
|
||||||
|
//! shared-memory frames). We never force X11 any more.
|
||||||
//!
|
//!
|
||||||
//! So, before GTK/WebKit initialise, and only when the NVIDIA driver is loaded:
|
//! Opt-ins / opt-outs (anything the user already set always wins):
|
||||||
//! - `WEBKIT_DISABLE_DMABUF_RENDERER=1`;
|
//! - `LOTUS_GPU_SAFE_MODE=1`: last resort, shared-memory rendering on Wayland
|
||||||
//! - on a Wayland session with XWayland available, `GDK_BACKEND=x11`.
|
//! too (slower, especially at high resolutions).
|
||||||
//!
|
//! - `LOTUS_NO_GPU_WORKAROUNDS=1`: change nothing.
|
||||||
//! Anything the user already set wins, and `LOTUS_NO_GPU_WORKAROUNDS=1`
|
|
||||||
//! disables all of it (e.g. once a newer driver/WebKit fixes this).
|
|
||||||
|
|
||||||
/// Environment variables to set: pure, for tests.
|
/// Environment variables to set: pure, for tests.
|
||||||
pub(crate) fn decide(
|
pub(crate) fn decide(
|
||||||
@@ -22,15 +27,30 @@ pub(crate) fn decide(
|
|||||||
if !nvidia || set("LOTUS_NO_GPU_WORKAROUNDS") {
|
if !nvidia || set("LOTUS_NO_GPU_WORKAROUNDS") {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let mut out = Vec::new();
|
let wayland_session = set("WAYLAND_DISPLAY")
|
||||||
if !set("WEBKIT_DISABLE_DMABUF_RENDERER") {
|
|
||||||
out.push(("WEBKIT_DISABLE_DMABUF_RENDERER", "1"));
|
|
||||||
}
|
|
||||||
let wayland = set("WAYLAND_DISPLAY")
|
|
||||||
|| get("XDG_SESSION_TYPE").is_some_and(|v| v.eq_ignore_ascii_case("wayland"));
|
|| get("XDG_SESSION_TYPE").is_some_and(|v| v.eq_ignore_ascii_case("wayland"));
|
||||||
// Only fall back to X11 when there is an X server (XWayland) to talk to.
|
// GTK uses Wayland unless GDK_BACKEND says otherwise (it may list
|
||||||
if wayland && set("DISPLAY") && !set("GDK_BACKEND") {
|
// several, e.g. "wayland,x11": the first one wins).
|
||||||
out.push(("GDK_BACKEND", "x11"));
|
let native_wayland = wayland_session
|
||||||
|
&& get("GDK_BACKEND").map_or(true, |v| {
|
||||||
|
v.is_empty()
|
||||||
|
|| v.trim_start().to_ascii_lowercase().starts_with("wayland")
|
||||||
|
|| v.trim() == "*"
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut want = |k: &'static str, v: &'static str| {
|
||||||
|
if !set(k) {
|
||||||
|
out.push((k, v));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if native_wayland {
|
||||||
|
want("__NV_DISABLE_EXPLICIT_SYNC", "1");
|
||||||
|
if set("LOTUS_GPU_SAFE_MODE") {
|
||||||
|
want("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
want("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -44,9 +64,20 @@ fn nvidia_driver_loaded() -> bool {
|
|||||||
/// Call first thing in `main`, before anything starts GTK or spawns threads.
|
/// Call first thing in `main`, before anything starts GTK or spawns threads.
|
||||||
pub fn apply() {
|
pub fn apply() {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
for (key, value) in decide(nvidia_driver_loaded(), |k| std::env::var(k).ok()) {
|
{
|
||||||
eprintln!("gpu-workarounds: NVIDIA driver detected, setting {key}={value} (LOTUS_NO_GPU_WORKAROUNDS=1 to disable)");
|
let changes = decide(nvidia_driver_loaded(), |k| std::env::var(k).ok());
|
||||||
std::env::set_var(key, value);
|
if changes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let list: Vec<String> = changes.iter().map(|(k, v)| format!("{k}={v}")).collect();
|
||||||
|
eprintln!(
|
||||||
|
"gpu-workarounds: NVIDIA driver detected, setting {} \
|
||||||
|
(LOTUS_GPU_SAFE_MODE=1 for shared-memory rendering, LOTUS_NO_GPU_WORKAROUNDS=1 to disable)",
|
||||||
|
list.join(" ")
|
||||||
|
);
|
||||||
|
for (key, value) in changes {
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,51 +102,70 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nvidia_wayland_gets_both() {
|
fn nvidia_wayland_keeps_gpu_renderer_and_disables_explicit_sync() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run(true, WAYLAND),
|
run(true, WAYLAND),
|
||||||
vec![
|
vec![("__NV_DISABLE_EXPLICIT_SYNC", "1")]
|
||||||
("WEBKIT_DISABLE_DMABUF_RENDERER", "1"),
|
|
||||||
("GDK_BACKEND", "x11")
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nvidia_x11_session_only_disables_dmabuf() {
|
fn never_forces_x11() {
|
||||||
assert_eq!(
|
for env in [
|
||||||
run(true, &[("DISPLAY", ":0")]),
|
WAYLAND,
|
||||||
vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
|
&[("DISPLAY", ":0")][..],
|
||||||
);
|
&[("WAYLAND_DISPLAY", "w")][..],
|
||||||
}
|
] {
|
||||||
|
assert!(run(true, env).iter().all(|(k, _)| *k != "GDK_BACKEND"));
|
||||||
#[test]
|
}
|
||||||
fn wayland_without_xwayland_keeps_wayland() {
|
|
||||||
assert_eq!(
|
|
||||||
run(true, &[("WAYLAND_DISPLAY", "wayland-0")]),
|
|
||||||
vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn xdg_session_type_counts_as_wayland() {
|
fn xdg_session_type_counts_as_wayland() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run(true, &[("XDG_SESSION_TYPE", "wayland"), ("DISPLAY", ":1")]),
|
run(true, &[("XDG_SESSION_TYPE", "wayland")]),
|
||||||
|
vec![("__NV_DISABLE_EXPLICIT_SYNC", "1")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nvidia_x11_session_disables_dmabuf_renderer() {
|
||||||
|
assert_eq!(
|
||||||
|
run(true, &[("DISPLAY", ":0"), ("XDG_SESSION_TYPE", "x11")]),
|
||||||
|
vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn user_forced_x11_on_wayland_counts_as_x11() {
|
||||||
|
let mut env = WAYLAND.to_vec();
|
||||||
|
env.push(("GDK_BACKEND", "x11"));
|
||||||
|
assert_eq!(
|
||||||
|
run(true, &env),
|
||||||
|
vec![("WEBKIT_DISABLE_DMABUF_RENDERER", "1")]
|
||||||
|
);
|
||||||
|
let mut env = WAYLAND.to_vec();
|
||||||
|
env.push(("GDK_BACKEND", "wayland,x11"));
|
||||||
|
assert_eq!(run(true, &env), vec![("__NV_DISABLE_EXPLICIT_SYNC", "1")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn safe_mode_adds_shared_memory_rendering_on_wayland() {
|
||||||
|
let mut env = WAYLAND.to_vec();
|
||||||
|
env.push(("LOTUS_GPU_SAFE_MODE", "1"));
|
||||||
|
assert_eq!(
|
||||||
|
run(true, &env),
|
||||||
vec![
|
vec![
|
||||||
("WEBKIT_DISABLE_DMABUF_RENDERER", "1"),
|
("__NV_DISABLE_EXPLICIT_SYNC", "1"),
|
||||||
("GDK_BACKEND", "x11")
|
("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn user_settings_win() {
|
fn user_settings_win() {
|
||||||
let env = [
|
let mut env = WAYLAND.to_vec();
|
||||||
("WAYLAND_DISPLAY", "wayland-0"),
|
env.push(("__NV_DISABLE_EXPLICIT_SYNC", "0"));
|
||||||
("DISPLAY", ":0"),
|
|
||||||
("WEBKIT_DISABLE_DMABUF_RENDERER", "0"),
|
|
||||||
("GDK_BACKEND", "wayland"),
|
|
||||||
];
|
|
||||||
assert!(run(true, &env).is_empty());
|
assert!(run(true, &env).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+105
-39
@@ -13,6 +13,8 @@ use tauri_plugin_opener::OpenerExt;
|
|||||||
|
|
||||||
pub mod gpu_workarounds;
|
pub mod gpu_workarounds;
|
||||||
mod native;
|
mod native;
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "windows", test))]
|
||||||
|
mod webview_permissions;
|
||||||
|
|
||||||
/// Bring the main window to the foreground from the tray / a hidden /
|
/// Bring the main window to the foreground from the tray / a hidden /
|
||||||
/// minimized state. Shared by the tray, single-instance, and deep-link paths.
|
/// minimized state. Shared by the tray, single-instance, and deep-link paths.
|
||||||
@@ -1221,41 +1223,76 @@ pub fn run() {
|
|||||||
let _ = window_vibrancy::apply_mica(&window, Some(true));
|
let _ = window_vibrancy::apply_mica(&window, Some(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-grant camera, microphone, and notification permissions in WebView2.
|
// cinny-desktop #22: the app's own page gets the microphone, camera
|
||||||
#[cfg(target_os = "windows")]
|
// and notifications without a prompt; other origins (room widgets,
|
||||||
window.with_webview(|webview| {
|
// link-preview embeds) are refused them. See webview_permissions.
|
||||||
use webview2_com::{
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
Microsoft::Web::WebView2::Win32::{
|
let app_origins = webview_permissions::AppOrigins::new(
|
||||||
COREWEBVIEW2_PERMISSION_KIND,
|
port,
|
||||||
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
|
app.config().build.dev_url.as_ref(),
|
||||||
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
|
);
|
||||||
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
|
|
||||||
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
|
|
||||||
},
|
|
||||||
PermissionRequestedEventHandler,
|
|
||||||
};
|
|
||||||
|
|
||||||
let controller = webview.controller();
|
#[cfg(target_os = "windows")]
|
||||||
if let Ok(core) = unsafe { controller.CoreWebView2() } {
|
window.with_webview({
|
||||||
let handler = PermissionRequestedEventHandler::create(Box::new(
|
let app_origins = app_origins.clone();
|
||||||
|_sender, args| {
|
move |webview| {
|
||||||
if let Some(args) = args {
|
use webview2_com::{
|
||||||
let mut kind = COREWEBVIEW2_PERMISSION_KIND(0);
|
Microsoft::Web::WebView2::Win32::{
|
||||||
unsafe { args.PermissionKind(&mut kind) }?;
|
COREWEBVIEW2_PERMISSION_KIND,
|
||||||
if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
|
||||||
|| kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|
COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION,
|
||||||
|| kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS
|
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
|
||||||
{
|
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
|
||||||
unsafe {
|
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
|
||||||
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
|
COREWEBVIEW2_PERMISSION_STATE_DENY,
|
||||||
}?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
},
|
},
|
||||||
));
|
PermissionRequestedEventHandler,
|
||||||
let mut token = Default::default();
|
};
|
||||||
let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
|
use webview_permissions::{decide, Decision, Kind, WINDOWS_GRANTS};
|
||||||
|
|
||||||
|
let controller = webview.controller();
|
||||||
|
if let Ok(core) = unsafe { controller.CoreWebView2() } {
|
||||||
|
let handler = PermissionRequestedEventHandler::create(Box::new(
|
||||||
|
move |_sender, args| {
|
||||||
|
if let Some(args) = args {
|
||||||
|
let mut raw = COREWEBVIEW2_PERMISSION_KIND(0);
|
||||||
|
unsafe { args.PermissionKind(&mut raw) }?;
|
||||||
|
let kind = if raw == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|
||||||
|
|| raw == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|
||||||
|
{
|
||||||
|
Kind::Media
|
||||||
|
} else if raw == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS {
|
||||||
|
Kind::Notifications
|
||||||
|
} else if raw == COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION {
|
||||||
|
Kind::Geolocation
|
||||||
|
} else {
|
||||||
|
Kind::Other
|
||||||
|
};
|
||||||
|
// The origin of the frame that asked.
|
||||||
|
let mut uri = windows::core::PWSTR::null();
|
||||||
|
unsafe { args.Uri(&mut uri) }?;
|
||||||
|
let uri = webview2_com::take_pwstr(uri);
|
||||||
|
match decide(kind, &uri, &app_origins, WINDOWS_GRANTS) {
|
||||||
|
Decision::Allow => unsafe {
|
||||||
|
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
|
||||||
|
}?,
|
||||||
|
Decision::Deny => {
|
||||||
|
eprintln!(
|
||||||
|
"webview: denied {kind:?} permission to {uri}"
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
args.SetState(COREWEBVIEW2_PERMISSION_STATE_DENY)
|
||||||
|
}?
|
||||||
|
}
|
||||||
|
Decision::Default => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let mut token = Default::default();
|
||||||
|
let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -1263,19 +1300,48 @@ pub fn run() {
|
|||||||
// default (unlike WebView2/WKWebView), which leaves
|
// default (unlike WebView2/WKWebView), which leaves
|
||||||
// `navigator.mediaDevices` undefined and makes Element Call
|
// `navigator.mediaDevices` undefined and makes Element Call
|
||||||
// report "browser does not support WebRTC". Turn them on and
|
// report "browser does not support WebRTC". Turn them on and
|
||||||
// auto-grant the resulting camera/mic permission prompt, mirroring
|
// answer the permission requests, mirroring the WebView2 handling
|
||||||
// the WebView2 handling above.
|
// above. WebKitGTK doesn't say which frame asked, so the origin
|
||||||
|
// checked is the page in the window (see webview_permissions).
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
window.with_webview(|webview| {
|
window.with_webview(move |webview| {
|
||||||
use webkit2gtk::{PermissionRequestExt, SettingsExt, WebViewExt};
|
use webkit2gtk::glib::prelude::ObjectExt;
|
||||||
|
use webkit2gtk::{
|
||||||
|
DeviceInfoPermissionRequest, GeolocationPermissionRequest,
|
||||||
|
NotificationPermissionRequest, PermissionRequestExt, SettingsExt,
|
||||||
|
UserMediaPermissionRequest, WebViewExt,
|
||||||
|
};
|
||||||
|
use webview_permissions::{decide, Decision, Kind, LINUX_GRANTS};
|
||||||
|
|
||||||
let wv = webview.inner();
|
let wv = webview.inner();
|
||||||
if let Some(settings) = WebViewExt::settings(&wv) {
|
if let Some(settings) = WebViewExt::settings(&wv) {
|
||||||
settings.set_enable_media_stream(true);
|
settings.set_enable_media_stream(true);
|
||||||
settings.set_enable_webrtc(true);
|
settings.set_enable_webrtc(true);
|
||||||
}
|
}
|
||||||
wv.connect_permission_request(|_webview, request| {
|
wv.connect_permission_request(move |wv, request| {
|
||||||
request.allow();
|
let kind = if request.is::<UserMediaPermissionRequest>() {
|
||||||
|
Kind::Media
|
||||||
|
} else if request.is::<DeviceInfoPermissionRequest>() {
|
||||||
|
Kind::DeviceInfo
|
||||||
|
} else if request.is::<NotificationPermissionRequest>() {
|
||||||
|
Kind::Notifications
|
||||||
|
} else if request.is::<GeolocationPermissionRequest>() {
|
||||||
|
Kind::Geolocation
|
||||||
|
} else {
|
||||||
|
Kind::Other
|
||||||
|
};
|
||||||
|
let uri = wv.uri().map(|u| u.to_string()).unwrap_or_default();
|
||||||
|
match decide(kind, &uri, &app_origins, LINUX_GRANTS) {
|
||||||
|
Decision::Allow => request.allow(),
|
||||||
|
// No prompt of our own: anything not granted is denied.
|
||||||
|
Decision::Deny | Decision::Default => {
|
||||||
|
eprintln!(
|
||||||
|
"webview: denied {} to {uri}",
|
||||||
|
request.type_().name()
|
||||||
|
);
|
||||||
|
request.deny();
|
||||||
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
//! Which WebView permission requests the app grants (cinny-desktop #22).
|
||||||
|
//!
|
||||||
|
//! The web client asks for the microphone/camera/screen (calls, voice
|
||||||
|
//! messages), the device list (audio-output picker), notifications and the
|
||||||
|
//! location (location sharing). Those are granted without a prompt, but only
|
||||||
|
//! to the app's own origin. Everything else is left alone (Windows: WebView2's
|
||||||
|
//! own prompt) or denied (Linux: WebKitGTK has no prompt of its own).
|
||||||
|
//!
|
||||||
|
//! What "the requesting origin" means differs per engine:
|
||||||
|
//! - WebView2 reports the origin of the frame that asked (`args.Uri()`), so a
|
||||||
|
//! room widget or link-preview embed is refused here.
|
||||||
|
//! - WebKitGTK doesn't say which frame asked; the check is on the page loaded
|
||||||
|
//! in the window. Frames are gated before the request gets this far by the
|
||||||
|
//! Permissions Policy: cinny only puts `microphone; camera` in the `allow=`
|
||||||
|
//! of the call frame (same origin on desktop), and cross-origin frames get
|
||||||
|
//! neither location nor notifications.
|
||||||
|
|
||||||
|
use tauri::Url;
|
||||||
|
|
||||||
|
/// A permission request, reduced to what the policy cares about.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum Kind {
|
||||||
|
/// Microphone, camera or screen capture (getUserMedia/getDisplayMedia).
|
||||||
|
Media,
|
||||||
|
/// Device labels/ids from enumerateDevices (WebKitGTK only).
|
||||||
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
|
DeviceInfo,
|
||||||
|
Notifications,
|
||||||
|
Geolocation,
|
||||||
|
/// Anything else: clipboard read, storage access, pointer lock, DRM, …
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum Decision {
|
||||||
|
Allow,
|
||||||
|
Deny,
|
||||||
|
/// Let the engine decide (WebView2 prompts; WebKitGTK denies).
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the Linux (WebKitGTK) handler grants to the app.
|
||||||
|
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||||
|
pub(crate) const LINUX_GRANTS: &[Kind] = &[
|
||||||
|
Kind::Media,
|
||||||
|
Kind::DeviceInfo,
|
||||||
|
Kind::Notifications,
|
||||||
|
Kind::Geolocation,
|
||||||
|
];
|
||||||
|
/// What the Windows (WebView2) handler grants to the app. Location keeps
|
||||||
|
/// WebView2's own prompt, as before.
|
||||||
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
|
pub(crate) const WINDOWS_GRANTS: &[Kind] = &[Kind::Media, Kind::Notifications];
|
||||||
|
|
||||||
|
/// The decision for a request of `kind` from `uri`.
|
||||||
|
pub(crate) fn decide(kind: Kind, uri: &str, app: &AppOrigins, grants: &[Kind]) -> Decision {
|
||||||
|
if kind == Kind::Other {
|
||||||
|
return Decision::Default;
|
||||||
|
}
|
||||||
|
if !app.contains(uri) {
|
||||||
|
return Decision::Deny;
|
||||||
|
}
|
||||||
|
if grants.contains(&kind) {
|
||||||
|
Decision::Allow
|
||||||
|
} else {
|
||||||
|
Decision::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// scheme, host, port (explicit or the scheme's default).
|
||||||
|
type Origin = (String, String, Option<u16>);
|
||||||
|
|
||||||
|
fn origin_of(uri: &str) -> Option<Origin> {
|
||||||
|
let url = Url::parse(uri).ok()?;
|
||||||
|
let host = url.host_str()?.to_ascii_lowercase();
|
||||||
|
Some((url.scheme().to_owned(), host, url.port_or_known_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The origins the app's own page is served from.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct AppOrigins(Vec<Origin>);
|
||||||
|
|
||||||
|
impl AppOrigins {
|
||||||
|
/// Release builds load `http://localhost:{port}` (tauri-plugin-localhost).
|
||||||
|
/// Debug builds load the bundled page (`tauri://localhost`, or
|
||||||
|
/// `http://tauri.localhost` on Windows) or, under `tauri dev`, `dev_url`.
|
||||||
|
pub(crate) fn new(port: u16, dev_url: Option<&Url>) -> Self {
|
||||||
|
let mut uris = vec![format!("http://localhost:{port}/")];
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
uris.push("tauri://localhost/".into());
|
||||||
|
uris.push("http://tauri.localhost/".into());
|
||||||
|
if let Some(dev) = dev_url {
|
||||||
|
uris.push(dev.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self(uris.iter().filter_map(|u| origin_of(u)).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains(&self, uri: &str) -> bool {
|
||||||
|
origin_of(uri).is_some_and(|o| self.0.contains(&o))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn app() -> AppOrigins {
|
||||||
|
AppOrigins::new(44548, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_origin_matches_only_the_app() {
|
||||||
|
let app = app();
|
||||||
|
assert!(app.contains("http://localhost:44548/"));
|
||||||
|
assert!(app.contains("http://localhost:44548/#/home/!room:server"));
|
||||||
|
assert!(app.contains("http://LOCALHOST:44548/public/element-call/index.html"));
|
||||||
|
for other in [
|
||||||
|
"http://localhost:44549/",
|
||||||
|
"https://localhost:44548/",
|
||||||
|
"http://127.0.0.1:44548/",
|
||||||
|
"http://localhost/",
|
||||||
|
"http://localhost.evil.example:44548/",
|
||||||
|
"http://evil.example/?http://localhost:44548/",
|
||||||
|
"https://www.youtube-nocookie.com/embed/x",
|
||||||
|
"https://chat.lotusguild.org/",
|
||||||
|
"about:blank",
|
||||||
|
"data:text/html,hi",
|
||||||
|
"null",
|
||||||
|
"",
|
||||||
|
] {
|
||||||
|
assert!(!app.contains(other), "{other}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn debug_builds_also_accept_the_bundled_and_dev_pages() {
|
||||||
|
let dev = Url::parse("http://localhost:8080").unwrap();
|
||||||
|
let app = AppOrigins::new(44548, Some(&dev));
|
||||||
|
assert_eq!(
|
||||||
|
app.contains("tauri://localhost/index.html"),
|
||||||
|
cfg!(debug_assertions)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.contains("http://tauri.localhost/"),
|
||||||
|
cfg!(debug_assertions)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.contains("http://localhost:8080/"),
|
||||||
|
cfg!(debug_assertions)
|
||||||
|
);
|
||||||
|
assert!(app.contains("http://localhost:44548/"));
|
||||||
|
assert!(!app.contains("tauri://evil/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn app_gets_its_grants_without_a_prompt() {
|
||||||
|
let app = app();
|
||||||
|
let uri = "http://localhost:44548/";
|
||||||
|
for kind in [
|
||||||
|
Kind::Media,
|
||||||
|
Kind::DeviceInfo,
|
||||||
|
Kind::Notifications,
|
||||||
|
Kind::Geolocation,
|
||||||
|
] {
|
||||||
|
assert_eq!(
|
||||||
|
decide(kind, uri, &app, LINUX_GRANTS),
|
||||||
|
Decision::Allow,
|
||||||
|
"{kind:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
decide(Kind::Media, uri, &app, WINDOWS_GRANTS),
|
||||||
|
Decision::Allow
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
decide(Kind::Notifications, uri, &app, WINDOWS_GRANTS),
|
||||||
|
Decision::Allow
|
||||||
|
);
|
||||||
|
// Location on Windows keeps WebView2's prompt.
|
||||||
|
assert_eq!(
|
||||||
|
decide(Kind::Geolocation, uri, &app, WINDOWS_GRANTS),
|
||||||
|
Decision::Default
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_origins_are_refused() {
|
||||||
|
let app = app();
|
||||||
|
for uri in [
|
||||||
|
"https://widget.example/",
|
||||||
|
"https://www.youtube-nocookie.com/embed/x",
|
||||||
|
"",
|
||||||
|
] {
|
||||||
|
for kind in [
|
||||||
|
Kind::Media,
|
||||||
|
Kind::DeviceInfo,
|
||||||
|
Kind::Notifications,
|
||||||
|
Kind::Geolocation,
|
||||||
|
] {
|
||||||
|
assert_eq!(decide(kind, uri, &app, LINUX_GRANTS), Decision::Deny);
|
||||||
|
assert_eq!(decide(kind, uri, &app, WINDOWS_GRANTS), Decision::Deny);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_kinds_are_left_to_the_engine() {
|
||||||
|
let app = app();
|
||||||
|
for uri in ["http://localhost:44548/", "https://widget.example/"] {
|
||||||
|
assert_eq!(
|
||||||
|
decide(Kind::Other, uri, &app, LINUX_GRANTS),
|
||||||
|
Decision::Default
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
decide(Kind::Other, uri, &app, WINDOWS_GRANTS),
|
||||||
|
Decision::Default
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user