|
import os |
|
import sys |
|
import subprocess |
|
import uuid |
|
import hashlib |
|
import platform |
|
import logging |
|
import re |
|
import json |
|
from pathlib import Path |
|
from typing import Dict, Optional, List, Tuple |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class DeviceFingerprint: |
|
"""设备指纹收集器 - 用于生成唯一的设备标识""" |
|
|
|
def __init__(self): |
|
"""初始化设备指纹收集器""" |
|
self.system = platform.system() |
|
self.fingerprint_cache_file = (Path(__file__).parent.parent.parent / |
|
"config" / ".device_fingerprint") |
|
|
|
def get_hostname(self) -> str: |
|
"""获取计算机主机名""" |
|
return platform.node() |
|
|
|
def get_mac_address(self) -> str: |
|
"""获取MAC地址(小写格式)""" |
|
mac = uuid.UUID(int=uuid.getnode()).hex[-12:] |
|
return ":".join([mac[i:i + 2] for i in range(0, 12, 2)]).lower() |
|
|
|
def get_all_mac_addresses(self) -> List[Dict[str, str]]: |
|
"""获取所有网络适配器的MAC地址""" |
|
mac_addresses = [] |
|
|
|
try: |
|
if self.system == "Windows": |
|
|
|
import wmi |
|
w = wmi.WMI() |
|
for nic in w.Win32_NetworkAdapter(): |
|
if nic.MACAddress: |
|
|
|
mac_addr = nic.MACAddress.lower() if nic.MACAddress else "" |
|
mac_addresses.append({ |
|
"name": nic.Name, |
|
"mac": mac_addr, |
|
"device_id": nic.DeviceID, |
|
"adapter_type": getattr(nic, "AdapterType", ""), |
|
"net_connection_id": getattr(nic, "NetConnectionID", ""), |
|
"physical": nic.PhysicalAdapter if hasattr(nic, "PhysicalAdapter") else False |
|
}) |
|
|
|
except Exception as e: |
|
logger.error(f"获取所有MAC地址时出错: {e}") |
|
|
|
return mac_addresses |
|
|
|
def get_primary_mac_address(self) -> Optional[Tuple[str, str]]: |
|
""" |
|
智能选择最适合的物理网卡MAC地址 |
|
|
|
Returns: |
|
Tuple[str, str]: (MAC地址, 网卡类型描述) |
|
""" |
|
all_macs = self.get_all_mac_addresses() |
|
|
|
|
|
if not all_macs: |
|
return None |
|
|
|
|
|
ethernet_adapters = [] |
|
wifi_adapters = [] |
|
bluetooth_adapters = [] |
|
physical_adapters = [] |
|
virtual_adapters = [] |
|
|
|
for adapter in all_macs: |
|
|
|
name = str(adapter.get("name", "")).lower() |
|
|
|
physical = adapter.get("physical", False) |
|
|
|
|
|
if (any(keyword in name for keyword in |
|
["ethernet", "realtek", "intel", "broadcom"]) and physical): |
|
ethernet_adapters.append(adapter) |
|
elif (any(keyword in name for keyword in |
|
["wi-fi", "wifi", "wireless", "wlan"]) and physical): |
|
wifi_adapters.append(adapter) |
|
elif "bluetooth" in name: |
|
bluetooth_adapters.append(adapter) |
|
elif physical: |
|
physical_adapters.append(adapter) |
|
else: |
|
virtual_adapters.append(adapter) |
|
|
|
|
|
if ethernet_adapters: |
|
return ethernet_adapters[0]["mac"], "有线网卡" |
|
elif wifi_adapters: |
|
return wifi_adapters[0]["mac"], "WiFi网卡" |
|
elif bluetooth_adapters: |
|
return bluetooth_adapters[0]["mac"], "蓝牙适配器" |
|
elif physical_adapters: |
|
return physical_adapters[0]["mac"], "物理网卡" |
|
elif virtual_adapters: |
|
return virtual_adapters[0]["mac"], "虚拟网卡" |
|
|
|
|
|
return all_macs[0]["mac"], "未知类型" |
|
|
|
def get_bluetooth_mac_address(self) -> Optional[str]: |
|
"""获取蓝牙适配器的MAC地址""" |
|
try: |
|
all_macs = self.get_all_mac_addresses() |
|
for mac_info in all_macs: |
|
|
|
if "Bluetooth" in mac_info.get("name", ""): |
|
return mac_info.get("mac") |
|
return None |
|
except Exception as e: |
|
logger.error(f"获取蓝牙MAC地址时出错: {e}") |
|
return None |
|
|
|
def get_cpu_info(self) -> Dict: |
|
"""获取CPU信息""" |
|
cpu_info = { |
|
"processor": platform.processor(), |
|
"machine": platform.machine() |
|
} |
|
|
|
try: |
|
if self.system == "Windows": |
|
|
|
import wmi |
|
w = wmi.WMI() |
|
processor = w.Win32_Processor()[0] |
|
cpu_info["id"] = processor.ProcessorId.strip() |
|
cpu_info["name"] = processor.Name.strip() |
|
cpu_info["cores"] = processor.NumberOfCores |
|
elif self.system == "Linux": |
|
|
|
with open('/proc/cpuinfo', 'r') as f: |
|
info = f.readlines() |
|
|
|
cpu_id = None |
|
model_name = None |
|
cpu_cores = 0 |
|
|
|
for line in info: |
|
if "serial" in line or "Serial" in line: |
|
|
|
cpu_id = line.split(':')[1].strip() |
|
elif "model name" in line: |
|
model_name = line.split(':')[1].strip() |
|
elif "cpu cores" in line: |
|
cpu_cores = int(line.split(':')[1].strip()) |
|
|
|
if cpu_id: |
|
cpu_info["id"] = cpu_id |
|
if model_name: |
|
cpu_info["name"] = model_name |
|
if cpu_cores: |
|
cpu_info["cores"] = cpu_cores |
|
elif self.system == "Darwin": |
|
|
|
cmd = "sysctl -n machdep.cpu.brand_string" |
|
model_name = subprocess.check_output(cmd, shell=True).decode().strip() |
|
|
|
cmd = "sysctl -n hw.physicalcpu" |
|
cpu_cores = int(subprocess.check_output(cmd, shell=True).decode().strip()) |
|
|
|
|
|
cmd = "ioreg -l | grep IOPlatformUUID" |
|
try: |
|
platform_uuid = subprocess.check_output(cmd, shell=True).decode().strip() |
|
uuid_match = re.search(r'IOPlatformUUID" = "([^"]+)"', platform_uuid) |
|
if uuid_match: |
|
cpu_info["id"] = uuid_match.group(1) |
|
except Exception: |
|
pass |
|
|
|
cpu_info["name"] = model_name |
|
cpu_info["cores"] = cpu_cores |
|
except Exception as e: |
|
logger.error(f"获取CPU信息时出错: {e}") |
|
|
|
return cpu_info |
|
|
|
def get_disk_info(self) -> List[Dict]: |
|
"""获取硬盘信息""" |
|
disks = [] |
|
|
|
try: |
|
if self.system == "Windows": |
|
|
|
import wmi |
|
w = wmi.WMI() |
|
for disk in w.Win32_DiskDrive(): |
|
if disk.SerialNumber: |
|
disks.append({ |
|
"model": disk.Model.strip(), |
|
"serial": disk.SerialNumber.strip(), |
|
"size": str(int(disk.Size or 0)) |
|
}) |
|
elif self.system == "Linux": |
|
|
|
cmd = "lsblk -d -o NAME,SERIAL,MODEL,SIZE --json" |
|
try: |
|
result = subprocess.check_output(cmd, shell=True).decode() |
|
data = json.loads(result) |
|
for device in data.get("blockdevices", []): |
|
if device.get("serial"): |
|
disks.append({ |
|
"model": device.get("model", "").strip(), |
|
"serial": device.get("serial").strip(), |
|
"size": device.get("size", "").strip() |
|
}) |
|
except Exception: |
|
|
|
try: |
|
disk_ids = os.listdir('/dev/disk/by-id') |
|
for disk_id in disk_ids: |
|
if (not disk_id.startswith('usb-') and |
|
not disk_id.startswith('nvme-eui') and |
|
not disk_id.startswith('wwn-') and |
|
'part' not in disk_id): |
|
disks.append({ |
|
"model": "Unknown", |
|
"serial": disk_id, |
|
"size": "0" |
|
}) |
|
except Exception: |
|
pass |
|
elif self.system == "Darwin": |
|
|
|
cmd = "diskutil list -plist" |
|
try: |
|
import plistlib |
|
result = subprocess.check_output(cmd, shell=True) |
|
data = plistlib.loads(result) |
|
|
|
for disk in data.get('AllDisksAndPartitions', []): |
|
disk_id = disk.get('DeviceIdentifier') |
|
if not disk_id: |
|
continue |
|
|
|
|
|
cmd = f"diskutil info -plist {disk_id}" |
|
disk_info = subprocess.check_output(cmd, shell=True) |
|
disk_data = plistlib.loads(disk_info) |
|
|
|
serial = (disk_data.get('IORegistryEntryName') or |
|
disk_data.get('MediaName')) |
|
if serial: |
|
disks.append({ |
|
"model": disk_data.get('MediaName', 'Unknown'), |
|
"serial": serial, |
|
"size": str(disk_data.get('TotalSize', 0)) |
|
}) |
|
except Exception as e: |
|
logger.error(f"在macOS上获取磁盘信息时出错: {e}") |
|
except Exception as e: |
|
logger.error(f"获取硬盘信息时出错: {e}") |
|
|
|
return disks |
|
|
|
def get_motherboard_info(self) -> Dict: |
|
"""获取主板信息""" |
|
mb_info = {} |
|
|
|
try: |
|
if self.system == "Windows": |
|
|
|
import wmi |
|
w = wmi.WMI() |
|
for board in w.Win32_BaseBoard(): |
|
mb_info["manufacturer"] = board.Manufacturer.strip() if board.Manufacturer else "" |
|
mb_info["model"] = board.Product.strip() if board.Product else "" |
|
mb_info["serial"] = board.SerialNumber.strip() if board.SerialNumber else "" |
|
break |
|
|
|
|
|
if not mb_info.get("serial"): |
|
for bios in w.Win32_BIOS(): |
|
if bios.SerialNumber: |
|
mb_info["bios_serial"] = bios.SerialNumber.strip() |
|
break |
|
elif self.system == "Linux": |
|
|
|
try: |
|
cmd = "dmidecode -t 2" |
|
result = subprocess.check_output( |
|
cmd, shell=True, stderr=subprocess.PIPE).decode() |
|
for line in result.split('\n'): |
|
if "Manufacturer" in line: |
|
mb_info["manufacturer"] = line.split(':')[1].strip() |
|
elif "Product Name" in line: |
|
mb_info["model"] = line.split(':')[1].strip() |
|
elif "Serial Number" in line: |
|
mb_info["serial"] = line.split(':')[1].strip() |
|
except Exception: |
|
|
|
try: |
|
with open('/sys/class/dmi/id/board_vendor', 'r') as f: |
|
mb_info["manufacturer"] = f.read().strip() |
|
with open('/sys/class/dmi/id/board_name', 'r') as f: |
|
mb_info["model"] = f.read().strip() |
|
with open('/sys/class/dmi/id/board_serial', 'r') as f: |
|
mb_info["serial"] = f.read().strip() |
|
except Exception: |
|
pass |
|
elif self.system == "Darwin": |
|
|
|
try: |
|
cmd = "ioreg -l | grep -E '(board-id|IOPlatformSerialNumber|IOPlatformUUID)'" |
|
result = subprocess.check_output(cmd, shell=True).decode() |
|
|
|
board_id = re.search(r'board-id" = <"([^"]+)">', result) |
|
if board_id: |
|
mb_info["model"] = board_id.group(1) |
|
|
|
serial = re.search(r'IOPlatformSerialNumber" = "([^"]+)"', result) |
|
if serial: |
|
mb_info["serial"] = serial.group(1) |
|
|
|
uuid_match = re.search(r'IOPlatformUUID" = "([^"]+)"', result) |
|
if uuid_match: |
|
mb_info["uuid"] = uuid_match.group(1) |
|
except Exception as e: |
|
logger.error(f"在macOS上获取主板信息时出错: {e}") |
|
except Exception as e: |
|
logger.error(f"获取主板信息时出错: {e}") |
|
|
|
return mb_info |
|
|
|
def generate_fingerprint(self) -> Dict: |
|
"""生成完整的设备指纹""" |
|
|
|
cached_fingerprint = self._load_cached_fingerprint() |
|
if cached_fingerprint: |
|
return cached_fingerprint |
|
|
|
|
|
primary_mac, mac_type = self.get_primary_mac_address() or (self.get_mac_address(), "系统MAC") |
|
|
|
|
|
fingerprint = { |
|
"system": self.system, |
|
"hostname": self.get_hostname(), |
|
"mac_address": self.get_mac_address(), |
|
"primary_mac": primary_mac, |
|
"primary_mac_type": mac_type, |
|
"bluetooth_mac": self.get_bluetooth_mac_address(), |
|
"cpu": self.get_cpu_info(), |
|
"disks": self.get_disk_info(), |
|
"motherboard": self.get_motherboard_info() |
|
} |
|
|
|
|
|
self._cache_fingerprint(fingerprint) |
|
|
|
return fingerprint |
|
|
|
def _load_cached_fingerprint(self) -> Optional[Dict]: |
|
"""从缓存文件加载指纹""" |
|
if not self.fingerprint_cache_file.exists(): |
|
return None |
|
|
|
try: |
|
with open(self.fingerprint_cache_file, 'r', encoding='utf-8') as f: |
|
return json.load(f) |
|
except Exception as e: |
|
logger.error(f"加载缓存的设备指纹时出错: {e}") |
|
return None |
|
|
|
def _cache_fingerprint(self, fingerprint: Dict): |
|
"""缓存指纹到文件""" |
|
try: |
|
|
|
self.fingerprint_cache_file.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
with open(self.fingerprint_cache_file, 'w', encoding='utf-8') as f: |
|
json.dump(fingerprint, f, indent=2, ensure_ascii=False) |
|
|
|
logger.info("设备指纹已缓存到文件") |
|
except Exception as e: |
|
logger.error(f"缓存设备指纹时出错: {e}") |
|
|
|
def generate_hardware_hash(self) -> str: |
|
"""根据硬件信息生成唯一的哈希值""" |
|
fingerprint = self.generate_fingerprint() |
|
|
|
|
|
identifiers = [] |
|
|
|
|
|
hostname = fingerprint.get("hostname") |
|
if hostname: |
|
identifiers.append(hostname) |
|
|
|
|
|
if fingerprint.get("cpu", {}).get("id"): |
|
identifiers.append(fingerprint["cpu"]["id"]) |
|
else: |
|
identifiers.append(fingerprint.get("cpu", {}).get("name", "unknown_cpu")) |
|
|
|
|
|
mb_serial = fingerprint.get("motherboard", {}).get("serial") |
|
if mb_serial and mb_serial != "To be filled by O.E.M.": |
|
identifiers.append(mb_serial) |
|
else: |
|
mb_uuid = fingerprint.get("motherboard", {}).get("uuid") |
|
if mb_uuid: |
|
identifiers.append(mb_uuid) |
|
|
|
|
|
for disk in fingerprint.get("disks", []): |
|
if disk.get("serial") and disk["serial"] != "0000_0000": |
|
identifiers.append(disk["serial"]) |
|
break |
|
|
|
|
|
if len(identifiers) < 2: |
|
identifiers.append(fingerprint.get("mac_address", "")) |
|
|
|
|
|
if fingerprint.get("bluetooth_mac"): |
|
identifiers.append(fingerprint.get("bluetooth_mac")) |
|
|
|
|
|
fingerprint_str = "||".join(identifiers) |
|
return hashlib.sha256(fingerprint_str.encode('utf-8')).hexdigest() |
|
|
|
def generate_serial_number(self) -> Tuple[str, str]: |
|
""" |
|
生成设备序列号 |
|
|
|
Returns: |
|
Tuple[str, str]: (序列号, 生成方法说明) |
|
""" |
|
fingerprint = self.generate_fingerprint() |
|
|
|
|
|
hostname = fingerprint.get("hostname", "") |
|
short_hostname = "".join(c for c in hostname if c.isalnum())[:8] |
|
|
|
|
|
primary_mac = fingerprint.get("primary_mac") |
|
primary_mac_type = fingerprint.get("primary_mac_type", "未知网卡") |
|
|
|
if primary_mac: |
|
|
|
mac_clean = primary_mac.lower().replace(":", "") |
|
short_hash = hashlib.md5(mac_clean.encode()).hexdigest()[:8].upper() |
|
serial_number = f"SN-{short_hash}-{mac_clean}" |
|
return serial_number, primary_mac_type |
|
|
|
|
|
bluetooth_mac = fingerprint.get("bluetooth_mac") |
|
if bluetooth_mac: |
|
|
|
mac_clean = bluetooth_mac.lower().replace(":", "") |
|
short_hash = hashlib.md5(mac_clean.encode()).hexdigest()[:8].upper() |
|
serial_number = f"SN-{short_hash}-{mac_clean}" |
|
return serial_number, "蓝牙MAC地址" |
|
|
|
|
|
mac_address = fingerprint.get("mac_address") |
|
if mac_address: |
|
|
|
mac_clean = mac_address.lower().replace(":", "") |
|
short_hash = hashlib.md5(mac_clean.encode()).hexdigest()[:8].upper() |
|
serial_number = f"SN-{short_hash}-{mac_clean}" |
|
return serial_number, "系统MAC地址" |
|
|
|
|
|
hardware_hash = self.generate_hardware_hash()[:16].upper() |
|
serial_number = f"SN-{hardware_hash}" |
|
return serial_number, "硬件哈希值" |
|
|
|
|
|
|
|
_fingerprint_instance = None |
|
|
|
|
|
def get_device_fingerprint() -> DeviceFingerprint: |
|
"""获取设备指纹实例(单例模式)""" |
|
global _fingerprint_instance |
|
if _fingerprint_instance is None: |
|
_fingerprint_instance = DeviceFingerprint() |
|
return _fingerprint_instance |