From 3b13fda56f515c708014c0396ed5ca295faaef84 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sun, 6 Mar 2022 10:15:43 -0300 Subject: [PATCH] feat(core): add `WindowBuilder::on_request`, closes #3533 (#3618) Co-authored-by: Jonas Kruckenberg --- .changes/window-request-handler.md | 5 ++ core/tauri-runtime-wry/src/lib.rs | 33 ++++++-- core/tauri-runtime/src/http/request.rs | 19 ++++- core/tauri-runtime/src/http/response.rs | 53 ++++++++++-- core/tauri-runtime/src/lib.rs | 2 +- core/tauri/src/app.rs | 23 +++-- core/tauri/src/lib.rs | 42 +-------- core/tauri/src/manager.rs | 54 ++++++++++-- core/tauri/src/test/mock_runtime.rs | 1 + core/tauri/src/window.rs | 108 ++++++++++++++++++++++-- 10 files changed, 256 insertions(+), 84 deletions(-) create mode 100644 .changes/window-request-handler.md diff --git a/.changes/window-request-handler.md b/.changes/window-request-handler.md new file mode 100644 index 000000000..0b5d7fe2e --- /dev/null +++ b/.changes/window-request-handler.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Added `WindowBuilder::on_web_resource_request`, which allows customizing the tauri custom protocol response. diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index b994ab379..1caad6682 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -219,10 +219,10 @@ struct HttpRequestWrapper(HttpRequest); impl From<&WryHttpRequest> for HttpRequestWrapper { fn from(req: &WryHttpRequest) -> Self { - Self(HttpRequest { - body: req.body.clone(), - head: HttpRequestPartsWrapper::from(req.head.clone()).0, - }) + Self(HttpRequest::new_internal( + HttpRequestPartsWrapper::from(req.head.clone()).0, + req.body.clone(), + )) } } @@ -242,9 +242,10 @@ impl From for HttpResponsePartsWrapper { struct HttpResponseWrapper(WryHttpResponse); impl From for HttpResponseWrapper { fn from(response: HttpResponse) -> Self { + let (parts, body) = response.into_parts(); Self(WryHttpResponse { - body: response.body, - head: HttpResponsePartsWrapper::from(response.head).0, + body, + head: HttpResponsePartsWrapper::from(parts).0, }) } } @@ -1466,6 +1467,26 @@ pub struct Wry { tray_context: TrayContext, } +impl fmt::Debug for Wry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("Wry"); + d.field("main_thread_id", &self.main_thread_id) + .field("global_shortcut_manager", &self.global_shortcut_manager) + .field( + "global_shortcut_manager_handle", + &self.global_shortcut_manager_handle, + ) + .field("clipboard_manager", &self.clipboard_manager) + .field("clipboard_manager_handle", &self.clipboard_manager_handle) + .field("event_loop", &self.event_loop) + .field("windows", &self.windows) + .field("web_context", &self.web_context); + #[cfg(feature = "system-tray")] + d.field("tray_context", &self.tray_context); + d.finish() + } +} + /// A handle to the Wry runtime. #[derive(Debug, Clone)] pub struct WryHandle { diff --git a/core/tauri-runtime/src/http/request.rs b/core/tauri-runtime/src/http/request.rs index 40ccd5629..594e7c10b 100644 --- a/core/tauri-runtime/src/http/request.rs +++ b/core/tauri-runtime/src/http/request.rs @@ -17,8 +17,8 @@ use super::{ /// /// - **Linux:** Headers are not exposed. pub struct Request { - pub head: RequestParts, - pub body: Vec, + head: RequestParts, + body: Vec, } /// Component parts of an HTTP `Request` @@ -47,6 +47,17 @@ impl Request { } } + /// Creates a new `Request` with the given head and body. + /// + /// # Stability + /// + /// This API is used internally. It may have breaking changes in the future. + #[inline] + #[doc(hidden)] + pub fn new_internal(head: RequestParts, body: Vec) -> Request { + Request { head, body } + } + /// Returns a reference to the associated HTTP method. #[inline] pub fn method(&self) -> &Method { @@ -72,6 +83,10 @@ impl Request { } /// Consumes the request returning the head and body RequestParts. + /// + /// # Stability + /// + /// This API is used internally. It may have breaking changes in the future. #[inline] pub fn into_parts(self) -> (RequestParts, Vec) { (self.head, self.body) diff --git a/core/tauri-runtime/src/http/response.rs b/core/tauri-runtime/src/http/response.rs index 7e2549834..0f18bc833 100644 --- a/core/tauri-runtime/src/http/response.rs +++ b/core/tauri-runtime/src/http/response.rs @@ -32,8 +32,8 @@ type Result = core::result::Result>; /// ``` /// pub struct Response { - pub head: ResponseParts, - pub body: Vec, + head: ResponseParts, + body: Vec, } /// Component parts of an HTTP `Response` @@ -42,16 +42,16 @@ pub struct Response { /// header fields. #[derive(Clone)] pub struct ResponseParts { - /// The response's status + /// The response's status. pub status: StatusCode, - /// The response's version + /// The response's version. pub version: Version, - /// The response's headers + /// The response's headers. pub headers: HeaderMap, - /// The response's mimetype type + /// The response's mimetype type. pub mimetype: Option, } @@ -74,16 +74,39 @@ impl Response { } } - /// Returns the `StatusCode`. + /// Consumes the response returning the head and body ResponseParts. + /// + /// # Stability + /// + /// This API is used internally. It may have breaking changes in the future. + #[inline] + #[doc(hidden)] + pub fn into_parts(self) -> (ResponseParts, Vec) { + (self.head, self.body) + } + + /// Sets the status code. + #[inline] + pub fn set_status(&mut self, status: StatusCode) { + self.head.status = status; + } + + /// Returns the [`StatusCode`]. #[inline] pub fn status(&self) -> StatusCode { self.head.status } + /// Sets the mimetype. + #[inline] + pub fn set_mimetype(&mut self, mimetype: Option) { + self.head.mimetype = mimetype; + } + /// Returns a reference to the mime type. #[inline] - pub fn mimetype(&self) -> Option { - self.head.mimetype.clone() + pub fn mimetype(&self) -> Option<&String> { + self.head.mimetype.as_ref() } /// Returns a reference to the associated version. @@ -92,12 +115,24 @@ impl Response { self.head.version } + /// Returns a mutable reference to the associated header field map. + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.head.headers + } + /// Returns a reference to the associated header field map. #[inline] pub fn headers(&self) -> &HeaderMap { &self.head.headers } + /// Returns a mutable reference to the associated HTTP body. + #[inline] + pub fn body_mut(&mut self) -> &mut Vec { + &mut self.body + } + /// Returns a reference to the associated HTTP body. #[inline] pub fn body(&self) -> &Vec { diff --git a/core/tauri-runtime/src/lib.rs b/core/tauri-runtime/src/lib.rs index f46f635f4..954155b70 100644 --- a/core/tauri-runtime/src/lib.rs +++ b/core/tauri-runtime/src/lib.rs @@ -311,7 +311,7 @@ pub trait ClipboardManager: Debug + Clone + Send + Sync { } /// The webview runtime interface. -pub trait Runtime: Sized + 'static { +pub trait Runtime: Debug + Sized + 'static { /// The message dispatcher. type Dispatcher: Dispatch; /// The runtime handle type. diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 70e1110c5..5c566f102 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -23,6 +23,7 @@ use crate::{ sealed::{ManagerBase, RuntimeOrDispatch}, utils::config::{Config, WindowUrl}, utils::{assets::Assets, Env}, + window::WindowBuilder, Context, Invoke, InvokeError, InvokeResponse, Manager, Scopes, StateManager, Window, }; @@ -386,15 +387,12 @@ macro_rules! shared_app_impl { WebviewAttributes, ), { - let (window_builder, webview_attributes) = setup( - ::WindowBuilder::new(), - WebviewAttributes::new(url), - ); - self.create_new_window(PendingWindow::new( - window_builder, - webview_attributes, - label, - )?) + let mut builder = WindowBuilder::::new(self, label, url); + let (window_builder, webview_attributes) = + setup(builder.window_builder, builder.webview_attributes); + builder.window_builder = window_builder; + builder.webview_attributes = webview_attributes; + builder.build() } #[cfg(feature = "system-tray")] @@ -1310,9 +1308,10 @@ impl Builder { let mut main_window = None; for pending in self.pending_windows { - let pending = app - .manager - .prepare_window(app.handle.clone(), pending, &window_labels)?; + let pending = + app + .manager + .prepare_window(app.handle.clone(), pending, &window_labels, None)?; let detached = app.runtime.as_ref().unwrap().create_window(pending)?; let _window = app.manager.attach_window(app.handle(), detached); #[cfg(feature = "updater")] diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index 29433fbb8..ccec5cd3a 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -174,7 +174,6 @@ pub type Result = std::result::Result; /// A task to run on the main thread. pub type SyncTask = Box; -use crate::runtime::window::PendingWindow; use serde::Serialize; use std::{collections::HashMap, fmt, sync::Arc}; @@ -600,7 +599,7 @@ pub trait Manager: sealed::ManagerBase { /// Prevent implementation details from leaking out of the [`Manager`] trait. pub(crate) mod sealed { use crate::{app::AppHandle, manager::WindowManager}; - use tauri_runtime::{Runtime, RuntimeHandle}; + use tauri_runtime::Runtime; /// A running [`Runtime`] or a dispatcher to it. pub enum RuntimeOrDispatch<'r, R: Runtime> { @@ -614,51 +613,12 @@ pub(crate) mod sealed { Dispatch(R::Dispatcher), } - #[derive(Clone, serde::Serialize)] - struct WindowCreatedEvent { - label: String, - } - /// Managed handle to the application runtime. pub trait ManagerBase { /// The manager behind the [`Managed`] item. fn manager(&self) -> &WindowManager; - fn runtime(&self) -> RuntimeOrDispatch<'_, R>; fn managed_app_handle(&self) -> AppHandle; - - /// Creates a new [`Window`] on the [`Runtime`] and attaches it to the [`Manager`]. - fn create_new_window( - &self, - pending: crate::PendingWindow, - ) -> crate::Result> { - use crate::runtime::Dispatch; - let labels = self.manager().labels().into_iter().collect::>(); - let pending = self - .manager() - .prepare_window(self.managed_app_handle(), pending, &labels)?; - let window = match self.runtime() { - RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending), - RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending), - RuntimeOrDispatch::Dispatch(mut dispatcher) => dispatcher.create_window(pending), - } - .map(|window| { - self - .manager() - .attach_window(self.managed_app_handle(), window) - })?; - - self.manager().emit_filter( - "tauri://window-created", - None, - Some(WindowCreatedEvent { - label: window.label().into(), - }), - |w| w != &window, - )?; - - Ok(window) - } } } diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index cea1968d3..0ef887b30 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -129,11 +129,16 @@ fn set_csp( let csp = Csp::DirectiveMap(csp).to_string(); #[cfg(target_os = "linux")] { - *asset = asset.replacen(tauri_utils::html::CSP_TOKEN, &csp, 1); + *asset = set_html_csp(asset, &csp); } csp } +#[cfg(target_os = "linux")] +fn set_html_csp(html: &str, csp: &str) -> String { + html.replacen(tauri_utils::html::CSP_TOKEN, csp, 1) +} + // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297 fn replace_with_callback String>( original: &str, @@ -383,6 +388,9 @@ impl WindowManager { label: &str, window_labels: &[String], app_handle: AppHandle, + web_resource_request_handler: Option< + Box, + >, ) -> crate::Result> { let is_init_global = self.inner.config.build.with_global_tauri; let plugin_init = self @@ -470,7 +478,10 @@ impl WindowManager { } if !registered_scheme_protocols.contains(&"tauri".into()) { - pending.register_uri_scheme_protocol("tauri", self.prepare_uri_scheme_protocol()); + pending.register_uri_scheme_protocol( + "tauri", + self.prepare_uri_scheme_protocol(web_resource_request_handler), + ); registered_scheme_protocols.push("tauri".into()); } @@ -788,6 +799,9 @@ impl WindowManager { #[allow(clippy::type_complexity)] fn prepare_uri_scheme_protocol( &self, + web_resource_request_handler: Option< + Box, + >, ) -> Box Result> + Send + Sync> { let manager = self.clone(); @@ -801,11 +815,28 @@ impl WindowManager { .to_string() .replace("tauri://localhost", ""); let asset = manager.get_asset(path)?; - let mut response = HttpResponseBuilder::new().mimetype(&asset.mime_type); - if let Some(csp) = asset.csp_header { - response = response.header("Content-Security-Policy", csp); + let mut builder = HttpResponseBuilder::new().mimetype(&asset.mime_type); + if let Some(csp) = &asset.csp_header { + builder = builder.header("Content-Security-Policy", csp); } - response.body(asset.bytes) + let mut response = builder.body(asset.bytes)?; + if let Some(handler) = &web_resource_request_handler { + handler(request, &mut response); + + // if it's an HTML file, we need to set the CSP meta tag on Linux + #[cfg(target_os = "linux")] + if let (Some(original_csp), Some(response_csp)) = ( + asset.csp_header, + response.headers().get("Content-Security_Policy"), + ) { + let response_csp = String::from_utf8_lossy(response_csp.as_bytes()); + if response_csp != original_csp { + let body = set_html_csp(&String::from_utf8_lossy(response.body()), &response_csp); + *response.body_mut() = body.as_bytes().to_vec(); + } + } + } + Ok(response) }) } @@ -990,6 +1021,9 @@ impl WindowManager { app_handle: AppHandle, mut pending: PendingWindow, window_labels: &[String], + web_resource_request_handler: Option< + Box, + >, ) -> crate::Result> { if self.windows_lock().contains_key(&pending.label) { return Err(crate::Error::WindowLabelAlreadyExists(pending.label)); @@ -1043,7 +1077,13 @@ impl WindowManager { if is_local { let label = pending.label.clone(); - pending = self.prepare_pending_window(pending, &label, window_labels, app_handle.clone())?; + pending = self.prepare_pending_window( + pending, + &label, + window_labels, + app_handle.clone(), + web_resource_request_handler, + )?; pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle.clone())); } diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index b5f7e1c52..f3fb6146c 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -489,6 +489,7 @@ impl TrayHandle for MockTrayHandler { } } +#[derive(Debug)] pub struct MockRuntime { pub context: RuntimeContext, global_shortcut_manager: MockGlobalShortcutManager, diff --git a/core/tauri/src/window.rs b/core/tauri/src/window.rs index fcfb26fcd..060c643fa 100644 --- a/core/tauri/src/window.rs +++ b/core/tauri/src/window.rs @@ -15,6 +15,7 @@ use crate::{ hooks::{InvokePayload, InvokeResponder}, manager::WindowManager, runtime::{ + http::{Request as HttpRequest, Response as HttpResponse}, menu::Menu, monitor::Monitor as RuntimeMonitor, webview::{WebviewAttributes, WindowBuilder as _}, @@ -22,7 +23,7 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, DetachedWindow, JsEventListenerKey, PendingWindow, WindowEvent, }, - Dispatch, Runtime, UserAttentionType, + Dispatch, Runtime, RuntimeHandle, UserAttentionType, }, sealed::ManagerBase, sealed::RuntimeOrDispatch, @@ -37,11 +38,17 @@ use windows::Win32::Foundation::HWND; use tauri_macros::default_runtime; use std::{ + fmt, hash::{Hash, Hasher}, path::PathBuf, sync::Arc, }; +#[derive(Clone, Serialize)] +struct WindowCreatedEvent { + label: String, +} + /// Monitor descriptor. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -98,14 +105,27 @@ pub enum RuntimeHandleOrDispatch { /// A builder for a webview window managed by Tauri. #[default_runtime(crate::Wry, wry)] -#[derive(Debug)] pub struct WindowBuilder { manager: WindowManager, runtime: RuntimeHandleOrDispatch, app_handle: AppHandle, label: String, pub(crate) window_builder: ::WindowBuilder, - webview_attributes: WebviewAttributes, + pub(crate) webview_attributes: WebviewAttributes, + web_resource_request_handler: Option>, +} + +impl fmt::Debug for WindowBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WindowBuilder") + .field("manager", &self.manager) + .field("runtime", &self.runtime) + .field("app_handle", &self.app_handle) + .field("label", &self.label) + .field("window_builder", &self.window_builder) + .field("webview_attributes", &self.webview_attributes) + .finish() + } } impl ManagerBase for WindowBuilder { @@ -141,16 +161,92 @@ impl WindowBuilder { label: label.into(), window_builder: ::WindowBuilder::new(), webview_attributes: WebviewAttributes::new(url), + web_resource_request_handler: None, } } + /// Defines a closure to be executed when the webview makes an HTTP request for a web resource, allowing you to modify the response. + /// + /// Currently only implemented for the `tauri` URI protocol. + /// + /// **NOTE:** Currently this is **not** executed when using external URLs such as a development server, + /// but it might be implemented in the future. **Always** check the request URL. + /// + /// # Examples + /// + /// ```rust,no_run + /// use tauri::{ + /// utils::config::{Csp, CspDirectiveSources, WindowUrl}, + /// runtime::http::header::HeaderValue, + /// window::WindowBuilder, + /// }; + /// use std::collections::HashMap; + /// tauri::Builder::default() + /// .setup(|app| { + /// WindowBuilder::new(app, "core", WindowUrl::App("index.html".into())) + /// .on_web_resource_request(|request, response| { + /// if request.uri().starts_with("tauri://") { + /// // if we have a CSP header, Tauri is loading an HTML file + /// // for this example, let's dynamically change the CSP + /// if let Some(csp) = response.headers_mut().get_mut("Content-Security-Policy") { + /// // use the tauri helper to parse the CSP policy to a map + /// let mut csp_map: HashMap = Csp::Policy(csp.to_str().unwrap().to_string()).into(); + /// csp_map.entry("script-src".to_string()).or_insert_with(Default::default).push("'unsafe-inline'"); + /// // use the tauri helper to get a CSP string from the map + /// let csp_string = Csp::from(csp_map).to_string(); + /// *csp = HeaderValue::from_str(&csp_string).unwrap(); + /// } + /// } + /// }) + /// .build() + /// .unwrap(); + /// Ok(()) + /// }); + /// ``` + pub fn on_web_resource_request( + mut self, + f: F, + ) -> Self { + self.web_resource_request_handler.replace(Box::new(f)); + self + } + /// Creates a new webview window. - pub fn build(self) -> crate::Result> { - self.create_new_window(PendingWindow::new( + pub fn build(mut self) -> crate::Result> { + let web_resource_request_handler = self.web_resource_request_handler.take(); + let pending = PendingWindow::new( self.window_builder.clone(), self.webview_attributes.clone(), self.label.clone(), - )?) + )?; + let labels = self.manager().labels().into_iter().collect::>(); + let pending = self.manager().prepare_window( + self.managed_app_handle(), + pending, + &labels, + web_resource_request_handler, + )?; + let window = match self.runtime() { + RuntimeOrDispatch::Runtime(runtime) => runtime.create_window(pending), + RuntimeOrDispatch::RuntimeHandle(handle) => handle.create_window(pending), + RuntimeOrDispatch::Dispatch(mut dispatcher) => dispatcher.create_window(pending), + } + .map(|window| { + self + .manager() + .attach_window(self.managed_app_handle(), window) + })?; + + self.manager().emit_filter( + "tauri://window-created", + None, + Some(WindowCreatedEvent { + label: window.label().into(), + }), + |w| w != &window, + )?; + + Ok(window) } // --------------------------------------------- Window builder ---------------------------------------------