core.rs 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650
  1. // Copyright 2019-2023 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. #[cfg(desktop)]
  6. use crate::api::file::{ArchiveFormat, Extract, Move};
  7. use crate::{
  8. api::http::{ClientBuilder, HttpRequestBuilder},
  9. AppHandle, Manager, Runtime,
  10. };
  11. use base64::Engine;
  12. use http::{
  13. header::{HeaderName, HeaderValue},
  14. HeaderMap, StatusCode,
  15. };
  16. use minisign_verify::{PublicKey, Signature};
  17. use semver::Version;
  18. use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
  19. use tauri_utils::{platform::current_exe, Env};
  20. use time::OffsetDateTime;
  21. use url::Url;
  22. #[cfg(desktop)]
  23. use std::io::Seek;
  24. use std::{
  25. collections::HashMap,
  26. env,
  27. fmt::{self},
  28. io::{Cursor, Read},
  29. path::{Path, PathBuf},
  30. str::{from_utf8, FromStr},
  31. time::Duration,
  32. };
  33. #[cfg(any(target_os = "linux", windows))]
  34. use std::ffi::OsStr;
  35. #[cfg(all(desktop, not(target_os = "windows")))]
  36. use crate::api::file::Compression;
  37. #[cfg(target_os = "windows")]
  38. use std::{
  39. fs::read_dir,
  40. process::{exit, Command},
  41. };
  42. type ShouldInstall = dyn FnOnce(&Version, &RemoteRelease) -> bool + Send;
  43. #[derive(Debug, Deserialize, Serialize)]
  44. #[serde(untagged)]
  45. pub enum RemoteReleaseInner {
  46. Dynamic(ReleaseManifestPlatform),
  47. Static {
  48. platforms: HashMap<String, ReleaseManifestPlatform>,
  49. },
  50. }
  51. /// Information about a release returned by the remote update server.
  52. ///
  53. /// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format.
  54. #[derive(Debug)]
  55. pub struct RemoteRelease {
  56. /// Version to install.
  57. version: Version,
  58. /// Release notes.
  59. notes: Option<String>,
  60. /// Release date.
  61. pub_date: Option<OffsetDateTime>,
  62. /// Release data.
  63. data: RemoteReleaseInner,
  64. }
  65. impl<'de> Deserialize<'de> for RemoteRelease {
  66. fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
  67. where
  68. D: Deserializer<'de>,
  69. {
  70. #[derive(Deserialize)]
  71. struct InnerRemoteRelease {
  72. #[serde(alias = "name", deserialize_with = "parse_version")]
  73. version: Version,
  74. notes: Option<String>,
  75. pub_date: Option<String>,
  76. platforms: Option<HashMap<String, ReleaseManifestPlatform>>,
  77. // dynamic platform response
  78. url: Option<Url>,
  79. signature: Option<String>,
  80. #[cfg(target_os = "windows")]
  81. #[serde(default)]
  82. with_elevated_task: bool,
  83. }
  84. let release = InnerRemoteRelease::deserialize(deserializer)?;
  85. let pub_date = if let Some(date) = release.pub_date {
  86. Some(
  87. OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339)
  88. .map_err(|e| DeError::custom(format!("invalid value for `pub_date`: {e}")))?,
  89. )
  90. } else {
  91. None
  92. };
  93. Ok(RemoteRelease {
  94. version: release.version,
  95. notes: release.notes,
  96. pub_date,
  97. data: if let Some(platforms) = release.platforms {
  98. RemoteReleaseInner::Static { platforms }
  99. } else {
  100. RemoteReleaseInner::Dynamic(ReleaseManifestPlatform {
  101. url: release.url.ok_or_else(|| {
  102. DeError::custom("the `url` field was not set on the updater response")
  103. })?,
  104. signature: release.signature.ok_or_else(|| {
  105. DeError::custom("the `signature` field was not set on the updater response")
  106. })?,
  107. #[cfg(target_os = "windows")]
  108. with_elevated_task: release.with_elevated_task,
  109. })
  110. },
  111. })
  112. }
  113. }
  114. #[derive(Debug, Deserialize, Serialize)]
  115. pub struct ReleaseManifestPlatform {
  116. /// Download URL for the platform
  117. pub url: Url,
  118. /// Signature for the platform
  119. pub signature: String,
  120. #[cfg(target_os = "windows")]
  121. #[serde(default)]
  122. /// Optional: Windows only try to use elevated task
  123. pub with_elevated_task: bool,
  124. }
  125. fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
  126. where
  127. D: serde::Deserializer<'de>,
  128. {
  129. let str = String::deserialize(deserializer)?;
  130. Version::from_str(str.trim_start_matches('v')).map_err(serde::de::Error::custom)
  131. }
  132. impl RemoteRelease {
  133. /// The release version.
  134. pub fn version(&self) -> &Version {
  135. &self.version
  136. }
  137. /// The release notes.
  138. pub fn notes(&self) -> Option<&String> {
  139. self.notes.as_ref()
  140. }
  141. /// The release date.
  142. pub fn pub_date(&self) -> Option<&OffsetDateTime> {
  143. self.pub_date.as_ref()
  144. }
  145. /// The release's download URL for the given target.
  146. pub fn download_url(&self, target: &str) -> Result<&Url> {
  147. match self.data {
  148. RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.url),
  149. RemoteReleaseInner::Static { ref platforms } => platforms
  150. .get(target)
  151. .map_or(Err(Error::TargetNotFound(target.to_string())), |p| {
  152. Ok(&p.url)
  153. }),
  154. }
  155. }
  156. /// The release's signature for the given target.
  157. pub fn signature(&self, target: &str) -> Result<&String> {
  158. match self.data {
  159. RemoteReleaseInner::Dynamic(ref platform) => Ok(&platform.signature),
  160. RemoteReleaseInner::Static { ref platforms } => platforms
  161. .get(target)
  162. .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| {
  163. Ok(&platform.signature)
  164. }),
  165. }
  166. }
  167. #[cfg(target_os = "windows")]
  168. /// Optional: Windows only try to use elevated task
  169. pub fn with_elevated_task(&self, target: &str) -> Result<bool> {
  170. match self.data {
  171. RemoteReleaseInner::Dynamic(ref platform) => Ok(platform.with_elevated_task),
  172. RemoteReleaseInner::Static { ref platforms } => platforms
  173. .get(target)
  174. .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| {
  175. Ok(platform.with_elevated_task)
  176. }),
  177. }
  178. }
  179. }
  180. pub struct UpdateBuilder<R: Runtime> {
  181. /// Application handle.
  182. pub app: AppHandle<R>,
  183. /// Current version we are running to compare with announced version
  184. pub current_version: Version,
  185. /// The URLs to checks updates. We suggest at least one fallback on a different domain.
  186. pub urls: Vec<String>,
  187. /// The platform the updater will check and install the update. Default is from `get_updater_target`
  188. pub target: Option<String>,
  189. /// The current executable path. Default is automatically extracted.
  190. pub executable_path: Option<PathBuf>,
  191. should_install: Option<Box<ShouldInstall>>,
  192. timeout: Option<Duration>,
  193. headers: HeaderMap,
  194. }
  195. impl<R: Runtime> fmt::Debug for UpdateBuilder<R> {
  196. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  197. f.debug_struct("UpdateBuilder")
  198. .field("app", &self.app)
  199. .field("current_version", &self.current_version)
  200. .field("urls", &self.urls)
  201. .field("target", &self.target)
  202. .field("executable_path", &self.executable_path)
  203. .field("timeout", &self.timeout)
  204. .field("headers", &self.headers)
  205. .finish()
  206. }
  207. }
  208. // Create new updater instance and return an Update
  209. impl<R: Runtime> UpdateBuilder<R> {
  210. pub fn new(app: AppHandle<R>) -> Self {
  211. UpdateBuilder {
  212. app,
  213. urls: Vec::new(),
  214. target: None,
  215. executable_path: None,
  216. // safe to unwrap: CARGO_PKG_VERSION is also a valid semver value
  217. current_version: env!("CARGO_PKG_VERSION").parse().unwrap(),
  218. should_install: None,
  219. timeout: None,
  220. headers: Default::default(),
  221. }
  222. }
  223. #[allow(dead_code)]
  224. pub fn url(mut self, url: String) -> Self {
  225. self.urls.push(
  226. percent_encoding::percent_decode(url.as_bytes())
  227. .decode_utf8_lossy()
  228. .to_string(),
  229. );
  230. self
  231. }
  232. /// Add multiple URLS at once inside a Vec for future reference
  233. pub fn urls(mut self, urls: &[String]) -> Self {
  234. let mut formatted_vec: Vec<String> = Vec::new();
  235. for url in urls {
  236. formatted_vec.push(
  237. percent_encoding::percent_decode(url.as_bytes())
  238. .decode_utf8_lossy()
  239. .to_string(),
  240. );
  241. }
  242. self.urls = formatted_vec;
  243. self
  244. }
  245. /// Set the current app version, used to compare against the latest available version.
  246. /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
  247. pub fn current_version(mut self, ver: Version) -> Self {
  248. self.current_version = ver;
  249. self
  250. }
  251. /// Set the target name. Represents the string that is looked up on the updater API or response JSON.
  252. pub fn target(mut self, target: impl Into<String>) -> Self {
  253. self.target.replace(target.into());
  254. self
  255. }
  256. /// Set the executable path
  257. #[allow(dead_code)]
  258. pub fn executable_path<A: AsRef<Path>>(mut self, executable_path: A) -> Self {
  259. self.executable_path = Some(PathBuf::from(executable_path.as_ref()));
  260. self
  261. }
  262. pub fn should_install<F: FnOnce(&Version, &RemoteRelease) -> bool + Send + 'static>(
  263. mut self,
  264. f: F,
  265. ) -> Self {
  266. self.should_install.replace(Box::new(f));
  267. self
  268. }
  269. pub fn timeout(mut self, timeout: Duration) -> Self {
  270. self.timeout.replace(timeout);
  271. self
  272. }
  273. /// Add a `Header` to the request.
  274. pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
  275. where
  276. HeaderName: TryFrom<K>,
  277. <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
  278. HeaderValue: TryFrom<V>,
  279. <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
  280. {
  281. let key: std::result::Result<HeaderName, http::Error> = key.try_into().map_err(Into::into);
  282. let value: std::result::Result<HeaderValue, http::Error> = value.try_into().map_err(Into::into);
  283. self.headers.insert(key?, value?);
  284. Ok(self)
  285. }
  286. pub async fn build(mut self) -> Result<Update<R>> {
  287. let mut remote_release: Option<RemoteRelease> = None;
  288. // make sure we have at least one url
  289. if self.urls.is_empty() {
  290. return Err(Error::Builder(
  291. "Unable to check update, `url` is required.".into(),
  292. ));
  293. };
  294. // If no executable path provided, we use current_exe from tauri_utils
  295. let executable_path = self.executable_path.unwrap_or(current_exe()?);
  296. let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?;
  297. // `target` is the `{{target}}` variable we replace in the endpoint
  298. // `json_target` is the value we search if the updater server returns a JSON with the `platforms` object
  299. let (target, json_target) = if let Some(target) = self.target {
  300. (target.clone(), target)
  301. } else {
  302. let target = get_updater_target().ok_or(Error::UnsupportedOs)?;
  303. (target.to_string(), format!("{target}-{arch}"))
  304. };
  305. // Get the extract_path from the provided executable_path
  306. let extract_path = extract_path_from_executable(&self.app.state::<Env>(), &executable_path);
  307. // Set SSL certs for linux if they aren't available.
  308. // We do not require to recheck in the download_and_install as we use
  309. // ENV variables, we can expect them to be set for the second call.
  310. #[cfg(target_os = "linux")]
  311. {
  312. if env::var_os("SSL_CERT_FILE").is_none() {
  313. env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt");
  314. }
  315. if env::var_os("SSL_CERT_DIR").is_none() {
  316. env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
  317. }
  318. }
  319. // we want JSON only
  320. let mut headers = self.headers;
  321. headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
  322. // Allow fallback if more than 1 urls is provided
  323. let mut last_error: Option<Error> = None;
  324. for url in &self.urls {
  325. // replace {{current_version}}, {{target}} and {{arch}} in the provided URL
  326. // this is useful if we need to query example
  327. // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}}
  328. // will be translated into ->
  329. // https://releases.myapp.com/update/darwin/aarch64/1.0.0
  330. // The main objective is if the update URL is defined via the Cargo.toml
  331. // the URL will be generated dynamically
  332. let fixed_link = url
  333. .replace("{{current_version}}", &self.current_version.to_string())
  334. .replace("{{target}}", &target)
  335. .replace("{{arch}}", arch);
  336. let mut request = HttpRequestBuilder::new("GET", &fixed_link)?.headers(headers.clone());
  337. if let Some(timeout) = self.timeout {
  338. request = request.timeout(timeout);
  339. }
  340. let resp = ClientBuilder::new().build()?.send(request).await;
  341. // If we got a success, we stop the loop
  342. // and we set our remote_release variable
  343. if let Ok(res) = resp {
  344. let status = res.status();
  345. // got status code 2XX
  346. if status.is_success() {
  347. // if we got 204
  348. if status == StatusCode::NO_CONTENT {
  349. // return with `UpToDate` error
  350. // we should catch on the client
  351. return Err(Error::UpToDate);
  352. };
  353. let res = res.read().await?;
  354. // Convert the remote result to our local struct
  355. let built_release = serde_json::from_value(res.data).map_err(Into::into);
  356. // make sure all went well and the remote data is compatible
  357. // with what we need locally
  358. match built_release {
  359. Ok(release) => {
  360. last_error = None;
  361. remote_release = Some(release);
  362. break;
  363. }
  364. Err(err) => last_error = Some(err),
  365. }
  366. } // if status code is not 2XX we keep loopin' our urls
  367. }
  368. }
  369. // Last error is cleaned on success -- shouldn't be triggered if
  370. // we have a successful call
  371. if let Some(error) = last_error {
  372. return Err(error);
  373. }
  374. // Extracted remote metadata
  375. let final_release = remote_release.ok_or(Error::ReleaseNotFound)?;
  376. // is the announced version greater than our current one?
  377. let should_update = if let Some(comparator) = self.should_install.take() {
  378. comparator(&self.current_version, &final_release)
  379. } else {
  380. final_release.version() > &self.current_version
  381. };
  382. headers.remove("Accept");
  383. // create our new updater
  384. Ok(Update {
  385. app: self.app,
  386. target,
  387. extract_path,
  388. should_update,
  389. version: final_release.version().to_string(),
  390. date: final_release.pub_date().cloned(),
  391. current_version: self.current_version,
  392. download_url: final_release.download_url(&json_target)?.to_owned(),
  393. body: final_release.notes().cloned(),
  394. signature: final_release.signature(&json_target)?.to_owned(),
  395. #[cfg(target_os = "windows")]
  396. with_elevated_task: final_release.with_elevated_task(&json_target)?,
  397. timeout: self.timeout,
  398. headers,
  399. })
  400. }
  401. }
  402. pub fn builder<R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<R> {
  403. UpdateBuilder::new(app)
  404. }
  405. #[derive(Debug)]
  406. pub struct Update<R: Runtime> {
  407. /// Application handle.
  408. pub app: AppHandle<R>,
  409. /// Update description
  410. pub body: Option<String>,
  411. /// Should we update or not
  412. pub should_update: bool,
  413. /// Version announced
  414. pub version: String,
  415. /// Running version
  416. pub current_version: Version,
  417. /// Update publish date
  418. pub date: Option<OffsetDateTime>,
  419. /// Target
  420. #[allow(dead_code)]
  421. target: String,
  422. /// Extract path
  423. extract_path: PathBuf,
  424. /// Download URL announced
  425. download_url: Url,
  426. /// Signature announced
  427. signature: String,
  428. #[cfg(target_os = "windows")]
  429. /// Optional: Windows only try to use elevated task
  430. /// Default to false
  431. with_elevated_task: bool,
  432. /// Request timeout
  433. timeout: Option<Duration>,
  434. /// Request headers
  435. headers: HeaderMap,
  436. }
  437. impl<R: Runtime> Clone for Update<R> {
  438. fn clone(&self) -> Self {
  439. Self {
  440. app: self.app.clone(),
  441. body: self.body.clone(),
  442. should_update: self.should_update,
  443. version: self.version.clone(),
  444. current_version: self.current_version.clone(),
  445. date: self.date,
  446. target: self.target.clone(),
  447. extract_path: self.extract_path.clone(),
  448. download_url: self.download_url.clone(),
  449. signature: self.signature.clone(),
  450. #[cfg(target_os = "windows")]
  451. with_elevated_task: self.with_elevated_task,
  452. timeout: self.timeout,
  453. headers: self.headers.clone(),
  454. }
  455. }
  456. }
  457. impl<R: Runtime> Update<R> {
  458. // Download and install our update
  459. // @todo(lemarier): Split into download and install (two step) but need to be thread safe
  460. pub(crate) async fn download_and_install<C: Fn(usize, Option<u64>), D: FnOnce()>(
  461. &self,
  462. pub_key: String,
  463. on_chunk: C,
  464. on_download_finish: D,
  465. ) -> Result {
  466. // make sure we can install the update on linux
  467. // We fail here because later we can add more linux support
  468. // actually if we use APPIMAGE, our extract path should already
  469. // be set with our APPIMAGE env variable, we don't need to do
  470. // anything with it yet
  471. #[cfg(target_os = "linux")]
  472. if self.app.state::<Env>().appimage.is_none() {
  473. return Err(Error::UnsupportedLinuxPackage);
  474. }
  475. // set our headers
  476. let mut headers = self.headers.clone();
  477. headers.insert(
  478. "Accept",
  479. HeaderValue::from_str("application/octet-stream").unwrap(),
  480. );
  481. headers.insert(
  482. "User-Agent",
  483. HeaderValue::from_str("tauri/updater").unwrap(),
  484. );
  485. let client = ClientBuilder::new().build()?;
  486. // Create our request
  487. let mut req = HttpRequestBuilder::new("GET", self.download_url.as_str())?.headers(headers);
  488. if let Some(timeout) = self.timeout {
  489. req = req.timeout(timeout);
  490. }
  491. let response = client.send(req).await?;
  492. // make sure it's success
  493. if !response.status().is_success() {
  494. return Err(Error::Network(format!(
  495. "Download request failed with status: {}",
  496. response.status()
  497. )));
  498. }
  499. let content_length: Option<u64> = response
  500. .headers()
  501. .get("Content-Length")
  502. .and_then(|value| value.to_str().ok())
  503. .and_then(|value| value.parse().ok());
  504. let mut buffer = Vec::new();
  505. {
  506. use futures_util::StreamExt;
  507. let mut stream = response.bytes_stream();
  508. while let Some(chunk) = stream.next().await {
  509. let chunk = chunk?;
  510. let bytes = chunk.as_ref().to_vec();
  511. on_chunk(bytes.len(), content_length);
  512. buffer.extend(bytes);
  513. }
  514. }
  515. on_download_finish();
  516. // create memory buffer from our archive (Seek + Read)
  517. let mut archive_buffer = Cursor::new(buffer);
  518. // We need an announced signature by the server
  519. // if there is no signature, bail out.
  520. verify_signature(&mut archive_buffer, &self.signature, &pub_key)?;
  521. // TODO: implement updater in mobile
  522. #[cfg(desktop)]
  523. {
  524. // we copy the files depending of the operating system
  525. // we run the setup, appimage re-install or overwrite the
  526. // macos .app
  527. #[cfg(target_os = "windows")]
  528. copy_files_and_run(
  529. archive_buffer,
  530. &self.extract_path,
  531. self.with_elevated_task,
  532. &self.app.config(),
  533. )?;
  534. #[cfg(not(target_os = "windows"))]
  535. copy_files_and_run(archive_buffer, &self.extract_path)?;
  536. }
  537. // We are done!
  538. Ok(())
  539. }
  540. }
  541. // Linux (AppImage)
  542. // ### Expected structure:
  543. // ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
  544. // │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
  545. // └── ...
  546. // We should have an AppImage already installed to be able to copy and install
  547. // the extract_path is the current AppImage path
  548. // tmp_dir is where our new AppImage is found
  549. #[cfg(target_os = "linux")]
  550. fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) -> Result {
  551. use std::os::unix::fs::{MetadataExt, PermissionsExt};
  552. let extract_path_metadata = extract_path.metadata()?;
  553. let tmp_dir_locations = vec![
  554. Box::new(|| Some(env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
  555. Box::new(dirs_next::cache_dir),
  556. Box::new(|| Some(extract_path.parent().unwrap().to_path_buf())),
  557. ];
  558. for tmp_dir_location in tmp_dir_locations {
  559. if let Some(tmp_dir_location) = tmp_dir_location() {
  560. let tmp_dir = tempfile::Builder::new()
  561. .prefix("tauri_current_app")
  562. .tempdir_in(tmp_dir_location)?;
  563. let tmp_dir_metadata = tmp_dir.path().metadata()?;
  564. if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
  565. let mut perms = tmp_dir_metadata.permissions();
  566. perms.set_mode(0o700);
  567. std::fs::set_permissions(tmp_dir.path(), perms)?;
  568. let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
  569. // create a backup of our current app image
  570. Move::from_source(extract_path).to_dest(tmp_app_image)?;
  571. // extract the buffer to the tmp_dir
  572. // we extract our signed archive into our final directory without any temp file
  573. let mut extractor =
  574. Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
  575. return extractor
  576. .with_files(|entry| {
  577. let path = entry.path()?;
  578. if path.extension() == Some(OsStr::new("AppImage")) {
  579. // if something went wrong during the extraction, we should restore previous app
  580. if let Err(err) = entry.extract(extract_path) {
  581. Move::from_source(tmp_app_image).to_dest(extract_path)?;
  582. return Err(crate::api::Error::Extract(err.to_string()));
  583. }
  584. // early finish we have everything we need here
  585. return Ok(true);
  586. }
  587. Ok(false)
  588. })
  589. .map_err(Into::into);
  590. }
  591. }
  592. }
  593. Err(Error::TempDirNotOnSameMountPoint)
  594. }
  595. // Windows
  596. //
  597. // ### Expected structure:
  598. // ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
  599. // │ └──[AppName]_[version]_x64.msi # Application MSI
  600. // ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler
  601. // │ └──[AppName]_[version]_x64-setup.exe # NSIS installer
  602. // └── ...
  603. //
  604. // ## MSI
  605. // Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
  606. // To replace current version of the application. In later version we'll offer
  607. // incremental update to push specific binaries.
  608. //
  609. // ## EXE
  610. // Update server can provide a custom EXE (installer) who can run any task.
  611. #[cfg(target_os = "windows")]
  612. #[allow(clippy::unnecessary_wraps)]
  613. fn copy_files_and_run<R: Read + Seek>(
  614. archive_buffer: R,
  615. _extract_path: &Path,
  616. with_elevated_task: bool,
  617. config: &crate::Config,
  618. ) -> Result {
  619. // FIXME: We need to create a memory buffer with the MSI and then run it.
  620. // (instead of extracting the MSI to a temp path)
  621. //
  622. // The tricky part is the MSI need to be exposed and spawned so the memory allocation
  623. // shouldn't drop but we should be able to pass the reference so we can drop it once the installation
  624. // is done, otherwise we have a huge memory leak.
  625. let tmp_dir = tempfile::Builder::new().tempdir()?.into_path();
  626. // extract the buffer to the tmp_dir
  627. // we extract our signed archive into our final directory without any temp file
  628. let mut extractor = Extract::from_cursor(archive_buffer, ArchiveFormat::Zip);
  629. // extract the msi
  630. extractor.extract_into(&tmp_dir)?;
  631. let paths = read_dir(&tmp_dir)?;
  632. for path in paths {
  633. let found_path = path?.path();
  634. // we support 2 type of files exe & msi for now
  635. // If it's an `exe` we expect an installer not a runtime.
  636. if found_path.extension() == Some(OsStr::new("exe")) {
  637. // Run the EXE
  638. Command::new(found_path)
  639. .args(config.tauri.updater.windows.install_mode.nsis_args())
  640. .args(&config.tauri.updater.windows.installer_args)
  641. .spawn()
  642. .expect("installer failed to start");
  643. exit(0);
  644. } else if found_path.extension() == Some(OsStr::new("msi")) {
  645. if with_elevated_task {
  646. if let Some(bin_name) = current_exe()
  647. .ok()
  648. .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
  649. .and_then(|s| s.into_string().ok())
  650. {
  651. let product_name = bin_name.replace(".exe", "");
  652. // Check if there is a task that enables the updater to skip the UAC prompt
  653. let update_task_name = format!("Update {product_name} - Skip UAC");
  654. if let Ok(output) = Command::new("schtasks")
  655. .arg("/QUERY")
  656. .arg("/TN")
  657. .arg(update_task_name.clone())
  658. .output()
  659. {
  660. if output.status.success() {
  661. // Rename the MSI to the match file name the Skip UAC task is expecting it to be
  662. let temp_msi = tmp_dir.with_file_name(bin_name).with_extension("msi");
  663. Move::from_source(&found_path)
  664. .to_dest(&temp_msi)
  665. .expect("Unable to move update MSI");
  666. let exit_status = Command::new("schtasks")
  667. .arg("/RUN")
  668. .arg("/TN")
  669. .arg(update_task_name)
  670. .status()
  671. .expect("failed to start updater task");
  672. if exit_status.success() {
  673. // Successfully launched task that skips the UAC prompt
  674. exit(0);
  675. }
  676. }
  677. // Failed to run update task. Following UAC Path
  678. }
  679. }
  680. }
  681. // we need to wrap the current exe path in quotes for Start-Process
  682. let mut current_exe_arg = std::ffi::OsString::new();
  683. current_exe_arg.push("\"");
  684. current_exe_arg.push(current_exe()?);
  685. current_exe_arg.push("\"");
  686. let mut msi_path_arg = std::ffi::OsString::new();
  687. msi_path_arg.push("\"\"\"");
  688. msi_path_arg.push(&found_path);
  689. msi_path_arg.push("\"\"\"");
  690. let mut msiexec_args = config
  691. .tauri
  692. .updater
  693. .windows
  694. .install_mode
  695. .msiexec_args()
  696. .iter()
  697. .map(|p| p.to_string())
  698. .collect::<Vec<String>>();
  699. msiexec_args.extend(config.tauri.updater.windows.installer_args.clone());
  700. // run the installer and relaunch the application
  701. let system_root = std::env::var("SYSTEMROOT");
  702. let powershell_path = system_root.as_ref().map_or_else(
  703. |_| "powershell.exe".to_string(),
  704. |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
  705. );
  706. let powershell_install_res = Command::new(powershell_path)
  707. .args(["-NoProfile", "-windowstyle", "hidden"])
  708. .args([
  709. "Start-Process",
  710. "-Wait",
  711. "-FilePath",
  712. "$env:SYSTEMROOT\\System32\\msiexec.exe",
  713. "-ArgumentList",
  714. ])
  715. .arg("/i,")
  716. .arg(msi_path_arg)
  717. .arg(format!(", {}, /promptrestart;", msiexec_args.join(", ")))
  718. .arg("Start-Process")
  719. .arg(current_exe_arg)
  720. .spawn();
  721. if powershell_install_res.is_err() {
  722. // fallback to running msiexec directly - relaunch won't be available
  723. // we use this here in case powershell fails in an older machine somehow
  724. let msiexec_path = system_root.as_ref().map_or_else(
  725. |_| "msiexec.exe".to_string(),
  726. |p| format!("{p}\\System32\\msiexec.exe"),
  727. );
  728. let _ = Command::new(msiexec_path)
  729. .arg("/i")
  730. .arg(found_path)
  731. .args(msiexec_args)
  732. .arg("/promptrestart")
  733. .spawn();
  734. }
  735. exit(0);
  736. }
  737. }
  738. Ok(())
  739. }
  740. // MacOS
  741. // ### Expected structure:
  742. // ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
  743. // │ └──[AppName].app # Main application
  744. // │ └── Contents # Application contents...
  745. // │ └── ...
  746. // └── ...
  747. #[cfg(target_os = "macos")]
  748. fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) -> Result {
  749. let mut extracted_files: Vec<PathBuf> = Vec::new();
  750. // extract the buffer to the tmp_dir
  751. // we extract our signed archive into our final directory without any temp file
  752. let mut extractor =
  753. Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
  754. // the first file in the tar.gz will always be
  755. // <app_name>/Contents
  756. let tmp_dir = tempfile::Builder::new()
  757. .prefix("tauri_current_app")
  758. .tempdir()?;
  759. // create backup of our current app
  760. Move::from_source(extract_path).to_dest(tmp_dir.path())?;
  761. // extract all the files
  762. extractor.with_files(|entry| {
  763. let path = entry.path()?;
  764. // skip the first folder (should be the app name)
  765. let collected_path: PathBuf = path.iter().skip(1).collect();
  766. let extraction_path = extract_path.join(collected_path);
  767. // if something went wrong during the extraction, we should restore previous app
  768. if let Err(err) = entry.extract(&extraction_path) {
  769. for file in &extracted_files {
  770. // delete all the files we extracted
  771. if file.is_dir() {
  772. std::fs::remove_dir(file)?;
  773. } else {
  774. std::fs::remove_file(file)?;
  775. }
  776. }
  777. Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
  778. return Err(crate::api::Error::Extract(err.to_string()));
  779. }
  780. extracted_files.push(extraction_path);
  781. Ok(false)
  782. })?;
  783. let _ = std::process::Command::new("touch")
  784. .arg(extract_path)
  785. .status();
  786. Ok(())
  787. }
  788. pub(crate) fn get_updater_target() -> Option<&'static str> {
  789. if cfg!(target_os = "linux") {
  790. Some("linux")
  791. } else if cfg!(target_os = "macos") {
  792. Some("darwin")
  793. } else if cfg!(target_os = "windows") {
  794. Some("windows")
  795. } else {
  796. None
  797. }
  798. }
  799. pub(crate) fn get_updater_arch() -> Option<&'static str> {
  800. if cfg!(target_arch = "x86") {
  801. Some("i686")
  802. } else if cfg!(target_arch = "x86_64") {
  803. Some("x86_64")
  804. } else if cfg!(target_arch = "arm") {
  805. Some("armv7")
  806. } else if cfg!(target_arch = "aarch64") {
  807. Some("aarch64")
  808. } else {
  809. None
  810. }
  811. }
  812. /// Get the extract_path from the provided executable_path
  813. #[allow(unused_variables)]
  814. pub fn extract_path_from_executable(env: &Env, executable_path: &Path) -> PathBuf {
  815. // Return the path of the current executable by default
  816. // Example C:\Program Files\My App\
  817. let extract_path = executable_path
  818. .parent()
  819. .map(PathBuf::from)
  820. .expect("Can't determine extract path");
  821. // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
  822. // We need to get /Applications/<app>.app
  823. // todo(lemarier): Need a better way here
  824. // Maybe we could search for <*.app> to get the right path
  825. #[cfg(target_os = "macos")]
  826. if extract_path
  827. .display()
  828. .to_string()
  829. .contains("Contents/MacOS")
  830. {
  831. return extract_path
  832. .parent()
  833. .map(PathBuf::from)
  834. .expect("Unable to find the extract path")
  835. .parent()
  836. .map(PathBuf::from)
  837. .expect("Unable to find the extract path");
  838. }
  839. // We should use APPIMAGE exposed env variable
  840. // This is where our APPIMAGE should sit and should be replaced
  841. #[cfg(target_os = "linux")]
  842. if let Some(app_image_path) = &env.appimage {
  843. return PathBuf::from(app_image_path);
  844. }
  845. extract_path
  846. }
  847. // Convert base64 to string and prevent failing
  848. fn base64_to_string(base64_string: &str) -> Result<String> {
  849. let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?;
  850. let result = from_utf8(decoded_string)
  851. .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
  852. .to_string();
  853. Ok(result)
  854. }
  855. // Validate signature
  856. // need to be public because its been used
  857. // by our tests in the bundler
  858. //
  859. // NOTE: The buffer position is not reset.
  860. pub fn verify_signature<R>(
  861. archive_reader: &mut R,
  862. release_signature: &str,
  863. pub_key: &str,
  864. ) -> Result<bool>
  865. where
  866. R: Read,
  867. {
  868. // we need to convert the pub key
  869. let pub_key_decoded = base64_to_string(pub_key)?;
  870. let public_key = PublicKey::decode(&pub_key_decoded)?;
  871. let signature_base64_decoded = base64_to_string(release_signature)?;
  872. let signature = Signature::decode(&signature_base64_decoded)?;
  873. // read all bytes until EOF in the buffer
  874. let mut data = Vec::new();
  875. archive_reader.read_to_end(&mut data)?;
  876. // Validate signature or bail out
  877. public_key.verify(&data, &signature, true)?;
  878. Ok(true)
  879. }
  880. #[cfg(test)]
  881. mod test {
  882. use super::*;
  883. #[cfg(target_os = "macos")]
  884. use std::fs::File;
  885. macro_rules! block {
  886. ($e:expr) => {
  887. tokio_test::block_on($e)
  888. };
  889. }
  890. fn generate_sample_raw_json() -> String {
  891. r#"{
  892. "version": "v2.0.0",
  893. "notes": "Test version !",
  894. "pub_date": "2020-06-22T19:25:57Z",
  895. "platforms": {
  896. "darwin-aarch64": {
  897. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
  898. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.app.tar.gz"
  899. },
  900. "darwin-x86_64": {
  901. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
  902. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.app.tar.gz"
  903. },
  904. "linux-x86_64": {
  905. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K",
  906. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.AppImage.tar.gz"
  907. },
  908. "windows-x86_64": {
  909. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K",
  910. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.x64.msi.zip"
  911. }
  912. }
  913. }"#.into()
  914. }
  915. fn generate_sample_platform_json(
  916. version: &str,
  917. public_signature: &str,
  918. download_url: &str,
  919. ) -> String {
  920. format!(
  921. r#"
  922. {{
  923. "name": "v{version}",
  924. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  925. "pub_date": "2020-06-25T14:14:19Z",
  926. "signature": "{public_signature}",
  927. "url": "{download_url}"
  928. }}
  929. "#
  930. )
  931. }
  932. fn generate_sample_with_elevated_task_platform_json(
  933. version: &str,
  934. public_signature: &str,
  935. download_url: &str,
  936. with_elevated_task: bool,
  937. ) -> String {
  938. format!(
  939. r#"
  940. {{
  941. "name": "v{version}",
  942. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  943. "pub_date": "2020-06-25T14:14:19Z",
  944. "signature": "{public_signature}",
  945. "url": "{download_url}",
  946. "with_elevated_task": {with_elevated_task}
  947. }}
  948. "#
  949. )
  950. }
  951. #[test]
  952. fn simple_http_updater() {
  953. let _m = mockito::mock("GET", "/")
  954. .with_status(200)
  955. .with_header("content-type", "application/json")
  956. .with_body(generate_sample_raw_json())
  957. .create();
  958. let app = crate::test::mock_app();
  959. let check_update = block!(builder(app.handle())
  960. .current_version("0.0.0".parse().unwrap())
  961. .url(mockito::server_url())
  962. .build());
  963. let updater = check_update.expect("Can't check update");
  964. assert!(updater.should_update);
  965. }
  966. #[test]
  967. fn simple_http_updater_raw_json() {
  968. let _m = mockito::mock("GET", "/")
  969. .with_status(200)
  970. .with_header("content-type", "application/json")
  971. .with_body(generate_sample_raw_json())
  972. .create();
  973. let app = crate::test::mock_app();
  974. let check_update = block!(builder(app.handle())
  975. .current_version("0.0.0".parse().unwrap())
  976. .url(mockito::server_url())
  977. .build());
  978. let updater = check_update.expect("Can't check update");
  979. assert!(updater.should_update);
  980. }
  981. #[test]
  982. fn simple_http_updater_raw_json_windows_x86_64() {
  983. let _m = mockito::mock("GET", "/")
  984. .with_status(200)
  985. .with_header("content-type", "application/json")
  986. .with_body(generate_sample_raw_json())
  987. .create();
  988. let app = crate::test::mock_app();
  989. let check_update = block!(builder(app.handle())
  990. .current_version("0.0.0".parse().unwrap())
  991. .target("windows-x86_64")
  992. .url(mockito::server_url())
  993. .build());
  994. let updater = check_update.expect("Can't check update");
  995. assert!(updater.should_update);
  996. assert_eq!(updater.version, "2.0.0");
  997. assert_eq!(updater.signature, "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K");
  998. assert_eq!(
  999. updater.download_url.to_string(),
  1000. "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.x64.msi.zip"
  1001. );
  1002. }
  1003. #[test]
  1004. fn simple_http_updater_raw_json_uptodate() {
  1005. let _m = mockito::mock("GET", "/")
  1006. .with_status(200)
  1007. .with_header("content-type", "application/json")
  1008. .with_body(generate_sample_raw_json())
  1009. .create();
  1010. let app = crate::test::mock_app();
  1011. let check_update = block!(builder(app.handle())
  1012. .current_version("10.0.0".parse().unwrap())
  1013. .url(mockito::server_url())
  1014. .build());
  1015. let updater = check_update.expect("Can't check update");
  1016. assert!(!updater.should_update);
  1017. }
  1018. #[test]
  1019. fn simple_http_updater_without_version() {
  1020. let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0")
  1021. .with_status(200)
  1022. .with_header("content-type", "application/json")
  1023. .with_body(generate_sample_platform_json(
  1024. "2.0.0",
  1025. "SampleTauriKey",
  1026. "https://tauri.app",
  1027. ))
  1028. .create();
  1029. let app = crate::test::mock_app();
  1030. let check_update = block!(builder(app.handle())
  1031. .current_version("1.0.0".parse().unwrap())
  1032. .url(format!(
  1033. "{}/darwin-aarch64/{{{{current_version}}}}",
  1034. mockito::server_url()
  1035. ))
  1036. .build());
  1037. let updater = check_update.expect("Can't check update");
  1038. assert!(updater.should_update);
  1039. }
  1040. #[test]
  1041. fn simple_http_updater_percent_decode() {
  1042. let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0")
  1043. .with_status(200)
  1044. .with_header("content-type", "application/json")
  1045. .with_body(generate_sample_platform_json(
  1046. "2.0.0",
  1047. "SampleTauriKey",
  1048. "https://tauri.app",
  1049. ))
  1050. .create();
  1051. let app = crate::test::mock_app();
  1052. let check_update = block!(builder(app.handle())
  1053. .current_version("1.0.0".parse().unwrap())
  1054. .url(
  1055. url::Url::parse(&format!(
  1056. "{}/darwin-aarch64/{{{{current_version}}}}",
  1057. mockito::server_url()
  1058. ))
  1059. .unwrap()
  1060. .to_string()
  1061. )
  1062. .build());
  1063. let updater = check_update.expect("Can't check update");
  1064. assert!(updater.should_update);
  1065. let app = crate::test::mock_app();
  1066. let check_update = block!(builder(app.handle())
  1067. .current_version("1.0.0".parse().unwrap())
  1068. .urls(&[url::Url::parse(&format!(
  1069. "{}/darwin-aarch64/{{{{current_version}}}}",
  1070. mockito::server_url()
  1071. ))
  1072. .unwrap()
  1073. .to_string()])
  1074. .build());
  1075. let updater = check_update.expect("Can't check update");
  1076. assert!(updater.should_update);
  1077. }
  1078. #[test]
  1079. fn simple_http_updater_with_elevated_task() {
  1080. let _m = mockito::mock("GET", "/windows-x86_64/1.0.0")
  1081. .with_status(200)
  1082. .with_header("content-type", "application/json")
  1083. .with_body(generate_sample_with_elevated_task_platform_json(
  1084. "2.0.0",
  1085. "SampleTauriKey",
  1086. "https://tauri.app",
  1087. true,
  1088. ))
  1089. .create();
  1090. let app = crate::test::mock_app();
  1091. let check_update = block!(builder(app.handle())
  1092. .current_version("1.0.0".parse().unwrap())
  1093. .url(format!(
  1094. "{}/windows-x86_64/{{{{current_version}}}}",
  1095. mockito::server_url()
  1096. ))
  1097. .build());
  1098. let updater = check_update.expect("Can't check update");
  1099. assert!(updater.should_update);
  1100. }
  1101. #[test]
  1102. fn http_updater_uptodate() {
  1103. let _m = mockito::mock("GET", "/darwin-aarch64/10.0.0")
  1104. .with_status(200)
  1105. .with_header("content-type", "application/json")
  1106. .with_body(generate_sample_platform_json(
  1107. "2.0.0",
  1108. "SampleTauriKey",
  1109. "https://tauri.app",
  1110. ))
  1111. .create();
  1112. let app = crate::test::mock_app();
  1113. let check_update = block!(builder(app.handle())
  1114. .current_version("10.0.0".parse().unwrap())
  1115. .url(format!(
  1116. "{}/darwin-aarch64/{{{{current_version}}}}",
  1117. mockito::server_url()
  1118. ))
  1119. .build());
  1120. let updater = check_update.expect("Can't check update");
  1121. assert!(!updater.should_update);
  1122. }
  1123. #[test]
  1124. fn http_updater_fallback_urls() {
  1125. let _m = mockito::mock("GET", "/")
  1126. .with_status(200)
  1127. .with_header("content-type", "application/json")
  1128. .with_body(generate_sample_raw_json())
  1129. .create();
  1130. let app = crate::test::mock_app();
  1131. let check_update = block!(builder(app.handle())
  1132. .url("http://badurl.www.tld/1".into())
  1133. .url(mockito::server_url())
  1134. .current_version("0.0.1".parse().unwrap())
  1135. .build());
  1136. let updater = check_update.expect("Can't check remote update");
  1137. assert!(updater.should_update);
  1138. }
  1139. #[test]
  1140. fn http_updater_fallback_urls_with_array() {
  1141. let _m = mockito::mock("GET", "/")
  1142. .with_status(200)
  1143. .with_header("content-type", "application/json")
  1144. .with_body(generate_sample_raw_json())
  1145. .create();
  1146. let app = crate::test::mock_app();
  1147. let check_update = block!(builder(app.handle())
  1148. .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),])
  1149. .current_version("0.0.1".parse().unwrap())
  1150. .build());
  1151. let updater = check_update.expect("Can't check remote update");
  1152. assert!(updater.should_update);
  1153. }
  1154. #[test]
  1155. fn http_updater_invalid_remote_data() {
  1156. let invalid_signature = r#"{
  1157. "version": "v0.0.3",
  1158. "notes": "Blablaa",
  1159. "pub_date": "2020-02-20T15:41:00Z",
  1160. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1161. "signature": true
  1162. }"#;
  1163. let invalid_version = r#"{
  1164. "version": 5,
  1165. "notes": "Blablaa",
  1166. "pub_date": "2020-02-20T15:41:00Z",
  1167. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1168. "signature": "x"
  1169. }"#;
  1170. let invalid_name = r#"{
  1171. "name": false,
  1172. "notes": "Blablaa",
  1173. "pub_date": "2020-02-20T15:41:00Z",
  1174. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1175. "signature": "x"
  1176. }"#;
  1177. let invalid_date = r#"{
  1178. "version": "1.0.0",
  1179. "notes": "Blablaa",
  1180. "pub_date": 345645646,
  1181. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1182. "signature": "x"
  1183. }"#;
  1184. let invalid_notes = r#"{
  1185. "version": "v0.0.3",
  1186. "notes": ["bla", "bla"],
  1187. "pub_date": "2020-02-20T15:41:00Z",
  1188. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1189. "signature": "x"
  1190. }"#;
  1191. let invalid_url = r#"{
  1192. "version": "v0.0.3",
  1193. "notes": "Blablaa",
  1194. "pub_date": "2020-02-20T15:41:00Z",
  1195. "url": ["https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz", "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"],
  1196. "signature": "x"
  1197. }"#;
  1198. let invalid_platform_signature = r#"{
  1199. "version": "v0.0.3",
  1200. "notes": "Blablaa",
  1201. "pub_date": "2020-02-20T15:41:00Z",
  1202. "platforms": {
  1203. "test-target": {
  1204. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1205. "signature": {
  1206. "test-target": "x"
  1207. }
  1208. }
  1209. }
  1210. }"#;
  1211. let invalid_platform_url = r#"{
  1212. "version": "v0.0.3",
  1213. "notes": "Blablaa",
  1214. "pub_date": "2020-02-20T15:41:00Z",
  1215. "platforms": {
  1216. "test-target": {
  1217. "url": {
  1218. "first": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"
  1219. }
  1220. "signature": "x"
  1221. }
  1222. }
  1223. }"#;
  1224. let test_cases = [
  1225. (
  1226. invalid_signature,
  1227. Box::new(|e| matches!(e, Error::InvalidResponseType("signature", "string", _)))
  1228. as Box<dyn FnOnce(Error) -> bool>,
  1229. ),
  1230. (
  1231. invalid_version,
  1232. Box::new(|e| matches!(e, Error::InvalidResponseType("version", "string", _)))
  1233. as Box<dyn FnOnce(Error) -> bool>,
  1234. ),
  1235. (
  1236. invalid_name,
  1237. Box::new(|e| matches!(e, Error::InvalidResponseType("name", "string", _)))
  1238. as Box<dyn FnOnce(Error) -> bool>,
  1239. ),
  1240. (
  1241. invalid_date,
  1242. Box::new(|e| matches!(e, Error::InvalidResponseType("pub_date", "string", _)))
  1243. as Box<dyn FnOnce(Error) -> bool>,
  1244. ),
  1245. (
  1246. invalid_notes,
  1247. Box::new(|e| matches!(e, Error::InvalidResponseType("notes", "string", _)))
  1248. as Box<dyn FnOnce(Error) -> bool>,
  1249. ),
  1250. (
  1251. invalid_url,
  1252. Box::new(|e| matches!(e, Error::InvalidResponseType("url", "string", _)))
  1253. as Box<dyn FnOnce(Error) -> bool>,
  1254. ),
  1255. (
  1256. invalid_platform_signature,
  1257. Box::new(|e| matches!(e, Error::InvalidResponseType("signature", "string", _)))
  1258. as Box<dyn FnOnce(Error) -> bool>,
  1259. ),
  1260. (
  1261. invalid_platform_url,
  1262. Box::new(|e| matches!(e, Error::InvalidResponseType("url", "string", _)))
  1263. as Box<dyn FnOnce(Error) -> bool>,
  1264. ),
  1265. ];
  1266. for (response, validator) in test_cases {
  1267. let _m = mockito::mock("GET", "/")
  1268. .with_status(200)
  1269. .with_header("content-type", "application/json")
  1270. .with_body(response)
  1271. .create();
  1272. let app = crate::test::mock_app();
  1273. let check_update = block!(builder(app.handle())
  1274. .url(mockito::server_url())
  1275. .current_version("0.0.1".parse().unwrap())
  1276. .target("test-target")
  1277. .build());
  1278. if let Err(e) = check_update {
  1279. validator(e);
  1280. } else {
  1281. panic!("unexpected Ok response");
  1282. }
  1283. }
  1284. }
  1285. #[test]
  1286. fn http_updater_missing_remote_data() {
  1287. let missing_signature = r#"{
  1288. "version": "v0.0.3",
  1289. "notes": "Blablaa",
  1290. "pub_date": "2020-02-20T15:41:00Z",
  1291. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"
  1292. }"#;
  1293. let missing_version = r#"{
  1294. "notes": "Blablaa",
  1295. "pub_date": "2020-02-20T15:41:00Z",
  1296. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1297. "signature": "x"
  1298. }"#;
  1299. let missing_url = r#"{
  1300. "version": "v0.0.3",
  1301. "notes": "Blablaa",
  1302. "pub_date": "2020-02-20T15:41:00Z",
  1303. "signature": "x"
  1304. }"#;
  1305. let missing_target = r#"{
  1306. "version": "v0.0.3",
  1307. "notes": "Blablaa",
  1308. "pub_date": "2020-02-20T15:41:00Z",
  1309. "platforms": {
  1310. "unknown-target": {
  1311. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1312. "signature": "x"
  1313. }
  1314. }
  1315. }"#;
  1316. let missing_platform_signature = r#"{
  1317. "version": "v0.0.3",
  1318. "notes": "Blablaa",
  1319. "pub_date": "2020-02-20T15:41:00Z",
  1320. "platforms": {
  1321. "test-target": {
  1322. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"
  1323. }
  1324. }
  1325. }"#;
  1326. let missing_platform_url = r#"{
  1327. "version": "v0.0.3",
  1328. "notes": "Blablaa",
  1329. "pub_date": "2020-02-20T15:41:00Z",
  1330. "platforms": {
  1331. "test-target": {
  1332. "signature": "x"
  1333. }
  1334. }
  1335. }"#;
  1336. fn missing_field_error(field: &str) -> String {
  1337. format!("the `{field}` field was not set on the updater response")
  1338. }
  1339. let test_cases = [
  1340. (missing_signature, missing_field_error("signature")),
  1341. (missing_version, "missing field `version`".to_string()),
  1342. (missing_url, missing_field_error("url")),
  1343. (
  1344. missing_target,
  1345. Error::TargetNotFound("test-target".into()).to_string(),
  1346. ),
  1347. (
  1348. missing_platform_signature,
  1349. "missing field `signature`".to_string(),
  1350. ),
  1351. (missing_platform_url, "missing field `url`".to_string()),
  1352. ];
  1353. for (response, error) in test_cases {
  1354. let _m = mockito::mock("GET", "/")
  1355. .with_status(200)
  1356. .with_header("content-type", "application/json")
  1357. .with_body(response)
  1358. .create();
  1359. let app = crate::test::mock_app();
  1360. let check_update = block!(builder(app.handle())
  1361. .url(mockito::server_url())
  1362. .current_version("0.0.1".parse().unwrap())
  1363. .target("test-target")
  1364. .build());
  1365. if let Err(e) = check_update {
  1366. println!("ERROR: {e}, expected: {error}");
  1367. assert!(e.to_string().contains(&error));
  1368. } else {
  1369. panic!("unexpected Ok response");
  1370. }
  1371. }
  1372. }
  1373. // run complete process on mac only for now as we don't have
  1374. // server (api) that we can use to test
  1375. #[test]
  1376. #[cfg(target_os = "macos")]
  1377. fn http_updater_complete_process() {
  1378. #[cfg(target_os = "macos")]
  1379. let archive_file = "archive.macos.tar.gz";
  1380. #[cfg(target_os = "linux")]
  1381. let archive_file = "archive.linux.tar.gz";
  1382. #[cfg(target_os = "windows")]
  1383. let archive_file = "archive.windows.zip";
  1384. let good_archive_url = format!("{}/{archive_file}", mockito::server_url());
  1385. let mut signature_file = File::open(format!(
  1386. "./test/updater/fixture/archives/{archive_file}.sig"
  1387. ))
  1388. .expect("Unable to open signature");
  1389. let mut signature = String::new();
  1390. signature_file
  1391. .read_to_string(&mut signature)
  1392. .expect("Unable to read signature as string");
  1393. let mut pubkey_file = File::open("./test/updater/fixture/good_signature/update.key.pub")
  1394. .expect("Unable to open pubkey");
  1395. let mut pubkey = String::new();
  1396. pubkey_file
  1397. .read_to_string(&mut pubkey)
  1398. .expect("Unable to read signature as string");
  1399. // add sample file
  1400. let _m = mockito::mock("GET", format!("/{archive_file}").as_str())
  1401. .with_status(200)
  1402. .with_header("content-type", "application/octet-stream")
  1403. .with_body_from_file(format!("./test/updater/fixture/archives/{archive_file}"))
  1404. .create();
  1405. // sample mock for update file
  1406. let _m = mockito::mock("GET", "/")
  1407. .with_status(200)
  1408. .with_header("content-type", "application/json")
  1409. .with_body(generate_sample_platform_json(
  1410. "2.0.1",
  1411. signature.as_ref(),
  1412. good_archive_url.as_ref(),
  1413. ))
  1414. .create();
  1415. // Build a tmpdir so we can test our extraction inside
  1416. // We dont want to overwrite our current executable or the directory
  1417. // Otherwise tests are failing...
  1418. let executable_path = current_exe().expect("Can't extract executable path");
  1419. let parent_path = executable_path
  1420. .parent()
  1421. .expect("Can't find the parent path");
  1422. let tmp_dir = tempfile::Builder::new()
  1423. .prefix("tauri_updater_test")
  1424. .tempdir_in(parent_path);
  1425. assert!(tmp_dir.is_ok());
  1426. let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir");
  1427. let tmp_dir_path = tmp_dir_unwrap.path();
  1428. #[cfg(target_os = "linux")]
  1429. let my_executable = &tmp_dir_path.join("updater-example_0.1.0_amd64.AppImage");
  1430. #[cfg(target_os = "macos")]
  1431. let my_executable = &tmp_dir_path.join("my_app");
  1432. #[cfg(target_os = "windows")]
  1433. let my_executable = &tmp_dir_path.join("my_app.exe");
  1434. // configure the updater
  1435. let app = crate::test::mock_app();
  1436. let check_update = block!(builder(app.handle())
  1437. .url(mockito::server_url())
  1438. // It should represent the executable path, that's why we add my_app.exe in our
  1439. // test path -- in production you shouldn't have to provide it
  1440. .executable_path(my_executable)
  1441. // make sure we force an update
  1442. .current_version("1.0.0".parse().unwrap())
  1443. .build());
  1444. #[cfg(target_os = "linux")]
  1445. {
  1446. env::set_var("APPIMAGE", my_executable);
  1447. }
  1448. // unwrap our results
  1449. let updater = check_update.expect("Can't check remote update");
  1450. // make sure we need to update
  1451. assert!(updater.should_update);
  1452. // make sure we can read announced version
  1453. assert_eq!(updater.version, "2.0.1");
  1454. // download, install and validate signature
  1455. let install_process = block!(updater.download_and_install(pubkey, |_, _| (), || ()));
  1456. assert!(install_process.is_ok());
  1457. // make sure the extraction went well (it should have skipped the main app.app folder)
  1458. // as we can't extract in /Applications directly
  1459. #[cfg(target_os = "macos")]
  1460. let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app");
  1461. #[cfg(target_os = "linux")]
  1462. // linux should extract at same place as the executable path
  1463. let bin_file = my_executable;
  1464. #[cfg(target_os = "windows")]
  1465. let bin_file = tmp_dir_path.join("with").join("long").join("path.json");
  1466. assert!(bin_file.exists());
  1467. }
  1468. }