From 28423e92aa020bc486d819fe84a82a3d98b4cedb Mon Sep 17 00:00:00 2001 From: Reactor Scram Date: Mon, 29 Apr 2024 12:21:18 -0500 Subject: [PATCH] chore(gui-client): use new download links (#4754) ae67064121 works on the live site. However if you click the notification while the tunnel is coming up, there's a chance that the download will fail because Firezone isn't fully up yet. Oops. That will probably only affect us since we have github.com as a resource. If real customers are okay with their Firezone updates coming through normal Internet it'll probably be fine. --------- Signed-off-by: Reactor Scram Co-authored-by: Jamil Bou Kheir --- .../src-tauri/src/client/debug_commands.rs | 5 +- rust/gui-client/src-tauri/src/client/gui.rs | 30 +-- .../src-tauri/src/client/updates.rs | 187 +++++------------- 3 files changed, 65 insertions(+), 157 deletions(-) diff --git a/rust/gui-client/src-tauri/src/client/debug_commands.rs b/rust/gui-client/src-tauri/src/client/debug_commands.rs index 856194426..50cff36cf 100644 --- a/rust/gui-client/src-tauri/src/client/debug_commands.rs +++ b/rust/gui-client/src-tauri/src/client/debug_commands.rs @@ -29,9 +29,8 @@ fn check_for_updates() -> Result<()> { client::logging::debug_command_setup()?; let rt = tokio::runtime::Runtime::new().unwrap(); - let release = rt.block_on(client::updates::check())?; - tracing::info!("{:#?}", serde_json::to_string(&release)); - tracing::info!("{:?}", release.download_url()); + let version = rt.block_on(client::updates::check())?; + tracing::info!("{:?}", version); Ok(()) } diff --git a/rust/gui-client/src-tauri/src/client/gui.rs b/rust/gui-client/src-tauri/src/client/gui.rs index 7ccf513c1..5038ce404 100644 --- a/rust/gui-client/src-tauri/src/client/gui.rs +++ b/rust/gui-client/src-tauri/src/client/gui.rs @@ -412,22 +412,16 @@ async fn check_for_updates(ctlr_tx: CtlrTx, always_show_update_notification: boo let release = client::updates::check() .await .context("Error in client::updates::check")?; - let Some(download_url) = release.download_url() else { - tracing::warn!("No installer for this OS/arch online"); - return Ok(()); - }; + let latest_version = release.version.clone(); let our_version = client::updates::current_version()?; - if always_show_update_notification || (our_version < release.tag_name) { - tracing::info!(?our_version, ?release.tag_name, "There is a new release"); + if always_show_update_notification || (our_version < latest_version) { + tracing::info!(?our_version, ?latest_version, "There is a new release"); // We don't necessarily need to route through the Controller here, but if we // want a persistent "Click here to download the new MSI" button, this would allow that. ctlr_tx - .send(ControllerRequest::UpdateAvailable { - download_url: download_url.clone(), - version_to_download: release.tag_name, - }) + .send(ControllerRequest::UpdateAvailable(release)) .await .context("Error while sending UpdateAvailable to Controller")?; return Ok(()); @@ -435,7 +429,7 @@ async fn check_for_updates(ctlr_tx: CtlrTx, always_show_update_notification: boo tracing::info!( ?our_version, - ?release.tag_name, + ?latest_version, "Our release is newer than, or the same as, the latest" ); Ok(()) @@ -489,10 +483,7 @@ pub(crate) enum ControllerRequest { SignIn, SystemTrayMenu(TrayMenuEvent), TunnelReady, - UpdateAvailable { - download_url: Url, - version_to_download: semver::Version, - }, + UpdateAvailable(crate::client::updates::Release), UpdateNotificationClicked(Url), } @@ -678,11 +669,8 @@ impl Controller { self.tunnel_ready = true; self.refresh_system_tray_menu()?; } - Req::UpdateAvailable { - download_url, - version_to_download, - } => { - let title = format!("Firezone {} available for download", version_to_download); + Req::UpdateAvailable(release) => { + let title = format!("Firezone {} available for download", release.version); // We don't need to route through the controller here either, we could // use the `open` crate directly instead of Tauri's wrapper @@ -691,7 +679,7 @@ impl Controller { &title, "Click here to download the new version.", self.ctlr_tx.clone(), - Req::UpdateNotificationClicked(download_url), + Req::UpdateNotificationClicked(release.download_url), )?; } Req::UpdateNotificationClicked(download_url) => { diff --git a/rust/gui-client/src-tauri/src/client/updates.rs b/rust/gui-client/src-tauri/src/client/updates.rs index e14e71073..c2ee5e118 100644 --- a/rust/gui-client/src-tauri/src/client/updates.rs +++ b/rust/gui-client/src-tauri/src/client/updates.rs @@ -1,7 +1,7 @@ //! Module to check the Github repo for new releases use crate::client::about::get_cargo_version; -use anyhow::Result; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use url::Url; @@ -9,164 +9,85 @@ use url::Url; /// GUI-friendly release struct /// /// Serialize is derived for debugging -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub(crate) struct Release { - /// All assets in a given release - assets: Vec, - /// Git tag name / Cargo version name - /// - /// e.g. 1.0.1 - pub tag_name: semver::Version, + pub download_url: url::Url, + pub version: semver::Version, } -#[derive(Deserialize, Serialize)] -struct Asset { - browser_download_url: Url, - /// Name of the asset, e.g. `firezone-client-gui-windows-x86_64.msi` - name: String, -} - -impl Release { - /// Download URL for current OS and arch - pub fn download_url(&self) -> Option<&Url> { - self.download_url_for(std::env::consts::ARCH, std::env::consts::OS) - } - - /// Download URL for the first asset that matches the given arch, OS, and package type - fn download_url_for(&self, arch: &str, os: &str) -> Option<&Url> { - let package = match os { - "linux" => "deb", - "macos" => "dmg", // Unused in practice - "windows" => "msi", - _ => panic!("Don't know what package this OS uses"), - }; - - let prefix = format!("firezone-client-gui-{os}_"); - let suffix = format!("_{arch}.{package}"); - - let mut iter = self - .assets - .iter() - .filter(|x| x.name.starts_with(&prefix) && x.name.ends_with(&suffix)); - iter.next().map(|asset| &asset.browser_download_url) - } -} - -#[derive(Debug, thiserror::Error)] -pub(crate) enum Error { - #[error(transparent)] - JsonParse(#[from] serde_json::Error), - #[error("Our own semver in the exe is invalid, this should be impossible")] - OurVersionIsInvalid(semver::Error), - #[error(transparent)] - Request(#[from] reqwest::Error), -} - -const LATEST_RELEASE_API_URL: &str = - "https://api.github.com/repos/firezone/firezone/releases/latest"; - -/// -const GITHUB_API_VERSION: &str = "2022-11-28"; - /// Returns the latest release, even if ours is already newer pub(crate) async fn check() -> Result { - let client = reqwest::Client::builder().build()?; + // Don't follow any redirects, just tell us what the Firezone site says the URL is + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .build()?; let arch = std::env::consts::ARCH; let os = std::env::consts::OS; - // + // We used to send this to Github, couldn't hurt to send it to our own site, too let user_agent = format!("Firezone Client/{:?} ({os}; {arch})", current_version()); - // Reqwest follows up to 10 redirects by default - // https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.redirect - // https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#follow-redirects + let mut latest_url = url::Url::parse("https://www.firezone.dev") + .context("Impossible: Hard-coded URL should always be parsable")?; + latest_url.set_path(&format!("/dl/firezone-client-gui-{os}/latest/{arch}")); + let response = client - .get(LATEST_RELEASE_API_URL) + .head(latest_url) .header("User-Agent", user_agent) - .header("X-GitHub-Api-Version", GITHUB_API_VERSION) .send() .await?; let status = response.status(); - if status != reqwest::StatusCode::OK { + if status != reqwest::StatusCode::TEMPORARY_REDIRECT { anyhow::bail!("HTTP status: {status}"); } + let download_url = response + .headers() + .get(reqwest::header::LOCATION) + .context("this URL should always have a redirect")? + .to_str()?; + tracing::debug!(?download_url); + let download_url = Url::parse(download_url)?; + let version = parse_version_from_url(&download_url)?; + Ok(Release { + download_url, + version, + }) +} - let response = response.text().await?; - Ok(serde_json::from_str(&response)?) +#[allow(clippy::print_stderr)] +fn parse_version_from_url(url: &Url) -> Result { + tracing::debug!(?url); + let filename = url + .path_segments() + .context("URL must have a path")? + .last() + .context("URL path must have a last segment")?; + let version_str = filename + .split('_') + .nth(1) + .context("Filename must have 3 parts separated by underscores")?; + Ok(semver::Version::parse(version_str)?) } // TODO: DRY with about.rs -pub(crate) fn current_version() -> Result { - semver::Version::from_str(&get_cargo_version()).map_err(Error::OurVersionIsInvalid) +pub(crate) fn current_version() -> Result { + semver::Version::from_str(&get_cargo_version()).context("Our version is invalid") } #[cfg(test)] mod tests { - use std::str::FromStr; - #[test] - fn new_format() { - let s = r#" - { - "tag_name": "1.0.0-pre.14", - "assets": [ - { - "name": "firezone-client-gui-linux_1.0.0-pre.14_aarch64.deb", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-gui-linux_1.0.0-pre.14_aarch64.deb" - }, - { - "name": "firezone-client-gui-linux_1.0.0-pre.14_x86_64.deb", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-gui-linux_1.0.0-pre.14_x86_64.deb" - }, - { - "name": "firezone-client-gui-windows_1.0.0-pre.14_aarch64.msi", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-gui-windows_1.0.0-pre.14_aarch64.msi" - }, - { - "name": "firezone-client-gui-windows_1.0.0-pre.14_x86_64.msi", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-gui-windows_1.0.0-pre.14_x86_64.msi" - }, - - { - "name": "firezone-client-headless-linux_1.0.0-pre.14_aarch64.deb", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-headless-linux_1.0.0-pre.14_aarch64.deb" - }, - { - "name": "firezone-client-headless-linux_1.0.0-pre.14_x86_64.deb", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-headless-linux_1.0.0-pre.14_x86_64.deb" - }, - { - "name": "firezone-client-headless-windows_1.0.0-pre.14_aarch64.msi", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-headless-windows_1.0.0-pre.14_aarch64.msi" - }, - { - "name": "firezone-client-headless-windows_1.0.0-pre.14_x86_64.msi", - "browser_download_url": "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-headless-windows_1.0.0-pre.14_x86_64.msi" - } - ] - }"#; - - let release: super::Release = serde_json::from_str(s).unwrap(); - let expected_url = "https://github.com/firezone/firezone/releases/download/1.0.0-pre.14/firezone-client-gui-windows_1.0.0-pre.14_x86_64.msi"; - assert_eq!( - release - .download_url_for("x86_64", "windows") - .unwrap() - .to_string(), - expected_url - ); - assert_eq!(release.tag_name.to_string(), "1.0.0-pre.14"); - - assert!( - semver::Version::from_str("1.0.0").unwrap() - > semver::Version::from_str("1.0.0-pre.14").unwrap() - ); - assert!( - semver::Version::from_str("1.0.0-pre.14").unwrap() - > semver::Version::from_str("0.7.0").unwrap() - ); - - assert!(super::current_version().is_ok()); + fn parse_version_from_url() { + for (input, expected) in [ + ("https://www.github.com/firezone/firezone/releases/download/1.0.0/firezone-client-gui-windows_1.0.0_x86_64.msi", Some((1, 0, 0))), + ("https://www.github.com/firezone/firezone/releases/download/1.0.1/firezone-client-gui-linux_1.0.1_x86_64.deb", Some((1, 0, 1))), + ("https://www.github.com/firezone/firezone/releases/download/1.0.1/firezone-client-gui-linux_x86_64.deb", None), + ] { + let input = url::Url::parse(input).unwrap(); + let expected = expected.map(|(a, b, c)| semver::Version::new(a, b, c)); + let actual = super::parse_version_from_url(&input).ok(); + assert_eq!(actual, expected); + } } #[test]