core.rs 51 KB

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