mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
refactor(core): allow custom protocol handler to resolve async (#7754)
This commit is contained in:
committed by
GitHub
parent
b75a1210be
commit
0d63732b96
5
.changes/custom-protocol-response-refactor.md
Normal file
5
.changes/custom-protocol-response-refactor.md
Normal 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.
|
||||
5
.changes/fix-channel-data-request.md
Normal file
5
.changes/fix-channel-data-request.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch:bug
|
||||
---
|
||||
|
||||
Fixes invalid header value type when requesting IPC body through a channel.
|
||||
7
.changes/http-types-refactor.md
Normal file
7
.changes/http-types-refactor.md
Normal 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.
|
||||
5
.changes/invoke-system-args.md
Normal file
5
.changes/invoke-system-args.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch:breaking
|
||||
---
|
||||
|
||||
Changed `Builder::invoke_system` to take references instead of owned values.
|
||||
5
.changes/register_asynchronous_uri_scheme_protocol.md
Normal file
5
.changes/register_asynchronous_uri_scheme_protocol.md
Normal 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.
|
||||
5
.changes/runtime-custom-protocol-async.md
Normal file
5
.changes/runtime-custom-protocol-async.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-runtime": patch:enhance
|
||||
---
|
||||
|
||||
Changed custom protocol closure type to enable asynchronous usage.
|
||||
5
.changes/window-on-message-refactor.md
Normal file
5
.changes/window-on-message-refactor.md
Normal 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
5
.changes/wry-0.32.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-runtime-wry": patch:enhance
|
||||
---
|
||||
|
||||
Update wry to 0.32 to include asynchronous custom protocol support.
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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" ]
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
) &&
|
||||
!(osName === 'macos' && location.protocol === 'https:')
|
||||
) {
|
||||
console.log('process')
|
||||
const {
|
||||
contentType,
|
||||
data
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
))
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
76
core/tauri/src/protocol/isolation.rs
Normal file
76
core/tauri/src/protocol/isolation.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
9
core/tauri/src/protocol/mod.rs
Normal file
9
core/tauri/src/protocol/mod.rs
Normal 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;
|
||||
179
core/tauri/src/protocol/tauri.rs
Normal file
179
core/tauri/src/protocol/tauri.rs
Normal 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)
|
||||
}
|
||||
@@ -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")),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(¤t_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.
|
||||
|
||||
21
examples/api/src-tauri/Cargo.lock
generated
21
examples/api/src-tauri/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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()?,
|
||||
));
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user