From ee597bc10ba1ff65af6e4344459c30d6019557de Mon Sep 17 00:00:00 2001 From: XiaoMo Date: Fri, 5 Sep 2025 17:02:19 +0800 Subject: [PATCH] 1111 --- proxy.js | 1212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1212 insertions(+) create mode 100644 proxy.js diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..9a8c6b6 --- /dev/null +++ b/proxy.js @@ -0,0 +1,1212 @@ +const http = require('http'); +const https = require('https'); +const url = require('url'); +const querystring = require('querystring'); +const fs = require('fs'); +const pathModule = require('path'); +const crypto = require('crypto'); + +const CACHE_DIR_NAME = '.cache'; +const DEFAULT_PORT = 9001; +const DEFAULT_API_ENDPOINT = 'http://183.6.121.121:9519/'; + +// 创建缓存目录结构 +const cacheDir = pathModule.join(__dirname, CACHE_DIR_NAME); +if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); + console.log(`Cache directory created: ${cacheDir}`); +} + +// 根据缓存键创建子目录,避免单个目录文件过多 +function getCacheSubDir(cacheKey) { + // 使用缓存键的前两个字符作为子目录名 + const subDir = cacheKey.substring(0, 2); + const fullPath = pathModule.join(cacheDir, subDir); + if (!fs.existsSync(fullPath)) { + fs.mkdirSync(fullPath, { recursive: true }); + } + return fullPath; +} + +// 缓存配置 +const META_CACHE_EXPIRY_MS = 30 * 60 * 1000; // 30分钟 +const metaCache = {}; // 存储meta信息的内存缓存 +const contentCache = {}; // 存储内容的内存索引 + +// 下载任务管理和并发控制 +const MAX_CONCURRENT_DOWNLOADS = 5; // 最大并发下载数 +const MAX_QUEUE_SIZE = 50; // 最大队列长度 +const MAX_CLIENTS_PER_TASK = 10; // 每个任务最大客户端数 +const REQUEST_TIMEOUT = 30000; // 请求超时时间(30秒) +const downloadTasks = new Map(); // 下载任务管理器 +const downloadQueue = []; // 下载队列 +let activeDownloads = 0; // 当前活跃下载数 + + + +// 服务器启动时间 +const serverStartTime = Date.now(); + +// 下载任务状态 +const DOWNLOAD_STATUS = { + PENDING: 'pending', + DOWNLOADING: 'downloading', + COMPLETED: 'completed', + FAILED: 'failed', + PAUSED: 'paused' +}; + +// 下载任务类 +class DownloadTask { + constructor(url, cacheKey, metaCacheFile, contentCacheFile) { + this.id = `${cacheKey}_${Date.now()}`; + this.url = url; + this.cacheKey = cacheKey; + this.metaCacheFile = metaCacheFile; + this.contentCacheFile = contentCacheFile; + this.status = DOWNLOAD_STATUS.PENDING; + this.waitingClients = []; + this.error = null; + this.createdAt = Date.now(); + + // 进度跟踪 + this.progress = { + totalSize: 0, + downloadedSize: 0, + percentage: 0, + speed: 0, // bytes/second + eta: 0, // estimated time remaining in seconds + startTime: null, + lastUpdateTime: null, + lastDownloadedSize: 0 + }; + + // 状态管理 + this.retryCount = 0; + this.maxRetries = 3; + this.lastError = null; + this.pauseRequested = false; + } + + addClient(res, rangeHeader) { + this.waitingClients.push({ res, rangeHeader, addedAt: Date.now() }); + } + + removeClient(res) { + this.waitingClients = this.waitingClients.filter(client => client.res !== res); + } + + hasClients() { + return this.waitingClients.length > 0; + } + + // 更新下载进度 + updateProgress(downloadedBytes, totalBytes) { + const now = Date.now(); + + if (!this.progress.startTime) { + this.progress.startTime = now; + } + + this.progress.totalSize = totalBytes; + this.progress.downloadedSize = downloadedBytes; + this.progress.percentage = totalBytes > 0 ? (downloadedBytes / totalBytes) * 100 : 0; + + // 计算下载速度 + if (this.progress.lastUpdateTime) { + const timeDiff = (now - this.progress.lastUpdateTime) / 1000; // seconds + const sizeDiff = downloadedBytes - this.progress.lastDownloadedSize; + + if (timeDiff > 0) { + this.progress.speed = sizeDiff / timeDiff; + + // 计算预计剩余时间 + const remainingBytes = totalBytes - downloadedBytes; + this.progress.eta = this.progress.speed > 0 ? remainingBytes / this.progress.speed : 0; + } + } + + this.progress.lastUpdateTime = now; + this.progress.lastDownloadedSize = downloadedBytes; + } + + // 获取进度信息 + getProgressInfo() { + return { + id: this.id, + url: this.url, + status: this.status, + progress: { ...this.progress }, + clientCount: this.waitingClients.length, + retryCount: this.retryCount, + error: this.lastError, + createdAt: this.createdAt + }; + } + + // 设置错误状态 + setError(error) { + this.lastError = error; + this.error = error; + this.status = DOWNLOAD_STATUS.FAILED; + } + + // 请求暂停 + requestPause() { + this.pauseRequested = true; + } + + // 检查是否应该重试 + shouldRetry() { + return this.retryCount < this.maxRetries && this.status === DOWNLOAD_STATUS.FAILED; + } + + // 增加重试次数 + incrementRetry() { + this.retryCount++; + } +} + +// 下载任务管理器函数 +function getOrCreateDownloadTask(url, cacheKey, metaCacheFile, contentCacheFile) { + let task = downloadTasks.get(cacheKey); + if (!task) { + task = new DownloadTask(url, cacheKey, metaCacheFile, contentCacheFile); + downloadTasks.set(cacheKey, task); + } + return task; +} + +function startNextDownload() { + if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) { + return; + } + + const task = downloadQueue.shift(); + if (task && task.status === DOWNLOAD_STATUS.PENDING) { + activeDownloads++; + task.status = DOWNLOAD_STATUS.DOWNLOADING; + console.log(`Starting download: ${task.url} (${activeDownloads}/${MAX_CONCURRENT_DOWNLOADS})`); + executeDownload(task); + } +} + +function completeDownload(task, success = true) { + activeDownloads = Math.max(0, activeDownloads - 1); + task.status = success ? DOWNLOAD_STATUS.COMPLETED : DOWNLOAD_STATUS.FAILED; + + if (!success) { + // 清理失败的下载任务 + downloadTasks.delete(task.cacheKey); + } + + console.log(`Download ${success ? 'completed' : 'failed'}: ${task.url} (${activeDownloads}/${MAX_CONCURRENT_DOWNLOADS})`); + + // 启动下一个下载任务 + setTimeout(startNextDownload, 100); +} + +function cleanupExpiredTasks() { + const now = Date.now(); + const expiredTasks = []; + + downloadTasks.forEach((task, key) => { + // 清理超过1小时且没有客户端的已完成任务 + if (task.status === DOWNLOAD_STATUS.COMPLETED && + !task.hasClients() && + (now - task.startTime) > 3600000) { + expiredTasks.push(key); + } + // 清理超过10分钟的失败任务 + else if (task.status === DOWNLOAD_STATUS.FAILED && + (now - task.startTime) > 600000) { + expiredTasks.push(key); + } + }); + + expiredTasks.forEach(key => { + downloadTasks.delete(key); + }); + + if (expiredTasks.length > 0) { + console.log(`Cleaned up ${expiredTasks.length} expired download tasks`); + } +} + +// 定期清理过期任务 +setInterval(cleanupExpiredTasks, 300000); // 每5分钟清理一次 + +// 创建HTTP服务器 +const server = http.createServer(async (req, res) => { + const parsedUrl = url.parse(req.url, true); + const path = parsedUrl.pathname; + const clientIP = req.connection.remoteAddress || req.socket.remoteAddress || '127.0.0.1'; + + // 处理favicon请求 + if (path === '/favicon.ico') { + res.writeHead(204); + res.end(); + return; + } + + // 处理状态查询请求 + if (path === '/status') { + const tasks = Array.from(downloadTasks.values()).map(task => task.getProgressInfo()); + const status = { + activeDownloads, + queueLength: downloadQueue.length, + totalTasks: downloadTasks.size, + tasks: tasks.slice(0, 20), // 只返回前20个任务 + serverInfo: { + maxConcurrentDownloads: MAX_CONCURRENT_DOWNLOADS, + maxQueueSize: MAX_QUEUE_SIZE, + maxClientsPerTask: MAX_CLIENTS_PER_TASK, + requestTimeout: REQUEST_TIMEOUT, + uptime: Date.now() - serverStartTime + } + }; + + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }); + res.end(JSON.stringify(status, null, 2)); + return; + } + + // 处理任务控制请求 + if (path.startsWith('/control/')) { + const action = path.split('/')[2]; // pause, resume, cancel + const taskId = parsedUrl.query.taskId; + + if (!taskId) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing taskId parameter' })); + return; + } + + const task = downloadTasks.get(taskId); + if (!task) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Task not found' })); + return; + } + + let result = { success: false, message: '' }; + + switch (action) { + case 'pause': + if (task.status === DOWNLOAD_STATUS.DOWNLOADING) { + task.requestPause(); + result = { success: true, message: 'Pause requested' }; + } else { + result = { success: false, message: 'Task is not downloading' }; + } + break; + + case 'resume': + if (task.status === DOWNLOAD_STATUS.PAUSED || task.status === DOWNLOAD_STATUS.FAILED) { + task.status = DOWNLOAD_STATUS.PENDING; + task.pauseRequested = false; + if (!downloadQueue.includes(task)) { + downloadQueue.push(task); + } + startNextDownload(); + result = { success: true, message: 'Resume requested' }; + } else { + result = { success: false, message: 'Task cannot be resumed' }; + } + break; + + case 'cancel': + task.requestPause(); + task.status = DOWNLOAD_STATUS.FAILED; + task.setError('Cancelled by user'); + // 通知所有等待的客户端 + task.waitingClients.forEach(({ res: clientRes }) => { + if (!clientRes.headersSent && !clientRes.destroyed) { + clientRes.writeHead(499, { 'Content-Type': 'text/plain' }); + clientRes.end('Download cancelled'); + } + }); + task.waitingClients = []; + result = { success: true, message: 'Task cancelled' }; + break; + + default: + result = { success: false, message: 'Invalid action' }; + } + + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }); + res.end(JSON.stringify(result)); + return; + } + + + + // 设置请求超时 + const timeout = setTimeout(() => { + if (!res.headersSent) { + res.writeHead(408, { 'Content-Type': 'text/plain' }); + res.end('Request Timeout'); + } + }, REQUEST_TIMEOUT); + + // 设置CORS头,自动获取Origin + const origin = req.headers.origin || req.headers.referer || '*'; + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, HEAD'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, Accept, Origin'); + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Max-Age', '86400'); + + // 处理预检请求 + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + // 生成缓存键并创建分层目录结构 + const cacheKey = crypto.createHash('md5').update(req.url).digest('hex'); + const cacheSubDir = getCacheSubDir(cacheKey); + const metaCacheFile = pathModule.join(cacheSubDir, `${cacheKey}.meta`); + const contentCacheFile = pathModule.join(cacheSubDir, `${cacheKey}.content`); + + // 检查meta缓存是否有效 + const now = Date.now(); + let metaInfo = metaCache[cacheKey]; + let metaExpired = !metaInfo || (now - metaInfo.timestamp > META_CACHE_EXPIRY_MS); + + // 如果meta缓存过期,尝试从文件读取 + if (metaExpired && fs.existsSync(metaCacheFile)) { + try { + const metaData = JSON.parse(fs.readFileSync(metaCacheFile, 'utf8')); + if (now - metaData.timestamp <= META_CACHE_EXPIRY_MS) { + metaInfo = metaData; + metaCache[cacheKey] = metaInfo; + metaExpired = false; + console.log(`Meta cache loaded from file for: ${req.url}`); + } + } catch (err) { + console.error(`Error reading meta cache file: ${err.message}`); + } + } + + // 如果meta缓存有效且内容缓存存在,直接返回缓存内容 + if (!metaExpired && fs.existsSync(contentCacheFile)) { + console.log(`Serving from cache: ${req.url}`); + const rangeHeader = req.headers.range; + + // 检查是否可以从完整缓存文件提供Range请求 + if (rangeHeader && fs.existsSync(contentCacheFile)) { + try { + const stat = fs.statSync(contentCacheFile); + const expectedSize = parseInt(metaInfo.headers['content-length'] || '0', 10); + + if (stat.size === expectedSize) { + const ranges = parseRange(rangeHeader, expectedSize); + if (ranges) { + console.log(`Serving range request from complete cache: ${contentCacheFile}`); + serveFromCache(res, metaInfo, contentCacheFile, rangeHeader, req); + return; + } + } else if (stat.size > 0 && stat.size < expectedSize) { + // 部分下载的文件,检查是否可以提供请求的范围 + const ranges = parseRange(rangeHeader, expectedSize); + if (ranges && !Array.isArray(ranges)) { + const { start, end } = ranges; + if (end < stat.size) { + // 请求的范围在已下载的部分内 + console.log(`Serving range request from partial cache: ${contentCacheFile}`); + serveFromCache(res, metaInfo, contentCacheFile, rangeHeader); + return; + } + } + } + } catch (err) { + console.error(`Error checking cached file for range request: ${err.message}`); + } + } + + serveFromCache(res, metaInfo, contentCacheFile, rangeHeader, req); + return; + } + + // 需要请求原始API - 使用下载任务管理器 + console.log(`Fetching from API: ${req.url}`); + const rangeHeader = req.headers.range; + + // 获取或创建下载任务 + const task = getOrCreateDownloadTask(req.url, cacheKey, metaCacheFile, contentCacheFile); + + // 检查队列是否已满 + if (downloadQueue.length >= MAX_QUEUE_SIZE && task.status === DOWNLOAD_STATUS.PENDING) { + console.log(`Download queue full, rejecting request: ${req.url}`); + clearTimeout(timeout); + res.writeHead(503, { 'Content-Type': 'text/plain' }); + res.end('Service Unavailable - Queue Full'); + return; + } + + // 检查任务客户端数量限制 + if (task.waitingClients.length >= MAX_CLIENTS_PER_TASK) { + console.log(`Too many clients for task, rejecting: ${req.url}`); + clearTimeout(timeout); + res.writeHead(503, { 'Content-Type': 'text/plain' }); + res.end('Service Unavailable - Too Many Clients'); + return; + } + + // 添加客户端到等待列表 + task.addClient(res, rangeHeader); + + // 处理客户端断开连接 + res.on('close', () => { + clearTimeout(timeout); + task.removeClient(res); + console.log(`Client disconnected from task: ${task.id}`); + }); + + // 处理响应完成 + res.on('finish', () => { + clearTimeout(timeout); + }); + + if (task.status === DOWNLOAD_STATUS.COMPLETED) { + // 任务已完成,直接从缓存提供内容 + console.log(`Serving completed download from cache: ${req.url}`); + serveFromCache(res, metaCache[cacheKey], contentCacheFile, rangeHeader, req); + task.removeClient(res); + } else if (task.status === DOWNLOAD_STATUS.DOWNLOADING) { + // 任务正在下载中,客户端等待 + console.log(`Client waiting for ongoing download: ${req.url}`); + } else if (task.status === DOWNLOAD_STATUS.PENDING) { + // 将任务加入下载队列 + if (!downloadQueue.includes(task)) { + downloadQueue.push(task); + console.log(`Added to download queue: ${req.url}`); + } + startNextDownload(); + } else if (task.status === DOWNLOAD_STATUS.FAILED) { + // 任务失败,重新尝试 + task.status = DOWNLOAD_STATUS.PENDING; + task.error = null; + if (!downloadQueue.includes(task)) { + downloadQueue.push(task); + } + startNextDownload(); + } +}); + +// 从缓存提供内容,支持多段Range和HTTP缓存 +function serveFromCache(res, metaInfo, contentCacheFile, rangeHeader, req) { + // 设置响应头,保持CORS头 + const headers = { ...metaInfo.headers }; + // 确保CORS头不被覆盖 + if (!headers['access-control-allow-origin']) { + headers['access-control-allow-origin'] = res.getHeader('access-control-allow-origin') || '*'; + } + + // 确保Content-Type正确设置,特别是对于视频文件 + if (!headers['content-type']) { + const ext = pathModule.extname(contentCacheFile).toLowerCase(); + const mimeTypes = { + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.ogg': 'video/ogg', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + '.flv': 'video/x-flv', + '.mkv': 'video/x-matroska', + '.m4v': 'video/mp4', + '.3gp': 'video/3gpp', + '.ts': 'video/mp2t' + }; + headers['content-type'] = mimeTypes[ext] || 'application/octet-stream'; + } + + // 确保Accept-Ranges头存在,这对视频播放很重要 + headers['accept-ranges'] = 'bytes'; + + // 处理Range请求 + if (rangeHeader && fs.existsSync(contentCacheFile)) { + const stat = fs.statSync(contentCacheFile); + const fileSize = stat.size; + + // 生成ETag和Last-Modified + const etag = `"${stat.mtime.getTime()}-${fileSize}"`; + const lastModified = stat.mtime.toUTCString(); + + // 设置缓存相关头 + headers['etag'] = etag; + headers['last-modified'] = lastModified; + headers['cache-control'] = 'public, max-age=3600, must-revalidate'; // 1小时缓存,必须重新验证 + headers['vary'] = 'Accept-Encoding, Range'; // 根据编码和范围请求变化 + + // 检查条件请求 + const ifNoneMatch = req && req.headers['if-none-match']; + const ifModifiedSince = req && req.headers['if-modified-since']; + + // 304 Not Modified 检查 + if ((ifNoneMatch && ifNoneMatch === etag) || + (ifModifiedSince && new Date(ifModifiedSince) >= stat.mtime)) { + res.writeHead(304, headers); + res.end(); + return; + } + + // 解析Range头 + const ranges = parseRange(rangeHeader, fileSize); + if (ranges) { + // 单段Range请求 + if (!Array.isArray(ranges)) { + const { start, end } = ranges; + const chunkSize = (end - start) + 1; + + // 设置206 Partial Content响应头 + headers['content-range'] = `bytes ${start}-${end}/${fileSize}`; + headers['accept-ranges'] = 'bytes'; + headers['content-length'] = chunkSize; + + res.writeHead(206, headers); + + // 创建范围读取流 + const fileStream = fs.createReadStream(contentCacheFile, { start, end }); + fileStream.pipe(res); + + fileStream.on('error', (err) => { + console.error(`Error reading cache file: ${err.message}`); + res.statusCode = 500; + res.end('Internal Server Error'); + }); + + return; + } + + // 多段Range请求 - 暂时不支持,返回完整文件 + console.log('Multi-range request detected, serving full file instead'); + } + } + + // 普通请求或Range解析失败 + const stat = fs.statSync(contentCacheFile); + + // 生成ETag和Last-Modified + const etag = `"${stat.mtime.getTime()}-${stat.size}"`; + const lastModified = stat.mtime.toUTCString(); + + // 设置缓存相关头 + headers['etag'] = etag; + headers['last-modified'] = lastModified; + headers['cache-control'] = 'public, max-age=3600, must-revalidate'; // 1小时缓存,必须重新验证 + headers['vary'] = 'Accept-Encoding, Range'; // 根据编码和范围请求变化 + + // 检查条件请求 + const ifNoneMatch = req && req.headers['if-none-match']; + const ifModifiedSince = req && req.headers['if-modified-since']; + + // 304 Not Modified 检查 + if ((ifNoneMatch && ifNoneMatch === etag) || + (ifModifiedSince && new Date(ifModifiedSince) >= stat.mtime)) { + res.writeHead(304, headers); + res.end(); + return; + } + + res.writeHead(200, headers); + + // 创建文件读取流并直接管道到响应 + const fileStream = fs.createReadStream(contentCacheFile); + fileStream.pipe(res); + + // 处理错误 + fileStream.on('error', (err) => { + console.error(`Error reading cache file: ${err.message}`); + res.statusCode = 500; + res.end('Internal Server Error'); + }); +} + +// 获取Content-Type +function getContentType(url) { + const ext = pathModule.extname(url).toLowerCase(); + const mimeTypes = { + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.ogg': 'video/ogg', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + '.flv': 'video/x-flv', + '.mkv': 'video/x-matroska', + '.m4v': 'video/mp4', + '.3gp': 'video/3gpp', + '.ts': 'video/mp2t' + }; + return mimeTypes[ext] || 'application/octet-stream'; +} + +// 解析Range头,支持多段Range +function parseRange(rangeHeader, fileSize) { + if (!rangeHeader || !rangeHeader.startsWith('bytes=')) { + return null; + } + + const rangeSpec = rangeHeader.substring(6); + const ranges = []; + + // 支持多个范围,如 bytes=0-499,1000-1499 + const rangeParts = rangeSpec.split(','); + + for (const part of rangeParts) { + const trimmed = part.trim(); + const [start, end] = trimmed.split('-').map(s => s ? parseInt(s, 10) : undefined); + + let rangeStart, rangeEnd; + + if (start !== undefined && end !== undefined) { + // 完整范围: 0-499 + rangeStart = start; + rangeEnd = end; + } else if (start !== undefined && end === undefined) { + // 从某位置到结尾: 500- + rangeStart = start; + rangeEnd = fileSize - 1; + } else if (start === undefined && end !== undefined) { + // 最后几个字节: -500 + rangeStart = Math.max(0, fileSize - end); + rangeEnd = fileSize - 1; + } else { + continue; // 无效范围 + } + + // 验证范围有效性 + if (rangeStart >= fileSize || rangeEnd >= fileSize || rangeStart > rangeEnd || rangeStart < 0) { + continue; + } + + ranges.push({ start: rangeStart, end: rangeEnd }); + } + + return ranges.length > 0 ? (ranges.length === 1 ? ranges[0] : ranges) : null; +} + +// 执行下载任务 +function executeDownload(task) { + const targetUrl = new URL(task.url, DEFAULT_API_ENDPOINT); + const protocol = targetUrl.protocol === 'https:' ? https : http; + + const options = { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (compatible; AlistProxy/1.0)' + } + }; + + // 检查是否有缓存文件,如果有则添加条件请求头 + if (fs.existsSync(task.contentCacheFile)) { + try { + const stat = fs.statSync(task.contentCacheFile); + const etag = `"${stat.mtime.getTime()}-${stat.size}"`; + const lastModified = stat.mtime.toUTCString(); + + // 添加条件请求头,让上游服务器判断是否需要重新下载 + options.headers['if-none-match'] = etag; + options.headers['if-modified-since'] = lastModified; + } catch (err) { + console.log(`Error reading cache file stats: ${err.message}`); + } + } + + const proxyReq = protocol.request(targetUrl, options, (proxyRes) => { + console.log(`Response status: ${proxyRes.statusCode} for ${task.url}`); + + // 处理304 Not Modified响应 + if (proxyRes.statusCode === 304) { + console.log(`304 Not Modified, serving from cache: ${task.url}`); + + // 从缓存提供内容 + if (fs.existsSync(task.contentCacheFile) && fs.existsSync(task.metaCacheFile)) { + try { + const cachedMeta = JSON.parse(fs.readFileSync(task.metaCacheFile, 'utf8')); + + // 为所有等待的客户端提供缓存内容 + task.waitingClients.forEach(({ res: clientRes, rangeHeader }) => { + if (!clientRes.headersSent && !clientRes.destroyed) { + serveFromCache(clientRes, cachedMeta, task.contentCacheFile, rangeHeader, null); + } + }); + + task.status = DOWNLOAD_STATUS.COMPLETED; + completeDownload(task, true); + return; + } catch (err) { + console.error(`Error serving 304 from cache: ${err.message}`); + } + } + + // 如果缓存文件不存在,继续正常下载 + console.log(`Cache file missing for 304 response, continuing download`); + } + + const metaInfo = { + timestamp: Date.now(), + headers: proxyRes.headers, + statusCode: proxyRes.statusCode + }; + + // 更新内存缓存 + metaCache[task.cacheKey] = metaInfo; + + // 写入meta缓存文件 + fs.writeFile(task.metaCacheFile, JSON.stringify(metaInfo), (err) => { + if (err) console.error(`Error writing meta cache: ${err.message}`); + }); + + if (proxyRes.statusCode >= 200 && proxyRes.statusCode < 300) { + const contentLength = parseInt(proxyRes.headers['content-length'] || '0', 10); + task.updateProgress(0, contentLength); + + // 创建临时文件用于断点续传 + const tempFile = `${task.contentCacheFile}.tmp`; + const fileStream = fs.createWriteStream(tempFile); + let downloadedBytes = 0; + + // 进度跟踪变量 + let lastLoggedSize = 0; + let lastLogTime = Date.now(); + + // 监听数据流 + proxyRes.on('data', (chunk) => { + downloadedBytes += chunk.length; + task.updateProgress(downloadedBytes, contentLength); + + // 每1MB或每5秒输出一次进度日志 + const now = Date.now(); + if (downloadedBytes - lastLoggedSize >= 1024 * 1024 || now - lastLogTime >= 5000) { + const progress = task.getProgressInfo(); + console.log(`Download progress [${task.id}]: ${progress.progress.percentage.toFixed(1)}% ` + + `(${formatBytes(progress.progress.downloadedSize)}/${formatBytes(progress.progress.totalSize)}) ` + + `Speed: ${formatBytes(progress.progress.speed)}/s ` + + `ETA: ${formatTime(progress.progress.eta)}`); + lastLoggedSize = downloadedBytes; + lastLogTime = now; + } + + // 检查是否请求暂停 + if (task.pauseRequested) { + console.log(`Download paused for task: ${task.id}`); + task.status = DOWNLOAD_STATUS.PAUSED; + proxyRes.destroy(); + if (fileStream && !fileStream.destroyed) { + fileStream.end(); + } + return; + } + + // 检查等待的Range请求客户端 + const clientsToServe = []; + task.waitingClients.forEach((client, index) => { + if (client.res.destroyed) { + return; + } + + if (client.rangeHeader) { + const range = parseRange(client.rangeHeader, contentLength); + if (range && !Array.isArray(range)) { + const { start, end } = range; + // 检查请求的范围是否已经下载完成 + if (downloadedBytes > end) { + console.log(`Serving range to waiting client: ${start}-${end} (downloaded: ${downloadedBytes})`); + clientsToServe.push({ client, range, index }); + } + } + } + }); + + // 为可以服务的客户端提供Range响应 + clientsToServe.forEach(({ client, range, index }) => { + try { + const { start, end } = range; + const chunkSize = (end - start) + 1; + + // 设置响应头 + const headers = { + 'content-type': getContentType(task.url), + 'accept-ranges': 'bytes', + 'content-range': `bytes ${start}-${end}/${contentLength}`, + 'content-length': chunkSize, + 'access-control-allow-origin': '*', + 'cache-control': 'public, max-age=3600' + }; + + client.res.writeHead(206, headers); + + // 直接从临时文件读取指定范围的数据 + const rangeStream = fs.createReadStream(tempFile, { start, end }); + rangeStream.pipe(client.res); + + rangeStream.on('error', (err) => { + console.error(`Error reading range from temp file: ${err.message}`); + if (!client.res.headersSent) { + client.res.writeHead(500); + client.res.end('Internal Server Error'); + } + }); + + // 从等待列表中移除该客户端 + task.waitingClients.splice(index, 1); + console.log(`Served range ${start}-${end} from temp file to waiting client`); + } catch (err) { + console.error(`Error serving range to client: ${err.message}`); + } + }); + + // 向非Range请求的等待客户端发送数据 + task.waitingClients.forEach(client => { + if (!client.res.destroyed && !client.rangeHeader) { + // 设置响应头(仅第一次) + if (!client.headersSent) { + const headers = { ...proxyRes.headers }; + if (!headers['access-control-allow-origin']) { + headers['access-control-allow-origin'] = client.res.getHeader('access-control-allow-origin') || '*'; + } + headers['accept-ranges'] = 'bytes'; + client.res.writeHead(200, headers); + client.headersSent = true; + } + + // 发送数据块 + try { + client.res.write(chunk); + } catch (err) { + console.error(`Error writing to client: ${err.message}`); + task.removeClient(client.res); + } + } + }); + }); + + // 将数据写入文件 + proxyRes.pipe(fileStream); + + proxyRes.on('end', () => { + // 结束所有客户端响应 + task.waitingClients.forEach(client => { + if (!client.res.destroyed) { + try { + client.res.end(); + } catch (err) { + console.error(`Error ending client response: ${err.message}`); + } + } + }); + task.waitingClients = []; + + // 完成文件写入 + fileStream.end(); + }); + + fileStream.on('finish', () => { + fs.rename(tempFile, task.contentCacheFile, (err) => { + if (err) { + console.error(`Error saving cache file: ${err.message}`); + completeDownload(task, false); + } else { + console.log(`Cache saved for: ${task.url}`); + completeDownload(task, true); + } + }); + }); + + fileStream.on('error', (err) => { + console.error(`Error writing cache file: ${err.message}`); + fs.unlink(tempFile, () => {}); + completeDownload(task, false); + }); + + } else { + // 非成功响应 + task.waitingClients.forEach(client => { + if (!client.res.destroyed) { + const headers = { ...proxyRes.headers }; + if (!headers['access-control-allow-origin']) { + headers['access-control-allow-origin'] = client.res.getHeader('access-control-allow-origin') || '*'; + } + client.res.writeHead(proxyRes.statusCode, headers); + proxyRes.pipe(client.res); + } + }); + completeDownload(task, false); + } + }); + + proxyReq.on('error', (err) => { + console.error(`Download request error: ${err.message}`); + task.error = err.message; + + // 向所有等待的客户端发送错误响应 + task.waitingClients.forEach(client => { + if (!client.res.destroyed) { + client.res.statusCode = 502; + client.res.end('Bad Gateway'); + } + }); + task.waitingClients = []; + + completeDownload(task, false); + }); + + proxyReq.end(); +} + +// 从API获取内容(保留用于非大文件请求) +function fetchFromApi(req, res, cacheKey, metaCacheFile, contentCacheFile, rangeHeader) { + // 构建目标URL + const targetUrl = new URL(req.url, DEFAULT_API_ENDPOINT); + + // 选择合适的协议模块 + const protocol = targetUrl.protocol === 'https:' ? https : http; + + // 准备请求选项 + const options = { + method: req.method, + headers: { ...req.headers } + }; + + // 删除可能导致问题的头信息 + delete options.headers.host; + + // 检查是否有缓存文件,如果有则添加条件请求头 + if (fs.existsSync(contentCacheFile)) { + try { + const stat = fs.statSync(contentCacheFile); + const etag = `"${stat.mtime.getTime()}-${stat.size}"`; + const lastModified = stat.mtime.toUTCString(); + + // 添加条件请求头,让上游服务器判断是否需要重新下载 + options.headers['if-none-match'] = etag; + options.headers['if-modified-since'] = lastModified; + } catch (err) { + console.log(`Error reading cache file stats: ${err.message}`); + } + } + + // 检查是否有正在进行的下载任务 + const existingTask = downloadTasks.get(cacheKey); + if (existingTask && rangeHeader) { + const range = parseRange(rangeHeader, existingTask.totalSize || Number.MAX_SAFE_INTEGER); + if (range && !Array.isArray(range)) { + const { start, end } = range; + + // 检查请求的范围是否已经下载 + if (fs.existsSync(contentCacheFile)) { + const stat = fs.statSync(contentCacheFile); + if (stat.size > end) { + // 请求的范围已经下载完成,从部分文件提供内容 + console.log(`Serving range from partial download: ${req.url} (${start}-${end}, downloaded: ${stat.size})`); + + // 创建临时的meta信息 + const tempMeta = { + headers: { + 'content-type': getContentType(req.url), + 'accept-ranges': 'bytes', + 'access-control-allow-origin': '*' + }, + statusCode: 200 + }; + + serveFromCache(res, tempMeta, contentCacheFile, rangeHeader, req); + return; + } + } + + // 如果请求的范围还没下载完,将客户端添加到等待列表 + console.log(`Adding client to waiting list for ongoing download: ${req.url}`); + existingTask.addClient(res, rangeHeader); + return; + } + } + + // 如果是Range请求且缓存文件已存在,检查是否可以从缓存提供部分内容 + if (rangeHeader && fs.existsSync(contentCacheFile)) { + const stat = fs.statSync(contentCacheFile); + const range = parseRange(rangeHeader, stat.size); + if (range) { + // 尝试从缓存读取meta信息来验证文件完整性 + try { + const metaData = JSON.parse(fs.readFileSync(metaCacheFile, 'utf8')); + if (metaData.headers && metaData.headers['content-length']) { + const expectedSize = parseInt(metaData.headers['content-length'], 10); + if (stat.size === expectedSize) { + // 缓存文件完整,直接提供Range响应 + console.log(`Serving range from complete cache: ${req.url}`); + serveFromCache(res, metaData, contentCacheFile, rangeHeader, req); + return; + } + } + } catch (err) { + // 忽略meta文件读取错误,继续正常流程 + } + } + } + + // 创建请求 + const proxyReq = protocol.request(targetUrl, options, (proxyRes) => { + // 处理304 Not Modified响应 + if (proxyRes.statusCode === 304) { + console.log(`304 Not Modified, serving from cache: ${req.url}`); + + // 从缓存提供内容 + if (fs.existsSync(contentCacheFile) && fs.existsSync(metaCacheFile)) { + try { + const cachedMeta = JSON.parse(fs.readFileSync(metaCacheFile, 'utf8')); + serveFromCache(res, cachedMeta, contentCacheFile, rangeHeader, req); + return; + } catch (err) { + console.error(`Error serving 304 from cache: ${err.message}`); + } + } + + // 如果缓存文件不存在,返回304给客户端 + res.writeHead(304, { + 'Access-Control-Allow-Origin': res.getHeader('access-control-allow-origin') || '*', + 'Cache-Control': 'public, max-age=3600' + }); + res.end(); + return; + } + + // 记录meta信息 + const metaInfo = { + timestamp: Date.now(), + headers: proxyRes.headers, + statusCode: proxyRes.statusCode + }; + + // 更新内存缓存 + metaCache[cacheKey] = metaInfo; + + // 写入meta缓存文件 + fs.writeFile(metaCacheFile, JSON.stringify(metaInfo), (err) => { + if (err) console.error(`Error writing meta cache: ${err.message}`); + }); + + // 设置响应头,保持CORS头 + const responseHeaders = { ...proxyRes.headers }; + // 确保CORS头不被覆盖 + if (!responseHeaders['access-control-allow-origin']) { + responseHeaders['access-control-allow-origin'] = res.getHeader('access-control-allow-origin') || '*'; + } + + // 确保Content-Type正确设置,特别是对于视频文件 + if (!responseHeaders['content-type']) { + const ext = pathModule.extname(req.url).toLowerCase(); + const mimeTypes = { + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.ogg': 'video/ogg', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.wmv': 'video/x-ms-wmv', + '.flv': 'video/x-flv', + '.mkv': 'video/x-matroska', + '.m4v': 'video/mp4', + '.3gp': 'video/3gpp', + '.ts': 'video/mp2t' + }; + responseHeaders['content-type'] = mimeTypes[ext] || 'application/octet-stream'; + } + + // 确保Accept-Ranges头存在 + responseHeaders['accept-ranges'] = 'bytes'; + + res.writeHead(proxyRes.statusCode, responseHeaders); + + // 如果是成功的响应,缓存内容 + if (proxyRes.statusCode >= 200 && proxyRes.statusCode < 300) { + // 对于Range请求的206响应,不进行缓存,直接转发 + if (proxyRes.statusCode === 206 || rangeHeader) { + console.log(`Streaming range response: ${req.url}`); + proxyRes.pipe(res); + } else { + // 完整内容响应,进行缓存 + const tempFile = `${contentCacheFile}.tmp`; + const fileStream = fs.createWriteStream(tempFile); + + // 将响应写入文件并同时发送给客户端 + proxyRes.pipe(fileStream); + proxyRes.pipe(res); + + // 完成后重命名临时文件 + fileStream.on('finish', () => { + fs.rename(tempFile, contentCacheFile, (err) => { + if (err) console.error(`Error saving cache file: ${err.message}`); + else console.log(`Cache saved for: ${req.url}`); + }); + }); + + // 处理错误 + fileStream.on('error', (err) => { + console.error(`Error writing cache file: ${err.message}`); + fs.unlink(tempFile, () => {}); // 删除临时文件 + }); + } + } else { + // 非成功响应直接转发,不缓存内容 + proxyRes.pipe(res); + } + }); + + // 处理请求错误 + proxyReq.on('error', (err) => { + console.error(`Proxy request error: ${err.message}`); + res.statusCode = 502; + res.end('Bad Gateway'); + }); + + // 如果原始请求有body,转发它 + if (['POST', 'PUT', 'PATCH'].includes(req.method)) { + req.pipe(proxyReq); + } else { + proxyReq.end(); + } +} + +// 格式化字节数 +function formatBytes(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// 格式化时间(秒) +function formatTime(seconds) { + if (!seconds || seconds === Infinity) return 'Unknown'; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours}h ${minutes}m ${secs}s`; + } else if (minutes > 0) { + return `${minutes}m ${secs}s`; + } else { + return `${secs}s`; + } +} + +// 启动服务器 +server.listen(DEFAULT_PORT, () => { + console.log(`Proxy server running at http://localhost:${DEFAULT_PORT}/`); + console.log(`Proxying requests to: ${DEFAULT_API_ENDPOINT}`); + console.log(`Cache directory: ${cacheDir}`); + console.log(`Meta cache expiry: ${META_CACHE_EXPIRY_MS / 60000} minutes`); +});