protocol.rs 12 KB

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