protocol.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. use crate::document::NATIVE_EVAL_JS;
  2. use crate::{assets::*, webview::WebviewEdits};
  3. use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
  4. use dioxus_interpreter_js::NATIVE_JS;
  5. use std::path::{Path, PathBuf};
  6. use wry::{
  7. http::{status::StatusCode, Request, Response},
  8. RequestAsyncResponder, Result,
  9. };
  10. #[cfg(any(target_os = "android", target_os = "windows"))]
  11. const EDITS_PATH: &str = "http://dioxus.index.html/__edits";
  12. #[cfg(not(any(target_os = "android", target_os = "windows")))]
  13. const EDITS_PATH: &str = "dioxus://index.html/__edits";
  14. #[cfg(any(target_os = "android", target_os = "windows"))]
  15. const EVENTS_PATH: &str = "http://dioxus.index.html/__events";
  16. #[cfg(not(any(target_os = "android", target_os = "windows")))]
  17. const EVENTS_PATH: &str = "dioxus://index.html/__events";
  18. static DEFAULT_INDEX: &str = include_str!("./index.html");
  19. #[allow(clippy::too_many_arguments)] // just for now, should fix this eventually
  20. /// Handle a request from the webview
  21. ///
  22. /// - Tries to stream edits if they're requested.
  23. /// - If that doesn't match, tries a user provided asset handler
  24. /// - If that doesn't match, tries to serve a file from the filesystem
  25. pub(super) fn desktop_handler(
  26. request: Request<Vec<u8>>,
  27. asset_handlers: AssetHandlerRegistry,
  28. responder: RequestAsyncResponder,
  29. edit_state: &WebviewEdits,
  30. custom_head: Option<String>,
  31. custom_index: Option<String>,
  32. root_name: &str,
  33. headless: bool,
  34. ) {
  35. // Try to serve the index file first
  36. if let Some(index_bytes) =
  37. index_request(&request, custom_head, custom_index, root_name, headless)
  38. {
  39. return responder.respond(index_bytes);
  40. }
  41. // If the request is asking for edits (ie binary protocol streaming), do that
  42. let trimmed_uri = request.uri().path().trim_matches('/');
  43. if trimmed_uri == "__edits" {
  44. return edit_state.wry_queue.handle_request(responder);
  45. }
  46. // If the request is asking for an event response, do that
  47. if trimmed_uri == "__events" {
  48. return edit_state.handle_event(request, responder);
  49. }
  50. // todo: we want to move the custom assets onto a different protocol or something
  51. if let Some(name) = request.uri().path().split('/').next() {
  52. if asset_handlers.has_handler(name) {
  53. let _name = name.to_string();
  54. return asset_handlers.handle_request(&_name, request, responder);
  55. }
  56. }
  57. match serve_asset(request) {
  58. Ok(res) => responder.respond(res),
  59. Err(_e) => responder.respond(
  60. Response::builder()
  61. .status(StatusCode::INTERNAL_SERVER_ERROR)
  62. .body(String::from("Failed to serve asset").into_bytes())
  63. .unwrap(),
  64. ),
  65. }
  66. }
  67. fn serve_asset(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>> {
  68. // If the user provided a custom asset handler, then call it and return the response if the request was handled.
  69. // The path is the first part of the URI, so we need to trim the leading slash.
  70. let mut uri_path = PathBuf::from(
  71. urlencoding::decode(request.uri().path())
  72. .expect("expected URL to be UTF-8 encoded")
  73. .as_ref(),
  74. );
  75. // Attempt to serve from the asset dir on android using its loader
  76. #[cfg(target_os = "android")]
  77. {
  78. if let Some(asset) = to_java_load_asset(request.uri().path()) {
  79. return Ok(Response::builder()
  80. .header("Content-Type", get_mime_by_ext(&uri_path))
  81. .header("Access-Control-Allow-Origin", "*")
  82. .body(asset)?);
  83. }
  84. }
  85. // If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
  86. // This lets us handle both absolute and relative paths without being too "special"
  87. // It just means that our macos bundle is a little "special" because we need to place an `assets`
  88. // dir in the `Resources` dir.
  89. //
  90. // If there's no asset root, we use the cargo manifest dir as the root, or the current dir
  91. if !uri_path.exists() || uri_path.starts_with("/assets/") {
  92. let bundle_root = get_asset_root();
  93. let relative_path = uri_path.strip_prefix("/").unwrap();
  94. uri_path = bundle_root.join(relative_path);
  95. }
  96. // If the asset exists, then we can serve it!
  97. if uri_path.exists() {
  98. let mime_type = get_mime_from_path(&uri_path);
  99. return Ok(Response::builder()
  100. .header("Content-Type", mime_type?)
  101. .header("Access-Control-Allow-Origin", "*")
  102. .body(std::fs::read(uri_path)?)?);
  103. }
  104. Ok(Response::builder()
  105. .status(StatusCode::NOT_FOUND)
  106. .body(String::from("Not Found").into_bytes())?)
  107. }
  108. /// Build the index.html file we use for bootstrapping a new app
  109. ///
  110. /// We use wry/webview by building a special index.html that forms a bridge between the webview and your rust code
  111. ///
  112. /// This is similar to tauri, except we give more power to your rust code and less power to your frontend code.
  113. /// This lets us skip a build/bundle step - your code just works - but limits how your Rust code can actually
  114. /// mess with UI elements. We make this decision since other renderers like LiveView are very separate and can
  115. /// never properly bridge the gap. Eventually of course, the idea is to build a custom CSS/HTML renderer where you
  116. /// *do* have native control over elements, but that still won't work with liveview.
  117. fn index_request(
  118. request: &Request<Vec<u8>>,
  119. custom_head: Option<String>,
  120. custom_index: Option<String>,
  121. root_name: &str,
  122. headless: bool,
  123. ) -> Option<Response<Vec<u8>>> {
  124. // If the request is for the root, we'll serve the index.html file.
  125. if request.uri().path() != "/" {
  126. return None;
  127. }
  128. // Load a custom index file if provided
  129. let mut index = custom_index.unwrap_or_else(|| DEFAULT_INDEX.to_string());
  130. // Insert a custom head if provided
  131. // We look just for the closing head tag. If a user provided a custom index with weird syntax, this might fail
  132. if let Some(head) = custom_head {
  133. index.insert_str(index.find("</head>").expect("Head element to exist"), &head);
  134. }
  135. // Inject our module loader by looking for a body tag
  136. // A failure mode here, obviously, is if the user provided a custom index without a body tag
  137. // Might want to document this
  138. index.insert_str(
  139. index.find("</body>").expect("Body element to exist"),
  140. &module_loader(root_name, headless),
  141. );
  142. Response::builder()
  143. .header("Content-Type", "text/html")
  144. .header("Access-Control-Allow-Origin", "*")
  145. .body(index.into())
  146. .ok()
  147. }
  148. /// Construct the inline script that boots up the page and bridges the webview with rust code.
  149. ///
  150. /// The arguments here:
  151. /// - root_name: the root element (by Id) that we stream edits into
  152. /// - headless: is this page being loaded but invisible? Important because not all windows are visible and the
  153. /// interpreter can't connect until the window is ready.
  154. fn module_loader(root_id: &str, headless: bool) -> String {
  155. format!(
  156. r#"
  157. <script type="module">
  158. // Bring the sledgehammer code
  159. {SLEDGEHAMMER_JS}
  160. // And then extend it with our native bindings
  161. {NATIVE_JS}
  162. // The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
  163. window.interpreter = new NativeInterpreter("{EDITS_PATH}", "{EVENTS_PATH}");
  164. // Wait for the page to load before sending the initialize message
  165. window.onload = function() {{
  166. let root_element = window.document.getElementById("{root_id}");
  167. if (root_element != null) {{
  168. window.interpreter.initialize(root_element);
  169. window.ipc.postMessage(window.interpreter.serializeIpcMessage("initialize"));
  170. }}
  171. window.interpreter.waitForRequest({headless});
  172. }}
  173. </script>
  174. <script type="module">
  175. // Include the code for eval
  176. {NATIVE_EVAL_JS}
  177. </script>
  178. "#
  179. )
  180. }
  181. /// Get the asset directory, following tauri/cargo-bundles directory discovery approach
  182. ///
  183. /// Currently supports:
  184. /// - [x] macOS
  185. /// - [x] iOS
  186. /// - [x] Windows
  187. /// - [x] Linux (appimage)
  188. /// - [ ] Linux (rpm)
  189. /// - [ ] Linux (deb)
  190. /// - [ ] Android
  191. #[allow(unreachable_code)]
  192. fn get_asset_root() -> PathBuf {
  193. let cur_exe = std::env::current_exe().unwrap();
  194. #[cfg(target_os = "macos")]
  195. {
  196. return cur_exe
  197. .parent()
  198. .unwrap()
  199. .parent()
  200. .unwrap()
  201. .join("Resources");
  202. }
  203. // For all others, the structure looks like this:
  204. // app.(exe/appimage)
  205. // main.exe
  206. // assets/
  207. cur_exe.parent().unwrap().to_path_buf()
  208. }
  209. /// Get the mime type from a path-like string
  210. fn get_mime_from_path(asset: &Path) -> Result<&'static str> {
  211. if asset.extension().is_some_and(|ext| ext == "svg") {
  212. return Ok("image/svg+xml");
  213. }
  214. match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
  215. Some(f) if f != "text/plain" => Ok(f),
  216. _other => Ok(get_mime_by_ext(asset)),
  217. }
  218. }
  219. /// Get the mime type from a URI using its extension
  220. fn get_mime_by_ext(trimmed: &Path) -> &'static str {
  221. match trimmed.extension().and_then(|e| e.to_str()) {
  222. // The common assets are all utf-8 encoded
  223. Some("js") => "text/javascript; charset=utf-8",
  224. Some("css") => "text/css; charset=utf-8",
  225. Some("json") => "application/json; charset=utf-8",
  226. Some("svg") => "image/svg+xml; charset=utf-8",
  227. Some("html") => "text/html; charset=utf-8",
  228. // the rest... idk? probably not
  229. Some("mjs") => "text/javascript; charset=utf-8",
  230. Some("bin") => "application/octet-stream",
  231. Some("csv") => "text/csv",
  232. Some("ico") => "image/vnd.microsoft.icon",
  233. Some("jsonld") => "application/ld+json",
  234. Some("rtf") => "application/rtf",
  235. Some("mp4") => "video/mp4",
  236. // Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
  237. Some(_) => "text/html; charset=utf-8",
  238. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
  239. // using octet stream according to this:
  240. None => "application/octet-stream",
  241. }
  242. }
  243. #[cfg(target_os = "android")]
  244. pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
  245. let normalized = filepath
  246. .trim_start_matches("/assets/")
  247. .trim_start_matches('/');
  248. // in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
  249. #[cfg(debug_assertions)]
  250. {
  251. let path = std::path::PathBuf::from("/data/local/tmp/dx/").join(normalized);
  252. if path.exists() {
  253. return std::fs::read(path).ok();
  254. }
  255. }
  256. use std::{io::Read, ptr::NonNull};
  257. let ctx = ndk_context::android_context();
  258. let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
  259. let mut env = vm.attach_current_thread().unwrap();
  260. // Query the Asset Manager
  261. let asset_manager_ptr = env
  262. .call_method(
  263. unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
  264. "getAssets",
  265. "()Landroid/content/res/AssetManager;",
  266. &[],
  267. )
  268. .expect("Failed to get asset manager")
  269. .l()
  270. .expect("Failed to get asset manager as object");
  271. unsafe {
  272. let asset_manager =
  273. ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
  274. let asset_manager = ndk::asset::AssetManager::from_ptr(
  275. NonNull::new(asset_manager).expect("Invalid asset manager"),
  276. );
  277. let cstr = std::ffi::CString::new(normalized).unwrap();
  278. let mut asset = asset_manager.open(&cstr)?;
  279. Some(asset.buffer().unwrap().to_vec())
  280. }
  281. }