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'; 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(async () => { const currentTime = Date.now(); const keysToDelete = []; const filesToDelete = []; // 第一步:收集需要删除的键和文件 for (const key in pathIndex) { if (currentTime - pathIndex[key].timestamp > CACHE_EXPIRY_MS) { keysToDelete.push(key); const cacheMetaFile = pathModule.join(cacheDir, `${key}.meta`); const cacheContentFile = pathModule.join(cacheDir, `${pathIndex[key].uniqid}.content`); filesToDelete.push(cacheMetaFile, cacheContentFile); } } // 第二步:从内存中删除过期索引 keysToDelete.forEach(key => delete pathIndex[key]); // 第三步:异步删除文件系统中的缓存文件 if (filesToDelete.length > 0) { console.log(`Cleaning up ${keysToDelete.length} expired cache entries`); // 并行删除文件,但限制并发数为10 const deletePromises = filesToDelete.map(file => fs.promises.unlink(file).catch(err => { if (err.code !== 'ENOENT') { // 忽略文件不存在的错误 console.warn(`Failed to delete cache file ${file}:`, err.message); } }) ); // 使用Promise.all处理所有删除操作 await Promise.all(deletePromises); } }, 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) { 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); } else { serveFromCache(data, cacheContentFile, cacheMetaFile, res, reqPath, token, sign, uniqidhex); } } else { fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); } } async function tryServeFromStaleCacheOrError(uniqidhex, res, 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, null, null, null, uniqidhex); 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) { // 处理OPTIONS请求,支持跨域预检 if (req.method === 'OPTIONS') { res.writeHead(200, { 'Access-Control-Allow-Origin': req.headers.origin || '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Max-Age': '86400' }); res.end(); return; } 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', 'qrcode', 'school']; 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, reqPath, token, sign, uniqidhex); } } 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); } else { viewsInfo.increment('fetchApiWarning'); await tryServeFromStaleCacheOrError(uniqidhex, res, apiData.message); } } catch (error) { viewsInfo.increment('fetchApiError'); console.error('Error in API call or processing:', error); await tryServeFromStaleCacheOrError(uniqidhex, res, `Bad Gateway: API request failed. ${error.message}`); } } } const server = http.createServer(handleMainRequest); // 检查缓存头并返回是否为304 async function checkCacheHeaders(req, cacheMetaFile) { try { const metaContent = await fs.promises.readFile(cacheMetaFile, 'utf8'); const cacheData = JSON.parse(metaContent); const ifNoneMatch = req.headers['if-none-match']; const ifModifiedSince = req.headers['if-modified-since']; // Check ETag first if (ifNoneMatch && cacheData.uniqid && ifNoneMatch === cacheData.uniqid) { return { cacheData, isNotModified: true }; } // Check 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); // The time resolution of an HTTP date is one second. // If If-Modified-Since is at least as new as Last-Modified, send 304. if (lastModifiedDate.getTime() <= ifModifiedSinceDate.getTime()) { return { cacheData, isNotModified: true }; } } catch (dateParseError) { console.warn(`Error parsing date for cache header check (${cacheMetaFile}):`, dateParseError); // Proceed as if not modified check failed if dates are invalid } } return { cacheData, isNotModified: false }; } catch (error) { console.error(`Error reading or parsing cache meta file ${cacheMetaFile} in checkCacheHeaders:`, error); return { cacheData: null, isNotModified: false }; // Indicate failure to load cacheData } } // 检查缓存是否有效 async function isCacheValid(cacheMetaFile, cacheContentFile) { try { // 使用Promise.all并行检查文件是否存在 const [metaExists, contentExists] = await Promise.all([ fs.promises.access(cacheMetaFile).then(() => true).catch(() => false), fs.promises.access(cacheContentFile).then(() => true).catch(() => false) ]); if (!metaExists || !contentExists) { return false; } const metaContent = await fs.promises.readFile(cacheMetaFile, 'utf8'); const cacheData = JSON.parse(metaContent); // Ensure expiration is a number and in the future 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; // If meta file is corrupt or unreadable, cache is not valid } } // 从 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'; async function fetchApiData(reqPath, token, sign) { const queryParams = querystring.stringify({ type: reqPath, sign: sign }); const apiUrl = `${apiEndpoint}?${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(errorPayload); // Resolve with error structure for consistency return; } resolve(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(); }); } // 从真实 URL 获取数据并写入缓存 const REAL_URL_FETCH_TIMEOUT_MS = 0; // 0 means no timeout for the actual file download const fetchAndServe = async (data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res) => { const protocol = data.realUrl.startsWith('https:') ? https : http; protocol.get(data.realUrl, { timeout: REAL_URL_FETCH_TIMEOUT_MS, rejectUnauthorized: false }, (realRes) => { const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: 'w', highWaterMark: 64 * 1024 }); // 增加缓冲区大小到64KB let isVideo = data.path && typeof data.path === 'string' && data.path.includes('.mp4'); // 确保 content-length 是有效的 const contentLength = realRes.headers['content-length']; if (contentLength) { data.headers['content-length'] = contentLength; // 异步更新 data 到缓存 cacheMetaFile fs.promises.writeFile(cacheMetaFile, JSON.stringify(data)) .catch(err => console.error(`Error writing meta file ${cacheMetaFile}:`, err)); } else { console.warn('Warning: content-length is undefined for the response from:', data.realUrl); } const baseHeaders = { 'Cloud-Type': data.cloudtype, 'ETag': data.uniqid || '', 'Cache-Control': 'public, max-age=31536000', // 1 year 'Expires': new Date(Date.now() + 31536000000).toUTCString(), 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive', 'Date': new Date().toUTCString(), 'Last-Modified': data.headers['last-modified'] || new Date().toUTCString(), 'Access-Control-Allow-Origin': req.headers.origin || '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; const responseHeaders = { ...baseHeaders, 'Content-Type': realRes.headers['content-type'] || (isVideo ? 'video/mp4' : 'application/octet-stream'), ...data.headers, }; res.writeHead(realRes.statusCode, responseHeaders); // 使用管道优化流传输 const pipeline = require('stream').pipeline; // 创建一个流分支,同时写入缓存和响应 const { PassThrough } = require('stream'); const passThrough = new PassThrough(); passThrough.pipe(cacheStream); passThrough.pipe(res); // 使用pipeline处理流错误 pipeline( realRes, passThrough, (err) => { if (err) { console.error(`Pipeline error for ${data.realUrl}:`, err); handleResponseError(res, tempCacheContentFile, data.realUrl); return; } // 流处理完成后,重命名临时文件 fs.promises.access(tempCacheContentFile) .then(() => { // 确保目标目录存在 return fs.promises.mkdir(pathModule.dirname(cacheContentFile), { recursive: true }) .then(() => fs.promises.rename(tempCacheContentFile, cacheContentFile)) .then(() => console.log(`Successfully cached: ${cacheContentFile}`)) .catch(renameError => { console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameError); return fs.promises.unlink(tempCacheContentFile).catch(() => {}); }); }) .catch(() => { console.warn(`Temp cache file ${tempCacheContentFile} not found after stream end for ${data.realUrl}`); }); } ); }).on('error', (requestError) => { console.error(`Error making GET request to ${data.realUrl}:`, requestError); handleResponseError(res, tempCacheContentFile, data.realUrl); }); }; // 从缓存中读取数据并返回 async function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res, reqPath, token, sign, uniqidhex) { if (!cacheData) { // 缓存数据不可用,尝试重新获取 console.warn(`Cache metadata unavailable for ${cacheContentFile}, attempting to fetch fresh data`); // 如果提供了请求参数,尝试重新获取数据 if (reqPath && token) { try { viewsInfo.increment('apiCall'); const apiData = await fetchApiData(reqPath, token, sign); if (apiData.code === HTTP_STATUS.REDIRECT || apiData.code === 301) { res.writeHead(HTTP_STATUS.REDIRECT, { Location: apiData.data.url }); res.end(); return; } if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) { 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() }; // 写入新的元数据 await fs.promises.mkdir(pathModule.dirname(cacheMetaFile), { recursive: true }); await fs.promises.writeFile(cacheMetaFile, JSON.stringify(data)); // 获取并提供新数据 const tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`); fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); return; } else { viewsInfo.increment('fetchApiWarning'); sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API'); return; } } catch (error) { viewsInfo.increment('fetchApiError'); console.error('Error fetching fresh data:', error); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${error.message}`); return; } } else { // 如果没有提供请求参数,无法重新获取 console.error(`serveFromCache called with null cacheData and insufficient request info for ${cacheContentFile}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Cache metadata unavailable and cannot fetch fresh data.'); return; } } // 使用异步方式获取ETag和Last-Modified let etag = cacheData.uniqid; let lastModified = cacheData.headers && cacheData.headers['last-modified']; if (!etag || !lastModified) { try { const [fileStats, metaStats] = await Promise.all([ fs.promises.stat(cacheContentFile).catch(() => null), fs.promises.stat(cacheMetaFile).catch(() => null) ]); if (!etag && fileStats) { // 使用文件大小和修改时间作为ETag的一部分,避免读取整个文件计算MD5 etag = crypto.createHash('md5') .update(`${fileStats.size}-${fileStats.mtime.getTime()}`) .digest('hex'); } if (!lastModified && metaStats) { lastModified = new Date(metaStats.mtime).toUTCString(); } } catch (error) { console.warn(`Error getting file stats for cache: ${error.message}`); } } const baseHeaders = { 'Cloud-Type': cacheData.cloudtype || 'unknown', 'ETag': etag || '', 'Cache-Control': 'public, max-age=31536000', // 1 year 'Expires': new Date(Date.now() + 31536000000).toUTCString(), 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive', 'Date': new Date().toUTCString(), 'Last-Modified': lastModified || new Date().toUTCString(), 'Access-Control-Allow-Origin': req.headers.origin || '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; viewsInfo.increment('cacheCall'); // 先检查缓存文件是否存在且可读 try { await fs.promises.access(cacheContentFile, fs.constants.R_OK); } catch (error) { console.warn(`Cache content file ${cacheContentFile} not accessible: ${error.message}`); // 如果提供了请求参数,尝试重新获取数据 if (reqPath && token) { console.log(`Attempting to fetch fresh data for ${cacheContentFile}`); try { viewsInfo.increment('apiCall'); const apiData = await fetchApiData(reqPath, token, sign); if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) { 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 tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`); fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); return; } else { sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API'); return; } } catch (fetchError) { console.error(`Error fetching fresh data: ${fetchError.message}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${fetchError.message}`); return; } } else { sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Unable to read cache content file and cannot fetch fresh data.'); return; } } const readStream = fs.createReadStream(cacheContentFile, { highWaterMark: 64 * 1024 }); // 增加读取缓冲区大小 const isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4'); let currentContentLength = cacheData.headers && cacheData.headers['content-length'] ? parseInt(cacheData.headers['content-length'], 10) : 0; if (!currentContentLength || currentContentLength === 0) { try { const stats = fs.statSync(cacheContentFile); currentContentLength = stats.size; if (currentContentLength > 0) { if (!cacheData.headers) cacheData.headers = {}; cacheData.headers['content-length'] = currentContentLength.toString(); // Update meta file if content-length was missing or zero fs.writeFileSync(cacheMetaFile, JSON.stringify(cacheData)); console.log(`Updated content-length in ${cacheMetaFile} to ${currentContentLength}`); } else { console.warn(`Cached content file ${cacheContentFile} has size 0 or stat failed.`); // Potentially treat as an error or serve as is if 0 length is valid for some files } } catch (statError) { console.error(`Error stating cache content file ${cacheContentFile}:`, statError); handleCacheReadError(res, cacheContentFile, reqPath, token, sign, uniqidhex); // Treat stat error as read error return; } } readStream.on('open', () => { const responseHeaders = { ...baseHeaders, 'Content-Type': (cacheData.headers && cacheData.headers['content-type']) || (isVideo ? 'video/mp4' : 'application/octet-stream'), // Merge other headers from cacheData.headers, letting them override base if necessary // but ensure our critical headers like Content-Length (if updated) are preserved. ...(cacheData.headers || {}), }; res.writeHead(HTTP_STATUS.OK, responseHeaders); readStream.pipe(res); }); readStream.on('error', (err) => { console.error(`Read stream error for ${cacheContentFile}:`, err); // 如果提供了请求参数,尝试重新获取数据而不是直接报错 if (reqPath && token) { console.log(`Read stream error, attempting to fetch fresh data for ${cacheContentFile}`); viewsInfo.increment('apiCall'); fetchApiData(reqPath, token, sign) .then(apiData => { if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) { 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 tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`); fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); } else { viewsInfo.increment('fetchApiWarning'); sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API'); } }) .catch(fetchError => { viewsInfo.increment('fetchApiError'); console.error(`Error fetching fresh data after read stream error: ${fetchError.message}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${fetchError.message}`); }); } else { // 如果没有提供请求参数,使用原始的错误处理 handleCacheReadError(res, cacheContentFile, reqPath, token, sign, uniqidhex); } }); // Handle cases where client closes connection prematurely res.on('close', () => { if (!res.writableEnded) { console.log(`Client closed connection prematurely for ${cacheContentFile}. Destroying read stream.`); 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, reqPath, token, sign, uniqidhex) => { viewsInfo.increment('cacheReadError'); console.error(`Error reading cache file: ${filePath}`); // 如果提供了请求参数,尝试重新获取数据 if (reqPath && token) { console.log(`Cache read error, attempting to fetch fresh data for ${filePath}`); viewsInfo.increment('apiCall'); fetchApiData(reqPath, token, sign) .then(apiData => { if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) { const { url: realUrl, cloudtype, expiration, path: apiPath, headers, uniqid, thumb } = apiData.data; const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid, thumb }; // 更新索引 if (uniqidhex) { 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`); // 写入新的元数据 fs.promises.mkdir(pathModule.dirname(cacheMetaFile), { recursive: true }) .then(() => fs.promises.writeFile(cacheMetaFile, JSON.stringify(data))) .then(() => { fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); }) .catch(writeError => { console.error(`Error writing meta file after cache read error: ${writeError.message}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, 'Failed to write cache metadata'); }); } else { viewsInfo.increment('fetchApiWarning'); sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, apiData.message || 'Failed to fetch data from API'); } }) .catch(fetchError => { viewsInfo.increment('fetchApiError'); console.error(`Error fetching fresh data after cache read error: ${fetchError.message}`); sendErrorResponse(res, HTTP_STATUS.INTERNAL_SERVER_ERROR, `Failed to fetch fresh data: ${fetchError.message}`); }); } else { // 如果没有提供请求参数,返回错误 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); });