manager.rs 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. borrow::Cow,
  6. collections::{HashMap, HashSet},
  7. fmt,
  8. fs::create_dir_all,
  9. sync::{Arc, Mutex, MutexGuard},
  10. };
  11. use serde::Serialize;
  12. use serde_json::Value as JsonValue;
  13. use serialize_to_javascript::{default_template, DefaultTemplate, Template};
  14. use url::Url;
  15. use tauri_macros::default_runtime;
  16. use tauri_utils::debug_eprintln;
  17. #[cfg(feature = "isolation")]
  18. use tauri_utils::pattern::isolation::RawIsolationPayload;
  19. use tauri_utils::{
  20. assets::{AssetKey, CspHash},
  21. config::{Csp, CspDirectiveSources},
  22. html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
  23. };
  24. use crate::hooks::IpcJavascript;
  25. #[cfg(feature = "isolation")]
  26. use crate::hooks::IsolationJavascript;
  27. use crate::pattern::{format_real_schema, PatternJavascript};
  28. use crate::{
  29. app::{AppHandle, GlobalWindowEvent, GlobalWindowEventListener},
  30. event::{assert_event_name_is_valid, Event, EventHandler, Listeners},
  31. hooks::{InvokeHandler, InvokePayload, InvokeResponder, OnPageLoad, PageLoadPayload},
  32. plugin::PluginStore,
  33. runtime::{
  34. http::{
  35. MimeType, Request as HttpRequest, Response as HttpResponse,
  36. ResponseBuilder as HttpResponseBuilder,
  37. },
  38. webview::{WebviewIpcHandler, WindowBuilder},
  39. window::{dpi::PhysicalSize, DetachedWindow, FileDropEvent, PendingWindow},
  40. },
  41. utils::{
  42. assets::Assets,
  43. config::{AppUrl, Config, WindowUrl},
  44. PackageInfo,
  45. },
  46. Context, EventLoopMessage, Icon, Invoke, Manager, Pattern, Runtime, Scopes, StateManager, Window,
  47. WindowEvent,
  48. };
  49. use crate::{
  50. app::{GlobalMenuEventListener, WindowMenuEvent},
  51. window::WebResourceRequestHandler,
  52. };
  53. #[cfg(any(target_os = "linux", target_os = "windows"))]
  54. use crate::path::BaseDirectory;
  55. use crate::{runtime::menu::Menu, MenuEvent};
  56. const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
  57. const WINDOW_MOVED_EVENT: &str = "tauri://move";
  58. const WINDOW_CLOSE_REQUESTED_EVENT: &str = "tauri://close-requested";
  59. const WINDOW_DESTROYED_EVENT: &str = "tauri://destroyed";
  60. const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
  61. const WINDOW_BLUR_EVENT: &str = "tauri://blur";
  62. const WINDOW_SCALE_FACTOR_CHANGED_EVENT: &str = "tauri://scale-change";
  63. const WINDOW_THEME_CHANGED: &str = "tauri://theme-changed";
  64. const WINDOW_FILE_DROP_EVENT: &str = "tauri://file-drop";
  65. const WINDOW_FILE_DROP_HOVER_EVENT: &str = "tauri://file-drop-hover";
  66. const WINDOW_FILE_DROP_CANCELLED_EVENT: &str = "tauri://file-drop-cancelled";
  67. const MENU_EVENT: &str = "tauri://menu";
  68. pub(crate) const STRINGIFY_IPC_MESSAGE_FN: &str =
  69. include_str!("../scripts/stringify-ipc-message-fn.js");
  70. // we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
  71. // and we do not get a secure context without the custom protocol that proxies to the dev server
  72. // additionally, we need the custom protocol to inject the initialization scripts on Android
  73. // must also keep in sync with the `let mut response` assignment in prepare_uri_scheme_protocol
  74. const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
  75. #[derive(Default)]
  76. /// Spaced and quoted Content-Security-Policy hash values.
  77. struct CspHashStrings {
  78. script: Vec<String>,
  79. style: Vec<String>,
  80. }
  81. /// Sets the CSP value to the asset HTML if needed (on Linux).
  82. /// Returns the CSP string for access on the response header (on Windows and macOS).
  83. fn set_csp<R: Runtime>(
  84. asset: &mut String,
  85. assets: Arc<dyn Assets>,
  86. asset_path: &AssetKey,
  87. manager: &WindowManager<R>,
  88. csp: Csp,
  89. ) -> String {
  90. let mut csp = csp.into();
  91. let hash_strings =
  92. assets
  93. .csp_hashes(asset_path)
  94. .fold(CspHashStrings::default(), |mut acc, hash| {
  95. match hash {
  96. CspHash::Script(hash) => {
  97. acc.script.push(hash.into());
  98. }
  99. CspHash::Style(hash) => {
  100. acc.style.push(hash.into());
  101. }
  102. _csp_hash => {
  103. debug_eprintln!("Unknown CspHash variant encountered: {:?}", _csp_hash);
  104. }
  105. }
  106. acc
  107. });
  108. let dangerous_disable_asset_csp_modification = &manager
  109. .config()
  110. .tauri
  111. .security
  112. .dangerous_disable_asset_csp_modification;
  113. if dangerous_disable_asset_csp_modification.can_modify("script-src") {
  114. replace_csp_nonce(
  115. asset,
  116. SCRIPT_NONCE_TOKEN,
  117. &mut csp,
  118. "script-src",
  119. hash_strings.script,
  120. );
  121. }
  122. if dangerous_disable_asset_csp_modification.can_modify("style-src") {
  123. replace_csp_nonce(
  124. asset,
  125. STYLE_NONCE_TOKEN,
  126. &mut csp,
  127. "style-src",
  128. hash_strings.style,
  129. );
  130. }
  131. #[cfg(feature = "isolation")]
  132. if let Pattern::Isolation { schema, .. } = &manager.inner.pattern {
  133. let default_src = csp
  134. .entry("default-src".into())
  135. .or_insert_with(Default::default);
  136. default_src.push(format_real_schema(schema));
  137. }
  138. Csp::DirectiveMap(csp).to_string()
  139. }
  140. // inspired by https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297
  141. fn replace_with_callback<F: FnMut() -> String>(
  142. original: &str,
  143. pattern: &str,
  144. mut replacement: F,
  145. ) -> String {
  146. let mut result = String::new();
  147. let mut last_end = 0;
  148. for (start, part) in original.match_indices(pattern) {
  149. result.push_str(unsafe { original.get_unchecked(last_end..start) });
  150. result.push_str(&replacement());
  151. last_end = start + part.len();
  152. }
  153. result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
  154. result
  155. }
  156. fn replace_csp_nonce(
  157. asset: &mut String,
  158. token: &str,
  159. csp: &mut HashMap<String, CspDirectiveSources>,
  160. directive: &str,
  161. hashes: Vec<String>,
  162. ) {
  163. let mut nonces = Vec::new();
  164. *asset = replace_with_callback(asset, token, || {
  165. let nonce = rand::random::<usize>();
  166. nonces.push(nonce);
  167. nonce.to_string()
  168. });
  169. if !(nonces.is_empty() && hashes.is_empty()) {
  170. let nonce_sources = nonces
  171. .into_iter()
  172. .map(|n| format!("'nonce-{n}'"))
  173. .collect::<Vec<String>>();
  174. let sources = csp.entry(directive.into()).or_insert_with(Default::default);
  175. let self_source = "'self'".to_string();
  176. if !sources.contains(&self_source) {
  177. sources.push(self_source);
  178. }
  179. sources.extend(nonce_sources);
  180. sources.extend(hashes);
  181. }
  182. }
  183. #[default_runtime(crate::Wry, wry)]
  184. pub struct InnerWindowManager<R: Runtime> {
  185. windows: Mutex<HashMap<String, Window<R>>>,
  186. #[cfg(all(desktop, feature = "system-tray"))]
  187. pub(crate) trays: Mutex<HashMap<String, crate::SystemTrayHandle<R>>>,
  188. pub(crate) plugins: Mutex<PluginStore<R>>,
  189. listeners: Listeners,
  190. pub(crate) state: Arc<StateManager>,
  191. /// The JS message handler.
  192. invoke_handler: Box<InvokeHandler<R>>,
  193. /// The page load hook, invoked when the webview performs a navigation.
  194. on_page_load: Box<OnPageLoad<R>>,
  195. config: Arc<Config>,
  196. assets: Arc<dyn Assets>,
  197. pub(crate) default_window_icon: Option<Icon>,
  198. pub(crate) app_icon: Option<Vec<u8>>,
  199. pub(crate) tray_icon: Option<Icon>,
  200. package_info: PackageInfo,
  201. /// The webview protocols available to all windows.
  202. uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
  203. /// The menu set to all windows.
  204. menu: Option<Menu>,
  205. /// Menu event listeners to all windows.
  206. menu_event_listeners: Arc<Vec<GlobalMenuEventListener<R>>>,
  207. /// Window event listeners to all windows.
  208. window_event_listeners: Arc<Vec<GlobalWindowEventListener<R>>>,
  209. /// Responder for invoke calls.
  210. invoke_responder: Arc<InvokeResponder<R>>,
  211. /// The script that initializes the invoke system.
  212. invoke_initialization_script: String,
  213. /// Application pattern.
  214. pattern: Pattern,
  215. }
  216. impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
  217. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  218. f.debug_struct("InnerWindowManager")
  219. .field("plugins", &self.plugins)
  220. .field("state", &self.state)
  221. .field("config", &self.config)
  222. .field("default_window_icon", &self.default_window_icon)
  223. .field("app_icon", &self.app_icon)
  224. .field("tray_icon", &self.tray_icon)
  225. .field("package_info", &self.package_info)
  226. .field("menu", &self.menu)
  227. .field("pattern", &self.pattern)
  228. .finish()
  229. }
  230. }
  231. /// A resolved asset.
  232. pub struct Asset {
  233. /// The asset bytes.
  234. pub bytes: Vec<u8>,
  235. /// The asset's mime type.
  236. pub mime_type: String,
  237. /// The `Content-Security-Policy` header value.
  238. pub csp_header: Option<String>,
  239. }
  240. /// Uses a custom URI scheme handler to resolve file requests
  241. pub struct CustomProtocol<R: Runtime> {
  242. /// Handler for protocol
  243. #[allow(clippy::type_complexity)]
  244. pub protocol: Box<
  245. dyn Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
  246. + Send
  247. + Sync,
  248. >,
  249. }
  250. #[default_runtime(crate::Wry, wry)]
  251. #[derive(Debug)]
  252. pub struct WindowManager<R: Runtime> {
  253. pub inner: Arc<InnerWindowManager<R>>,
  254. }
  255. impl<R: Runtime> Clone for WindowManager<R> {
  256. fn clone(&self) -> Self {
  257. Self {
  258. inner: self.inner.clone(),
  259. }
  260. }
  261. }
  262. impl<R: Runtime> WindowManager<R> {
  263. #[allow(clippy::too_many_arguments)]
  264. pub(crate) fn with_handlers(
  265. #[allow(unused_mut)] mut context: Context<impl Assets>,
  266. plugins: PluginStore<R>,
  267. invoke_handler: Box<InvokeHandler<R>>,
  268. on_page_load: Box<OnPageLoad<R>>,
  269. uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
  270. state: StateManager,
  271. window_event_listeners: Vec<GlobalWindowEventListener<R>>,
  272. (menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
  273. (invoke_responder, invoke_initialization_script): (Arc<InvokeResponder<R>>, String),
  274. ) -> Self {
  275. // generate a random isolation key at runtime
  276. #[cfg(feature = "isolation")]
  277. if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
  278. *key = uuid::Uuid::new_v4().to_string();
  279. }
  280. Self {
  281. inner: Arc::new(InnerWindowManager {
  282. windows: Mutex::default(),
  283. #[cfg(all(desktop, feature = "system-tray"))]
  284. trays: Default::default(),
  285. plugins: Mutex::new(plugins),
  286. listeners: Listeners::default(),
  287. state: Arc::new(state),
  288. invoke_handler,
  289. on_page_load,
  290. config: Arc::new(context.config),
  291. assets: context.assets,
  292. default_window_icon: context.default_window_icon,
  293. app_icon: context.app_icon,
  294. tray_icon: context.system_tray_icon,
  295. package_info: context.package_info,
  296. pattern: context.pattern,
  297. uri_scheme_protocols,
  298. menu,
  299. menu_event_listeners: Arc::new(menu_event_listeners),
  300. window_event_listeners: Arc::new(window_event_listeners),
  301. invoke_responder,
  302. invoke_initialization_script,
  303. }),
  304. }
  305. }
  306. pub(crate) fn pattern(&self) -> &Pattern {
  307. &self.inner.pattern
  308. }
  309. /// Get a locked handle to the windows.
  310. pub(crate) fn windows_lock(&self) -> MutexGuard<'_, HashMap<String, Window<R>>> {
  311. self.inner.windows.lock().expect("poisoned window manager")
  312. }
  313. /// State managed by the application.
  314. pub(crate) fn state(&self) -> Arc<StateManager> {
  315. self.inner.state.clone()
  316. }
  317. /// The invoke responder.
  318. pub(crate) fn invoke_responder(&self) -> Arc<InvokeResponder<R>> {
  319. self.inner.invoke_responder.clone()
  320. }
  321. /// Get the base path to serve data from.
  322. ///
  323. /// * In dev mode, this will be based on the `devPath` configuration value.
  324. /// * Otherwise, this will be based on the `distDir` configuration value.
  325. #[cfg(not(dev))]
  326. fn base_path(&self) -> &AppUrl {
  327. &self.inner.config.build.dist_dir
  328. }
  329. #[cfg(dev)]
  330. fn base_path(&self) -> &AppUrl {
  331. &self.inner.config.build.dev_path
  332. }
  333. /// Get the base URL to use for webview requests.
  334. ///
  335. /// In dev mode, this will be based on the `devPath` configuration value.
  336. fn get_url(&self) -> Cow<'_, Url> {
  337. match self.base_path() {
  338. AppUrl::Url(WindowUrl::External(url)) => Cow::Borrowed(url),
  339. _ => Cow::Owned(Url::parse("tauri://localhost").unwrap()),
  340. }
  341. }
  342. /// Get the origin as it will be seen in the webview.
  343. fn get_browser_origin(&self) -> String {
  344. match self.base_path() {
  345. AppUrl::Url(WindowUrl::External(url)) => {
  346. if PROXY_DEV_SERVER {
  347. format_real_schema("tauri")
  348. } else {
  349. url.origin().ascii_serialization()
  350. }
  351. }
  352. _ => format_real_schema("tauri"),
  353. }
  354. }
  355. fn csp(&self) -> Option<Csp> {
  356. if cfg!(feature = "custom-protocol") {
  357. self.inner.config.tauri.security.csp.clone()
  358. } else {
  359. self
  360. .inner
  361. .config
  362. .tauri
  363. .security
  364. .dev_csp
  365. .clone()
  366. .or_else(|| self.inner.config.tauri.security.csp.clone())
  367. }
  368. }
  369. fn prepare_pending_window(
  370. &self,
  371. mut pending: PendingWindow<EventLoopMessage, R>,
  372. label: &str,
  373. window_labels: &[String],
  374. app_handle: AppHandle<R>,
  375. web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
  376. ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
  377. let is_init_global = self.inner.config.build.with_global_tauri;
  378. let plugin_init = self
  379. .inner
  380. .plugins
  381. .lock()
  382. .expect("poisoned plugin store")
  383. .initialization_script();
  384. let pattern_init = PatternJavascript {
  385. pattern: self.pattern().into(),
  386. }
  387. .render_default(&Default::default())?;
  388. let ipc_init = IpcJavascript {
  389. isolation_origin: &match self.pattern() {
  390. #[cfg(feature = "isolation")]
  391. Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema),
  392. _ => "".to_string(),
  393. },
  394. }
  395. .render_default(&Default::default())?;
  396. let mut webview_attributes = pending.webview_attributes;
  397. let mut window_labels = window_labels.to_vec();
  398. let l = label.to_string();
  399. if !window_labels.contains(&l) {
  400. window_labels.push(l);
  401. }
  402. webview_attributes = webview_attributes
  403. .initialization_script(&self.inner.invoke_initialization_script)
  404. .initialization_script(&format!(
  405. r#"
  406. Object.defineProperty(window, '__TAURI_METADATA__', {{
  407. value: {{
  408. __windows: {window_labels_array}.map(function (label) {{ return {{ label: label }} }}),
  409. __currentWindow: {{ label: {current_window_label} }}
  410. }}
  411. }})
  412. "#,
  413. window_labels_array = serde_json::to_string(&window_labels)?,
  414. current_window_label = serde_json::to_string(&label)?,
  415. ))
  416. .initialization_script(&self.initialization_script(&ipc_init.into_string(),&pattern_init.into_string(),&plugin_init, is_init_global)?);
  417. #[cfg(feature = "isolation")]
  418. if let Pattern::Isolation { schema, .. } = self.pattern() {
  419. webview_attributes = webview_attributes.initialization_script(
  420. &IsolationJavascript {
  421. origin: self.get_browser_origin(),
  422. isolation_src: &crate::pattern::format_real_schema(schema),
  423. style: tauri_utils::pattern::isolation::IFRAME_STYLE,
  424. }
  425. .render_default(&Default::default())?
  426. .into_string(),
  427. );
  428. }
  429. pending.webview_attributes = webview_attributes;
  430. let mut registered_scheme_protocols = Vec::new();
  431. for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
  432. registered_scheme_protocols.push(uri_scheme.clone());
  433. let protocol = protocol.clone();
  434. let app_handle = Mutex::new(app_handle.clone());
  435. pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p| {
  436. (protocol.protocol)(&app_handle.lock().unwrap(), p)
  437. });
  438. }
  439. let window_url = Url::parse(&pending.url).unwrap();
  440. let window_origin =
  441. if cfg!(windows) && window_url.scheme() != "http" && window_url.scheme() != "https" {
  442. format!("https://{}.localhost", window_url.scheme())
  443. } else {
  444. format!(
  445. "{}://{}{}",
  446. window_url.scheme(),
  447. window_url.host().unwrap(),
  448. if let Some(port) = window_url.port() {
  449. format!(":{port}")
  450. } else {
  451. "".into()
  452. }
  453. )
  454. };
  455. if !registered_scheme_protocols.contains(&"tauri".into()) {
  456. pending.register_uri_scheme_protocol(
  457. "tauri",
  458. self.prepare_uri_scheme_protocol(&window_origin, web_resource_request_handler),
  459. );
  460. registered_scheme_protocols.push("tauri".into());
  461. }
  462. #[cfg(protocol_asset)]
  463. if !registered_scheme_protocols.contains(&"asset".into()) {
  464. use crate::path::SafePathBuf;
  465. use tokio::io::{AsyncReadExt, AsyncSeekExt};
  466. use url::Position;
  467. let asset_scope = self.state().get::<crate::Scopes>().asset_protocol.clone();
  468. pending.register_uri_scheme_protocol("asset", move |request| {
  469. let parsed_path = Url::parse(request.uri())?;
  470. let filtered_path = &parsed_path[..Position::AfterPath];
  471. let path = filtered_path
  472. .strip_prefix("asset://localhost/")
  473. // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
  474. // where `$P` is not `localhost/*`
  475. .unwrap_or("");
  476. let path = percent_encoding::percent_decode(path.as_bytes())
  477. .decode_utf8_lossy()
  478. .to_string();
  479. if let Err(e) = SafePathBuf::new(path.clone().into()) {
  480. debug_eprintln!("asset protocol path \"{}\" is not valid: {}", path, e);
  481. return HttpResponseBuilder::new().status(403).body(Vec::new());
  482. }
  483. if !asset_scope.is_allowed(&path) {
  484. debug_eprintln!("asset protocol not configured to allow the path: {}", path);
  485. return HttpResponseBuilder::new().status(403).body(Vec::new());
  486. }
  487. let path_ = path.clone();
  488. let mut response =
  489. HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin);
  490. // handle 206 (partial range) http request
  491. if let Some(range) = request
  492. .headers()
  493. .get("range")
  494. .and_then(|r| r.to_str().map(|r| r.to_string()).ok())
  495. {
  496. #[derive(Default)]
  497. struct RangeMetadata {
  498. file: Option<tokio::fs::File>,
  499. range: Option<crate::runtime::http::HttpRange>,
  500. metadata: Option<std::fs::Metadata>,
  501. headers: HashMap<&'static str, String>,
  502. status_code: u16,
  503. body: Vec<u8>,
  504. }
  505. let mut range_metadata = crate::async_runtime::safe_block_on(async move {
  506. let mut data = RangeMetadata::default();
  507. // open the file
  508. let mut file = match tokio::fs::File::open(path_.clone()).await {
  509. Ok(file) => file,
  510. Err(e) => {
  511. debug_eprintln!("Failed to open asset: {}", e);
  512. data.status_code = 404;
  513. return data;
  514. }
  515. };
  516. // Get the file size
  517. let file_size = match file.metadata().await {
  518. Ok(metadata) => {
  519. let len = metadata.len();
  520. data.metadata.replace(metadata);
  521. len
  522. }
  523. Err(e) => {
  524. debug_eprintln!("Failed to read asset metadata: {}", e);
  525. data.file.replace(file);
  526. data.status_code = 404;
  527. return data;
  528. }
  529. };
  530. // parse the range
  531. let range = match crate::runtime::http::HttpRange::parse(
  532. &if range.ends_with("-*") {
  533. range.chars().take(range.len() - 1).collect::<String>()
  534. } else {
  535. range.clone()
  536. },
  537. file_size,
  538. ) {
  539. Ok(r) => r,
  540. Err(e) => {
  541. debug_eprintln!("Failed to parse range {}: {:?}", range, e);
  542. data.file.replace(file);
  543. data.status_code = 400;
  544. return data;
  545. }
  546. };
  547. // FIXME: Support multiple ranges
  548. // let support only 1 range for now
  549. if let Some(range) = range.first() {
  550. data.range.replace(*range);
  551. let mut real_length = range.length;
  552. // prevent max_length;
  553. // specially on webview2
  554. if range.length > file_size / 3 {
  555. // max size sent (400ko / request)
  556. // as it's local file system we can afford to read more often
  557. real_length = std::cmp::min(file_size - range.start, 1024 * 400);
  558. }
  559. // last byte we are reading, the length of the range include the last byte
  560. // who should be skipped on the header
  561. let last_byte = range.start + real_length - 1;
  562. data.headers.insert("Connection", "Keep-Alive".into());
  563. data.headers.insert("Accept-Ranges", "bytes".into());
  564. data
  565. .headers
  566. .insert("Content-Length", real_length.to_string());
  567. data.headers.insert(
  568. "Content-Range",
  569. format!("bytes {}-{last_byte}/{file_size}", range.start),
  570. );
  571. if let Err(e) = file.seek(std::io::SeekFrom::Start(range.start)).await {
  572. debug_eprintln!("Failed to seek file to {}: {}", range.start, e);
  573. data.file.replace(file);
  574. data.status_code = 422;
  575. return data;
  576. }
  577. let mut f = file.take(real_length);
  578. let r = f.read_to_end(&mut data.body).await;
  579. file = f.into_inner();
  580. data.file.replace(file);
  581. if let Err(e) = r {
  582. debug_eprintln!("Failed read file: {}", e);
  583. data.status_code = 422;
  584. return data;
  585. }
  586. // partial content
  587. data.status_code = 206;
  588. } else {
  589. data.status_code = 200;
  590. }
  591. data
  592. });
  593. for (k, v) in range_metadata.headers {
  594. response = response.header(k, v);
  595. }
  596. let mime_type = if let (Some(mut file), Some(metadata), Some(range)) = (
  597. range_metadata.file,
  598. range_metadata.metadata,
  599. range_metadata.range,
  600. ) {
  601. // if we're already reading the beginning of the file, we do not need to re-read it
  602. if range.start == 0 {
  603. MimeType::parse(&range_metadata.body, &path)
  604. } else {
  605. let (status, bytes) = crate::async_runtime::safe_block_on(async move {
  606. let mut status = None;
  607. if let Err(e) = file.rewind().await {
  608. debug_eprintln!("Failed to rewind file: {}", e);
  609. status.replace(422);
  610. (status, Vec::with_capacity(0))
  611. } else {
  612. // taken from https://docs.rs/infer/0.9.0/src/infer/lib.rs.html#240-251
  613. let limit = std::cmp::min(metadata.len(), 8192) as usize + 1;
  614. let mut bytes = Vec::with_capacity(limit);
  615. if let Err(e) = file.take(8192).read_to_end(&mut bytes).await {
  616. debug_eprintln!("Failed read file: {}", e);
  617. status.replace(422);
  618. }
  619. (status, bytes)
  620. }
  621. });
  622. if let Some(s) = status {
  623. range_metadata.status_code = s;
  624. }
  625. MimeType::parse(&bytes, &path)
  626. }
  627. } else {
  628. MimeType::parse(&range_metadata.body, &path)
  629. };
  630. response
  631. .mimetype(&mime_type)
  632. .status(range_metadata.status_code)
  633. .body(range_metadata.body)
  634. } else {
  635. match crate::async_runtime::safe_block_on(async move { tokio::fs::read(path_).await }) {
  636. Ok(data) => {
  637. let mime_type = MimeType::parse(&data, &path);
  638. response.mimetype(&mime_type).body(data)
  639. }
  640. Err(e) => {
  641. debug_eprintln!("Failed to read file: {}", e);
  642. response.status(404).body(Vec::new())
  643. }
  644. }
  645. }
  646. });
  647. }
  648. #[cfg(feature = "isolation")]
  649. if let Pattern::Isolation {
  650. assets,
  651. schema,
  652. key: _,
  653. crypto_keys,
  654. } = &self.inner.pattern
  655. {
  656. let assets = assets.clone();
  657. let schema_ = schema.clone();
  658. let url_base = format!("{schema_}://localhost");
  659. let aes_gcm_key = *crypto_keys.aes_gcm().raw();
  660. pending.register_uri_scheme_protocol(schema, move |request| {
  661. match request_to_path(request, &url_base).as_str() {
  662. "index.html" => match assets.get(&"index.html".into()) {
  663. Some(asset) => {
  664. let asset = String::from_utf8_lossy(asset.as_ref());
  665. let template = tauri_utils::pattern::isolation::IsolationJavascriptRuntime {
  666. runtime_aes_gcm_key: &aes_gcm_key,
  667. stringify_ipc_message_fn: STRINGIFY_IPC_MESSAGE_FN,
  668. };
  669. match template.render(asset.as_ref(), &Default::default()) {
  670. Ok(asset) => HttpResponseBuilder::new()
  671. .mimetype("text/html")
  672. .body(asset.into_string().as_bytes().to_vec()),
  673. Err(_) => HttpResponseBuilder::new()
  674. .status(500)
  675. .mimetype("text/plain")
  676. .body(Vec::new()),
  677. }
  678. }
  679. None => HttpResponseBuilder::new()
  680. .status(404)
  681. .mimetype("text/plain")
  682. .body(Vec::new()),
  683. },
  684. _ => HttpResponseBuilder::new()
  685. .status(404)
  686. .mimetype("text/plain")
  687. .body(Vec::new()),
  688. }
  689. });
  690. }
  691. Ok(pending)
  692. }
  693. fn prepare_ipc_handler(
  694. &self,
  695. app_handle: AppHandle<R>,
  696. ) -> WebviewIpcHandler<EventLoopMessage, R> {
  697. let manager = self.clone();
  698. Box::new(move |window, #[allow(unused_mut)] mut request| {
  699. let window = Window::new(manager.clone(), window, app_handle.clone());
  700. #[cfg(feature = "isolation")]
  701. if let Pattern::Isolation { crypto_keys, .. } = manager.pattern() {
  702. match RawIsolationPayload::try_from(request.as_str())
  703. .and_then(|raw| crypto_keys.decrypt(raw))
  704. {
  705. Ok(json) => request = json,
  706. Err(e) => {
  707. let error: crate::Error = e.into();
  708. let _ = window.eval(&format!(
  709. r#"console.error({})"#,
  710. JsonValue::String(error.to_string())
  711. ));
  712. return;
  713. }
  714. }
  715. }
  716. match serde_json::from_str::<InvokePayload>(&request) {
  717. Ok(message) => {
  718. let _ = window.on_message(message);
  719. }
  720. Err(e) => {
  721. let error: crate::Error = e.into();
  722. let _ = window.eval(&format!(
  723. r#"console.error({})"#,
  724. JsonValue::String(error.to_string())
  725. ));
  726. }
  727. }
  728. })
  729. }
  730. pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
  731. let assets = &self.inner.assets;
  732. if path.ends_with('/') {
  733. path.pop();
  734. }
  735. path = percent_encoding::percent_decode(path.as_bytes())
  736. .decode_utf8_lossy()
  737. .to_string();
  738. let path = if path.is_empty() {
  739. // if the url is `tauri://localhost`, we should load `index.html`
  740. "index.html".to_string()
  741. } else {
  742. // skip leading `/`
  743. path.chars().skip(1).collect::<String>()
  744. };
  745. let mut asset_path = AssetKey::from(path.as_str());
  746. let asset_response = assets
  747. .get(&path.as_str().into())
  748. .or_else(|| {
  749. eprintln!("Asset `{path}` not found; fallback to {path}.html");
  750. let fallback = format!("{}.html", path.as_str()).into();
  751. let asset = assets.get(&fallback);
  752. asset_path = fallback;
  753. asset
  754. })
  755. .or_else(|| {
  756. debug_eprintln!(
  757. "Asset `{}` not found; fallback to {}/index.html",
  758. path,
  759. path
  760. );
  761. let fallback = format!("{}/index.html", path.as_str()).into();
  762. let asset = assets.get(&fallback);
  763. asset_path = fallback;
  764. asset
  765. })
  766. .or_else(|| {
  767. debug_eprintln!("Asset `{}` not found; fallback to index.html", path);
  768. let fallback = AssetKey::from("index.html");
  769. let asset = assets.get(&fallback);
  770. asset_path = fallback;
  771. asset
  772. })
  773. .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
  774. .map(Cow::into_owned);
  775. let mut csp_header = None;
  776. let is_html = asset_path.as_ref().ends_with(".html");
  777. match asset_response {
  778. Ok(asset) => {
  779. let final_data = if is_html {
  780. let mut asset = String::from_utf8_lossy(&asset).into_owned();
  781. if let Some(csp) = self.csp() {
  782. csp_header.replace(set_csp(
  783. &mut asset,
  784. self.inner.assets.clone(),
  785. &asset_path,
  786. self,
  787. csp,
  788. ));
  789. }
  790. asset.as_bytes().to_vec()
  791. } else {
  792. asset
  793. };
  794. let mime_type = MimeType::parse(&final_data, &path);
  795. Ok(Asset {
  796. bytes: final_data.to_vec(),
  797. mime_type,
  798. csp_header,
  799. })
  800. }
  801. Err(e) => {
  802. debug_eprintln!("{:?}", e); // TODO log::error!
  803. Err(Box::new(e))
  804. }
  805. }
  806. }
  807. #[allow(clippy::type_complexity)]
  808. fn prepare_uri_scheme_protocol(
  809. &self,
  810. window_origin: &str,
  811. web_resource_request_handler: Option<
  812. Box<dyn Fn(&HttpRequest, &mut HttpResponse) + Send + Sync>,
  813. >,
  814. ) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
  815. {
  816. #[cfg(all(dev, mobile))]
  817. let url = {
  818. let mut url = self.get_url().as_str().to_string();
  819. if url.ends_with('/') {
  820. url.pop();
  821. }
  822. url
  823. };
  824. #[cfg(not(all(dev, mobile)))]
  825. let manager = self.clone();
  826. let window_origin = window_origin.to_string();
  827. #[cfg(all(dev, mobile))]
  828. #[derive(Clone)]
  829. struct CachedResponse {
  830. status: http::StatusCode,
  831. headers: http::HeaderMap,
  832. body: bytes::Bytes,
  833. }
  834. #[cfg(all(dev, mobile))]
  835. let response_cache = Arc::new(Mutex::new(HashMap::new()));
  836. Box::new(move |request| {
  837. // use the entire URI as we are going to proxy the request
  838. let path = if PROXY_DEV_SERVER {
  839. request.uri()
  840. } else {
  841. // ignore query string and fragment
  842. request.uri().split(&['?', '#'][..]).next().unwrap()
  843. };
  844. let path = path
  845. .strip_prefix("tauri://localhost")
  846. .map(|p| p.to_string())
  847. // the `strip_prefix` only returns None when a request is made to `https://tauri.$P` on Windows
  848. // where `$P` is not `localhost/*`
  849. .unwrap_or_else(|| "".to_string());
  850. let mut builder =
  851. HttpResponseBuilder::new().header("Access-Control-Allow-Origin", &window_origin);
  852. #[cfg(all(dev, mobile))]
  853. let mut response = {
  854. use reqwest::StatusCode;
  855. let decoded_path = percent_encoding::percent_decode(path.as_bytes())
  856. .decode_utf8_lossy()
  857. .to_string();
  858. let url = format!("{url}{decoded_path}");
  859. #[allow(unused_mut)]
  860. let mut client_builder = reqwest::ClientBuilder::new();
  861. #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
  862. {
  863. client_builder = client_builder.danger_accept_invalid_certs(true);
  864. }
  865. let mut proxy_builder = client_builder.build().unwrap().get(&url);
  866. for (name, value) in request.headers() {
  867. proxy_builder = proxy_builder.header(name, value);
  868. }
  869. match crate::async_runtime::block_on(proxy_builder.send()) {
  870. Ok(r) => {
  871. let mut response_cache_ = response_cache.lock().unwrap();
  872. let mut response = None;
  873. if r.status() == StatusCode::NOT_MODIFIED {
  874. response = response_cache_.get(&url);
  875. }
  876. let response = if let Some(r) = response {
  877. r
  878. } else {
  879. let status = r.status();
  880. let headers = r.headers().clone();
  881. let body = crate::async_runtime::block_on(r.bytes())?;
  882. let response = CachedResponse {
  883. status,
  884. headers,
  885. body,
  886. };
  887. response_cache_.insert(url.clone(), response);
  888. response_cache_.get(&url).unwrap()
  889. };
  890. for (name, value) in &response.headers {
  891. builder = builder.header(name, value);
  892. }
  893. builder
  894. .status(response.status)
  895. .body(response.body.to_vec())?
  896. }
  897. Err(e) => {
  898. debug_eprintln!("Failed to request {}: {}", url.as_str(), e);
  899. return Err(Box::new(e));
  900. }
  901. }
  902. };
  903. #[cfg(not(all(dev, mobile)))]
  904. let mut response = {
  905. let asset = manager.get_asset(path)?;
  906. builder = builder.mimetype(&asset.mime_type);
  907. if let Some(csp) = &asset.csp_header {
  908. builder = builder.header("Content-Security-Policy", csp);
  909. }
  910. builder.body(asset.bytes)?
  911. };
  912. if let Some(handler) = &web_resource_request_handler {
  913. handler(request, &mut response);
  914. }
  915. // if it's an HTML file, we need to set the CSP meta tag on Linux
  916. #[cfg(all(not(dev), target_os = "linux"))]
  917. if let Some(response_csp) = response.headers().get("Content-Security-Policy") {
  918. let response_csp = String::from_utf8_lossy(response_csp.as_bytes());
  919. let html = String::from_utf8_lossy(response.body());
  920. let body = html.replacen(tauri_utils::html::CSP_TOKEN, &response_csp, 1);
  921. *response.body_mut() = body.as_bytes().to_vec().into();
  922. }
  923. Ok(response)
  924. })
  925. }
  926. fn initialization_script(
  927. &self,
  928. ipc_script: &str,
  929. pattern_script: &str,
  930. plugin_initialization_script: &str,
  931. with_global_tauri: bool,
  932. ) -> crate::Result<String> {
  933. #[derive(Template)]
  934. #[default_template("../scripts/init.js")]
  935. struct InitJavascript<'a> {
  936. origin: String,
  937. #[raw]
  938. pattern_script: &'a str,
  939. #[raw]
  940. ipc_script: &'a str,
  941. #[raw]
  942. bundle_script: &'a str,
  943. // A function to immediately listen to an event.
  944. #[raw]
  945. listen_function: &'a str,
  946. #[raw]
  947. core_script: &'a str,
  948. #[raw]
  949. window_dialogs_script: &'a str,
  950. #[raw]
  951. window_print_script: &'a str,
  952. #[raw]
  953. event_initialization_script: &'a str,
  954. #[raw]
  955. plugin_initialization_script: &'a str,
  956. #[raw]
  957. freeze_prototype: &'a str,
  958. #[raw]
  959. hotkeys: &'a str,
  960. }
  961. let bundle_script = if with_global_tauri {
  962. include_str!("../scripts/bundle.global.js")
  963. } else {
  964. ""
  965. };
  966. let freeze_prototype = if self.inner.config.tauri.security.freeze_prototype {
  967. include_str!("../scripts/freeze_prototype.js")
  968. } else {
  969. ""
  970. };
  971. #[cfg(any(debug_assertions, feature = "devtools"))]
  972. let hotkeys = &format!(
  973. "
  974. {};
  975. window.hotkeys('{}', () => {{
  976. window.__TAURI_INVOKE__('tauri', {{
  977. __tauriModule: 'Window',
  978. message: {{
  979. cmd: 'manage',
  980. data: {{
  981. cmd: {{
  982. type: '__toggleDevtools'
  983. }}
  984. }}
  985. }}
  986. }});
  987. }});
  988. ",
  989. include_str!("../scripts/hotkey.js"),
  990. if cfg!(target_os = "macos") {
  991. "command+option+i"
  992. } else {
  993. "ctrl+shift+i"
  994. }
  995. );
  996. #[cfg(not(any(debug_assertions, feature = "devtools")))]
  997. let hotkeys = "";
  998. InitJavascript {
  999. origin: self.get_browser_origin(),
  1000. pattern_script,
  1001. ipc_script,
  1002. bundle_script,
  1003. listen_function: &format!(
  1004. "function listen(eventName, cb) {{ {} }}",
  1005. crate::event::listen_js(
  1006. self.event_listeners_object_name(),
  1007. "eventName".into(),
  1008. 0,
  1009. None,
  1010. "window['_' + window.__TAURI__.transformCallback(cb) ]".into()
  1011. )
  1012. ),
  1013. core_script: include_str!("../scripts/core.js"),
  1014. // window.print works on Linux/Windows; need to use the API on macOS
  1015. #[cfg(any(target_os = "macos", target_os = "ios"))]
  1016. window_print_script: include_str!("../scripts/window_print.js"),
  1017. #[cfg(not(any(target_os = "macos", target_os = "ios")))]
  1018. window_print_script: "",
  1019. // dialogs are implemented natively on Android
  1020. #[cfg(not(target_os = "android"))]
  1021. window_dialogs_script: include_str!("../scripts/window_dialogs.js"),
  1022. #[cfg(target_os = "android")]
  1023. window_dialogs_script: "",
  1024. event_initialization_script: &self.event_initialization_script(),
  1025. plugin_initialization_script,
  1026. freeze_prototype,
  1027. hotkeys,
  1028. }
  1029. .render_default(&Default::default())
  1030. .map(|s| s.into_string())
  1031. .map_err(Into::into)
  1032. }
  1033. fn event_initialization_script(&self) -> String {
  1034. format!(
  1035. "
  1036. Object.defineProperty(window, '{function}', {{
  1037. value: function (eventData) {{
  1038. const listeners = (window['{listeners}'] && window['{listeners}'][eventData.event]) || []
  1039. for (let i = listeners.length - 1; i >= 0; i--) {{
  1040. const listener = listeners[i]
  1041. if (listener.windowLabel === null || listener.windowLabel === eventData.windowLabel) {{
  1042. eventData.id = listener.id
  1043. listener.handler(eventData)
  1044. }}
  1045. }}
  1046. }}
  1047. }});
  1048. ",
  1049. function = self.event_emit_function_name(),
  1050. listeners = self.event_listeners_object_name()
  1051. )
  1052. }
  1053. }
  1054. #[cfg(test)]
  1055. mod test {
  1056. use crate::{generate_context, plugin::PluginStore, StateManager, Wry};
  1057. use super::WindowManager;
  1058. #[test]
  1059. fn check_get_url() {
  1060. let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate);
  1061. let manager: WindowManager<Wry> = WindowManager::with_handlers(
  1062. context,
  1063. PluginStore::default(),
  1064. Box::new(|_| false),
  1065. Box::new(|_, _| ()),
  1066. Default::default(),
  1067. StateManager::new(),
  1068. Default::default(),
  1069. Default::default(),
  1070. (std::sync::Arc::new(|_, _, _, _| ()), "".into()),
  1071. );
  1072. #[cfg(custom_protocol)]
  1073. assert_eq!(manager.get_url().to_string(), "tauri://localhost");
  1074. #[cfg(dev)]
  1075. assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
  1076. }
  1077. }
  1078. impl<R: Runtime> WindowManager<R> {
  1079. pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
  1080. (self.inner.invoke_handler)(invoke)
  1081. }
  1082. pub fn run_on_page_load(&self, window: Window<R>, payload: PageLoadPayload) {
  1083. (self.inner.on_page_load)(window.clone(), payload.clone());
  1084. self
  1085. .inner
  1086. .plugins
  1087. .lock()
  1088. .expect("poisoned plugin store")
  1089. .on_page_load(window, payload);
  1090. }
  1091. pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
  1092. self
  1093. .inner
  1094. .plugins
  1095. .lock()
  1096. .expect("poisoned plugin store")
  1097. .extend_api(plugin, invoke)
  1098. }
  1099. pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
  1100. self
  1101. .inner
  1102. .plugins
  1103. .lock()
  1104. .expect("poisoned plugin store")
  1105. .initialize(app, &self.inner.config.plugins)
  1106. }
  1107. pub fn prepare_window(
  1108. &self,
  1109. app_handle: AppHandle<R>,
  1110. mut pending: PendingWindow<EventLoopMessage, R>,
  1111. window_labels: &[String],
  1112. web_resource_request_handler: Option<Box<WebResourceRequestHandler>>,
  1113. ) -> crate::Result<PendingWindow<EventLoopMessage, R>> {
  1114. if self.windows_lock().contains_key(&pending.label) {
  1115. return Err(crate::Error::WindowLabelAlreadyExists(pending.label));
  1116. }
  1117. #[allow(unused_mut)] // mut url only for the data-url parsing
  1118. let (is_local, mut url) = match &pending.webview_attributes.url {
  1119. WindowUrl::App(path) => {
  1120. let url = if PROXY_DEV_SERVER {
  1121. Cow::Owned(Url::parse("tauri://localhost").unwrap())
  1122. } else {
  1123. self.get_url()
  1124. };
  1125. (
  1126. true,
  1127. // ignore "index.html" just to simplify the url
  1128. if path.to_str() != Some("index.html") {
  1129. url
  1130. .join(&path.to_string_lossy())
  1131. .map_err(crate::Error::InvalidUrl)
  1132. // this will never fail
  1133. .unwrap()
  1134. } else {
  1135. url.into_owned()
  1136. },
  1137. )
  1138. }
  1139. WindowUrl::External(url) => {
  1140. let config_url = self.get_url();
  1141. let is_local = config_url.make_relative(url).is_some();
  1142. let mut url = url.clone();
  1143. if is_local && PROXY_DEV_SERVER {
  1144. url.set_scheme("tauri").unwrap();
  1145. url.set_host(Some("localhost")).unwrap();
  1146. }
  1147. (is_local, url)
  1148. }
  1149. _ => unimplemented!(),
  1150. };
  1151. #[cfg(not(feature = "window-data-url"))]
  1152. if url.scheme() == "data" {
  1153. return Err(crate::Error::InvalidWindowUrl(
  1154. "data URLs are not supported without the `window-data-url` feature.",
  1155. ));
  1156. }
  1157. #[cfg(feature = "window-data-url")]
  1158. if let Some(csp) = self.csp() {
  1159. if url.scheme() == "data" {
  1160. if let Ok(data_url) = data_url::DataUrl::process(url.as_str()) {
  1161. let (body, _) = data_url.decode_to_vec().unwrap();
  1162. let html = String::from_utf8_lossy(&body).into_owned();
  1163. // naive way to check if it's an html
  1164. if html.contains('<') && html.contains('>') {
  1165. let mut document = tauri_utils::html::parse(html);
  1166. tauri_utils::html::inject_csp(&mut document, &csp.to_string());
  1167. url.set_path(&format!("text/html,{}", document.to_string()));
  1168. }
  1169. }
  1170. }
  1171. }
  1172. pending.url = url.to_string();
  1173. if !pending.window_builder.has_icon() {
  1174. if let Some(default_window_icon) = self.inner.default_window_icon.clone() {
  1175. pending.window_builder = pending
  1176. .window_builder
  1177. .icon(default_window_icon.try_into()?)?;
  1178. }
  1179. }
  1180. if pending.window_builder.get_menu().is_none() {
  1181. if let Some(menu) = &self.inner.menu {
  1182. pending = pending.set_menu(menu.clone());
  1183. }
  1184. }
  1185. #[cfg(target_os = "android")]
  1186. {
  1187. pending = pending.on_webview_created(move |ctx| {
  1188. let plugin_manager = ctx
  1189. .env
  1190. .call_method(
  1191. ctx.activity,
  1192. "getPluginManager",
  1193. "()Lapp/tauri/plugin/PluginManager;",
  1194. &[],
  1195. )?
  1196. .l()?;
  1197. // tell the manager the webview is ready
  1198. ctx.env.call_method(
  1199. plugin_manager,
  1200. "onWebViewCreated",
  1201. "(Landroid/webkit/WebView;)V",
  1202. &[ctx.webview.into()],
  1203. )?;
  1204. Ok(())
  1205. });
  1206. }
  1207. // in `Windows`, we need to force a data_directory
  1208. // but we do respect user-specification
  1209. #[cfg(any(target_os = "linux", target_os = "windows"))]
  1210. if pending.webview_attributes.data_directory.is_none() {
  1211. let local_app_data = app_handle.path().resolve(
  1212. &self.inner.config.tauri.bundle.identifier,
  1213. BaseDirectory::LocalData,
  1214. );
  1215. if let Ok(user_data_dir) = local_app_data {
  1216. pending.webview_attributes.data_directory = Some(user_data_dir);
  1217. }
  1218. }
  1219. // make sure the directory is created and available to prevent a panic
  1220. if let Some(user_data_dir) = &pending.webview_attributes.data_directory {
  1221. if !user_data_dir.exists() {
  1222. create_dir_all(user_data_dir)?;
  1223. }
  1224. }
  1225. if is_local {
  1226. let label = pending.label.clone();
  1227. pending = self.prepare_pending_window(
  1228. pending,
  1229. &label,
  1230. window_labels,
  1231. app_handle.clone(),
  1232. web_resource_request_handler,
  1233. )?;
  1234. pending.ipc_handler = Some(self.prepare_ipc_handler(app_handle));
  1235. }
  1236. Ok(pending)
  1237. }
  1238. pub fn attach_window(
  1239. &self,
  1240. app_handle: AppHandle<R>,
  1241. window: DetachedWindow<EventLoopMessage, R>,
  1242. ) -> Window<R> {
  1243. let window = Window::new(self.clone(), window, app_handle);
  1244. let window_ = window.clone();
  1245. let window_event_listeners = self.inner.window_event_listeners.clone();
  1246. let manager = self.clone();
  1247. window.on_window_event(move |event| {
  1248. let _ = on_window_event(&window_, &manager, event);
  1249. for handler in window_event_listeners.iter() {
  1250. handler(GlobalWindowEvent {
  1251. window: window_.clone(),
  1252. event: event.clone(),
  1253. });
  1254. }
  1255. });
  1256. {
  1257. let window_ = window.clone();
  1258. let menu_event_listeners = self.inner.menu_event_listeners.clone();
  1259. window.on_menu_event(move |event| {
  1260. let _ = on_menu_event(&window_, &event);
  1261. for handler in menu_event_listeners.iter() {
  1262. handler(WindowMenuEvent {
  1263. window: window_.clone(),
  1264. menu_item_id: event.menu_item_id.clone(),
  1265. });
  1266. }
  1267. });
  1268. }
  1269. // insert the window into our manager
  1270. {
  1271. self
  1272. .windows_lock()
  1273. .insert(window.label().to_string(), window.clone());
  1274. }
  1275. // let plugins know that a new window has been added to the manager
  1276. let manager = self.inner.clone();
  1277. let window_ = window.clone();
  1278. // run on main thread so the plugin store doesn't dead lock with the event loop handler in App
  1279. let _ = window.run_on_main_thread(move || {
  1280. manager
  1281. .plugins
  1282. .lock()
  1283. .expect("poisoned plugin store")
  1284. .created(window_);
  1285. });
  1286. #[cfg(target_os = "ios")]
  1287. {
  1288. window
  1289. .with_webview(|w| {
  1290. unsafe { crate::ios::on_webview_created(w.inner() as _, w.view_controller() as _) };
  1291. })
  1292. .expect("failed to run on_webview_created hook");
  1293. }
  1294. window
  1295. }
  1296. pub(crate) fn on_window_close(&self, label: &str) {
  1297. self.windows_lock().remove(label);
  1298. }
  1299. pub fn emit_filter<S, F>(
  1300. &self,
  1301. event: &str,
  1302. source_window_label: Option<&str>,
  1303. payload: S,
  1304. filter: F,
  1305. ) -> crate::Result<()>
  1306. where
  1307. S: Serialize + Clone,
  1308. F: Fn(&Window<R>) -> bool,
  1309. {
  1310. assert_event_name_is_valid(event);
  1311. self
  1312. .windows_lock()
  1313. .values()
  1314. .filter(|&w| filter(w))
  1315. .try_for_each(|window| window.emit_internal(event, source_window_label, payload.clone()))
  1316. }
  1317. pub fn eval_script_all<S: Into<String>>(&self, script: S) -> crate::Result<()> {
  1318. let script = script.into();
  1319. self
  1320. .windows_lock()
  1321. .values()
  1322. .try_for_each(|window| window.eval(&script))
  1323. }
  1324. pub fn labels(&self) -> HashSet<String> {
  1325. self.windows_lock().keys().cloned().collect()
  1326. }
  1327. pub fn config(&self) -> Arc<Config> {
  1328. self.inner.config.clone()
  1329. }
  1330. pub fn package_info(&self) -> &PackageInfo {
  1331. &self.inner.package_info
  1332. }
  1333. pub fn unlisten(&self, handler_id: EventHandler) {
  1334. self.inner.listeners.unlisten(handler_id)
  1335. }
  1336. pub fn trigger(&self, event: &str, window: Option<String>, data: Option<String>) {
  1337. assert_event_name_is_valid(event);
  1338. self.inner.listeners.trigger(event, window, data)
  1339. }
  1340. pub fn listen<F: Fn(Event) + Send + 'static>(
  1341. &self,
  1342. event: String,
  1343. window: Option<String>,
  1344. handler: F,
  1345. ) -> EventHandler {
  1346. assert_event_name_is_valid(&event);
  1347. self.inner.listeners.listen(event, window, handler)
  1348. }
  1349. pub fn once<F: FnOnce(Event) + Send + 'static>(
  1350. &self,
  1351. event: String,
  1352. window: Option<String>,
  1353. handler: F,
  1354. ) -> EventHandler {
  1355. assert_event_name_is_valid(&event);
  1356. self.inner.listeners.once(event, window, handler)
  1357. }
  1358. pub fn event_listeners_object_name(&self) -> String {
  1359. self.inner.listeners.listeners_object_name()
  1360. }
  1361. pub fn event_emit_function_name(&self) -> String {
  1362. self.inner.listeners.function_name()
  1363. }
  1364. pub fn get_window(&self, label: &str) -> Option<Window<R>> {
  1365. self.windows_lock().get(label).cloned()
  1366. }
  1367. pub fn windows(&self) -> HashMap<String, Window<R>> {
  1368. self.windows_lock().clone()
  1369. }
  1370. }
  1371. /// Tray APIs
  1372. #[cfg(all(desktop, feature = "system-tray"))]
  1373. impl<R: Runtime> WindowManager<R> {
  1374. pub fn get_tray(&self, id: &str) -> Option<crate::SystemTrayHandle<R>> {
  1375. self.inner.trays.lock().unwrap().get(id).cloned()
  1376. }
  1377. pub fn trays(&self) -> HashMap<String, crate::SystemTrayHandle<R>> {
  1378. self.inner.trays.lock().unwrap().clone()
  1379. }
  1380. pub fn attach_tray(&self, id: String, tray: crate::SystemTrayHandle<R>) {
  1381. self.inner.trays.lock().unwrap().insert(id, tray);
  1382. }
  1383. pub fn get_tray_by_runtime_id(&self, id: u16) -> Option<(String, crate::SystemTrayHandle<R>)> {
  1384. let trays = self.inner.trays.lock().unwrap();
  1385. let iter = trays.iter();
  1386. for (tray_id, tray) in iter {
  1387. if tray.id == id {
  1388. return Some((tray_id.clone(), tray.clone()));
  1389. }
  1390. }
  1391. None
  1392. }
  1393. }
  1394. fn on_window_event<R: Runtime>(
  1395. window: &Window<R>,
  1396. manager: &WindowManager<R>,
  1397. event: &WindowEvent,
  1398. ) -> crate::Result<()> {
  1399. match event {
  1400. WindowEvent::Resized(size) => window.emit(WINDOW_RESIZED_EVENT, size)?,
  1401. WindowEvent::Moved(position) => window.emit(WINDOW_MOVED_EVENT, position)?,
  1402. WindowEvent::CloseRequested { api } => {
  1403. if window.has_js_listener(Some(window.label().into()), WINDOW_CLOSE_REQUESTED_EVENT) {
  1404. api.prevent_close();
  1405. }
  1406. window.emit(WINDOW_CLOSE_REQUESTED_EVENT, ())?;
  1407. }
  1408. WindowEvent::Destroyed => {
  1409. window.emit(WINDOW_DESTROYED_EVENT, ())?;
  1410. let label = window.label();
  1411. let windows_map = manager.inner.windows.lock().unwrap();
  1412. let windows = windows_map.values();
  1413. for window in windows {
  1414. window.eval(&format!(
  1415. r#"(function () {{ const metadata = window.__TAURI_METADATA__; if (metadata != null) {{ metadata.__windows = window.__TAURI_METADATA__.__windows.filter(w => w.label !== "{label}"); }} }})()"#,
  1416. ))?;
  1417. }
  1418. }
  1419. WindowEvent::Focused(focused) => window.emit(
  1420. if *focused {
  1421. WINDOW_FOCUS_EVENT
  1422. } else {
  1423. WINDOW_BLUR_EVENT
  1424. },
  1425. (),
  1426. )?,
  1427. WindowEvent::ScaleFactorChanged {
  1428. scale_factor,
  1429. new_inner_size,
  1430. ..
  1431. } => window.emit(
  1432. WINDOW_SCALE_FACTOR_CHANGED_EVENT,
  1433. ScaleFactorChanged {
  1434. scale_factor: *scale_factor,
  1435. size: *new_inner_size,
  1436. },
  1437. )?,
  1438. WindowEvent::FileDrop(event) => match event {
  1439. FileDropEvent::Hovered(paths) => window.emit(WINDOW_FILE_DROP_HOVER_EVENT, paths)?,
  1440. FileDropEvent::Dropped(paths) => {
  1441. let scopes = window.state::<Scopes>();
  1442. for path in paths {
  1443. if path.is_file() {
  1444. let _ = scopes.allow_file(path);
  1445. } else {
  1446. let _ = scopes.allow_directory(path, false);
  1447. }
  1448. }
  1449. window.emit(WINDOW_FILE_DROP_EVENT, paths)?
  1450. }
  1451. FileDropEvent::Cancelled => window.emit(WINDOW_FILE_DROP_CANCELLED_EVENT, ())?,
  1452. _ => unimplemented!(),
  1453. },
  1454. WindowEvent::ThemeChanged(theme) => window.emit(WINDOW_THEME_CHANGED, theme.to_string())?,
  1455. }
  1456. Ok(())
  1457. }
  1458. #[derive(Clone, Serialize)]
  1459. #[serde(rename_all = "camelCase")]
  1460. struct ScaleFactorChanged {
  1461. scale_factor: f64,
  1462. size: PhysicalSize<u32>,
  1463. }
  1464. fn on_menu_event<R: Runtime>(window: &Window<R>, event: &MenuEvent) -> crate::Result<()> {
  1465. window.emit(MENU_EVENT, event.menu_item_id.clone())
  1466. }
  1467. #[cfg(feature = "isolation")]
  1468. fn request_to_path(request: &tauri_runtime::http::Request, base_url: &str) -> String {
  1469. let mut path = request
  1470. .uri()
  1471. .split(&['?', '#'][..])
  1472. // ignore query string
  1473. .next()
  1474. .unwrap()
  1475. .trim_start_matches(base_url)
  1476. .to_string();
  1477. if path.ends_with('/') {
  1478. path.pop();
  1479. }
  1480. let path = percent_encoding::percent_decode(path.as_bytes())
  1481. .decode_utf8_lossy()
  1482. .to_string();
  1483. if path.is_empty() {
  1484. // if the url has no path, we should load `index.html`
  1485. "index.html".to_string()
  1486. } else {
  1487. // skip leading `/`
  1488. path.chars().skip(1).collect()
  1489. }
  1490. }
  1491. #[cfg(test)]
  1492. mod tests {
  1493. use super::replace_with_callback;
  1494. #[test]
  1495. fn string_replace_with_callback() {
  1496. let mut tauri_index = 0;
  1497. #[allow(clippy::single_element_loop)]
  1498. for (src, pattern, replacement, result) in [(
  1499. "tauri is awesome, tauri is amazing",
  1500. "tauri",
  1501. || {
  1502. tauri_index += 1;
  1503. tauri_index.to_string()
  1504. },
  1505. "1 is awesome, 2 is amazing",
  1506. )] {
  1507. assert_eq!(replace_with_callback(src, pattern, replacement), result);
  1508. }
  1509. }
  1510. }