core.rs 34 KB

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