1use crate::license::HardwareBinding;
14use std::sync::Arc;
15
16pub trait HardwareEnvironment: Send + Sync {
21 fn snapshot(&self) -> HardwareInfo;
23}
24
25#[derive(Debug, Clone, Copy, Default)]
27pub struct DefaultHardwareEnvironment;
28
29impl HardwareEnvironment for DefaultHardwareEnvironment {
30 fn snapshot(&self) -> HardwareInfo {
31 detect_hardware()
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct FixedHardwareEnvironment(pub HardwareInfo);
38
39impl HardwareEnvironment for FixedHardwareEnvironment {
40 fn snapshot(&self) -> HardwareInfo {
41 self.0.clone()
42 }
43}
44
45pub fn default_hardware_environment() -> Arc<dyn HardwareEnvironment> {
47 Arc::new(DefaultHardwareEnvironment)
48}
49
50#[derive(Debug, Clone, Default)]
52pub struct HardwareInfo {
53 pub mac_addresses: Vec<String>,
55
56 pub disk_ids: Vec<String>,
58
59 pub hostname: Option<String>,
61
62 pub machine_id: Option<String>,
64}
65
66impl HardwareInfo {
67 pub fn to_binding(&self) -> HardwareBinding {
69 let mut binding = HardwareBinding::new();
70
71 if !self.mac_addresses.is_empty() {
72 binding.mac_addresses = self.mac_addresses.clone();
73 }
74
75 if !self.disk_ids.is_empty() {
76 binding.disk_ids = self.disk_ids.clone();
77 }
78
79 if let Some(ref hostname) = self.hostname {
80 binding.hostnames.push(hostname.clone());
81 }
82
83 if let Some(ref machine_id) = self.machine_id {
84 binding
85 .custom
86 .insert("machine_id".to_string(), vec![machine_id.clone()]);
87 }
88
89 binding
90 }
91}
92
93pub fn detect_hardware() -> HardwareInfo {
97 #[cfg(feature = "hardware-detect")]
98 {
99 HardwareInfo {
100 mac_addresses: detect_mac_addresses(),
101 hostname: detect_hostname(),
102 disk_ids: detect_disk_ids(),
103 machine_id: detect_machine_id(),
104 }
105 }
106 #[cfg(not(feature = "hardware-detect"))]
107 {
108 HardwareInfo {
109 mac_addresses: vec![],
110 hostname: None,
111 disk_ids: vec![],
112 machine_id: None,
113 }
114 }
115}
116
117#[cfg(feature = "hardware-detect")]
118fn detect_mac_addresses() -> Vec<String> {
120 let mut macs = Vec::new();
121
122 if let Ok(Some(mac)) = mac_address::get_mac_address() {
124 macs.push(mac.to_string().to_uppercase());
125 }
126
127 if let Ok(Some(mac)) = mac_address::mac_address_by_name("eth0") {
129 let mac_str = mac.to_string().to_uppercase();
130 if !macs.contains(&mac_str) {
131 macs.push(mac_str);
132 }
133 }
134
135 use sysinfo::Networks;
137 let networks = Networks::new_with_refreshed_list();
138
139 for (interface_name, _data) in networks.iter() {
140 if let Ok(Some(mac)) = mac_address::mac_address_by_name(interface_name) {
142 let mac_str = mac.to_string().to_uppercase();
143 if !macs.contains(&mac_str) && !mac_str.starts_with("00:00:00") {
144 macs.push(mac_str);
145 }
146 }
147 }
148
149 macs
150}
151
152#[cfg(feature = "hardware-detect")]
153fn detect_hostname() -> Option<String> {
155 hostname::get()
156 .ok()
157 .and_then(|h| h.into_string().ok())
158 .map(|s| s.to_lowercase())
159}
160
161#[cfg(feature = "hardware-detect")]
162fn detect_disk_ids() -> Vec<String> {
164 let mut disk_ids = Vec::new();
165
166 use sysinfo::Disks;
167 let disks = Disks::new_with_refreshed_list();
168
169 for disk in disks.iter() {
170 let name = disk.name().to_string_lossy().to_string();
172 if !name.is_empty() && !disk_ids.contains(&name) {
173 disk_ids.push(name);
174 }
175 }
176
177 #[cfg(target_os = "linux")]
179 {
180 if let Ok(entries) = std::fs::read_dir("/sys/block") {
181 for entry in entries.flatten() {
182 let path = entry.path().join("device/serial");
183 if let Ok(serial) = std::fs::read_to_string(&path) {
184 let serial = serial.trim().to_string();
185 if !serial.is_empty() && !disk_ids.contains(&serial) {
186 disk_ids.push(serial);
187 }
188 }
189 }
190 }
191 }
192
193 disk_ids
194}
195
196#[cfg(feature = "hardware-detect")]
197fn detect_machine_id() -> Option<String> {
199 #[cfg(target_os = "linux")]
201 {
202 if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
203 return Some(id.trim().to_string());
204 }
205 if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
206 return Some(id.trim().to_string());
207 }
208 }
209
210 #[cfg(target_os = "macos")]
212 {
213 use std::process::Command;
214 if let Ok(output) = Command::new("ioreg")
215 .args(["-rd1", "-c", "IOPlatformExpertDevice"])
216 .output()
217 {
218 let stdout = String::from_utf8_lossy(&output.stdout);
219 for line in stdout.lines() {
220 if line.contains("IOPlatformUUID") {
221 if let Some(start) = line.find('"') {
222 if let Some(end) = line.rfind('"') {
223 if start < end {
224 return Some(line[start + 1..end].to_string());
225 }
226 }
227 }
228 }
229 }
230 }
231 }
232
233 #[cfg(target_os = "windows")]
235 {
236 use std::process::Command;
237 if let Ok(output) = Command::new("wmic")
238 .args(["csproduct", "get", "UUID"])
239 .output()
240 {
241 let stdout = String::from_utf8_lossy(&output.stdout);
242 for line in stdout.lines().skip(1) {
243 let uuid = line.trim();
244 if !uuid.is_empty() && uuid != "UUID" {
245 return Some(uuid.to_string());
246 }
247 }
248 }
249 }
250
251 None
252}
253
254pub fn verify_hardware_binding(
256 binding: &HardwareBinding,
257 current: &HardwareInfo,
258) -> Result<(), HardwareBindingError> {
259 if binding.is_empty() {
261 return Ok(());
262 }
263
264 if !binding.mac_addresses.is_empty() {
266 let current_macs: Vec<String> = current
267 .mac_addresses
268 .iter()
269 .map(|m| m.to_uppercase())
270 .collect();
271
272 let has_match = binding
273 .mac_addresses
274 .iter()
275 .any(|bound| current_macs.contains(&bound.to_uppercase()));
276
277 if !has_match {
278 return Err(HardwareBindingError::MacAddressMismatch {
279 expected: binding.mac_addresses.clone(),
280 found: current.mac_addresses.clone(),
281 });
282 }
283 }
284
285 if !binding.hostnames.is_empty() {
287 if let Some(ref current_hostname) = current.hostname {
288 let has_match = binding
289 .hostnames
290 .iter()
291 .any(|bound| bound.eq_ignore_ascii_case(current_hostname));
292
293 if !has_match {
294 return Err(HardwareBindingError::HostnameMismatch {
295 expected: binding.hostnames.clone(),
296 found: current_hostname.clone(),
297 });
298 }
299 } else {
300 return Err(HardwareBindingError::HostnameMismatch {
301 expected: binding.hostnames.clone(),
302 found: "<unknown>".to_string(),
303 });
304 }
305 }
306
307 if !binding.disk_ids.is_empty() {
309 let has_match = binding
310 .disk_ids
311 .iter()
312 .any(|bound| current.disk_ids.contains(bound));
313
314 if !has_match {
315 return Err(HardwareBindingError::DiskIdMismatch {
316 expected: binding.disk_ids.clone(),
317 found: current.disk_ids.clone(),
318 });
319 }
320 }
321
322 for (key, expected_values) in &binding.custom {
324 if key == "machine_id" {
325 if let Some(ref current_id) = current.machine_id {
326 if !expected_values.contains(current_id) {
327 return Err(HardwareBindingError::CustomMismatch {
328 key: key.clone(),
329 expected: expected_values.clone(),
330 found: current_id.clone(),
331 });
332 }
333 }
334 }
335 }
336
337 Ok(())
338}
339
340#[derive(Debug, Clone)]
342pub enum HardwareBindingError {
343 MacAddressMismatch {
344 expected: Vec<String>,
345 found: Vec<String>,
346 },
347 HostnameMismatch {
348 expected: Vec<String>,
349 found: String,
350 },
351 DiskIdMismatch {
352 expected: Vec<String>,
353 found: Vec<String>,
354 },
355 CustomMismatch {
356 key: String,
357 expected: Vec<String>,
358 found: String,
359 },
360}
361
362impl std::fmt::Display for HardwareBindingError {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 match self {
365 Self::MacAddressMismatch { expected, found } => {
366 write!(
367 f,
368 "MAC address mismatch: expected one of {:?}, found {:?}",
369 expected, found
370 )
371 }
372 Self::HostnameMismatch { expected, found } => {
373 write!(
374 f,
375 "Hostname mismatch: expected one of {:?}, found {}",
376 expected, found
377 )
378 }
379 Self::DiskIdMismatch { expected, found } => {
380 write!(
381 f,
382 "Disk ID mismatch: expected one of {:?}, found {:?}",
383 expected, found
384 )
385 }
386 Self::CustomMismatch {
387 key,
388 expected,
389 found,
390 } => {
391 write!(
392 f,
393 "Custom binding '{}' mismatch: expected one of {:?}, found {}",
394 key, expected, found
395 )
396 }
397 }
398 }
399}
400
401impl std::error::Error for HardwareBindingError {}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn test_empty_binding_always_passes() {
409 let binding = HardwareBinding::new();
410 let hardware = HardwareInfo::default();
411
412 assert!(verify_hardware_binding(&binding, &hardware).is_ok());
413 }
414
415 #[test]
416 fn test_mac_address_binding() {
417 let binding = HardwareBinding::new().with_mac_address("AA:BB:CC:DD:EE:FF");
418
419 let mut hardware = HardwareInfo {
420 mac_addresses: vec!["AA:BB:CC:DD:EE:FF".to_string()],
421 ..Default::default()
422 };
423
424 assert!(verify_hardware_binding(&binding, &hardware).is_ok());
425
426 hardware.mac_addresses = vec!["11:22:33:44:55:66".to_string()];
427 assert!(verify_hardware_binding(&binding, &hardware).is_err());
428 }
429
430 #[test]
431 fn test_hostname_binding() {
432 let binding = HardwareBinding::new().with_hostname("my-server");
433
434 let mut hardware = HardwareInfo {
435 hostname: Some("my-server".to_string()),
436 ..Default::default()
437 };
438
439 assert!(verify_hardware_binding(&binding, &hardware).is_ok());
440
441 hardware.hostname = Some("other-server".to_string());
442 assert!(verify_hardware_binding(&binding, &hardware).is_err());
443 }
444}