diff options
| author | David Lönnhager <david.l@mullvad.net> | 2020-11-23 18:41:36 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2020-11-23 18:41:36 +0100 |
| commit | f6cc8319c978ef4a2caf23c626e39ebcdb21529f (patch) | |
| tree | 0960bd63af11c93f3ebb09da2c9207a3f6bfa8a1 | |
| parent | 79f67878b68592843720234e814f469d06e1d779 (diff) | |
| parent | 63f258cf70a8ded3bbf8b8427931d1cbf71c133f (diff) | |
| download | mullvadvpn-f6cc8319c978ef4a2caf23c626e39ebcdb21529f.tar.xz mullvadvpn-f6cc8319c978ef4a2caf23c626e39ebcdb21529f.zip | |
Merge branch 'fix-downgrade'
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | dist-assets/windows/installer.nsh | 45 | ||||
| -rw-r--r-- | mullvad-daemon/src/version_check.rs | 132 | ||||
| -rw-r--r-- | mullvad-setup/Cargo.toml | 2 | ||||
| -rw-r--r-- | mullvad-setup/src/main.rs | 52 | ||||
| -rw-r--r-- | mullvad-types/src/version.rs | 173 |
7 files changed, 282 insertions, 127 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index acf0b093a6..96beef42f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,9 @@ Line wrap the file at 100 chars. Th - Try to connect even if VPN permission is denied, so that the app shows an error message saying that the VPN permission was denied. +#### Windows +- Fully uninstall the app when it is downgraded. Traffic is not blocked. + #### Linux - Make route monitor ignore loopback routes. - Increase NetworkManager device readiness timeout to 15 seconds. diff --git a/Cargo.lock b/Cargo.lock index a602a316f2..ae329be543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,10 +1404,12 @@ dependencies = [ "clap", "env_logger", "err-derive", + "lazy_static", "mullvad-daemon", "mullvad-management-interface", "mullvad-paths", "mullvad-rpc", + "mullvad-types", "talpid-core", "talpid-types", "tokio", diff --git a/dist-assets/windows/installer.nsh b/dist-assets/windows/installer.nsh index 93f62f9703..80c4ba998b 100644 --- a/dist-assets/windows/installer.nsh +++ b/dist-assets/windows/installer.nsh @@ -35,6 +35,11 @@ !define ERROR_SERVICE_MARKED_FOR_DELETE 1072 !define ERROR_SERVICE_DEPENDENCY_DELETED 1075 +# mullvad-setup status codes +!define MVSETUP_OK 0 +!define MVSETUP_ERROR 1 +!define MVSETUP_VERSION_NOT_OLDER 2 + # Override electron-builder generated application settings key. # electron-builder uses a GUID here rather than the application name. !define INSTALL_REGISTRY_KEY "Software\${PRODUCT_NAME}" @@ -704,6 +709,8 @@ Pop $0 Pop $0 + WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${APP_GUID}" "NewVersion" "${VERSION}" + Pop $0 !macroend @@ -821,23 +828,46 @@ # Check command line arguments Var /GLOBAL FullUninstall + Var /GLOBAL Silent + Var /GLOBAL NewVersion ${GetParameters} $0 ${GetOptions} $0 "/S" $1 ${If} ${Errors} - Push 1 + Push 0 log::Initialize ${LOG_VOID} ${Else} - Push 0 + Push 1 log::Initialize ${LOG_FILE} ${EndIf} - Pop $FullUninstall + + Pop $Silent log::Log "Running uninstaller for ${PRODUCT_NAME} ${VERSION}" ${ExtractMullvadSetup} - ${If} $FullUninstall != 1 + ${If} $Silent == 1 + ReadRegStr $NewVersion HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\${APP_GUID}" "NewVersion" + + nsExec::ExecToStack '"$TEMP\mullvad-setup.exe" is-older-version $0' + Pop $0 + Pop $1 + + ${If} $0 == ${MVSETUP_OK} + ${OrIf} $NewVersion == "" + log::Log "Downgrading. Performing a full uninstall" + Push 1 + ${Else} + Push 0 + ${EndIf} + ${Else} + Push 1 + ${EndIf} + + Pop $FullUninstall + + ${If} $FullUninstall == 0 # Save the target tunnel state if we're upgrading nsExec::ExecToStack '"$TEMP\mullvad-setup.exe" prepare-restart' Pop $0 @@ -864,7 +894,6 @@ ${ExtractTapDriver} ${RemoveBrandedTap} - # If not ran silently ${If} $FullUninstall == 1 ${ClearFirewallRules} ${ClearAccountHistory} @@ -874,8 +903,10 @@ ${RemoveWintun} ${RemoveLogsAndCache} - MessageBox MB_ICONQUESTION|MB_YESNO "Would you like to remove settings files as well?" IDNO customRemoveFiles_after_remove_settings - ${RemoveSettings} + ${If} $Silent != 1 + MessageBox MB_ICONQUESTION|MB_YESNO "Would you like to remove settings files as well?" IDNO customRemoveFiles_after_remove_settings + ${RemoveSettings} + ${EndIf} customRemoveFiles_after_remove_settings: ${EndIf} diff --git a/mullvad-daemon/src/version_check.rs b/mullvad-daemon/src/version_check.rs index 183082f457..d0895671ba 100644 --- a/mullvad-daemon/src/version_check.rs +++ b/mullvad-daemon/src/version_check.rs @@ -4,11 +4,9 @@ use crate::{ }; use futures::{channel::mpsc, stream::FusedStream, FutureExt, SinkExt, StreamExt, TryFutureExt}; use mullvad_rpc::{rest::MullvadRestHandle, AppVersionProxy}; -use mullvad_types::version::AppVersionInfo; -use regex::Regex; +use mullvad_types::version::{AppVersionInfo, ParsedAppVersion}; use serde::{Deserialize, Serialize}; use std::{ - cmp::{Ord, Ordering, PartialOrd}, fs, future::Future, io, @@ -22,10 +20,8 @@ use tokio::fs::File; const VERSION_INFO_FILENAME: &str = "version-info.json"; lazy_static::lazy_static! { - static ref STABLE_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)$").unwrap(); - static ref BETA_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)-beta(\d+)$").unwrap(); - static ref APP_VERSION: Option<AppVersion> = AppVersion::from_str(PRODUCT_VERSION); - static ref IS_DEV_BUILD: bool = APP_VERSION.is_none(); + static ref APP_VERSION: ParsedAppVersion = ParsedAppVersion::from_str(PRODUCT_VERSION).unwrap(); + static ref IS_DEV_BUILD: bool = APP_VERSION.is_dev(); } const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(15); @@ -175,13 +171,15 @@ impl VersionUpdater { &mut self, response: mullvad_rpc::AppVersionResponse, ) -> AppVersionInfo { - let suggested_upgrade = APP_VERSION.and_then(|current_version| { + let suggested_upgrade = if !*IS_DEV_BUILD { Self::suggested_upgrade( - ¤t_version, + &*APP_VERSION, &response, self.show_beta_releases || is_beta_version(), ) - }); + } else { + None + }; AppVersionInfo { supported: response.supported, @@ -192,17 +190,17 @@ impl VersionUpdater { } fn suggested_upgrade( - current_version: &AppVersion, + current_version: &ParsedAppVersion, response: &mullvad_rpc::AppVersionResponse, show_beta: bool, ) -> Option<String> { let stable_version = response .latest_stable .as_ref() - .and_then(|stable| AppVersion::from_str(stable)); + .and_then(|stable| ParsedAppVersion::from_str(stable)); let beta_version = if show_beta { - AppVersion::from_str(&response.latest_beta) + ParsedAppVersion::from_str(&response.latest_beta) } else { None }; @@ -322,107 +320,11 @@ pub fn load_cache(cache_dir: &Path) -> AppVersionInfo { } } -#[derive(Eq, PartialEq, Debug, Copy, Clone)] -enum AppVersion { - Stable(u32, u32), - Beta(u32, u32, u32), -} - -impl AppVersion { - fn from_str(version: &str) -> Option<Self> { - let get_int = |cap: ®ex::Captures<'_>, idx| cap.get(idx)?.as_str().parse().ok(); - - if let Some(caps) = STABLE_REGEX.captures(version) { - let year = get_int(&caps, 1)?; - let version = get_int(&caps, 2)?; - Some(Self::Stable(year, version)) - } else if let Some(caps) = BETA_REGEX.captures(version) { - let year = get_int(&caps, 1)?; - let version = get_int(&caps, 2)?; - let beta_version = get_int(&caps, 3)?; - Some(Self::Beta(year, version, beta_version)) - } else { - None - } - } -} - -impl Ord for AppVersion { - fn cmp(&self, other: &Self) -> Ordering { - use AppVersion::*; - match (self, other) { - (Stable(year, version), Stable(other_year, other_version)) => { - year.cmp(other_year).then(version.cmp(other_version)) - } - // A stable version of the same year and version is always greater than a beta - (Stable(year, version), Beta(other_year, other_version, _)) => year - .cmp(other_year) - .then(version.cmp(other_version)) - .then(Ordering::Greater), - ( - Beta(year, version, beta_version), - Beta(other_year, other_version, other_beta_version), - ) => year - .cmp(other_year) - .then(version.cmp(other_version)) - .then(beta_version.cmp(other_beta_version)), - (Beta(year, version, _beta_version), Stable(other_year, other_version)) => year - .cmp(other_year) - .then(version.cmp(other_version)) - .then(Ordering::Less), - } - } -} - -impl PartialOrd for AppVersion { - fn partial_cmp(&self, other: &AppVersion) -> Option<Ordering> { - Some(self.cmp(other)) - } -} - -impl ToString for AppVersion { - fn to_string(&self) -> String { - match self { - Self::Stable(year, version) => format!("{}.{}", year, version), - Self::Beta(year, version, beta_version) => { - format!("{}.{}-beta{}", year, version, beta_version) - } - } - } -} - #[cfg(test)] mod test { use super::*; #[test] - fn test_version_regex() { - assert!(STABLE_REGEX.is_match("2020.4")); - assert!(!STABLE_REGEX.is_match("2020.4-beta3")); - assert!(BETA_REGEX.is_match("2020.4-beta3")); - assert!(!STABLE_REGEX.is_match("2020.5-beta1-dev-f16be4")); - assert!(!STABLE_REGEX.is_match("2020.5-dev-f16be4")); - assert!(!BETA_REGEX.is_match("2020.5-beta1-dev-f16be4")); - assert!(!BETA_REGEX.is_match("2020.5-dev-f16be4")); - assert!(!BETA_REGEX.is_match("2020.4")); - } - - #[test] - fn test_version_parsing() { - let tests = vec![ - ("2020.4", Some(AppVersion::Stable(2020, 4))), - ("2020.4-beta3", Some(AppVersion::Beta(2020, 4, 3))), - ("2020.15-beta1-dev-f16be4", None), - ("2020.15-dev-f16be4", None), - ("", None), - ]; - - for (input, expected_output) in tests { - assert_eq!(AppVersion::from_str(&input), expected_output,); - } - } - - #[test] fn test_version_upgrade_suggestions() { let app_version_info = mullvad_rpc::AppVersionResponse { supported: true, @@ -431,13 +333,13 @@ mod test { latest_beta: "2020.5-beta3".to_string(), }; - let older_stable = AppVersion::from_str("2020.3").unwrap(); - let current_stable = AppVersion::from_str("2020.4").unwrap(); - let newer_stable = AppVersion::from_str("2021.5").unwrap(); + let older_stable = ParsedAppVersion::from_str("2020.3").unwrap(); + let current_stable = ParsedAppVersion::from_str("2020.4").unwrap(); + let newer_stable = ParsedAppVersion::from_str("2021.5").unwrap(); - let older_beta = AppVersion::from_str("2020.3-beta3").unwrap(); - let current_beta = AppVersion::from_str("2020.5-beta3").unwrap(); - let newer_beta = AppVersion::from_str("2021.5-beta3").unwrap(); + let older_beta = ParsedAppVersion::from_str("2020.3-beta3").unwrap(); + let current_beta = ParsedAppVersion::from_str("2020.5-beta3").unwrap(); + let newer_beta = ParsedAppVersion::from_str("2021.5-beta3").unwrap(); assert_eq!( VersionUpdater::suggested_upgrade(&older_stable, &app_version_info, false), diff --git a/mullvad-setup/Cargo.toml b/mullvad-setup/Cargo.toml index 5f509b65d6..aa816e47c6 100644 --- a/mullvad-setup/Cargo.toml +++ b/mullvad-setup/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" clap = "2.32" env_logger = "0.7" err-derive = "0.2.1" +lazy_static = "1.1.0" mullvad-management-interface = { path = "../mullvad-management-interface" } @@ -23,6 +24,7 @@ tokio = { version = "0.2", features = [ "rt-threaded" ] } mullvad-daemon = { path = "../mullvad-daemon" } mullvad-paths = { path = "../mullvad-paths" } mullvad-rpc = { path = "../mullvad-rpc" } +mullvad-types = { path = "../mullvad-types" } talpid-core = { path = "../talpid-core" } talpid-types = { path = "../talpid-types" } diff --git a/mullvad-setup/src/main.rs b/mullvad-setup/src/main.rs index 396b86fa2b..f03ddb352c 100644 --- a/mullvad-setup/src/main.rs +++ b/mullvad-setup/src/main.rs @@ -2,12 +2,25 @@ use clap::{crate_authors, crate_description, crate_name, SubCommand}; use mullvad_daemon::account_history; use mullvad_management_interface::new_rpc_client; use mullvad_rpc::MullvadRpcRuntime; +use mullvad_types::version::ParsedAppVersion; use std::{path::PathBuf, process}; use talpid_core::firewall::{self, Firewall, FirewallArguments}; use talpid_types::ErrorExt; pub const PRODUCT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/product-version.txt")); +lazy_static::lazy_static! { + static ref APP_VERSION: ParsedAppVersion = ParsedAppVersion::from_str(PRODUCT_VERSION).unwrap(); + static ref IS_DEV_BUILD: bool = APP_VERSION.is_dev(); +} + +#[repr(i32)] +enum ExitStatus { + Ok = 0, + Error = 1, + VersionNotOlder = 2, +} + #[cfg(windows)] mod daemon_paths; @@ -46,6 +59,9 @@ pub enum Error { #[error(display = "Failed to initialize account history")] ClearAccountHistoryError(#[error(source)] account_history::Error), + + #[error(display = "Cannot parse the version string")] + ParseVersionStringError, } #[tokio::main] @@ -58,6 +74,13 @@ async fn main() { SubCommand::with_name("reset-firewall") .about("Remove any firewall rules introduced by the daemon"), SubCommand::with_name("clear-history").about("Clear account history"), + SubCommand::with_name("is-older-version") + .about("Checks whether the given version is older than the current version") + .arg( + clap::Arg::with_name("OLDVERSION") + .required(true) + .help("Version string to compare the current version"), + ), ]; let app = clap::App::new(crate_name!()) @@ -72,19 +95,38 @@ async fn main() { .subcommands(subcommands); let matches = app.get_matches(); - let result = match matches.subcommand_name().expect("Subcommand has no name") { - "prepare-restart" => prepare_restart().await, - "reset-firewall" => reset_firewall().await, - "clear-history" => clear_history().await, + let result = match matches.subcommand() { + ("prepare-restart", _) => prepare_restart().await, + ("reset-firewall", _) => reset_firewall().await, + ("clear-history", _) => clear_history().await, + ("is-older-version", Some(sub_matches)) => { + let old_version = sub_matches.value_of("OLDVERSION").unwrap(); + match is_older_version(old_version).await { + // Returning exit status + Ok(status) => process::exit(status as i32), + Err(error) => Err(error), + } + } _ => unreachable!("No command matched"), }; if let Err(e) = result { eprintln!("{}", e.display_chain()); - process::exit(1); + process::exit(ExitStatus::Error as i32); } } +async fn is_older_version(old_version: &str) -> Result<ExitStatus, Error> { + let parsed_version = + ParsedAppVersion::from_str(old_version).ok_or(Error::ParseVersionStringError)?; + + Ok(if parsed_version < *APP_VERSION { + ExitStatus::Ok + } else { + ExitStatus::VersionNotOlder + }) +} + async fn prepare_restart() -> Result<(), Error> { let mut rpc = new_rpc_client().await.map_err(Error::RpcConnectionError)?; rpc.prepare_restart(()) diff --git a/mullvad-types/src/version.rs b/mullvad-types/src/version.rs index 47f26f2de0..5bbe10618c 100644 --- a/mullvad-types/src/version.rs +++ b/mullvad-types/src/version.rs @@ -1,6 +1,15 @@ #[cfg(target_os = "android")] use jnix::IntoJava; +use regex::Regex; use serde::{Deserialize, Serialize}; +use std::cmp::{Ord, Ordering, PartialOrd}; + +lazy_static::lazy_static! { + static ref STABLE_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)$").unwrap(); + static ref BETA_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)-beta(\d+)$").unwrap(); + static ref DEV_REGEX: Regex = Regex::new(r"^(\d{4})\.(\d+)(\.\d+)?(-beta(\d+))?-dev-(\w+)$").unwrap(); +} + /// AppVersionInfo represents the current stable and the current latest release versions of the /// Mullvad VPN app. @@ -27,3 +36,167 @@ pub struct AppVersionInfo { } pub type AppVersion = String; + + +/// Parses a version string into a type that can be used for comparisons. +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum ParsedAppVersion { + Stable(u32, u32), + Beta(u32, u32, u32), + Dev(u32, u32, Option<u32>, String), +} + +impl ParsedAppVersion { + pub fn from_str(version: &str) -> Option<Self> { + let get_int = |cap: ®ex::Captures<'_>, idx| cap.get(idx)?.as_str().parse().ok(); + + if let Some(caps) = STABLE_REGEX.captures(version) { + let year = get_int(&caps, 1)?; + let version = get_int(&caps, 2)?; + Some(Self::Stable(year, version)) + } else if let Some(caps) = BETA_REGEX.captures(version) { + let year = get_int(&caps, 1)?; + let version = get_int(&caps, 2)?; + let beta_version = get_int(&caps, 3)?; + Some(Self::Beta(year, version, beta_version)) + } else if let Some(caps) = DEV_REGEX.captures(version) { + let year = get_int(&caps, 1)?; + let version = get_int(&caps, 2)?; + let beta_version = caps.get(4).map(|_| get_int(&caps, 5).unwrap()); + let dev_hash = caps.get(6)?.as_str().to_string(); + Some(Self::Dev(year, version, beta_version, dev_hash)) + } else { + None + } + } + + pub fn is_dev(&self) -> bool { + match self { + ParsedAppVersion::Dev(..) => true, + _ => false, + } + } +} + +impl Ord for ParsedAppVersion { + fn cmp(&self, other: &Self) -> Ordering { + use ParsedAppVersion::*; + match (self, other) { + (Stable(year, version), Stable(other_year, other_version)) => { + year.cmp(other_year).then(version.cmp(other_version)) + } + // A stable version of the same year and version is always greater than a beta + (Stable(year, version), Beta(other_year, other_version, _)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Greater), + // We assume that a dev version of the same year and version is newer + (Stable(year, version), Dev(other_year, other_version, ..)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Less), + + ( + Beta(year, version, beta_version), + Beta(other_year, other_version, other_beta_version), + ) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(beta_version.cmp(other_beta_version)), + (Beta(year, version, _beta_version), Stable(other_year, other_version)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Less), + // We assume that a dev version of the same year and version is newer + (Beta(year, version, _), Dev(other_year, other_version, ..)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Less), + + // Dev versions of the same year and version are assumed to be equal + (Dev(year, version, ..), Dev(other_year, other_version, ..)) => { + year.cmp(other_year).then(version.cmp(other_version)) + } + (Dev(year, version, ..), Stable(other_year, other_version)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Greater), + (Dev(year, version, ..), Beta(other_year, other_version, _)) => year + .cmp(other_year) + .then(version.cmp(other_version)) + .then(Ordering::Greater), + } + } +} + +impl PartialOrd for ParsedAppVersion { + fn partial_cmp(&self, other: &ParsedAppVersion) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl ToString for ParsedAppVersion { + fn to_string(&self) -> String { + match self { + Self::Stable(year, version) => format!("{}.{}", year, version), + Self::Beta(year, version, beta_version) => { + format!("{}.{}-beta{}", year, version, beta_version) + } + Self::Dev(year, version, beta_version, hash) => { + if let Some(beta_version) = beta_version { + format!("{}.{}-beta{}-dev-{}", year, version, beta_version, hash) + } else { + format!("{}.{}-dev-{}", year, version, hash) + } + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_version_regex() { + assert!(STABLE_REGEX.is_match("2020.4")); + assert!(!STABLE_REGEX.is_match("2020.4-beta3")); + assert!(BETA_REGEX.is_match("2020.4-beta3")); + assert!(!STABLE_REGEX.is_match("2020.5-beta1-dev-f16be4")); + assert!(!STABLE_REGEX.is_match("2020.5-dev-f16be4")); + assert!(!BETA_REGEX.is_match("2020.5-beta1-dev-f16be4")); + assert!(!BETA_REGEX.is_match("2020.5-dev-f16be4")); + assert!(!BETA_REGEX.is_match("2020.4")); + assert!(DEV_REGEX.is_match("2020.5-dev-f16be4")); + assert!(DEV_REGEX.is_match("2020.5-beta1-dev-f16be4")); + assert!(!DEV_REGEX.is_match("2020.5")); + assert!(!DEV_REGEX.is_match("2020.5-beta1")); + } + + #[test] + fn test_version_parsing() { + let tests = vec![ + ("2020.4", Some(ParsedAppVersion::Stable(2020, 4))), + ("2020.4-beta3", Some(ParsedAppVersion::Beta(2020, 4, 3))), + ( + "2020.15-beta1-dev-f16be4", + Some(ParsedAppVersion::Dev( + 2020, + 15, + Some(1), + "f16be4".to_string(), + )), + ), + ( + "2020.15-dev-f16be4", + Some(ParsedAppVersion::Dev(2020, 15, None, "f16be4".to_string())), + ), + ("2020.15-9000", None), + ("", None), + ]; + + for (input, expected_output) in tests { + assert_eq!(ParsedAppVersion::from_str(&input), expected_output,); + } + } +} |
