Skip to content

Commit

Permalink
feat(dgw): add agent version field to heartbeat API (#1122)
Browse files Browse the repository at this point in the history
  • Loading branch information
pacmancoder authored Nov 29, 2024
1 parent 8585030 commit 83fbddb
Show file tree
Hide file tree
Showing 17 changed files with 302 additions and 186 deletions.
33 changes: 27 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/devolutions-agent-shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,11 @@ cfg-if = "1"
serde = { version = "1", features = ["derive"] }
thiserror = "1"

[target.'cfg(windows)'.dependencies]
windows-registry = "0.3"
# Required for `windows-registry` error type. (`Error` is not reexported from `windows-registry`).
windows-result = "0.2"
uuid = "1"

[dev-dependencies]
serde_json = "1"
8 changes: 7 additions & 1 deletion crates/devolutions-agent-shared/src/date_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ pub struct DateVersion {
pub revision: u32,
}

impl DateVersion {
pub fn fmt_without_revision(&self) -> String {
format!("{}.{}.{}", self.year, self.month, self.day)
}
}

impl Serialize for DateVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand Down Expand Up @@ -75,7 +81,7 @@ mod tests {
use super::*;

#[test]
fn date_version_rountrip() {
fn date_version_roundtrip() {
let version = DateVersion {
year: 2022,
month: 10,
Expand Down
24 changes: 24 additions & 0 deletions crates/devolutions-agent-shared/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
mod date_version;
mod update_json;
#[cfg(windows)]
pub mod windows;

#[cfg(not(windows))]
use std::convert::Infallible;
use std::env;

use camino::Utf8PathBuf;
Expand All @@ -25,6 +29,26 @@ cfg_if! {
}
}

#[derive(Debug, thiserror::Error)]
#[error("failed to get package info")]
pub struct PackageInfoError {
#[cfg(windows)]
#[from]
source: windows::registry::RegistryError,
}

#[cfg(windows)]
pub fn get_installed_agent_version() -> Result<Option<DateVersion>, PackageInfoError> {
Ok(windows::registry::get_installed_product_version(
windows::AGENT_UPDATE_CODE,
)?)
}

#[cfg(not(windows))]
pub fn get_installed_agent_version() -> Result<Option<DateVersion>, PackageInfoError> {
Ok(None)
}

pub fn get_data_dir() -> Utf8PathBuf {
if let Ok(config_path_env) = env::var("DAGENT_CONFIG_PATH") {
Utf8PathBuf::from(config_path_env)
Expand Down
18 changes: 18 additions & 0 deletions crates/devolutions-agent-shared/src/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
mod reversed_hex_uuid;

pub mod registry;

use uuid::{uuid, Uuid};

pub use reversed_hex_uuid::InvalidReversedHexUuid;

/// MSI upgrade code for the Devolutions Gateway.
///
/// MSI update code is same for all versions of the product, while product code is different for
/// each version. We are using the update code to find installed product version or its product
/// code in the Windows registry.
pub const GATEWAY_UPDATE_CODE: Uuid = uuid!("{db3903d6-c451-4393-bd80-eb9f45b90214}");
/// MSI upgrade code for the Devolutions Agent.
///
/// See [`GATEWAY_UPDATE_CODE`] for more information on update codes.
pub const AGENT_UPDATE_CODE: Uuid = uuid!("{82318d3c-811f-4d5d-9a82-b7c31b076755}");
92 changes: 92 additions & 0 deletions crates/devolutions-agent-shared/src/windows/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use uuid::Uuid;

use crate::windows::reversed_hex_uuid::{reversed_hex_to_uuid, uuid_to_reversed_hex, InvalidReversedHexUuid};
use crate::DateVersion;

const REG_CURRENT_VERSION: &str = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion";

#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
#[error("failed to open registry key `{key}`")]
OpenKey { key: String, source: windows_result::Error },
#[error("failed to enumerate registry key values from `{key}`")]
EnumKeyValues { key: String, source: windows_result::Error },
#[error("failed to read registry value `{value}` from key `{key}`")]
ReadValue {
value: String,
key: String,
source: windows_result::Error,
},
#[error(transparent)]
InvalidReversedHexUuid(#[from] InvalidReversedHexUuid),
}

/// Get the product code of an installed MSI using its upgrade code.
pub fn get_product_code(update_code: Uuid) -> Result<Option<Uuid>, RegistryError> {
let reversed_hex_uuid = uuid_to_reversed_hex(update_code);

let key_path = format!("{REG_CURRENT_VERSION}\\Installer\\UpgradeCodes\\{reversed_hex_uuid}");

let update_code_key = windows_registry::LOCAL_MACHINE.open(&key_path);

// Product not installed if no key found.
let update_code_key = match update_code_key {
Ok(key) => key,
Err(_) => return Ok(None),
};

// Product code is the name of the only value in the registry key.
let (product_code, _) = match update_code_key
.values()
.map_err(|source| RegistryError::EnumKeyValues { key: key_path, source })?
.next()
{
Some(value) => value,
None => return Ok(None),
};

Ok(Some(reversed_hex_to_uuid(&product_code)?))
}

/// Get the installed version of a product using Windows registry. Returns `None` if the product
/// is not installed.
pub fn get_installed_product_version(update_code: Uuid) -> Result<Option<DateVersion>, RegistryError> {
let product_code_uuid = match get_product_code(update_code)? {
Some(uuid) => uuid,
None => return Ok(None),
};

let key_path = format!("{REG_CURRENT_VERSION}\\Uninstall\\{product_code_uuid}");

const VERSION_VALUE_NAME: &str = "Version";

// Now we know the product code of installed MSI, we could read its version.
let product_tree = windows_registry::LOCAL_MACHINE
.open(&key_path)
.map_err(|source| RegistryError::OpenKey {
key: key_path.clone(),
source,
})?;

let product_version: u32 = product_tree
.get_value(VERSION_VALUE_NAME)
.and_then(TryInto::try_into)
.map_err(|source| RegistryError::ReadValue {
value: VERSION_VALUE_NAME.to_owned(),
key: key_path.clone(),
source,
})?;

// Convert encoded MSI version number to human-readable date.
let short_year = (product_version >> 24) + 2000;
let month = (product_version >> 16) & 0xFF;
let day = product_version & 0xFFFF;

Ok(Some(DateVersion {
year: short_year,
month,
day,
// NOTE: Windows apps could only have 3 version numbers (major, minor, patch).
revision: 0,
}))
}
90 changes: 90 additions & 0 deletions crates/devolutions-agent-shared/src/windows/reversed_hex_uuid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Windows-specific UUID format conversion functions.
use std::str::FromStr;
use uuid::Uuid;

const REVERSED_UUID_STR_LENGTH: usize = uuid::fmt::Simple::LENGTH;
const UUID_REVERSING_PATTERN: &[usize] = &[8, 4, 4, 2, 2, 2, 2, 2, 2, 2, 2];

#[derive(Debug, thiserror::Error)]
#[error("invalid UUID representation {uuid}")]
pub struct InvalidReversedHexUuid {
uuid: String,
}

/// Converts standard UUID to its reversed hex representation used in Windows Registry
/// for upgrade code table.
///
/// e.g.: `{82318d3c-811f-4d5d-9a82-b7c31b076755}` => `C3D81328F118D5D4A9287B3CB1707655`
pub(crate) fn uuid_to_reversed_hex(uuid: Uuid) -> String {
let mut simple_uuid_buffer = [0u8; REVERSED_UUID_STR_LENGTH];
let mut hex_chars_slice: &str = uuid.as_simple().encode_upper(&mut simple_uuid_buffer);

let mut reversed_hex = String::with_capacity(REVERSED_UUID_STR_LENGTH);

for block_len in UUID_REVERSING_PATTERN.iter() {
let (block, rest) = hex_chars_slice.split_at(*block_len);
reversed_hex.extend(block.chars().rev());
hex_chars_slice = rest;
}

assert!(
reversed_hex.len() == REVERSED_UUID_STR_LENGTH,
"UUID_REVERSING_PATTERN should ensure output length"
);

reversed_hex
}

/// Converts reversed hex UUID back to standard Windows Registry format (upper case letters).
///
/// e.g.: `C3D81328F118D5D4A9287B3CB1707655` => `{82318d3c-811f-4d5d-9a82-b7c31b076755}`
pub(crate) fn reversed_hex_to_uuid(mut hex: &str) -> Result<Uuid, InvalidReversedHexUuid> {
if hex.len() != REVERSED_UUID_STR_LENGTH {
return Err(InvalidReversedHexUuid { uuid: hex.to_owned() });
}

let mut uuid_chars = String::with_capacity(uuid::fmt::Simple::LENGTH);

for pattern in UUID_REVERSING_PATTERN.iter() {
let (part, rest) = hex.split_at(*pattern);
uuid_chars.extend(part.chars().rev());
hex = rest;
}

assert!(
uuid_chars.len() == REVERSED_UUID_STR_LENGTH,
"UUID_REVERSING_PATTERN should ensure output length"
);

let uuid = Uuid::from_str(&uuid_chars).map_err(|_| InvalidReversedHexUuid { uuid: hex.to_owned() })?;

Ok(uuid)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn convert_uuid_to_reversed_hex() {
assert_eq!(
uuid_to_reversed_hex(uuid::uuid!("{82318d3c-811f-4d5d-9a82-b7c31b076755}")),
"C3D81328F118D5D4A9287B3CB1707655"
);
}

#[test]
fn convert_reversed_hex_to_uuid() {
assert_eq!(
reversed_hex_to_uuid("C3D81328F118D5D4A9287B3CB1707655").unwrap(),
uuid::uuid!("{82318D3C-811F-4D5D-9A82-B7C31B076755}")
);
}

#[test]
fn reversed_hex_to_uuid_failure() {
assert!(reversed_hex_to_uuid("XXX81328F118D5D4A9287B3CB1707655").is_err());
assert!(reversed_hex_to_uuid("ABCD").is_err());
}
}
2 changes: 0 additions & 2 deletions devolutions-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ hex = "0.4"
notify-debouncer-mini = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] }
sha2 = "0.10"
smallvec = "1"
thiserror = "1"
uuid = { version = "1.10", features = ["v4"] }
winreg = "0.52"
devolutions-pedm = { path = "../crates/devolutions-pedm" }
win-api-wrappers = { path = "../crates/win-api-wrappers" }

Expand Down
4 changes: 2 additions & 2 deletions devolutions-agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use {

#[cfg(windows)]
use {
devolutions_pedm as _, hex as _, notify_debouncer_mini as _, reqwest as _, sha2 as _, smallvec as _,
thiserror as _, uuid as _, win_api_wrappers as _, windows as _, winreg as _,
devolutions_pedm as _, hex as _, notify_debouncer_mini as _, reqwest as _, sha2 as _, thiserror as _, uuid as _,
win_api_wrappers as _, windows as _,
};

#[macro_use]
Expand Down
Loading

0 comments on commit 83fbddb

Please sign in to comment.