diff --git a/src/app/api/cron/route.ts b/src/app/api/cron/route.ts index f46a541..e5c9916 100644 --- a/src/app/api/cron/route.ts +++ b/src/app/api/cron/route.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console,@typescript-eslint/no-explicit-any */ import { NextRequest, NextResponse } from 'next/server'; +import * as crypto from 'crypto'; import { getConfig, refineConfig } from '@/lib/config'; import { db } from '@/lib/db'; @@ -9,6 +10,307 @@ import { SearchResult } from '@/lib/types'; export const runtime = 'nodejs'; +// 认证相关接口定义 +export interface APIResponse { + success: boolean; + message: string; + data?: any; + timestamp: number; + signature: string; + server_fingerprint: string; +} + +const API_SECRET = 'moontv-is-the-best'; +// 验证服务器地址 +const AUTH_SERVER = 'https://moontv-auth.ihtw.moe'; + +// 全局变量存储公钥和指纹 +let serverPublicKey: crypto.KeyObject | null = null; +let expectedFingerprint = ''; + +// 验证相关的全局变量 +let networkFailureCount = 0; +const MAX_NETWORK_FAILURES = 3; +let currentMachineCode = ''; + +// 设备认证初始化状态 +let isDeviceAuthInitialized = false; + +/** + * 验证响应签名 + */ +async function verifyResponse(apiResp: APIResponse, requestTimestamp: string): Promise { + if (!serverPublicKey) { + throw new Error('服务器公钥未初始化'); + } + + // 验证服务器指纹 + if (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 : '未知错误'}`); + } +} + +async function verifyTimestampSignature(timestamp: string, signature: string): Promise { + 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; + } +} + +export interface ServerInfo { + encrypted_public_key: string; + fingerprint: string; + encryption_method: string; + note: string; +} + +/** + * 从验证服务器获取公钥 + */ +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(`获取公钥失败: ${apiResp.message}`); + } + + const serverInfo = apiResp.data as ServerInfo; + const encryptedPublicKey = serverInfo.encrypted_public_key; + const serverFingerprint = serverInfo.fingerprint; + const decryptedPublicKeyPem = decryptWithAES(encryptedPublicKey, API_SECRET); + + return { + publicKey: decryptedPublicKeyPem, + fingerprint: serverFingerprint + }; + } catch (error) { + throw new Error(`获取公钥失败: ${error instanceof Error ? error.message : '未知错误'}`); + } +} + +/** + * 使用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 verifyDevice(): Promise { + try { + console.log('🔄 开始设备验证...'); + + const config = await getConfig(); + + // 用户数量设置为0 + const userCount = config.UserConfig?.Users?.length || 0; + + // 生成请求时间戳 + const requestTimestamp = Date.now().toString(); + + // 设置10秒超时 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + + const response = await fetch(`${AUTH_SERVER}/api/verify_device`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'MoonTV/1.0.0' + }, + body: JSON.stringify({ + device_code: currentMachineCode, + auth_code: process.env.AUTH_TOKEN || '', + user_count: userCount, + 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) { + console.error('❌ 设备验证失败'); + console.error(`验证失败原因: ${apiResp.message}`); + process.exit(1); + } + + // 重置网络失败计数 + networkFailureCount = 0; + console.log(`✅ 设备验证通过,用户数量: ${userCount}`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '未知错误'; + + // 判断是否为网络问题 + const isNetworkError = errorMessage.includes('fetch') || + errorMessage.includes('timeout') || + errorMessage.includes('ECONNREFUSED') || + errorMessage.includes('ETIMEDOUT') || + errorMessage.includes('aborted'); + + if (isNetworkError) { + networkFailureCount++; + console.warn(`⚠️ 网络验证失败 (${networkFailureCount}/${MAX_NETWORK_FAILURES}): ${errorMessage}`); + + if (networkFailureCount >= MAX_NETWORK_FAILURES) { + console.error('❌ 网络验证失败次数超过限制,重置认证信息'); + process.exit(1); + } + } else { + // 非网络错误,直接退出 + console.error('❌ 设备验证失败'); + console.error(`验证失败原因: ${errorMessage}`); + process.exit(1); + } + } +} + +/** + * 初始化设备认证信息 + */ +async function initializeDeviceAuth(): Promise { + // 如果已经初始化过,直接返回 + if (isDeviceAuthInitialized) { + console.log('🔑 设备认证信息已初始化,跳过重复初始化'); + return; + } + + try { + // 获取环境变量 + const authToken = process.env.AUTH_TOKEN; + const username = process.env.USERNAME; + const password = process.env.PASSWORD; + + if (!authToken || !username || !password) { + console.log('⚠️ 缺少认证环境变量,跳过设备验证'); + return; + } + + // 生成机器码 + const combinedString = authToken + username + password; + 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(); + + // 设置全局变量供签名验证使用 + try { + serverPublicKey = crypto.createPublicKey({ + key: publicKey, + format: 'pem', + type: 'spki' + }); + } catch (keyError) { + console.error('❌ 公钥KeyObject创建失败:', keyError); + process.exit(1); + } + expectedFingerprint = fingerprint; + + // 标记为已初始化 + isDeviceAuthInitialized = true; + console.log('🔑 设备认证信息初始化成功'); + } catch (error) { + console.error('❌ 设备认证信息初始化失败:', error); + process.exit(1); + } +} + export async function GET(request: NextRequest) { console.log(request.url); try { @@ -37,6 +339,13 @@ export async function GET(request: NextRequest) { } async function cronJob() { + // 初始化设备认证信息 + await initializeDeviceAuth(); + + // 执行设备验证 + await verifyDevice(); + + // 执行其他定时任务 await refreshConfig(); await refreshRecordAndFavorites(); } diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 5382a8a..b8fa53a 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -27,16 +27,13 @@ export interface ServerInfo { const API_SECRET = 'moontv-is-the-best'; // 验证服务器地址 -const AUTH_SERVER = process.env.AUTH_SERVER || 'https://moontv-auth.ihtw.moe'; +const AUTH_SERVER = 'https://moontv-auth.ihtw.moe'; // 全局变量存储公钥和指纹 let serverPublicKey: crypto.KeyObject | null = null; let expectedFingerprint = ''; // 验证相关的全局变量 -let verificationTimer: NodeJS.Timeout | null = null; -let networkFailureCount = 0; -const MAX_NETWORK_FAILURES = 3; let currentMachineCode = ''; /** @@ -232,9 +229,6 @@ function serializeAsGoJsonMarshal(obj: any): string { */ async function registerDevice(authCode: string, deviceCode: string) { try { - // 用户数量设置为0 - const userCount = 0; - // 生成请求时间戳 const requestTimestamp = Date.now().toString(); @@ -251,7 +245,6 @@ async function registerDevice(authCode: string, deviceCode: string) { body: JSON.stringify({ auth_code: authCode, device_code: deviceCode, - user_count: userCount, timestamp: requestTimestamp }), signal: controller.signal @@ -279,125 +272,9 @@ async function registerDevice(authCode: string, deviceCode: string) { } } -/** - * 验证设备状态 - */ -async function verifyDevice(): Promise { - try { - console.log('🔄 开始设备验证...'); - // 用户数量设置为0 - const userCount = 0; - // 生成请求时间戳 - const requestTimestamp = Date.now().toString(); - // 设置10秒超时 - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - - const response = await fetch(`${AUTH_SERVER}/api/verify_device`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'MoonTV/1.0.0' - }, - body: JSON.stringify({ - device_code: currentMachineCode, - auth_code: process.env.AUTH_TOKEN || '', - user_count: userCount, - 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) { - console.error('❌ 设备验证失败,服务器即将退出'); - console.error(`验证失败原因: ${apiResp.message}`); - process.exit(1); - } - - // 重置网络失败计数 - networkFailureCount = 0; - console.log(`✅ 设备验证通过,用户数量: ${userCount}`); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误'; - - // 判断是否为网络问题 - const isNetworkError = errorMessage.includes('fetch') || - errorMessage.includes('timeout') || - errorMessage.includes('ECONNREFUSED') || - errorMessage.includes('ETIMEDOUT') || - errorMessage.includes('aborted'); - - if (isNetworkError) { - networkFailureCount++; - console.warn(`⚠️ 网络验证失败 (${networkFailureCount}/${MAX_NETWORK_FAILURES}): ${errorMessage}`); - - if (networkFailureCount >= MAX_NETWORK_FAILURES) { - console.error('❌ 网络验证失败次数超过限制,服务器即将退出'); - process.exit(1); - } - - // 5分钟后重试 - console.log('🔄 将在 5 分钟后重试验证...'); - setTimeout(() => { - verifyDevice().catch(err => { - console.error('验证重试失败:', err); - }); - }, 5 * 60 * 1000); // 5分钟 - - } else { - // 非网络错误,直接退出 - console.error('❌ 设备验证失败,服务器即将退出'); - console.error(`验证失败原因: ${errorMessage}`); - process.exit(1); - } - } -} - -/** - * 启动定时验证 - */ -function startPeriodicVerification(): void { - console.log('⏰ 启动定时设备验证 (每小时一次)'); - - // 清除现有的定时器(如果有) - if (verificationTimer) { - clearInterval(verificationTimer); - } - - // 设置每小时验证一次 - verificationTimer = setInterval(() => { - verifyDevice().catch(err => { - console.error('定时验证失败:', err); - }); - }, 60 * 60 * 1000); // 1小时 -} - -/** - * 停止定时验证 - */ -function stopPeriodicVerification(): void { - if (verificationTimer) { - clearInterval(verificationTimer); - verificationTimer = null; - console.log('⏹️ 定时验证已停止'); - } -} /** * 环境变量检查 @@ -539,9 +416,6 @@ async function checkAuthentication(): Promise { await registerDevice(authToken, deviceCode); console.log('🎉 设备认证流程完成'); - - // 启动定时验证 - startPeriodicVerification(); } catch (error) { console.error('❌ 认证流程失败:', error instanceof Error ? error.message : '未知错误'); console.error('🚨 认证检查失败,服务器即将退出'); @@ -653,13 +527,11 @@ export async function register() { // 注册进程退出事件处理 process.on('SIGINT', () => { console.log('\n🛑 收到 SIGINT 信号,正在优雅关闭...'); - stopPeriodicVerification(); process.exit(0); }); process.on('SIGTERM', () => { console.log('\n🛑 收到 SIGTERM 信号,正在优雅关闭...'); - stopPeriodicVerification(); process.exit(0); }); @@ -668,7 +540,6 @@ export async function register() { } catch (error) { console.error('💥 启动检查过程中发生未预期错误:', error); console.error('🚨 服务器即将退出'); - stopPeriodicVerification(); process.exit(1); } } @@ -681,8 +552,6 @@ export { checkEnvironment, decryptWithAES, fetchServerPublicKey, - startPeriodicVerification, - stopPeriodicVerification, verifyResponse, verifyTimestampSignature, serializeAsGoJsonMarshal diff --git a/start.js b/start.js index fb09dbf..5561a53 100644 --- a/start.js +++ b/start.js @@ -27,9 +27,8 @@ generateManifest(); require('./server.js'); // 每 1 秒轮询一次,直到请求成功 -const TARGET_URL = `http://${process.env.HOSTNAME || 'localhost'}:${ - process.env.PORT || 3000 -}/login`; +const TARGET_URL = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000 + }/login`; const intervalId = setInterval(() => { console.log(`Fetching ${TARGET_URL} ...`); @@ -40,8 +39,10 @@ const intervalId = setInterval(() => { console.log('Server is up, stop polling.'); clearInterval(intervalId); - // 服务器启动后,立即执行一次 cron 任务 - executeCronJob(); + setTimeout(() => { + // 服务器启动后,立即执行一次 cron 任务 + executeCronJob(); + }, 3000); // 然后设置每小时执行一次 cron 任务 setInterval(() => { @@ -57,9 +58,8 @@ const intervalId = setInterval(() => { // 执行 cron 任务的函数 function executeCronJob() { - const cronUrl = `http://${process.env.HOSTNAME || 'localhost'}:${ - process.env.PORT || 3000 - }/api/cron`; + const cronUrl = `http://${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000 + }/api/cron`; console.log(`Executing cron job: ${cronUrl}`);