Merge pull request 'WebView permissions only for the app's own origin (#22)' (#26) from webview-permission-origin into main
Build Lotus Chat Desktop / prepare (push) Canceled after 0s
Build Lotus Chat Desktop / build-windows (push) Canceled after 0s
Build Lotus Chat Desktop / build-linux (push) Canceled after 0s
Build Lotus Chat Desktop / build-arch (push) Canceled after 0s
Build Lotus Chat Desktop / update-manifest (push) Canceled after 0s

Merge pull request #26: WebView permissions only for the app's own origin (#22)
This commit was merged in pull request #26.
This commit is contained in:
2026-09-30 20:17:19 -04:00
2 changed files with 326 additions and 39 deletions
+105 -39
View File
@@ -13,6 +13,8 @@ use tauri_plugin_opener::OpenerExt;
pub mod gpu_workarounds;
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 /
/// minimized state. Shared by the tray, single-instance, and deep-link paths.
@@ -1272,41 +1274,76 @@ pub fn run() {
let _ = window_vibrancy::apply_mica(&window, Some(true));
}
// Auto-grant camera, microphone, and notification permissions in WebView2.
#[cfg(target_os = "windows")]
window.with_webview(|webview| {
use webview2_com::{
Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_PERMISSION_KIND,
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
},
PermissionRequestedEventHandler,
};
// cinny-desktop #22: the app's own page gets the microphone, camera
// and notifications without a prompt; other origins (room widgets,
// link-preview embeds) are refused them. See webview_permissions.
#[cfg(any(target_os = "linux", target_os = "windows"))]
let app_origins = webview_permissions::AppOrigins::new(
port,
app.config().build.dev_url.as_ref(),
);
let controller = webview.controller();
if let Ok(core) = unsafe { controller.CoreWebView2() } {
let handler = PermissionRequestedEventHandler::create(Box::new(
|_sender, args| {
if let Some(args) = args {
let mut kind = COREWEBVIEW2_PERMISSION_KIND(0);
unsafe { args.PermissionKind(&mut kind) }?;
if kind == COREWEBVIEW2_PERMISSION_KIND_MICROPHONE
|| kind == COREWEBVIEW2_PERMISSION_KIND_CAMERA
|| kind == COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS
{
unsafe {
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)
}?;
}
}
Ok(())
#[cfg(target_os = "windows")]
window.with_webview({
let app_origins = app_origins.clone();
move |webview| {
use webview2_com::{
Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_PERMISSION_KIND,
COREWEBVIEW2_PERMISSION_KIND_CAMERA,
COREWEBVIEW2_PERMISSION_KIND_GEOLOCATION,
COREWEBVIEW2_PERMISSION_KIND_MICROPHONE,
COREWEBVIEW2_PERMISSION_KIND_NOTIFICATIONS,
COREWEBVIEW2_PERMISSION_STATE_ALLOW,
COREWEBVIEW2_PERMISSION_STATE_DENY,
},
));
let mut token = Default::default();
let _ = unsafe { core.add_PermissionRequested(&handler, &mut token) };
PermissionRequestedEventHandler,
};
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) };
}
}
})?;
@@ -1314,19 +1351,48 @@ pub fn run() {
// default (unlike WebView2/WKWebView), which leaves
// `navigator.mediaDevices` undefined and makes Element Call
// report "browser does not support WebRTC". Turn them on and
// auto-grant the resulting camera/mic permission prompt, mirroring
// the WebView2 handling above.
// answer the permission requests, mirroring the WebView2 handling
// 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")]
window.with_webview(|webview| {
use webkit2gtk::{PermissionRequestExt, SettingsExt, WebViewExt};
window.with_webview(move |webview| {
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();
if let Some(settings) = WebViewExt::settings(&wv) {
settings.set_enable_media_stream(true);
settings.set_enable_webrtc(true);
}
wv.connect_permission_request(|_webview, request| {
request.allow();
wv.connect_permission_request(move |wv, request| {
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
});
})?;