diff --git a/Cargo.lock b/Cargo.lock index 3eb3283..5632ed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3197,13 +3197,14 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hello-agent" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "env_logger 0.10.2", "hbb_common", "log", "rustdesk", + "serde_json 1.0.118", "tokio", "winapi", "windows-service", diff --git a/Cargo.toml b/Cargo.toml index 5587afa..0383c3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hello-agent" -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.75" description = "Headless RustDesk-protocol-compatible support agent for Windows" @@ -28,10 +28,16 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", log = "0.4" env_logger = "0.10" anyhow = "1" +# Used by `inventory.rs` to validate the PowerShell-produced JSON before +# we stamp it into the sysinfo upload. hbb_common already pulls serde_json +# transitively, so this is a free re-export — listed explicitly here so +# the inventory module's `use serde_json` doesn't depend on internal +# implementation details of hbb_common. +serde_json = "1" [target.'cfg(target_os = "windows")'.dependencies] windows-service = "0.6" -winapi = { version = "0.3", features = ["winuser", "wtsapi32", "processthreadsapi", "synchapi", "handleapi", "winbase"] } +winapi = { version = "0.3", features = ["winuser", "wtsapi32", "processthreadsapi", "synchapi", "handleapi", "winbase", "wlanapi", "wlantypes"] } winreg = "0.11" # Embed the icon and EXE metadata via the Windows resource compiler. diff --git a/src/inventory.rs b/src/inventory.rs new file mode 100644 index 0000000..3e7cc97 --- /dev/null +++ b/src/inventory.rs @@ -0,0 +1,208 @@ +//! System inventory collection for hello-agent (CMDB). +//! +//! Collects hardware and OS metadata at startup — BIOS serial number, +//! manufacturer / model, AD domain, OS edition + release, CPU details, +//! RAM, disks, and the BitLocker recovery key for the system drive — and +//! returns it as a compact JSON object. The caller stamps the result into +//! `hbb_common::config::INVENTORY` so the next /api/sysinfo upload carries +//! it under the `inventory` key. The rustdesk-server admin UI's per-device +//! detail page reads it back from `device_sysinfo.payload`. +//! +//! Implementation: a single PowerShell child gathers everything via +//! Get-CimInstance and emits compact JSON. One subprocess for the whole +//! inventory is cheaper than per-field WMI queries and avoids pulling a +//! `wmi`/COM crate into the dep tree. Inventory is collected once at +//! startup; the sysinfo loop's hash-compare suppresses re-uploads of +//! unchanged data, so this isn't repeated on every 120 s tick. +//! +//! Non-Windows builds return an empty JSON object — hello-agent v0 only +//! ships on Windows, but keeping the cross-platform surface compiling +//! makes future Linux work cheap. + +#[cfg(target_os = "windows")] +const PS_SCRIPT: &str = r#" +$ErrorActionPreference = 'SilentlyContinue' +$bios = Get-CimInstance -ClassName Win32_BIOS +$cs = Get-CimInstance -ClassName Win32_ComputerSystem +$os = Get-CimInstance -ClassName Win32_OperatingSystem +$cpus = @(Get-CimInstance -ClassName Win32_Processor) +$first_cpu = $cpus | Select-Object -First 1 +$total_phys_cores = ($cpus | Measure-Object -Property NumberOfCores -Sum).Sum +$total_log_cores = ($cpus | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum +$disks = @(Get-CimInstance -ClassName Win32_DiskDrive | ForEach-Object { + [pscustomobject]@{ + name = $_.DeviceID + model = $_.Model + size_gb = if ($_.Size) { [math]::Round([double]$_.Size / 1GB, 1) } else { 0 } + media = $_.MediaType + } +}) +# DisplayVersion is the marketing release ID (e.g. "23H2") — not surfaced +# by Win32_OperatingSystem.Version, which only carries the build number. +$displayVersion = '' +try { + $displayVersion = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name DisplayVersion -ErrorAction Stop).DisplayVersion +} catch {} +# BitLocker recovery key for the system drive. Get-BitLockerVolume needs +# the BitLocker PowerShell module (present on Pro/Enterprise SKUs) and +# admin rights; the agent runs as LocalSystem in production. On Home SKUs +# or Linux this just stays empty. +$bl_key = '' +try { + $sysDrive = $env:SystemDrive + $bv = Get-BitLockerVolume -MountPoint $sysDrive -ErrorAction Stop + $kp = $bv.KeyProtector | Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' } | Select-Object -First 1 + if ($kp -and $kp.RecoveryPassword) { $bl_key = $kp.RecoveryPassword } +} catch {} + +# Network interfaces: every adapter (Up + Disabled + Disconnected) with +# its MAC and bound IPv4/IPv6 addresses. Filtering happens server-side +# so the operator can still see "this NIC is disabled" rather than +# silently dropping it. LinkSpeed comes back as a string like "1 Gbps" +# or "100 Mbps" — normalize to integer Mbps, 0 when unknown / down. +$nics = @(Get-NetAdapter | ForEach-Object { + $nic = $_ + $ipv4 = @(Get-NetIPAddress -InterfaceIndex $nic.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | ForEach-Object { $_.IPAddress }) + $ipv6 = @(Get-NetIPAddress -InterfaceIndex $nic.ifIndex -AddressFamily IPv6 -ErrorAction SilentlyContinue | ForEach-Object { $_.IPAddress }) + $speed_mbps = 0 + if ($nic.LinkSpeed) { + if ($nic.LinkSpeed -match '^([\d.]+)\s*Gbps') { $speed_mbps = [int]([double]$Matches[1] * 1000) } + elseif ($nic.LinkSpeed -match '^([\d.]+)\s*Mbps') { $speed_mbps = [int]([double]$Matches[1]) } + elseif ($nic.LinkSpeed -match '^([\d.]+)\s*Kbps') { $speed_mbps = 0 } + } + [pscustomobject]@{ + name = $nic.Name + description = $nic.InterfaceDescription + mac = $nic.MacAddress + status = "$($nic.Status)" + ipv4 = $ipv4 + ipv6 = $ipv6 + speed_mbps = $speed_mbps + is_wifi = ($nic.PhysicalMediaType -eq 'Native 802.11') + } +}) + +# Wi-Fi inventory is collected separately, in Rust, against the Win32 +# Native Wi-Fi API (`wlanapi.dll`). netsh's text output is partially +# localized — `Authentication` becomes `Authentifizierung` / +# `Autenticación` / `Autentificare` on de/es/ro Windows — and our +# regexes silently dropped fields on non-English hosts. The native API +# returns SSIDs as bytes and auth/cipher as numeric enums, so the +# resulting data is locale-stable. See `src/wifi_native.rs`. The fields +# `wifi_current` / `wifi_nearby` are merged into this object after +# PowerShell exits. + +# Public egress IP: best-effort lookup against an external echo service. +# Used when the operator wants to correlate the device with a NAT'd +# location. 5 s timeout so a blocked corporate firewall doesn't stall +# inventory collection. ipify is HTTPS, IPv4-only by default, no auth. +$public_ip = '' +try { + $public_ip = (Invoke-RestMethod -Uri 'https://api.ipify.org' -TimeoutSec 5 -ErrorAction Stop).ToString().Trim() +} catch {} + +$os_release = "$($os.Version)" +if ($displayVersion) { $os_release = "$($os.Version) $displayVersion" } +$result = [pscustomobject]@{ + serial_number = $bios.SerialNumber + manufacturer = $cs.Manufacturer + model = $cs.Model + domain = $cs.Domain + os_distro = $os.Caption + os_release = $os_release + cpu_model = $first_cpu.Name + cpu_speed_ghz = if ($first_cpu.MaxClockSpeed) { [math]::Round([double]$first_cpu.MaxClockSpeed / 1000, 2) } else { 0 } + cpu_cores_physical = $total_phys_cores + cpu_cores_logical = $total_log_cores + ram_gb = if ($cs.TotalPhysicalMemory) { [math]::Round([double]$cs.TotalPhysicalMemory / 1GB, 1) } else { 0 } + disks = $disks + bitlocker_recovery_key = $bl_key + network_interfaces = $nics + public_ip = $public_ip +} +$result | ConvertTo-Json -Compress -Depth 6 +"#; + +/// Collect the inventory and return it as a JSON string. Empty string on +/// any failure — the caller treats that as "skip this upload's +/// `inventory` field" rather than uploading garbage. +#[cfg(target_os = "windows")] +pub fn collect_inventory() -> String { + use std::os::windows::process::CommandExt; + use std::process::Command; + // CREATE_NO_WINDOW prevents a brief PowerShell console flash if the + // agent is ever run interactively (dev mode). In service mode there's + // no console anyway, but the flag is harmless. + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let output = match Command::new("powershell.exe") + .args([ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + PS_SCRIPT, + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + { + Ok(o) => o, + Err(e) => { + log::warn!("inventory: powershell failed to spawn: {e}"); + return String::new(); + } + }; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + log::warn!( + "inventory: powershell exited non-zero ({:?}): {}", + output.status.code(), + stderr.trim() + ); + return String::new(); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let trimmed = stdout.trim(); + if trimmed.is_empty() { + log::warn!("inventory: powershell produced empty stdout"); + return String::new(); + } + + // Parse the PowerShell-produced object so we can merge in the + // native Wi-Fi data below. Any parse failure aborts the whole + // collection — sync.rs would otherwise have to reject malformed + // payloads mid-upload and we'd carry the bad data until the next + // collect. + let mut value: serde_json::Value = match serde_json::from_str(trimmed) { + Ok(v) => v, + Err(e) => { + log::warn!( + "inventory: powershell output is not valid JSON: {e}; first 200 chars: {:.200}", + trimmed + ); + return String::new(); + } + }; + + // Native Wi-Fi via wlanapi.dll. Emits SSIDs as bytes and auth/cipher + // as numeric enums, so the result is locale-stable across the + // en/de/es/ro Windows builds in our fleet — replaces the + // previously-localized netsh parser. + let (wifi_current, wifi_nearby) = crate::wifi_native::collect(); + if let Some(c) = wifi_current { + value["wifi_current"] = c; + } + value["wifi_nearby"] = serde_json::Value::Array(wifi_nearby); + + let serialized = value.to_string(); + log::info!("inventory: collected ({} bytes)", serialized.len()); + serialized +} + +#[cfg(not(target_os = "windows"))] +pub fn collect_inventory() -> String { + String::new() +} diff --git a/src/main.rs b/src/main.rs index 2bb244d..1688fd6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod cli; mod config_import; +mod inventory; #[cfg(target_os = "windows")] mod cm_popup; @@ -30,6 +31,8 @@ mod cm_popup; mod service; #[cfg(target_os = "windows")] mod unattended_password; +#[cfg(target_os = "windows")] +mod wifi_native; use cli::{Action, ParsedArgs}; @@ -206,6 +209,22 @@ fn run_server() { ), } + // Kick off CMDB inventory collection on a background thread before + // start_server boots. PowerShell's first-run cost (a few hundred ms + // to a few seconds) shouldn't delay the rendezvous heartbeat — the + // sysinfo upload loop only fires every TIME_CONN seconds, so the + // inventory will be ready in time for the very first /api/sysinfo + // POST whether collection finishes in 50ms or 5s. We deliberately + // don't retry on failure: a transient PowerShell hiccup leaves the + // INVENTORY global empty, and sync.rs simply omits the `inventory` + // key from the upload. Next agent restart re-tries. + std::thread::spawn(|| { + let inv = inventory::collect_inventory(); + if !inv.is_empty() { + *hbb_common::config::INVENTORY.write().unwrap() = inv; + } + }); + // `start_server` is `#[tokio::main]` and runs forever. (is_server=true, // no_server=false). It boots the default IPC server, input service, // rendezvous mediator, and heartbeat sync. diff --git a/src/wifi_native.rs b/src/wifi_native.rs new file mode 100644 index 0000000..3dac90b --- /dev/null +++ b/src/wifi_native.rs @@ -0,0 +1,278 @@ +//! Locale-independent Wi-Fi inventory via the Win32 Native Wi-Fi API +//! (`wlanapi.dll`). +//! +//! Replaces the previous `netsh wlan show …` text-parsing approach. +//! `netsh` partially localizes its output — labels like +//! `Authentication` / `Authentifizierung` / `Autenticación` / `Autentificare` +//! shift with the user's display language, and our regexes silently +//! dropped fields on non-English Windows. The fleet runs four +//! languages (en, de, es, ro), so we move to the structured API: +//! +//! * SSIDs come back as raw `UCHAR[32]` bytes — no localization +//! possible. +//! * Authentication comes back as a `DOT11_AUTH_ALGORITHM` enum +//! (a small integer); we map to our own English labels. +//! * Cipher / signal quality are likewise plain enums / integers. +//! +//! Returned shape (consumed by `inventory::collect_inventory` and +//! merged into the sysinfo upload): +//! +//! ```text +//! wifi_current: { ssid, bssid, signal_pct, rssi_dbm, rx_kbps, tx_kbps, +//! auth, cipher } | None +//! wifi_nearby: [ { ssid, signal_pct, auth, cipher }, … up to 30 ] +//! ``` +//! +//! All allocations from the API (handle, interface list, network list, +//! query-interface buffer) are freed in this module — no caller cleanup. +//! Every failure path returns best-effort partial data; we never panic. +//! +//! We deliberately do **not** trigger a fresh scan via `WlanScan`. The +//! OS scans periodically on its own, and the cached BSS list is usually +//! < 60 s old. Triggering a scan would either delay startup by ~5 s +//! waiting for `wlan_notification_acm_scan_complete`, or return stale +//! data anyway if we don't wait. The next inventory cycle (≤ 120 s) +//! picks up any change. + +#![cfg(target_os = "windows")] + +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::ptr; +use std::slice; + +// IMPORTANT: use winapi's `HANDLE` / `PVOID` (which under the hood wrap +// `winapi::ctypes::c_void`) rather than `std::ffi::c_void`. Rust treats +// the two void types as distinct even though they're the same in C — +// mixing them produces opaque "expected `winapi::ctypes::c_void`, found +// `std::ffi::c_void`" errors. macOS hosts can't catch this because +// winapi is gated to Windows targets, so Cargo never typechecks this +// file off Windows. +use winapi::shared::minwindef::DWORD; +use winapi::shared::ntdef::{HANDLE, PVOID}; +use winapi::shared::winerror::ERROR_SUCCESS; +use winapi::shared::wlantypes::DOT11_SSID; +use winapi::um::wlanapi::{ + wlan_intf_opcode_current_connection, wlan_interface_state_connected, WlanCloseHandle, + WlanEnumInterfaces, WlanFreeMemory, WlanGetAvailableNetworkList, WlanOpenHandle, + WlanQueryInterface, WLAN_AVAILABLE_NETWORK_LIST, WLAN_CONNECTION_ATTRIBUTES, + WLAN_INTERFACE_INFO_LIST, +}; + +/// Vista+ client version. Win10/Win11 happily negotiate down via +/// `negotiated_version`. We never need v1 (XP-era). +const CLIENT_VERSION_VISTA: DWORD = 2; + +/// Public entry point. Returns `(current_connection, nearby_list)`. +/// On hosts without a Wi-Fi adapter, without the WLAN AutoConfig +/// service, or on any FFI failure → `(None, vec![])`. +pub fn collect() -> (Option, Vec) { + // SAFETY: every raw-pointer dereference and array read below is + // bounds-checked against the `dwNumberOfItems` field of the parent + // list, and the API contract is that those fields match the + // allocated array length. All allocations are paired with + // `WlanFreeMemory` before this function returns. + unsafe { collect_inner() } +} + +unsafe fn collect_inner() -> (Option, Vec) { + let mut handle: HANDLE = ptr::null_mut(); + let mut neg_ver: DWORD = 0; + if WlanOpenHandle( + CLIENT_VERSION_VISTA, + ptr::null_mut(), + &mut neg_ver, + &mut handle, + ) != ERROR_SUCCESS as DWORD + { + return (None, Vec::new()); + } + + let mut current: Option = None; + // Dedupe nearby by SSID, keeping the strongest-quality entry per + // network. WlanGetAvailableNetworkList already collapses BSSIDs to + // one row per (SSID, BSS-type), but if multiple Wi-Fi NICs are + // present we'd see the same SSID twice; this keeps the louder copy. + let mut nearby_by_ssid: HashMap = HashMap::new(); + + let mut iface_list: *mut WLAN_INTERFACE_INFO_LIST = ptr::null_mut(); + if WlanEnumInterfaces(handle, ptr::null_mut(), &mut iface_list) == ERROR_SUCCESS as DWORD + && !iface_list.is_null() + { + let count = (*iface_list).dwNumberOfItems as usize; + if count > 0 { + let ifaces = slice::from_raw_parts((*iface_list).InterfaceInfo.as_ptr(), count); + for iface in ifaces { + read_iface(handle, iface, &mut current, &mut nearby_by_ssid); + } + } + WlanFreeMemory(iface_list as *mut _); + } + + WlanCloseHandle(handle, ptr::null_mut()); + + let mut nearby: Vec = nearby_by_ssid.into_values().collect(); + nearby.sort_by(|a, b| { + let bs = b.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0); + let as_ = a.get("signal_pct").and_then(|v| v.as_u64()).unwrap_or(0); + bs.cmp(&as_) + }); + nearby.truncate(30); + + (current, nearby) +} + +unsafe fn read_iface( + handle: HANDLE, + iface: &winapi::um::wlanapi::WLAN_INTERFACE_INFO, + current: &mut Option, + nearby_by_ssid: &mut HashMap, +) { + let guid = iface.InterfaceGuid; + + // Current connection. Querying when the interface isn't connected + // returns ERROR_NOT_FOUND — no harm, but skipping the call is + // tidier and avoids a noisy ETW event on locked-down endpoints. + if iface.isState == wlan_interface_state_connected && current.is_none() { + let mut data_size: DWORD = 0; + let mut data: PVOID = ptr::null_mut(); + let mut value_type: u32 = 0; + if WlanQueryInterface( + handle, + &guid, + wlan_intf_opcode_current_connection, + ptr::null_mut(), + &mut data_size, + &mut data, + &mut value_type, + ) == ERROR_SUCCESS as DWORD + && !data.is_null() + && data_size as usize >= std::mem::size_of::() + { + let attrs = &*(data as *const WLAN_CONNECTION_ATTRIBUTES); + let assoc = &attrs.wlanAssociationAttributes; + let sec = &attrs.wlanSecurityAttributes; + let ssid = read_ssid(&assoc.dot11Ssid); + let bssid = format_mac(&assoc.dot11Bssid); + *current = Some(json!({ + "ssid": ssid, + "bssid": bssid, + "signal_pct": assoc.wlanSignalQuality, + "rssi_dbm": rssi_from_quality(assoc.wlanSignalQuality), + "rx_kbps": assoc.ulRxRate, + "tx_kbps": assoc.ulTxRate, + "auth": auth_label(sec.dot11AuthAlgorithm as u32), + "cipher": cipher_label(sec.dot11CipherAlgorithm as u32), + })); + WlanFreeMemory(data); + } + } + + // Available networks (cached scan). + let mut net_list: *mut WLAN_AVAILABLE_NETWORK_LIST = ptr::null_mut(); + if WlanGetAvailableNetworkList(handle, &guid, 0, ptr::null_mut(), &mut net_list) + == ERROR_SUCCESS as DWORD + && !net_list.is_null() + { + let n = (*net_list).dwNumberOfItems as usize; + if n > 0 { + let nets = slice::from_raw_parts((*net_list).Network.as_ptr(), n); + for net in nets { + let ssid = read_ssid(&net.dot11Ssid); + if ssid.is_empty() { + // Hidden networks broadcast empty SSIDs in beacons — + // skip rather than emit `{ ssid: "" }` rows that + // collapse together in the dedupe map. + continue; + } + let entry_signal = net.wlanSignalQuality; + let prev_signal = nearby_by_ssid + .get(&ssid) + .and_then(|v| v.get("signal_pct")) + .and_then(|v| v.as_u64()) + .unwrap_or(0); + if (entry_signal as u64) >= prev_signal { + nearby_by_ssid.insert( + ssid.clone(), + json!({ + "ssid": ssid, + "signal_pct": entry_signal, + "auth": auth_label(net.dot11DefaultAuthAlgorithm as u32), + "cipher": cipher_label(net.dot11DefaultCipherAlgorithm as u32), + }), + ); + } + } + } + WlanFreeMemory(net_list as *mut _); + } +} + +/// Read a `DOT11_SSID` (length-prefixed UCHAR[32]) into a Rust String. +/// SSIDs are nominally any byte sequence ≤ 32 octets; UTF-8 is by far +/// the most common (also the IEEE recommendation), but Latin-1 and +/// random bytes occur. We use lossy UTF-8 decoding so weird encodings +/// don't yield panics or empty strings — the operator can still +/// recognize most networks visually. +fn read_ssid(s: &DOT11_SSID) -> String { + let len = (s.uSSIDLength as usize).min(s.ucSSID.len()); + String::from_utf8_lossy(&s.ucSSID[..len]).into_owned() +} + +fn format_mac(bytes: &[u8; 6]) -> String { + format!( + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5] + ) +} + +/// MS docs: WLAN_SIGNAL_QUALITY 0 ↔ −100 dBm, 100 ↔ −50 dBm, linear +/// interpolation in between. `quality / 2 - 100`. +fn rssi_from_quality(q: u32) -> i32 { + -100 + (q.min(100) as i32 / 2) +} + +/// Human label for a `DOT11_AUTH_ALGORITHM` value. Matched on raw +/// integers because winapi 0.3 was tagged before WPA3 / OWE went into +/// the SDK — relying on the named constants would force a dep upgrade +/// just to recognize WPA3 networks. Numeric values are part of the +/// stable Wi-Fi protocol spec, not the SDK. +fn auth_label(a: u32) -> &'static str { + match a { + 1 => "Open", + 2 => "Shared", + 3 => "WPA-Enterprise", + 4 => "WPA-Personal", + 5 => "WPA-None", + 6 => "WPA2-Enterprise", + 7 => "WPA2-Personal", + 8 => "WPA3-Enterprise (192-bit)", + 9 => "WPA3-Personal (SAE)", + 10 => "OWE", + 11 => "WPA3-Enterprise", + _ => "Unknown", + } +} + +/// Human label for a `DOT11_CIPHER_ALGORITHM` value. Same numeric-match +/// rationale as `auth_label`. Values from the IEEE 802.11 / Microsoft +/// header (wlantypes.h DOT11_CIPHER_ALGORITHM enum). +fn cipher_label(c: u32) -> &'static str { + match c { + 0x00 => "None", + 0x01 => "WEP-40", + 0x02 => "TKIP", + 0x04 => "AES (CCMP)", + 0x05 => "WEP-104", + 0x06 => "BIP-CMAC-128", + 0x08 => "GCMP", + 0x09 => "GCMP-256", + 0x0A => "CCMP-256", + 0x0B => "BIP-GMAC-128", + 0x0C => "BIP-GMAC-256", + 0x0D => "BIP-CMAC-256", + 0x100 => "WPA-Use-Group", + 0x101 => "WEP", + _ => "Unknown", + } +} diff --git a/vendor/rustdesk/libs/hbb_common/src/config.rs b/vendor/rustdesk/libs/hbb_common/src/config.rs index d644512..6c78953 100644 --- a/vendor/rustdesk/libs/hbb_common/src/config.rs +++ b/vendor/rustdesk/libs/hbb_common/src/config.rs @@ -123,6 +123,17 @@ lazy_static::lazy_static! { /// "empty = omit" convention as `AGENT_NAME`. For hello-agent this /// is `env!("CARGO_PKG_VERSION")`. pub static ref AGENT_VERSION: RwLock = RwLock::new(String::new()); + /// Pre-serialized JSON object describing the host's hardware / + /// firmware / OS edition inventory (BIOS serial, manufacturer, model, + /// AD domain, OS edition + release, CPU details, RAM, disks, + /// BitLocker recovery key). Same "empty = omit" convention as + /// `AGENT_NAME`: when non-empty, `hbbs_http::sync` parses it and + /// merges it into the sysinfo upload under the `inventory` key, where + /// the rustdesk-server admin UI's per-device detail page reads it. + /// Stored pre-serialized so the producer (hello-agent's `inventory` + /// module) owns the schema and the consumer (sync) doesn't need to + /// know the field set. + pub static ref INVENTORY: RwLock = RwLock::new(String::new()); static ref KEY_PAIR: Mutex> = Default::default(); static ref USER_DEFAULT_CONFIG: RwLock<(UserDefaultConfig, Instant)> = RwLock::new((UserDefaultConfig::load(), Instant::now())); pub static ref NEW_STORED_PEER_CONFIG: Mutex> = Default::default(); diff --git a/vendor/rustdesk/src/hbbs_http/sync.rs b/vendor/rustdesk/src/hbbs_http/sync.rs index 8460b98..7cc5072 100644 --- a/vendor/rustdesk/src/hbbs_http/sync.rs +++ b/vendor/rustdesk/src/hbbs_http/sync.rs @@ -147,6 +147,22 @@ async fn start_hbbs_sync_async() { if !agent_version.is_empty() { v["agent_version"] = json!(agent_version); } + // Optional CMDB inventory. Producer (hello-agent's + // `inventory` module) populates this with a pre- + // serialized JSON object covering BIOS / hardware / + // OS-edition fields. We re-parse on each upload (the + // string is small — single-digit kB at most) rather + // than caching, so a refreshed inventory is picked up + // without bookkeeping. Parse failure is silently + // dropped: the sysinfo upload still goes out without + // the `inventory` key, identical to a vanilla rustdesk + // install. + let inventory = config::INVENTORY.read().unwrap().clone(); + if !inventory.is_empty() { + if let Ok(inv_v) = serde_json::from_str::(&inventory) { + v["inventory"] = inv_v; + } + } let ab_name = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_NAME); if !ab_name.is_empty() { v[keys::OPTION_PRESET_ADDRESS_BOOK_NAME] = json!(ab_name);