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 <ReactorScram@users.noreply.github.com>
Co-authored-by: Jamil Bou Kheir <jamilbk@users.noreply.github.com>
This commit is contained in:
Reactor Scram
2024-04-29 12:21:18 -05:00
committed by GitHub
parent 509fdd0d8f
commit 28423e92aa
3 changed files with 65 additions and 157 deletions

View File

@@ -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(())
}

View File

@@ -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) => {

View File

@@ -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<Asset>,
/// 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";
/// <https://docs.github.com/en/rest/about-the-rest-api/api-versions?apiVersion=2022-11-28>
const GITHUB_API_VERSION: &str = "2022-11-28";
/// Returns the latest release, even if ours is already newer
pub(crate) async fn check() -> Result<Release> {
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;
// <https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#user-agent-required>
// 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<semver::Version> {
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, Error> {
semver::Version::from_str(&get_cargo_version()).map_err(Error::OurVersionIsInvalid)
pub(crate) fn current_version() -> Result<semver::Version> {
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]