diff --git a/.changes/custom-protocol.md b/.changes/custom-protocol.md new file mode 100644 index 000000000..1a035ad66 --- /dev/null +++ b/.changes/custom-protocol.md @@ -0,0 +1,10 @@ +--- +"wry": patch +--- + +The custom protocol now return a `Request` and expect a `Response`. + +- This allow us to get the complete request from the Webview. (Method, GET, POST, PUT etc..) + Read the complete header. + +- And allow us to be more flexible in the future without bringing breaking changes. diff --git a/.gitignore b/.gitignore index 19bfb194b..03a8a250c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target Cargo.lock gh-pages .DS_Store +examples/test_video.mp4 \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2b83e67b9..4cedc183e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,11 +37,13 @@ serde_json = "1.0" thiserror = "1.0" url = "2.2" tao = { version = "0.5.2", default-features = false, features = [ "serde" ] } +http = "0.2.4" [dev-dependencies] anyhow = "1.0.43" chrono = "0.4.19" tempfile = "3.2.0" +http-range = "0.1.4" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] webkit2gtk = { version = "0.14", features = [ "v2_18" ] } diff --git a/examples/README.md b/examples/README.md index 9b1a61617..f051173c4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,17 +2,17 @@ Run the `cargo run --example ` to see how each example works. -- `hello_world`: the basic example to show the types and methods to create an application. -- `fullscreen`: full screen example demonstrates how to configure the window with attributes. -- `transparent`: transparent example that also show how to create a valid data URI. -- `rpc`: A RPC example to explain how to use the RPC handler and interact with it. -- `multi_window`: create the window dynamically even after the application is running. -- `dragndrop`: example for file drop handler. -- `custom_titlebar`: A frameless window with custom title-bar to show `drag-region` class in action. - `custom_protocol`: uses a custom protocol to load files from bytes. -- `html`: load the html string and load other assets with custom protocol. +- `custom_titlebar`: A frameless window with custom title-bar to show `drag-region` class in action. - `detect_js_ecma`: detects which versions of ECMAScript is supported by the webview. +- `dragndrop`: example for file drop handler. +- `form_post`: submit form POST and get data in rust without any web server. +- `fullscreen`: full screen example demonstrates how to configure the window with attributes. +- `hello_world`: the basic example to show the types and methods to create an application. - `menu_bar`: uses a custom menu for the application in macOS and the Window and Linux/Windows. -- `system_tray`: sample tray application with different behaviours. +- `multi_window`: create the window dynamically even after the application is running. +- `rpc`: A RPC example to explain how to use the RPC handler and interact with it. +- `stream_range`: read the incoming header from the custom protocol and return part of the data. [RFC7233](https://httpwg.org/specs/rfc7233.html#header.range) - `system_tray_no_menu`: open window on tray icon left click. - +- `system_tray`: sample tray application with different behaviours. +- `transparent`: transparent example that also show how to create a valid data URI. diff --git a/examples/custom_protocol.rs b/examples/custom_protocol.rs index 3db5acf27..f532299da 100644 --- a/examples/custom_protocol.rs +++ b/examples/custom_protocol.rs @@ -11,6 +11,7 @@ fn main() -> wry::Result<()> { event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }, + http::ResponseBuilder, webview::WebViewBuilder, }; @@ -22,24 +23,26 @@ fn main() -> wry::Result<()> { let _webview = WebViewBuilder::new(window) .unwrap() - .with_custom_protocol("wry".into(), move |requested_asset_path| { + .with_custom_protocol("wry".into(), move |request| { // Remove url scheme - let path = requested_asset_path.replace("wry://", ""); + let path = request.uri().replace("wry://", ""); // Read the file content from file path let content = read(canonicalize(&path)?)?; // Return asset contents and mime types based on file extentions // If you don't want to do this manually, there are some crates for you. // Such as `infer` and `mime_guess`. - if path.ends_with(".html") { - Ok((content, String::from("text/html"))) + let (data, meta) = if path.ends_with(".html") { + (content, "text/html") } else if path.ends_with(".js") { - Ok((content, String::from("text/javascript"))) + (content, "text/javascript") } else if path.ends_with(".png") { - Ok((content, String::from("image/png"))) + (content, "image/png") } else { unimplemented!(); - } + }; + + ResponseBuilder::new().mimetype(meta).body(data) }) // tell the webview to load the custom protocol .with_url("wry://examples/index.html")? diff --git a/examples/form.html b/examples/form.html new file mode 100644 index 000000000..88b533cb0 --- /dev/null +++ b/examples/form.html @@ -0,0 +1,22 @@ + + + + + + + + +

Welcome to WRY!

+
+
+
+
+

+ +
+

+ If you click the "Submit" button, the form-data will be sent to the custom + protocol. +

+ + diff --git a/examples/form_post.rs b/examples/form_post.rs new file mode 100644 index 000000000..4ae994342 --- /dev/null +++ b/examples/form_post.rs @@ -0,0 +1,55 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() -> wry::Result<()> { + use std::fs::{canonicalize, read}; + + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + http::{method::Method, ResponseBuilder}, + webview::WebViewBuilder, + }; + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop) + .unwrap(); + + let _webview = WebViewBuilder::new(window) + .unwrap() + .with_custom_protocol("wry".into(), move |request| { + if request.method() == Method::POST { + let body_string = String::from_utf8_lossy(request.body()); + for body in body_string.split('&') { + println!("Value sent; {:?}", body); + } + } + // Remove url scheme + let path = request.uri().replace("wry://", ""); + ResponseBuilder::new() + .mimetype("text/html") + .body(read(canonicalize(&path)?)?) + }) + // tell the webview to load the custom protocol + .with_url("wry://examples/form.html")? + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry application started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/examples/html.rs b/examples/html.rs deleted file mode 100644 index d48794133..000000000 --- a/examples/html.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2019-2021 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -fn main() -> wry::Result<()> { - use std::fs::{canonicalize, read}; - - use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, - }; - - let event_loop = EventLoop::new(); - let window = WindowBuilder::new() - .with_title("Hello World") - .build(&event_loop) - .unwrap(); - - let _webview = WebViewBuilder::new(window) - .unwrap() - // We still register custom protocol here to show that how the page with http:// origin can - // load them. - .with_custom_protocol("wry".into(), move |requested_asset_path| { - // Remove url scheme - let path = requested_asset_path.replace("wry://", ""); - // Read the file content from file path - let content = read(canonicalize(&path)?)?; - - // Return asset contents and mime types based on file extentions - // If you don't want to do this manually, there are some crates for you. - // Such as `infer` and `mime_guess`. - if path.ends_with(".html") { - Ok((content, String::from("text/html"))) - } else if path.ends_with(".js") { - Ok((content, String::from("text/javascript"))) - } else if path.ends_with(".png") { - Ok((content, String::from("image/png"))) - } else { - unimplemented!(); - } - }) - // tell the webview to load the html string - .with_html( - r#" - - - - - - - -

Welcome to WRY!

- Link - - - -"#, - )? - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => println!("Wry application started!"), - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/stream.html b/examples/stream.html new file mode 100644 index 000000000..094b89874 --- /dev/null +++ b/examples/stream.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/examples/stream_range.rs b/examples/stream_range.rs new file mode 100644 index 000000000..726b64926 --- /dev/null +++ b/examples/stream_range.rs @@ -0,0 +1,144 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() -> wry::Result<()> { + use http_range::HttpRange; + use std::{ + fs::{canonicalize, File}, + io::{Read, Seek, SeekFrom}, + path::PathBuf, + process::{Command, Stdio}, + }; + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + http::ResponseBuilder, + webview::WebViewBuilder, + }; + + let video_file = PathBuf::from("examples/test_video.mp4"); + let video_url = + "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4"; + + if !video_file.exists() { + // Downloading with curl this saves us from adding + // a Rust HTTP client dependency. + println!("Downloading {}", video_url); + let status = Command::new("curl") + .arg("-L") + .arg("-o") + .arg(&video_file) + .arg(video_url) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .unwrap(); + + assert!(status.status.success()); + assert!(video_file.exists()); + } + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop) + .unwrap(); + + let _webview = WebViewBuilder::new(window) + .unwrap() + .with_custom_protocol("wry".into(), move |request| { + // Remove url scheme + let path = request.uri().replace("wry://", ""); + // Read the file content from file path + let mut content = File::open(canonicalize(&path)?)?; + + // Return asset contents and mime types based on file extentions + // If you don't want to do this manually, there are some crates for you. + // Such as `infer` and `mime_guess`. + let mut status_code = 200; + let mut buf = Vec::new(); + + // guess our mimetype from the path + let mimetype = if path.ends_with(".html") { + "text/html" + } else if path.ends_with(".mp4") { + "video/mp4" + } else { + unimplemented!(); + }; + + // prepare our http response + let mut response = ResponseBuilder::new(); + + // read our range header if it exist, so we can return partial content + if let Some(range) = request.headers().get("range") { + // Get the file size + let file_size = content.metadata().unwrap().len(); + + // we parse the range header + let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap(); + + // let support only 1 range for now + let first_range = range.first(); + if let Some(range) = first_range { + let mut real_length = range.length; + + // prevent max_length; + // specially on webview2 + if range.length > file_size / 3 { + // max size sent (400ko / request) + // as it's local file system we can afford to read more often + real_length = 1024 * 400; + } + + // last byte we are reading, the length of the range include the last byte + // who should be skipped on the header + let last_byte = range.start + real_length - 1; + // partial content + status_code = 206; + + response = response.header("Connection", "Keep-Alive"); + response = response.header("Accept-Ranges", "bytes"); + // we need to overwrite our content length + response = response.header("Content-Length", real_length); + response = response.header( + "Content-Range", + format!("bytes {}-{}/{}", range.start, last_byte, file_size), + ); + + // seek our file bytes + content.seek(SeekFrom::Start(range.start))?; + content.take(real_length).read_to_end(&mut buf)?; + } else { + content.read_to_end(&mut buf)?; + } + } else { + content.read_to_end(&mut buf)?; + } + + response.mimetype(mimetype).status(status_code).body(buf) + }) + // tell the webview to load the custom protocol + .with_url("wry://examples/stream.html")? + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry application started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => { + #[cfg(target_os = "windows")] + let _ = _webview.resize(); + } + } + }); +} diff --git a/examples/system_tray.rs b/examples/system_tray.rs index 099b45e62..68561ae86 100644 --- a/examples/system_tray.rs +++ b/examples/system_tray.rs @@ -26,6 +26,7 @@ fn main() -> wry::Result<()> { system_tray::SystemTrayBuilder, window::{WindowBuilder, WindowId}, }, + http::ResponseBuilder, webview::{WebView, WebViewBuilder}, }; @@ -129,8 +130,10 @@ fn main() -> wry::Result<()> { let webview = WebViewBuilder::new(window) .unwrap() - .with_custom_protocol("wry.dev".into(), move |_uri| { - Ok((index_html.as_bytes().into(), "text/html".into())) + .with_custom_protocol("wry.dev".into(), move |_request| { + ResponseBuilder::new() + .mimetype("text/html") + .body(index_html.as_bytes().into()) }) .with_url("wry.dev://") .unwrap() diff --git a/examples/system_tray_no_menu.rs b/examples/system_tray_no_menu.rs index b2ba2fee3..0aa5fc478 100644 --- a/examples/system_tray_no_menu.rs +++ b/examples/system_tray_no_menu.rs @@ -24,6 +24,7 @@ fn main() -> wry::Result<()> { system_tray::SystemTrayBuilder, window::{WindowBuilder, WindowId}, }, + http::ResponseBuilder, webview::{WebView, WebViewBuilder}, }; @@ -135,7 +136,9 @@ fn main() -> wry::Result<()> { let webview = WebViewBuilder::new(window) .unwrap() .with_custom_protocol("wry.dev".into(), move |_uri| { - Ok((index_html.as_bytes().into(), "text/html".into())) + ResponseBuilder::new() + .mimetype("text/html") + .body(index_html.as_bytes().into()) }) .with_url("wry.dev://") .unwrap() diff --git a/src/lib.rs b/src/lib.rs index 79fc3f803..bc0b40d90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,13 +87,24 @@ extern crate objc; use std::sync::mpsc::{RecvError, SendError}; -use crate::application::window::BadIcon; +use crate::{ + application::window::BadIcon, + shared::http::{ + header::{InvalidHeaderName, InvalidHeaderValue}, + method::InvalidMethod, + status::InvalidStatusCode, + InvalidUri, + }, +}; pub use serde_json::Value; use url::ParseError; pub mod application; pub mod webview; +mod shared; +pub use shared::*; + /// Convenient type alias of Result type for wry. pub type Result = std::result::Result; @@ -155,4 +166,16 @@ pub enum Error { WebView2Error(#[from] webview2::Error), #[error("Duplicate custom protocol registered: {0}")] DuplicateCustomProtocol(String), + #[error("Invalid header name: {0}")] + 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}")] + InvalidMethod(#[from] InvalidMethod), + #[error("Infallible error, something went really wrong: {0}")] + Infallible(#[from] std::convert::Infallible), } diff --git a/src/shared/http/mod.rs b/src/shared/http/mod.rs new file mode 100644 index 000000000..ae843e65a --- /dev/null +++ b/src/shared/http/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2019-2021 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}, +}; + +// re-expose default http types +pub use http::{header, method, status, uri::InvalidUri, version}; + +// we don't need to expose our request builder +// as it's used internally only +pub(crate) use self::request::Builder as RequestBuilder; diff --git a/src/shared/http/request.rs b/src/shared/http/request.rs new file mode 100644 index 000000000..13541ce7b --- /dev/null +++ b/src/shared/http/request.rs @@ -0,0 +1,221 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{convert::TryFrom, fmt}; + +use super::{ + header::{HeaderMap, HeaderName, HeaderValue}, + method::Method, +}; + +use crate::Result; + +/// 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 { + pub head: RequestParts, + pub body: Vec, +} + +/// Component parts of an HTTP `Request` +/// +/// The HTTP request head consists of a method, uri, and a set of +/// header fields. +#[derive(Clone)] +pub struct RequestParts { + /// The request's method + pub method: Method, + + /// The request's URI + pub uri: String, + + /// The request's headers + pub headers: HeaderMap, +} + +/// An HTTP request builder +/// +/// This type can be used to construct an instance or `Request` +/// through a builder-like pattern. +#[derive(Debug)] +pub(crate) struct Builder { + inner: Result, +} + +impl Request { + /// Creates a new blank `Request` with the body + #[inline] + pub fn new(body: Vec) -> Request { + Request { + head: RequestParts::new(), + body, + } + } + + /// Returns a reference to the associated HTTP method. + #[inline] + pub fn method(&self) -> &Method { + &self.head.method + } + + /// Returns a reference to the associated URI. + #[inline] + pub fn uri(&self) -> &str { + &self.head.uri + } + + /// Returns a reference to the associated header field map. + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + /// Returns a reference to the associated HTTP body. + #[inline] + pub fn body(&self) -> &Vec { + &self.body + } + + /// Consumes the request returning the head and body RequestParts. + #[inline] + pub fn into_parts(self) -> (RequestParts, Vec) { + (self.head, self.body) + } +} + +impl Default for Request { + fn default() -> Request { + Request::new(Vec::new()) + } +} + +impl fmt::Debug for Request { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Request") + .field("method", self.method()) + .field("uri", &self.uri()) + .field("headers", self.headers()) + .field("body", self.body()) + .finish() + } +} + +impl RequestParts { + /// Creates a new default instance of `RequestParts` + fn new() -> RequestParts { + RequestParts { + method: Method::default(), + uri: "".into(), + headers: HeaderMap::default(), + } + } +} + +impl fmt::Debug for RequestParts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Parts") + .field("method", &self.method) + .field("uri", &self.uri) + .field("headers", &self.headers) + .finish() + } +} + +impl Builder { + /// Creates a new default instance of `Builder` to construct a `Request`. + #[inline] + pub fn new() -> Builder { + Builder::default() + } + + /// Set the HTTP method for this request. + /// + /// This function will configure the HTTP method of the `Request` that will + /// be returned from `Builder::build`. + /// + /// By default this is `GET`. + pub fn method(self, method: T) -> Builder + where + Method: TryFrom, + >::Error: Into, + { + self.and_then(move |mut head| { + let method = TryFrom::try_from(method).map_err(Into::into)?; + head.method = method; + Ok(head) + }) + } + + /// Set the URI for this request. + /// + /// This function will configure the URI of the `Request` that will + /// be returned from `Builder::build`. + /// + /// By default this is `/`. + pub fn uri(self, uri: &str) -> Builder { + self.and_then(move |mut head| { + head.uri = uri.to_string(); + Ok(head) + }) + } + + /// Appends a header to this request 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`. + pub fn header(self, key: K, value: V) -> Builder + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + self.and_then(move |mut head| { + let name = >::try_from(key).map_err(Into::into)?; + let value = >::try_from(value).map_err(Into::into)?; + head.headers.append(name, value); + Ok(head) + }) + } + + /// "Consumes" this builder, using the provided `body` to return a + /// constructed `Request`. + /// + /// # 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. + pub fn body(self, body: Vec) -> Result { + self.inner.map(move |head| Request { head, body }) + } + + // private + + fn and_then(self, func: F) -> Self + where + F: FnOnce(RequestParts) -> Result, + { + Builder { + inner: self.inner.and_then(func), + } + } +} + +impl Default for Builder { + #[inline] + fn default() -> Builder { + Builder { + inner: Ok(RequestParts::new()), + } + } +} diff --git a/src/shared/http/response.rs b/src/shared/http/response.rs new file mode 100644 index 000000000..60dac0ef9 --- /dev/null +++ b/src/shared/http/response.rs @@ -0,0 +1,264 @@ +// Copyright 2019-2021 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 crate::Result; +use std::{convert::TryFrom, fmt}; + +/// 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 wry::http::*; +/// +/// let response = ResponseBuilder::new() +/// .status(202) +/// .body("hello!".as_bytes().to_vec()) +/// .unwrap(); +/// ``` +/// + +pub struct Response { + pub head: ResponseParts, + pub body: Vec, +} + +/// Component parts of an HTTP `Response` +/// +/// The HTTP response head consists of a status, version, and a set of +/// header fields. +#[derive(Clone)] +pub struct ResponseParts { + /// The response's status + pub status: StatusCode, + + /// The response's version + pub version: Version, + + /// The response's headers + pub headers: HeaderMap, + + /// The response's mimetype type + pub mimetype: Option, +} + +/// An HTTP response builder +/// +/// This type can be used to construct an instance of `Response` through a +/// builder-like pattern. +#[derive(Debug)] +pub struct Builder { + inner: Result, +} + +impl Response { + /// Creates a new blank `Response` with the body + #[inline] + pub fn new(body: Vec) -> Response { + Response { + head: ResponseParts::new(), + body, + } + } + + /// Returns the `StatusCode`. + #[inline] + pub fn status(&self) -> StatusCode { + self.head.status + } + + /// Returns a reference to the mime type. + #[inline] + pub fn mimetype(&self) -> Option<&str> { + self.head.mimetype.as_deref() + } + + /// Returns a reference to the associated version. + #[inline] + pub fn version(&self) -> Version { + self.head.version + } + + /// Returns a reference to the associated header field map. + #[inline] + pub fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + /// Returns a reference to the associated HTTP body. + #[inline] + pub fn body(&self) -> &Vec { + &self.body + } +} + +impl Default for Response { + #[inline] + fn default() -> Response { + Response::new(Vec::new()) + } +} + +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 wry::http::*; + /// + /// let response = ResponseBuilder::new() + /// .status(200) + /// .body(Vec::new()) + /// .unwrap(); + /// ``` + #[inline] + pub fn new() -> Builder { + Builder { + inner: Ok(ResponseParts::new()), + } + } + + /// Set the HTTP mimetype for this response. + pub fn mimetype(self, mimetype: &str) -> Builder { + self.and_then(move |mut head| { + head.mimetype = Some(mimetype.to_string()); + Ok(head) + }) + } + + /// Set the HTTP status for this response. + pub fn status(self, status: T) -> Builder + where + StatusCode: TryFrom, + >::Error: Into, + { + self.and_then(move |mut head| { + head.status = TryFrom::try_from(status).map_err(Into::into)?; + Ok(head) + }) + } + + /// Set the HTTP version for this response. + /// + /// This function will configure the HTTP version of the `Response` that + /// will be returned from `Builder::build`. + /// + /// By default this is HTTP/1.1 + pub fn version(self, version: Version) -> Builder { + 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`. + pub fn header(self, key: K, value: V) -> Builder + where + HeaderName: TryFrom, + >::Error: Into, + HeaderValue: TryFrom, + >::Error: Into, + { + self.and_then(move |mut head| { + let name = >::try_from(key).map_err(Into::into)?; + let value = >::try_from(value).map_err(Into::into)?; + head.headers.append(name, value); + Ok(head) + }) + } + + /// "Consumes" this builder, using the provided `body` to return a + /// constructed `Response`. + /// + /// # Errors + /// + /// This function may return an error if any previously configured argument + /// failed to parse or get converted to the internal representation. For + /// example if an invalid `head` was specified via `header("Foo", + /// "Bar\r\n")` the error will be returned when this function is called + /// rather than when `header` was called. + /// + /// # Examples + /// + /// ``` + /// # use wry::http::*; + /// + /// let response = ResponseBuilder::new() + /// .body(Vec::new()) + /// .unwrap(); + /// ``` + pub fn body(self, body: Vec) -> Result { + self.inner.map(move |head| Response { head, body }) + } + + // private + + fn and_then(self, func: F) -> Self + where + F: FnOnce(ResponseParts) -> Result, + { + Builder { + inner: self.inner.and_then(func), + } + } +} + +impl Default for Builder { + #[inline] + fn default() -> Builder { + Builder { + inner: Ok(ResponseParts::new()), + } + } +} diff --git a/src/shared/mod.rs b/src/shared/mod.rs new file mode 100644 index 000000000..b29df64f3 --- /dev/null +++ b/src/shared/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/// HTTP types used by wry protocol. +pub mod http; diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 364f6281b..1aa72c477 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -44,6 +44,8 @@ use url::Url; use crate::application::platform::windows::WindowExtWindows; use crate::application::window::Window; +use crate::http::{Request as HttpRequest, Response as HttpResponse}; + pub struct WebViewAttributes { /// Whether the WebView window should be visible. pub visible: bool, @@ -86,7 +88,7 @@ pub struct WebViewAttributes { /// - Windows: `https://.` (so it will be `https://wry.examples` in `custom_protocol` example) /// /// [bug]: https://bugs.webkit.org/show_bug.cgi?id=229034 - pub custom_protocols: Vec<(String, Box Result<(Vec, String)>>)>, + pub custom_protocols: Vec<(String, Box Result>)>, /// Set the RPC handler to Communicate between the host Rust code and Javascript on webview. /// /// The communication is done via [JSON-RPC](https://www.jsonrpc.org). Users can use this to register an incoming @@ -191,7 +193,7 @@ impl<'a> WebViewBuilder<'a> { #[cfg(feature = "protocol")] pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self where - F: Fn(&str) -> Result<(Vec, String)> + 'static, + F: Fn(&HttpRequest) -> Result + 'static, { self .webview diff --git a/src/webview/web_context.rs b/src/webview/web_context.rs index 9b2c81fab..96bc7dd06 100644 --- a/src/webview/web_context.rs +++ b/src/webview/web_context.rs @@ -104,7 +104,12 @@ use self::unix::WebContextImpl; pub mod unix { //! Unix platform extensions for [`WebContext`](super::WebContext). - use crate::Error; + use crate::{ + http::{ + Request as HttpRequest, RequestBuilder as HttpRequestBuilder, Response as HttpResponse, + }, + Error, + }; use glib::FileError; use std::{ collections::{HashSet, VecDeque}, @@ -115,6 +120,7 @@ pub mod unix { }, }; use url::Url; + //use webkit2gtk_sys::webkit_uri_request_get_http_headers; use webkit2gtk::{ traits::*, ApplicationInfo, LoadEvent, UserContentManager, WebContext, WebContextBuilder, WebView, WebsiteDataManagerBuilder, @@ -206,7 +212,7 @@ pub mod unix { /// relying on the platform's implementation to properly handle duplicated scheme handlers. fn register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> where - F: Fn(&str) -> crate::Result<(Vec, String)> + 'static; + F: Fn(&HttpRequest) -> crate::Result + 'static; /// Register a custom protocol to the web context, only if it is not a duplicate scheme. /// @@ -214,7 +220,7 @@ pub mod unix { /// function will return `Err(Error::DuplicateCustomProtocol)`. fn try_register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> where - F: Fn(&str) -> crate::Result<(Vec, String)> + 'static; + F: Fn(&HttpRequest) -> crate::Result + 'static; /// Add a [`WebView`] to the queue waiting to be opened. /// @@ -245,7 +251,7 @@ pub mod unix { fn register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> where - F: Fn(&str) -> crate::Result<(Vec, String)> + 'static, + F: Fn(&HttpRequest) -> crate::Result + 'static, { actually_register_uri_scheme(self, name, handler)?; if self.os.registered_protocols.insert(name.to_string()) { @@ -257,7 +263,7 @@ pub mod unix { fn try_register_uri_scheme(&mut self, name: &str, handler: F) -> crate::Result<()> where - F: Fn(&str) -> crate::Result<(Vec, String)> + 'static, + F: Fn(&HttpRequest) -> crate::Result + 'static, { if self.os.registered_protocols.insert(name.to_string()) { actually_register_uri_scheme(self, name, handler) @@ -305,7 +311,7 @@ pub mod unix { handler: F, ) -> crate::Result<()> where - F: Fn(&str) -> crate::Result<(Vec, String)> + 'static, + F: Fn(&HttpRequest) -> crate::Result + 'static, { use webkit2gtk::traits::*; let context = &context.os.context; @@ -318,10 +324,28 @@ pub mod unix { if let Some(uri) = request.uri() { let uri = uri.as_str(); - match handler(uri) { - Ok((buffer, mime)) => { - let input = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&buffer)); - request.finish(&input, buffer.len() as i64, Some(&mime)) + //let headers = unsafe { + // webkit_uri_request_get_http_headers(request.clone().to_glib_none().0) + //}; + + // FIXME: Read the method + // FIXME: Read the headers + // FIXME: Read the body (forms post) + let http_request = HttpRequestBuilder::new() + .uri(uri) + .method("GET") + .body(Vec::new()) + .unwrap(); + + match handler(&http_request) { + Ok(http_response) => { + let buffer = http_response.body(); + + // FIXME: Set status code + // FIXME: Set sent headers + + let input = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(buffer)); + request.finish(&input, buffer.len() as i64, http_response.mimetype()) } Err(_) => request.finish_error(&mut glib::Error::new( FileError::Exist, diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 9b6a756f6..3441a4fc0 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -12,7 +12,7 @@ use crate::{ use file_drop::FileDropController; -use std::{collections::HashSet, os::raw::c_void, rc::Rc}; +use std::{collections::HashSet, io::Read, os::raw::c_void, rc::Rc}; use once_cell::unsync::OnceCell; use webview2::{Controller, PermissionKind, PermissionState, WebView}; @@ -21,10 +21,13 @@ use winapi::{ um::winuser::{DestroyWindow, GetClientRect}, }; -use crate::application::{ - event_loop::{ControlFlow, EventLoop}, - platform::{run_return::EventLoopExtRunReturn, windows::WindowExtWindows}, - window::Window, +use crate::{ + application::{ + event_loop::{ControlFlow, EventLoop}, + platform::{run_return::EventLoopExtRunReturn, windows::WindowExtWindows}, + window::Window, + }, + http::RequestBuilder as HttpRequestBuilder, }; pub struct InnerWebView { @@ -197,27 +200,73 @@ impl InnerWebView { let custom_protocols = attributes.custom_protocols; let env_clone = env_.clone(); w.add_web_resource_requested(move |_, args| { - let uri = args.get_request()?.get_uri()?; + let webview_request = args.get_request()?; + let mut request = HttpRequestBuilder::new(); + + // request method (GET, POST, PUT etc..) + let request_method = webview_request.get_method()?; + + // get all headers from the request + let headers = webview_request.get_headers()?; + for (key, value) in headers.get_iterator()? { + request = request.header(&key, &value); + } + + // get the body content if available + let mut body_sent = Vec::new(); + if let Ok(mut content) = webview_request.get_content() { + content.read_to_end(&mut body_sent)?; + } + + // uri + let uri = webview_request.get_uri()?; + // Undo the protocol workaround when giving path to resolver let path = uri .replace("https://", "") .replacen(".", "://", 1); + let scheme = path.split("://").next().unwrap(); + let final_request = request.uri(&path).method(request_method.as_str()).body(body_sent).unwrap(); match (custom_protocols .iter() .find(|(name, _)| name == &scheme) .unwrap() - .1)(&path) + .1)(&final_request) { - Ok((content, mime)) => { + Ok(sent_response) => { + + let mime = sent_response.mimetype(); + let content = sent_response.body(); + let status_code = sent_response.status().as_u16() as i32; + + let mut headers_map = String::new(); + + // set mime type if provided + if let Some(mime) = sent_response.mimetype() { + headers_map.push_str(&format!("Content-Type: {}\n", mime)) + } + + // build headers + for (name, value) in sent_response.headers().iter() { + let header_key = name.to_string(); + if let Ok(value) = value.to_str() { + headers_map.push_str(&format!("{}: {}\n", header_key, value)) + } + } + let stream = webview2::Stream::from_bytes(&content); + + // FIXME: Set http response version + let response = env_clone.create_web_resource_response( stream, - 200, + status_code, "OK", - &format!("Content-Type: {}", mime), + &headers_map, )?; + args.put_response(response)?; Ok(()) } diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index 5fd3fc205..efcced1c9 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -2,6 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +#[cfg(target_os = "macos")] +use cocoa::{ + appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable}, + base::YES, +}; +use cocoa::{ + base::id, + foundation::{NSDictionary, NSFastEnumeration}, +}; use std::{ ffi::{c_void, CStr}, os::raw::c_char, @@ -10,13 +19,6 @@ use std::{ slice, str, }; -use cocoa::base::id; -#[cfg(target_os = "macos")] -use cocoa::{ - appkit::{NSView, NSViewHeightSizable, NSViewWidthSizable}, - base::YES, -}; - use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use objc::{ declare::ClassDecl, @@ -38,6 +40,10 @@ use crate::{ Result, }; +use crate::http::{ + Request as HttpRequest, RequestBuilder as HttpRequestBuilder, Response as HttpResponse, +}; + #[cfg(target_os = "macos")] mod file_drop; @@ -54,7 +60,7 @@ pub struct InnerWebView { ), #[cfg(target_os = "macos")] file_drop_ptr: *mut (Box bool>, Rc), - protocol_ptrs: Vec<*mut Box Result<(Vec, String)>>>, + protocol_ptrs: Vec<*mut Box Result>>, } impl InnerWebView { @@ -97,7 +103,7 @@ impl InnerWebView { unsafe { let function = this.get_ivar::<*mut c_void>("function"); let function = - &mut *(*function as *mut Box Fn(&'s str) -> Result<(Vec, String)>>); + &mut *(*function as *mut Box Fn(&'s HttpRequest) -> Result>); // Get url request let request: id = msg_send![task, request]; @@ -106,16 +112,69 @@ impl InnerWebView { let s: id = msg_send![url, absoluteString]; NSString(Id::from_ptr(s)) }; - let uri = nsstring.to_str(); - // Send response - if let Ok((content, mime)) = function(uri) { + // Get request method (GET, POST, PUT etc...) + let method = { + let s: id = msg_send![request, HTTPMethod]; + NSString(Id::from_ptr(s)) + }; + + // Prepare our HttpRequest + let mut http_request = HttpRequestBuilder::new() + .uri(nsstring.to_str()) + .method(method.to_str()); + + // Get body + // FIXME: Add support of `HTTPBodyStream` + // https://developer.apple.com/documentation/foundation/nsurlrequest/1407341-httpbodystream?language=objc + let mut sent_form_body = Vec::new(); + let nsdata: id = msg_send![request, HTTPBody]; + // if we have a body + if !nsdata.is_null() { + let length = msg_send![nsdata, length]; + let data_bytes: id = msg_send![nsdata, bytes]; + sent_form_body = slice::from_raw_parts(data_bytes as *const u8, length).to_vec(); + } + + // Extract all headers fields + let all_headers: id = msg_send![request, allHTTPHeaderFields]; + + // get all our headers values and inject them in our request + for current_header_ptr in all_headers.iter() { + let header_field = NSString(Id::from_ptr(current_header_ptr)); + let header_value = NSString(Id::from_ptr(all_headers.valueForKey_(current_header_ptr))); + // inject the header into the request + http_request = http_request.header(header_field.to_str(), header_value.to_str()); + } + + // send response + let final_request = http_request.body(sent_form_body).unwrap(); + if let Ok(sent_response) = function(&final_request) { + let content = sent_response.body(); + // default: application/octet-stream, but should be provided by the client + let wanted_mime = sent_response.mimetype(); + // default to 200 + let wanted_status_code = sent_response.status().as_u16() as i32; + // default to HTTP/1.1 + let wanted_version = format!("{:#?}", sent_response.version()); + let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; let headers: id = msg_send![dictionary, initWithCapacity:1]; - let () = msg_send![headers, setObject:NSString::new(&mime) forKey: NSString::new("content-type")]; + if let Some(mime) = wanted_mime { + let () = msg_send![headers, setObject:NSString::new(mime) forKey: NSString::new("content-type")]; + } let () = msg_send![headers, setObject:NSString::new(&content.len().to_string()) forKey: NSString::new("content-length")]; + + // add headers + for (name, value) in sent_response.headers().iter() { + let header_key = name.to_string(); + if let Ok(value) = value.to_str() { + let () = msg_send![headers, setObject:NSString::new(value) forKey: NSString::new(&header_key)]; + } + } + let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; - let response: id = msg_send![urlresponse, initWithURL:url statusCode:200 HTTPVersion:NSString::new("HTTP/1.1") headerFields:headers]; + let response: id = msg_send![urlresponse, initWithURL:url statusCode: wanted_status_code HTTPVersion:NSString::new(&wanted_version) headerFields:headers]; let () = msg_send![task, didReceiveResponse: response]; // Send data