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/api'; // 备用 API const BACKUP_API_ENDPOINT = 'http://x-mo.cn:9519/api'; const cacheDir = pathModule.join(__dirname, CACHE_DIR_NAME); const pathIndex = {}; // 访问计数器 const viewsInfo = { request: 0, cacheHit: 0, apiCall: 0, cacheCall: 0, cacheReadError: 0, fetchApiError: 0, fetchApiWarning: 0, increment: function (key) { if (this.hasOwnProperty(key)) { this[key]++; } } }; let port = DEFAULT_PORT; let apiEndpoint = DEFAULT_API_ENDPOINT; // 解析命令行参数函数 function parseArguments() { const args = process.argv.slice(2); args.forEach(arg => { const cleanArg = arg.startsWith('--') ? arg.substring(2) : arg; const [key, value] = cleanArg.split('='); if (key === 'port' && value) { const parsedPort = parseInt(value, 10); if (!isNaN(parsedPort)) { port = parsedPort; } } else if (key === 'api' && value) { apiEndpoint = value; } }); } // 初始化函数,包含参数解析和目录创建 function initializeApp() { parseArguments(); if (!fs.existsSync(cacheDir)) { try { fs.mkdirSync(cacheDir, { recursive: true }); console.log(`Cache directory created: ${cacheDir}`); } catch (err) { console.error(`Error creating cache directory ${cacheDir}:`, err); process.exit(1); // Exit if cache directory cannot be created } } } initializeApp(); const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours const CACHE_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour const HTTP_STATUS = { OK: 200, NO_CONTENT: 204, REDIRECT: 302, NOT_MODIFIED: 304, BAD_REQUEST: 400, NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, BAD_GATEWAY: 502, }; // 定时清理过期缓存数据 setInterval(() => { const currentTime = Date.now(); for (const key in pathIndex) { if (currentTime - pathIndex[key].timestamp > CACHE_EXPIRY_MS) { delete pathIndex[key]; } } }, CACHE_CLEANUP_INTERVAL_MS); // 统一发送错误响应 function sendErrorResponse(res, statusCode, message) { if (!res.headersSent) { res.writeHead(statusCode, { 'Content-Type': 'text/plain;charset=UTF-8' }); res.end(message); } } // --- Request Handling Logic --- async function handleFavicon(req, res) { res.writeHead(HTTP_STATUS.NO_CONTENT); res.end(); } async function handleEndpoint(req, res, parsedUrl) { if (parsedUrl.query.api) { const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/; if (urlRegex.test(parsedUrl.query.api)) { apiEndpoint = parsedUrl.query.api; console.log(`API endpoint updated to: ${apiEndpoint}`); } } res.writeHead(HTTP_STATUS.OK, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ code: HTTP_STATUS.OK, data: { api: apiEndpoint, port: port, cacheDir: cacheDir, pathIndexCount: Object.keys(pathIndex).length, viewsInfo: { request: viewsInfo.request, cacheHit: viewsInfo.cacheHit, apiCall: viewsInfo.apiCall, cacheCall: viewsInfo.cacheCall, cacheReadError: viewsInfo.cacheReadError, fetchApiError: viewsInfo.fetchApiError, fetchApiWarning: viewsInfo.fetchApiWarning, } } })); } async function handleApiRedirect(res, apiData) { res.writeHead(HTTP_STATUS.REDIRECT, { Location: apiData.data.url }); res.end(); } async function processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res, req) { const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data; const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb }; pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() }; const cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`); const cacheContentFile = pathModule.join(cacheDir, `${data.uniqid}.content`); const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`); try { fs.writeFileSync(cacheMetaFile, JSON.stringify(data)); } catch (writeError) { console.error(`Error writing meta file ${cacheMetaFile}:`, writeError); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Failed to write cache metadata.'); return; } if (fs.existsSync(cacheContentFile)) { const stats = fs.statSync(cacheContentFile); const contentLength = stats.size; if (contentLength < 2048 && data.headers['content-length'] && parseInt(data.headers['content-length'], 10) !== contentLength) { console.warn(`Content length mismatch for ${cacheContentFile}. API: ${data.headers['content-length']}, Cache: ${contentLength}. Re-fetching.`); fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res, req); } else { serveFromCache(data, cacheContentFile, cacheMetaFile, res, req); } } else { fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res, req); } } async function tryServeFromStaleCacheOrError(uniqidhex, res, req, errorMessage) { if (pathIndex[uniqidhex]) { const cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`); const cacheContentFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.content`); if (fs.existsSync(cacheMetaFile) && fs.existsSync(cacheContentFile)) { console.warn(`API call failed or returned non-200. Serving stale cache for ${uniqidhex}`); try { const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')); serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, req); return; } catch (parseError) { console.error(`Error parsing stale meta file ${cacheMetaFile}:`, parseError); // Fall through to generic error if stale cache is also broken } } } sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, errorMessage || 'Bad Gateway'); } async function handleMainRequest(req, res) { req.url = req.url.replace(/\/{2,}/g, '/'); const parsedUrl = url.parse(req.url, true); const sign = parsedUrl.query.sign || ''; let reqPath = parsedUrl.pathname.split('/')[1] || ''; // Ensure reqPath is not undefined let token = parsedUrl.pathname.split('/').slice(2).join('/'); if (reqPath === 'favicon.ico') return handleFavicon(req, res); if (reqPath === 'endpoint') return handleEndpoint(req, res, parsedUrl); if (!token && reqPath) { // If token is empty but reqPath is not, assume reqPath is the token token = reqPath; reqPath = 'app'; // Default to 'app' if only one path segment is provided } const ALLOWED_PATHS = ['avatar', 'go', 'bbs', 'www', 'url', 'thumb', 'app']; if (!ALLOWED_PATHS.includes(reqPath) || !token) { return sendErrorResponse(res, HTTP_STATUS.BAD_REQUEST, `Bad Request: Invalid path or missing token.`); } viewsInfo.increment('request'); const uniqidhex = crypto.createHash('md5').update(reqPath + token + sign).digest('hex'); let cacheMetaFile = ''; let cacheContentFile = ''; if (pathIndex[uniqidhex]) { cacheMetaFile = pathModule.join(cacheDir, `${uniqidhex}.meta`); cacheContentFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.content`); } if (pathIndex[uniqidhex] && isCacheValid(cacheMetaFile, cacheContentFile)) { const { cacheData, isNotModified } = await checkCacheHeaders(req, cacheMetaFile); if (isNotModified) { res.writeHead(HTTP_STATUS.NOT_MODIFIED); res.end(); } else { viewsInfo.increment('cacheHit'); serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, req); } } else { try { viewsInfo.increment('apiCall'); const apiData = await fetchApiData(reqPath, token, sign); if (apiData.code === HTTP_STATUS.REDIRECT || apiData.code === 301) { return handleApiRedirect(res, apiData); } if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) { await processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res, req); } else { viewsInfo.increment('fetchApiWarning'); await tryServeFromStaleCacheOrError(uniqidhex, res, req, apiData.message); } } catch (error) { viewsInfo.increment('fetchApiError'); console.error('Error in API call or processing:', error); await tryServeFromStaleCacheOrError(uniqidhex, res, req, `Bad Gateway: API request failed. ${error.message}`); } } } const server = http.createServer(handleMainRequest); // 检查缓存头并返回是否为304 async function checkCacheHeaders(req, cacheMetaFile) { try { const metaContent = fs.readFileSync(cacheMetaFile, 'utf8'); const cacheData = JSON.parse(metaContent); const ifNoneMatch = req.headers['if-none-match']; const ifModifiedSince = req.headers['if-modified-since']; // 优先检查ETag (更精确的缓存验证方式) if (ifNoneMatch && cacheData.uniqid) { // 支持弱验证器格式 "W/"etag-value"" const cleanEtag = ifNoneMatch.replace(/^W\//, '').replace(/"/g, ''); const cleanCacheEtag = cacheData.uniqid.replace(/^W\//, '').replace(/"/g, ''); if (cleanEtag === cleanCacheEtag || ifNoneMatch === cacheData.uniqid) { console.log(`304 Not Modified: ETag match for ${cacheMetaFile}`); return { cacheData, isNotModified: true }; } } // 检查If-Modified-Since (基于时间的缓存验证) if (ifModifiedSince && cacheData.headers && cacheData.headers['last-modified']) { try { const lastModifiedDate = new Date(cacheData.headers['last-modified']); const ifModifiedSinceDate = new Date(ifModifiedSince); // HTTP日期的时间精度是1秒 // 如果If-Modified-Since至少与Last-Modified一样新,则返回304 if (lastModifiedDate.getTime() <= ifModifiedSinceDate.getTime()) { console.log(`304 Not Modified: Last-Modified check for ${cacheMetaFile}`); return { cacheData, isNotModified: true }; } } catch (dateParseError) { console.warn(`Error parsing date for cache header check (${cacheMetaFile}):`, dateParseError); // 如果日期无效,则继续处理,视为未修改检查失败 } } // 如果没有缓存验证头或验证失败,返回正常内容 return { cacheData, isNotModified: false }; } catch (error) { console.error(`Error reading or parsing cache meta file ${cacheMetaFile} in checkCacheHeaders:`, error); // 如果无法读取元数据,假设缓存无效 return { cacheData: null, isNotModified: false }; } } // 检查缓存是否有效 function isCacheValid(cacheMetaFile, cacheContentFile) { if (!fs.existsSync(cacheMetaFile) || !fs.existsSync(cacheContentFile)) { return false; } try { const metaContent = fs.readFileSync(cacheMetaFile, 'utf8'); const cacheData = JSON.parse(metaContent); // 确保expiration是一个数字并且在未来 // 如果expiration未定义或为0,则设置为24小时后过期 if (!cacheData.expiration || cacheData.expiration === 0) { cacheData.expiration = Date.now() + CACHE_EXPIRY_MS; // 更新缓存元数据文件 fs.writeFileSync(cacheMetaFile, JSON.stringify(cacheData)); console.log(`Updated missing expiration in ${cacheMetaFile} to ${cacheData.expiration}`); return true; } return typeof cacheData.expiration === 'number' && cacheData.expiration > Date.now(); } catch (error) { console.warn(`Error reading or parsing cache meta file ${cacheMetaFile} for validation:`, error); return false; // 如果元数据文件损坏或不可读,则缓存无效 } } // 从 API 获取数据 const API_TIMEOUT_MS = 5000; const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'; // 用于跟踪API状态的变量 let isDefaultApiHealthy = true; let lastApiSwitchTime = 0; const API_SWITCH_COOLDOWN_MS = 60000; // 切换API后的冷却时间,防止频繁切换 // 尝试使用指定的API端点获取数据 async function tryFetchWithEndpoint(endpoint, reqPath, token, sign) { const queryParams = querystring.stringify({ type: reqPath, sign: sign }); const apiUrl = `${endpoint}?${queryParams}`; const parsedApiUrl = new URL(apiUrl); const protocol = parsedApiUrl.protocol === 'https:' ? https : http; const options = { method: 'GET', headers: { 'Accept': 'application/json; charset=utf-8', 'User-Agent': USER_AGENT, 'token': token }, timeout: API_TIMEOUT_MS, rejectUnauthorized: false, // Allow self-signed certificates, use with caution }; return new Promise((resolve, reject) => { const apiReq = protocol.request(apiUrl, options, (apiRes) => { let responseData = ''; apiRes.setEncoding('utf8'); apiRes.on('data', chunk => responseData += chunk); apiRes.on('end', () => { try { if (apiRes.statusCode >= 400) { // Treat HTTP errors from API as rejections for easier handling console.error(`API request to ${apiUrl} failed with status ${apiRes.statusCode}: ${responseData}`); // Attempt to parse for a message, but prioritize status code for error type let errorPayload = { code: apiRes.statusCode, message: `API Error: ${apiRes.statusCode}` }; try { const parsedError = JSON.parse(responseData); if (parsedError && parsedError.message) errorPayload.message = parsedError.message; } catch (e) { /* Ignore if response is not JSON */ } resolve({ success: false, data: errorPayload }); return; } resolve({ success: true, data: JSON.parse(responseData) }); } catch (parseError) { console.error(`Error parsing JSON response from ${apiUrl}:`, parseError, responseData); reject(new Error(`Failed to parse API response: ${parseError.message}`)); } }); }); apiReq.on('timeout', () => { apiReq.destroy(); // Destroy the request to free up resources console.error(`API request to ${apiUrl} timed out after ${API_TIMEOUT_MS}ms`); reject(new Error('API request timed out')); }); apiReq.on('error', (networkError) => { console.error(`API request to ${apiUrl} failed:`, networkError); reject(networkError); }); apiReq.end(); }); } // 主API获取函数,支持故障转移 async function fetchApiData(reqPath, token, sign) { // 确定当前使用的API端点 const currentEndpoint = isDefaultApiHealthy ? DEFAULT_API_ENDPOINT : BACKUP_API_ENDPOINT; const backupEndpoint = isDefaultApiHealthy ? BACKUP_API_ENDPOINT : DEFAULT_API_ENDPOINT; try { // 尝试使用当前API端点 const result = await tryFetchWithEndpoint(currentEndpoint, reqPath, token, sign); // 如果当前使用的是备用API且成功了,考虑是否切回主API if (!isDefaultApiHealthy && result.success) { const now = Date.now(); if (now - lastApiSwitchTime > API_SWITCH_COOLDOWN_MS) { // 尝试恢复使用默认API console.log(`尝试恢复使用默认API: ${DEFAULT_API_ENDPOINT}`); isDefaultApiHealthy = true; lastApiSwitchTime = now; } } return result.data; } catch (error) { console.error(`API请求失败,尝试使用备用API: ${backupEndpoint}`, error); // 如果当前API失败,切换到备用API if (isDefaultApiHealthy) { console.log(`主API ${DEFAULT_API_ENDPOINT} 不可用,切换到备用API ${BACKUP_API_ENDPOINT}`); isDefaultApiHealthy = false; lastApiSwitchTime = Date.now(); } else { console.log(`备用API ${BACKUP_API_ENDPOINT} 不可用,切换回主API ${DEFAULT_API_ENDPOINT}`); isDefaultApiHealthy = true; lastApiSwitchTime = Date.now(); } try { // 尝试使用备用API端点 const backupResult = await tryFetchWithEndpoint(backupEndpoint, reqPath, token, sign); return backupResult.data; } catch (backupError) { console.error(`备用API也失败了,无法获取数据`, backupError); throw backupError; // 如果备用API也失败,则抛出错误 } } } // 从真实 URL 获取数据并写入缓存 const REAL_URL_FETCH_TIMEOUT_MS = 30000; // 30秒超时 const HIGH_WATER_MARK = 64 * 1024; // 64KB 缓冲区 // 媒体文件扩展名列表 const VIDEO_EXTENSIONS = ['.mp4', '.mkv', '.webm', '.avi', '.mov', '.flv']; const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a']; // 检查文件类型的辅助函数 const isMediaFile = (path, extensions) => { if (!path || typeof path !== 'string') return false; return extensions.some(ext => path.includes(ext)); }; // 从缓存提供范围请求的辅助函数 const serveRangeFromCache = (rangeHeader, cacheContentFile, cacheMetaFile, res, isVideo, isAudio) => { try { const stats = fs.statSync(cacheContentFile); const fileSize = stats.size; // 解析Range头 const ranges = rangeHeader.replace(/bytes=/, '').split(','); const rangeSpec = ranges[0].trim(); const parts = rangeSpec.split('-'); let start = parseInt(parts[0], 10); let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; // 处理特殊范围格式,如 bytes=-500 (最后500字节) if (isNaN(start)) { start = Math.max(0, fileSize - parseInt(parts[1], 10)); end = fileSize - 1; } // 确保范围有效 end = Math.min(end, fileSize - 1); start = Math.min(start, end); if (start < fileSize && fileSize > 0) { console.log(`Serving range request from cache: bytes ${start}-${end}/${fileSize}`); const chunkSize = (end - start) + 1; const metaData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')); const readStream = fs.createReadStream(cacheContentFile, { start, end, highWaterMark: HIGH_WATER_MARK }); const contentType = (metaData.headers && metaData.headers['content-type']) || (isVideo ? 'video/mp4' : (isAudio ? 'audio/mpeg' : 'application/octet-stream')); res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Content-Length': chunkSize.toString(), 'Content-Type': contentType, 'Accept-Ranges': 'bytes', 'ETag': metaData.uniqid || '', 'Cache-Control': 'public, max-age=3600', 'Cloud-Type': metaData.cloudtype || 'unknown', 'Cloud-Expiration': new Date(metaData.expiration || (Date.now() + 3600000)).toLocaleString() }); readStream.pipe(res); readStream.on('error', (err) => { console.error(`Read stream error: ${err.message}`); if (!res.headersSent) { sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Error reading from cache'); } else { try { res.end(); } catch (e) { /* ignore */ } } }); return true; // 成功从缓存提供服务 } } catch (error) { console.error(`Error serving range request from cache: ${error.message}`); } return false; // 从缓存提供服务失败 }; const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res, req) => { const protocol = data.realUrl.startsWith('https:') ? https : http; const cacheFileExists = fs.existsSync(cacheContentFile); // 如果缓存文件已存在,直接使用它 if (cacheFileExists) { tempCacheContentFile = cacheContentFile; console.log(`Using existing cache file: ${cacheContentFile}`); } // 检查媒体文件类型 const isVideo = isMediaFile(data.path, VIDEO_EXTENSIONS); const isAudio = isMediaFile(data.path, AUDIO_EXTENSIONS); const rangeHeader = req && req.headers && req.headers.range; // 构建请求选项 const requestOptions = { timeout: REAL_URL_FETCH_TIMEOUT_MS, rejectUnauthorized: false, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive' } }; // 处理范围请求 if (rangeHeader) { console.log(`Forwarding range request: ${rangeHeader} to ${data.realUrl}`); requestOptions.headers['Range'] = rangeHeader; // 尝试从缓存提供范围请求 if (cacheFileExists && fs.existsSync(cacheMetaFile)) { if (serveRangeFromCache(rangeHeader, cacheContentFile, cacheMetaFile, res, isVideo, isAudio)) { return; // 成功从缓存提供服务,不需要继续 } } } // 转发条件请求头 if (req && req.headers) { ['if-range', 'if-match', 'if-none-match'].forEach(header => { if (req.headers[header]) { requestOptions.headers[header.charAt(0).toUpperCase() + header.slice(1)] = req.headers[header]; console.log(`Forwarding ${header} header: ${req.headers[header]}`); } }); } protocol.get(data.realUrl, requestOptions, (realRes) => { // 处理304 Not Modified响应 if (realRes.statusCode === 304) { console.log(`Received 304 Not Modified from source for ${data.realUrl}`); res.writeHead(304, { 'ETag': data.uniqid || '', 'Cache-Control': 'public, max-age=3600', 'Date': new Date().toUTCString() }); res.end(); return; } // 使用更高效的缓冲区设置,对于已存在的文件使用追加模式 const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: tempCacheContentFile === cacheContentFile && fs.existsSync(tempCacheContentFile) ? 'a' : 'w', highWaterMark: HIGH_WATER_MARK }); // 处理响应头和缓存元数据 const handleResponseHeaders = () => { const contentLength = realRes.headers['content-length']; // 验证内容长度 if (contentLength) { // 检查内容长度是否异常 if (contentLength < 2048 && data.headers['content-length'] !== contentLength && !rangeHeader) { console.warn('Warning: content-length mismatch from:', data.realUrl); sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, `Bad Gateway: Content-Length mismatch`); // 清理临时文件 if (fs.existsSync(tempCacheContentFile)) { fs.unlinkSync(tempCacheContentFile); } return false; } // 只在非范围请求时更新content-length if (!rangeHeader) { data.headers['content-length'] = contentLength; updateCacheMetadata(); } } else { console.warn('Warning: content-length undefined from:', data.realUrl); } // 确保过期时间有效 if (!data.expiration || data.expiration === 0) { data.expiration = Date.now() + CACHE_EXPIRY_MS; updateCacheMetadata(); } return true; }; // 更新缓存元数据 const updateCacheMetadata = () => { fs.writeFile(cacheMetaFile, JSON.stringify(data), (err) => { if (err) console.error(`Error updating cache metadata: ${err.message}`); }); }; // 如果头部处理失败,直接返回 if (!handleResponseHeaders()) return; // 格式化过期时间为可读格式 const expirationDate = new Date(data.expiration).toLocaleString(); // 设置响应头 - 更精简的方式 const responseHeaders = { 'Cloud-Type': data.cloudtype, 'Cloud-Expiration': expirationDate, 'ETag': data.uniqid || '', 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', 'Expires': new Date(Date.now() + 3600000).toUTCString(), 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive', 'Date': new Date().toUTCString(), 'Last-Modified': data.headers['last-modified'] || new Date().toUTCString(), 'Vary': 'Accept-Encoding', 'Content-Type': realRes.headers['content-type'] || (isVideo ? 'video/mp4' : 'application/octet-stream'), ...data.headers }; // 处理范围响应 if (realRes.headers['content-range']) { responseHeaders['Content-Range'] = realRes.headers['content-range']; console.log(`Received partial content: ${realRes.headers['content-range']} for ${data.realUrl}`); // 从Content-Range中提取文件总大小 const contentRangeMatch = /\/([0-9]+)$/.exec(realRes.headers['content-range']); if (contentRangeMatch && contentRangeMatch[1]) { const totalSize = parseInt(contentRangeMatch[1], 10); if (totalSize > 0 && (!data.headers['content-length'] || parseInt(data.headers['content-length'], 10) !== totalSize)) { data.headers['content-length'] = totalSize.toString(); console.log(`Updated content-length: ${totalSize}`); fs.writeFile(cacheMetaFile, JSON.stringify(data), err => { if (err) console.error(`Error updating meta: ${err.message}`); }); } } } // 使用源服务器返回的状态码 res.writeHead(realRes.statusCode, responseHeaders); // 使用流事件处理来优化性能 let bytesReceived = 0; const startTime = Date.now(); // 检测是否需要处理压缩内容 const contentEncoding = realRes.headers['content-encoding']; if (contentEncoding && (contentEncoding.includes('gzip') || contentEncoding.includes('deflate') || contentEncoding.includes('br'))) { // 对于压缩内容,我们直接传递,不做解压处理 console.log(`Streaming compressed content (${contentEncoding}) for ${data.realUrl}`); } // 使用更高效的流处理方式,确保边下边播 // 创建一个Transform流,同时写入缓存和响应 const { Transform } = require('stream'); const streamRelay = new Transform({ transform(chunk, encoding, callback) { // 将数据传递给下一个流 this.push(chunk); // 更新接收的字节数 bytesReceived += chunk.length; // 每10MB记录一次进度 if (bytesReceived % (10 * 1024 * 1024) === 0) { const elapsedSeconds = (Date.now() - startTime) / 1000; const mbReceived = bytesReceived / (1024 * 1024); const mbps = mbReceived / elapsedSeconds; console.log(`Progress for ${data.realUrl}: ${mbReceived.toFixed(2)}MB received at ${mbps.toFixed(2)}MB/s`); } callback(); } }); // 设置错误处理 streamRelay.on('error', (err) => { console.error(`Stream relay error for ${data.realUrl}:`, err); try { cacheStream.end(); if (!res.writableEnded) res.end(); } catch (e) { /* ignore */ } }); // 优化流处理,确保数据立即流向客户端,实现真正的边下边播 // 直接将源响应通过中继流同时发送到客户端和缓存 realRes.pipe(streamRelay); // 优先将数据发送给客户端,确保低延迟 streamRelay.pipe(res, { end: true }); // 同时将数据写入缓存 streamRelay.pipe(cacheStream, { end: false }); // 不自动结束缓存流,我们需要在完成后手动处理 // 处理客户端提前关闭连接 res.on('close', () => { if (!res.writableEnded) { console.log(`Client closed connection prematurely for ${data.realUrl}`); // 断开客户端连接,但继续下载到缓存 streamRelay.unpipe(res); // 不中断缓存写入,继续下载完整文件 } }); // 处理完成事件 realRes.on('end', () => { const totalTime = (Date.now() - startTime) / 1000; const totalMB = bytesReceived / (1024 * 1024); console.log(`Completed ${data.realUrl}: ${totalMB.toFixed(2)}MB in ${totalTime.toFixed(2)}s (${(totalMB/totalTime).toFixed(2)}MB/s)`); // 确保缓存流正确结束 cacheStream.end(() => { if (fs.existsSync(tempCacheContentFile)) { // 如果临时文件就是缓存文件,不需要重命名 if (tempCacheContentFile === cacheContentFile) { console.log(`Successfully cached: ${cacheContentFile}`); // 更新缓存元数据,添加文件大小信息 if (bytesReceived > 0) { data.headers['content-length'] = bytesReceived.toString(); fs.writeFile(cacheMetaFile, JSON.stringify(data), (err) => { if (err) console.error(`Error updating content-length in cache meta file ${cacheMetaFile}:`, err); }); } } else { // 确保目标目录存在 const targetDir = pathModule.dirname(cacheContentFile); fs.mkdir(targetDir, { recursive: true }, (mkdirErr) => { if (mkdirErr) { console.error(`Error creating directory ${targetDir}:`, mkdirErr); try { fs.unlinkSync(tempCacheContentFile); } catch (e) { /* ignore */ } return; } // 异步重命名文件 fs.rename(tempCacheContentFile, cacheContentFile, (renameErr) => { if (renameErr) { console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameErr); try { fs.unlinkSync(tempCacheContentFile); } catch (e) { /* ignore */ } } else { console.log(`Successfully cached: ${cacheContentFile}`); // 更新缓存元数据,添加文件大小信息 if (bytesReceived > 0) { data.headers['content-length'] = bytesReceived.toString(); fs.writeFile(cacheMetaFile, JSON.stringify(data), (err) => { if (err) console.error(`Error updating content-length in cache meta file ${cacheMetaFile}:`, err); }); } } }); }); } } else { console.warn(`Temp cache file ${tempCacheContentFile} not found after stream end for ${data.realUrl}`); } }); }); // 改进错误处理 realRes.on('error', (streamError) => { console.error(`Error during response stream from ${data.realUrl}:`, streamError); cacheStream.end(); // Close the writable stream // 如果响应已经开始发送,我们不能再发送错误响应 if (!res.headersSent) { handleResponseError(res, tempCacheContentFile, data.realUrl); } else { // 如果头部已发送,尝试结束响应 try { res.end(); } catch (e) { /* ignore */ } // 清理临时文件 try { fs.unlinkSync(tempCacheContentFile); } catch (e) { /* ignore */ } } }); }).on('error', (requestError) => { console.error(`Error making GET request to ${data.realUrl}:`, requestError); // No cacheStream involved here if the request itself fails before response handleResponseError(res, tempCacheContentFile, data.realUrl); // tempCacheContentFile might not exist or be empty }); }; // 从缓存中读取数据并返回 function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, req) { if (!cacheData) { // Added check for null cacheData from checkCacheHeaders failure console.error(`serveFromCache called with null cacheData for ${cacheContentFile}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Cache metadata unavailable.'); return; } // 获取文件大小 let fileSize = 0; try { const stats = fs.statSync(cacheContentFile); fileSize = stats.size; // 更新缓存元数据中的content-length if (fileSize > 0) { if (!cacheData.headers) cacheData.headers = {}; if (!cacheData.headers['content-length'] || parseInt(cacheData.headers['content-length'], 10) !== fileSize) { cacheData.headers['content-length'] = fileSize.toString(); // 异步更新缓存元数据 fs.writeFile(cacheMetaFile, JSON.stringify(cacheData), (err) => { if (err) console.error(`Error updating content-length in ${cacheMetaFile}:`, err); else console.log(`Updated content-length in ${cacheMetaFile} to ${fileSize}`); }); } } } catch (statError) { console.error(`Error stating cache content file ${cacheContentFile}:`, statError); handleCacheReadError(res, cacheContentFile); return; } // 设置更强的缓存控制,确保短时间内不会重复请求 const now = Date.now(); const nowUTC = new Date(now).toUTCString(); const expiresUTC = new Date(now + 3600000).toUTCString(); // 获取Last-Modified,优先使用API的值 let lastModified = cacheData.headers && cacheData.headers['last-modified'] ? cacheData.headers['last-modified'] : new Date(fs.statSync(cacheMetaFile).mtime).toUTCString(); // 生成ETag,用于断点下载 const etag = cacheData.uniqid || crypto.createHash('md5').update(cacheContentFile + fileSize).digest('hex'); const baseHeaders = { 'Cloud-Type': cacheData.cloudtype || 'unknown', 'Cloud-Expiration': new Date(cacheData.expiration || (now + CACHE_EXPIRY_MS)).toLocaleString(), 'ETag': etag, 'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400', 'Expires': expiresUTC, 'Pragma': 'cache', 'Accept-Ranges': 'bytes', // 关键:支持范围请求和断点下载 'Connection': 'keep-alive', 'Date': nowUTC, 'Last-Modified': lastModified, 'Vary': 'Accept-Encoding', }; viewsInfo.increment('cacheCall'); // 检测文件类型 const isVideo = cacheData.path && typeof cacheData.path === 'string' && (cacheData.path.includes('.mp4') || cacheData.path.includes('.mkv') || cacheData.path.includes('.webm') || cacheData.path.includes('.avi') || cacheData.path.includes('.mov') || cacheData.path.includes('.flv')); const isAudio = cacheData.path && typeof cacheData.path === 'string' && (cacheData.path.includes('.mp3') || cacheData.path.includes('.wav') || cacheData.path.includes('.ogg') || cacheData.path.includes('.flac') || cacheData.path.includes('.aac') || cacheData.path.includes('.m4a')); // 检查是否是断点续传请求 const ifRange = req && req.headers && req.headers['if-range']; const ifMatch = req && req.headers && req.headers['if-match']; const ifNoneMatch = req && req.headers && req.headers['if-none-match']; // 处理条件请求 - 如果ETag匹配,返回304 if (ifNoneMatch && ifNoneMatch.includes(etag)) { console.log(`304 Not Modified for ${cacheContentFile} - ETag match`); res.writeHead(304, baseHeaders); res.end(); return; } // 处理范围请求 (Range Requests) const rangeHeader = req && req.headers && req.headers.range; // 如果有If-Range头且不匹配当前ETag,则忽略Range头,返回整个文件 const ignoreRange = ifRange && ifRange !== etag && ifRange !== lastModified; if (rangeHeader && fileSize > 0 && !ignoreRange) { console.log(`Range request received: ${rangeHeader} for file ${cacheContentFile}`); // 解析Range头 const ranges = rangeHeader.replace(/bytes=/, '').split(','); // 如果是多范围请求,我们简化处理,只返回第一个范围 const rangeSpec = ranges[0].trim(); const parts = rangeSpec.split('-'); let start = parseInt(parts[0], 10); let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; // 处理特殊范围格式,如 bytes=-500 (最后500字节) if (isNaN(start)) { start = Math.max(0, fileSize - parseInt(parts[1], 10)); end = fileSize - 1; } // 确保范围不超过文件大小 end = Math.min(end, fileSize - 1); start = Math.min(start, end); // 验证范围有效性 if (start >= fileSize) { // 无效范围,返回416错误 res.writeHead(416, { 'Content-Range': `bytes */${fileSize}`, ...baseHeaders }); res.end(); return; } // 计算范围大小 const chunkSize = (end - start) + 1; // 创建范围流 const readStream = fs.createReadStream(cacheContentFile, { start, end, highWaterMark: HIGH_WATER_MARK // 使用更高效的缓冲区设置 }); // 设置206部分内容响应 const responseHeaders = { ...baseHeaders, 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Content-Length': chunkSize.toString(), 'Content-Type': (cacheData.headers && cacheData.headers['content-type']) || (isVideo ? 'video/mp4' : (isAudio ? 'audio/mpeg' : 'application/octet-stream')), }; console.log(`Serving partial content: bytes ${start}-${end}/${fileSize} for ${cacheContentFile}`); res.writeHead(206, responseHeaders); // 处理流错误 readStream.on('error', (err) => { console.error(`Read stream error for range request ${cacheContentFile}:`, err); if (!res.headersSent) { handleCacheReadError(res, cacheContentFile); } else { try { res.end(); } catch (e) { /* ignore */ } } }); // 监控数据传输速度 let bytesSent = 0; const startTime = Date.now(); readStream.on('data', (chunk) => { bytesSent += chunk.length; // 每10MB记录一次进度 if (bytesSent % (10 * 1024 * 1024) === 0) { const elapsedSeconds = (Date.now() - startTime) / 1000; const mbSent = bytesSent / (1024 * 1024); const mbps = mbSent / elapsedSeconds; console.log(`Range progress for ${cacheContentFile}: ${mbSent.toFixed(2)}MB sent at ${mbps.toFixed(2)}MB/s`); } }); // 将范围流传输到响应 readStream.pipe(res); // 处理客户端提前关闭连接 res.on('close', () => { if (!res.writableEnded) { console.log(`Client closed range request connection prematurely for ${cacheContentFile}`); readStream.destroy(); } }); } else { // 非范围请求,返回完整文件 const readStream = fs.createReadStream(cacheContentFile, { highWaterMark: HIGH_WATER_MARK // 使用更高效的缓冲区设置 }); readStream.on('open', () => { const responseHeaders = { ...baseHeaders, 'Content-Length': fileSize.toString(), 'Content-Type': (cacheData.headers && cacheData.headers['content-type']) || (isVideo ? 'video/mp4' : (isAudio ? 'audio/mpeg' : 'application/octet-stream')), ...(cacheData.headers || {}), }; res.writeHead(HTTP_STATUS.OK, responseHeaders); // 监控数据传输速度 let bytesSent = 0; const startTime = Date.now(); readStream.on('data', (chunk) => { bytesSent += chunk.length; // 每10MB记录一次进度 if (bytesSent % (10 * 1024 * 1024) === 0) { const elapsedSeconds = (Date.now() - startTime) / 1000; const mbSent = bytesSent / (1024 * 1024); const mbps = mbSent / elapsedSeconds; console.log(`Full file progress for ${cacheContentFile}: ${mbSent.toFixed(2)}MB sent at ${mbps.toFixed(2)}MB/s`); } }); readStream.pipe(res); }); readStream.on('error', (err) => { console.error(`Read stream error for ${cacheContentFile}:`, err); handleCacheReadError(res, cacheContentFile); }); // 处理客户端提前关闭连接 res.on('close', () => { if (!res.writableEnded) { console.log(`Client closed connection prematurely for ${cacheContentFile}`); readStream.destroy(); } }); } } // 处理响应错误 const handleResponseError = (res, tempCacheContentFile, realUrl) => { viewsInfo.increment('fetchApiError'); console.error(`Error fetching from real URL: ${realUrl}`); sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, `Bad Gateway: Failed to fetch from ${realUrl}`); if (fs.existsSync(tempCacheContentFile)) { try { fs.unlinkSync(tempCacheContentFile); } catch (unlinkErr) { console.error(`Error unlinking temp file ${tempCacheContentFile}:`, unlinkErr); } } }; // 处理缓存读取错误 const handleCacheReadError = (res, filePath) => { viewsInfo.increment('cacheReadError'); console.error(`Error reading cache file: ${filePath}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Internal Server Error: Unable to read cache content file'); }; // 启动服务器 server.listen(port, () => { console.log(`Proxy server is running on http://localhost:${port}`); }); // 处理 SIGINT 信号(Ctrl+C) process.on('SIGINT', () => { console.log('Received SIGINT. Shutting down gracefully...'); server.close(() => { console.log('Server closed.'); process.exit(0); }); setTimeout(() => { console.error('Forcing shutdown...'); process.exit(1); }, 10000); });