diff --git a/.changes/custom-protocol-response-refactor.md b/.changes/custom-protocol-response-refactor.md new file mode 100644 index 000000000000..791a71ba18aa --- /dev/null +++ b/.changes/custom-protocol-response-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +Changed `Builder::register_uri_scheme_protocol` to return a `http::Response` instead of `Result`. To return an error response, manually create a response with status code >= 400. diff --git a/.changes/fix-channel-data-request.md b/.changes/fix-channel-data-request.md new file mode 100644 index 000000000000..632c27f155ff --- /dev/null +++ b/.changes/fix-channel-data-request.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:bug +--- + +Fixes invalid header value type when requesting IPC body through a channel. diff --git a/.changes/http-types-refactor.md b/.changes/http-types-refactor.md new file mode 100644 index 000000000000..d44158cf1f16 --- /dev/null +++ b/.changes/http-types-refactor.md @@ -0,0 +1,7 @@ +--- +"tauri": patch:breaking +"tauri-runtime": patch:breaking +"tauri-runtime-wry": patch:breaking +--- + +`tauri-runtime` no longer implements its own HTTP types and relies on the `http` crate instead. diff --git a/.changes/invoke-system-args.md b/.changes/invoke-system-args.md new file mode 100644 index 000000000000..b104100078d2 --- /dev/null +++ b/.changes/invoke-system-args.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +Changed `Builder::invoke_system` to take references instead of owned values. diff --git a/.changes/register_asynchronous_uri_scheme_protocol.md b/.changes/register_asynchronous_uri_scheme_protocol.md new file mode 100644 index 000000000000..68617316f66c --- /dev/null +++ b/.changes/register_asynchronous_uri_scheme_protocol.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:enhance +--- + +Added `Builder::register_asynchronous_uri_scheme_protocol` to allow resolving a custom URI scheme protocol request asynchronously to prevent blocking the main thread. diff --git a/.changes/runtime-custom-protocol-async.md b/.changes/runtime-custom-protocol-async.md new file mode 100644 index 000000000000..76967cca0c2c --- /dev/null +++ b/.changes/runtime-custom-protocol-async.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime": patch:enhance +--- + +Changed custom protocol closure type to enable asynchronous usage. diff --git a/.changes/window-on-message-refactor.md b/.changes/window-on-message-refactor.md new file mode 100644 index 000000000000..335e8ff19228 --- /dev/null +++ b/.changes/window-on-message-refactor.md @@ -0,0 +1,5 @@ +--- +"tauri": patch:breaking +--- + +Changed `Window::on_message` signature to take a responder closure instead of returning the response object in order to asynchronously process the request. diff --git a/.changes/wry-0.32.md b/.changes/wry-0.32.md new file mode 100644 index 000000000000..a9bfc8f0a2e1 --- /dev/null +++ b/.changes/wry-0.32.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": patch:enhance +--- + +Update wry to 0.32 to include asynchronous custom protocol support. diff --git a/core/tauri-runtime-wry/Cargo.toml b/core/tauri-runtime-wry/Cargo.toml index 3d17bf3a2b99..1266214686d6 100644 --- a/core/tauri-runtime-wry/Cargo.toml +++ b/core/tauri-runtime-wry/Cargo.toml @@ -16,12 +16,13 @@ rust-version = { workspace = true } features = [ "dox" ] [dependencies] -wry = { version = "0.31", default-features = false, features = [ "file-drop", "protocol" ] } +wry = { version = "0.32", default-features = false, features = [ "tao", "file-drop", "protocol" ] } tauri-runtime = { version = "1.0.0-alpha.0", path = "../tauri-runtime" } tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils" } uuid = { version = "1", features = [ "v4" ] } rand = "0.8" raw-window-handle = "0.5" +http = "0.2" [target."cfg(windows)".dependencies] webview2-com = "0.25" diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 2e9752c9e68e..d1e36a5508da 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -13,7 +13,6 @@ use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle}; use tauri_runtime::{ - http::{header::CONTENT_TYPE, Request as HttpRequest, RequestParts, Response as HttpResponse}, monitor::Monitor, webview::{WebviewIpcHandler, WindowBuilder, WindowBuilderBase}, window::{ @@ -61,7 +60,6 @@ use wry::{ UserAttentionType as WryUserAttentionType, }, }, - http::{Request as WryRequest, Response as WryResponse}, webview::{FileDropEvent as WryFileDropEvent, Url, WebContext, WebView, WebViewBuilder}, }; @@ -85,7 +83,6 @@ pub use wry::application::platform::macos::{ }; use std::{ - borrow::Cow, cell::RefCell, collections::{ hash_map::Entry::{Occupied, Vacant}, @@ -259,39 +256,6 @@ impl fmt::Debug for Context { } } -struct HttpRequestWrapper(HttpRequest); - -impl From<&WryRequest>> for HttpRequestWrapper { - fn from(req: &WryRequest>) -> Self { - let parts = RequestParts { - uri: req.uri().to_string(), - method: req.method().clone(), - headers: req.headers().clone(), - }; - Self(HttpRequest::new_internal(parts, req.body().clone())) - } -} - -// response -struct HttpResponseWrapper(WryResponse>); -impl From for HttpResponseWrapper { - fn from(response: HttpResponse) -> Self { - let (parts, body) = response.into_parts(); - let mut res_builder = WryResponse::builder() - .status(parts.status) - .version(parts.version); - if let Some(mime) = parts.mimetype { - res_builder = res_builder.header(CONTENT_TYPE, mime); - } - for (name, val) in parts.headers.iter() { - res_builder = res_builder.header(name, val); - } - - let res = res_builder.body(body).unwrap(); - Self(res) - } -} - pub struct DeviceEventFilterWrapper(pub WryDeviceEventFilter); impl From for DeviceEventFilterWrapper { @@ -2701,11 +2665,13 @@ fn create_webview( } for (scheme, protocol) in uri_scheme_protocols { - webview_builder = webview_builder.with_custom_protocol(scheme, move |wry_request| { - protocol(&HttpRequestWrapper::from(wry_request).0) - .map(|tauri_response| HttpResponseWrapper::from(tauri_response).0) - .map_err(|_| wry::Error::InitScriptError) - }); + webview_builder = + webview_builder.with_asynchronous_custom_protocol(scheme, move |request, responder| { + protocol( + request, + Box::new(move |response| responder.respond(response)), + ) + }); } for script in webview_attributes.initialization_scripts { diff --git a/core/tauri-runtime/Cargo.toml b/core/tauri-runtime/Cargo.toml index 0185ca771cae..aa95edebac77 100644 --- a/core/tauri-runtime/Cargo.toml +++ b/core/tauri-runtime/Cargo.toml @@ -29,7 +29,6 @@ thiserror = "1.0" tauri-utils = { version = "2.0.0-alpha.7", path = "../tauri-utils" } uuid = { version = "1", features = [ "v4" ] } http = "0.2.4" -http-range = "0.1.4" raw-window-handle = "0.5" rand = "0.8" url = { version = "2" } diff --git a/core/tauri-runtime/src/http/mod.rs b/core/tauri-runtime/src/http/mod.rs deleted file mode 100644 index 7ce36f4fadee..000000000000 --- a/core/tauri-runtime/src/http/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -// custom wry types -mod request; -mod response; - -pub use self::{ - request::{Request, RequestParts}, - response::{Builder as ResponseBuilder, Response, ResponseParts}, -}; - -pub use tauri_utils::mime_type::MimeType; - -// re-expose default http types -pub use http::{header, method, status, uri::InvalidUri, version, Uri}; - -// re-export httprange helper as it can be useful and we need it locally -pub use http_range::HttpRange; diff --git a/core/tauri-runtime/src/http/request.rs b/core/tauri-runtime/src/http/request.rs deleted file mode 100644 index 42729de06baf..000000000000 --- a/core/tauri-runtime/src/http/request.rs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::fmt; - -use super::{ - header::{HeaderMap, HeaderValue}, - method::Method, -}; - -/// Represents an HTTP request from the WebView. -/// -/// An HTTP request consists of a head and a potentially optional body. -/// -/// ## Platform-specific -/// -/// - **Linux:** Headers are not exposed. -pub struct Request { - head: RequestParts, - body: Vec, -} - -/// Component parts of an HTTP `Request` -/// -/// The HTTP request head consists of a method, uri, and a set of -/// header fields. -#[derive(Clone)] -pub struct RequestParts { - /// The request's method - pub method: Method, - - /// The request's URI - pub uri: String, - - /// The request's headers - pub headers: HeaderMap, -} - -impl Request { - /// Creates a new blank `Request` with the body - #[inline] - pub fn new(body: Vec) -> Request { - Request { - head: RequestParts::new(), - body, - } - } - - /// 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 { - &self.head.method - } - - /// Returns a reference to the associated URI. - #[inline] - pub fn uri(&self) -> &str { - &self.head.uri - } - - /// Returns a reference to the associated header field map. - #[inline] - pub fn headers(&self) -> &HeaderMap { - &self.head.headers - } - - /// Returns a reference to the associated HTTP body. - #[inline] - pub fn body(&self) -> &Vec { - &self.body - } - - /// 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) - } -} - -impl Default for Request { - fn default() -> Request { - Request::new(Vec::new()) - } -} - -impl fmt::Debug for Request { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Request") - .field("method", self.method()) - .field("uri", &self.uri()) - .field("headers", self.headers()) - .field("body", self.body()) - .finish() - } -} - -impl RequestParts { - /// Creates a new default instance of `RequestParts` - fn new() -> RequestParts { - RequestParts { - method: Method::default(), - uri: "".into(), - headers: HeaderMap::default(), - } - } -} - -impl fmt::Debug for RequestParts { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Parts") - .field("method", &self.method) - .field("uri", &self.uri) - .field("headers", &self.headers) - .finish() - } -} diff --git a/core/tauri-runtime/src/http/response.rs b/core/tauri-runtime/src/http/response.rs deleted file mode 100644 index a004d4dc0d5f..000000000000 --- a/core/tauri-runtime/src/http/response.rs +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use super::{ - header::{HeaderMap, HeaderName, HeaderValue}, - status::StatusCode, - version::Version, -}; -use std::{borrow::Cow, fmt}; - -type Result = core::result::Result>; - -/// Represents an HTTP response -/// -/// An HTTP response consists of a head and a potentially body. -/// -/// ## Platform-specific -/// -/// - **Linux:** Headers and status code cannot be changed. -/// -/// # Examples -/// -/// ``` -/// # use tauri_runtime::http::*; -/// -/// let response = ResponseBuilder::new() -/// .status(202) -/// .mimetype("text/html") -/// .body("hello!".as_bytes().to_vec()) -/// .unwrap(); -/// ``` -/// -pub struct Response { - head: ResponseParts, - body: Cow<'static, [u8]>, -} - -/// Component parts of an HTTP `Response` -/// -/// The HTTP response head consists of a status, version, and a set of -/// header fields. -#[derive(Clone)] -pub struct ResponseParts { - /// The response's status. - pub status: StatusCode, - - /// The response's version. - pub version: Version, - - /// The response's headers. - pub headers: HeaderMap, - - /// The response's mimetype type. - pub mimetype: Option, -} - -/// An HTTP response builder -/// -/// This type can be used to construct an instance of `Response` through a -/// builder-like pattern. -#[derive(Debug)] -pub struct Builder { - inner: Result, -} - -impl Response { - /// Creates a new blank `Response` with the body - #[inline] - pub fn new(body: Cow<'static, [u8]>) -> Response { - Response { - head: ResponseParts::new(), - body, - } - } - - /// 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, Cow<'static, [u8]>) { - (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<&String> { - self.head.mimetype.as_ref() - } - - /// Returns a reference to the associated version. - #[inline] - pub fn version(&self) -> Version { - 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 Cow<'static, [u8]> { - &mut self.body - } - - /// Returns a reference to the associated HTTP body. - #[inline] - pub fn body(&self) -> &Cow<'static, [u8]> { - &self.body - } -} - -impl Default for Response { - #[inline] - fn default() -> Response { - Response::new(Default::default()) - } -} - -impl fmt::Debug for Response { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Response") - .field("status", &self.status()) - .field("version", &self.version()) - .field("headers", self.headers()) - .field("body", self.body()) - .finish() - } -} - -impl ResponseParts { - /// Creates a new default instance of `ResponseParts` - fn new() -> ResponseParts { - ResponseParts { - status: StatusCode::default(), - version: Version::default(), - headers: HeaderMap::default(), - mimetype: None, - } - } -} - -impl fmt::Debug for ResponseParts { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Parts") - .field("status", &self.status) - .field("version", &self.version) - .field("headers", &self.headers) - .finish() - } -} - -impl Builder { - /// Creates a new default instance of `Builder` to construct either a - /// `Head` or a `Response`. - /// - /// # Examples - /// - /// ``` - /// # use tauri_runtime::http::*; - /// - /// let response = ResponseBuilder::new() - /// .status(200) - /// .mimetype("text/html") - /// .body(Vec::new()) - /// .unwrap(); - /// ``` - #[inline] - pub fn new() -> Builder { - Builder { - inner: Ok(ResponseParts::new()), - } - } - - /// Set the HTTP mimetype for this response. - #[must_use] - pub fn mimetype(self, mimetype: &str) -> Self { - self.and_then(move |mut head| { - head.mimetype = Some(mimetype.to_string()); - Ok(head) - }) - } - - /// Set the HTTP status for this response. - #[must_use] - pub fn status(self, status: T) -> Self - where - StatusCode: TryFrom, - >::Error: Into, - { - self.and_then(move |mut head| { - head.status = TryFrom::try_from(status).map_err(Into::into)?; - Ok(head) - }) - } - - /// Set the HTTP version for this response. - /// - /// This function will configure the HTTP version of the `Response` that - /// will be returned from `Builder::build`. - /// - /// By default this is HTTP/1.1 - #[must_use] - pub fn version(self, version: Version) -> Self { - self.and_then(move |mut head| { - head.version = version; - Ok(head) - }) - } - - /// Appends a header to this response builder. - /// - /// This function will append the provided key/value as a header to the - /// internal `HeaderMap` being constructed. Essentially this is equivalent - /// to calling `HeaderMap::append`. - #[must_use] - pub fn header(self, key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - HeaderValue: TryFrom, - >::Error: Into, - { - self.and_then(move |mut head| { - let name = >::try_from(key).map_err(Into::into)?; - let value = >::try_from(value).map_err(Into::into)?; - head.headers.append(name, value); - Ok(head) - }) - } - - /// "Consumes" this builder, using the provided `body` to return a - /// constructed `Response`. - /// - /// # Errors - /// - /// This function may return an error if any previously configured argument - /// failed to parse or get converted to the internal representation. For - /// example if an invalid `head` was specified via `header("Foo", - /// "Bar\r\n")` the error will be returned when this function is called - /// rather than when `header` was called. - /// - /// # Examples - /// - /// ``` - /// # use tauri_runtime::http::*; - /// - /// let response = ResponseBuilder::new() - /// .mimetype("text/html") - /// .body(Vec::new()) - /// .unwrap(); - /// ``` - pub fn body(self, body: impl Into>) -> Result { - self.inner.map(move |head| Response { - head, - body: body.into(), - }) - } - - // private - - fn and_then(self, func: F) -> Self - where - F: FnOnce(ResponseParts) -> Result, - { - Builder { - inner: self.inner.and_then(func), - } - } -} - -impl Default for Builder { - #[inline] - fn default() -> Builder { - Builder { - inner: Ok(ResponseParts::new()), - } - } -} diff --git a/core/tauri-runtime/src/lib.rs b/core/tauri-runtime/src/lib.rs index 25c743b8ae35..52158ccff121 100644 --- a/core/tauri-runtime/src/lib.rs +++ b/core/tauri-runtime/src/lib.rs @@ -19,7 +19,6 @@ use tauri_utils::Theme; use url::Url; use uuid::Uuid; -pub mod http; /// Types useful for interacting with a user's monitors. pub mod monitor; pub mod webview; @@ -32,11 +31,10 @@ use window::{ CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent, }; -use crate::http::{ +use http::{ header::{InvalidHeaderName, InvalidHeaderValue}, method::InvalidMethod, status::InvalidStatusCode, - InvalidUri, }; /// Type of user attention requested on a window. @@ -101,8 +99,6 @@ pub enum Error { InvalidHeaderName(#[from] InvalidHeaderName), #[error("Invalid header value: {0}")] InvalidHeaderValue(#[from] InvalidHeaderValue), - #[error("Invalid uri: {0}")] - InvalidUri(#[from] InvalidUri), #[error("Invalid status code: {0}")] InvalidStatusCode(#[from] InvalidStatusCode), #[error("Invalid method: {0}")] diff --git a/core/tauri-runtime/src/window.rs b/core/tauri-runtime/src/window.rs index 5f06b6219eef..c5e4fb500575 100644 --- a/core/tauri-runtime/src/window.rs +++ b/core/tauri-runtime/src/window.rs @@ -5,16 +5,17 @@ //! A layer between raw [`Runtime`] webview windows and Tauri. use crate::{ - http::{Request as HttpRequest, Response as HttpResponse}, webview::{WebviewAttributes, WebviewIpcHandler}, Dispatch, Runtime, UserEvent, WindowBuilder, }; +use http::{Request as HttpRequest, Response as HttpResponse}; use serde::{Deserialize, Deserializer}; use tauri_utils::{config::WindowConfig, Theme}; use url::Url; use std::{ + borrow::Cow, collections::HashMap, hash::{Hash, Hasher}, marker::PhantomData, @@ -24,10 +25,13 @@ use std::{ use self::dpi::PhysicalPosition; -type UriSchemeProtocol = - dyn Fn(&HttpRequest) -> Result> + Send + Sync + 'static; +type UriSchemeProtocol = dyn Fn(HttpRequest>, Box>) + Send>) + + Send + + Sync + + 'static; -type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync; +type WebResourceRequestHandler = + dyn Fn(HttpRequest>, &mut HttpResponse>) + Send + Sync; type NavigationHandler = dyn Fn(&Url) -> bool + Send; @@ -306,16 +310,20 @@ impl> PendingWindow { pub fn register_uri_scheme_protocol< N: Into, - H: Fn(&HttpRequest) -> Result> + Send + Sync + 'static, + H: Fn(HttpRequest>, Box>) + Send>) + + Send + + Sync + + 'static, >( &mut self, uri_scheme: N, protocol: H, ) { let uri_scheme = uri_scheme.into(); - self - .uri_scheme_protocols - .insert(uri_scheme, Box::new(move |data| (protocol)(data))); + self.uri_scheme_protocols.insert( + uri_scheme, + Box::new(move |data, responder| (protocol)(data, responder)), + ); } #[cfg(target_os = "android")] diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 537929c46db4..390bd281eb2f 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -67,6 +67,7 @@ serialize-to-javascript = "=0.1.1" infer = { version = "0.15", optional = true } png = { version = "0.17", optional = true } ico = { version = "0.3.0", optional = true } +http-range = { version = "0.1.4", optional = true } [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies] muda = { version = "0.8", default-features = false } @@ -116,6 +117,7 @@ serde_json = "1.0" tauri = { path = ".", default-features = false, features = [ "wry" ] } tokio = { version = "1", features = [ "full" ] } cargo_toml = "0.15" +http-range = "0.1.4" [features] default = [ @@ -145,7 +147,7 @@ macos-private-api = [ "tauri-runtime-wry/macos-private-api" ] window-data-url = [ "data-url" ] -protocol-asset = [ ] +protocol-asset = [ "http-range" ] config-json5 = [ "tauri-macros/config-json5" ] config-toml = [ "tauri-macros/config-toml" ] icon-ico = [ "infer", "ico" ] diff --git a/core/tauri/scripts/ipc-protocol.js b/core/tauri/scripts/ipc-protocol.js index b2af9c47897b..082a8ddd5466 100644 --- a/core/tauri/scripts/ipc-protocol.js +++ b/core/tauri/scripts/ipc-protocol.js @@ -32,7 +32,6 @@ ) && !(osName === 'macos' && location.protocol === 'https:') ) { - console.log('process') const { contentType, data diff --git a/core/tauri/scripts/ipc.js b/core/tauri/scripts/ipc.js index 433eba1e5db4..1101e123b2c8 100644 --- a/core/tauri/scripts/ipc.js +++ b/core/tauri/scripts/ipc.js @@ -34,7 +34,7 @@ */ function isIsolationMessage(event) { if (typeof event.data === 'object' && typeof event.data.payload === 'object') { - const keys = Object.keys(event.data.payload) + const keys = Object.keys(event.data.payload || {}) return ( keys.length > 0 && keys.every((key) => key === 'nonce' || key === 'payload') diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index e6dc32998911..4a7bd8214f97 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -11,7 +11,6 @@ use crate::{ manager::{Asset, CustomProtocol, WindowManager}, plugin::{Plugin, PluginStore}, runtime::{ - http::{Request as HttpRequest, Response as HttpResponse}, webview::WebviewAttributes, window::{PendingWindow, WindowEvent as RuntimeWindowEvent}, ExitRequestedEventAction, RunEvent as RuntimeRunEvent, @@ -33,6 +32,7 @@ use crate::menu::{Menu, MenuEvent}; use crate::tray::{TrayIcon, TrayIconBuilder, TrayIconEvent, TrayIconId}; #[cfg(desktop)] use crate::window::WindowMenu; +use http::{Request as HttpRequest, Response as HttpResponse}; use raw_window_handle::HasRawDisplayHandle; use serde::Deserialize; use serialize_to_javascript::{default_template, DefaultTemplate, Template}; @@ -49,6 +49,7 @@ use tauri_runtime::{ use tauri_utils::PackageInfo; use std::{ + borrow::Cow, collections::HashMap, fmt, sync::{mpsc::Sender, Arc, Weak}, @@ -1097,7 +1098,7 @@ impl Builder { #[must_use] pub fn invoke_system(mut self, initialization_script: String, responder: F) -> Self where - F: Fn(Window, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static, + F: Fn(&Window, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static, { self.invoke_initialization_script = initialization_script; self.invoke_responder.replace(Arc::new(responder)); @@ -1348,14 +1349,78 @@ impl Builder { /// # Arguments /// /// * `uri_scheme` The URI scheme to register, such as `example`. - /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`. + /// * `protocol` the protocol associated with the given URI scheme. It's a function that takes a request and returns a response. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .register_uri_scheme_protocol("app-files", |_app, request| { + /// let path = request.uri().path().trim_start_matches('/'); + /// if let Ok(data) = std::fs::read(path) { + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// } else { + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// } + /// }); + /// ``` #[must_use] pub fn register_uri_scheme_protocol< N: Into, - H: Fn(&AppHandle, &HttpRequest) -> Result> - + Send - + Sync - + 'static, + T: Into>, + H: Fn(&AppHandle, HttpRequest>) -> HttpResponse + Send + Sync + 'static, + >( + mut self, + uri_scheme: N, + protocol: H, + ) -> Self { + self.uri_scheme_protocols.insert( + uri_scheme.into(), + Arc::new(CustomProtocol { + protocol: Box::new(move |app, request, responder| { + responder.respond(protocol(app, request)) + }), + }), + ); + self + } + + /// Similar to [`Self::register_uri_scheme_protocol`] but with an asynchronous responder that allows you + /// to process the request in a separate thread and respond asynchronously. + /// + /// # Examples + /// ``` + /// tauri::Builder::default() + /// .register_asynchronous_uri_scheme_protocol("app-files", |_app, request, responder| { + /// let path = request.uri().path().trim_start_matches('/').to_string(); + /// std::thread::spawn(move || { + /// if let Ok(data) = std::fs::read(path) { + /// responder.respond( + /// http::Response::builder() + /// .body(data) + /// .unwrap() + /// ); + /// } else { + /// responder.respond( + /// http::Response::builder() + /// .status(http::StatusCode::BAD_REQUEST) + /// .header(http::header::CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + /// .body("failed to read file".as_bytes().to_vec()) + /// .unwrap() + /// ); + /// } + /// }); + /// }); + /// ``` + #[must_use] + pub fn register_asynchronous_uri_scheme_protocol< + N: Into, + H: Fn(&AppHandle, HttpRequest>, UriSchemeResponder) + Send + Sync + 'static, >( mut self, uri_scheme: N, @@ -1579,6 +1644,17 @@ impl Builder { } } +pub(crate) type UriSchemeResponderFn = Box>) + Send>; +pub struct UriSchemeResponder(pub(crate) UriSchemeResponderFn); + +impl UriSchemeResponder { + /// Resolves the request with the given response. + pub fn respond>>(self, response: HttpResponse) { + let (parts, body) = response.into_parts(); + (self.0)(HttpResponse::from_parts(parts, body.into())) + } +} + #[cfg(target_os = "macos")] fn init_app_menu(menu: &Menu) -> crate::Result<()> { menu.inner().init_for_nsapp(); diff --git a/core/tauri/src/ipc/channel.rs b/core/tauri/src/ipc/channel.rs index e09a8d922b53..76a9101cebaf 100644 --- a/core/tauri/src/ipc/channel.rs +++ b/core/tauri/src/ipc/channel.rs @@ -78,7 +78,7 @@ impl Channel { .unwrap() .insert(data_id, body); window.eval(&format!( - "__TAURI_INVOKE__('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': {data_id} }} }}).then(window['_' + {}]).catch(console.error)", + "__TAURI_INVOKE__('{FETCH_CHANNEL_DATA_COMMAND}', null, {{ headers: {{ '{CHANNEL_ID_HEADER_NAME}': '{data_id}' }} }}).then(window['_' + {}]).catch(console.error)", callback.0 )) }) diff --git a/core/tauri/src/ipc/mod.rs b/core/tauri/src/ipc/mod.rs index 66579fbbef7e..3d51726d277c 100644 --- a/core/tauri/src/ipc/mod.rs +++ b/core/tauri/src/ipc/mod.rs @@ -6,7 +6,7 @@ //! //! This module includes utilities to send messages to the JS layer of the webview. -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use futures_util::Future; use http::HeaderMap; @@ -21,6 +21,7 @@ use crate::{ }; pub(crate) mod channel; +#[cfg(any(target_os = "macos", not(ipc_custom_protocol)))] pub(crate) mod format_callback; pub(crate) mod protocol; @@ -31,9 +32,10 @@ pub type InvokeHandler = dyn Fn(Invoke) -> bool + Send + Sync + 'static; /// A closure that is responsible for respond a JS message. pub type InvokeResponder = - dyn Fn(Window, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; -type OwnedInvokeResponder = - dyn Fn(Window, String, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; + dyn Fn(&Window, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static; +/// Similar to [`InvokeResponder`] but taking owned arguments. +pub type OwnedInvokeResponder = + dyn FnOnce(Window, String, InvokeResponse, CallbackFn, CallbackFn) + Send + 'static; /// Possible values of an IPC payload. #[derive(Debug, Clone)] @@ -225,7 +227,7 @@ impl From for InvokeResponse { #[default_runtime(crate::Wry, wry)] pub struct InvokeResolver { window: Window, - responder: Arc>, + responder: Arc>>>>, cmd: String, pub(crate) callback: CallbackFn, pub(crate) error: CallbackFn, @@ -246,7 +248,7 @@ impl Clone for InvokeResolver { impl InvokeResolver { pub(crate) fn new( window: Window, - responder: Arc>, + responder: Arc>>>>, cmd: String, callback: CallbackFn, error: CallbackFn, @@ -348,7 +350,7 @@ impl InvokeResolver { /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. pub async fn return_task( window: Window, - responder: Arc>, + responder: Arc>>>>, task: F, cmd: String, success_callback: CallbackFn, @@ -370,7 +372,7 @@ impl InvokeResolver { pub(crate) fn return_closure Result>( window: Window, - responder: Arc>, + responder: Arc>>>>, f: F, cmd: String, success_callback: CallbackFn, @@ -388,13 +390,19 @@ impl InvokeResolver { pub(crate) fn return_result( window: Window, - responder: Arc>, + responder: Arc>>>>, response: InvokeResponse, cmd: String, success_callback: CallbackFn, error_callback: CallbackFn, ) { - (responder)(window, cmd, response, success_callback, error_callback); + (responder.lock().unwrap().take().expect("resolver consumed"))( + window, + cmd, + response, + success_callback, + error_callback, + ); } } diff --git a/core/tauri/src/ipc/protocol.rs b/core/tauri/src/ipc/protocol.rs index e2a0330d1751..043ea931a4d8 100644 --- a/core/tauri/src/ipc/protocol.rs +++ b/core/tauri/src/ipc/protocol.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +use std::borrow::Cow; + use http::{ - header::{ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN}, - HeaderValue, Method, StatusCode, + header::{ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}, + HeaderValue, Method, Request as HttpRequest, Response as HttpResponse, StatusCode, }; use crate::{ manager::WindowManager, - runtime::http::{Request as HttpRequest, Response as HttpResponse}, window::{InvokeRequest, UriSchemeProtocolHandler}, Runtime, }; @@ -27,34 +28,74 @@ pub fn message_handler( } pub fn get(manager: WindowManager, label: String) -> UriSchemeProtocolHandler { - Box::new(move |request| { - let mut response = match *request.method() { + Box::new(move |request, responder| { + let manager = manager.clone(); + let label = label.clone(); + + let respond = move |mut response: http::Response>| { + response + .headers_mut() + .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); + responder.respond(response); + }; + + match *request.method() { Method::POST => { - let (mut response, content_type) = match handle_ipc_request(request, &manager, &label) { - Ok(data) => match data { - InvokeResponse::Ok(InvokeBody::Json(v)) => ( - HttpResponse::new(serde_json::to_vec(&v)?.into()), - mime::APPLICATION_JSON, - ), - InvokeResponse::Ok(InvokeBody::Raw(v)) => { - (HttpResponse::new(v.into()), mime::APPLICATION_OCTET_STREAM) + if let Some(window) = manager.get_window(&label) { + match parse_invoke_request(&manager, request) { + Ok(request) => { + window.on_message( + request, + Box::new(move |_window, _cmd, response, _callback, _error| { + let (mut response, mime_type) = match response { + InvokeResponse::Ok(InvokeBody::Json(v)) => ( + HttpResponse::new(serde_json::to_vec(&v).unwrap().into()), + mime::APPLICATION_JSON, + ), + InvokeResponse::Ok(InvokeBody::Raw(v)) => { + (HttpResponse::new(v.into()), mime::APPLICATION_OCTET_STREAM) + } + InvokeResponse::Err(e) => { + let mut response = + HttpResponse::new(serde_json::to_vec(&e.0).unwrap().into()); + *response.status_mut() = StatusCode::BAD_REQUEST; + (response, mime::TEXT_PLAIN) + } + }; + + response.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_str(mime_type.essence_str()).unwrap(), + ); + + respond(response); + }), + ); } - InvokeResponse::Err(e) => { - let mut response = HttpResponse::new(serde_json::to_vec(&e.0)?.into()); - response.set_status(StatusCode::BAD_REQUEST); - (response, mime::TEXT_PLAIN) + Err(e) => { + respond( + HttpResponse::builder() + .status(StatusCode::BAD_REQUEST) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body(e.as_bytes().to_vec().into()) + .unwrap(), + ); } - }, - Err(e) => { - let mut response = HttpResponse::new(e.as_bytes().to_vec().into()); - response.set_status(StatusCode::BAD_REQUEST); - (response, mime::TEXT_PLAIN) } - }; - - response.set_mimetype(Some(content_type.essence_str().into())); - - response + } else { + respond( + HttpResponse::builder() + .status(StatusCode::BAD_REQUEST) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body( + "failed to acquire window reference" + .as_bytes() + .to_vec() + .into(), + ) + .unwrap(), + ); + } } Method::OPTIONS => { @@ -63,7 +104,7 @@ pub fn get(manager: WindowManager, label: String) -> UriSchemePro ACCESS_CONTROL_ALLOW_HEADERS, HeaderValue::from_static("Content-Type, Tauri-Callback, Tauri-Error, Tauri-Channel-Id"), ); - r + respond(r); } _ => { @@ -73,17 +114,14 @@ pub fn get(manager: WindowManager, label: String) -> UriSchemePro .to_vec() .into(), ); - r.set_status(StatusCode::METHOD_NOT_ALLOWED); - r.set_mimetype(Some(mime::TEXT_PLAIN.essence_str().into())); - r + *r.status_mut() = StatusCode::METHOD_NOT_ALLOWED; + r.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_str(mime::TEXT_PLAIN.essence_str()).unwrap(), + ); + respond(r); } - }; - - response - .headers_mut() - .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); - - Ok(response) + } }) } @@ -166,13 +204,76 @@ fn handle_ipc_message(message: String, manager: &WindowManager, l .unwrap_or_else(|| serde_json::from_str::(&message).map_err(Into::into)) { Ok(message) => { - let _ = window.on_message(InvokeRequest { - cmd: message.cmd, - callback: message.callback, - error: message.error, - body: message.payload.into(), - headers: message.options.map(|o| o.headers.0).unwrap_or_default(), - }); + window.on_message( + InvokeRequest { + cmd: message.cmd, + callback: message.callback, + error: message.error, + body: message.payload.into(), + headers: message.options.map(|o| o.headers.0).unwrap_or_default(), + }, + Box::new(move |window, cmd, response, callback, error| { + use crate::ipc::{ + format_callback::{ + format as format_callback, format_result as format_callback_result, + }, + Channel, + }; + use serde_json::Value as JsonValue; + + // the channel data command is the only command that uses a custom protocol on Linux + if window.manager.invoke_responder().is_none() + && cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND + { + fn responder_eval( + window: &crate::Window, + js: crate::api::Result, + error: CallbackFn, + ) { + let eval_js = match js { + Ok(js) => js, + Err(e) => format_callback(error, &e.to_string()) + .expect("unable to serialize response error string to json"), + }; + + let _ = window.eval(&eval_js); + } + + match &response { + InvokeResponse::Ok(InvokeBody::Json(v)) => { + if !cfg!(target_os = "macos") + && matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) + { + let _ = Channel::from_ipc(window.clone(), callback).send(v); + } else { + responder_eval( + &window, + format_callback_result(Result::<_, ()>::Ok(v), callback, error), + error, + ) + } + } + InvokeResponse::Ok(InvokeBody::Raw(v)) => { + responder_eval( + &window, + format_callback_result(Result::<_, ()>::Ok(v), callback, error), + error, + ); + if cfg!(target_os = "macos") { + } else { + let _ = + Channel::from_ipc(window.clone(), callback).send(InvokeBody::Raw(v.clone())); + } + } + InvokeResponse::Err(e) => responder_eval( + &window, + format_callback_result(Result::<(), _>::Err(&e.0), callback, error), + error, + ), + } + } + }), + ); } Err(e) => { let _ = window.eval(&format!( @@ -184,90 +285,76 @@ fn handle_ipc_message(message: String, manager: &WindowManager, l } } -fn handle_ipc_request( - request: &HttpRequest, - manager: &WindowManager, - label: &str, -) -> std::result::Result { - if let Some(window) = manager.get_window(label) { - // TODO: consume instead - #[allow(unused_mut)] - let mut body = request.body().clone(); - - let cmd = request - .uri() - .strip_prefix("ipc://localhost/") - .map(|c| c.to_string()) - // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows - // where `$P` is not `localhost/*` - // in this case the IPC call is considered invalid - .unwrap_or_else(|| "".to_string()); - let cmd = percent_encoding::percent_decode(cmd.as_bytes()) - .decode_utf8_lossy() - .to_string(); - - // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it - #[cfg(all(feature = "isolation", ipc_custom_protocol))] - if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() { - body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) - .and_then(|raw| crypto_keys.decrypt(raw)) - .map_err(|e| e.to_string())?; - } +fn parse_invoke_request( + #[allow(unused_variables)] manager: &WindowManager, + request: HttpRequest>, +) -> std::result::Result { + #[allow(unused_mut)] + let (parts, mut body) = request.into_parts(); - let callback = CallbackFn( - request - .headers() - .get(TAURI_CALLBACK_HEADER_NAME) - .ok_or("missing Tauri-Callback header")? - .to_str() - .map_err(|_| "Tauri callback header value must be a string")? - .parse() - .map_err(|_| "Tauri callback header value must be a numeric string")?, - ); - let error = CallbackFn( - request - .headers() - .get(TAURI_ERROR_HEADER_NAME) - .ok_or("missing Tauri-Error header")? - .to_str() - .map_err(|_| "Tauri error header value must be a string")? - .parse() - .map_err(|_| "Tauri error header value must be a numeric string")?, - ); - - let content_type = request - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .map(|mime| mime.parse()) - .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM)) - .map_err(|_| "unknown content type")?; - let body = if content_type == mime::APPLICATION_OCTET_STREAM { - body.into() - } else if content_type == mime::APPLICATION_JSON { - if cfg!(ipc_custom_protocol) { - serde_json::from_slice::(&body) - .map_err(|e| e.to_string())? - .into() - } else { - // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it - serde_json::Value::Object(Default::default()).into() - } - } else { - return Err(format!("content type {content_type} is not implemented")); - }; + let cmd = parts.uri.path().trim_start_matches('/'); + let cmd = percent_encoding::percent_decode(cmd.as_bytes()) + .decode_utf8_lossy() + .to_string(); - let payload = InvokeRequest { - cmd, - callback, - error, - body, - headers: request.headers().clone(), - }; + // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it + #[cfg(all(feature = "isolation", ipc_custom_protocol))] + if let crate::Pattern::Isolation { crypto_keys, .. } = manager.pattern() { + body = crate::utils::pattern::isolation::RawIsolationPayload::try_from(&body) + .and_then(|raw| crypto_keys.decrypt(raw)) + .map_err(|e| e.to_string())?; + } - let rx = window.on_message(payload); - Ok(rx.recv().unwrap()) + let callback = CallbackFn( + parts + .headers + .get(TAURI_CALLBACK_HEADER_NAME) + .ok_or("missing Tauri-Callback header")? + .to_str() + .map_err(|_| "Tauri callback header value must be a string")? + .parse() + .map_err(|_| "Tauri callback header value must be a numeric string")?, + ); + let error = CallbackFn( + parts + .headers + .get(TAURI_ERROR_HEADER_NAME) + .ok_or("missing Tauri-Error header")? + .to_str() + .map_err(|_| "Tauri error header value must be a string")? + .parse() + .map_err(|_| "Tauri error header value must be a numeric string")?, + ); + + let content_type = parts + .headers + .get(reqwest::header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(|mime| mime.parse()) + .unwrap_or(Ok(mime::APPLICATION_OCTET_STREAM)) + .map_err(|_| "unknown content type")?; + let body = if content_type == mime::APPLICATION_OCTET_STREAM { + body.into() + } else if content_type == mime::APPLICATION_JSON { + if cfg!(ipc_custom_protocol) { + serde_json::from_slice::(&body) + .map_err(|e| e.to_string())? + .into() + } else { + // the body is not set if ipc_custom_protocol is not enabled so we'll just ignore it + serde_json::Value::Object(Default::default()).into() + } } else { - Err("window not found".into()) - } + return Err(format!("content type {content_type} is not implemented")); + }; + + let payload = InvokeRequest { + cmd, + callback, + error, + body, + headers: parts.headers, + }; + + Ok(payload) } diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index a6135c181422..eabcd02d1e22 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -76,8 +76,6 @@ pub use tauri_macros::{command, generate_handler}; pub mod api; pub(crate) mod app; -#[cfg(feature = "protocol-asset")] -pub(crate) mod asset_protocol; pub mod async_runtime; pub mod command; mod error; @@ -86,6 +84,7 @@ pub mod ipc; mod manager; mod pattern; pub mod plugin; +pub(crate) mod protocol; mod vibrancy; pub mod window; use tauri_runtime as runtime; @@ -107,6 +106,8 @@ mod state; pub mod tray; pub use tauri_utils as utils; +pub use http; + /// A Tauri [`Runtime`] wrapper around wry. #[cfg(feature = "wry")] #[cfg_attr(doc_cfg, doc(cfg(feature = "wry")))] @@ -174,9 +175,6 @@ use std::{ sync::Arc, }; -// Export types likely to be used by the application. -pub use runtime::http; - #[cfg(feature = "wry")] #[cfg_attr(doc_cfg, doc(cfg(feature = "wry")))] pub use tauri_runtime_wry::webview_version; diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 90886ec3dc46..57738953c6f9 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -19,6 +19,7 @@ use serde::Serialize; use serialize_to_javascript::{default_template, DefaultTemplate, Template}; use url::Url; +use http::Request as HttpRequest; use tauri_macros::default_runtime; use tauri_utils::debug_eprintln; use tauri_utils::{ @@ -28,16 +29,15 @@ use tauri_utils::{ }; use crate::{ - app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, PageLoadPayload}, + app::{ + AppHandle, GlobalWindowEvent, GlobalWindowEventListener, OnPageLoad, PageLoadPayload, + UriSchemeResponder, + }, event::{assert_event_name_is_valid, Event, EventHandler, Listeners}, ipc::{Invoke, InvokeHandler, InvokeResponder}, pattern::PatternJavascript, plugin::PluginStore, runtime::{ - http::{ - MimeType, Request as HttpRequest, Response as HttpResponse, - ResponseBuilder as HttpResponseBuilder, - }, webview::WindowBuilder, window::{ dpi::{PhysicalPosition, PhysicalSize}, @@ -49,7 +49,6 @@ use crate::{ config::{AppUrl, Config, WindowUrl}, PackageInfo, }, - window::{UriSchemeProtocolHandler, WebResourceRequestHandler}, Context, EventLoopMessage, Icon, Manager, Pattern, Runtime, Scopes, StateManager, Window, WindowEvent, }; @@ -81,7 +80,7 @@ pub(crate) const PROCESS_IPC_MESSAGE_FN: &str = // and we do not get a secure context without the custom protocol that proxies to the dev server // additionally, we need the custom protocol to inject the initialization scripts on Android // must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol -const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile)); +pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile)); #[cfg(feature = "isolation")] #[derive(Template)] @@ -309,11 +308,7 @@ pub struct Asset { pub struct CustomProtocol { /// Handler for protocol #[allow(clippy::type_complexity)] - pub protocol: Box< - dyn Fn(&AppHandle, &HttpRequest) -> Result> - + Send - + Sync, - >, + pub protocol: Box, HttpRequest>, UriSchemeResponder) + Send + Sync>, } #[default_runtime(crate::Wry, wry)] @@ -604,8 +599,12 @@ impl WindowManager { registered_scheme_protocols.push(uri_scheme.clone()); let protocol = protocol.clone(); let app_handle = Mutex::new(app_handle.clone()); - pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p| { - (protocol.protocol)(&app_handle.lock().unwrap(), p) + pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p, responder| { + (protocol.protocol)( + &app_handle.lock().unwrap(), + p, + UriSchemeResponder(responder), + ) }); } @@ -628,30 +627,28 @@ impl WindowManager { if !registered_scheme_protocols.contains(&"tauri".into()) { let web_resource_request_handler = pending.web_resource_request_handler.take(); - pending.register_uri_scheme_protocol( - "tauri", - self.prepare_uri_scheme_protocol(&window_origin, web_resource_request_handler), - ); + let protocol = + crate::protocol::tauri::get(self, &window_origin, web_resource_request_handler); + pending.register_uri_scheme_protocol("tauri", move |request, responder| { + protocol(request, UriSchemeResponder(responder)) + }); registered_scheme_protocols.push("tauri".into()); } if !registered_scheme_protocols.contains(&"ipc".into()) { - pending.register_uri_scheme_protocol( - "ipc", - crate::ipc::protocol::get(self.clone(), pending.label.clone()), - ); + let protocol = crate::ipc::protocol::get(self.clone(), pending.label.clone()); + pending.register_uri_scheme_protocol("ipc", move |request, responder| { + protocol(request, UriSchemeResponder(responder)) + }); registered_scheme_protocols.push("ipc".into()); } #[cfg(feature = "protocol-asset")] if !registered_scheme_protocols.contains(&"asset".into()) { let asset_scope = self.state().get::().asset_protocol.clone(); - pending.register_uri_scheme_protocol("asset", move |request| { - crate::asset_protocol::asset_protocol_handler( - request, - asset_scope.clone(), - window_origin.clone(), - ) + let protocol = crate::protocol::asset::get(asset_scope.clone(), window_origin.clone()); + pending.register_uri_scheme_protocol("asset", move |request, responder| { + protocol(request, UriSchemeResponder(responder)) }); } @@ -663,41 +660,9 @@ impl WindowManager { crypto_keys, } = &self.inner.pattern { - let assets = assets.clone(); - let schema_ = schema.clone(); - let url_base = format!("{schema_}://localhost"); - let aes_gcm_key = *crypto_keys.aes_gcm().raw(); - - pending.register_uri_scheme_protocol(schema, move |request| { - match request_to_path(request, &url_base).as_str() { - "index.html" => match assets.get(&"index.html".into()) { - Some(asset) => { - let asset = String::from_utf8_lossy(asset.as_ref()); - let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime { - runtime_aes_gcm_key: &aes_gcm_key, - process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN, - }; - match template.render(asset.as_ref(), &Default::default()) { - Ok(asset) => HttpResponseBuilder::new() - .mimetype(mime::TEXT_HTML.as_ref()) - .body(asset.into_string().as_bytes().to_vec()), - Err(_) => HttpResponseBuilder::new() - .status(500) - .mimetype(mime::TEXT_PLAIN.as_ref()) - .body(Vec::new()), - } - } - - None => HttpResponseBuilder::new() - .status(404) - .mimetype(mime::TEXT_PLAIN.as_ref()) - .body(Vec::new()), - }, - _ => HttpResponseBuilder::new() - .status(404) - .mimetype(mime::TEXT_PLAIN.as_ref()) - .body(Vec::new()), - } + let protocol = crate::protocol::isolation::get(assets.clone(), *crypto_keys.aes_gcm().raw()); + pending.register_uri_scheme_protocol(schema, move |request, responder| { + protocol(request, UriSchemeResponder(responder)) }); } @@ -773,7 +738,7 @@ impl WindowManager { } else { asset }; - let mime_type = MimeType::parse(&final_data, &path); + let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path); Ok(Asset { bytes: final_data.to_vec(), mime_type, @@ -787,131 +752,6 @@ impl WindowManager { } } - fn prepare_uri_scheme_protocol( - &self, - window_origin: &str, - web_resource_request_handler: Option>, - ) -> UriSchemeProtocolHandler { - #[cfg(all(dev, mobile))] - let url = { - let mut url = self.get_url().as_str().to_string(); - if url.ends_with('/') { - url.pop(); - } - url - }; - #[cfg(not(all(dev, mobile)))] - let manager = self.clone(); - let window_origin = window_origin.to_string(); - - #[cfg(all(dev, mobile))] - #[derive(Clone)] - struct CachedResponse { - status: http::StatusCode, - headers: http::HeaderMap, - body: bytes::Bytes, - } - - #[cfg(all(dev, mobile))] - let response_cache = Arc::new(Mutex::new(HashMap::new())); - - Box::new(move |request| { - // use the entire URI as we are going to proxy the request - let path = if PROXY_DEV_SERVER { - request.uri() - } else { - // ignore query string and fragment - request.uri().split(&['?', '#'][..]).next().unwrap() - }; - - let path = path - .strip_prefix("tauri://localhost") - .map(|p| p.to_string()) - // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows - // where `$P` is not `localhost/*` - .unwrap_or_else(|| "".to_string()); - - let mut builder = - HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin); - - #[cfg(all(dev, mobile))] - let mut response = { - let decoded_path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - let url = format!("{url}{decoded_path}"); - #[allow(unused_mut)] - let mut client_builder = reqwest::ClientBuilder::new(); - #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] - { - client_builder = client_builder.danger_accept_invalid_certs(true); - } - let mut proxy_builder = client_builder - .build() - .unwrap() - .request(request.method().clone(), &url); - for (name, value) in request.headers() { - proxy_builder = proxy_builder.header(name, value); - } - match crate::async_runtime::block_on(proxy_builder.send()) { - Ok(r) => { - let mut response_cache_ = response_cache.lock().unwrap(); - let mut response = None; - if r.status() == http::StatusCode::NOT_MODIFIED { - response = response_cache_.get(&url); - } - let response = if let Some(r) = response { - r - } else { - let status = r.status(); - let headers = r.headers().clone(); - let body = crate::async_runtime::block_on(r.bytes())?; - let response = CachedResponse { - status, - headers, - body, - }; - response_cache_.insert(url.clone(), response); - response_cache_.get(&url).unwrap() - }; - for (name, value) in &response.headers { - builder = builder.header(name, value); - } - builder - .status(response.status) - .body(response.body.to_vec())? - } - Err(e) => { - debug_eprintln!("Failed to request {}: {}", url.as_str(), e); - return Err(Box::new(e)); - } - } - }; - - #[cfg(not(all(dev, mobile)))] - let mut response = { - let asset = manager.get_asset(path)?; - builder = builder.mimetype(&asset.mime_type); - if let Some(csp) = &asset.csp_header { - builder = builder.header("Content-Security-Policy", csp); - } - 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(all(not(dev), target_os = "linux"))] - if let Some(response_csp) = response.headers().get("Content-Security-Policy") { - let response_csp = String::from_utf8_lossy(response_csp.as_bytes()); - let html = String::from_utf8_lossy(response.body()); - let body = html.replacen(tauri_utils::html::CSP_TOKEN, &response_csp, 1); - *response.body_mut() = body.as_bytes().to_vec().into(); - } - Ok(response) - }) - } - fn initialization_script( &self, ipc_script: &str, @@ -1486,34 +1326,6 @@ struct ScaleFactorChanged { size: PhysicalSize, } -#[cfg(feature = "isolation")] -fn request_to_path(request: &tauri_runtime::http::Request, base_url: &str) -> String { - let mut path = request - .uri() - .split(&['?', '#'][..]) - // ignore query string - .next() - .unwrap() - .trim_start_matches(base_url) - .to_string(); - - if path.ends_with('/') { - path.pop(); - } - - let path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - - if path.is_empty() { - // if the url has no path, we should load `index.html` - "index.html".to_string() - } else { - // skip leading `/` - path.chars().skip(1).collect() - } -} - #[cfg(test)] mod tests { use super::replace_with_callback; diff --git a/core/tauri/src/menu/icon.rs b/core/tauri/src/menu/icon.rs index 699a0ea9f49e..b398499e10ce 100644 --- a/core/tauri/src/menu/icon.rs +++ b/core/tauri/src/menu/icon.rs @@ -207,7 +207,7 @@ impl IconMenuItem { /// - **Windows / Linux**: Unsupported. pub fn set_native_icon(&mut self, _icon: Option) -> crate::Result<()> { #[cfg(target_os = "macos")] - return run_main_thread!(self, |mut self_: Self| self_ + return run_main_thread!(self, |self_: Self| self_ .inner .set_native_icon(_icon.map(Into::into))); #[allow(unreachable_code)] diff --git a/core/tauri/src/asset_protocol.rs b/core/tauri/src/protocol/asset.rs similarity index 81% rename from core/tauri/src/asset_protocol.rs rename to core/tauri/src/protocol/asset.rs index ec961a9a1169..37ced2382571 100644 --- a/core/tauri/src/asset_protocol.rs +++ b/core/tauri/src/protocol/asset.rs @@ -4,45 +4,57 @@ use crate::path::SafePathBuf; use crate::scope::FsScope; +use crate::window::UriSchemeProtocolHandler; +use http::{header::*, status::StatusCode, Request, Response}; +use http_range::HttpRange; use rand::RngCore; -use std::io::SeekFrom; -use tauri_runtime::http::HttpRange; -use tauri_runtime::http::{ - header::*, status::StatusCode, MimeType, Request, Response, ResponseBuilder, -}; +use std::{borrow::Cow, io::SeekFrom}; use tauri_utils::debug_eprintln; +use tauri_utils::mime_type::MimeType; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; -use url::Position; -use url::Url; - -pub fn asset_protocol_handler( - request: &Request, - scope: FsScope, - window_origin: String, -) -> Result> { - let parsed_path = Url::parse(request.uri())?; - let filtered_path = &parsed_path[..Position::AfterPath]; - let path = filtered_path - .strip_prefix("asset://localhost/") - // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows - // where `$P` is not `localhost/*` - .unwrap_or(""); - let path = percent_encoding::percent_decode(path.as_bytes()) + +pub fn get(scope: FsScope, window_origin: String) -> UriSchemeProtocolHandler { + Box::new( + move |request, responder| match get_response(request, &scope, &window_origin) { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + http::Response::builder() + .status(http::StatusCode::BAD_REQUEST) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body(e.to_string().as_bytes().to_vec()) + .unwrap(), + ), + }, + ) +} + +fn get_response( + request: Request>, + scope: &FsScope, + window_origin: &str, +) -> Result>, Box> { + let path = percent_encoding::percent_decode(request.uri().path().as_bytes()) .decode_utf8_lossy() .to_string(); if let Err(e) = SafePathBuf::new(path.clone().into()) { debug_eprintln!("asset protocol path \"{}\" is not valid: {}", path, e); - return ResponseBuilder::new().status(403).body(Vec::new()); + return Response::builder() + .status(403) + .body(Vec::new().into()) + .map_err(Into::into); } if !scope.is_allowed(&path) { debug_eprintln!("asset protocol not configured to allow the path: {}", path); - return ResponseBuilder::new().status(403).body(Vec::new()); + return Response::builder() + .status(403) + .body(Vec::new().into()) + .map_err(Into::into); } - let mut resp = ResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin); + let mut resp = Response::builder().header("Access-Control-Allow-Origin", window_origin); let (mut file, len, mime_type, read_bytes) = crate::async_runtime::safe_block_on(async move { let mut file = File::open(&path).await?; @@ -84,10 +96,11 @@ pub fn asset_protocol_handler( resp = resp.header(ACCEPT_RANGES, "bytes"); let not_satisfiable = || { - ResponseBuilder::new() + Response::builder() .status(StatusCode::RANGE_NOT_SATISFIABLE) .header(CONTENT_RANGE, format!("bytes */{len}")) - .body(vec![]) + .body(vec![].into()) + .map_err(Into::into) }; // parse range header @@ -132,7 +145,7 @@ pub fn asset_protocol_handler( resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); resp = resp.header(CONTENT_LENGTH, end + 1 - start); resp = resp.status(StatusCode::PARTIAL_CONTENT); - resp.body(buf) + resp.body(buf.into()) } else { let ranges = ranges .iter() @@ -192,7 +205,7 @@ pub fn asset_protocol_handler( Ok::, anyhow::Error>(buf) })?; - resp.body(buf) + resp.body(buf.into()) } } else { // avoid reading the file if we already read it @@ -207,10 +220,10 @@ pub fn asset_protocol_handler( })? }; resp = resp.header(CONTENT_LENGTH, len); - resp.body(buf) + resp.body(buf.into()) }; - response + response.map_err(Into::into) } fn random_boundary() -> String { diff --git a/core/tauri/src/protocol/isolation.rs b/core/tauri/src/protocol/isolation.rs new file mode 100644 index 000000000000..ba3974031095 --- /dev/null +++ b/core/tauri/src/protocol/isolation.rs @@ -0,0 +1,76 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use http::header::CONTENT_TYPE; +use serialize_to_javascript::Template; +use tauri_utils::assets::{Assets, EmbeddedAssets}; + +use std::sync::Arc; + +use crate::{manager::PROCESS_IPC_MESSAGE_FN, window::UriSchemeProtocolHandler}; + +pub fn get(assets: Arc, aes_gcm_key: [u8; 32]) -> UriSchemeProtocolHandler { + Box::new(move |request, responder| { + let response = match request_to_path(&request).as_str() { + "index.html" => match assets.get(&"index.html".into()) { + Some(asset) => { + let asset = String::from_utf8_lossy(asset.as_ref()); + let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime { + runtime_aes_gcm_key: &aes_gcm_key, + process_ipc_message_fn: PROCESS_IPC_MESSAGE_FN, + }; + match template.render(asset.as_ref(), &Default::default()) { + Ok(asset) => http::Response::builder() + .header(CONTENT_TYPE, mime::TEXT_HTML.as_ref()) + .body(asset.into_string().as_bytes().to_vec()), + Err(_) => http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + } + } + + None => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + }, + _ => http::Response::builder() + .status(http::StatusCode::NOT_FOUND) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.as_ref()) + .body(Vec::new()), + }; + + if let Ok(r) = response { + responder.respond(r); + } else { + responder.respond( + http::Response::builder() + .status(http::StatusCode::BAD_REQUEST) + .body("failed to get response".as_bytes().to_vec()) + .unwrap(), + ); + } + }) +} + +fn request_to_path(request: &http::Request>) -> String { + let path = request + .uri() + .path() + .trim_start_matches('/') + .trim_end_matches('/'); + + let path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + + if path.is_empty() { + // if the url has no path, we should load `index.html` + "index.html".to_string() + } else { + // skip leading `/` + path.chars().skip(1).collect() + } +} diff --git a/core/tauri/src/protocol/mod.rs b/core/tauri/src/protocol/mod.rs new file mode 100644 index 000000000000..3f75e4de4990 --- /dev/null +++ b/core/tauri/src/protocol/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(feature = "protocol-asset")] +pub mod asset; +#[cfg(feature = "isolation")] +pub mod isolation; +pub mod tauri; diff --git a/core/tauri/src/protocol/tauri.rs b/core/tauri/src/protocol/tauri.rs new file mode 100644 index 000000000000..109d8a77debc --- /dev/null +++ b/core/tauri/src/protocol/tauri.rs @@ -0,0 +1,179 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::borrow::Cow; + +use http::{header::CONTENT_TYPE, Request, Response as HttpResponse, StatusCode}; + +use crate::{ + manager::{WindowManager, PROXY_DEV_SERVER}, + window::{UriSchemeProtocolHandler, WebResourceRequestHandler}, + Runtime, +}; + +#[cfg(all(dev, mobile))] +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +#[cfg(all(dev, mobile))] +#[derive(Clone)] +struct CachedResponse { + status: http::StatusCode, + headers: http::HeaderMap, + body: bytes::Bytes, +} + +pub fn get( + manager: &WindowManager, + window_origin: &str, + web_resource_request_handler: Option>, +) -> UriSchemeProtocolHandler { + #[cfg(all(dev, mobile))] + let url = { + let mut url = manager.get_url().as_str().to_string(); + if url.ends_with('/') { + url.pop(); + } + url + }; + + let manager = manager.clone(); + let window_origin = window_origin.to_string(); + + #[cfg(all(dev, mobile))] + let response_cache = Arc::new(Mutex::new(HashMap::new())); + + Box::new(move |request, responder| { + match get_response( + request, + &manager, + &window_origin, + web_resource_request_handler.as_deref(), + #[cfg(all(dev, mobile))] + (&url, &response_cache), + ) { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + HttpResponse::builder() + .status(StatusCode::BAD_REQUEST) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .body(e.to_string().as_bytes().to_vec()) + .unwrap(), + ), + } + }) +} + +fn get_response( + request: Request>, + manager: &WindowManager, + window_origin: &str, + web_resource_request_handler: Option<&WebResourceRequestHandler>, + #[cfg(all(dev, mobile))] (url, response_cache): ( + &str, + &Arc>>, + ), +) -> Result>, Box> { + // use the entire URI as we are going to proxy the request + let path = if PROXY_DEV_SERVER { + request.uri().to_string() + } else { + // ignore query string and fragment + request + .uri() + .to_string() + .split(&['?', '#'][..]) + .next() + .unwrap() + .into() + }; + + let path = path + .strip_prefix("tauri://localhost") + .map(|p| p.to_string()) + // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows + // where `$P` is not `localhost/*` + .unwrap_or_else(|| "".to_string()); + + let mut builder = HttpResponse::builder().header("Access-Control-Allow-Origin", window_origin); + + #[cfg(all(dev, mobile))] + let mut response = { + let decoded_path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let url = format!("{url}{decoded_path}"); + #[allow(unused_mut)] + let mut client_builder = reqwest::ClientBuilder::new(); + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + { + client_builder = client_builder.danger_accept_invalid_certs(true); + } + let mut proxy_builder = client_builder + .build() + .unwrap() + .request(request.method().clone(), &url); + for (name, value) in request.headers() { + proxy_builder = proxy_builder.header(name, value); + } + match crate::async_runtime::block_on(proxy_builder.send()) { + Ok(r) => { + let mut response_cache_ = response_cache.lock().unwrap(); + let mut response = None; + if r.status() == http::StatusCode::NOT_MODIFIED { + response = response_cache_.get(&url); + } + let response = if let Some(r) = response { + r + } else { + let status = r.status(); + let headers = r.headers().clone(); + let body = crate::async_runtime::block_on(r.bytes())?; + let response = CachedResponse { + status, + headers, + body, + }; + response_cache_.insert(url.clone(), response); + response_cache_.get(&url).unwrap() + }; + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + builder + .status(response.status) + .body(response.body.to_vec().into())? + } + Err(e) => { + tauri_utils::debug_eprintln!("Failed to request {}: {}", url.as_str(), e); + return Err(Box::new(e)); + } + } + }; + + #[cfg(not(all(dev, mobile)))] + let mut response = { + let asset = manager.get_asset(path)?; + builder = builder.header(CONTENT_TYPE, &asset.mime_type); + if let Some(csp) = &asset.csp_header { + builder = builder.header("Content-Security-Policy", csp); + } + builder.body(asset.bytes.into())? + }; + 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(all(not(dev), target_os = "linux"))] + if let Some(response_csp) = response.headers().get("Content-Security-Policy") { + let response_csp = String::from_utf8_lossy(response_csp.as_bytes()); + let html = String::from_utf8_lossy(response.body()); + let body = html.replacen(tauri_utils::html::CSP_TOKEN, &response_csp, 1); + *response.body_mut() = body.as_bytes().to_vec().into(); + } + + Ok(response) +} diff --git a/core/tauri/src/scope/ipc.rs b/core/tauri/src/scope/ipc.rs index b0eea0bbd172..363c257cdb8c 100644 --- a/core/tauri/src/scope/ipc.rs +++ b/core/tauri/src/scope/ipc.rs @@ -231,7 +231,7 @@ mod tests { assert_ipc_response( &window, path_is_absolute_request(), - Err(&crate::window::ipc_scope_not_found_error_message( + Err(crate::window::ipc_scope_not_found_error_message( "main", "https://tauri.app/", )), @@ -248,7 +248,7 @@ mod tests { assert_ipc_response( &window, path_is_absolute_request(), - Err(&crate::window::ipc_scope_window_error_message("main")), + Err(crate::window::ipc_scope_window_error_message("main")), ); } @@ -262,7 +262,7 @@ mod tests { assert_ipc_response( &window, path_is_absolute_request(), - Err(&crate::window::ipc_scope_domain_error_message( + Err(crate::window::ipc_scope_domain_error_message( "https://tauri.app/", )), ); @@ -286,7 +286,7 @@ mod tests { assert_ipc_response( &window, path_is_absolute_request(), - Err(&crate::window::ipc_scope_domain_error_message( + Err(crate::window::ipc_scope_domain_error_message( "https://blog.tauri.app/", )), ); @@ -299,7 +299,7 @@ mod tests { assert_ipc_response( &window, path_is_absolute_request(), - Err(&crate::window::ipc_scope_not_found_error_message( + Err(crate::window::ipc_scope_not_found_error_message( "test", "https://dev.tauri.app/", )), @@ -340,7 +340,7 @@ mod tests { assert_ipc_response( &window, plugin_test_request(), - Err(&format!("plugin {PLUGIN_NAME} not found")), + Err(format!("plugin {PLUGIN_NAME} not found")), ); } diff --git a/core/tauri/src/test/mod.rs b/core/tauri/src/test/mod.rs index 7b0a20e4385d..ff163eb2a3fc 100644 --- a/core/tauri/src/test/mod.rs +++ b/core/tauri/src/test/mod.rs @@ -220,23 +220,30 @@ pub fn mock_app() -> App { /// } /// } /// ``` -pub fn assert_ipc_response( +pub fn assert_ipc_response( window: &Window, request: InvokeRequest, expected: Result, ) { - let rx = window.clone().on_message(request); - let response = rx.recv().unwrap(); + let (tx, rx) = std::sync::mpsc::sync_channel(1); + window.clone().on_message( + request, + Box::new(move |_window, _cmd, response, _callback, _error| { + assert_eq!( + match response { + InvokeResponse::Ok(b) => Ok(b.into_json()), + InvokeResponse::Err(e) => Err(e.0), + }, + expected + .map(|e| serde_json::to_value(e).unwrap()) + .map_err(|e| serde_json::to_value(e).unwrap()) + ); - assert_eq!( - match response { - InvokeResponse::Ok(b) => Ok(b.into_json()), - InvokeResponse::Err(e) => Err(e.0), - }, - expected - .map(|e| serde_json::to_value(e).unwrap()) - .map_err(|e| serde_json::to_value(e).unwrap()) + tx.send(()).unwrap(); + }), ); + + rx.recv().unwrap(); } #[cfg(test)] diff --git a/core/tauri/src/window.rs b/core/tauri/src/window.rs index bfb22f47c86e..351ce0d0f317 100644 --- a/core/tauri/src/window.rs +++ b/core/tauri/src/window.rs @@ -11,15 +11,15 @@ use url::Url; #[cfg(target_os = "macos")] use crate::TitleBarStyle; use crate::{ - app::AppHandle, + app::{AppHandle, UriSchemeResponder}, command::{CommandArg, CommandItem}, event::{Event, EventHandler}, ipc::{ - CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver, InvokeResponse, + CallbackFn, Invoke, InvokeBody, InvokeError, InvokeMessage, InvokeResolver, + OwnedInvokeResponder, }, manager::WindowManager, runtime::{ - http::{Request as HttpRequest, Response as HttpResponse}, monitor::Monitor as RuntimeMonitor, webview::{WebviewAttributes, WindowBuilder as _}, window::{ @@ -43,6 +43,7 @@ use crate::{ CursorIcon, Icon, }; +use http::{Request as HttpRequest, Response as HttpResponse}; use serde::Serialize; #[cfg(windows)] use windows::Win32::Foundation::HWND; @@ -50,20 +51,19 @@ use windows::Win32::Foundation::HWND; use tauri_macros::default_runtime; use std::{ + borrow::Cow, collections::{HashMap, HashSet}, fmt, hash::{Hash, Hasher}, path::PathBuf, - sync::{ - mpsc::{sync_channel, Receiver}, - Arc, Mutex, - }, + sync::{Arc, Mutex}, }; -pub(crate) type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync; +pub(crate) type WebResourceRequestHandler = + dyn Fn(HttpRequest>, &mut HttpResponse>) + Send + Sync; pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send; pub(crate) type UriSchemeProtocolHandler = - Box Result> + Send + Sync>; + Box>, UriSchemeResponder) + Send + Sync>; #[derive(Clone, Serialize)] struct WindowCreatedEvent { @@ -266,15 +266,15 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// ```rust,no_run /// use tauri::{ /// utils::config::{Csp, CspDirectiveSources, WindowUrl}, - /// http::header::HeaderValue, /// window::WindowBuilder, /// }; + /// use http::header::HeaderValue; /// 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 request.uri().scheme_str() == Some("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") { @@ -291,7 +291,9 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// Ok(()) /// }); /// ``` - pub fn on_web_resource_request( + pub fn on_web_resource_request< + F: Fn(HttpRequest>, &mut HttpResponse>) + Send + Sync + 'static, + >( mut self, f: F, ) -> Self { @@ -306,9 +308,9 @@ impl<'a, R: Runtime> WindowBuilder<'a, R> { /// ```rust,no_run /// use tauri::{ /// utils::config::{Csp, CspDirectiveSources, WindowUrl}, - /// http::header::HeaderValue, /// window::WindowBuilder, /// }; + /// use http::header::HeaderValue; /// use std::collections::HashMap; /// tauri::Builder::default() /// .setup(|app| { @@ -892,7 +894,7 @@ pub struct Window { /// The webview window created by the runtime. pub(crate) window: DetachedWindow, /// The manager to associate this webview window with. - manager: WindowManager, + pub(crate) manager: WindowManager, pub(crate) app_handle: AppHandle, js_event_listeners: Arc>>>, // The menu set for this window @@ -2067,7 +2069,7 @@ impl Window { } /// Handles this window receiving an [`InvokeRequest`]. - pub fn on_message(self, request: InvokeRequest) -> Receiver { + pub fn on_message(self, request: InvokeRequest, responder: Box>) { let manager = self.manager.clone(); let current_url = self.url(); let is_local = self.is_local_url(¤t_url); @@ -2090,75 +2092,20 @@ impl Window { } }; - let (tx, rx) = sync_channel(1); - let custom_responder = self.manager.invoke_responder(); let resolver = InvokeResolver::new( self.clone(), - Arc::new( + Arc::new(Mutex::new(Some(Box::new( #[allow(unused_variables)] move |window: Window, cmd, response, callback, error| { - if (cfg!(target_os = "macos") && window.url().scheme() == "https") - || !cfg!(ipc_custom_protocol) - { - use crate::ipc::{ - format_callback::{ - format as format_callback, format_result as format_callback_result, - }, - Channel, - }; - use serde_json::Value as JsonValue; - - // the channel data command is the only command that uses a custom protocol on Linux - if custom_responder.is_none() && cmd != crate::ipc::channel::FETCH_CHANNEL_DATA_COMMAND - { - fn responder_eval( - window: &Window, - js: crate::api::Result, - error: CallbackFn, - ) { - let eval_js = match js { - Ok(js) => js, - Err(e) => format_callback(error, &e.to_string()) - .expect("unable to serialize response error string to json"), - }; - - let _ = window.eval(&eval_js); - } - - match &response { - InvokeResponse::Ok(InvokeBody::Json(v)) => { - if matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) { - let _ = Channel::from_ipc(window.clone(), callback).send(v); - } else { - responder_eval( - &window, - format_callback_result(Result::<_, ()>::Ok(v), callback, error), - error, - ) - } - } - InvokeResponse::Ok(InvokeBody::Raw(v)) => { - let _ = - Channel::from_ipc(window.clone(), callback).send(InvokeBody::Raw(v.clone())); - } - InvokeResponse::Err(e) => responder_eval( - &window, - format_callback_result(Result::<(), _>::Err(&e.0), callback, error), - error, - ), - } - } - } - if let Some(responder) = &custom_responder { - (responder)(window, cmd, &response, callback, error); + (responder)(&window, &cmd, &response, callback, error); } - let _ = tx.send(response); + responder(window, cmd, response, callback, error); }, - ), + )))), request.cmd.clone(), request.callback, request.error, @@ -2208,7 +2155,7 @@ impl Window { .unwrap_or(true)) { invoke.resolver.reject(IPC_SCOPE_DOES_NOT_ALLOW); - return rx; + return; } let command = invoke.message.command.clone(); @@ -2252,7 +2199,7 @@ impl Window { }, ) { resolver.reject(e.to_string()); - return rx; + return; } } } @@ -2269,8 +2216,6 @@ impl Window { } } } - - rx } /// Evaluates JavaScript on this window. diff --git a/examples/api/src-tauri/Cargo.lock b/examples/api/src-tauri/Cargo.lock index 01f36e50fd94..4e8e334fd5a5 100644 --- a/examples/api/src-tauri/Cargo.lock +++ b/examples/api/src-tauri/Cargo.lock @@ -3399,7 +3399,7 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tauri" -version = "2.0.0-alpha.10" +version = "2.0.0-alpha.11" dependencies = [ "anyhow", "bytes", @@ -3412,6 +3412,7 @@ dependencies = [ "gtk", "heck", "http", + "http-range", "ico", "infer 0.15.0", "jni", @@ -3449,7 +3450,7 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.0.0-alpha.6" +version = "2.0.0-alpha.8" dependencies = [ "anyhow", "cargo_toml", @@ -3469,7 +3470,7 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.0.0-alpha.6" +version = "2.0.0-alpha.7" dependencies = [ "base64", "brotli", @@ -3493,7 +3494,7 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.0.0-alpha.6" +version = "2.0.0-alpha.7" dependencies = [ "heck", "proc-macro2", @@ -3549,11 +3550,10 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.13.0-alpha.6" +version = "1.0.0-alpha.0" dependencies = [ "gtk", "http", - "http-range", "jni", "rand 0.8.5", "raw-window-handle", @@ -3568,10 +3568,11 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.13.0-alpha.6" +version = "1.0.0-alpha.0" dependencies = [ "cocoa 0.24.1", "gtk", + "http", "jni", "percent-encoding", "rand 0.8.5", @@ -3587,7 +3588,7 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.0.0-alpha.6" +version = "2.0.0-alpha.7" dependencies = [ "aes-gcm", "brotli", @@ -4529,9 +4530,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6289018fa3cbc051c13f4ae1a102d80c3f35a527456c75567eb2cad6989020" +checksum = "41fc00d1511c9ff5b600a6c6bde254eb39b9fcc5c0369b71a8efd5ff807bf937" dependencies = [ "base64", "block", diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 8af633098e91..9a06be515b09 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -63,8 +63,8 @@ pub fn run_app) + Send + 'static>( #[cfg(desktop)] app.manage(PopupMenu( tauri::menu::MenuBuilder::new(app) - .check("Tauri is awesome!") - .text("Do something") + .check("check", "Tauri is awesome!") + .text("text", "Do something") .copy() .build()?, )); diff --git a/examples/streaming/main.rs b/examples/streaming/main.rs index 087aa4566bbc..e86b235b75f6 100644 --- a/examples/streaming/main.rs +++ b/examples/streaming/main.rs @@ -4,16 +4,16 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; +use http_range::HttpRange; use std::sync::{Arc, Mutex}; +use std::{ + io::{Read, Seek, SeekFrom, Write}, + path::PathBuf, + process::{Command, Stdio}, +}; fn main() { - use std::{ - io::{Read, Seek, SeekFrom, Write}, - path::PathBuf, - process::{Command, Stdio}, - }; - use tauri::http::{header::*, status::StatusCode, HttpRange, ResponseBuilder}; - let video_file = PathBuf::from("test_video.mp4"); let video_url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; @@ -41,144 +41,17 @@ fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![video_uri]) - .register_uri_scheme_protocol("stream", move |_app, request| { - // get the file path - let path = request.uri().strip_prefix("stream://localhost/").unwrap(); - let path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - - if path != "test_video.mp4" { - // return error 404 if it's not our video - return ResponseBuilder::new().status(404).body(Vec::new()); - } - - let mut file = std::fs::File::open(&path)?; - - // get file length - let len = { - let old_pos = file.stream_position()?; - let len = file.seek(SeekFrom::End(0))?; - file.seek(SeekFrom::Start(old_pos))?; - len - }; - - let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4"); - - // if the webview sent a range header, we need to send a 206 in return - // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. - let response = if let Some(range_header) = request.headers().get("range") { - let not_satisfiable = || { + .register_asynchronous_uri_scheme_protocol("stream", move |_app, request, responder| { + match get_stream_response(request, &boundary_id) { + Ok(http_response) => responder.respond(http_response), + Err(e) => responder.respond( ResponseBuilder::new() - .status(StatusCode::RANGE_NOT_SATISFIABLE) - .header(CONTENT_RANGE, format!("bytes */{len}")) - .body(vec![]) - }; - - // parse range header - let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) { - ranges - .iter() - // map the output back to spec range , example: 0-499 - .map(|r| (r.start, r.start + r.length - 1)) - .collect::>() - } else { - return not_satisfiable(); - }; - - /// The Maximum bytes we send in one range - const MAX_LEN: u64 = 1000 * 1024; - - if ranges.len() == 1 { - let &(start, mut end) = ranges.first().unwrap(); - - // check if a range is not satisfiable - // - // this should be already taken care of by HttpRange::parse - // but checking here again for extra assurance - if start >= len || end >= len || end < start { - return not_satisfiable(); - } - - // adjust end byte for MAX_LEN - end = start + (end - start).min(len - start).min(MAX_LEN - 1); - - // calculate number of bytes needed to be read - let bytes_to_read = end + 1 - start; - - // allocate a buf with a suitable capacity - let mut buf = Vec::with_capacity(bytes_to_read as usize); - // seek the file to the starting byte - file.seek(SeekFrom::Start(start))?; - // read the needed bytes - file.take(bytes_to_read).read_to_end(&mut buf)?; - - resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); - resp = resp.header(CONTENT_LENGTH, end + 1 - start); - resp = resp.status(StatusCode::PARTIAL_CONTENT); - resp.body(buf) - } else { - let mut buf = Vec::new(); - let ranges = ranges - .iter() - .filter_map(|&(start, mut end)| { - // filter out unsatisfiable ranges - // - // this should be already taken care of by HttpRange::parse - // but checking here again for extra assurance - if start >= len || end >= len || end < start { - None - } else { - // adjust end byte for MAX_LEN - end = start + (end - start).min(len - start).min(MAX_LEN - 1); - Some((start, end)) - } - }) - .collect::>(); - - let mut id = boundary_id.lock().unwrap(); - *id += 1; - let boundary = format!("sadasq2e{id}"); - let boundary_sep = format!("\r\n--{boundary}\r\n"); - let boundary_closer = format!("\r\n--{boundary}\r\n"); - - resp = resp.header( - CONTENT_TYPE, - format!("multipart/byteranges; boundary={boundary}"), - ); - - for (end, start) in ranges { - // a new range is being written, write the range boundary - buf.write_all(boundary_sep.as_bytes())?; - - // write the needed headers `Content-Type` and `Content-Range` - buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?; - buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; - - // write the separator to indicate the start of the range body - buf.write_all("\r\n".as_bytes())?; - - // calculate number of bytes needed to be read - let bytes_to_read = end + 1 - start; - - let mut local_buf = vec![0_u8; bytes_to_read as usize]; - file.seek(SeekFrom::Start(start))?; - file.read_exact(&mut local_buf)?; - buf.extend_from_slice(&local_buf); - } - // all ranges have been written, write the closing boundary - buf.write_all(boundary_closer.as_bytes())?; - - resp.body(buf) - } - } else { - resp = resp.header(CONTENT_LENGTH, len); - let mut buf = Vec::with_capacity(len as usize); - file.read_to_end(&mut buf)?; - resp.body(buf) - }; - - response + .status(StatusCode::BAD_REQUEST) + .header(CONTENT_TYPE, "text/plain") + .body(e.to_string().as_bytes().to_vec()) + .unwrap(), + ), + } }) .run(tauri::generate_context!( "../../examples/streaming/tauri.conf.json" @@ -200,3 +73,146 @@ fn video_uri() -> (&'static str, std::path::PathBuf) { #[cfg(not(feature = "protocol-asset"))] ("stream", "test_video.mp4".into()) } + +fn get_stream_response( + request: http::Request>, + boundary_id: &Arc>, +) -> Result>, Box> { + // get the file path + let path = request.uri().path(); + let path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + + if path != "test_video.mp4" { + // return error 404 if it's not our video + return Ok(ResponseBuilder::new().status(404).body(Vec::new())?); + } + + let mut file = std::fs::File::open(&path)?; + + // get file length + let len = { + let old_pos = file.stream_position()?; + let len = file.seek(SeekFrom::End(0))?; + file.seek(SeekFrom::Start(old_pos))?; + len + }; + + let mut resp = ResponseBuilder::new().header(CONTENT_TYPE, "video/mp4"); + + // if the webview sent a range header, we need to send a 206 in return + // Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers. + let http_response = if let Some(range_header) = request.headers().get("range") { + let not_satisfiable = || { + ResponseBuilder::new() + .status(StatusCode::RANGE_NOT_SATISFIABLE) + .header(CONTENT_RANGE, format!("bytes */{len}")) + .body(vec![]) + }; + + // parse range header + let ranges = if let Ok(ranges) = HttpRange::parse(range_header.to_str()?, len) { + ranges + .iter() + // map the output back to spec range , example: 0-499 + .map(|r| (r.start, r.start + r.length - 1)) + .collect::>() + } else { + return Ok(not_satisfiable()?); + }; + + /// The Maximum bytes we send in one range + const MAX_LEN: u64 = 1000 * 1024; + + if ranges.len() == 1 { + let &(start, mut end) = ranges.first().unwrap(); + + // check if a range is not satisfiable + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + return Ok(not_satisfiable()?); + } + + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + // allocate a buf with a suitable capacity + let mut buf = Vec::with_capacity(bytes_to_read as usize); + // seek the file to the starting byte + file.seek(SeekFrom::Start(start))?; + // read the needed bytes + file.take(bytes_to_read).read_to_end(&mut buf)?; + + resp = resp.header(CONTENT_RANGE, format!("bytes {start}-{end}/{len}")); + resp = resp.header(CONTENT_LENGTH, end + 1 - start); + resp = resp.status(StatusCode::PARTIAL_CONTENT); + resp.body(buf) + } else { + let mut buf = Vec::new(); + let ranges = ranges + .iter() + .filter_map(|&(start, mut end)| { + // filter out unsatisfiable ranges + // + // this should be already taken care of by HttpRange::parse + // but checking here again for extra assurance + if start >= len || end >= len || end < start { + None + } else { + // adjust end byte for MAX_LEN + end = start + (end - start).min(len - start).min(MAX_LEN - 1); + Some((start, end)) + } + }) + .collect::>(); + + let mut id = boundary_id.lock().unwrap(); + *id += 1; + let boundary = format!("sadasq2e{id}"); + let boundary_sep = format!("\r\n--{boundary}\r\n"); + let boundary_closer = format!("\r\n--{boundary}\r\n"); + + resp = resp.header( + CONTENT_TYPE, + format!("multipart/byteranges; boundary={boundary}"), + ); + + for (end, start) in ranges { + // a new range is being written, write the range boundary + buf.write_all(boundary_sep.as_bytes())?; + + // write the needed headers `Content-Type` and `Content-Range` + buf.write_all(format!("{CONTENT_TYPE}: video/mp4\r\n").as_bytes())?; + buf.write_all(format!("{CONTENT_RANGE}: bytes {start}-{end}/{len}\r\n").as_bytes())?; + + // write the separator to indicate the start of the range body + buf.write_all("\r\n".as_bytes())?; + + // calculate number of bytes needed to be read + let bytes_to_read = end + 1 - start; + + let mut local_buf = vec![0_u8; bytes_to_read as usize]; + file.seek(SeekFrom::Start(start))?; + file.read_exact(&mut local_buf)?; + buf.extend_from_slice(&local_buf); + } + // all ranges have been written, write the closing boundary + buf.write_all(boundary_closer.as_bytes())?; + + resp.body(buf) + } + } else { + resp = resp.header(CONTENT_LENGTH, len); + let mut buf = Vec::with_capacity(len as usize); + file.read_to_end(&mut buf)?; + resp.body(buf) + }; + + http_response.map_err(Into::into) +}