diff options
| author | David Lönnhager <david.l@mullvad.net> | 2025-10-14 17:18:29 +0200 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-10-23 10:22:18 +0200 |
| commit | 25c19edab4d18f8b80e6200963b6d5b33438440d (patch) | |
| tree | d605888051ae1a028bbeeb9d89ad2adff272a3e7 /mullvad-api/src | |
| parent | b8f2c2574ebed40973a990047d54454c5963d7f4 (diff) | |
| download | mullvadvpn-25c19edab4d18f8b80e6200963b6d5b33438440d.tar.xz mullvadvpn-25c19edab4d18f8b80e6200963b6d5b33438440d.zip | |
Update version check
It now makes an API call whenever manually triggered as well as once
per hour, but only includes platform headers if 24 hours have passed
Co-authored-by: Sebastian Holmin <sebastian.holmin@mullvad.net>
Diffstat (limited to 'mullvad-api/src')
| -rw-r--r-- | mullvad-api/src/version.rs | 115 |
1 files changed, 90 insertions, 25 deletions
diff --git a/mullvad-api/src/version.rs b/mullvad-api/src/version.rs index 3f3c4a4e7e..ff1ec63788 100644 --- a/mullvad-api/src/version.rs +++ b/mullvad-api/src/version.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use std::sync::Arc; use http::StatusCode; +use http::header; use mullvad_update::version::{VersionInfo, VersionParameters, is_version_supported}; type AppVersion = String; @@ -15,12 +16,32 @@ pub struct AppVersionProxy { handle: super::rest::MullvadRestHandle, } -#[derive(serde::Deserialize, Debug)] +#[derive(Debug)] pub struct AppVersionResponse { - pub supported: bool, - pub latest: AppVersion, - pub latest_stable: Option<AppVersion>, - pub latest_beta: Option<AppVersion>, + response: AppVersionResponseRaw, + pub etag: Option<String>, +} + +#[derive(serde::Deserialize, Debug)] +struct AppVersionResponseRaw { + supported: bool, + latest: AppVersion, + latest_stable: Option<AppVersion>, + latest_beta: Option<AppVersion>, +} + +impl AppVersionResponse { + pub const fn supported(&self) -> bool { + self.response.supported + } + + pub const fn latest_stable(&self) -> Option<&AppVersion> { + self.response.latest_stable.as_ref() + } + + pub const fn latest_beta(&self) -> Option<&AppVersion> { + self.response.latest_beta.as_ref() + } } /// Reply from `/app/releases/<platform>.json` endpoint @@ -32,6 +53,8 @@ pub struct AppVersionResponse2 { pub metadata_version: usize, /// Whether or not the current app version (mullvad_version::VERSION) is supported. pub current_version_supported: bool, + /// ETag for the response + pub etag: Option<String>, } impl AppVersionProxy { @@ -46,47 +69,75 @@ impl AppVersionProxy { &self, app_version: AppVersion, platform: &str, - platform_version: String, - ) -> impl Future<Output = Result<AppVersionResponse, rest::Error>> + use<> { + platform_version: Option<String>, + etag: Option<String>, + ) -> impl Future<Output = Result<Option<AppVersionResponse>, rest::Error>> + use<> { let service = self.handle.service.clone(); let path = format!("{APP_URL_PREFIX}/releases/{platform}/{app_version}"); let request = self.handle.factory.get(&path); async move { - let request = request? - .expected_status(&[StatusCode::OK]) - .header("M-Platform-Version", &platform_version)?; + let mut request = request?.expected_status(&[StatusCode::NOT_MODIFIED, StatusCode::OK]); + if let Some(platform_version) = platform_version { + request = request.header("M-Platform-Version", &platform_version)?; + } + if let Some(ref tag) = etag { + request = request.header(header::IF_NONE_MATCH, tag)?; + } let response = service.request(request).await?; - response.deserialize().await + if etag.is_some() && response.status() == StatusCode::NOT_MODIFIED { + return Ok(None); + } + let etag = Self::extract_etag(&response); + let deserialized: AppVersionResponseRaw = response.deserialize().await?; + let _ = deserialized.latest; // we do not use this + + Ok(Some(AppVersionResponse { + response: deserialized, + etag, + })) } } /// Get versions from `/app/releases/<platform>.json` + /// + /// This returns `None` if the server responds with 304 (version is same as etag). pub fn version_check_2( &self, platform: &str, architecture: mullvad_update::format::Architecture, rollout: f32, lowest_metadata_version: usize, - platform_version: String, - ) -> impl Future<Output = Result<AppVersionResponse2, rest::Error>> + use<> { + platform_version: Option<String>, + etag: Option<String>, + ) -> impl Future<Output = Result<Option<AppVersionResponse2>, rest::Error>> + use<> { let service = self.handle.service.clone(); let path = format!("app/releases/{platform}.json"); let request = self.handle.factory.get(&path); async move { - let request = request? - .expected_status(&[StatusCode::OK]) - .header( - "M-App-Version", - &sanitize_header_value(mullvad_version::VERSION), - )? - .header( - "M-Platform-Version", - &sanitize_header_value(&platform_version), - )?; + let mut request = request?.expected_status(&[StatusCode::NOT_MODIFIED, StatusCode::OK]); + if let Some(platform_version) = platform_version { + request = request + .header( + "M-App-Version", + &sanitize_header_value(mullvad_version::VERSION), + )? + .header( + "M-Platform-Version", + &sanitize_header_value(&platform_version), + )?; + } + if let Some(ref tag) = etag { + request = request.header(header::IF_NONE_MATCH, tag)?; + } let response = service.request(request).await?; + if etag.is_some() && response.status() == StatusCode::NOT_MODIFIED { + return Ok(None); + } + let etag = Self::extract_etag(&response); + let bytes = response.body_with_max_size(Self::SIZE_LIMIT).await?; let response = mullvad_update::format::SignedResponse::deserialize_and_verify( @@ -108,15 +159,29 @@ impl AppVersionProxy { let current_version_supported = is_version_supported(current_version, &response.signed); let metadata_version = response.signed.metadata_version; - Ok(AppVersionResponse2 { + Ok(Some(AppVersionResponse2 { version_info: VersionInfo::try_from_response(¶ms, response.signed) .map_err(Arc::new) .map_err(rest::Error::FetchVersions)?, metadata_version, current_version_supported, - }) + etag, + })) } } + + pub fn extract_etag(response: &rest::Response<hyper::body::Incoming>) -> Option<String> { + response + .headers() + .get(header::ETAG) + .and_then(|tag| match tag.to_str() { + Ok(tag) => Some(tag.to_string()), + Err(_) => { + log::error!("Ignoring invalid tag from server: {:?}", tag.as_bytes()); + None + } + }) + } } // This function makes a string conform to the allowed characters and length of header values. |
