protocol.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. use dioxus_core::ScopeState;
  2. use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
  3. use slab::Slab;
  4. use std::{
  5. borrow::Cow,
  6. future::Future,
  7. ops::{Deref, DerefMut},
  8. path::{Path, PathBuf},
  9. pin::Pin,
  10. rc::Rc,
  11. sync::Arc,
  12. };
  13. use tokio::{
  14. runtime::Handle,
  15. sync::{OnceCell, RwLock},
  16. };
  17. use wry::{
  18. http::{status::StatusCode, Request, Response},
  19. Result,
  20. };
  21. use crate::{use_window, DesktopContext};
  22. fn module_loader(root_name: &str) -> String {
  23. let js = INTERPRETER_JS.replace(
  24. "/*POST_HANDLE_EDITS*/",
  25. r#"// Prevent file inputs from opening the file dialog on click
  26. let inputs = document.querySelectorAll("input");
  27. for (let input of inputs) {
  28. if (!input.getAttribute("data-dioxus-file-listener")) {
  29. // prevent file inputs from opening the file dialog on click
  30. const type = input.getAttribute("type");
  31. if (type === "file") {
  32. input.setAttribute("data-dioxus-file-listener", true);
  33. input.addEventListener("click", (event) => {
  34. let target = event.target;
  35. let target_id = find_real_id(target);
  36. if (target_id !== null) {
  37. const send = (event_name) => {
  38. const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
  39. window.ipc.postMessage(message);
  40. };
  41. send("change&input");
  42. }
  43. event.preventDefault();
  44. });
  45. }
  46. }
  47. }"#,
  48. );
  49. format!(
  50. r#"
  51. <script type="module">
  52. {js}
  53. let rootname = "{root_name}";
  54. let root = window.document.getElementById(rootname);
  55. if (root != null) {{
  56. window.interpreter = new Interpreter(root, new InterpreterConfig(true));
  57. window.ipc.postMessage(serializeIpcMessage("initialize"));
  58. }}
  59. </script>
  60. "#
  61. )
  62. }
  63. /// An arbitrary asset is an HTTP response containing a binary body.
  64. pub type AssetResponse = Response<Cow<'static, [u8]>>;
  65. /// A future that returns an [`AssetResponse`]. This future may be spawned in a new thread,
  66. /// so it must be [`Send`], [`Sync`], and `'static`.
  67. pub trait AssetFuture: Future<Output = Option<AssetResponse>> + Send + Sync + 'static {}
  68. impl<T: Future<Output = Option<AssetResponse>> + Send + Sync + 'static> AssetFuture for T {}
  69. /// A request for an asset. This is a wrapper around [`Request<Vec<u8>>`] that provides methods specific to asset requests.
  70. pub struct AssetRequest {
  71. path: PathBuf,
  72. request: Request<Vec<u8>>,
  73. }
  74. impl AssetRequest {
  75. /// Get the path the asset request is for
  76. pub fn path(&self) -> &Path {
  77. &self.path
  78. }
  79. }
  80. impl From<Request<Vec<u8>>> for AssetRequest {
  81. fn from(request: Request<Vec<u8>>) -> Self {
  82. let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
  83. .expect("expected URL to be UTF-8 encoded");
  84. let path = PathBuf::from(&*decoded);
  85. Self { request, path }
  86. }
  87. }
  88. impl Deref for AssetRequest {
  89. type Target = Request<Vec<u8>>;
  90. fn deref(&self) -> &Self::Target {
  91. &self.request
  92. }
  93. }
  94. impl DerefMut for AssetRequest {
  95. fn deref_mut(&mut self) -> &mut Self::Target {
  96. &mut self.request
  97. }
  98. }
  99. /// A handler that takes an [`AssetRequest`] and returns a future that either loads the asset, or returns `None`.
  100. /// This handler is stashed indefinitely in a context object, so it must be `'static`.
  101. pub trait AssetHandler<F: AssetFuture>: Send + Sync + 'static {
  102. /// Handle an asset request, returning a future that either loads the asset, or returns `None`
  103. fn handle_request(&self, request: &AssetRequest) -> F;
  104. }
  105. impl<F: AssetFuture, T: Fn(&AssetRequest) -> F + Send + Sync + 'static> AssetHandler<F> for T {
  106. fn handle_request(&self, request: &AssetRequest) -> F {
  107. self(request)
  108. }
  109. }
  110. type AssetHandlerRegistryInner =
  111. Slab<Box<dyn Fn(&AssetRequest) -> Pin<Box<dyn AssetFuture>> + Send + Sync + 'static>>;
  112. #[derive(Clone)]
  113. pub struct AssetHandlerRegistry(Arc<RwLock<AssetHandlerRegistryInner>>);
  114. impl AssetHandlerRegistry {
  115. pub fn new() -> Self {
  116. AssetHandlerRegistry(Arc::new(RwLock::new(Slab::new())))
  117. }
  118. pub async fn register_handler<F: AssetFuture>(&self, f: impl AssetHandler<F>) -> usize {
  119. let mut registry = self.0.write().await;
  120. registry.insert(Box::new(move |req| Box::pin(f.handle_request(req))))
  121. }
  122. pub async fn remove_handler(&self, id: usize) -> Option<()> {
  123. let mut registry = self.0.write().await;
  124. registry.try_remove(id).map(|_| ())
  125. }
  126. pub async fn try_handlers(&self, req: &AssetRequest) -> Option<AssetResponse> {
  127. let registry = self.0.read().await;
  128. for (_, handler) in registry.iter() {
  129. if let Some(response) = handler(req).await {
  130. return Some(response);
  131. }
  132. }
  133. None
  134. }
  135. }
  136. /// A handle to a registered asset handler.
  137. pub struct AssetHandlerHandle {
  138. desktop: DesktopContext,
  139. handler_id: Rc<OnceCell<usize>>,
  140. }
  141. impl AssetHandlerHandle {
  142. /// Returns the ID for this handle.
  143. ///
  144. /// Because registering an ID is asynchronous, this may return `None` if the
  145. /// registration has not completed yet.
  146. pub fn handler_id(&self) -> Option<usize> {
  147. self.handler_id.get().copied()
  148. }
  149. }
  150. impl Drop for AssetHandlerHandle {
  151. fn drop(&mut self) {
  152. let cell = Rc::clone(&self.handler_id);
  153. let desktop = Rc::clone(&self.desktop);
  154. tokio::task::block_in_place(move || {
  155. Handle::current().block_on(async move {
  156. if let Some(id) = cell.get() {
  157. desktop.asset_handlers.remove_handler(*id).await;
  158. }
  159. })
  160. });
  161. }
  162. }
  163. /// Provide a callback to handle asset loading yourself.
  164. ///
  165. /// The callback takes a path as requested by the web view, and it should return `Some(response)`
  166. /// if you want to load the asset, and `None` if you want to fallback on the default behavior.
  167. pub fn use_asset_handler<F: AssetFuture>(
  168. cx: &ScopeState,
  169. handler: impl AssetHandler<F>,
  170. ) -> &AssetHandlerHandle {
  171. let desktop = Rc::clone(use_window(cx));
  172. cx.use_hook(|| {
  173. let handler_id = Rc::new(OnceCell::new());
  174. let handler_id_ref = Rc::clone(&handler_id);
  175. let desktop_ref = Rc::clone(&desktop);
  176. cx.push_future(async move {
  177. let id = desktop.asset_handlers.register_handler(handler).await;
  178. handler_id.set(id).unwrap();
  179. });
  180. AssetHandlerHandle {
  181. desktop: desktop_ref,
  182. handler_id: handler_id_ref,
  183. }
  184. })
  185. }
  186. pub(super) async fn desktop_handler(
  187. request: Request<Vec<u8>>,
  188. custom_head: Option<String>,
  189. custom_index: Option<String>,
  190. root_name: &str,
  191. asset_handlers: &AssetHandlerRegistry,
  192. ) -> Result<AssetResponse> {
  193. let request = AssetRequest::from(request);
  194. // If the request is for the root, we'll serve the index.html file.
  195. if request.uri().path() == "/" {
  196. // If a custom index is provided, just defer to that, expecting the user to know what they're doing.
  197. // we'll look for the closing </body> tag and insert our little module loader there.
  198. let body = match custom_index {
  199. Some(custom_index) => custom_index
  200. .replace("</body>", &format!("{}</body>", module_loader(root_name)))
  201. .into_bytes(),
  202. None => {
  203. // Otherwise, we'll serve the default index.html and apply a custom head if that's specified.
  204. let mut template = include_str!("./index.html").to_string();
  205. if let Some(custom_head) = custom_head {
  206. template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
  207. }
  208. template
  209. .replace("<!-- MODULE LOADER -->", &module_loader(root_name))
  210. .into_bytes()
  211. }
  212. };
  213. return Response::builder()
  214. .header("Content-Type", "text/html")
  215. .body(Cow::from(body))
  216. .map_err(From::from);
  217. } else if request.uri().path() == "/common.js" {
  218. return Response::builder()
  219. .header("Content-Type", "text/javascript")
  220. .body(Cow::from(COMMON_JS.as_bytes()))
  221. .map_err(From::from);
  222. }
  223. // If the user provided a custom asset handler, then call it and return the response
  224. // if the request was handled.
  225. if let Some(response) = asset_handlers.try_handlers(&request).await {
  226. return Ok(response);
  227. }
  228. // Else, try to serve a file from the filesystem.
  229. // If the path is relative, we'll try to serve it from the assets directory.
  230. let mut asset = get_asset_root()
  231. .unwrap_or_else(|| Path::new(".").to_path_buf())
  232. .join(&request.path);
  233. if !asset.exists() {
  234. asset = PathBuf::from("/").join(request.path);
  235. }
  236. if asset.exists() {
  237. return Response::builder()
  238. .header("Content-Type", get_mime_from_path(&asset)?)
  239. .body(Cow::from(std::fs::read(asset)?))
  240. .map_err(From::from);
  241. }
  242. Response::builder()
  243. .status(StatusCode::NOT_FOUND)
  244. .body(Cow::from(String::from("Not Found").into_bytes()))
  245. .map_err(From::from)
  246. }
  247. #[allow(unreachable_code)]
  248. fn get_asset_root() -> Option<PathBuf> {
  249. /*
  250. We're matching exactly how cargo-bundle works.
  251. - [x] macOS
  252. - [ ] Windows
  253. - [ ] Linux (rpm)
  254. - [ ] Linux (deb)
  255. - [ ] iOS
  256. - [ ] Android
  257. */
  258. if std::env::var_os("CARGO").is_some() {
  259. return None;
  260. }
  261. // TODO: support for other platforms
  262. #[cfg(target_os = "macos")]
  263. {
  264. let bundle = core_foundation::bundle::CFBundle::main_bundle();
  265. let bundle_path = bundle.path()?;
  266. let resources_path = bundle.resources_path()?;
  267. let absolute_resources_root = bundle_path.join(resources_path);
  268. let canonical_resources_root = dunce::canonicalize(absolute_resources_root).ok()?;
  269. return Some(canonical_resources_root);
  270. }
  271. None
  272. }
  273. /// Get the mime type from a path-like string
  274. fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
  275. if trimmed.extension().is_some_and(|ext| ext == "svg") {
  276. return Ok("image/svg+xml");
  277. }
  278. let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
  279. Some(f) => {
  280. if f == "text/plain" {
  281. get_mime_by_ext(trimmed)
  282. } else {
  283. f
  284. }
  285. }
  286. None => get_mime_by_ext(trimmed),
  287. };
  288. Ok(res)
  289. }
  290. /// Get the mime type from a URI using its extension
  291. fn get_mime_by_ext(trimmed: &Path) -> &'static str {
  292. match trimmed.extension().and_then(|e| e.to_str()) {
  293. Some("bin") => "application/octet-stream",
  294. Some("css") => "text/css",
  295. Some("csv") => "text/csv",
  296. Some("html") => "text/html",
  297. Some("ico") => "image/vnd.microsoft.icon",
  298. Some("js") => "text/javascript",
  299. Some("json") => "application/json",
  300. Some("jsonld") => "application/ld+json",
  301. Some("mjs") => "text/javascript",
  302. Some("rtf") => "application/rtf",
  303. Some("svg") => "image/svg+xml",
  304. Some("mp4") => "video/mp4",
  305. // Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
  306. Some(_) => "text/html",
  307. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
  308. // using octet stream according to this:
  309. None => "application/octet-stream",
  310. }
  311. }