core.rs 61 KB

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