Files
MoonTVPlus/src/instrumentation.ts
2025-08-20 21:06:37 +08:00

576 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable */
/**
* Next.js Instrumentation Hook
* 在应用启动时执行关键检查,失败时立即退出
*/
import * as crypto from 'crypto';
// 认证相关接口定义
export interface APIResponse {
success: boolean;
message: string;
data?: any;
timestamp: number;
signature: string;
server_fingerprint: string;
}
export interface ServerInfo {
encrypted_public_key: string;
fingerprint: string;
encryption_method: string;
note: string;
}
// API密钥 - 用于解密公钥
const API_SECRET = 'moontv-is-the-best';
// 验证服务器地址
const AUTH_SERVER = 'https://moontv-auth.ihtw.moe';
// 全局变量存储公钥和指纹
let serverPublicKey: crypto.KeyObject | null = null;
let expectedFingerprint = '';
// 验证相关的全局变量
let currentMachineCode = '';
/**
* 使用AES-GCM解密数据
*/
function decryptWithAES(encryptedData: string, key: string): string {
try {
// 将密钥转换为32字节SHA256哈希
const keyHash = crypto.createHash('sha256').update(key).digest();
// Base64解码密文
const encryptedBytes = Buffer.from(encryptedData, 'base64');
// 提取nonce前12字节和密文
const nonceSize = 12;
const nonce = encryptedBytes.slice(0, nonceSize);
const ciphertext = encryptedBytes.slice(nonceSize, -16); // 除去最后16字节的认证标签
const tag = encryptedBytes.slice(-16); // 最后16字节是认证标签
// 创建AES-GCM解密器
const decipher = crypto.createDecipheriv('aes-256-gcm', keyHash, nonce);
decipher.setAuthTag(tag);
const decrypted = decipher.update(ciphertext);
const final = decipher.final();
// 合并 Buffer 并转换为字符串
const result = Buffer.concat([decrypted, final]);
return result.toString('utf8');
} catch (error) {
throw new Error(`AES解密失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 从验证服务器获取公钥
*/
async function fetchServerPublicKey(): Promise<{ publicKey: string, fingerprint: string }> {
try {
// 设置10秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${AUTH_SERVER}/api/public_key`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'MoonTV/1.0.0'
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const apiResp: APIResponse = await response.json();
if (!apiResp.success) {
throw new Error(`API错误: ${apiResp.message}`);
}
const serverInfo = apiResp.data as ServerInfo;
const encryptedPublicKey = serverInfo.encrypted_public_key;
const serverFingerprint = serverInfo.fingerprint;
const decryptedPublicKeyPem = decryptWithAES(encryptedPublicKey, API_SECRET);
console.log('✅ 公钥解密成功');
return { publicKey: decryptedPublicKeyPem, fingerprint: serverFingerprint };
} catch (error) {
throw new Error(`获取服务器公钥失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 验证API响应的签名
*/
async function verifyResponse(apiResp: APIResponse, requestTimestamp: string): Promise<void> {
if (!serverPublicKey) {
throw new Error('未获取服务器公钥');
}
// 验证服务器指纹
if (expectedFingerprint && apiResp.server_fingerprint !== expectedFingerprint) {
throw new Error('服务器指纹不匹配,可能是伪造的服务器');
}
try {
// 现在服务端只对时间戳字符串进行签名,而不是整个响应对象
// 使用我们发送请求时的时间戳,而不是响应中的时间戳
const timestampToVerify = requestTimestamp;
const verified = await verifyTimestampSignature(timestampToVerify, apiResp.signature);
if (!verified) {
throw new Error('时间戳签名验证失败');
}
} catch (error) {
throw new Error(`签名验证失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 验证时间戳的RSA签名服务端现在只对时间戳字符串进行签名
*/
async function verifyTimestampSignature(timestamp: string, signature: string): Promise<boolean> {
try {
if (!serverPublicKey) {
console.error('❌ 服务器公钥未初始化');
return false;
}
// 将时间戳转换为字符串与Go服务端保持一致
const timestampString = String(timestamp);
// 将十六进制签名转换为Buffer
const signatureBuffer = Buffer.from(signature, 'hex');
// 使用正确的方法:验证原始时间戳字符串
// Go服务端实际上是对原始时间戳字符串进行签名的
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(timestampString, 'utf8');
const result = verifier.verify(serverPublicKey, signatureBuffer);
return result;
} catch (error) {
console.error('❌ 时间戳签名验证出错:', error);
return false;
}
}
/**
* 模拟Go的json.Marshal行为进行JSON序列化
* Go对map[string]interface{}会按键的字母顺序排序
*/
function serializeAsGoJsonMarshal(obj: any): string {
if (obj === null) return 'null';
if (obj === undefined) return 'undefined';
if (typeof obj === 'string') {
return JSON.stringify(obj);
}
if (typeof obj === 'number' || typeof obj === 'boolean') {
return String(obj);
}
// 处理BigInt类型
if (typeof obj === 'bigint') {
return String(obj);
}
if (Array.isArray(obj)) {
const items = obj.map(item => serializeAsGoJsonMarshal(item));
return '[' + items.join(',') + ']';
}
if (typeof obj === 'object') {
// 按键的字母顺序排序Go的map[string]interface{}行为)
const sortedKeys = Object.keys(obj).sort();
const pairs: string[] = [];
for (const key of sortedKeys) {
if (obj[key] !== undefined) {
const serializedKey = JSON.stringify(key);
const serializedValue = serializeAsGoJsonMarshal(obj[key]);
pairs.push(`${serializedKey}:${serializedValue}`);
}
}
return '{' + pairs.join(',') + '}';
}
// 处理其他类型包括可能的BigInt
try {
return JSON.stringify(obj);
} catch (error) {
// 如果JSON.stringify失败比如因为BigInt尝试转换为字符串
if (error instanceof TypeError && error.message.includes('BigInt')) {
return String(obj);
}
throw error;
}
}
/**
* 注册设备到认证服务器
*/
async function registerDevice(authCode: string, deviceCode: string) {
try {
// 生成请求时间戳
const requestTimestamp = Date.now().toString();
// 设置10秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`${AUTH_SERVER}/api/register_device`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'MoonTV/1.0.0'
},
body: JSON.stringify({
auth_code: authCode,
device_code: deviceCode,
timestamp: requestTimestamp
}),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const responseBody = await response.text();
const apiResp: APIResponse = JSON.parse(responseBody);
// 验证响应签名(使用我们发送的时间戳)
await verifyResponse(apiResp, requestTimestamp);
if (!apiResp.success) {
throw new Error(`设备注册失败: ${apiResp.message}`);
}
console.log(`✅ 设备注册成功`);
} catch (error) {
throw new Error(`设备注册失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 环境变量检查
*/
function checkEnvironment(): void {
// 检查 USERNAME
const username = process.env.USERNAME;
if (!username || username.trim() === '') {
console.error('❌ USERNAME 环境变量不得为空');
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
// 检查 PASSWORD
const password = process.env.PASSWORD;
if (!password || password.trim() === '') {
console.error('❌ PASSWORD 环境变量不得为空');
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
// 检查弱密码
const weakPasswords = [
'admin_password',
'password',
'123456',
'admin',
'root',
'password123',
'12345678',
'qwerty',
'abc123',
'admin123',
'test123',
'password1',
'000000',
'111111',
'11111111112233',
'112233',
'123123',
'123321',
'654321',
'666666',
'888888',
'abcdef',
'abcabc',
'a1b2c3',
'aaa111',
'123qwe',
'qweasd'
];
if (weakPasswords.includes(password.toLowerCase())) {
console.error(`❌ PASSWORD 不能使用常见弱密码: ${password}`);
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
if (password.length < 8) {
console.error('❌ PASSWORD 长度不能少于8位');
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
// 检查密码不能与用户名相同
if (password.toLowerCase() === username.toLowerCase()) {
console.error('❌ PASSWORD 不能与 USERNAME 相同');
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
// 检查 AUTH_TOKEN
const authToken = process.env.AUTH_TOKEN;
if (!authToken || authToken.trim() === '') {
console.error('❌ AUTH_TOKEN 不得为空');
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
// 检查 AUTH_SERVER可选但如果设置了需要验证格式
const authServer = process.env.AUTH_SERVER;
if (authServer && authServer.trim() !== '') {
if (!authServer.startsWith('https://') && !authServer.startsWith('http://')) {
console.error('❌ AUTH_SERVER 必须以 http:// 或 https:// 开头');
console.error('🚨 环境变量检查失败,服务器即将退出');
process.exit(0);
}
}
}
/**
* 认证检查
*/
async function checkAuthentication(): Promise<void> {
// 获取环境变量
const authToken = process.env.AUTH_TOKEN;
const username = process.env.USERNAME;
const password = process.env.PASSWORD;
if (!authToken || !username || !password) {
console.error('❌ 认证检查失败:缺少必需的环境变量');
console.error('🚨 认证检查失败,服务器即将退出');
process.exit(0);
}
try {
// 第一步生成机器码包含存储URL信息
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
let storageUrl = '';
// 根据存储类型获取对应的URL
switch (storageType) {
case 'kvrocks':
storageUrl = process.env.KVROCKS_URL || '';
break;
case 'upstash':
storageUrl = process.env.UPSTASH_URL || '';
break;
case 'redis':
storageUrl = process.env.REDIS_URL || '';
break;
default:
storageUrl = 'localstorage';
}
const combinedString = authToken + username + password + storageUrl;
const encoder = new TextEncoder();
const data = encoder.encode(combinedString);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
const machineCode = hashHex.substring(0, 16);
currentMachineCode = machineCode; // 保存到全局变量
// 第二步:从验证服务器获取公钥
const { publicKey, fingerprint } = await fetchServerPublicKey();
// 设置全局变量供签名验证使用
// 将PEM格式的公钥字符串转换为KeyObject
try {
serverPublicKey = crypto.createPublicKey({
key: publicKey,
format: 'pem',
type: 'spki'
});
} catch (keyError) {
console.error('❌ 公钥KeyObject创建失败:', keyError);
throw new Error(`公钥格式错误: ${keyError instanceof Error ? keyError.message : '未知错误'}`);
}
expectedFingerprint = fingerprint;
console.log('🔑 公钥获取成功,准备进行设备注册');
// 第三步:注册设备
// 使用机器码作为认证码和设备码
const deviceCode = machineCode;
await registerDevice(authToken, deviceCode);
console.log('🎉 设备认证流程完成');
} catch (error) {
console.error('❌ 认证流程失败:', error instanceof Error ? error.message : '未知错误');
console.error('🚨 认证检查失败,服务器即将退出');
process.exit(0);
}
}
/**
* 数据库配置检查
*/
function checkDatabaseConfig(): void {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
// 检查存储类型配置
const allowedStorageTypes = ['localstorage', 'kvrocks', 'upstash', 'redis'];
if (!allowedStorageTypes.includes(storageType)) {
console.error(`❌ NEXT_PUBLIC_STORAGE_TYPE 必须是 ${allowedStorageTypes.join(', ')} 之一,当前值: ${storageType}`);
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
// 根据存储类型检查相应的环境变量
switch (storageType) {
case 'kvrocks':
const kvrocksUrl = process.env.KVROCKS_URL;
if (!kvrocksUrl || kvrocksUrl.trim() === '') {
console.error('❌ KVROCKS_URL 环境变量不得为空');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
if (!kvrocksUrl.startsWith('redis://')) {
console.error('❌ KVROCKS_URL 必须以 redis:// 开头');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
break;
case 'upstash':
const upstashUrl = process.env.UPSTASH_URL;
const upstashToken = process.env.UPSTASH_TOKEN;
if (!upstashUrl || upstashUrl.trim() === '') {
console.error('❌ UPSTASH_URL 环境变量不得为空');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
if (!upstashUrl.startsWith('https://')) {
console.error('❌ UPSTASH_URL 必须以 https:// 开头');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
if (!upstashToken || upstashToken.trim() === '') {
console.error('❌ UPSTASH_TOKEN 环境变量不得为空');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
break;
case 'redis':
const redisUrl = process.env.REDIS_URL;
if (!redisUrl || redisUrl.trim() === '') {
console.error('❌ REDIS_URL 环境变量不得为空');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
if (!redisUrl.startsWith('redis://') && !redisUrl.startsWith('rediss://')) {
console.error('❌ REDIS_URL 必须以 redis:// 或 rediss:// 开头');
console.error('🚨 数据库配置检查失败,服务器即将退出');
process.exit(0);
}
break;
}
}
/**
* 执行启动检查并在失败时退出
*/
async function runCriticalStartupChecks(): Promise<void> {
console.log('🔧 执行关键启动检查...');
// 1. 环境变量检查
console.log('📝 检查环境变量...');
checkEnvironment();
console.log('✅ 环境变量检查通过');
// 2. 数据库配置检查
console.log('🗄️ 检查数据库配置...');
checkDatabaseConfig();
console.log('✅ 数据库配置检查通过');
// 3. 认证检查
console.log('🔐 检查认证信息...');
await checkAuthentication();
console.log('✅ 认证检查通过');
console.log('🎉 所有关键检查通过,服务器正常启动');
}
/**
* Next.js Instrumentation Hook
* 这个函数会在应用启动时自动被 Next.js 调用
*/
export async function register() {
// 只在服务器端运行
if (typeof window === 'undefined' && typeof process !== 'undefined' && process.on && typeof process.on === 'function') {
console.log('🚀 MoonTV 启动检查开始...');
// 注册进程退出事件处理
process.on('SIGINT', () => {
console.log('\n🛑 收到 SIGINT 信号,正在优雅关闭...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 收到 SIGTERM 信号,正在优雅关闭...');
process.exit(0);
});
try {
await runCriticalStartupChecks();
} catch (error) {
console.error('💥 启动检查过程中发生未预期错误:', error);
console.error('🚨 服务器即将退出');
process.exit(0);
}
}
}
// 导出检查函数供其他模块使用(如果需要)
export {
checkAuthentication,
checkDatabaseConfig,
checkEnvironment,
decryptWithAES,
fetchServerPublicKey,
verifyResponse,
verifyTimestampSignature,
serializeAsGoJsonMarshal
};