core.rs 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609
  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::http::{ClientBuilder, HttpRequestBuilder},
  9. AppHandle, Manager, Runtime,
  10. };
  11. use base64::decode;
  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(feature = "updater")]
  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(feature = "updater")]
  34. #[cfg(not(target_os = "macos"))]
  35. use std::ffi::OsStr;
  36. #[cfg(all(feature = "updater", not(target_os = "windows")))]
  37. use crate::api::file::Compression;
  38. #[cfg(target_os = "windows")]
  39. use std::{
  40. fs::read_dir,
  41. process::{exit, Command},
  42. };
  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<dyn FnOnce(&Version, &RemoteRelease) -> bool + Send>>,
  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 usefull 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 res = res.read().await?;
  345. // got status code 2XX
  346. if StatusCode::from_u16(res.status)
  347. .map_err(|e| Error::Builder(e.to_string()))?
  348. .is_success()
  349. {
  350. // if we got 204
  351. if StatusCode::NO_CONTENT.as_u16() == res.status {
  352. // return with `UpToDate` error
  353. // we should catch on the client
  354. return Err(Error::UpToDate);
  355. };
  356. // Convert the remote result to our local struct
  357. let built_release = serde_json::from_value(res.data).map_err(Into::into);
  358. // make sure all went well and the remote data is compatible
  359. // with what we need locally
  360. match built_release {
  361. Ok(release) => {
  362. last_error = None;
  363. remote_release = Some(release);
  364. break;
  365. }
  366. Err(err) => last_error = Some(err),
  367. }
  368. } // if status code is not 2XX we keep loopin' our urls
  369. }
  370. }
  371. // Last error is cleaned on success -- shouldn't be triggered if
  372. // we have a successful call
  373. if let Some(error) = last_error {
  374. return Err(error);
  375. }
  376. // Extracted remote metadata
  377. let final_release = remote_release.ok_or(Error::ReleaseNotFound)?;
  378. // did the announced version is greated than our current one?
  379. let should_update = if let Some(comparator) = self.should_install.take() {
  380. comparator(&self.current_version, &final_release)
  381. } else {
  382. final_release.version() > &self.current_version
  383. };
  384. headers.remove("Accept");
  385. // create our new updater
  386. Ok(Update {
  387. app: self.app,
  388. target,
  389. extract_path,
  390. should_update,
  391. version: final_release.version().to_string(),
  392. date: final_release.pub_date().cloned(),
  393. current_version: self.current_version,
  394. download_url: final_release.download_url(&json_target)?.to_owned(),
  395. body: final_release.notes().cloned(),
  396. signature: final_release.signature(&json_target)?.to_owned(),
  397. #[cfg(target_os = "windows")]
  398. with_elevated_task: final_release.with_elevated_task(&json_target)?,
  399. timeout: self.timeout,
  400. headers,
  401. })
  402. }
  403. }
  404. pub fn builder<R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<R> {
  405. UpdateBuilder::new(app)
  406. }
  407. #[derive(Debug)]
  408. pub struct Update<R: Runtime> {
  409. /// Application handle.
  410. pub app: AppHandle<R>,
  411. /// Update description
  412. pub body: Option<String>,
  413. /// Should we update or not
  414. pub should_update: bool,
  415. /// Version announced
  416. pub version: String,
  417. /// Running version
  418. pub current_version: Version,
  419. /// Update publish date
  420. pub date: Option<OffsetDateTime>,
  421. /// Target
  422. #[allow(dead_code)]
  423. target: String,
  424. /// Extract path
  425. extract_path: PathBuf,
  426. /// Download URL announced
  427. download_url: Url,
  428. /// Signature announced
  429. signature: String,
  430. #[cfg(target_os = "windows")]
  431. /// Optional: Windows only try to use elevated task
  432. /// Default to false
  433. with_elevated_task: bool,
  434. /// Request timeout
  435. timeout: Option<Duration>,
  436. /// Request headers
  437. headers: HeaderMap,
  438. }
  439. impl<R: Runtime> Clone for Update<R> {
  440. fn clone(&self) -> Self {
  441. Self {
  442. app: self.app.clone(),
  443. body: self.body.clone(),
  444. should_update: self.should_update,
  445. version: self.version.clone(),
  446. current_version: self.current_version.clone(),
  447. date: self.date,
  448. target: self.target.clone(),
  449. extract_path: self.extract_path.clone(),
  450. download_url: self.download_url.clone(),
  451. signature: self.signature.clone(),
  452. #[cfg(target_os = "windows")]
  453. with_elevated_task: self.with_elevated_task,
  454. timeout: self.timeout,
  455. headers: self.headers.clone(),
  456. }
  457. }
  458. }
  459. impl<R: Runtime> Update<R> {
  460. // Download and install our update
  461. // @todo(lemarier): Split into download and install (two step) but need to be thread safe
  462. pub(crate) async fn download_and_install<C: Fn(usize, Option<u64>), D: FnOnce()>(
  463. &self,
  464. pub_key: String,
  465. on_chunk: C,
  466. on_download_finish: D,
  467. ) -> Result {
  468. // make sure we can install the update on linux
  469. // We fail here because later we can add more linux support
  470. // actually if we use APPIMAGE, our extract path should already
  471. // be set with our APPIMAGE env variable, we don't need to do
  472. // anythin with it yet
  473. #[cfg(target_os = "linux")]
  474. if self.app.state::<Env>().appimage.is_none() {
  475. return Err(Error::UnsupportedLinuxPackage);
  476. }
  477. // set our headers
  478. let mut headers = self.headers.clone();
  479. headers.insert(
  480. "Accept",
  481. HeaderValue::from_str("application/octet-stream").unwrap(),
  482. );
  483. headers.insert(
  484. "User-Agent",
  485. HeaderValue::from_str("tauri/updater").unwrap(),
  486. );
  487. let client = ClientBuilder::new().build()?;
  488. // Create our request
  489. let mut req = HttpRequestBuilder::new("GET", self.download_url.as_str())?.headers(headers);
  490. if let Some(timeout) = self.timeout {
  491. req = req.timeout(timeout);
  492. }
  493. let response = client.send(req).await?;
  494. // make sure it's success
  495. if !response.status().is_success() {
  496. return Err(Error::Network(format!(
  497. "Download request failed with status: {}",
  498. response.status()
  499. )));
  500. }
  501. let content_length: Option<u64> = response
  502. .headers()
  503. .get("Content-Length")
  504. .and_then(|value| value.to_str().ok())
  505. .and_then(|value| value.parse().ok());
  506. let mut buffer = Vec::new();
  507. #[cfg(feature = "reqwest-client")]
  508. {
  509. use futures::StreamExt;
  510. let mut stream = response.bytes_stream();
  511. while let Some(chunk) = stream.next().await {
  512. let chunk = chunk?;
  513. let bytes = chunk.as_ref().to_vec();
  514. on_chunk(bytes.len(), content_length);
  515. buffer.extend(bytes);
  516. }
  517. }
  518. #[cfg(not(feature = "reqwest-client"))]
  519. {
  520. let mut reader = response.reader();
  521. let mut buf = [0; 16384];
  522. loop {
  523. match reader.read(&mut buf) {
  524. Ok(b) => {
  525. if b == 0 {
  526. break;
  527. } else {
  528. let bytes = buf[0..b].to_vec();
  529. on_chunk(bytes.len(), content_length);
  530. buffer.extend(bytes);
  531. }
  532. }
  533. Err(e) => return Err(e.into()),
  534. }
  535. }
  536. }
  537. on_download_finish();
  538. // create memory buffer from our archive (Seek + Read)
  539. let mut archive_buffer = Cursor::new(buffer);
  540. // We need an announced signature by the server
  541. // if there is no signature, bail out.
  542. verify_signature(&mut archive_buffer, &self.signature, &pub_key)?;
  543. #[cfg(feature = "updater")]
  544. {
  545. // we copy the files depending of the operating system
  546. // we run the setup, appimage re-install or overwrite the
  547. // macos .app
  548. #[cfg(target_os = "windows")]
  549. copy_files_and_run(
  550. archive_buffer,
  551. &self.extract_path,
  552. self.with_elevated_task,
  553. self
  554. .app
  555. .config()
  556. .tauri
  557. .updater
  558. .windows
  559. .install_mode
  560. .clone()
  561. .msiexec_args(),
  562. )?;
  563. #[cfg(not(target_os = "windows"))]
  564. copy_files_and_run(archive_buffer, &self.extract_path)?;
  565. }
  566. // We are done!
  567. Ok(())
  568. }
  569. }
  570. // Linux (AppImage)
  571. // ### Expected structure:
  572. // ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
  573. // │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
  574. // └── ...
  575. // We should have an AppImage already installed to be able to copy and install
  576. // the extract_path is the current AppImage path
  577. // tmp_dir is where our new AppImage is found
  578. #[cfg(feature = "updater")]
  579. #[cfg(target_os = "linux")]
  580. fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) -> Result {
  581. use std::os::unix::fs::PermissionsExt;
  582. let tmp_dir = tempfile::Builder::new()
  583. .prefix("tauri_current_app")
  584. .tempdir()?;
  585. let mut perms = std::fs::metadata(tmp_dir.path())?.permissions();
  586. perms.set_mode(0o700);
  587. std::fs::set_permissions(tmp_dir.path(), perms)?;
  588. let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
  589. // create a backup of our current app image
  590. Move::from_source(extract_path).to_dest(tmp_app_image)?;
  591. // extract the buffer to the tmp_dir
  592. // we extract our signed archive into our final directory without any temp file
  593. let mut extractor =
  594. Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
  595. extractor.with_files(|entry| {
  596. let path = entry.path()?;
  597. if path.extension() == Some(OsStr::new("AppImage")) {
  598. // if something went wrong during the extraction, we should restore previous app
  599. if let Err(err) = entry.extract(extract_path) {
  600. Move::from_source(tmp_app_image).to_dest(extract_path)?;
  601. return Err(crate::api::Error::Extract(err.to_string()));
  602. }
  603. // early finish we have everything we need here
  604. return Ok(true);
  605. }
  606. Ok(false)
  607. })?;
  608. Ok(())
  609. }
  610. // Windows
  611. // ### Expected structure:
  612. // ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
  613. // │ └──[AppName]_[version]_x64.msi # Application MSI
  614. // └── ...
  615. // ## MSI
  616. // Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
  617. // To replace current version of the application. In later version we'll offer
  618. // incremental update to push specific binaries.
  619. // ## EXE
  620. // Update server can provide a custom EXE (installer) who can run any task.
  621. #[cfg(feature = "updater")]
  622. #[cfg(target_os = "windows")]
  623. #[allow(clippy::unnecessary_wraps)]
  624. fn copy_files_and_run<R: Read + Seek>(
  625. archive_buffer: R,
  626. _extract_path: &Path,
  627. with_elevated_task: bool,
  628. msiexec_args: &[&str],
  629. ) -> Result {
  630. // FIXME: We need to create a memory buffer with the MSI and then run it.
  631. // (instead of extracting the MSI to a temp path)
  632. //
  633. // The tricky part is the MSI need to be exposed and spawned so the memory allocation
  634. // shouldn't drop but we should be able to pass the reference so we can drop it once the installation
  635. // is done, otherwise we have a huge memory leak.
  636. let tmp_dir = tempfile::Builder::new().tempdir()?.into_path();
  637. // extract the buffer to the tmp_dir
  638. // we extract our signed archive into our final directory without any temp file
  639. let mut extractor = Extract::from_cursor(archive_buffer, ArchiveFormat::Zip);
  640. // extract the msi
  641. extractor.extract_into(&tmp_dir)?;
  642. let paths = read_dir(&tmp_dir)?;
  643. // This consumes the TempDir without deleting directory on the filesystem,
  644. // meaning that the directory will no longer be automatically deleted.
  645. for path in paths {
  646. let found_path = path?.path();
  647. // we support 2 type of files exe & msi for now
  648. // If it's an `exe` we expect an installer not a runtime.
  649. if found_path.extension() == Some(OsStr::new("exe")) {
  650. // Run the EXE
  651. Command::new(found_path)
  652. .spawn()
  653. .expect("installer failed to start");
  654. exit(0);
  655. } else if found_path.extension() == Some(OsStr::new("msi")) {
  656. if with_elevated_task {
  657. if let Some(bin_name) = current_exe()
  658. .ok()
  659. .and_then(|pb| pb.file_name().map(|s| s.to_os_string()))
  660. .and_then(|s| s.into_string().ok())
  661. {
  662. let product_name = bin_name.replace(".exe", "");
  663. // Check if there is a task that enables the updater to skip the UAC prompt
  664. let update_task_name = format!("Update {} - Skip UAC", product_name);
  665. if let Ok(output) = Command::new("schtasks")
  666. .arg("/QUERY")
  667. .arg("/TN")
  668. .arg(update_task_name.clone())
  669. .output()
  670. {
  671. if output.status.success() {
  672. // Rename the MSI to the match file name the Skip UAC task is expecting it to be
  673. let temp_msi = tmp_dir.with_file_name(bin_name).with_extension("msi");
  674. Move::from_source(&found_path)
  675. .to_dest(&temp_msi)
  676. .expect("Unable to move update MSI");
  677. let exit_status = Command::new("schtasks")
  678. .arg("/RUN")
  679. .arg("/TN")
  680. .arg(update_task_name)
  681. .status()
  682. .expect("failed to start updater task");
  683. if exit_status.success() {
  684. // Successfully launched task that skips the UAC prompt
  685. exit(0);
  686. }
  687. }
  688. // Failed to run update task. Following UAC Path
  689. }
  690. }
  691. }
  692. // restart should be handled by WIX as we exit the process
  693. Command::new("msiexec.exe")
  694. .arg("/i")
  695. .arg(found_path)
  696. .args(msiexec_args)
  697. .arg("/promptrestart")
  698. .spawn()
  699. .expect("installer failed to start");
  700. exit(0);
  701. }
  702. }
  703. Ok(())
  704. }
  705. // MacOS
  706. // ### Expected structure:
  707. // ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
  708. // │ └──[AppName].app # Main application
  709. // │ └── Contents # Application contents...
  710. // │ └── ...
  711. // └── ...
  712. #[cfg(feature = "updater")]
  713. #[cfg(target_os = "macos")]
  714. fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) -> Result {
  715. let mut extracted_files: Vec<PathBuf> = Vec::new();
  716. // extract the buffer to the tmp_dir
  717. // we extract our signed archive into our final directory without any temp file
  718. let mut extractor =
  719. Extract::from_cursor(archive_buffer, ArchiveFormat::Tar(Some(Compression::Gz)));
  720. // the first file in the tar.gz will always be
  721. // <app_name>/Contents
  722. let tmp_dir = tempfile::Builder::new()
  723. .prefix("tauri_current_app")
  724. .tempdir()?;
  725. // create backup of our current app
  726. Move::from_source(extract_path).to_dest(tmp_dir.path())?;
  727. // extract all the files
  728. extractor.with_files(|entry| {
  729. let path = entry.path()?;
  730. // skip the first folder (should be the app name)
  731. let collected_path: PathBuf = path.iter().skip(1).collect();
  732. let extraction_path = extract_path.join(collected_path);
  733. // if something went wrong during the extraction, we should restore previous app
  734. if let Err(err) = entry.extract(&extraction_path) {
  735. for file in &extracted_files {
  736. // delete all the files we extracted
  737. if file.is_dir() {
  738. std::fs::remove_dir(file)?;
  739. } else {
  740. std::fs::remove_file(file)?;
  741. }
  742. }
  743. Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
  744. return Err(crate::api::Error::Extract(err.to_string()));
  745. }
  746. extracted_files.push(extraction_path);
  747. Ok(false)
  748. })?;
  749. Ok(())
  750. }
  751. pub(crate) fn get_updater_target() -> Option<&'static str> {
  752. if cfg!(target_os = "linux") {
  753. Some("linux")
  754. } else if cfg!(target_os = "macos") {
  755. Some("darwin")
  756. } else if cfg!(target_os = "windows") {
  757. Some("windows")
  758. } else {
  759. None
  760. }
  761. }
  762. pub(crate) fn get_updater_arch() -> Option<&'static str> {
  763. if cfg!(target_arch = "x86") {
  764. Some("i686")
  765. } else if cfg!(target_arch = "x86_64") {
  766. Some("x86_64")
  767. } else if cfg!(target_arch = "arm") {
  768. Some("armv7")
  769. } else if cfg!(target_arch = "aarch64") {
  770. Some("aarch64")
  771. } else {
  772. None
  773. }
  774. }
  775. /// Get the extract_path from the provided executable_path
  776. #[allow(unused_variables)]
  777. pub fn extract_path_from_executable(env: &Env, executable_path: &Path) -> PathBuf {
  778. // Return the path of the current executable by default
  779. // Example C:\Program Files\My App\
  780. let extract_path = executable_path
  781. .parent()
  782. .map(PathBuf::from)
  783. .expect("Can't determine extract path");
  784. // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp
  785. // We need to get /Applications/<app>.app
  786. // todo(lemarier): Need a better way here
  787. // Maybe we could search for <*.app> to get the right path
  788. #[cfg(target_os = "macos")]
  789. if extract_path
  790. .display()
  791. .to_string()
  792. .contains("Contents/MacOS")
  793. {
  794. return extract_path
  795. .parent()
  796. .map(PathBuf::from)
  797. .expect("Unable to find the extract path")
  798. .parent()
  799. .map(PathBuf::from)
  800. .expect("Unable to find the extract path");
  801. }
  802. // We should use APPIMAGE exposed env variable
  803. // This is where our APPIMAGE should sit and should be replaced
  804. #[cfg(target_os = "linux")]
  805. if let Some(app_image_path) = &env.appimage {
  806. return PathBuf::from(app_image_path);
  807. }
  808. extract_path
  809. }
  810. // Convert base64 to string and prevent failing
  811. fn base64_to_string(base64_string: &str) -> Result<String> {
  812. let decoded_string = &decode(base64_string)?;
  813. let result = from_utf8(decoded_string)
  814. .map_err(|_| Error::SignatureUtf8(base64_string.into()))?
  815. .to_string();
  816. Ok(result)
  817. }
  818. // Validate signature
  819. // need to be public because its been used
  820. // by our tests in the bundler
  821. //
  822. // NOTE: The buffer position is not reset.
  823. pub fn verify_signature<R>(
  824. archive_reader: &mut R,
  825. release_signature: &str,
  826. pub_key: &str,
  827. ) -> Result<bool>
  828. where
  829. R: Read,
  830. {
  831. // we need to convert the pub key
  832. let pub_key_decoded = base64_to_string(pub_key)?;
  833. let public_key = PublicKey::decode(&pub_key_decoded)?;
  834. let signature_base64_decoded = base64_to_string(release_signature)?;
  835. let signature = Signature::decode(&signature_base64_decoded)?;
  836. // read all bytes until EOF in the buffer
  837. let mut data = Vec::new();
  838. archive_reader.read_to_end(&mut data)?;
  839. // Validate signature or bail out
  840. public_key.verify(&data, &signature, true)?;
  841. Ok(true)
  842. }
  843. #[cfg(test)]
  844. mod test {
  845. use super::*;
  846. #[cfg(target_os = "macos")]
  847. use std::fs::File;
  848. macro_rules! block {
  849. ($e:expr) => {
  850. tokio_test::block_on($e)
  851. };
  852. }
  853. fn generate_sample_raw_json() -> String {
  854. r#"{
  855. "version": "v2.0.0",
  856. "notes": "Test version !",
  857. "pub_date": "2020-06-22T19:25:57Z",
  858. "platforms": {
  859. "darwin-aarch64": {
  860. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
  861. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.app.tar.gz"
  862. },
  863. "darwin-x86_64": {
  864. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJZVGdpKzJmRWZ0SkRvWS9TdFpqTU9xcm1mUmJSSG5OWVlwSklrWkN1SFpWbmh4SDlBcTU3SXpjbm0xMmRjRkphbkpVeGhGcTdrdzlrWGpGVWZQSWdzPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1MDU3CWZpbGU6L1VzZXJzL3J1bm5lci9ydW5uZXJzLzIuMjYzLjAvd29yay90YXVyaS90YXVyaS90YXVyaS9leGFtcGxlcy9jb21tdW5pY2F0aW9uL3NyYy10YXVyaS90YXJnZXQvZGVidWcvYnVuZGxlL29zeC9hcHAuYXBwLnRhci5negp4ZHFlUkJTVnpGUXdDdEhydTE5TGgvRlVPeVhjTnM5RHdmaGx3c0ZPWjZXWnFwVDRNWEFSbUJTZ1ZkU1IwckJGdmlwSzJPd00zZEZFN2hJOFUvL1FDZz09Cg==",
  865. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.app.tar.gz"
  866. },
  867. "linux-x86_64": {
  868. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOWZSM29hTFNmUEdXMHRoOC81WDFFVVFRaXdWOUdXUUdwT0NlMldqdXkyaWVieXpoUmdZeXBJaXRqSm1YVmczNXdRL1Brc0tHb1NOTzhrL1hadFcxdmdnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE3MzQzCWZpbGU6L2hvbWUvcnVubmVyL3dvcmsvdGF1cmkvdGF1cmkvdGF1cmkvZXhhbXBsZXMvY29tbXVuaWNhdGlvbi9zcmMtdGF1cmkvdGFyZ2V0L2RlYnVnL2J1bmRsZS9hcHBpbWFnZS9hcHAuQXBwSW1hZ2UudGFyLmd6CmRUTUM2bWxnbEtTbUhOZGtERUtaZnpUMG5qbVo5TGhtZWE1SFNWMk5OOENaVEZHcnAvVW0zc1A2ajJEbWZUbU0yalRHT0FYYjJNVTVHOHdTQlYwQkF3PT0K",
  869. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.AppImage.tar.gz"
  870. },
  871. "windows-x86_64": {
  872. "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K",
  873. "url": "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.x64.msi.zip"
  874. }
  875. }
  876. }"#.into()
  877. }
  878. fn generate_sample_platform_json(
  879. version: &str,
  880. public_signature: &str,
  881. download_url: &str,
  882. ) -> String {
  883. format!(
  884. r#"
  885. {{
  886. "name": "v{}",
  887. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  888. "pub_date": "2020-06-25T14:14:19Z",
  889. "signature": "{}",
  890. "url": "{}"
  891. }}
  892. "#,
  893. version, public_signature, download_url
  894. )
  895. }
  896. fn generate_sample_with_elevated_task_platform_json(
  897. version: &str,
  898. public_signature: &str,
  899. download_url: &str,
  900. with_elevated_task: bool,
  901. ) -> String {
  902. format!(
  903. r#"
  904. {{
  905. "name": "v{}",
  906. "notes": "This is the latest version! Once updated you shouldn't see this prompt.",
  907. "pub_date": "2020-06-25T14:14:19Z",
  908. "signature": "{}",
  909. "url": "{}",
  910. "with_elevated_task": {}
  911. }}
  912. "#,
  913. version, public_signature, download_url, with_elevated_task
  914. )
  915. }
  916. #[test]
  917. fn simple_http_updater() {
  918. let _m = mockito::mock("GET", "/")
  919. .with_status(200)
  920. .with_header("content-type", "application/json")
  921. .with_body(generate_sample_raw_json())
  922. .create();
  923. let app = crate::test::mock_app();
  924. let check_update = block!(builder(app.handle())
  925. .current_version("0.0.0".parse().unwrap())
  926. .url(mockito::server_url())
  927. .build());
  928. let updater = check_update.expect("Can't check update");
  929. assert!(updater.should_update);
  930. }
  931. #[test]
  932. fn simple_http_updater_raw_json() {
  933. let _m = mockito::mock("GET", "/")
  934. .with_status(200)
  935. .with_header("content-type", "application/json")
  936. .with_body(generate_sample_raw_json())
  937. .create();
  938. let app = crate::test::mock_app();
  939. let check_update = block!(builder(app.handle())
  940. .current_version("0.0.0".parse().unwrap())
  941. .url(mockito::server_url())
  942. .build());
  943. let updater = check_update.expect("Can't check update");
  944. assert!(updater.should_update);
  945. }
  946. #[test]
  947. fn simple_http_updater_raw_json_windows_x86_64() {
  948. let _m = mockito::mock("GET", "/")
  949. .with_status(200)
  950. .with_header("content-type", "application/json")
  951. .with_body(generate_sample_raw_json())
  952. .create();
  953. let app = crate::test::mock_app();
  954. let check_update = block!(builder(app.handle())
  955. .current_version("0.0.0".parse().unwrap())
  956. .target("windows-x86_64")
  957. .url(mockito::server_url())
  958. .build());
  959. let updater = check_update.expect("Can't check update");
  960. assert!(updater.should_update);
  961. assert_eq!(updater.version, "2.0.0");
  962. assert_eq!(updater.signature, "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUldUTE5QWWxkQnlZOVJHMWlvTzRUSlQzTHJOMm5waWpic0p0VVI2R0hUNGxhQVMxdzBPRndlbGpXQXJJakpTN0toRURtVzBkcm15R0VaNTJuS1lZRWdzMzZsWlNKUVAzZGdJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNTkyOTE1NTIzCWZpbGU6RDpcYVx0YXVyaVx0YXVyaVx0YXVyaVxleGFtcGxlc1xjb21tdW5pY2F0aW9uXHNyYy10YXVyaVx0YXJnZXRcZGVidWdcYXBwLng2NC5tc2kuemlwCitXa1lQc3A2MCs1KzEwZnVhOGxyZ2dGMlZqbjBaVUplWEltYUdyZ255eUF6eVF1dldWZzFObStaVEQ3QU1RS1lzcjhDVU4wWFovQ1p1QjJXbW1YZUJ3PT0K");
  963. assert_eq!(
  964. updater.download_url.to_string(),
  965. "https://github.com/tauri-apps/updater-test/releases/download/v1.0.0/app.x64.msi.zip"
  966. );
  967. }
  968. #[test]
  969. fn simple_http_updater_raw_json_uptodate() {
  970. let _m = mockito::mock("GET", "/")
  971. .with_status(200)
  972. .with_header("content-type", "application/json")
  973. .with_body(generate_sample_raw_json())
  974. .create();
  975. let app = crate::test::mock_app();
  976. let check_update = block!(builder(app.handle())
  977. .current_version("10.0.0".parse().unwrap())
  978. .url(mockito::server_url())
  979. .build());
  980. let updater = check_update.expect("Can't check update");
  981. assert!(!updater.should_update);
  982. }
  983. #[test]
  984. fn simple_http_updater_without_version() {
  985. let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0")
  986. .with_status(200)
  987. .with_header("content-type", "application/json")
  988. .with_body(generate_sample_platform_json(
  989. "2.0.0",
  990. "SampleTauriKey",
  991. "https://tauri.app",
  992. ))
  993. .create();
  994. let app = crate::test::mock_app();
  995. let check_update = block!(builder(app.handle())
  996. .current_version("1.0.0".parse().unwrap())
  997. .url(format!(
  998. "{}/darwin-aarch64/{{{{current_version}}}}",
  999. mockito::server_url()
  1000. ))
  1001. .build());
  1002. let updater = check_update.expect("Can't check update");
  1003. assert!(updater.should_update);
  1004. }
  1005. #[test]
  1006. fn simple_http_updater_percent_decode() {
  1007. let _m = mockito::mock("GET", "/darwin-aarch64/1.0.0")
  1008. .with_status(200)
  1009. .with_header("content-type", "application/json")
  1010. .with_body(generate_sample_platform_json(
  1011. "2.0.0",
  1012. "SampleTauriKey",
  1013. "https://tauri.app",
  1014. ))
  1015. .create();
  1016. let app = crate::test::mock_app();
  1017. let check_update = block!(builder(app.handle())
  1018. .current_version("1.0.0".parse().unwrap())
  1019. .url(
  1020. url::Url::parse(&format!(
  1021. "{}/darwin-aarch64/{{{{current_version}}}}",
  1022. mockito::server_url()
  1023. ))
  1024. .unwrap()
  1025. .to_string()
  1026. )
  1027. .build());
  1028. let updater = check_update.expect("Can't check update");
  1029. assert!(updater.should_update);
  1030. let app = crate::test::mock_app();
  1031. let check_update = block!(builder(app.handle())
  1032. .current_version("1.0.0".parse().unwrap())
  1033. .urls(&[url::Url::parse(&format!(
  1034. "{}/darwin-aarch64/{{{{current_version}}}}",
  1035. mockito::server_url()
  1036. ))
  1037. .unwrap()
  1038. .to_string()])
  1039. .build());
  1040. let updater = check_update.expect("Can't check update");
  1041. assert!(updater.should_update);
  1042. }
  1043. #[test]
  1044. fn simple_http_updater_with_elevated_task() {
  1045. let _m = mockito::mock("GET", "/windows-x86_64/1.0.0")
  1046. .with_status(200)
  1047. .with_header("content-type", "application/json")
  1048. .with_body(generate_sample_with_elevated_task_platform_json(
  1049. "2.0.0",
  1050. "SampleTauriKey",
  1051. "https://tauri.app",
  1052. true,
  1053. ))
  1054. .create();
  1055. let app = crate::test::mock_app();
  1056. let check_update = block!(builder(app.handle())
  1057. .current_version("1.0.0".parse().unwrap())
  1058. .url(format!(
  1059. "{}/windows-x86_64/{{{{current_version}}}}",
  1060. mockito::server_url()
  1061. ))
  1062. .build());
  1063. let updater = check_update.expect("Can't check update");
  1064. assert!(updater.should_update);
  1065. }
  1066. #[test]
  1067. fn http_updater_uptodate() {
  1068. let _m = mockito::mock("GET", "/darwin-aarch64/10.0.0")
  1069. .with_status(200)
  1070. .with_header("content-type", "application/json")
  1071. .with_body(generate_sample_platform_json(
  1072. "2.0.0",
  1073. "SampleTauriKey",
  1074. "https://tauri.app",
  1075. ))
  1076. .create();
  1077. let app = crate::test::mock_app();
  1078. let check_update = block!(builder(app.handle())
  1079. .current_version("10.0.0".parse().unwrap())
  1080. .url(format!(
  1081. "{}/darwin-aarch64/{{{{current_version}}}}",
  1082. mockito::server_url()
  1083. ))
  1084. .build());
  1085. let updater = check_update.expect("Can't check update");
  1086. assert!(!updater.should_update);
  1087. }
  1088. #[test]
  1089. fn http_updater_fallback_urls() {
  1090. let _m = mockito::mock("GET", "/")
  1091. .with_status(200)
  1092. .with_header("content-type", "application/json")
  1093. .with_body(generate_sample_raw_json())
  1094. .create();
  1095. let app = crate::test::mock_app();
  1096. let check_update = block!(builder(app.handle())
  1097. .url("http://badurl.www.tld/1".into())
  1098. .url(mockito::server_url())
  1099. .current_version("0.0.1".parse().unwrap())
  1100. .build());
  1101. let updater = check_update.expect("Can't check remote update");
  1102. assert!(updater.should_update);
  1103. }
  1104. #[test]
  1105. fn http_updater_fallback_urls_withs_array() {
  1106. let _m = mockito::mock("GET", "/")
  1107. .with_status(200)
  1108. .with_header("content-type", "application/json")
  1109. .with_body(generate_sample_raw_json())
  1110. .create();
  1111. let app = crate::test::mock_app();
  1112. let check_update = block!(builder(app.handle())
  1113. .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),])
  1114. .current_version("0.0.1".parse().unwrap())
  1115. .build());
  1116. let updater = check_update.expect("Can't check remote update");
  1117. assert!(updater.should_update);
  1118. }
  1119. #[test]
  1120. fn http_updater_invalid_remote_data() {
  1121. let invalid_signature = r#"{
  1122. "version": "v0.0.3",
  1123. "notes": "Blablaa",
  1124. "pub_date": "2020-02-20T15:41:00Z",
  1125. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1126. "signature": true
  1127. }"#;
  1128. let invalid_version = r#"{
  1129. "version": 5,
  1130. "notes": "Blablaa",
  1131. "pub_date": "2020-02-20T15:41:00Z",
  1132. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1133. "signature": "x"
  1134. }"#;
  1135. let invalid_name = r#"{
  1136. "name": false,
  1137. "notes": "Blablaa",
  1138. "pub_date": "2020-02-20T15:41:00Z",
  1139. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1140. "signature": "x"
  1141. }"#;
  1142. let invalid_date = r#"{
  1143. "version": "1.0.0",
  1144. "notes": "Blablaa",
  1145. "pub_date": 345645646,
  1146. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1147. "signature": "x"
  1148. }"#;
  1149. let invalid_notes = r#"{
  1150. "version": "v0.0.3",
  1151. "notes": ["bla", "bla"],
  1152. "pub_date": "2020-02-20T15:41:00Z",
  1153. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1154. "signature": "x"
  1155. }"#;
  1156. let invalid_url = 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", "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"],
  1161. "signature": "x"
  1162. }"#;
  1163. let invalid_platform_signature = r#"{
  1164. "version": "v0.0.3",
  1165. "notes": "Blablaa",
  1166. "pub_date": "2020-02-20T15:41:00Z",
  1167. "platforms": {
  1168. "test-target": {
  1169. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1170. "signature": {
  1171. "test-target": "x"
  1172. }
  1173. }
  1174. }
  1175. }"#;
  1176. let invalid_platform_url = r#"{
  1177. "version": "v0.0.3",
  1178. "notes": "Blablaa",
  1179. "pub_date": "2020-02-20T15:41:00Z",
  1180. "platforms": {
  1181. "test-target": {
  1182. "url": {
  1183. "first": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"
  1184. }
  1185. "signature": "x"
  1186. }
  1187. }
  1188. }"#;
  1189. let test_cases = [
  1190. (
  1191. invalid_signature,
  1192. Box::new(|e| matches!(e, Error::InvalidResponseType("signature", "string", _)))
  1193. as Box<dyn FnOnce(Error) -> bool>,
  1194. ),
  1195. (
  1196. invalid_version,
  1197. Box::new(|e| matches!(e, Error::InvalidResponseType("version", "string", _)))
  1198. as Box<dyn FnOnce(Error) -> bool>,
  1199. ),
  1200. (
  1201. invalid_name,
  1202. Box::new(|e| matches!(e, Error::InvalidResponseType("name", "string", _)))
  1203. as Box<dyn FnOnce(Error) -> bool>,
  1204. ),
  1205. (
  1206. invalid_date,
  1207. Box::new(|e| matches!(e, Error::InvalidResponseType("pub_date", "string", _)))
  1208. as Box<dyn FnOnce(Error) -> bool>,
  1209. ),
  1210. (
  1211. invalid_notes,
  1212. Box::new(|e| matches!(e, Error::InvalidResponseType("notes", "string", _)))
  1213. as Box<dyn FnOnce(Error) -> bool>,
  1214. ),
  1215. (
  1216. invalid_url,
  1217. Box::new(|e| matches!(e, Error::InvalidResponseType("url", "string", _)))
  1218. as Box<dyn FnOnce(Error) -> bool>,
  1219. ),
  1220. (
  1221. invalid_platform_signature,
  1222. Box::new(|e| matches!(e, Error::InvalidResponseType("signature", "string", _)))
  1223. as Box<dyn FnOnce(Error) -> bool>,
  1224. ),
  1225. (
  1226. invalid_platform_url,
  1227. Box::new(|e| matches!(e, Error::InvalidResponseType("url", "string", _)))
  1228. as Box<dyn FnOnce(Error) -> bool>,
  1229. ),
  1230. ];
  1231. for (response, validator) in test_cases {
  1232. let _m = mockito::mock("GET", "/")
  1233. .with_status(200)
  1234. .with_header("content-type", "application/json")
  1235. .with_body(response)
  1236. .create();
  1237. let app = crate::test::mock_app();
  1238. let check_update = block!(builder(app.handle())
  1239. .url(mockito::server_url())
  1240. .current_version("0.0.1".parse().unwrap())
  1241. .target("test-target")
  1242. .build());
  1243. if let Err(e) = check_update {
  1244. validator(e);
  1245. } else {
  1246. panic!("unexpected Ok response");
  1247. }
  1248. }
  1249. }
  1250. #[test]
  1251. fn http_updater_missing_remote_data() {
  1252. let missing_signature = r#"{
  1253. "version": "v0.0.3",
  1254. "notes": "Blablaa",
  1255. "pub_date": "2020-02-20T15:41:00Z",
  1256. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"
  1257. }"#;
  1258. let missing_version = r#"{
  1259. "notes": "Blablaa",
  1260. "pub_date": "2020-02-20T15:41:00Z",
  1261. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1262. "signature": "x"
  1263. }"#;
  1264. let missing_url = r#"{
  1265. "version": "v0.0.3",
  1266. "notes": "Blablaa",
  1267. "pub_date": "2020-02-20T15:41:00Z",
  1268. "signature": "x"
  1269. }"#;
  1270. let missing_target = r#"{
  1271. "version": "v0.0.3",
  1272. "notes": "Blablaa",
  1273. "pub_date": "2020-02-20T15:41:00Z",
  1274. "platforms": {
  1275. "unknown-target": {
  1276. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz",
  1277. "signature": "x"
  1278. }
  1279. }
  1280. }"#;
  1281. let missing_platform_signature = r#"{
  1282. "version": "v0.0.3",
  1283. "notes": "Blablaa",
  1284. "pub_date": "2020-02-20T15:41:00Z",
  1285. "platforms": {
  1286. "test-target": {
  1287. "url": "https://github.com/tauri-apps/updater-test/releases/download/v0.0.1/update3.tar.gz"
  1288. }
  1289. }
  1290. }"#;
  1291. let missing_platform_url = r#"{
  1292. "version": "v0.0.3",
  1293. "notes": "Blablaa",
  1294. "pub_date": "2020-02-20T15:41:00Z",
  1295. "platforms": {
  1296. "test-target": {
  1297. "signature": "x"
  1298. }
  1299. }
  1300. }"#;
  1301. fn missing_field_error(field: &str) -> String {
  1302. format!("the `{}` field was not set on the updater response", field)
  1303. }
  1304. let test_cases = [
  1305. (missing_signature, missing_field_error("signature")),
  1306. (missing_version, "missing field `version`".to_string()),
  1307. (missing_url, missing_field_error("url")),
  1308. (
  1309. missing_target,
  1310. Error::TargetNotFound("test-target".into()).to_string(),
  1311. ),
  1312. (
  1313. missing_platform_signature,
  1314. "missing field `signature`".to_string(),
  1315. ),
  1316. (missing_platform_url, "missing field `url`".to_string()),
  1317. ];
  1318. for (response, error) in test_cases {
  1319. let _m = mockito::mock("GET", "/")
  1320. .with_status(200)
  1321. .with_header("content-type", "application/json")
  1322. .with_body(response)
  1323. .create();
  1324. let app = crate::test::mock_app();
  1325. let check_update = block!(builder(app.handle())
  1326. .url(mockito::server_url())
  1327. .current_version("0.0.1".parse().unwrap())
  1328. .target("test-target")
  1329. .build());
  1330. if let Err(e) = check_update {
  1331. println!("ERROR: {}, expected: {}", e, error);
  1332. assert!(e.to_string().contains(&error));
  1333. } else {
  1334. panic!("unexpected Ok response");
  1335. }
  1336. }
  1337. }
  1338. // run complete process on mac only for now as we don't have
  1339. // server (api) that we can use to test
  1340. #[test]
  1341. #[cfg(target_os = "macos")]
  1342. fn http_updater_complete_process() {
  1343. #[cfg(target_os = "macos")]
  1344. let archive_file = "archive.macos.tar.gz";
  1345. #[cfg(target_os = "linux")]
  1346. let archive_file = "archive.linux.tar.gz";
  1347. #[cfg(target_os = "windows")]
  1348. let archive_file = "archive.windows.zip";
  1349. let good_archive_url = format!("{}/{}", mockito::server_url(), archive_file);
  1350. let mut signature_file = File::open(format!(
  1351. "./test/updater/fixture/archives/{}.sig",
  1352. archive_file
  1353. ))
  1354. .expect("Unable to open signature");
  1355. let mut signature = String::new();
  1356. signature_file
  1357. .read_to_string(&mut signature)
  1358. .expect("Unable to read signature as string");
  1359. let mut pubkey_file = File::open("./test/updater/fixture/good_signature/update.key.pub")
  1360. .expect("Unable to open pubkey");
  1361. let mut pubkey = String::new();
  1362. pubkey_file
  1363. .read_to_string(&mut pubkey)
  1364. .expect("Unable to read signature as string");
  1365. // add sample file
  1366. let _m = mockito::mock("GET", format!("/{}", archive_file).as_str())
  1367. .with_status(200)
  1368. .with_header("content-type", "application/octet-stream")
  1369. .with_body_from_file(format!("./test/updater/fixture/archives/{}", archive_file))
  1370. .create();
  1371. // sample mock for update file
  1372. let _m = mockito::mock("GET", "/")
  1373. .with_status(200)
  1374. .with_header("content-type", "application/json")
  1375. .with_body(generate_sample_platform_json(
  1376. "2.0.1",
  1377. signature.as_ref(),
  1378. good_archive_url.as_ref(),
  1379. ))
  1380. .create();
  1381. // Build a tmpdir so we can test our extraction inside
  1382. // We dont want to overwrite our current executable or the directory
  1383. // Otherwise tests are failing...
  1384. let executable_path = current_exe().expect("Can't extract executable path");
  1385. let parent_path = executable_path
  1386. .parent()
  1387. .expect("Can't find the parent path");
  1388. let tmp_dir = tempfile::Builder::new()
  1389. .prefix("tauri_updater_test")
  1390. .tempdir_in(parent_path);
  1391. assert!(tmp_dir.is_ok());
  1392. let tmp_dir_unwrap = tmp_dir.expect("Can't find tmp_dir");
  1393. let tmp_dir_path = tmp_dir_unwrap.path();
  1394. #[cfg(target_os = "linux")]
  1395. let my_executable = &tmp_dir_path.join("updater-example_0.1.0_amd64.AppImage");
  1396. #[cfg(target_os = "macos")]
  1397. let my_executable = &tmp_dir_path.join("my_app");
  1398. #[cfg(target_os = "windows")]
  1399. let my_executable = &tmp_dir_path.join("my_app.exe");
  1400. // configure the updater
  1401. let app = crate::test::mock_app();
  1402. let check_update = block!(builder(app.handle())
  1403. .url(mockito::server_url())
  1404. // It should represent the executable path, that's why we add my_app.exe in our
  1405. // test path -- in production you shouldn't have to provide it
  1406. .executable_path(my_executable)
  1407. // make sure we force an update
  1408. .current_version("1.0.0".parse().unwrap())
  1409. .build());
  1410. #[cfg(target_os = "linux")]
  1411. {
  1412. env::set_var("APPIMAGE", my_executable);
  1413. }
  1414. // unwrap our results
  1415. let updater = check_update.expect("Can't check remote update");
  1416. // make sure we need to update
  1417. assert!(updater.should_update);
  1418. // make sure we can read announced version
  1419. assert_eq!(updater.version, "2.0.1");
  1420. // download, install and validate signature
  1421. let install_process = block!(updater.download_and_install(pubkey, |_, _| (), || ()));
  1422. assert!(install_process.is_ok());
  1423. // make sure the extraction went well (it should have skipped the main app.app folder)
  1424. // as we can't extract in /Applications directly
  1425. #[cfg(target_os = "macos")]
  1426. let bin_file = tmp_dir_path.join("Contents").join("MacOS").join("app");
  1427. #[cfg(target_os = "linux")]
  1428. // linux should extract at same place as the executable path
  1429. let bin_file = my_executable;
  1430. #[cfg(target_os = "windows")]
  1431. let bin_file = tmp_dir_path.join("with").join("long").join("path.json");
  1432. assert!(bin_file.exists());
  1433. }
  1434. }