summaryrefslogtreecommitdiffhomepage
path: root/mullvad-api/src
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-10-14 17:18:29 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-10-23 10:22:18 +0200
commit25c19edab4d18f8b80e6200963b6d5b33438440d (patch)
treed605888051ae1a028bbeeb9d89ad2adff272a3e7 /mullvad-api/src
parentb8f2c2574ebed40973a990047d54454c5963d7f4 (diff)
downloadmullvadvpn-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.rs115
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(&params, 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.