use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::time::Instant;
use tauri_runtime::{
window::{PendingWindow, WindowBuilder},
ExitRequestedEventAction, RunEvent, Runtime as _, RuntimeHandle as _, WindowDispatch as _,
};
use tauri_runtime_wry::{WindowBuilderWrapper, Wry, WryHandle, WryWindowDispatcher};
use url::Url;
use wry::{NewWindowResponse, PageLoadEvent, WebView, WebViewBuilder};
#[cfg(target_os = "linux")]
#[path = "tauri/linux_webkitgtk.rs"]
mod linux_webkitgtk;
#[cfg(feature = "wasm-sketch-host")]
pub(crate) mod sketch;
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
use wry::raw_window_handle::{HandleError, HasWindowHandle, WindowHandle};
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
use wry::WebViewBuilderExtUnix as _;
#[cfg(target_os = "linux")]
use wry::WebViewExtUnix as _;
use crate::async_engine::{self, OneshotReceiver, OneshotSender, RuntimeHandle};
use crate::operations::{HubError, OpaqueToken, OperationHub, Terminal};
pub(crate) mod capture;
#[cfg(feature = "tauri-webview-test-support")]
mod trace;
pub use capture::{ViewportCaptureLimits, WebviewSnapshot, WebviewSnapshotChunk};
#[cfg(feature = "tauri-webview-test-support")]
pub use trace::WebviewTestTraceEvent;
static NEXT_LABEL: AtomicU64 = AtomicU64::new(1);
static NEXT_WEBVIEW_STORE: AtomicU64 = AtomicU64::new(1);
thread_local! {
static UI_WEBVIEWS: RefCell<HashMap<u64, WebView>> = RefCell::new(HashMap::new());
}
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
#[derive(Clone)]
struct NativeWindowHandle(WryWindowDispatcher<()>);
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
impl HasWindowHandle for NativeWindowHandle {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
self.0.window_handle()
}
}
#[allow(dead_code)] type PinnedTauriEventLoopMessage = tauri::EventLoopMessage;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(crate) enum NativeWebviewError {
#[error("webview URL is malformed or not an HTTP(S) URL")]
InvalidUrl,
#[error("webview navigation was rejected because {0}")]
RejectedNavigation(String),
#[error("the webview window was closed before its requested page loaded")]
WindowClosed,
#[error("the native webview host failed: {0}")]
HostFailure(String),
}
#[derive(Clone, Debug)]
pub(crate) struct NativeWebviewRequest {
url: Url,
permissions: WebviewPermissions,
window: Option<WebviewWindowOptions>,
bootstrap: Option<WebviewPageBootstrap>,
}
impl NativeWebviewRequest {
pub(crate) fn parse(
url: &str,
permissions: WebviewPermissions,
) -> Result<Self, NativeWebviewError> {
let Some((_, authority_and_path)) = url.split_once("://") else {
return Err(NativeWebviewError::InvalidUrl);
};
if authority_and_path.is_empty() || authority_and_path.starts_with('/') {
return Err(NativeWebviewError::InvalidUrl);
}
let url = Url::parse(url).map_err(|_| NativeWebviewError::InvalidUrl)?;
if is_allowed_url(&url) {
Ok(Self {
url,
permissions,
window: None,
bootstrap: None,
})
} else {
Err(NativeWebviewError::InvalidUrl)
}
}
}
pub(crate) struct NativeWebviewLoop {
runtime: Wry<()>,
}
#[derive(Clone)]
pub(crate) struct NativeWebviewBackend {
async_runtime: RuntimeHandle,
wry: WryHandle<()>,
}
impl NativeWebviewLoop {
pub(crate) fn new(
async_runtime: RuntimeHandle,
) -> Result<(Self, NativeWebviewBackend), NativeWebviewError> {
let runtime = Wry::new(Default::default())
.map_err(|error| NativeWebviewError::HostFailure(error.to_string()))?;
let backend = NativeWebviewBackend {
async_runtime,
wry: runtime.handle(),
};
Ok((Self { runtime }, backend))
}
pub(crate) fn run(self) -> i32 {
self.runtime.run_return(|event| {
if let RunEvent::ExitRequested { code: None, tx, .. } = event {
let _ = tx.send(ExitRequestedEventAction::Prevent);
}
})
}
}
impl NativeWebviewBackend {
pub(crate) fn request_exit(&self) -> Result<(), NativeWebviewError> {
self.wry
.request_exit(0)
.map_err(|error| NativeWebviewError::HostFailure(error.to_string()))
}
pub(crate) async fn open(
&self,
request: NativeWebviewRequest,
lease: crate::operations::NativeOpenLease,
) -> Result<NativeWebview, NativeWebviewError> {
let (created_sender, created_receiver) = async_engine::oneshot_channel();
let backend = self.clone();
self.async_runtime
.launch_blocking(move || backend.create_on_wry_thread(request, created_sender, lease))
.detach();
created_receiver.await.map_err(|_| {
NativeWebviewError::HostFailure("event loop stopped during creation".into())
})?
}
fn create_on_wry_thread(
&self,
request: NativeWebviewRequest,
created_sender: OneshotSender<Result<NativeWebview, NativeWebviewError>>,
lease: crate::operations::NativeOpenLease,
) {
let (completion, load_waiter) = LoadCompletion::new(request.url.clone());
let (terminal, terminal_waiter) = TerminalCompletion::new();
let (closed, close_waiter) = CloseCompletion::new();
let created_sender = Arc::new(Mutex::new(Some(created_sender)));
let native_id = NEXT_LABEL.fetch_add(1, Ordering::Relaxed);
let label = format!("kernal-api-webview-{native_id}");
let window_builder = match request.window.as_ref() {
Some(options) => WindowBuilderWrapper::new()
.title(options.title())
.inner_size(f64::from(options.width), f64::from(options.height)),
None => WindowBuilderWrapper::new().title("kernal-api external-content proof"),
};
let pending_window = match PendingWindow::<(), Wry<()>>::new(window_builder, label) {
Ok(window) => window,
Err(error) => {
let _ = created_sender
.lock()
.expect("creation sender lock poisoned")
.take()
.expect("creation sender is present")
.send(Err(NativeWebviewError::HostFailure(error.to_string())));
return;
}
};
let detached = match self.wry.create_window(
pending_window,
None::<for<'a> fn(tauri_runtime::window::RawWindow<'a>)>,
) {
Ok(window) => window,
Err(error) => {
let _ = created_sender
.lock()
.expect("creation sender lock poisoned")
.take()
.expect("creation sender is present")
.send(Err(NativeWebviewError::HostFailure(error.to_string())));
return;
}
};
let dispatcher = detached.dispatcher;
debug_assert!(detached.webview.is_none());
let completion_on_close = Arc::clone(&completion);
let terminal_on_close = Arc::clone(&terminal);
let closed_on_close = Arc::clone(&closed);
dispatcher.on_window_event(move |event| {
if matches!(event, tauri_runtime::window::WindowEvent::Destroyed) {
capture::cancel_for_view(native_id);
let removed = UI_WEBVIEWS.with(|webviews| webviews.borrow_mut().remove(&native_id));
drop(removed);
completion_on_close.finish(Err(NativeWebviewError::WindowClosed));
terminal_on_close.finish(Err(NativeWebviewError::WindowClosed));
closed_on_close.finish();
}
});
let bootstrap_source = request.bootstrap.as_ref().map(|script| {
script.for_origin(&request.url, dispatcher.scale_factor().unwrap_or(1.0))
});
let window_for_ui = dispatcher.clone();
let completion_for_ui = Arc::clone(&completion);
let terminal_for_ui = Arc::clone(&terminal);
let created_sender_for_ui = Arc::clone(&created_sender);
if let Err(error) = dispatcher.run_on_main_thread(move || {
let _lease = lease;
let result = build_isolated_webview(
&window_for_ui,
request.url,
request.permissions,
bootstrap_source,
completion_for_ui,
terminal_for_ui,
);
let created_sender = created_sender_for_ui
.lock()
.expect("creation sender lock poisoned")
.take();
match (result, created_sender) {
(Ok(webview), Some(created_sender)) if !created_sender.is_closed() => {
UI_WEBVIEWS.with(|webviews| {
webviews.borrow_mut().insert(native_id, webview);
});
let _ = created_sender.send(Ok(NativeWebview {
window: window_for_ui,
native_id,
completion,
load_waiter: Some(load_waiter),
terminal_waiter: Some(terminal_waiter),
close_waiter: Some(close_waiter),
close_requested: AtomicBool::new(false),
}));
}
(Ok(webview), _) => {
drop(webview);
let _ = window_for_ui.close();
}
(Err(error), Some(created_sender)) => {
let _ = window_for_ui.close();
let _ = created_sender.send(Err(error));
}
(Err(_), None) => {
let _ = window_for_ui.close();
}
}
}) {
let _ = dispatcher.close();
if let Some(created_sender) = created_sender
.lock()
.expect("creation sender lock poisoned")
.take()
{
let _ =
created_sender.send(Err(NativeWebviewError::HostFailure(error.to_string())));
}
}
}
}
pub(crate) struct NativeWebview {
window: WryWindowDispatcher<()>,
native_id: u64,
completion: Arc<LoadCompletion>,
load_waiter: Option<OneshotReceiver<Result<Instant, NativeWebviewError>>>,
terminal_waiter: Option<OneshotReceiver<Result<(), NativeWebviewError>>>,
close_waiter: Option<OneshotReceiver<()>>,
close_requested: AtomicBool,
}
impl NativeWebview {
pub(crate) fn wait_until_loaded(
&mut self,
) -> Result<OneshotReceiver<Result<Instant, NativeWebviewError>>, NativeWebviewError> {
self.load_waiter
.take()
.ok_or_else(|| NativeWebviewError::HostFailure("load waiter already consumed".into()))
}
pub(crate) fn wait_until_terminal(
&mut self,
) -> Result<OneshotReceiver<Result<(), NativeWebviewError>>, NativeWebviewError> {
self.terminal_waiter.take().ok_or_else(|| {
NativeWebviewError::HostFailure("terminal waiter already consumed".into())
})
}
pub(crate) fn wait_until_closed(&mut self) -> Result<OneshotReceiver<()>, NativeWebviewError> {
self.close_waiter
.take()
.ok_or_else(|| NativeWebviewError::HostFailure("close waiter already consumed".into()))
}
pub(crate) fn close(&self) -> Result<(), NativeWebviewError> {
if self
.close_requested
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
if let Err(error) = self.window.close() {
self.close_requested.store(false, Ordering::Release);
let error = NativeWebviewError::HostFailure(error.to_string());
self.completion.finish(Err(error.clone()));
return Err(error);
}
let native_id = self.native_id;
let _ = self.window.run_on_main_thread(move || {
capture::cancel_for_view(native_id);
let removed = UI_WEBVIEWS.with(|webviews| webviews.borrow_mut().remove(&native_id));
drop(removed);
});
self.completion
.finish(Err(NativeWebviewError::WindowClosed));
}
Ok(())
}
}
impl Drop for NativeWebview {
fn drop(&mut self) {
let _ = self.close();
}
}
struct LoadCompletion {
target: Url,
sender: Mutex<Option<OneshotSender<Result<Instant, NativeWebviewError>>>>,
}
struct TerminalCompletion {
sender: Mutex<Option<OneshotSender<Result<(), NativeWebviewError>>>>,
}
impl TerminalCompletion {
fn new() -> (Arc<Self>, OneshotReceiver<Result<(), NativeWebviewError>>) {
let (sender, receiver) = async_engine::oneshot_channel();
(
Arc::new(Self {
sender: Mutex::new(Some(sender)),
}),
receiver,
)
}
fn finish(&self, result: Result<(), NativeWebviewError>) {
let sender = self
.sender
.lock()
.expect("terminal completion lock poisoned")
.take();
if let Some(sender) = sender {
let _ = sender.send(result);
}
}
}
struct CloseCompletion {
sender: Mutex<Option<OneshotSender<()>>>,
}
impl CloseCompletion {
fn new() -> (Arc<Self>, OneshotReceiver<()>) {
let (sender, receiver) = async_engine::oneshot_channel();
(
Arc::new(Self {
sender: Mutex::new(Some(sender)),
}),
receiver,
)
}
fn finish(&self) {
let sender = self
.sender
.lock()
.expect("close completion lock poisoned")
.take();
if let Some(sender) = sender {
let _ = sender.send(());
}
}
}
impl LoadCompletion {
fn new(
target: Url,
) -> (
Arc<Self>,
OneshotReceiver<Result<Instant, NativeWebviewError>>,
) {
let (sender, receiver) = async_engine::oneshot_channel();
(
Arc::new(Self {
target,
sender: Mutex::new(Some(sender)),
}),
receiver,
)
}
fn finish(&self, result: Result<(), NativeWebviewError>) {
let result = result.map(|()| Instant::now());
let sender = self
.sender
.lock()
.expect("load completion lock poisoned")
.take();
if let Some(sender) = sender {
let _ = sender.send(result);
}
}
fn matches_requested(&self, loaded: &Url) -> bool {
self.target == *loaded
}
}
fn build_isolated_webview(
dispatcher: &WryWindowDispatcher<()>,
target: Url,
permissions: WebviewPermissions,
bootstrap_source: Option<String>,
completion: Arc<LoadCompletion>,
terminal: Arc<TerminalCompletion>,
) -> Result<WebView, NativeWebviewError> {
#[cfg(not(target_os = "linux"))]
let _ = permissions;
let completion_for_navigation = Arc::clone(&completion);
let terminal_for_navigation = Arc::clone(&terminal);
let completion_for_popup = Arc::clone(&completion);
let terminal_for_popup = Arc::clone(&terminal);
let completion_for_load = Arc::clone(&completion);
let bootstrap_origin = bootstrap_source.as_ref().map(|_| target.origin());
let builder = WebViewBuilder::new()
.with_incognito(true)
.with_clipboard(false)
.with_devtools(false)
.with_general_autofill_enabled(false)
.with_navigation_handler(move |url| match Url::parse(&url) {
Ok(url) if navigation_allowed(&url, bootstrap_origin.as_ref()) => true,
Ok(url) => {
let reason = if is_allowed_url(&url) {
"cross-origin bootstrap navigation"
} else {
url.scheme()
};
let error = NativeWebviewError::RejectedNavigation(reason.to_owned());
completion_for_navigation.finish(Err(error.clone()));
terminal_for_navigation.finish(Err(error));
false
}
Err(_) => {
let error = NativeWebviewError::RejectedNavigation("malformed URL".into());
completion_for_navigation.finish(Err(error.clone()));
terminal_for_navigation.finish(Err(error));
false
}
})
.with_new_window_req_handler(move |url, _| {
let scheme = Url::parse(&url)
.map(|url| url.scheme().to_owned())
.unwrap_or_else(|_| "malformed URL".into());
let error = NativeWebviewError::RejectedNavigation(format!("popup to {scheme}"));
completion_for_popup.finish(Err(error.clone()));
terminal_for_popup.finish(Err(error));
NewWindowResponse::Deny
})
.with_on_page_load_handler(move |event, loaded_url| {
if matches!(event, PageLoadEvent::Finished)
&& Url::parse(&loaded_url)
.is_ok_and(|loaded| completion_for_load.matches_requested(&loaded))
{
completion_for_load.finish(Ok(()));
}
})
.with_download_started_handler(|_, _| false);
#[cfg(not(target_os = "linux"))]
let builder = if let Some(source) = bootstrap_source.as_ref() {
builder.with_initialization_script_for_main_only(source.clone(), true)
} else {
builder
};
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
{
linux_webkitgtk::ensure_font_dpi();
let webview = builder
.build_gtk(&dispatcher.default_vbox().map_err(host_failure)?)
.map_err(host_failure)?;
linux_webkitgtk::remove_host_bridge(&webview.webview())?;
if let Some(source) = bootstrap_source.as_ref() {
linux_webkitgtk::install_page_bootstrap(&webview.webview(), source)?;
}
linux_webkitgtk::configure_permissions(&webview.webview(), permissions);
webview.load_url(target.as_str()).map_err(host_failure)?;
Ok(webview)
}
#[cfg(not(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
{
let webview = builder
.build(&NativeWindowHandle(dispatcher.clone()))
.map_err(host_failure)?;
webview.load_url(target.as_str()).map_err(host_failure)?;
Ok(webview)
}
}
fn host_failure(error: impl std::fmt::Display) -> NativeWebviewError {
NativeWebviewError::HostFailure(error.to_string())
}
fn is_allowed_url(url: &Url) -> bool {
matches!(url.scheme(), "http" | "https")
&& url.host_str().is_some()
&& !url.cannot_be_a_base()
&& url.username().is_empty()
&& url.password().is_none()
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum WebviewError {
#[error("the native viewport capture queue is full")]
CaptureBusy,
#[error("viewport capture exceeds its pixel limit or has invalid dimensions")]
CapturePixelLimit,
#[error("viewport capture exceeds its encoded-byte or blob quota")]
CaptureByteLimit,
#[error("native viewport capture or PNG encoding failed")]
CaptureFailed,
#[error("webview URL is malformed or not an HTTP(S) URL")]
InvalidUrl,
#[error("webview navigation was rejected: {0}")]
RejectedNavigation(String),
#[error("webview load timed out")]
TimedOut,
#[error("webview operation was cancelled")]
Cancelled,
#[error("the webview window was closed")]
WindowClosed,
#[error("the webview host failed: {0}")]
HostFailure(String),
#[error("a terminal webview wait is already active")]
TerminalWaitInProgress,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct WebviewPermissions {
pub(crate) allow_user_media: bool,
}
impl WebviewPermissions {
pub const fn deny_all() -> Self {
Self {
allow_user_media: false,
}
}
pub const fn allow_user_media(mut self) -> Self {
self.allow_user_media = true;
self
}
}
fn navigation_allowed(url: &Url, required_origin: Option<&url::Origin>) -> bool {
is_allowed_url(url) && required_origin.is_none_or(|origin| *origin == url.origin())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum PageBootstrapError {
#[error("page bootstrap exceeds 65536 UTF-8 bytes")]
SourceTooLarge,
#[error("page bootstrap contains a NUL character")]
ContainsNul,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebviewPageBootstrap {
source: String,
}
impl WebviewPageBootstrap {
pub fn new(source: &str) -> Result<Self, PageBootstrapError> {
if source.len() > 65536 {
return Err(PageBootstrapError::SourceTooLarge);
}
if source.contains('\0') {
return Err(PageBootstrapError::ContainsNul);
}
Ok(Self {
source: source.to_owned(),
})
}
pub fn source(&self) -> &str {
&self.source
}
fn for_origin(&self, target: &Url, native_scale: f64) -> String {
let scale = if native_scale.is_finite() && native_scale > 0.0 {
native_scale
} else {
1.0
};
format!(
"if (window === window.top && location.origin === \"{}\") {{\nconst kernalWindow = Object.freeze({{ initialScaleFactor: {scale} }});\n{}\n}}\n",
target.origin().ascii_serialization().escape_default(),
self.source
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum WindowOptionsError {
#[error("webview title exceeds 1024 UTF-8 bytes or contains a control character")]
InvalidTitle,
#[error("webview logical width and height must each be between 1 and 16384")]
InvalidSize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WebviewWindowOptions {
title: String,
width: u32,
height: u32,
}
impl WebviewWindowOptions {
pub fn new(title: &str, width: u32, height: u32) -> Result<Self, WindowOptionsError> {
if title.len() > 1024 || title.chars().any(char::is_control) {
return Err(WindowOptionsError::InvalidTitle);
}
if !(1..=16384).contains(&width) || !(1..=16384).contains(&height) {
return Err(WindowOptionsError::InvalidSize);
}
Ok(Self {
title: title.to_owned(),
width,
height,
})
}
pub fn title(&self) -> &str {
&self.title
}
pub const fn logical_size(&self) -> (u32, u32) {
(self.width, self.height)
}
}
pub struct ExternalWebviewHost {
event_loop: NativeWebviewLoop,
client: ExternalWebviewClient,
}
#[derive(Clone)]
pub struct ExternalWebviewClient {
service: Arc<WebviewService>,
store: u64,
}
#[derive(Clone)]
pub struct WebviewUrlGrant {
url: Arc<str>,
}
impl WebviewUrlGrant {
#[cfg(feature = "wasm-sketch-worker")]
pub(crate) fn worker_url(&self) -> &str {
&self.url
}
pub fn new(url: &str) -> Result<Self, WebviewError> {
if url.len() > crate::operations::MAX_WEBVIEW_URL_BYTES {
return Err(WebviewError::InvalidUrl);
}
let request =
NativeWebviewRequest::parse(url, WebviewPermissions::deny_all()).map_err(map_native)?;
if request.url.as_str().len() > crate::operations::MAX_WEBVIEW_URL_BYTES {
return Err(WebviewError::InvalidUrl);
}
Ok(Self {
url: Arc::from(request.url.as_str()),
})
}
pub(crate) fn bind(&self, hub: &OperationHub, store: u64) -> Result<OpaqueToken, HubError> {
hub.grant_webview_url(store, Arc::clone(&self.url))
}
}
pub struct WebviewHandle {
service: Arc<WebviewService>,
store: u64,
resource: OpaqueToken,
terminal_operation: OpaqueToken,
terminal_wait_active: AtomicBool,
}
struct TerminalWaitGuard<'a>(&'a AtomicBool);
impl Drop for TerminalWaitGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
#[cfg(feature = "tauri-webview-test-support")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WebviewTestObservation {
pub active_clocks: usize,
pub active_output_jobs: usize,
pub active_native_captures: usize,
pub active_native_opens: usize,
pub live_blobs: usize,
pub retained_transfer_capacity: usize,
pub native_backings: usize,
pub live_resources: usize,
pub pending_operations: usize,
}
struct WebviewService {
#[cfg(feature = "tauri-webview-test-support")]
trace: Arc<trace::Recorder>,
runtime: RuntimeHandle,
backend: NativeWebviewBackend,
hub: Arc<OperationHub>,
native: Mutex<BTreeMap<OpaqueToken, NativeWebview>>,
closing: Mutex<BTreeSet<OpaqueToken>>,
}
struct PendingWebviewOpen {
service: Arc<WebviewService>,
store: u64,
resource: OpaqueToken,
operation: OpaqueToken,
transferred: bool,
}
struct PendingUrlGrant {
hub: Arc<OperationHub>,
resource: OpaqueToken,
}
impl Drop for PendingUrlGrant {
fn drop(&mut self) {
let _ = self.hub.close_resource(self.resource);
}
}
impl Drop for PendingWebviewOpen {
fn drop(&mut self) {
if !self.transferred {
self.service
.hub
.finish_external_operation(self.operation, Terminal::Cancelled);
let _ = self
.service
.hub
.observe_terminal(self.store, self.operation);
self.service
.revoke_with_terminal(self.resource, Terminal::Cancelled);
}
}
}
impl ExternalWebviewHost {
pub fn new(runtime: RuntimeHandle) -> Result<Self, WebviewError> {
let (event_loop, backend) = NativeWebviewLoop::new(runtime.clone()).map_err(map_native)?;
let hub = OperationHub::new(64, 64).map_err(map_hub)?;
Ok(Self {
event_loop,
client: ExternalWebviewClient {
service: Arc::new(WebviewService {
#[cfg(feature = "tauri-webview-test-support")]
trace: Arc::new(trace::Recorder::new()),
runtime,
backend,
hub,
native: Mutex::new(BTreeMap::new()),
closing: Mutex::new(BTreeSet::new()),
}),
store: next_store()?,
},
})
}
pub fn client(&self) -> ExternalWebviewClient {
self.client.clone()
}
pub fn run(self) -> i32 {
self.event_loop.run()
}
}
impl ExternalWebviewClient {
#[cfg(feature = "tauri-webview-test-support")]
pub fn test_trace(&self) -> (Vec<WebviewTestTraceEvent>, usize) {
self.service.trace.snapshot()
}
pub fn new_instance(&self) -> Result<Self, WebviewError> {
Ok(Self {
service: Arc::clone(&self.service),
store: next_store()?,
})
}
pub async fn open_webview(&self, url: &str) -> Result<WebviewHandle, WebviewError> {
let grant = WebviewUrlGrant::new(url)?;
self.open_granted_webview(&grant).await
}
pub async fn open_webview_with_permissions(
&self,
url: &str,
permissions: WebviewPermissions,
) -> Result<WebviewHandle, WebviewError> {
let grant = WebviewUrlGrant::new(url)?;
self.open_granted_with_permissions(&grant, permissions)
.await
}
pub async fn open_granted_webview(
&self,
grant: &WebviewUrlGrant,
) -> Result<WebviewHandle, WebviewError> {
self.open_granted_with_permissions(grant, WebviewPermissions::deny_all())
.await
}
async fn open_granted_with_permissions(
&self,
grant: &WebviewUrlGrant,
permissions: WebviewPermissions,
) -> Result<WebviewHandle, WebviewError> {
self.open_granted_with_request(grant, permissions, None, None)
.await
}
pub async fn open_webview_with_options(
&self,
url: &str,
window: WebviewWindowOptions,
permissions: WebviewPermissions,
) -> Result<WebviewHandle, WebviewError> {
let grant = WebviewUrlGrant::new(url)?;
self.open_granted_with_request(&grant, permissions, Some(window), None)
.await
}
pub async fn open_webview_with_bootstrap(
&self,
url: &str,
window: WebviewWindowOptions,
permissions: WebviewPermissions,
bootstrap: WebviewPageBootstrap,
) -> Result<WebviewHandle, WebviewError> {
let preview = NativeWebviewRequest::parse(url, permissions).map_err(map_native)?;
if preview.url.origin().ascii_serialization().len() > 4096 {
return Err(WebviewError::InvalidUrl);
}
let grant = WebviewUrlGrant::new(url)?;
self.open_granted_with_request(&grant, permissions, Some(window), Some(bootstrap))
.await
}
async fn open_granted_with_request(
&self,
grant: &WebviewUrlGrant,
permissions: WebviewPermissions,
window: Option<WebviewWindowOptions>,
bootstrap: Option<WebviewPageBootstrap>,
) -> Result<WebviewHandle, WebviewError> {
let grant = PendingUrlGrant {
hub: Arc::clone(&self.service.hub),
resource: grant.bind(&self.service.hub, self.store).map_err(map_hub)?,
};
let (resource, operation, url) = self
.service
.hub
.begin_granted_webview_open(self.store, grant.resource)
.map_err(map_hub)?;
let mut pending = PendingWebviewOpen {
service: Arc::clone(&self.service),
store: self.store,
resource,
operation,
transferred: false,
};
let mut request = NativeWebviewRequest::parse(&url, permissions).map_err(map_native)?;
request.window = window;
request.bootstrap = bootstrap;
let lease = self.service.hub.acquire_native_open().map_err(map_hub)?;
let mut native = match self.service.backend.open(request, lease).await {
Ok(native) => native,
Err(error) => {
self.service
.hub
.finish_external_operation(operation, terminal_for_native(&error));
self.service.revoke(resource);
return Err(map_native(error));
}
};
let terminal = native.wait_until_terminal().map_err(map_native)?;
self.service
.native
.lock()
.map_err(|_| WebviewError::HostFailure("native backing table poisoned".into()))?
.insert(resource, native);
if !self.service.hub.finish_external_open(operation, resource) {
self.service.revoke(resource);
}
let service = Arc::clone(&self.service);
self.service
.runtime
.launch(async move {
let terminal = match terminal.await {
Ok(Ok(())) => return,
Ok(Err(error)) => terminal_for_native(&error),
Err(_) => Terminal::Closed,
};
if !service.is_explicitly_closing(resource) {
service.revoke_with_terminal(resource, terminal);
}
})
.detach();
match self.service.hub.observe_terminal(self.store, operation) {
Ok(Some(result)) if result.terminal == Terminal::Completed => {
let terminal_operation = self
.service
.hub
.begin_external_webview_wait(self.store, resource)
.map_err(map_hub)?;
pending.transferred = true;
Ok(WebviewHandle {
service: Arc::clone(&self.service),
store: self.store,
resource,
terminal_operation,
terminal_wait_active: AtomicBool::new(false),
})
}
Ok(Some(result)) => Err(map_terminal(result.terminal)),
Ok(None) => Err(WebviewError::HostFailure(
"webview open did not complete".into(),
)),
Err(error) => Err(map_hub(error)),
}
}
pub fn request_exit(&self) -> Result<(), WebviewError> {
self.service.backend.request_exit().map_err(map_native)
}
#[cfg(feature = "tauri-webview-test-support")]
pub fn test_observation(&self) -> WebviewTestObservation {
let snapshot = self.service.hub.snapshot();
let native_backings = self.service.native.lock().map_or(0, |native| native.len());
WebviewTestObservation {
active_clocks: snapshot.active_clocks,
active_output_jobs: snapshot.active_output_jobs,
active_native_captures: snapshot.active_native_captures,
active_native_opens: snapshot.active_native_opens,
live_blobs: snapshot.live_blobs,
retained_transfer_capacity: snapshot.retained_transfer_capacity,
native_backings,
live_resources: snapshot.live_resources,
pending_operations: snapshot.pending_operations,
}
}
}
impl WebviewHandle {
#[cfg(feature = "tauri-webview-test-support")]
pub fn verify_window_options_for_test(
&self,
expected: &WebviewWindowOptions,
) -> Result<(), WebviewError> {
let window = self
.service
.native
.lock()
.map_err(|_| WebviewError::HostFailure("native backing table poisoned".into()))?
.get(&self.resource)
.ok_or(WebviewError::WindowClosed)?
.window
.clone();
let host_error = |error: tauri_runtime::Error| WebviewError::HostFailure(error.to_string());
let title = window.title().map_err(host_error)?;
let size = window.inner_size().map_err(host_error)?;
let scale = window.scale_factor().map_err(host_error)?;
if !scale.is_finite()
|| scale <= 0.0
|| title != expected.title
|| (f64::from(size.width) / scale - f64::from(expected.width)).abs() > 1.0
|| (f64::from(size.height) / scale - f64::from(expected.height)).abs() > 1.0
{
return Err(WebviewError::HostFailure(format!(
"window presentation mismatch: title={title:?}, physical_size={size:?}, scale={scale}, expected={expected:?}"
)));
}
Ok(())
}
pub async fn wait_until_loaded(&self, timeout: Duration) -> Result<(), WebviewError> {
let operation = self
.service
.hub
.begin_external_webview_wait(self.store, self.resource)
.map_err(map_hub)?;
let receiver = {
let mut native =
self.service.native.lock().map_err(|_| {
WebviewError::HostFailure("native backing table poisoned".into())
})?;
native
.get_mut(&self.resource)
.ok_or(WebviewError::WindowClosed)?
.wait_until_loaded()
.map_err(map_native)?
};
let terminal = match async_engine::timeout(timeout, receiver).await {
Ok(Ok(Ok(_loaded_at))) => Terminal::Completed,
Ok(Ok(Err(error))) => terminal_for_native(&error),
Ok(Err(_)) => Terminal::Closed,
Err(_) => Terminal::TimedOut,
};
self.service
.hub
.finish_external_operation(operation, terminal);
if terminal != Terminal::Completed {
self.service.revoke_with_terminal(self.resource, terminal);
}
match self.service.hub.observe_terminal(self.store, operation) {
Ok(Some(result)) if result.terminal == Terminal::Completed => Ok(()),
Ok(Some(result)) => Err(map_terminal(result.terminal)),
Ok(None) => Err(WebviewError::HostFailure(
"load operation did not complete".into(),
)),
Err(error) => Err(map_hub(error)),
}
}
pub async fn wait_until_terminal(&self, timeout: Duration) -> Result<(), WebviewError> {
self.wait_terminal(Some(timeout)).await
}
pub async fn wait_for_terminal(&self) -> Result<(), WebviewError> {
self.wait_terminal(None).await
}
async fn wait_terminal(&self, timeout: Option<Duration>) -> Result<(), WebviewError> {
self.terminal_wait_active
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.map_err(|_| WebviewError::TerminalWaitInProgress)?;
let _admission = TerminalWaitGuard(&self.terminal_wait_active);
if let Some(result) = self
.service
.hub
.observe_terminal(self.store, self.terminal_operation)
.map_err(map_hub)?
{
return Err(map_terminal(result.terminal));
}
match self
.service
.hub
.wait_external_operation(self.store, self.terminal_operation)
{
Ok(wake) => {
if let Some(timeout) = timeout {
if async_engine::timeout(timeout, wake.notified())
.await
.is_err()
{
self.service
.revoke_with_terminal(self.resource, Terminal::TimedOut);
}
} else {
wake.notified().await;
}
}
Err(HubError::Closed) => {}
Err(error) => return Err(map_hub(error)),
}
match self
.service
.hub
.observe_terminal(self.store, self.terminal_operation)
{
Ok(Some(result)) => Err(map_terminal(result.terminal)),
Ok(None) => Err(WebviewError::HostFailure(
"terminal webview operation did not complete".into(),
)),
Err(error) => Err(map_hub(error)),
}
}
pub async fn close(self) -> Result<(), WebviewError> {
let operation = self
.service
.hub
.begin_external_webview_close(self.store, self.resource)
.map_err(map_hub)?;
self.service.mark_explicitly_closing(self.resource);
let mut native = self.service.take_native(self.resource).ok_or_else(|| {
self.service.clear_explicitly_closing(self.resource);
WebviewError::WindowClosed
})?;
let closed = match native.wait_until_closed() {
Ok(closed) => closed,
Err(error) => {
self.service
.hub
.finish_external_operation(operation, terminal_for_native(&error));
self.service
.revoke_with_terminal(self.resource, terminal_for_native(&error));
self.service.clear_explicitly_closing(self.resource);
return Err(map_native(error));
}
};
if let Err(error) = native.close() {
self.service
.hub
.finish_external_operation(operation, terminal_for_native(&error));
self.service
.revoke_with_terminal(self.resource, terminal_for_native(&error));
self.service.clear_explicitly_closing(self.resource);
return Err(map_native(error));
}
let terminal = match closed.await {
Ok(()) => Terminal::Completed,
Err(_) => Terminal::Closed,
};
self.service
.hub
.finish_external_operation(operation, terminal);
let outcome = match self.service.hub.observe_terminal(self.store, operation) {
Ok(Some(result)) if result.terminal == Terminal::Completed => Ok(()),
Ok(Some(result)) => Err(map_terminal(result.terminal)),
Ok(None) => Err(WebviewError::HostFailure(
"close operation did not complete".into(),
)),
Err(error) => Err(map_hub(error)),
};
let _ = self.service.hub.close_resource(self.resource);
self.service.clear_explicitly_closing(self.resource);
outcome
}
pub fn cancel(&self) {
self.service
.revoke_with_terminal(self.resource, Terminal::Cancelled);
}
#[cfg(feature = "tauri-webview-test-support")]
pub fn request_window_close_for_test(&self) -> Result<(), WebviewError> {
let native = self
.service
.native
.lock()
.map_err(|_| WebviewError::HostFailure("native backing table poisoned".into()))?;
native
.get(&self.resource)
.ok_or(WebviewError::WindowClosed)?
.close()
.map_err(map_native)
}
}
impl Drop for WebviewHandle {
fn drop(&mut self) {
self.service
.revoke_with_terminal(self.resource, Terminal::Cancelled);
}
}
impl WebviewService {
fn take_native(&self, resource: OpaqueToken) -> Option<NativeWebview> {
self.native.lock().ok()?.remove(&resource)
}
fn revoke(&self, resource: OpaqueToken) {
self.revoke_with_terminal(resource, Terminal::Closed);
}
fn revoke_with_terminal(&self, resource: OpaqueToken, terminal: Terminal) {
let _ = self.hub.revoke_external_resource(resource, terminal);
if let Some(native) = self.take_native(resource) {
let _ = native.close();
drop(native);
}
}
fn mark_explicitly_closing(&self, resource: OpaqueToken) {
if let Ok(mut closing) = self.closing.lock() {
closing.insert(resource);
}
}
fn clear_explicitly_closing(&self, resource: OpaqueToken) {
if let Ok(mut closing) = self.closing.lock() {
closing.remove(&resource);
}
}
fn is_explicitly_closing(&self, resource: OpaqueToken) -> bool {
self.closing
.lock()
.is_ok_and(|closing| closing.contains(&resource))
}
}
fn next_store() -> Result<u64, WebviewError> {
NEXT_WEBVIEW_STORE
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
value.checked_add(1)
})
.map(|value| value + 1)
.map_err(|_| WebviewError::HostFailure("webview instance identifiers exhausted".into()))
}
fn terminal_for_native(error: &NativeWebviewError) -> Terminal {
match error {
NativeWebviewError::RejectedNavigation(_) | NativeWebviewError::InvalidUrl => {
Terminal::Rejected
}
NativeWebviewError::WindowClosed => Terminal::Closed,
NativeWebviewError::HostFailure(_) => Terminal::Trapped,
}
}
fn map_native(error: NativeWebviewError) -> WebviewError {
match error {
NativeWebviewError::InvalidUrl => WebviewError::InvalidUrl,
NativeWebviewError::RejectedNavigation(reason) => WebviewError::RejectedNavigation(reason),
NativeWebviewError::WindowClosed => WebviewError::WindowClosed,
NativeWebviewError::HostFailure(reason) => WebviewError::HostFailure(reason),
}
}
fn map_terminal(terminal: Terminal) -> WebviewError {
match terminal {
Terminal::Cancelled => WebviewError::Cancelled,
Terminal::TimedOut => WebviewError::TimedOut,
Terminal::Closed => WebviewError::WindowClosed,
Terminal::Rejected => WebviewError::RejectedNavigation("navigation policy".into()),
Terminal::Completed => WebviewError::HostFailure("unexpected completed error".into()),
Terminal::Trapped | Terminal::OwnerExited => {
WebviewError::HostFailure("native webview operation failed".into())
}
}
}
fn map_hub(error: HubError) -> WebviewError {
match error {
HubError::Quota => WebviewError::HostFailure("webview operation quota exhausted".into()),
HubError::Closed | HubError::Stale => WebviewError::WindowClosed,
HubError::Invalid | HubError::WrongKind | HubError::WrongRights | HubError::Exhausted => {
WebviewError::HostFailure("invalid semantic webview operation".into())
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn bootstrap_exposes_native_scale_snapshot_before_caller_source() {
let target = Url::parse("http://127.0.0.1:8080/").unwrap();
let bootstrap =
WebviewPageBootstrap::new("window.scale = kernalWindow.initialScaleFactor;").unwrap();
for scale in [1.0, 1.25, 2.0] {
let wrapped = bootstrap.for_origin(&target, scale);
assert!(wrapped.contains(&format!("const kernalWindow = Object.freeze({{ initialScaleFactor: {scale} }});\nwindow.scale")));
}
for invalid in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let wrapped = bootstrap.for_origin(&target, invalid);
assert!(wrapped.contains("initialScaleFactor: 1 }"));
}
}
use super::*;
#[test]
fn prevalidated_url_grant_is_bounded_and_rejects_ambient_authority() {
for url in [
"file:///etc/passwd",
"data:text/html,x",
"javascript:alert(1)",
"tauri://localhost",
"https:///bad",
"https://user@example.test/",
] {
assert!(matches!(
WebviewUrlGrant::new(url),
Err(WebviewError::InvalidUrl)
));
}
let huge = format!(
"https://example.test/{}",
"a".repeat(crate::operations::MAX_WEBVIEW_URL_BYTES)
);
assert!(matches!(
WebviewUrlGrant::new(&huge),
Err(WebviewError::InvalidUrl)
));
let grant = WebviewUrlGrant::new("HTTPS://EXAMPLE.TEST:443/exact?q=1").unwrap();
let hub = OperationHub::new(4, 4).unwrap();
let token = grant.bind(&hub, 7).unwrap();
let (_, _, url) = hub.begin_granted_webview_open(7, token).unwrap();
assert_eq!(&*url, "https://example.test/exact?q=1");
hub.close_all(Terminal::Cancelled);
assert_eq!(hub.snapshot().live_resources, 0);
}
#[test]
fn bootstrap_navigation_is_origin_scoped_without_changing_default_route() {
let target = Url::parse("http://127.0.0.1:8080/start").unwrap();
for allowed in [
"http://127.0.0.1:8080/reload",
"http://127.0.0.1:8080/start#fragment",
] {
assert!(navigation_allowed(
&Url::parse(allowed).unwrap(),
Some(&target.origin())
));
}
for rejected in [
"http://localhost:8080/",
"http://127.0.0.1:8081/",
"https://127.0.0.1:8080/",
] {
let url = Url::parse(rejected).unwrap();
assert!(!navigation_allowed(&url, Some(&target.origin())));
assert!(navigation_allowed(&url, None));
}
assert!(!navigation_allowed(
&Url::parse("file:///tmp/test").unwrap(),
None
));
let bootstrap = WebviewPageBootstrap::new("window.marker = 1; // comment").unwrap();
let wrapped = bootstrap.for_origin(&target, 1.0);
assert!(wrapped.starts_with(
"if (window === window.top && location.origin === \"http://127.0.0.1:8080\") {\n"
));
assert!(wrapped.ends_with("// comment\n}\n"));
}
#[test]
fn external_url_policy_admits_loopback_and_refuses_ambient_schemes() {
assert!(NativeWebviewRequest::parse(
"http://127.0.0.1:8080/page",
WebviewPermissions::default(),
)
.is_ok());
assert!(NativeWebviewRequest::parse(
"https://example.test/",
WebviewPermissions::default(),
)
.is_ok());
for forbidden in [
"file:///etc/passwd",
"data:text/html,hello",
"tauri://localhost",
"javascript:alert(1)",
"https:///missing-host",
"https://user@example.test/",
] {
assert_eq!(
NativeWebviewRequest::parse(forbidden, WebviewPermissions::default()).unwrap_err(),
NativeWebviewError::InvalidUrl,
"must reject {forbidden}",
);
}
}
#[test]
fn user_media_permission_is_explicitly_opt_in() {
assert_eq!(
WebviewPermissions::default(),
WebviewPermissions::deny_all()
);
assert_ne!(
WebviewPermissions::deny_all(),
WebviewPermissions::deny_all().allow_user_media()
);
}
}