feat: ready for public

This commit is contained in:
shinya
2025-08-26 22:53:04 +08:00
parent 56edd35675
commit 2a338f1bab
11 changed files with 134 additions and 1461 deletions

View File

@@ -11,318 +11,6 @@ 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<void> {
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<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;
}
}
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<void> {
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.status === 401) {
console.log('❌ 设备验证失败401');
process.exit(0);
}
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(0);
}
// 重置网络失败计数
networkFailureCount = 0;
console.log(`✅ 设备验证通过,用户数量: ${userCount}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
// 网络问题
networkFailureCount++;
console.warn(`⚠️ 网络验证失败 (${networkFailureCount}/${MAX_NETWORK_FAILURES}): ${errorMessage}`);
if (networkFailureCount >= MAX_NETWORK_FAILURES) {
console.error('❌ 网络验证失败次数超过限制,重置认证信息');
process.exit(0);
}
}
}
/**
* 初始化设备认证信息
*/
async function initializeDeviceAuth(): Promise<void> {
// 如果已经初始化过,直接返回
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;
}
// 生成机器码包含存储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();
// 设置全局变量供签名验证使用
try {
serverPublicKey = crypto.createPublicKey({
key: publicKey,
format: 'pem',
type: 'spki'
});
} catch (keyError) {
console.error('❌ 公钥KeyObject创建失败:', keyError);
process.exit(0);
}
expectedFingerprint = fingerprint;
// 标记为已初始化
isDeviceAuthInitialized = true;
console.log('🔑 设备认证信息初始化成功');
} catch (error) {
console.error('❌ 设备认证信息初始化失败:', error);
process.exit(0);
}
}
export async function GET(request: NextRequest) {
console.log(request.url);
try {
@@ -351,13 +39,6 @@ export async function GET(request: NextRequest) {
}
async function cronJob() {
// 初始化设备认证信息
await initializeDeviceAuth();
// 执行设备验证
await verifyDevice();
// 执行其他定时任务
await refreshConfig();
await refreshAllLiveChannels();
await refreshRecordAndFavorites();