core.rs 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use super::error::{Error, Result};
  5. use crate::api::{file::Extract, version};
  6. use base64::decode;
  7. use http::StatusCode;
  8. use minisign_verify::{PublicKey, Signature};
  9. use std::{
  10. collections::HashMap,
  11. env,
  12. ffi::OsStr,
  13. fs::{read_dir, remove_file, File, OpenOptions},
  14. io::{prelude::*, BufReader, Read},
  15. path::{Path, PathBuf},
  16. str::from_utf8,
  17. time::{SystemTime, UNIX_EPOCH},
  18. };
  19. #[cfg(target_os = "macos")]
  20. use std::fs::rename;
  21. #[cfg(not(target_os = "macos"))]
  22. use std::process::Command;
  23. #[cfg(target_os = "macos")]
  24. use crate::api::file::Move;
  25. use crate::api::http::{ClientBuilder, HttpRequestBuilder};
  26. #[cfg(target_os = "windows")]
  27. use std::process::exit;
  28. #[derive(Debug)]
  29. pub struct RemoteRelease {
  30. /// Version to install
  31. pub version: String,
  32. /// Release date
  33. pub date: String,
  34. /// Download URL for current platform
  35. pub download_url: String,
  36. /// Update short description
  37. pub body: Option<String>,
  38. /// Optional signature for the current platform
  39. pub signature: Option<String>,
  40. #[cfg(target_os = "windows")]
  41. /// Optional: Windows only try to use elevated task
  42. pub with_elevated_task: bool,
  43. }
  44. impl RemoteRelease {
  45. // Read JSON and confirm this is a valid Schema
  46. fn from_release(release: &serde_json::Value, target: &str) -> Result<RemoteRelease> {
  47. // Version or name is required for static and dynamic JSON
  48. // if `version` is not announced, we fallback to `name` (can be the tag name example v1.0.0)
  49. let version = match release.get("version") {
  50. Some(version) => version
  51. .as_str()
  52. .ok_or_else(|| {
  53. Error::RemoteMetadata("Unable to extract `version` from remote server".into())
  54. })?
  55. .trim_start_matches('v')
  56. .to_string(),
  57. None => release
  58. .get("name")
  59. .ok_or_else(|| Error::RemoteMetadata("Release missing `name` and `version`".into()))?
  60. .as_str()
  61. .ok_or_else(|| {
  62. Error::RemoteMetadata("Unable to extract `name` from remote server`".into())
  63. })?
  64. .trim_start_matches('v')
  65. .to_string(),
  66. };
  67. // pub_date is required default is: `N/A` if not provided by the remote JSON
  68. let date = release
  69. .get("pub_date")
  70. .and_then(|v| v.as_str())
  71. .unwrap_or("N/A")
  72. .to_string();
  73. // body is optional to build our update
  74. let body = release
  75. .get("notes")
  76. .map(|notes| notes.as_str().unwrap_or("").to_string());
  77. // signature is optional to build our update
  78. let mut signature = release
  79. .get("signature")
  80. .map(|signature| signature.as_str().unwrap_or("").to_string());
  81. let download_url;
  82. #[cfg(target_os = "windows")]
  83. let with_elevated_task;
  84. match release.get("platforms") {
  85. //
  86. // Did we have a platforms field?
  87. // If we did, that mean it's a static JSON.
  88. // The main difference with STATIC and DYNAMIC is static announce ALL platforms
  89. // and dynamic announce only the current platform.
  90. //
  91. // This could be used if you do NOT want an update server and use
  92. // a GIST, S3 or any static JSON file to announce your updates.
  93. //
  94. // Notes:
  95. // Dynamic help to reduce bandwidth usage or to intelligently update your clients
  96. // based on the request you give. The server can remotely drive behaviors like
  97. // rolling back or phased rollouts.
  98. //
  99. Some(platforms) => {
  100. // make sure we have our target available
  101. if let Some(current_target_data) = platforms.get(target) {
  102. // use provided signature if available
  103. signature = current_target_data
  104. .get("signature")
  105. .map(|found_signature| found_signature.as_str().unwrap_or("").to_string());
  106. // Download URL is required
  107. download_url = current_target_data
  108. .get("url")
  109. .ok_or_else(|| Error::RemoteMetadata("Release missing `url`".into()))?
  110. .as_str()
  111. .ok_or_else(|| {
  112. Error::RemoteMetadata("Unable to extract `url` from remote server`".into())
  113. })?
  114. .to_string();
  115. #[cfg(target_os = "windows")]
  116. {
  117. with_elevated_task = current_target_data
  118. .get("with_elevated_task")
  119. .and_then(|v| v.as_bool())
  120. .unwrap_or_default();
  121. }
  122. } else {
  123. // make sure we have an available platform from the static
  124. return Err(Error::RemoteMetadata("Platform not available".into()));
  125. }
  126. }
  127. // We don't have the `platforms` field announced, let's assume our
  128. // download URL is at the root of the JSON.
  129. None => {
  130. download_url = release
  131. .get("url")
  132. .ok_or_else(|| Error::RemoteMetadata("Release missing `url`".into()))?
  133. .as_str()
  134. .ok_or_else(|| {
  135. Error::RemoteMetadata("Unable to extract `url` from remote server`".into())
  136. })?
  137. .to_string();
  138. #[cfg(target_os = "windows")]
  139. {
  140. with_elevated_task = match release.get("with_elevated_task") {
  141. Some(with_elevated_task) => with_elevated_task.as_bool().unwrap_or(false),
  142. None => false,
  143. };
  144. }
  145. }
  146. }
  147. // Return our formatted release
  148. Ok(RemoteRelease {
  149. version,
  150. date,
  151. download_url,
  152. body,
  153. signature,
  154. #[cfg(target_os = "windows")]
  155. with_elevated_task,
  156. })
  157. }
  158. }
  159. #[derive(Debug)]
  160. pub struct UpdateBuilder<'a> {
  161. /// Current version we are running to compare with announced version
  162. pub current_version: &'a str,
  163. /// The URLs to checks updates. We suggest at least one fallback on a different domain.
  164. pub urls: Vec<String>,
  165. /// The platform the updater will check and install the update. Default is from `get_updater_target`
  166. pub target: Option<String>,
  167. /// The current executable path. Default is automatically extracted.
  168. pub executable_path: Option<PathBuf>,
  169. }
  170. impl<'a> Default for UpdateBuilder<'a> {
  171. fn default() -> Self {
  172. UpdateBuilder {
  173. urls: Vec::new(),
  174. target: None,
  175. executable_path: None,
  176. current_version: env!("CARGO_PKG_VERSION"),
  177. }
  178. }
  179. }
  180. // Create new updater instance and return an Update
  181. impl<'a> UpdateBuilder<'a> {
  182. pub fn new() -> Self {
  183. UpdateBuilder::default()
  184. }
  185. #[allow(dead_code)]
  186. pub fn url(mut self, url: String) -> Self {
  187. self.urls.push(url);
  188. self
  189. }
  190. /// Add multiple URLS at once inside a Vec for future reference
  191. pub fn urls(mut self, urls: &[String]) -> Self {
  192. let mut formatted_vec: Vec<String> = Vec::new();
  193. for url in urls {
  194. formatted_vec.push(url.to_owned());
  195. }
  196. self.urls = formatted_vec;
  197. self
  198. }
  199. /// Set the current app version, used to compare against the latest available version.
  200. /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
  201. pub fn current_version(mut self, ver: &'a str) -> Self {
  202. self.current_version = ver;
  203. self
  204. }
  205. /// Set the target (os)
  206. /// win32, win64, darwin and linux are currently supported
  207. #[allow(dead_code)]
  208. pub fn target(mut self, target: &str) -> Self {
  209. self.target = Some(target.to_owned());
  210. self
  211. }
  212. /// Set the executable path
  213. #[allow(dead_code)]
  214. pub fn executable_path<A: AsRef<Path>>(mut self, executable_path: A) -> Self {
  215. self.executable_path = Some(PathBuf::from(executable_path.as_ref()));
  216. self
  217. }
  218. pub async fn build(self) -> Result<Update> {
  219. let mut remote_release: Option<RemoteRelease> = None;
  220. // make sure we have at least one url
  221. if self.urls.is_empty() {
  222. return Err(Error::Builder(
  223. "Unable to check update, `url` is required.".into(),
  224. ));
  225. };
  226. // set current version if not set
  227. let current_version = self.current_version;
  228. // If no executable path provided, we use current_exe from rust
  229. let executable_path = if let Some(v) = &self.executable_path {
  230. v.clone()
  231. } else {
  232. // we expect it to fail if we can't find the executable path
  233. // without this path we can't continue the update process.
  234. env::current_exe()?
  235. };
  236. // Did the target is provided by the config?
  237. // Should be: linux, darwin, win32 or win64
  238. let target = if let Some(t) = &self.target {
  239. t.clone()
  240. } else {
  241. get_updater_target().ok_or(Error::UnsupportedPlatform)?
  242. };
  243. // Get the extract_path from the provided executable_path
  244. let extract_path = extract_path_from_executable(&executable_path);
  245. // Set SSL certs for linux if they aren't available.
  246. // We do not require to recheck in the download_and_install as we use
  247. // ENV variables, we can expect them to be set for the second call.
  248. #[cfg(target_os = "linux")]
  249. {
  250. if env::var_os("SSL_CERT_FILE").is_none() {
  251. env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
  252. }
  253. if env::var_os("SSL_CERT_DIR").is_none() {
  254. env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
  255. }
  256. }
  257. // Allow fallback if more than 1 urls is provided
  258. let mut last_error: Option<Error> = None;
  259. for url in &self.urls {
  260. // replace {{current_version}} and {{target}} in the provided URL
  261. // this is usefull if we need to query example
  262. // https://releases.myapp.com/update/{{target}}/{{current_version}}
  263. // will be transleted into ->
  264. // https://releases.myapp.com/update/darwin/1.0.0
  265. // The main objective is if the update URL is defined via the Cargo.toml
  266. // the URL will be generated dynamicly
  267. let fixed_link = str::replace(
  268. &str::replace(url, "{{current_version}}", current_version),
  269. "{{target}}",
  270. &target,
  271. );
  272. // we want JSON only
  273. let mut headers = HashMap::new();
  274. headers.insert("Accept".into(), "application/json".into());
  275. let resp = ClientBuilder::new()
  276. .build()?
  277. .send(
  278. HttpRequestBuilder::new("GET", &fixed_link)
  279. .headers(headers)
  280. // wait 20sec for the firewall
  281. .timeout(20),
  282. )
  283. .await;
  284. // If we got a success, we stop the loop
  285. // and we set our remote_release variable
  286. if let Ok(res) = resp {
  287. let res = res.read().await?;
  288. // got status code 2XX
  289. if StatusCode::from_u16(res.status).unwrap().is_success() {
  290. // if we got 204
  291. if StatusCode::NO_CONTENT.as_u16() == res.status {
  292. // return with `UpToDate` error
  293. // we should catch on the client
  294. return Err(Error::UpToDate);
  295. };
  296. // Convert the remote result to our local struct
  297. let built_release = RemoteRelease::from_release(&res.data, &target);
  298. // make sure all went well and the remote data is compatible
  299. // with what we need locally
  300. match built_release {
  301. Ok(release) => {
  302. last_error = None;
  303. remote_release = Some(release);
  304. break;
  305. }
  306. Err(err) => last_error = Some(err),
  307. }
  308. } // if status code is not 2XX we keep loopin' our urls
  309. }
  310. }
  311. // Last error is cleaned on success -- shouldn't be triggered if
  312. // we have a successful call
  313. if let Some(error) = last_error {
  314. return Err(Error::Network(error.to_string()));
  315. }
  316. // Extracted remote metadata
  317. let final_release = remote_release.ok_or_else(|| {
  318. Error::RemoteMetadata("Unable to extract update metadata from the remote server.".into())
  319. })?;
  320. // did the announced version is greated than our current one?
  321. let should_update =
  322. version::is_greater(current_version, &final_release.version).unwrap_or(false);
  323. // create our new updater
  324. Ok(Update {
  325. target,
  326. extract_path,
  327. should_update,
  328. version: final_release.version,
  329. date: final_release.date,
  330. current_version: self.current_version.to_owned(),
  331. download_url: final_release.download_url,
  332. body: final_release.body,
  333. signature: final_release.signature,
  334. #[cfg(target_os = "windows")]
  335. with_elevated_task: final_release.with_elevated_task,
  336. })
  337. }
  338. }
  339. pub fn builder<'a>() -> UpdateBuilder<'a> {
  340. UpdateBuilder::new()
  341. }
  342. #[derive(Debug, Clone)]
  343. pub struct Update {
  344. /// Update description
  345. pub body: Option<String>,
  346. /// Should we update or not
  347. pub should_update: bool,
  348. /// Version announced
  349. pub version: String,
  350. /// Running version
  351. pub current_version: String,
  352. /// Update publish date
  353. pub date: String,
  354. /// Target
  355. #[allow(dead_code)]
  356. target: String,
  357. /// Extract path
  358. extract_path: PathBuf,
  359. /// Download URL announced
  360. download_url: String,
  361. /// Signature announced
  362. signature: Option<String>,
  363. #[cfg(target_os = "windows")]
  364. /// Optional: Windows only try to use elevated task
  365. /// Default to false
  366. with_elevated_task: bool,
  367. }
  368. impl Update {
  369. // Download and install our update
  370. // @todo(lemarier): Split into download and install (two step) but need to be thread safe
  371. pub async fn download_and_install(&self, pub_key: Option<String>) -> Result {
  372. // download url for selected release
  373. let url = self.download_url.clone();
  374. // extract path
  375. let extract_path = self.extract_path.clone();
  376. // make sure we can install the update on linux
  377. // We fail here because later we can add more linux support
  378. // actually if we use APPIMAGE, our extract path should already
  379. // be set with our APPIMAGE env variable, we don't need to do
  380. // anythin with it yet
  381. #[cfg(target_os = "linux")]
  382. if env::var_os("APPIMAGE").is_none() {
  383. return Err(Error::UnsupportedPlatform);
  384. }
  385. // used for temp file name
  386. // if we cant extract app name, we use unix epoch duration
  387. let current_time = SystemTime::now()
  388. .duration_since(UNIX_EPOCH)
  389. .expect("Unable to get Unix Epoch")
  390. .subsec_nanos()
  391. .to_string();
  392. // get the current app name
  393. let bin_name = std::env::current_exe()
  394. .ok()
  395. .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
  396. .and_then(|s| s.into_string().ok())
  397. .unwrap_or_else(|| current_time.clone());
  398. // tmp dir for extraction
  399. let tmp_dir = tempfile::Builder::new()
  400. .prefix(&format!("{}_{}_download", bin_name, current_time))
  401. .tempdir()?;
  402. // tmp directories are used to create backup of current application
  403. // if something goes wrong, we can restore to previous state
  404. let tmp_archive_path = tmp_dir.path().join(detect_archive_in_url(&url));
  405. let mut tmp_archive = File::create(&tmp_archive_path)?;
  406. // set our headers
  407. let mut headers = HashMap::new();
  408. headers.insert("Accept".into(), "application/octet-stream".into());
  409. headers.insert("User-Agent".into(), "tauri/updater".into());
  410. // Create our request
  411. let resp = ClientBuilder::new()
  412. .build()?
  413. .send(
  414. HttpRequestBuilder::new("GET", &url)
  415. .headers(headers)
  416. // wait 20sec for the firewall
  417. .timeout(20),
  418. )
  419. .await?
  420. .bytes()
  421. .await?;
  422. // make sure it's success
  423. if !StatusCode::from_u16(resp.status).unwrap().is_success() {
  424. return Err(Error::Network(format!(
  425. "Download request failed with status: {}",
  426. resp.status
  427. )));
  428. }
  429. tmp_archive.write_all(&resp.data)?;
  430. // Validate signature ONLY if pubkey is available in tauri.conf.json
  431. if let Some(pub_key) = pub_key {
  432. // We need an announced signature by the server
  433. // if there is no signature, bail out.
  434. if let Some(signature) = self.signature.clone() {
  435. // we make sure the archive is valid and signed with the private key linked with the publickey
  436. verify_signature(&tmp_archive_path, signature, &pub_key)?;
  437. } else {
  438. // We have a public key inside our source file, but not announced by the server,
  439. // we assume this update is NOT valid.
  440. return Err(Error::PubkeyButNoSignature);
  441. }
  442. }
  443. // extract using tauri api inside a tmp path
  444. Extract::from_source(&tmp_archive_path).extract_into(tmp_dir.path())?;
  445. // Remove archive (not needed anymore)
  446. remove_file(&tmp_archive_path)?;
  447. // we copy the files depending of the operating system
  448. // we run the setup, appimage re-install or overwrite the
  449. // macos .app
  450. #[cfg(target_os = "windows")]
  451. copy_files_and_run(tmp_dir, extract_path, self.with_elevated_task)?;
  452. #[cfg(not(target_os = "windows"))]
  453. copy_files_and_run(tmp_dir, extract_path)?;
  454. // We are done!
  455. Ok(())
  456. }
  457. }
  458. // Linux (AppImage)
  459. // ### Expected structure:
  460. // ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
  461. // │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
  462. // └── ...
  463. // We should have an AppImage already installed to be able to copy and install
  464. // the extract_path is the current AppImage path
  465. // tmp_dir is where our new AppImage is found
  466. #[cfg(target_os = "linux")]
  467. fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result {
  468. // we delete our current AppImage (we'll create a new one later)
  469. remove_file(&extract_path)?;
  470. // In our tempdir we expect 1 directory (should be the <app>.app)
  471. let paths = read_dir(&tmp_dir)?;
  472. for path in paths {
  473. let found_path = path?.path();
  474. // make sure it's our .AppImage
  475. if found_path.extension() == Some(OsStr::new("AppImage")) {
  476. // Simply overwrite our AppImage (we use the command)
  477. // because it prevent failing of bytes stream
  478. Command::new("mv")
  479. .arg("-f")
  480. .arg(&found_path)
  481. .arg(&extract_path)
  482. .status()?;
  483. // early finish we have everything we need here
  484. return Ok(());
  485. }
  486. }
  487. Ok(())
  488. }
  489. // Windows
  490. // ### Expected structure:
  491. // ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
  492. // │ └──[AppName]_[version]_x64.msi # Application MSI
  493. // └── ...
  494. // ## MSI
  495. // Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
  496. // To replace current version of the application. In later version we'll offer
  497. // incremental update to push specific binaries.
  498. // ## EXE
  499. // Update server can provide a custom EXE (installer) who can run any task.
  500. #[cfg(target_os = "windows")]
  501. #[allow(clippy::unnecessary_wraps)]
  502. fn copy_files_and_run(
  503. tmp_dir: tempfile::TempDir,
  504. _extract_path: PathBuf,
  505. with_elevated_task: bool,
  506. ) -> Result {
  507. use crate::api::file::Move;
  508. let paths = read_dir(&tmp_dir)?;
  509. // This consumes the TempDir without deleting directory on the filesystem,
  510. // meaning that the directory will no longer be automatically deleted.
  511. let tmp_path = tmp_dir.into_path();
  512. for path in paths {
  513. let found_path = path?.path();
  514. // we support 2 type of files exe & msi for now
  515. // If it's an `exe` we expect an installer not a runtime.
  516. if found_path.extension() == Some(OsStr::new("exe")) {
  517. // Run the EXE
  518. Command::new(found_path)
  519. .spawn()
  520. .expect("installer failed to start");
  521. exit(0);
  522. } else if found_path.extension() == Some(OsStr::new("msi")) {
  523. if with_elevated_task {
  524. if let Some(bin_name) = std::env::current_exe()
  525. .ok()
  526. .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
  527. .and_then(|s| s.into_string().ok())
  528. {
  529. let product_name = bin_name.replace(".exe", "");
  530. // Check if there is a task that enables the updater to skip the UAC prompt
  531. let update_task_name = format!("Update {} - Skip UAC", product_name);
  532. if let Ok(status) = Command::new("schtasks")
  533. .arg("/QUERY")
  534. .arg("/TN")
  535. .arg(update_task_name.clone())
  536. .status()
  537. {
  538. if status.success() {
  539. // Rename the MSI to the match file name the Skip UAC task is expecting it to be
  540. let temp_msi = tmp_path.with_file_name(bin_name).with_extension("msi");
  541. Move::from_source(&found_path)
  542. .to_dest(&temp_msi)
  543. .expect("Unable to move update MSI");
  544. let exit_status = Command::new("schtasks")
  545. .arg("/RUN")
  546. .arg("/TN")
  547. .arg(update_task_name)
  548. .status()
  549. .expect("failed to start updater task");
  550. if exit_status.success() {
  551. // Successfully launched task that skips the UAC prompt
  552. exit(0);
  553. }
  554. }
  555. // Failed to run update task. Following UAC Path
  556. }
  557. }
  558. }
  559. // restart should be handled by WIX as we exit the process
  560. Command::new("msiexec.exe")
  561. .arg("/i")
  562. .arg(found_path)
  563. // quiet basic UI with prompt at the end
  564. .arg("/qb+")
  565. .spawn()
  566. .expect("installer failed to start");
  567. exit(0);
  568. }
  569. }
  570. Ok(())
  571. }
  572. // Get the current app name in the path
  573. // Example; `/Applications/updater-example.app/Contents/MacOS/updater-example`
  574. // Should return; `updater-example.app`
  575. #[cfg(target_os = "macos")]
  576. fn macos_app_name_in_path(extract_path: &Path) -> String {
  577. let components = extract_path.components();
  578. let app_name = components.last().unwrap();
  579. let app_name = app_name.as_os_str().to_str().unwrap();
  580. app_name.to_string()
  581. }
  582. // MacOS
  583. // ### Expected structure:
  584. // ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
  585. // │ └──[AppName].app # Main application
  586. // │ └── Contents # Application contents...
  587. // │ └── ...
  588. // └── ...
  589. #[cfg(target_os = "macos")]
  590. fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result {
  591. // In our tempdir we expect 1 directory (should be the <app>.app)
  592. let paths = read_dir(&tmp_dir)?;
  593. // current app name in /Applications/<app>.app
  594. let app_name = macos_app_name_in_path(&extract_path);
  595. for path in paths {
  596. let mut found_path = path?.path();
  597. // make sure it's our .app
  598. if found_path.extension() == Some(OsStr::new("app")) {
  599. let found_app_name = macos_app_name_in_path(&found_path);
  600. // make sure the app name in the archive matche the installed app name on path
  601. if found_app_name != app_name {
  602. // we need to replace the app name in the updater archive to match
  603. // installed app name
  604. let new_path = found_path.parent().unwrap().join(app_name);
  605. rename(&found_path, &new_path)?;
  606. found_path = new_path;
  607. }
  608. let sandbox_app_path = tempfile::Builder::new()
  609. .prefix("tauri_current_app_sandbox")
  610. .tempdir()?;
  611. // Replace the whole application to make sure the
  612. // code signature is following
  613. Move::from_source(&found_path)
  614. .replace_using_temp(sandbox_app_path.path())
  615. .to_dest(&extract_path)?;
  616. // early finish we have everything we need here
  617. return Ok(());
  618. }
  619. }
  620. Ok(())
  621. }
  622. /// Returns a target os
  623. /// We do not use a helper function like the target_triple
  624. /// from tauri-utils because this function return `None` if
  625. /// the updater do not support the platform.
  626. ///
  627. /// Available target: `linux, darwin, win32, win64`
  628. pub fn get_updater_target() -> Option<String> {
  629. if cfg!(target_os = "linux") {
  630. Some("linux".into())
  631. } else if cfg!(target_os = "macos") {
  632. Some("darwin".into())
  633. } else if cfg!(target_os = "windows") {
  634. if cfg!(target_pointer_width = "32") {
  635. Some("win32".into())
  636. } else {
  637. Some("win64".into())
  638. }
  639. } else {
  640. None
  641. }
  642. }
  643. /// Get the extract_path from the provided executable_path
  644. pub fn extract_path_from_executable(executable_path: &Path) -> PathBuf {
  645. // Return the path of the current executable by default
  646. // Example C:\Program Files\My App\
  647. let extract_path = executable_path
  648. .parent()
  649. .map(PathBuf::from)
  650. .expect("Can't determine extract path");
  651. // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
  652. // We need to get /Applications/<app>.app
  653. // todo(lemarier): Need a better way here
  654. // Maybe we could search for <*.app> to get the right path
  655. #[cfg(target_os = "macos")]
  656. if extract_path
  657. .display()
  658. .to_string()
  659. .contains("Contents/MacOS")
  660. {
  661. return extract_path
  662. .parent()
  663. .map(PathBuf::from)
  664. .expect("Unable to find the extract path")
  665. .parent()
  666. .map(PathBuf::from)
  667. .expect("Unable to find the extract path");
  668. }
  669. // We should use APPIMAGE exposed env variable
  670. // This is where our APPIMAGE should sit and should be replaced
  671. #[cfg(target_os = "linux")]
  672. if let Some(app_image_path) = env::var_os("APPIMAGE") {
  673. return PathBuf::from(app_image_path);
  674. }
  675. extract_path
  676. }
  677. // Return the archive type to save on disk
  678. fn detect_archive_in_url(path: &str) -> String {
  679. path
  680. .split('/')
  681. .next_back()
  682. .unwrap_or(&default_archive_name_by_os())
  683. .to_string()
  684. }
  685. // Fallback archive name by os
  686. // The main objective is to provide the right extension based on the target
  687. // if we cant extract the archive type in the url we'll fallback to this value
  688. fn default_archive_name_by_os() -> String {
  689. #[cfg(target_os = "windows")]
  690. {
  691. "update.zip".into()
  692. }
  693. #[cfg(not(target_os = "windows"))]
  694. {
  695. "update.tar.gz".into()
  696. }
  697. }
  698. // Convert base64 to string and prevent failing
  699. fn base64_to_string(base64_string: &str) -> Result<String> {
  700. let decoded_string = &decode(base64_string)?;
  701. let result = from_utf8(decoded_string)?.to_string();
  702. Ok(result)
  703. }
  704. // Validate signature
  705. // need to be public because its been used
  706. // by our tests in the bundler
  707. pub fn verify_signature(
  708. archive_path: &Path,
  709. release_signature: String,
  710. pub_key: &str,
  711. ) -> Result<bool> {
  712. // we need to convert the pub key
  713. let pub_key_decoded = &base64_to_string(pub_key)?;
  714. let public_key = PublicKey::decode(pub_key_decoded)?;
  715. let signature_base64_decoded = base64_to_string(&release_signature)?;
  716. let signature = Signature::decode(&signature_base64_decoded)?;
  717. // We need to open the file and extract the datas to make sure its not corrupted
  718. let file_open = OpenOptions::new().read(true).open(&archive_path)?;
  719. let mut file_buff: BufReader<File> = BufReader::new(file_open);
  720. // read all bytes since EOF in the buffer
  721. let mut data = vec![];
  722. file_buff.read_to_end(&mut data)?;
  723. // Validate signature or bail out
  724. public_key.verify(&data, &signature, false)?;
  725. Ok(true)
  726. }
  727. #[cfg(test)]
  728. mod test {
  729. use super::*;
  730. #[cfg(target_os = "macos")]
  731. use std::env::current_exe;
  732. #[cfg(target_os = "macos")]
  733. use std::fs::File;
  734. #[cfg(target_os = "macos")]
  735. use std::path::Path;
  736. macro_rules! block {
  737. ($e:expr) => {
  738. tokio_test::block_on($e)
  739. };
  740. }
  741. fn generate_sample_raw_json() -> String {
  742. r#"{
  743. "version": "v2.0.0",
  744. "notes": "Test version !",
  745. "pub_date": "2020-06-22T19:25:57Z",
  746. "platforms": {
  747. "darwin": {
  748. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
  749. "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.app.tar.gz"
  750. },
  751. "linux": {
  752. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K",
  753. "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.AppImage.tar.gz"
  754. },
  755. "win64": {
  756. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K",
  757. "url": "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
  758. }
  759. }
  760. }"#.into()
  761. }
  762. fn generate_sample_platform_json(
  763. version: &str,
  764. public_signature: &str,
  765. download_url: &str,
  766. ) -> String {
  767. format!(
  768. r#"
  769. {{
  770. "name": "v{}",
  771. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  772. "pub_date": "2020-06-25T14:14:19Z",
  773. "signature": "{}",
  774. "url": "{}"
  775. }}
  776. "#,
  777. version, public_signature, download_url
  778. )
  779. }
  780. fn generate_sample_with_elevated_task_platform_json(
  781. version: &str,
  782. public_signature: &str,
  783. download_url: &str,
  784. with_elevated_task: bool,
  785. ) -> String {
  786. format!(
  787. r#"
  788. {{
  789. "name": "v{}",
  790. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  791. "pub_date": "2020-06-25T14:14:19Z",
  792. "signature": "{}",
  793. "url": "{}",
  794. "with_elevated_task": "{}"
  795. }}
  796. "#,
  797. version, public_signature, download_url, with_elevated_task
  798. )
  799. }
  800. fn generate_sample_bad_json() -> String {
  801. r#"{
  802. "version": "v0.0.3",
  803. "notes": "Blablaa",
  804. "date": "2020-02-20T15:41:00Z",
  805. "download_link": "https://github.com/lemarier/tauri-test/releases/download/v0.0.1/update3.tar.gz"
  806. }"#.into()
  807. }
  808. #[cfg(target_os = "macos")]
  809. #[test]
  810. fn test_app_name_in_path() {
  811. let executable = extract_path_from_executable(Path::new(
  812. "/Applications/updater-example.app/Contents/MacOS/updater-example",
  813. ));
  814. let app_name = macos_app_name_in_path(&executable);
  815. assert!(executable.ends_with("updater-example.app"));
  816. assert_eq!(app_name, "updater-example.app".to_string());
  817. }
  818. #[test]
  819. fn simple_http_updater() {
  820. let _m = mockito::mock("GET", "/")
  821. .with_status(200)
  822. .with_header("content-type", "application/json")
  823. .with_body(generate_sample_raw_json())
  824. .create();
  825. let check_update = block!(builder()
  826. .current_version("0.0.0")
  827. .url(mockito::server_url())
  828. .build());
  829. assert!(check_update.is_ok());
  830. let updater = check_update.expect("Can't check update");
  831. assert!(updater.should_update);
  832. }
  833. #[test]
  834. fn simple_http_updater_raw_json() {
  835. let _m = mockito::mock("GET", "/")
  836. .with_status(200)
  837. .with_header("content-type", "application/json")
  838. .with_body(generate_sample_raw_json())
  839. .create();
  840. let check_update = block!(builder()
  841. .current_version("0.0.0")
  842. .url(mockito::server_url())
  843. .build());
  844. assert!(check_update.is_ok());
  845. let updater = check_update.expect("Can't check update");
  846. assert!(updater.should_update);
  847. }
  848. #[test]
  849. fn simple_http_updater_raw_json_win64() {
  850. let _m = mockito::mock("GET", "/")
  851. .with_status(200)
  852. .with_header("content-type", "application/json")
  853. .with_body(generate_sample_raw_json())
  854. .create();
  855. let check_update = block!(builder()
  856. .current_version("0.0.0")
  857. .target("win64")
  858. .url(mockito::server_url())
  859. .build());
  860. assert!(check_update.is_ok());
  861. let updater = check_update.expect("Can't check update");
  862. assert!(updater.should_update);
  863. assert_eq!(updater.version, "2.0.0");
  864. assert_eq!(updater.signature, Some("dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K".into()));
  865. assert_eq!(
  866. updater.download_url,
  867. "https://github.com/lemarier/tauri-test/releases/download/v1.0.0/app.x64.msi.zip"
  868. );
  869. }
  870. #[test]
  871. fn simple_http_updater_raw_json_uptodate() {
  872. let _m = mockito::mock("GET", "/")
  873. .with_status(200)
  874. .with_header("content-type", "application/json")
  875. .with_body(generate_sample_raw_json())
  876. .create();
  877. let check_update = block!(builder()
  878. .current_version("10.0.0")
  879. .url(mockito::server_url())
  880. .build());
  881. assert!(check_update.is_ok());
  882. let updater = check_update.expect("Can't check update");
  883. assert!(!updater.should_update);
  884. }
  885. #[test]
  886. fn simple_http_updater_without_version() {
  887. let _m = mockito::mock("GET", "/darwin/1.0.0")
  888. .with_status(200)
  889. .with_header("content-type", "application/json")
  890. .with_body(generate_sample_platform_json(
  891. "2.0.0",
  892. "SampleTauriKey",
  893. "https://tauri.studio",
  894. ))
  895. .create();
  896. let check_update = block!(builder()
  897. .current_version("1.0.0")
  898. .url(format!(
  899. "{}/darwin/{{{{current_version}}}}",
  900. mockito::server_url()
  901. ))
  902. .build());
  903. assert!(check_update.is_ok());
  904. let updater = check_update.expect("Can't check update");
  905. assert!(updater.should_update);
  906. }
  907. #[test]
  908. fn simple_http_updater_with_elevated_task() {
  909. let _m = mockito::mock("GET", "/win64/1.0.0")
  910. .with_status(200)
  911. .with_header("content-type", "application/json")
  912. .with_body(generate_sample_with_elevated_task_platform_json(
  913. "2.0.0",
  914. "SampleTauriKey",
  915. "https://tauri.studio",
  916. true,
  917. ))
  918. .create();
  919. let check_update = block!(builder()
  920. .current_version("1.0.0")
  921. .url(format!(
  922. "{}/win64/{{{{current_version}}}}",
  923. mockito::server_url()
  924. ))
  925. .build());
  926. assert!(check_update.is_ok());
  927. let updater = check_update.expect("Can't check update");
  928. assert!(updater.should_update);
  929. }
  930. #[test]
  931. fn http_updater_uptodate() {
  932. let _m = mockito::mock("GET", "/darwin/10.0.0")
  933. .with_status(200)
  934. .with_header("content-type", "application/json")
  935. .with_body(generate_sample_platform_json(
  936. "2.0.0",
  937. "SampleTauriKey",
  938. "https://tauri.studio",
  939. ))
  940. .create();
  941. let check_update = block!(builder()
  942. .current_version("10.0.0")
  943. .url(format!(
  944. "{}/darwin/{{{{current_version}}}}",
  945. mockito::server_url()
  946. ))
  947. .build());
  948. assert!(check_update.is_ok());
  949. let updater = check_update.expect("Can't check update");
  950. assert!(!updater.should_update);
  951. }
  952. #[test]
  953. fn http_updater_fallback_urls() {
  954. let _m = mockito::mock("GET", "/")
  955. .with_status(200)
  956. .with_header("content-type", "application/json")
  957. .with_body(generate_sample_raw_json())
  958. .create();
  959. let check_update = block!(builder()
  960. .url("http://badurl.www.tld/1".into())
  961. .url(mockito::server_url())
  962. .current_version("0.0.1")
  963. .build());
  964. assert!(check_update.is_ok());
  965. let updater = check_update.expect("Can't check remote update");
  966. assert!(updater.should_update);
  967. }
  968. #[test]
  969. fn http_updater_fallback_urls_withs_array() {
  970. let _m = mockito::mock("GET", "/")
  971. .with_status(200)
  972. .with_header("content-type", "application/json")
  973. .with_body(generate_sample_raw_json())
  974. .create();
  975. let check_update = block!(builder()
  976. .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),])
  977. .current_version("0.0.1")
  978. .build());
  979. assert!(check_update.is_ok());
  980. let updater = check_update.expect("Can't check remote update");
  981. assert!(updater.should_update);
  982. }
  983. #[test]
  984. fn http_updater_missing_remote_data() {
  985. let _m = mockito::mock("GET", "/")
  986. .with_status(200)
  987. .with_header("content-type", "application/json")
  988. .with_body(generate_sample_bad_json())
  989. .create();
  990. let check_update = block!(builder()
  991. .url(mockito::server_url())
  992. .current_version("0.0.1")
  993. .build());
  994. assert!(check_update.is_err());
  995. }
  996. // run complete process on mac only for now as we don't have
  997. // server (api) that we can use to test
  998. #[cfg(target_os = "macos")]
  999. #[test]
  1000. fn http_updater_complete_process() {
  1001. let good_archive_url = format!("{}/archive.tar.gz", mockito::server_url());
  1002. let mut signature_file = File::open("./test/updater/fixture/archives/archive.tar.gz.sig")
  1003. .expect("Unable to open signature");
  1004. let mut signature = String::new();
  1005. signature_file
  1006. .read_to_string(&mut signature)
  1007. .expect("Unable to read signature as string");
  1008. let mut pubkey_file = File::open("./test/updater/fixture/good_signature/update.key.pub")
  1009. .expect("Unable to open pubkey");
  1010. let mut pubkey = String::new();
  1011. pubkey_file
  1012. .read_to_string(&mut pubkey)
  1013. .expect("Unable to read signature as string");
  1014. // add sample file
  1015. let _m = mockito::mock("GET", "/archive.tar.gz")
  1016. .with_status(200)
  1017. .with_header("content-type", "application/octet-stream")
  1018. .with_body_from_file("./test/updater/fixture/archives/archive.tar.gz")
  1019. .create();
  1020. // sample mock for update file
  1021. let _m = mockito::mock("GET", "/")
  1022. .with_status(200)
  1023. .with_header("content-type", "application/json")
  1024. .with_body(generate_sample_platform_json(
  1025. "2.0.1",
  1026. signature.as_ref(),
  1027. good_archive_url.as_ref(),
  1028. ))
  1029. .create();
  1030. // Build a tmpdir so we can test our extraction inside
  1031. // We dont want to overwrite our current executable or the directory
  1032. // Otherwise tests are failing...
  1033. let executable_path = current_exe().expect("Can't extract executable path");
  1034. let parent_path = executable_path
  1035. .parent()
  1036. .expect("Can't find the parent path");
  1037. let tmp_dir = tempfile::Builder::new()
  1038. .prefix("tauri_updater_test")
  1039. .tempdir_in(parent_path);
  1040. assert!(tmp_dir.is_ok());
  1041. let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir");
  1042. let tmp_dir_path = tmp_dir_unwrap.path();
  1043. // configure the updater
  1044. let check_update = block!(builder()
  1045. .url(mockito::server_url())
  1046. // It should represent the executable path, that's why we add my_app.exe in our
  1047. // test path -- in production you shouldn't have to provide it
  1048. .executable_path(&tmp_dir_path.join("my_app.exe"))
  1049. // make sure we force an update
  1050. .current_version("1.0.0")
  1051. .build());
  1052. // make sure the process worked
  1053. assert!(check_update.is_ok());
  1054. // unwrap our results
  1055. let updater = check_update.expect("Can't check remote update");
  1056. // make sure we need to update
  1057. assert!(updater.should_update);
  1058. // make sure we can read announced version
  1059. assert_eq!(updater.version, "2.0.1");
  1060. // download, install and validate signature
  1061. let install_process = block!(updater.download_and_install(Some(pubkey)));
  1062. assert!(install_process.is_ok());
  1063. // make sure the extraction went well (it should have skipped the main app.app folder)
  1064. // as we can't extract in /Applications directly
  1065. let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app");
  1066. let bin_file_exist = Path::new(&bin_file).exists();
  1067. assert!(bin_file_exist);
  1068. }
  1069. }