refactor(core): allow custom protocol handler to resolve async (#7754)

This commit is contained in:
Lucas Fernandes Nogueira
2023-09-06 15:53:03 -03:00
committed by GitHub
parent b75a1210be
commit 0d63732b96
36 changed files with 948 additions and 1169 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
Changed `Builder::register_uri_scheme_protocol` to return a `http::Response` instead of `Result<http::Response>`. To return an error response, manually create a response with status code >= 400.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:bug
---
Fixes invalid header value type when requesting IPC body through a channel.

View File

@@ -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.

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:breaking
---
Changed `Builder::invoke_system` to take references instead of owned values.

View File

@@ -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.

View File

@@ -0,0 +1,5 @@
---
"tauri-runtime": patch:enhance
---
Changed custom protocol closure type to enable asynchronous usage.

View File

@@ -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.

5
.changes/wry-0.32.md Normal file
View File

@@ -0,0 +1,5 @@
---
"tauri-runtime-wry": patch:enhance
---
Update wry to 0.32 to include asynchronous custom protocol support.

View File

@@ -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"

View File

@@ -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<T: UserEvent> fmt::Debug for Context<T> {
}
}
struct HttpRequestWrapper(HttpRequest);
impl From<&WryRequest<Vec<u8>>> for HttpRequestWrapper {
fn from(req: &WryRequest<Vec<u8>>) -> 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<Cow<'static, [u8]>>);
impl From<HttpResponse> 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<DeviceEventFilter> for DeviceEventFilterWrapper {
@@ -2701,11 +2665,13 @@ fn create_webview<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
}
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 {

View File

@@ -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" }

View File

@@ -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;

View File

@@ -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<u8>,
}
/// 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<HeaderValue>,
}
impl Request {
/// Creates a new blank `Request` with the body
#[inline]
pub fn new(body: Vec<u8>) -> 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<u8>) -> 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<HeaderValue> {
&self.head.headers
}
/// Returns a reference to the associated HTTP body.
#[inline]
pub fn body(&self) -> &Vec<u8> {
&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<u8>) {
(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()
}
}

View File

@@ -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<T> = core::result::Result<T, Box<dyn std::error::Error>>;
/// 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<HeaderValue>,
/// The response's mimetype type.
pub mimetype: Option<String>,
}
/// 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<ResponseParts>,
}
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<String>) {
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<HeaderValue> {
&mut self.head.headers
}
/// Returns a reference to the associated header field map.
#[inline]
pub fn headers(&self) -> &HeaderMap<HeaderValue> {
&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<T>(self, status: T) -> Self
where
StatusCode: TryFrom<T>,
<StatusCode as TryFrom<T>>::Error: Into<crate::Error>,
{
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<K, V>(self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<crate::Error>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<crate::Error>,
{
self.and_then(move |mut head| {
let name = <HeaderName as TryFrom<K>>::try_from(key).map_err(Into::into)?;
let value = <HeaderValue as TryFrom<V>>::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<Cow<'static, [u8]>>) -> Result<Response> {
self.inner.map(move |head| Response {
head,
body: body.into(),
})
}
// private
fn and_then<F>(self, func: F) -> Self
where
F: FnOnce(ResponseParts) -> Result<ResponseParts>,
{
Builder {
inner: self.inner.and_then(func),
}
}
}
impl Default for Builder {
#[inline]
fn default() -> Builder {
Builder {
inner: Ok(ResponseParts::new()),
}
}
}

View File

@@ -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}")]

View File

@@ -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<HttpResponse, Box<dyn std::error::Error>> + Send + Sync + 'static;
type UriSchemeProtocol = dyn Fn(HttpRequest<Vec<u8>>, Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>) + Send>)
+ Send
+ Sync
+ 'static;
type WebResourceRequestHandler = dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync;
type WebResourceRequestHandler =
dyn Fn(HttpRequest<Vec<u8>>, &mut HttpResponse<Cow<'static, [u8]>>) + Send + Sync;
type NavigationHandler = dyn Fn(&Url) -> bool + Send;
@@ -306,16 +310,20 @@ impl<T: UserEvent, R: Runtime<T>> PendingWindow<T, R> {
pub fn register_uri_scheme_protocol<
N: Into<String>,
H: Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync + 'static,
H: Fn(HttpRequest<Vec<u8>>, Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>) + 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")]

View File

@@ -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" ]

View File

@@ -32,7 +32,6 @@
) &&
!(osName === 'macos' && location.protocol === 'https:')
) {
console.log('process')
const {
contentType,
data

View File

@@ -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')

View File

@@ -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<R: Runtime> Builder<R> {
#[must_use]
pub fn invoke_system<F>(mut self, initialization_script: String, responder: F) -> Self
where
F: Fn(Window<R>, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static,
F: Fn(&Window<R>, &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<R: Runtime> Builder<R> {
/// # 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<String>,
H: Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
+ Send
+ Sync
+ 'static,
T: Into<Cow<'static, [u8]>>,
H: Fn(&AppHandle<R>, HttpRequest<Vec<u8>>) -> HttpResponse<T> + 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<String>,
H: Fn(&AppHandle<R>, HttpRequest<Vec<u8>>, UriSchemeResponder) + Send + Sync + 'static,
>(
mut self,
uri_scheme: N,
@@ -1579,6 +1644,17 @@ impl<R: Runtime> Builder<R> {
}
}
pub(crate) type UriSchemeResponderFn = Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>) + Send>;
pub struct UriSchemeResponder(pub(crate) UriSchemeResponderFn);
impl UriSchemeResponder {
/// Resolves the request with the given response.
pub fn respond<T: Into<Cow<'static, [u8]>>>(self, response: HttpResponse<T>) {
let (parts, body) = response.into_parts();
(self.0)(HttpResponse::from_parts(parts, body.into()))
}
}
#[cfg(target_os = "macos")]
fn init_app_menu<R: Runtime>(menu: &Menu<R>) -> crate::Result<()> {
menu.inner().init_for_nsapp();

View File

@@ -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
))
})

View File

@@ -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<R> = dyn Fn(Invoke<R>) -> bool + Send + Sync + 'static;
/// A closure that is responsible for respond a JS message.
pub type InvokeResponder<R> =
dyn Fn(Window<R>, String, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
type OwnedInvokeResponder<R> =
dyn Fn(Window<R>, String, InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
dyn Fn(&Window<R>, &str, &InvokeResponse, CallbackFn, CallbackFn) + Send + Sync + 'static;
/// Similar to [`InvokeResponder`] but taking owned arguments.
pub type OwnedInvokeResponder<R> =
dyn FnOnce(Window<R>, String, InvokeResponse, CallbackFn, CallbackFn) + Send + 'static;
/// Possible values of an IPC payload.
#[derive(Debug, Clone)]
@@ -225,7 +227,7 @@ impl From<InvokeError> for InvokeResponse {
#[default_runtime(crate::Wry, wry)]
pub struct InvokeResolver<R: Runtime> {
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
cmd: String,
pub(crate) callback: CallbackFn,
pub(crate) error: CallbackFn,
@@ -246,7 +248,7 @@ impl<R: Runtime> Clone for InvokeResolver<R> {
impl<R: Runtime> InvokeResolver<R> {
pub(crate) fn new(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
cmd: String,
callback: CallbackFn,
error: CallbackFn,
@@ -348,7 +350,7 @@ impl<R: Runtime> InvokeResolver<R> {
/// 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<T, F>(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
task: F,
cmd: String,
success_callback: CallbackFn,
@@ -370,7 +372,7 @@ impl<R: Runtime> InvokeResolver<R> {
pub(crate) fn return_closure<T: IpcResponse, F: FnOnce() -> Result<T, InvokeError>>(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
f: F,
cmd: String,
success_callback: CallbackFn,
@@ -388,13 +390,19 @@ impl<R: Runtime> InvokeResolver<R> {
pub(crate) fn return_result(
window: Window<R>,
responder: Arc<OwnedInvokeResponder<R>>,
responder: Arc<Mutex<Option<Box<OwnedInvokeResponder<R>>>>>,
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,
);
}
}

View File

@@ -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<R: Runtime>(
}
pub fn get<R: Runtime>(manager: WindowManager<R>, 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<Cow<'static, [u8]>>| {
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<R: Runtime>(manager: WindowManager<R>, 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<R: Runtime>(manager: WindowManager<R>, 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<R: Runtime>(message: String, manager: &WindowManager<R>, l
.unwrap_or_else(|| serde_json::from_str::<Message>(&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<R: Runtime>(
window: &crate::Window<R>,
js: crate::api::Result<String>,
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<R: Runtime>(message: String, manager: &WindowManager<R>, l
}
}
fn handle_ipc_request<R: Runtime>(
request: &HttpRequest,
manager: &WindowManager<R>,
label: &str,
) -> std::result::Result<InvokeResponse, String> {
if let Some(window) = manager.get_window(label) {
// TODO: consume instead
#[allow(unused_mut)]
let mut body = request.body().clone();
fn parse_invoke_request<R: Runtime>(
#[allow(unused_variables)] manager: &WindowManager<R>,
request: HttpRequest<Vec<u8>>,
) -> std::result::Result<InvokeRequest, String> {
#[allow(unused_mut)]
let (parts, mut body) = request.into_parts();
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();
let cmd = parts.uri.path().trim_start_matches('/');
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())?;
}
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::<serde_json::Value>(&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 payload = InvokeRequest {
cmd,
callback,
error,
body,
headers: request.headers().clone(),
};
let rx = window.on_message(payload);
Ok(rx.recv().unwrap())
} else {
Err("window not found".into())
// 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 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::<serde_json::Value>(&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 payload = InvokeRequest {
cmd,
callback,
error,
body,
headers: parts.headers,
};
Ok(payload)
}

View File

@@ -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;

View File

@@ -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<R: Runtime> {
/// Handler for protocol
#[allow(clippy::type_complexity)]
pub protocol: Box<
dyn Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
+ Send
+ Sync,
>,
pub protocol: Box<dyn Fn(&AppHandle<R>, HttpRequest<Vec<u8>>, UriSchemeResponder) + Send + Sync>,
}
#[default_runtime(crate::Wry, wry)]
@@ -604,8 +599,12 @@ impl<R: Runtime> WindowManager<R> {
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<R: Runtime> WindowManager<R> {
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::<crate::Scopes>().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<R: Runtime> WindowManager<R> {
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<R: Runtime> WindowManager<R> {
} 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<R: Runtime> WindowManager<R> {
}
}
fn prepare_uri_scheme_protocol(
&self,
window_origin: &str,
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
) -> 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<u32>,
}
#[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;

View File

@@ -207,7 +207,7 @@ impl<R: Runtime> IconMenuItem<R> {
/// - **Windows / Linux**: Unsupported.
pub fn set_native_icon(&mut self, _icon: Option<NativeIcon>) -> 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)]

View File

@@ -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<Response, Box<dyn std::error::Error>> {
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<Vec<u8>>,
scope: &FsScope,
window_origin: &str,
) -> Result<Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
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::<Vec<u8>, 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 {

View File

@@ -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<EmbeddedAssets>, 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<Vec<u8>>) -> 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()
}
}

View File

@@ -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;

View File

@@ -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<R: Runtime>(
manager: &WindowManager<R>,
window_origin: &str,
web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
) -> 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<R: Runtime>(
request: Request<Vec<u8>>,
manager: &WindowManager<R>,
window_origin: &str,
web_resource_request_handler: Option<&WebResourceRequestHandler>,
#[cfg(all(dev, mobile))] (url, response_cache): (
&str,
&Arc<Mutex<HashMap<String, CachedResponse>>>,
),
) -> Result<HttpResponse<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
// 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)
}

View File

@@ -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")),
);
}

View File

@@ -220,23 +220,30 @@ pub fn mock_app() -> App<MockRuntime> {
/// }
/// }
/// ```
pub fn assert_ipc_response<T: Serialize + Debug>(
pub fn assert_ipc_response<T: Serialize + Debug + Send + Sync + 'static>(
window: &Window<MockRuntime>,
request: InvokeRequest,
expected: Result<T, T>,
) {
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)]

View File

@@ -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<Vec<u8>>, &mut HttpResponse<Cow<'static, [u8]>>) + Send + Sync;
pub(crate) type NavigationHandler = dyn Fn(&Url) -> bool + Send;
pub(crate) type UriSchemeProtocolHandler =
Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>;
Box<dyn Fn(HttpRequest<Vec<u8>>, 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<F: Fn(&HttpRequest, &mut HttpResponse) + Send + Sync + 'static>(
pub fn on_web_resource_request<
F: Fn(HttpRequest<Vec<u8>>, &mut HttpResponse<Cow<'static, [u8]>>) + 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<R: Runtime> {
/// The webview window created by the runtime.
pub(crate) window: DetachedWindow<EventLoopMessage, R>,
/// The manager to associate this webview window with.
manager: WindowManager<R>,
pub(crate) manager: WindowManager<R>,
pub(crate) app_handle: AppHandle<R>,
js_event_listeners: Arc<Mutex<HashMap<JsEventListenerKey, HashSet<usize>>>>,
// The menu set for this window
@@ -2067,7 +2069,7 @@ impl<R: Runtime> Window<R> {
}
/// Handles this window receiving an [`InvokeRequest`].
pub fn on_message(self, request: InvokeRequest) -> Receiver<InvokeResponse> {
pub fn on_message(self, request: InvokeRequest, responder: Box<OwnedInvokeResponder<R>>) {
let manager = self.manager.clone();
let current_url = self.url();
let is_local = self.is_local_url(&current_url);
@@ -2090,75 +2092,20 @@ impl<R: Runtime> Window<R> {
}
};
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<R>, 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<R: Runtime>(
window: &Window<R>,
js: crate::api::Result<String>,
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<R: Runtime> Window<R> {
.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<R: Runtime> Window<R> {
},
) {
resolver.reject(e.to_string());
return rx;
return;
}
}
}
@@ -2269,8 +2216,6 @@ impl<R: Runtime> Window<R> {
}
}
}
rx
}
/// Evaluates JavaScript on this window.

View File

@@ -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",

View File

@@ -63,8 +63,8 @@ pub fn run_app<R: Runtime, F: FnOnce(&App<R>) + 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()?,
));

View File

@@ -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 <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} 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::<Vec<_>>();
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<Vec<u8>>,
boundary_id: &Arc<Mutex<i32>>,
) -> Result<http::Response<Vec<u8>>, Box<dyn std::error::Error>> {
// 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 <start-end>, example: 0-499
.map(|r| (r.start, r.start + r.length - 1))
.collect::<Vec<_>>()
} 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::<Vec<_>>();
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)
}