context.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::collections::BTreeMap;
  5. use std::convert::identity;
  6. use std::path::{Path, PathBuf};
  7. use std::{ffi::OsStr, str::FromStr};
  8. use crate::{
  9. embedded_assets::{
  10. ensure_out_dir, AssetOptions, CspHashes, EmbeddedAssets, EmbeddedAssetsResult,
  11. },
  12. image::CachedIcon,
  13. };
  14. use base64::Engine;
  15. use proc_macro2::TokenStream;
  16. use quote::quote;
  17. use sha2::{Digest, Sha256};
  18. use syn::Expr;
  19. use tauri_utils::{
  20. acl::capability::{Capability, CapabilityFile},
  21. acl::manifest::Manifest,
  22. acl::resolved::Resolved,
  23. assets::AssetKey,
  24. config::{CapabilityEntry, Config, FrontendDist, PatternKind},
  25. html::{inject_nonce_token, parse as parse_html, serialize_node as serialize_html_node, NodeRef},
  26. platform::Target,
  27. plugin::GLOBAL_API_SCRIPT_FILE_LIST_PATH,
  28. tokens::{map_lit, str_lit},
  29. };
  30. const ACL_MANIFESTS_FILE_NAME: &str = "acl-manifests.json";
  31. const CAPABILITIES_FILE_NAME: &str = "capabilities.json";
  32. /// Necessary data needed by [`context_codegen`] to generate code for a Tauri application context.
  33. pub struct ContextData {
  34. pub dev: bool,
  35. pub config: Config,
  36. pub config_parent: PathBuf,
  37. pub root: TokenStream,
  38. /// Additional capabilities to include.
  39. pub capabilities: Option<Vec<PathBuf>>,
  40. /// The custom assets implementation
  41. pub assets: Option<Expr>,
  42. /// Skip runtime-only types generation for tests (e.g. embed-plist usage).
  43. pub test: bool,
  44. }
  45. fn inject_script_hashes(document: &NodeRef, key: &AssetKey, csp_hashes: &mut CspHashes) {
  46. if let Ok(inline_script_elements) = document.select("script:not(empty)") {
  47. let mut scripts = Vec::new();
  48. for inline_script_el in inline_script_elements {
  49. let script = inline_script_el.as_node().text_contents();
  50. let mut hasher = Sha256::new();
  51. hasher.update(&script);
  52. let hash = hasher.finalize();
  53. scripts.push(format!(
  54. "'sha256-{}'",
  55. base64::engine::general_purpose::STANDARD.encode(hash)
  56. ));
  57. }
  58. csp_hashes
  59. .inline_scripts
  60. .entry(key.clone().into())
  61. .or_default()
  62. .append(&mut scripts);
  63. }
  64. }
  65. fn map_core_assets(
  66. options: &AssetOptions,
  67. ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> {
  68. let csp = options.csp;
  69. let dangerous_disable_asset_csp_modification =
  70. options.dangerous_disable_asset_csp_modification.clone();
  71. move |key, path, input, csp_hashes| {
  72. if path.extension() == Some(OsStr::new("html")) {
  73. #[allow(clippy::collapsible_if)]
  74. if csp {
  75. let document = parse_html(String::from_utf8_lossy(input).into_owned());
  76. inject_nonce_token(&document, &dangerous_disable_asset_csp_modification);
  77. if dangerous_disable_asset_csp_modification.can_modify("script-src") {
  78. inject_script_hashes(&document, key, csp_hashes);
  79. }
  80. *input = serialize_html_node(&document);
  81. }
  82. }
  83. Ok(())
  84. }
  85. }
  86. #[cfg(feature = "isolation")]
  87. fn map_isolation(
  88. _options: &AssetOptions,
  89. dir: PathBuf,
  90. ) -> impl Fn(&AssetKey, &Path, &mut Vec<u8>, &mut CspHashes) -> EmbeddedAssetsResult<()> {
  91. // create the csp for the isolation iframe styling now, to make the runtime less complex
  92. let mut hasher = Sha256::new();
  93. hasher.update(tauri_utils::pattern::isolation::IFRAME_STYLE);
  94. let hash = hasher.finalize();
  95. let iframe_style_csp_hash = format!(
  96. "'sha256-{}'",
  97. base64::engine::general_purpose::STANDARD.encode(hash)
  98. );
  99. move |key, path, input, csp_hashes| {
  100. if path.extension() == Some(OsStr::new("html")) {
  101. let isolation_html = parse_html(String::from_utf8_lossy(input).into_owned());
  102. // this is appended, so no need to reverse order it
  103. tauri_utils::html::inject_codegen_isolation_script(&isolation_html);
  104. // temporary workaround for windows not loading assets
  105. tauri_utils::html::inline_isolation(&isolation_html, &dir);
  106. inject_nonce_token(
  107. &isolation_html,
  108. &tauri_utils::config::DisabledCspModificationKind::Flag(false),
  109. );
  110. inject_script_hashes(&isolation_html, key, csp_hashes);
  111. csp_hashes.styles.push(iframe_style_csp_hash.clone());
  112. *input = isolation_html.to_string().as_bytes().to_vec()
  113. }
  114. Ok(())
  115. }
  116. }
  117. /// Build a `tauri::Context` for including in application code.
  118. pub fn context_codegen(data: ContextData) -> EmbeddedAssetsResult<TokenStream> {
  119. let ContextData {
  120. dev,
  121. config,
  122. config_parent,
  123. root,
  124. capabilities: additional_capabilities,
  125. assets,
  126. test,
  127. } = data;
  128. #[allow(unused_variables)]
  129. let running_tests = test;
  130. let target = std::env::var("TAURI_ENV_TARGET_TRIPLE")
  131. .as_deref()
  132. .map(Target::from_triple)
  133. .unwrap_or_else(|_| Target::current());
  134. let mut options = AssetOptions::new(config.app.security.pattern.clone())
  135. .freeze_prototype(config.app.security.freeze_prototype)
  136. .dangerous_disable_asset_csp_modification(
  137. config
  138. .app
  139. .security
  140. .dangerous_disable_asset_csp_modification
  141. .clone(),
  142. );
  143. let csp = if dev {
  144. config
  145. .app
  146. .security
  147. .dev_csp
  148. .as_ref()
  149. .or(config.app.security.csp.as_ref())
  150. } else {
  151. config.app.security.csp.as_ref()
  152. };
  153. if csp.is_some() {
  154. options = options.with_csp();
  155. }
  156. let assets = if let Some(assets) = assets {
  157. quote!(#assets)
  158. } else if dev && config.build.dev_url.is_some() {
  159. let assets = EmbeddedAssets::default();
  160. quote!(#assets)
  161. } else {
  162. let assets = match &config.build.frontend_dist {
  163. Some(url) => match url {
  164. FrontendDist::Url(_url) => Default::default(),
  165. FrontendDist::Directory(path) => {
  166. let assets_path = config_parent.join(path);
  167. if !assets_path.exists() {
  168. panic!(
  169. "The `frontendDist` configuration is set to `{path:?}` but this path doesn't exist"
  170. )
  171. }
  172. EmbeddedAssets::new(assets_path, &options, map_core_assets(&options))?
  173. }
  174. FrontendDist::Files(files) => EmbeddedAssets::new(
  175. files
  176. .iter()
  177. .map(|p| config_parent.join(p))
  178. .collect::<Vec<_>>(),
  179. &options,
  180. map_core_assets(&options),
  181. )?,
  182. _ => unimplemented!(),
  183. },
  184. None => Default::default(),
  185. };
  186. quote!(#assets)
  187. };
  188. let out_dir = ensure_out_dir()?;
  189. let default_window_icon = {
  190. if target == Target::Windows {
  191. // handle default window icons for Windows targets
  192. let icon_path = find_icon(
  193. &config,
  194. &config_parent,
  195. |i| i.ends_with(".ico"),
  196. "icons/icon.ico",
  197. );
  198. if icon_path.exists() {
  199. let icon = CachedIcon::new(&root, &icon_path)?;
  200. quote!(::std::option::Option::Some(#icon))
  201. } else {
  202. let icon_path = find_icon(
  203. &config,
  204. &config_parent,
  205. |i| i.ends_with(".png"),
  206. "icons/icon.png",
  207. );
  208. let icon = CachedIcon::new(&root, &icon_path)?;
  209. quote!(::std::option::Option::Some(#icon))
  210. }
  211. } else {
  212. // handle default window icons for Unix targets
  213. let icon_path = find_icon(
  214. &config,
  215. &config_parent,
  216. |i| i.ends_with(".png"),
  217. "icons/icon.png",
  218. );
  219. let icon = CachedIcon::new(&root, &icon_path)?;
  220. quote!(::std::option::Option::Some(#icon))
  221. }
  222. };
  223. let app_icon = if target == Target::MacOS && dev {
  224. let mut icon_path = find_icon(
  225. &config,
  226. &config_parent,
  227. |i| i.ends_with(".icns"),
  228. "icons/icon.png",
  229. );
  230. if !icon_path.exists() {
  231. icon_path = find_icon(
  232. &config,
  233. &config_parent,
  234. |i| i.ends_with(".png"),
  235. "icons/icon.png",
  236. );
  237. }
  238. let icon = CachedIcon::new_raw(&root, &icon_path)?;
  239. quote!(::std::option::Option::Some(#icon.to_vec()))
  240. } else {
  241. quote!(::std::option::Option::None)
  242. };
  243. let package_name = if let Some(product_name) = &config.product_name {
  244. quote!(#product_name.to_string())
  245. } else {
  246. quote!(env!("CARGO_PKG_NAME").to_string())
  247. };
  248. let package_version = if let Some(version) = &config.version {
  249. semver::Version::from_str(version)?;
  250. quote!(#version.to_string())
  251. } else {
  252. quote!(env!("CARGO_PKG_VERSION").to_string())
  253. };
  254. let package_info = quote!(
  255. #root::PackageInfo {
  256. name: #package_name,
  257. version: #package_version.parse().unwrap(),
  258. authors: env!("CARGO_PKG_AUTHORS"),
  259. description: env!("CARGO_PKG_DESCRIPTION"),
  260. crate_name: env!("CARGO_PKG_NAME"),
  261. }
  262. );
  263. let with_tray_icon_code = if target.is_desktop() {
  264. if let Some(tray) = &config.app.tray_icon {
  265. let tray_icon_icon_path = config_parent.join(&tray.icon_path);
  266. let icon = CachedIcon::new(&root, &tray_icon_icon_path)?;
  267. quote!(context.set_tray_icon(::std::option::Option::Some(#icon));)
  268. } else {
  269. quote!()
  270. }
  271. } else {
  272. quote!()
  273. };
  274. #[cfg(target_os = "macos")]
  275. let info_plist = if target == Target::MacOS && dev && !running_tests {
  276. let info_plist_path = config_parent.join("Info.plist");
  277. let mut info_plist = if info_plist_path.exists() {
  278. plist::Value::from_file(&info_plist_path)
  279. .unwrap_or_else(|e| panic!("failed to read plist {}: {}", info_plist_path.display(), e))
  280. } else {
  281. plist::Value::Dictionary(Default::default())
  282. };
  283. if let Some(plist) = info_plist.as_dictionary_mut() {
  284. if let Some(product_name) = &config.product_name {
  285. plist.insert("CFBundleName".into(), product_name.clone().into());
  286. }
  287. if let Some(version) = &config.version {
  288. plist.insert("CFBundleShortVersionString".into(), version.clone().into());
  289. plist.insert("CFBundleVersion".into(), version.clone().into());
  290. }
  291. }
  292. let mut plist_contents = std::io::BufWriter::new(Vec::new());
  293. info_plist
  294. .to_writer_xml(&mut plist_contents)
  295. .expect("failed to serialize plist");
  296. let plist_contents =
  297. String::from_utf8_lossy(&plist_contents.into_inner().unwrap()).into_owned();
  298. let plist = crate::Cached::try_from(plist_contents)?;
  299. quote!({
  300. tauri::embed_plist::embed_info_plist!(#plist);
  301. })
  302. } else {
  303. quote!(())
  304. };
  305. #[cfg(not(target_os = "macos"))]
  306. let info_plist = quote!(());
  307. let pattern = match &options.pattern {
  308. PatternKind::Brownfield => quote!(#root::Pattern::Brownfield),
  309. #[cfg(not(feature = "isolation"))]
  310. PatternKind::Isolation { dir: _ } => {
  311. quote!(#root::Pattern::Brownfield)
  312. }
  313. #[cfg(feature = "isolation")]
  314. PatternKind::Isolation { dir } => {
  315. let dir = config_parent.join(dir);
  316. if !dir.exists() {
  317. panic!("The isolation application path is set to `{dir:?}` but it does not exist")
  318. }
  319. let mut sets_isolation_hook = false;
  320. let key = uuid::Uuid::new_v4().to_string();
  321. let map_isolation = map_isolation(&options, dir.clone());
  322. let assets = EmbeddedAssets::new(dir, &options, |key, path, input, csp_hashes| {
  323. // we check if `__TAURI_ISOLATION_HOOK__` exists in the isolation code
  324. // before modifying the files since we inject our own `__TAURI_ISOLATION_HOOK__` reference in HTML files
  325. if String::from_utf8_lossy(input).contains("__TAURI_ISOLATION_HOOK__") {
  326. sets_isolation_hook = true;
  327. }
  328. map_isolation(key, path, input, csp_hashes)
  329. })?;
  330. if !sets_isolation_hook {
  331. panic!("The isolation application does not contain a file setting the `window.__TAURI_ISOLATION_HOOK__` value.");
  332. }
  333. let schema = options.isolation_schema;
  334. quote!(#root::Pattern::Isolation {
  335. assets: ::std::sync::Arc::new(#assets),
  336. schema: #schema.into(),
  337. key: #key.into(),
  338. crypto_keys: std::boxed::Box::new(::tauri::utils::pattern::isolation::Keys::new().expect("unable to generate cryptographically secure keys for Tauri \"Isolation\" Pattern")),
  339. })
  340. }
  341. };
  342. let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
  343. let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
  344. let acl_file =
  345. std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
  346. serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
  347. } else {
  348. Default::default()
  349. };
  350. let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
  351. let mut capabilities_from_files: BTreeMap<String, Capability> = if capabilities_file_path.exists()
  352. {
  353. let capabilities_file =
  354. std::fs::read_to_string(capabilities_file_path).expect("failed to read capabilities");
  355. serde_json::from_str(&capabilities_file).expect("failed to parse capabilities")
  356. } else {
  357. Default::default()
  358. };
  359. let mut capabilities = if config.app.security.capabilities.is_empty() {
  360. capabilities_from_files
  361. } else {
  362. let mut capabilities = BTreeMap::new();
  363. for capability_entry in &config.app.security.capabilities {
  364. match capability_entry {
  365. CapabilityEntry::Inlined(capability) => {
  366. capabilities.insert(capability.identifier.clone(), capability.clone());
  367. }
  368. CapabilityEntry::Reference(id) => {
  369. let capability = capabilities_from_files
  370. .remove(id)
  371. .unwrap_or_else(|| panic!("capability with identifier {id} not found"));
  372. capabilities.insert(id.clone(), capability);
  373. }
  374. }
  375. }
  376. capabilities
  377. };
  378. let acl_tokens = map_lit(
  379. quote! { ::std::collections::BTreeMap },
  380. &acl,
  381. str_lit,
  382. identity,
  383. );
  384. if let Some(paths) = additional_capabilities {
  385. for path in paths {
  386. let capability = CapabilityFile::load(&path)
  387. .unwrap_or_else(|e| panic!("failed to read capability {}: {e}", path.display()));
  388. match capability {
  389. CapabilityFile::Capability(c) => {
  390. capabilities.insert(c.identifier.clone(), c);
  391. }
  392. CapabilityFile::List(capabilities_list)
  393. | CapabilityFile::NamedList {
  394. capabilities: capabilities_list,
  395. } => {
  396. capabilities.extend(
  397. capabilities_list
  398. .into_iter()
  399. .map(|c| (c.identifier.clone(), c)),
  400. );
  401. }
  402. }
  403. }
  404. }
  405. let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");
  406. let runtime_authority = quote!(#root::ipc::RuntimeAuthority::new(#acl_tokens, #resolved));
  407. let plugin_global_api_script_file_list_path = out_dir.join(GLOBAL_API_SCRIPT_FILE_LIST_PATH);
  408. let plugin_global_api_script =
  409. if config.app.with_global_tauri && plugin_global_api_script_file_list_path.exists() {
  410. let file_list_str = std::fs::read_to_string(plugin_global_api_script_file_list_path)
  411. .expect("failed to read plugin global API script paths");
  412. let file_list = serde_json::from_str::<Vec<PathBuf>>(&file_list_str)
  413. .expect("failed to parse plugin global API script paths");
  414. let mut plugins = Vec::new();
  415. for path in file_list {
  416. plugins.push(std::fs::read_to_string(&path).unwrap_or_else(|e| {
  417. panic!(
  418. "failed to read plugin global API script {}: {e}",
  419. path.display()
  420. )
  421. }));
  422. }
  423. Some(plugins)
  424. } else {
  425. None
  426. };
  427. let plugin_global_api_script = if let Some(scripts) = plugin_global_api_script {
  428. let scripts = scripts.into_iter().map(|s| quote!(#s));
  429. quote!(::std::option::Option::Some(&[#(#scripts),*]))
  430. } else {
  431. quote!(::std::option::Option::None)
  432. };
  433. let maybe_config_parent_setter = if dev {
  434. let config_parent = config_parent.to_string_lossy();
  435. quote!({
  436. context.with_config_parent(#config_parent);
  437. })
  438. } else {
  439. quote!()
  440. };
  441. let context = quote!({
  442. #[allow(unused_mut, clippy::let_and_return)]
  443. let mut context = #root::Context::new(
  444. #config,
  445. ::std::boxed::Box::new(#assets),
  446. #default_window_icon,
  447. #app_icon,
  448. #package_info,
  449. #info_plist,
  450. #pattern,
  451. #runtime_authority,
  452. #plugin_global_api_script
  453. );
  454. #with_tray_icon_code
  455. #maybe_config_parent_setter
  456. context
  457. });
  458. Ok(quote!({
  459. let thread = ::std::thread::Builder::new()
  460. .name(String::from("generated tauri context creation"))
  461. .stack_size(8 * 1024 * 1024)
  462. .spawn(|| #context)
  463. .expect("unable to create thread with 8MiB stack");
  464. match thread.join() {
  465. Ok(context) => context,
  466. Err(_) => {
  467. eprintln!("the generated Tauri `Context` panicked during creation");
  468. ::std::process::exit(101);
  469. }
  470. }
  471. }))
  472. }
  473. fn find_icon(
  474. config: &Config,
  475. config_parent: &Path,
  476. predicate: impl Fn(&&String) -> bool,
  477. default: &str,
  478. ) -> PathBuf {
  479. let icon_path = config
  480. .bundle
  481. .icon
  482. .iter()
  483. .find(predicate)
  484. .map(AsRef::as_ref)
  485. .unwrap_or(default);
  486. config_parent.join(icon_path)
  487. }