From 46796d14b6121a085df0ed627e10a88d4c72857a Mon Sep 17 00:00:00 2001 From: Filip Kieres Date: Sun, 6 Nov 2022 22:48:45 +0100 Subject: [PATCH] Add the GetPlatformInfo action --- Cargo.toml | 10 + build.rs | 10 + src/action.rs | 8 + src/action/insttime.rs | 6 +- src/action/libc_version.c | 14 ++ src/action/platform_info.rs | 394 ++++++++++++++++++++++++++++++++++++ src/fs/mod.rs | 3 + src/fs/windows.rs | 63 ++++++ src/session/error.rs | 32 +++ 9 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 build.rs create mode 100644 src/action/libc_version.c create mode 100644 src/action/platform_info.rs create mode 100644 src/fs/windows.rs diff --git a/Cargo.toml b/Cargo.toml index 69bd8585..371c3710 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,13 +45,21 @@ proc-mounts = { version = "0.2.4", optional = true } # [1]: https://github.com/rust-lang/cargo/issues/1596 fuse = { version = "0.3.1", optional = true } +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = { git = "https://github.com/servo/core-foundation-rs.git", rev = "786895643140fa0ee4f913d7b4aeb0c4626b2085", optional = true } +objc = { version = "0.2", optional = true } + [target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.43.0", features = ["Win32_Foundation", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_SystemInformation"], optional = true } winreg = { version = "0.7.0", optional = true } [dev-dependencies] rand = { version = "0.8.5" } tempfile = { version = "3.3.0" } +[target.'cfg(target_os = "linux")'.build-dependencies] +cc = { version = "1.0", optional = true } + [features] default = [ "action-insttime", @@ -62,6 +70,7 @@ default = [ "action-memsize", "action-metadata", "action-network", + "action-platform-info", "action-stat", "action-timeline", ] @@ -74,6 +83,7 @@ action-listdir = [] action-memsize = ["dep:sysinfo"] action-metadata = [] action-network = ["dep:netstat2", "dep:sysinfo"] +action-platform-info = ["dep:cc", "dep:cocoa", "dep:objc", "dep:windows"] action-stat = [] action-timeline = ["dep:flate2", "dep:sha2"] diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..a8135254 --- /dev/null +++ b/build.rs @@ -0,0 +1,10 @@ +fn main() { + #[cfg(all(target_os = "linux", feature = "action-platform-info"))] + { + println!("cargo:rerun-if-changed=src/action/libc_version.c"); + + cc::Build::new() + .file("src/action/libc_version.c") + .compile("liblibc_version.a"); + } +} diff --git a/src/action.rs b/src/action.rs index 4af7071c..da768bd8 100644 --- a/src/action.rs +++ b/src/action.rs @@ -49,6 +49,9 @@ pub mod memsize; #[cfg(feature = "action-finder")] pub mod finder; +#[cfg(feature = "action-platform-info")] +pub mod platform_info; + pub use error::{ParseArgsError, ParseArgsErrorKind, DispatchError}; /// Dispatches the given `request` to an appropriate action handler. @@ -73,6 +76,11 @@ where handle(session, request, self::metadata::handle) } + #[cfg(feature = "action-platform-info")] + "GetPlatformInfo" => { + handle(session, request, self::platform_info::handle) + } + #[cfg(feature = "action-listdir")] "ListDirectory" => { handle(session, request, self::listdir::handle) diff --git a/src/action/insttime.rs b/src/action/insttime.rs index fa6d37e3..f619efb0 100644 --- a/src/action/insttime.rs +++ b/src/action/insttime.rs @@ -172,7 +172,7 @@ mod e2fs_utils { /// /// This function returns `None` in case of errors. #[cfg(target_os = "linux")] -fn get_install_time() -> Option { +pub fn get_install_time() -> Option { // First, check the creation time of the root. Rust implementation of // `Metadata::created()` on Linux utilizes `statx` syscall, so this // method will work only on kernels >= 4.11. @@ -221,7 +221,7 @@ fn get_install_time() -> Option { /// /// This function returns `None` in case of errors. #[cfg(target_os = "macos")] -fn get_install_time() -> Option { +pub fn get_install_time() -> Option { // Here, we use the same way as Python version of GRR client does. We just // check the modification time for some of the paths. const CANDIDATES: [&str; 3] = [ @@ -239,7 +239,7 @@ fn get_install_time() -> Option { /// /// This function returns `None` in case of errors. #[cfg(target_os = "windows")] -fn get_install_time() -> Option { +pub fn get_install_time() -> Option { use winreg::RegKey; // Don't use winreg::enums::KEY_WOW64_64KEY since it breaks on Windows 2000. diff --git a/src/action/libc_version.c b/src/action/libc_version.c new file mode 100644 index 00000000..79268412 --- /dev/null +++ b/src/action/libc_version.c @@ -0,0 +1,14 @@ +#include +#ifdef __GNU_LIBRARY__ +#include +#else +#include +#endif + +const char *libc_version(void) { +#ifdef __GNU_LIBRARY__ + return gnu_get_libc_version(); +#else + return NULL; +#endif +} diff --git a/src/action/platform_info.rs b/src/action/platform_info.rs new file mode 100644 index 00000000..dde1d41d --- /dev/null +++ b/src/action/platform_info.rs @@ -0,0 +1,394 @@ +// Copyright 2020 Google LLC +// +// Use of this source code is governed by an MIT-style license that can be found +// in the LICENSE file or at https://opensource.org/licenses/MIT. + +//! A handler and associated types for the platform info action. + +use crate::action::insttime::get_install_time; +#[cfg(target_os = "windows")] +use crate::fs::windows::dll_module_fileinfo; +use crate::session::{self, Session}; +#[cfg(target_os = "macos")] +use cocoa::{ + appkit::*, + base::nil, + foundation::{NSInteger, NSProcessInfo}, +}; +use log::error; +#[cfg(target_os = "macos")] +use objc::{msg_send, sel, sel_impl}; +use std::env::consts::ARCH; +#[cfg(target_family = "unix")] +use std::ffi::CStr; +use std::ffi::OsString; +#[cfg(target_family = "unix")] +use std::io::Error; +#[cfg(target_family = "unix")] +use std::mem; +#[cfg(target_family = "unix")] +use std::os::unix::prelude::OsStringExt as _; +#[cfg(target_os = "windows")] +use std::os::windows::ffi::OsStringExt as _; +#[cfg(target_family = "unix")] +use std::ptr; +use std::time::{SystemTime, UNIX_EPOCH}; +#[cfg(target_os = "windows")] +use windows::{ + core::{Error, PWSTR}, + w, + Win32::System::{ + Diagnostics::Debug::{ + PROCESSOR_ARCHITECTURE, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM, + PROCESSOR_ARCHITECTURE_IA64, PROCESSOR_ARCHITECTURE_INTEL, + }, + SystemInformation::{ + ComputerNamePhysicalDnsFullyQualified, ComputerNamePhysicalDnsHostname, + GetComputerNameExW, GetNativeSystemInfo, SYSTEM_INFO, + }, + }, +}; + +#[cfg(target_os = "linux")] +#[link(name = "libc_version")] +extern "C" { + fn libc_version() -> *const libc::c_char; +} + +#[cfg(target_os = "macos")] +#[allow(dead_code)] +#[repr(C)] +struct NSOperatingSystemVersion { + major_version: NSInteger, + minor_version: NSInteger, + patch_version: NSInteger, +} + +/// A response type for the platform information action. +struct Response { + /// The platform information. + platform_info: PlatformInfo, +} + +/// The platform information. +#[derive(Clone, Debug)] +struct PlatformInfo { + /// The system platform (Windows|Darwin|Linux). + system: String, + /// The hostname of this system. + node: OsString, + /// The OS release identifier e.g. 7, OSX, debian. + release: String, + /// The OS version ID e.g. 6.1.7601SP1, 10.9.2, 14.04. + version: Option, + /// The system architecture e.g. AMD64, x86_64. + machine: Option, + // The kernel version string e.g. 6.1.7601, 13.1.0, 3.15-rc2. + kernel: String, + /// The system's fully qualified domain name. + fqdn: OsString, + /// When system was installed. + install_date: Option, + /// The C library version. + libc_ver: Option, + /// The architecture of this binary. (Note this can be different from + /// the machine architecture in the case of a 32 bit binary running + /// on a 64 bit system) + architecture: String, +} + +impl Into for PlatformInfo { + fn into(self) -> rrg_proto::jobs::Uname { + let mut proto = rrg_proto::jobs::Uname::new(); + proto.set_system(self.system); + proto.set_node(self.node.to_string_lossy().into_owned()); + proto.set_release(self.release); + proto.set_kernel(self.kernel); + proto.set_fqdn(self.fqdn.to_string_lossy().into_owned()); + proto.set_architecture(self.architecture); + + if let Some(machine) = self.machine { + proto.set_machine(machine); + } + if let Some(version) = self.version { + proto.set_version(version); + } + if let Some(install_date) = self.install_date { + match install_date.duration_since(UNIX_EPOCH) { + Ok(duration) => { + proto.set_install_date(duration.as_secs()); + } + Err(err) => { + error!( + "install date is {} seconds earlier than Unix epoch", + err.duration().as_secs() + ); + } + }; + } + if let Some(libc_ver) = self.libc_ver { + proto.set_libc_ver(libc_ver); + } + + proto + } +} + +impl super::Item for Response { + const RDF_NAME: &'static str = "Uname"; + + type Proto = rrg_proto::jobs::Uname; + + fn into_proto(self) -> Self::Proto { + self.platform_info.into() + } +} + +/// Gets the platform information (Unix version). +#[cfg(target_family = "unix")] +fn get_platform_info() -> Result { + let mut utsname = mem::MaybeUninit::::zeroed(); + + let result = unsafe { libc::uname(utsname.as_mut_ptr()) }; + + if result != 0 { + return Err(Error::last_os_error()); + }; + + let utsname = unsafe { utsname.assume_init() }; + + let sysname = unsafe { + String::from_utf8_lossy(CStr::from_ptr(utsname.sysname.as_ptr()).to_bytes()).into_owned() + }; + let hostname = unsafe { + OsString::from_vec( + CStr::from_ptr(utsname.nodename.as_ptr()) + .to_bytes() + .to_vec(), + ) + }; + let kernel_release = unsafe { + String::from_utf8_lossy(CStr::from_ptr(utsname.release.as_ptr()).to_bytes()).into_owned() + }; + + #[cfg(target_os = "macos")] + let os_version = { + let appkit_version = unsafe { NSAppKitVersionNumber }; + + if appkit_version >= NSAppKitVersionNumber10_10 { + let NSOperatingSystemVersion { + major_version, + minor_version, + patch_version, + } = unsafe { + let proc_info = NSProcessInfo::processInfo(nil); + msg_send![proc_info, operatingSystemVersion] + }; + + Some(format!( + "{}.{}.{}", + major_version, minor_version, patch_version + )) + } else { + None + } + }; + + #[cfg(not(target_os = "macos"))] + let kernel_version = unsafe { + String::from_utf8_lossy(CStr::from_ptr(utsname.version.as_ptr()).to_bytes()).into_owned() + }; + + let machine = unsafe { + String::from_utf8_lossy(CStr::from_ptr(utsname.machine.as_ptr()).to_bytes()).into_owned() + }; + + #[cfg(target_os = "linux")] + let libc_version = { + unsafe { + let libc_version = libc_version(); + + if libc_version.is_null() { + None + } else { + Some(String::from_utf8_lossy(CStr::from_ptr(libc_version).to_bytes()).into_owned()) + } + } + }; + + #[cfg(not(target_os = "linux"))] + let libc_version = None; + + let hints = mem::MaybeUninit::::zeroed(); + let mut addrinfo = ptr::null_mut(); + + let result = unsafe { + let mut hints = hints.assume_init(); + hints.ai_flags = libc::AI_CANONNAME; + hints.ai_socktype = libc::SOCK_DGRAM; + + libc::getaddrinfo( + utsname.nodename.as_ptr(), + ptr::null(), + &hints, + &mut addrinfo, + ) + }; + + if result != 0 { + Err(Error::last_os_error()) + } else { + let fqdn = { + let addrinfo: libc::addrinfo = unsafe { ptr::read(addrinfo) }; + if addrinfo.ai_canonname.is_null() { + hostname.clone() + } else { + unsafe { + OsString::from_vec(CStr::from_ptr(addrinfo.ai_canonname).to_bytes().to_vec()) + } + } + }; + + unsafe { + libc::freeaddrinfo(addrinfo); + } + + Ok(PlatformInfo { + system: sysname, + node: hostname, + #[cfg(target_os = "linux")] + release: kernel_release.clone(), + #[cfg(target_os = "macos")] + release: "OSX".to_string(), + #[cfg(target_os = "linux")] + version: Some(kernel_version), + #[cfg(target_os = "macos")] + version: os_version, + machine: Some(machine), + kernel: kernel_release, + fqdn, + install_date: get_install_time(), + libc_ver: libc_version, + architecture: ARCH.to_string(), + }) + } +} + +/// Gets the platform information (Windows version). +#[cfg(target_os = "windows")] +fn get_platform_info() -> Result { + let mut computer_name_size = 0; + unsafe { + GetComputerNameExW( + ComputerNamePhysicalDnsHostname, + PWSTR::null(), + &mut computer_name_size, + ); + }; + + let mut computer_name = vec![0_u16; computer_name_size as usize]; + unsafe { + GetComputerNameExW( + ComputerNamePhysicalDnsHostname, + PWSTR::from_raw(computer_name.as_mut_ptr()), + &mut computer_name_size, + ) + } + .ok()?; + + unsafe { + computer_name.set_len(computer_name_size as usize); + } + let hostname = OsString::from_wide(&computer_name); + + let mut computer_name_size = 0; + unsafe { + GetComputerNameExW( + ComputerNamePhysicalDnsFullyQualified, + PWSTR::null(), + &mut computer_name_size, + ); + }; + + let mut computer_name = vec![0_u16; computer_name_size as usize]; + unsafe { + GetComputerNameExW( + ComputerNamePhysicalDnsFullyQualified, + PWSTR::from_raw(computer_name.as_mut_ptr()), + &mut computer_name_size, + ) + } + .ok()?; + + unsafe { + computer_name.set_len(computer_name_size as usize); + } + let fqdn = OsString::from_wide(&computer_name); + + let mut system_info = SYSTEM_INFO::default(); + unsafe { + GetNativeSystemInfo(&mut system_info); + }; + + let kernel_fileinfo = dll_module_fileinfo(w!("kernel32.dll"))?; + + let (major_version, minor_version, build_version) = ( + (kernel_fileinfo.dwProductVersionMS >> 16) & 0xffff, // HIWORD + kernel_fileinfo.dwProductVersionMS & 0xffff, // LOWORD + (kernel_fileinfo.dwProductVersionLS >> 16) & 0xffff, // HIWORD + ); + + let processor_architecture = unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture }; + + Ok(PlatformInfo { + system: "Windows".to_string(), + node: hostname, + release: major_version.to_string(), + version: Some(format!( + "{}.{} ({})", + major_version, minor_version, build_version, + )), + machine: match processor_architecture { + PROCESSOR_ARCHITECTURE_INTEL => Some("x86".to_string()), + PROCESSOR_ARCHITECTURE_ARM => Some("ARM".to_string()), + PROCESSOR_ARCHITECTURE_IA64 => Some("Intel Itanium-based".to_string()), + PROCESSOR_ARCHITECTURE_AMD64 => Some("x86_64".to_string()), + PROCESSOR_ARCHITECTURE(12_u16) => Some("ARM64".to_string()), + _ => None, + }, + kernel: format!( + "{}.{}.{} Build {}", + major_version, minor_version, build_version, build_version + ), + fqdn, + install_date: get_install_time(), + libc_ver: None, + architecture: ARCH.to_string(), + }) +} + +/// Handles requests for the platform information action. +pub fn handle(session: &mut S, _: ()) -> session::Result<()> { + session.reply(Response { + platform_info: get_platform_info()?, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_platform_info() { + let mut session = session::FakeSession::new(); + + if let Err(err) = handle(&mut session, ()) { + panic!("{:?}", err); + }; + + assert_eq!(session.reply_count(), 1); + let response: &Response = session.reply(0); + + println!("{:?}", response.platform_info); + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 6d68ad07..510b5855 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -23,6 +23,9 @@ pub mod macos; #[cfg(target_family = "unix")] pub mod unix; +#[cfg(target_family = "windows")] +pub mod windows; + /// A path to a filesystem item and associated metadata. /// /// This type is very similar to standard `DirEntry` but its `metadata` property diff --git a/src/fs/windows.rs b/src/fs/windows.rs new file mode 100644 index 00000000..7473a5b5 --- /dev/null +++ b/src/fs/windows.rs @@ -0,0 +1,63 @@ +// Copyright 2020 Google LLC +// +// Use of this source code is governed by an MIT-style license that can be found +// in the LICENSE file or at https://opensource.org/licenses/MIT. + +//! Windows-specific utilities for working with the filesystem. + +use std::ptr; +use windows::{ + core::{Error, PCWSTR}, + w, + Win32::{ + Foundation::MAX_PATH, + Storage::FileSystem::{ + GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW, VS_FIXEDFILEINFO, + }, + System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}, + }, +}; + +/// Retrieves the specified fileinfo version of a module. +/// +/// The module must have been loaded by the current process. +pub fn dll_module_fileinfo(dll_name: PCWSTR) -> Result { + let module_handle = unsafe { GetModuleHandleW(dll_name) }?; + + let module_file_path = { + let mut filename = vec![0_u16; MAX_PATH as usize]; + unsafe { GetModuleFileNameW(module_handle, &mut filename) }; + PCWSTR::from_raw(filename.as_ptr() as *const _) + }; + + let mut file_version_info_size = + unsafe { GetFileVersionInfoSizeW(module_file_path.clone(), None) }; + + let mut file_version_info = vec![0_u16; file_version_info_size as usize]; + unsafe { + GetFileVersionInfoW( + module_file_path, + 0, + file_version_info_size, + file_version_info.as_mut_ptr() as _, + ) + } + .ok()?; + + unsafe { + file_version_info.set_len(file_version_info_size as usize); + } + + let mut version_info: *mut VS_FIXEDFILEINFO = ptr::null_mut(); + unsafe { + VerQueryValueW( + file_version_info.as_ptr() as _, + w!("\\"), + &mut version_info as *mut *mut _ as _, + &mut file_version_info_size, + ) + } + .ok()?; + + Ok(unsafe { version_info.as_ref() }.unwrap().to_owned()) +} diff --git a/src/session/error.rs b/src/session/error.rs index 4d1b2415..dd25d34c 100644 --- a/src/session/error.rs +++ b/src/session/error.rs @@ -19,6 +19,12 @@ pub struct Error { pub enum ErrorKind { /// The action execution failed. ExecutionFailure, + /// I/O error. + #[cfg(target_family = "unix")] + IoError(std::io::ErrorKind), + /// Windows error. + #[cfg(target_os = "windows")] + WindowsError, } impl Error { @@ -45,6 +51,10 @@ impl ErrorKind { match *self { ExecutionFailure => "action execution failed", + #[cfg(target_family = "unix")] + IoError(_) => "I/O error", + #[cfg(target_os = "windows")] + WindowsError => "Windows error", } } } @@ -63,6 +73,28 @@ impl std::error::Error for Error { } } +#[cfg(target_family = "unix")] +impl From for Error { + + fn from(error: std::io::Error) -> Self { + Error { + kind: ErrorKind::IoError(error.kind()), + error: Box::new(error), + } + } +} + +#[cfg(target_os = "windows")] +impl From for Error { + + fn from(error: windows::core::Error) -> Self { + Error { + kind: ErrorKind::WindowsError, + error: Box::new(error), + } + } +} + /// An error type for failures that can occur when parsing proto messages. #[derive(Debug)] pub enum ParseError {