- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
192 lines
4.5 KiB
TypeScript
192 lines
4.5 KiB
TypeScript
import * as os from 'os';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import * as crypto from 'crypto';
|
||
import type { HardwareFingerprint } from './types';
|
||
|
||
// 实例 ID 存储路径
|
||
const INSTANCE_ID_FILE = path.join(process.cwd(), 'data', '.instance-id');
|
||
|
||
/**
|
||
* 获取 machine-id
|
||
* Linux: /etc/machine-id 或 /var/lib/dbus/machine-id
|
||
* Windows: 使用 hostname + 网卡信息生成
|
||
* Docker: 使用容器 ID 或挂载的 machine-id
|
||
*/
|
||
async function getMachineId(): Promise<string> {
|
||
// 尝试读取 Linux machine-id
|
||
const machineIdPaths = [
|
||
'/etc/machine-id',
|
||
'/var/lib/dbus/machine-id',
|
||
];
|
||
|
||
for (const filePath of machineIdPaths) {
|
||
try {
|
||
const machineId = fs.readFileSync(filePath, 'utf8').trim();
|
||
if (machineId) {
|
||
return machineId;
|
||
}
|
||
} catch {
|
||
// 文件不存在,继续尝试下一个
|
||
}
|
||
}
|
||
|
||
// 尝试读取 Docker 容器 ID
|
||
try {
|
||
const cgroupContent = fs.readFileSync('/proc/self/cgroup', 'utf8');
|
||
const match = cgroupContent.match(/docker[/-]([a-f0-9]{64})/);
|
||
if (match) {
|
||
return match[1].substring(0, 32);
|
||
}
|
||
} catch {
|
||
// 不在 Docker 容器中
|
||
}
|
||
|
||
// Windows 或其他系统:使用 hostname + 网卡信息生成
|
||
const hostname = os.hostname();
|
||
const networkInterfaces = os.networkInterfaces();
|
||
const macs: string[] = [];
|
||
|
||
for (const interfaces of Object.values(networkInterfaces)) {
|
||
if (interfaces) {
|
||
for (const iface of interfaces) {
|
||
if (!iface.internal && iface.mac !== '00:00:00:00:00:00') {
|
||
macs.push(iface.mac);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const data = `${hostname}-${macs.sort().join('-')}`;
|
||
return crypto.createHash('md5').update(data).digest('hex');
|
||
}
|
||
|
||
/**
|
||
* 获取主网卡 MAC 地址
|
||
*/
|
||
function getPrimaryMacAddress(): string {
|
||
const networkInterfaces = os.networkInterfaces();
|
||
|
||
// 优先查找常见的网卡名称
|
||
const preferredNames = ['eth0', 'en0', 'ens33', 'enp0s3', 'Ethernet', 'Wi-Fi'];
|
||
|
||
for (const name of preferredNames) {
|
||
const interfaces = networkInterfaces[name];
|
||
if (interfaces) {
|
||
for (const iface of interfaces) {
|
||
if (!iface.internal && iface.mac !== '00:00:00:00:00:00') {
|
||
return iface.mac;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有找到,返回第一个非内部网卡的 MAC
|
||
for (const interfaces of Object.values(networkInterfaces)) {
|
||
if (interfaces) {
|
||
for (const iface of interfaces) {
|
||
if (!iface.internal && iface.mac !== '00:00:00:00:00:00') {
|
||
return iface.mac;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return '00:00:00:00:00:00';
|
||
}
|
||
|
||
/**
|
||
* 获取或创建实例 ID
|
||
* 实例 ID 在首次激活时生成,并持久化存储
|
||
*/
|
||
async function getOrCreateInstanceId(): Promise<string> {
|
||
// 确保目录存在
|
||
const dir = path.dirname(INSTANCE_ID_FILE);
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
|
||
// 尝试读取现有的实例 ID
|
||
try {
|
||
const instanceId = fs.readFileSync(INSTANCE_ID_FILE, 'utf8').trim();
|
||
if (instanceId) {
|
||
return instanceId;
|
||
}
|
||
} catch {
|
||
// 文件不存在
|
||
}
|
||
|
||
// 生成新的实例 ID
|
||
const instanceId = crypto.randomBytes(16).toString('hex');
|
||
fs.writeFileSync(INSTANCE_ID_FILE, instanceId);
|
||
|
||
return instanceId;
|
||
}
|
||
|
||
/**
|
||
* 生成硬件指纹
|
||
*/
|
||
export async function generateFingerprint(): Promise<HardwareFingerprint> {
|
||
const [machineId, instanceId] = await Promise.all([
|
||
getMachineId(),
|
||
getOrCreateInstanceId(),
|
||
]);
|
||
|
||
return {
|
||
machineId,
|
||
hostname: os.hostname(),
|
||
macAddress: getPrimaryMacAddress(),
|
||
instanceId,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取实例 ID
|
||
*/
|
||
export async function getInstanceId(): Promise<string> {
|
||
return getOrCreateInstanceId();
|
||
}
|
||
|
||
/**
|
||
* 计算两个指纹的匹配分数
|
||
* 返回 0-100 的分数
|
||
*/
|
||
export function calculateFingerprintMatchScore(
|
||
stored: HardwareFingerprint,
|
||
current: HardwareFingerprint
|
||
): number {
|
||
let score = 0;
|
||
|
||
// machineId 权重 40%
|
||
if (stored.machineId === current.machineId) {
|
||
score += 40;
|
||
}
|
||
|
||
// hostname 权重 20%
|
||
if (stored.hostname === current.hostname) {
|
||
score += 20;
|
||
}
|
||
|
||
// macAddress 权重 30%
|
||
if (stored.macAddress === current.macAddress) {
|
||
score += 30;
|
||
}
|
||
|
||
// instanceId 权重 10%
|
||
if (stored.instanceId === current.instanceId) {
|
||
score += 10;
|
||
}
|
||
|
||
return score;
|
||
}
|
||
|
||
/**
|
||
* 检查指纹是否匹配(60% 阈值)
|
||
*/
|
||
export function isFingerprintMatch(
|
||
stored: HardwareFingerprint,
|
||
current: HardwareFingerprint
|
||
): boolean {
|
||
return calculateFingerprintMatchScore(stored, current) >= 60;
|
||
}
|