use crate::license::HardwareBinding;
use std::sync::Arc;
pub trait HardwareEnvironment: Send + Sync {
fn snapshot(&self) -> HardwareInfo;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultHardwareEnvironment;
impl HardwareEnvironment for DefaultHardwareEnvironment {
fn snapshot(&self) -> HardwareInfo {
detect_hardware()
}
}
#[derive(Debug, Clone)]
pub struct FixedHardwareEnvironment(pub HardwareInfo);
impl HardwareEnvironment for FixedHardwareEnvironment {
fn snapshot(&self) -> HardwareInfo {
self.0.clone()
}
}
pub fn default_hardware_environment() -> Arc<dyn HardwareEnvironment> {
Arc::new(DefaultHardwareEnvironment)
}
#[derive(Debug, Clone, Default)]
pub struct HardwareInfo {
pub mac_addresses: Vec<String>,
pub disk_ids: Vec<String>,
pub hostname: Option<String>,
pub machine_id: Option<String>,
}
impl HardwareInfo {
pub fn to_binding(&self) -> HardwareBinding {
let mut binding = HardwareBinding::new();
if !self.mac_addresses.is_empty() {
binding.mac_addresses = self.mac_addresses.clone();
}
if !self.disk_ids.is_empty() {
binding.disk_ids = self.disk_ids.clone();
}
if let Some(ref hostname) = self.hostname {
binding.hostnames.push(hostname.clone());
}
if let Some(ref machine_id) = self.machine_id {
binding
.custom
.insert("machine_id".to_string(), vec![machine_id.clone()]);
}
binding
}
}
pub fn detect_hardware() -> HardwareInfo {
#[cfg(feature = "hardware-detect")]
{
HardwareInfo {
mac_addresses: detect_mac_addresses(),
hostname: detect_hostname(),
disk_ids: detect_disk_ids(),
machine_id: detect_machine_id(),
}
}
#[cfg(not(feature = "hardware-detect"))]
{
HardwareInfo {
mac_addresses: vec![],
hostname: None,
disk_ids: vec![],
machine_id: None,
}
}
}
#[cfg(feature = "hardware-detect")]
fn detect_mac_addresses() -> Vec<String> {
let mut macs = Vec::new();
if let Ok(Some(mac)) = mac_address::get_mac_address() {
macs.push(mac.to_string().to_uppercase());
}
if let Ok(Some(mac)) = mac_address::mac_address_by_name("eth0") {
let mac_str = mac.to_string().to_uppercase();
if !macs.contains(&mac_str) {
macs.push(mac_str);
}
}
use sysinfo::Networks;
let networks = Networks::new_with_refreshed_list();
for (interface_name, _data) in networks.iter() {
if let Ok(Some(mac)) = mac_address::mac_address_by_name(interface_name) {
let mac_str = mac.to_string().to_uppercase();
if !macs.contains(&mac_str) && !mac_str.starts_with("00:00:00") {
macs.push(mac_str);
}
}
}
macs
}
#[cfg(feature = "hardware-detect")]
fn detect_hostname() -> Option<String> {
hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.map(|s| s.to_lowercase())
}
#[cfg(feature = "hardware-detect")]
fn detect_disk_ids() -> Vec<String> {
let mut disk_ids = Vec::new();
use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list();
for disk in disks.iter() {
let name = disk.name().to_string_lossy().to_string();
if !name.is_empty() && !disk_ids.contains(&name) {
disk_ids.push(name);
}
}
#[cfg(target_os = "linux")]
{
if let Ok(entries) = std::fs::read_dir("/sys/block") {
for entry in entries.flatten() {
let path = entry.path().join("device/serial");
if let Ok(serial) = std::fs::read_to_string(&path) {
let serial = serial.trim().to_string();
if !serial.is_empty() && !disk_ids.contains(&serial) {
disk_ids.push(serial);
}
}
}
}
}
disk_ids
}
#[cfg(feature = "hardware-detect")]
fn detect_machine_id() -> Option<String> {
#[cfg(target_os = "linux")]
{
if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
return Some(id.trim().to_string());
}
if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
return Some(id.trim().to_string());
}
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
if let Ok(output) = Command::new("ioreg")
.args(["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("IOPlatformUUID") {
if let Some(start) = line.find('"') {
if let Some(end) = line.rfind('"') {
if start < end {
return Some(line[start + 1..end].to_string());
}
}
}
}
}
}
}
#[cfg(target_os = "windows")]
{
use std::process::Command;
if let Ok(output) = Command::new("wmic")
.args(["csproduct", "get", "UUID"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines().skip(1) {
let uuid = line.trim();
if !uuid.is_empty() && uuid != "UUID" {
return Some(uuid.to_string());
}
}
}
}
None
}
pub fn verify_hardware_binding(
binding: &HardwareBinding,
current: &HardwareInfo,
) -> Result<(), HardwareBindingError> {
if binding.is_empty() {
return Ok(());
}
if !binding.mac_addresses.is_empty() {
let current_macs: Vec<String> = current
.mac_addresses
.iter()
.map(|m| m.to_uppercase())
.collect();
let has_match = binding
.mac_addresses
.iter()
.any(|bound| current_macs.contains(&bound.to_uppercase()));
if !has_match {
return Err(HardwareBindingError::MacAddressMismatch {
expected: binding.mac_addresses.clone(),
found: current.mac_addresses.clone(),
});
}
}
if !binding.hostnames.is_empty() {
if let Some(ref current_hostname) = current.hostname {
let has_match = binding
.hostnames
.iter()
.any(|bound| bound.eq_ignore_ascii_case(current_hostname));
if !has_match {
return Err(HardwareBindingError::HostnameMismatch {
expected: binding.hostnames.clone(),
found: current_hostname.clone(),
});
}
} else {
return Err(HardwareBindingError::HostnameMismatch {
expected: binding.hostnames.clone(),
found: "<unknown>".to_string(),
});
}
}
if !binding.disk_ids.is_empty() {
let has_match = binding
.disk_ids
.iter()
.any(|bound| current.disk_ids.contains(bound));
if !has_match {
return Err(HardwareBindingError::DiskIdMismatch {
expected: binding.disk_ids.clone(),
found: current.disk_ids.clone(),
});
}
}
for (key, expected_values) in &binding.custom {
if key == "machine_id" {
if let Some(ref current_id) = current.machine_id {
if !expected_values.contains(current_id) {
return Err(HardwareBindingError::CustomMismatch {
key: key.clone(),
expected: expected_values.clone(),
found: current_id.clone(),
});
}
}
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum HardwareBindingError {
MacAddressMismatch {
expected: Vec<String>,
found: Vec<String>,
},
HostnameMismatch {
expected: Vec<String>,
found: String,
},
DiskIdMismatch {
expected: Vec<String>,
found: Vec<String>,
},
CustomMismatch {
key: String,
expected: Vec<String>,
found: String,
},
}
impl std::fmt::Display for HardwareBindingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MacAddressMismatch { expected, found } => {
write!(
f,
"MAC address mismatch: expected one of {:?}, found {:?}",
expected, found
)
}
Self::HostnameMismatch { expected, found } => {
write!(
f,
"Hostname mismatch: expected one of {:?}, found {}",
expected, found
)
}
Self::DiskIdMismatch { expected, found } => {
write!(
f,
"Disk ID mismatch: expected one of {:?}, found {:?}",
expected, found
)
}
Self::CustomMismatch {
key,
expected,
found,
} => {
write!(
f,
"Custom binding '{}' mismatch: expected one of {:?}, found {}",
key, expected, found
)
}
}
}
}
impl std::error::Error for HardwareBindingError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_binding_always_passes() {
let binding = HardwareBinding::new();
let hardware = HardwareInfo::default();
assert!(verify_hardware_binding(&binding, &hardware).is_ok());
}
#[test]
fn test_mac_address_binding() {
let binding = HardwareBinding::new().with_mac_address("AA:BB:CC:DD:EE:FF");
let mut hardware = HardwareInfo {
mac_addresses: vec!["AA:BB:CC:DD:EE:FF".to_string()],
..Default::default()
};
assert!(verify_hardware_binding(&binding, &hardware).is_ok());
hardware.mac_addresses = vec!["11:22:33:44:55:66".to_string()];
assert!(verify_hardware_binding(&binding, &hardware).is_err());
}
#[test]
fn test_hostname_binding() {
let binding = HardwareBinding::new().with_hostname("my-server");
let mut hardware = HardwareInfo {
hostname: Some("my-server".to_string()),
..Default::default()
};
assert!(verify_hardware_binding(&binding, &hardware).is_ok());
hardware.hostname = Some("other-server".to_string());
assert!(verify_hardware_binding(&binding, &hardware).is_err());
}
}