From a211083da587ed2ef00c7d7e5ccc733940e15663 Mon Sep 17 00:00:00 2001 From: XiaoMo Date: Mon, 26 May 2025 08:32:48 +0800 Subject: [PATCH] 11111 --- package.json | 8 +- source.js | 677 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 418 insertions(+), 267 deletions(-) diff --git a/package.json b/package.json index 9c7a7ea..9c6e5a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,12 @@ { + "dependencies": { + "sharp": "^0.33.4" + }, "devDependencies": { "javascript-obfuscator": "^4.1.1" }, - "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf" + "packageManager": "pnpm@9.15.1+sha512.1acb565e6193efbebda772702950469150cf12bcc764262e7587e71d19dc98a423dff9536e57ea44c49bdf790ff694e83c27be5faa23d67e0c033b583be4bfcf", + "scripts": { + "start": "node source.js" + } } diff --git a/source.js b/source.js index 244f6bd..e3be6a6 100644 --- a/source.js +++ b/source.js @@ -5,134 +5,217 @@ const querystring = require('querystring'); const fs = require('fs'); const pathModule = require('path'); const crypto = require('crypto'); +const path = require('path'); +const sharp = require('sharp'); -const cacheDir = pathModule.join(__dirname, '.cache'); -const args = process.argv.slice(2); +const CACHE_DIR_NAME = '.cache'; +const DEFAULT_PORT = 9001; +const DEFAULT_API_ENDPOINT = 'http://183.6.121.121:9005/get/'; + +const cacheDir = pathModule.join(__dirname, CACHE_DIR_NAME); const pathIndex = {}; -// 增加访问计数器 +// 访问计数器 const viewsInfo = { - // 请求次数 request: 0, - // 缓存命中次数 cacheHit: 0, - // API调用次数 apiCall: 0, - // 缓存调用次数 cacheCall: 0, - // 缓存读取错误次数 cacheReadError: 0, - // API 调用错误次数 fetchApiError: 0, - // API 调用错误次数 fetchApiWarning: 0, + increment: function(key) { + if (this.hasOwnProperty(key)) { + this[key]++; + } + } }; -// 默认端口号和 API 地址 -let port = 9001; -let apiEndpoint = 'http://183.6.121.121:9005/get/'; +let port = DEFAULT_PORT; +let apiEndpoint = DEFAULT_API_ENDPOINT; -// 解析命令行参数 -args.forEach(arg => { - // 去掉-- - if (arg.startsWith('--')) { - arg = arg.substring(2); - } - const [key, value] = arg.split('='); - if (key === 'port') { - port = parseInt(value, 10); - } else if (key === 'api') { - apiEndpoint = value; - } -}); - -// 确保缓存目录存在 -if (!fs.existsSync(cacheDir)) { - fs.mkdirSync(cacheDir); +// 解析命令行参数函数 +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 > 24 * 60 * 60 * 1000) { + if (currentTime - pathIndex[key].timestamp > CACHE_EXPIRY_MS) { delete pathIndex[key]; + // Consider deleting actual cache files as well if not managed elsewhere } } -}, 60 * 60 * 1000); // 每隔 1 小时执行一次 +}, CACHE_CLEANUP_INTERVAL_MS); -// 处理请求并返回数据 -const server = http.createServer(async (req, res) => { +// 统一发送错误响应 +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 } = apiData.data; + const data = { realUrl, cloudtype, expiration: expiration * 1000, path: apiPath, headers, uniqid }; + + pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() }; + const cacheMetaFile = pathModule.join(cacheDir, `${data.uniqid}.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 file is very small and content length from API differs, consider re-fetching. + // The 2048 threshold seems arbitrary; could be configurable or based on content type. + 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); + } + } else { + fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); + } +} + +async function tryServeFromStaleCacheOrError(uniqidhex, res, errorMessage) { + if (pathIndex[uniqidhex]) { + const cacheMetaFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.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); + 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); - - // 解析得到 sign 字段 const sign = parsedUrl.query.sign || ''; - - // 获取第一个路径 - let reqPath = parsedUrl.pathname.split('/')[1]; - - // 取第一个路径以外的路径 + let reqPath = parsedUrl.pathname.split('/')[1] || ''; // Ensure reqPath is not undefined let token = parsedUrl.pathname.split('/').slice(2).join('/'); - // 处理根路径请求 - if (reqPath === 'favicon.ico') { - res.writeHead(204); - res.end(); - return; - } + if (reqPath === 'favicon.ico') return handleFavicon(req, res); + if (reqPath === 'endpoint') return handleEndpoint(req, res, parsedUrl); - // 返回 endpoint, 缓存目录, 缓存数量, 用于监听服务是否正常运行 - if (reqPath === 'endpoint') { - if (parsedUrl.query.api) { - // 判断 parsedUrl.query.api 是否为网址 - const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w.-]*)*\/?$/; - if (urlRegex.test(parsedUrl.query.api)) { - apiEndpoint = parsedUrl.query.api; - } - } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - code: 200, - data: { - api: apiEndpoint, - port: port, - cacheDir: cacheDir, - pathIndexCount: Object.keys(pathIndex).length, - viewsInfo: viewsInfo - } - })); - return; - } - - // 当没有 token 或 undefined - if (token === '' || typeof token === 'undefined') { + if (!token && reqPath) { // If token is empty but reqPath is not, assume reqPath is the token token = reqPath; - reqPath = 'app'; + reqPath = 'app'; // Default to 'app' if only one path segment is provided } - // 检查第一个路径只能是 avatar,endpoint,go,bbs,www - if (!['avatar', 'go', 'bbs', 'www', 'url', 'thumb', 'app'].includes(reqPath)) { - res.writeHead(404, { 'Content-Type': 'text/plain;charset=UTF-8' }); - res.end('Not Found'); - return; + 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.`); } - if (!token || reqPath === '') { - res.writeHead(400, { 'Content-Type': 'text/plain;charset=UTF-8' }); - res.end('Bad Request: Missing Token or path (' + reqPath + ')'); - return; - } - - // 增加请求次数 - viewsInfo.request++; - + viewsInfo.increment('request'); const uniqidhex = crypto.createHash('md5').update(reqPath + token + sign).digest('hex'); - let cacheMetaFile = ''; let cacheContentFile = ''; - let tempCacheContentFile = ''; if (pathIndex[uniqidhex]) { cacheMetaFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.meta`); @@ -140,156 +223,170 @@ const server = http.createServer(async (req, res) => { } if (pathIndex[uniqidhex] && isCacheValid(cacheMetaFile, cacheContentFile)) { - const { cacheData, isNotModified } = await checkCacheHeaders(req, cacheMetaFile); if (isNotModified) { - res.writeHead(304); + res.writeHead(HTTP_STATUS.NOT_MODIFIED); res.end(); } else { - // 增加缓存命中次数 - viewsInfo.cacheHit++; + viewsInfo.increment('cacheHit'); serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res); } - } else { try { - // 增加 API 调用次数 - viewsInfo.apiCall++; + viewsInfo.increment('apiCall'); const apiData = await fetchApiData(reqPath, token, sign); - // 302 或 301 重定向 - if (apiData.code === 302 || apiData.code === 301) { - res.writeHead(302, { Location: apiData.data.url }); - res.end(); - return; + if (apiData.code === HTTP_STATUS.REDIRECT || apiData.code === 301) { + return handleApiRedirect(res, apiData); } - if (apiData.code === 200 && apiData.data && apiData.data.url) { - const { url: realUrl, cloudtype, expiration, path, headers, uniqid } = apiData.data; - const data = { realUrl, cloudtype, expiration: expiration * 1000, path, headers, uniqid }; - - // 修改 pathIndex 记录时,添加时间戳 - pathIndex[uniqidhex] = { uniqid: data.uniqid, timestamp: Date.now() }; - cacheMetaFile = pathModule.join(cacheDir, `${data.uniqid}.meta`); - cacheContentFile = pathModule.join(cacheDir, `${data.uniqid}.content`); - tempCacheContentFile = pathModule.join(cacheDir, `${data.uniqid}_${crypto.randomBytes(16).toString('hex')}.temp`); - // 重新写入 meta 缓存 - fs.writeFileSync(cacheMetaFile, JSON.stringify(data)); - // 如果内容缓存存在, 则直接调用 - if (fs.existsSync(cacheContentFile)) { - // 读取大小是否一致 - const contentLength = fs.statSync(cacheContentFile).size; - // 如果文件小于 2KB, 且与缓存文件大小不一致时,重新获取 - if (contentLength < 2048 && data.headers['content-length'] !== contentLength) { - fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); - return; - } - serveFromCache(data, cacheContentFile, cacheMetaFile, res); - } else { - fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); - } + if (apiData.code === HTTP_STATUS.OK && apiData.data && apiData.data.url) { + await processSuccessfulApiData(apiData, uniqidhex, reqPath, token, sign, res); } else { - - // 记录响应错误 - viewsInfo.fetchApiWarning++; - - - // 如果有缓存有忽略过期,直接调用 - if (pathIndex[uniqidhex] && fs.existsSync(cacheMetaFile) && fs.existsSync(cacheContentFile)) { - serveFromCache(JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')), cacheContentFile, cacheMetaFile, res); - } else { - // utf8 解码 - res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8;' }); - res.end(apiData.message || 'Bad Gateway'); - } - - // utf8 解码 - + viewsInfo.increment('fetchApiWarning'); + await tryServeFromStaleCacheOrError(uniqidhex, res, apiData.message); } } catch (error) { - // 记录响应错误 - viewsInfo.fetchApiError++; - - // 起用备用 API - // apiEndpoint = 'https://x-mo.cn:9001/get/'; - - // utf8 解码 - res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8;' }); - res.end('Bad Gateway: Failed to decode JSON' + 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 -const checkCacheHeaders = async (req, cacheMetaFile) => { - const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')); - const ifNoneMatch = req.headers['if-none-match']; - const ifModifiedSince = req.headers['if-modified-since']; - let isNotModified = false; - if (ifNoneMatch && ifNoneMatch === cacheData.uniqid) { - isNotModified = true; - } else if (ifModifiedSince) { - const lastModified = new Date(cacheData.headers['Last-Modified']); - const modifiedSince = new Date(ifModifiedSince); +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']; - if (lastModified <= modifiedSince) { - isNotModified = true; + // Check ETag first + if (ifNoneMatch && cacheData.uniqid && ifNoneMatch === cacheData.uniqid) { + return { cacheData, isNotModified: true }; } - } - return { cacheData, isNotModified }; -}; + // 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); + // If we can't read meta, assume cache is invalid or treat as not modified: false + // Returning a dummy cacheData or null might be better depending on how caller handles it. + // For now, let it propagate and potentially fail later if cacheData is expected. + // Or, more safely, indicate cache is not valid / not modified is false. + return { cacheData: null, isNotModified: false }; // Indicate failure to load cacheData + } +} + // 检查缓存是否有效 -const isCacheValid = (cacheMetaFile, cacheContentFile) => { - if (!fs.existsSync(cacheMetaFile) || !fs.existsSync(cacheContentFile)) return false; - const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')); - return cacheData.expiration > Date.now(); -}; +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); + // 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 fetchApiData = (reqPath, token, sign) => { - return new Promise((resolve, reject) => { - const queryParams = querystring.stringify({ - type: reqPath, - sign: sign - }); - const apiUrl = `${apiEndpoint}?${queryParams}`; - const parsedUrl = url.parse(apiUrl); - const protocol = parsedUrl.protocol === 'https:' ? https : http; +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'; - const apiReq = protocol.request(apiUrl, { - method: 'GET', - headers: { - 'Accept': 'application/json; charset=utf-8', - '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', - 'token': token - }, - // 超时设置, 5 秒 - timeout: 5000, - rejectUnauthorized: false, - // 超时设置 - }, (apiRes) => { - let data = ''; - apiRes.on('data', chunk => data += chunk); +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 { - resolve(JSON.parse(data)); - } catch (error) { - reject(error); + 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('error', reject); + + 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 fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res) => { +const REAL_URL_FETCH_TIMEOUT_MS = 0; // 0 means no timeout for the actual file download - // 不限超时 - https.get(data.realUrl, { timeout: 0, rejectUnauthorized: false }, (realRes) => { +const fetchAndServe = (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' }); let isVideo = data.path && typeof data.path === 'string' && data.path.includes('.mp4'); @@ -300,9 +397,11 @@ const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFi // contentLength 小于 2KB 且与缓存文件大小不一致时,重新获取 if (contentLength < 2048 && data.headers['content-length'] !== contentLength) { console.warn('Warning: content-length is different for the response from:', data.realUrl); - // 返回错误 - res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8;' }); - res.end(`Bad Gateway: ${data.realUrl}`); + sendErrorResponse(res, HTTP_STATUS.BAD_GATEWAY, `Bad Gateway: Content-Length mismatch for ${data.realUrl}`); + // Clean up temp file if stream hasn't started or failed early + if (fs.existsSync(tempCacheContentFile)) { + fs.unlinkSync(tempCacheContentFile); + } return; } @@ -313,112 +412,158 @@ const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFi console.warn('Warning: content-length is undefined for the response from:', data.realUrl); } - const defaultHeaders = { + const baseHeaders = { 'Cloud-Type': data.cloudtype, 'Cloud-Expiration': data.expiration, - 'Content-Type': isVideo ? 'video/mp4' : 'application/octet-stream', 'ETag': data.uniqid || '', - 'Cache-Control': 'public, max-age=31536000', + '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': new Date().toUTCString(), + 'Date': new Date().toUTCString(), // Should be set by the server, but good for consistency + 'Last-Modified': data.headers['last-modified'] || new Date(fs.statSync(cacheMetaFile).mtime).toUTCString(), // Prefer API's Last-Modified if available + }; + const responseHeaders = { + ...baseHeaders, + 'Content-Type': realRes.headers['content-type'] || (isVideo ? 'video/mp4' : 'application/octet-stream'), // Prefer actual content-type + ...data.headers, // Allow API to override some headers if necessary }; - res.writeHead(realRes.statusCode, Object.assign({}, defaultHeaders, data.headers)); + res.writeHead(realRes.statusCode, responseHeaders); realRes.pipe(cacheStream); realRes.pipe(res); realRes.on('end', () => { - cacheStream.end(); - if (fs.existsSync(tempCacheContentFile)) { - try { - fs.renameSync(tempCacheContentFile, cacheContentFile); - } catch (err) { - console.error(`Error renaming file: ${err}`); + cacheStream.end(() => { // Ensure stream is fully flushed before renaming + if (fs.existsSync(tempCacheContentFile)) { + try { + // Ensure the target directory exists before renaming + const targetDir = pathModule.dirname(cacheContentFile); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); + } + fs.renameSync(tempCacheContentFile, cacheContentFile); + console.log(`Successfully cached: ${cacheContentFile}`); + } catch (renameError) { + console.error(`Error renaming temp cache file ${tempCacheContentFile} to ${cacheContentFile}:`, renameError); + // If rename fails, try to remove the temp file to avoid clutter + try { fs.unlinkSync(tempCacheContentFile); } catch (e) { /* ignore */ } + } + } else { + // This case might indicate an issue if the stream ended but no temp file was created/found + console.warn(`Temp cache file ${tempCacheContentFile} not found after stream end for ${data.realUrl}`); } - } + }); }); - realRes.on('error', (e) => { - handleResponseError(res, tempCacheContentFile, data.realUrl); + realRes.on('error', (streamError) => { + console.error(`Error during response stream from ${data.realUrl}:`, streamError); + cacheStream.end(); // Close the writable stream + handleResponseError(res, tempCacheContentFile, data.realUrl); // tempCacheContentFile might be partially written }); - }).on('error', (e) => { - handleResponseError(res, tempCacheContentFile, data.realUrl); + + }).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 }); }; // 从缓存中读取数据并返回 -const serveFromCache = (cacheData, cacheContentFile, cacheMetaFile, res) => { - // 增加缓存调用次数 - viewsInfo.cacheCall++; +function serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res) { + 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; + } + + viewsInfo.increment('cacheCall'); const readStream = fs.createReadStream(cacheContentFile); - let isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4'); + 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; - // 查询 cacheData.headers['content-length'] 是否存在 - if (!cacheData.headers['content-length'] || cacheData.headers['content-length'] === 0) { - // 读取文件大小并更新 cacheData.headers['content-length'] - const contentLength = fs.statSync(cacheContentFile).size; - if (contentLength) { - cacheData.headers['content-length'] = contentLength; - // 更新 cacheData 到缓存 cacheMetaFile - fs.writeFileSync(cacheMetaFile, JSON.stringify(cacheData)); - } else { - console.warn('Warning: content-length is undefined for cached content file:', cacheContentFile); + 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); // Treat stat error as read error + return; } } readStream.on('open', () => { - const defaultHeaders = { - 'Cloud-Type': cacheData.cloudtype, - 'Cloud-Expiration': cacheData.expiration, - 'Content-Type': isVideo ? 'video/mp4' : 'application/octet-stream', - 'ETag': cacheData.uniqid || '', - 'Cache-Control': 'public, max-age=31536000', + const baseHeaders = { + 'Cloud-Type': cacheData.cloudtype || 'unknown', + 'Cloud-Expiration': cacheData.expiration || 'N/A', + 'ETag': cacheData.uniqid || crypto.createHash('md5').update(fs.readFileSync(cacheContentFile)).digest('hex'), // Fallback ETag if missing + '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': new Date().toUTCString(), - } + 'Last-Modified': (cacheData.headers && cacheData.headers['last-modified']) || new Date(fs.statSync(cacheMetaFile).mtime).toUTCString(), + }; - res.writeHead(200, Object.assign({}, defaultHeaders, cacheData.headers)); + 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 || {}), + 'Content-Length': currentContentLength.toString(), // Ensure this is set correctly + }; + + res.writeHead(HTTP_STATUS.OK, responseHeaders); readStream.pipe(res); }); readStream.on('error', (err) => { - handleCacheReadError(res); + console.error(`Read stream error for ${cacheContentFile}:`, err); + handleCacheReadError(res, cacheContentFile); }); -}; + + // 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.cacheReadError++; - - if (!res.headersSent) { - res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8' }); - res.end(`Bad Gateway: ${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)) { - fs.unlinkSync(tempCacheContentFile); + try { + fs.unlinkSync(tempCacheContentFile); + } catch (unlinkErr) { + console.error(`Error unlinking temp file ${tempCacheContentFile}:`, unlinkErr); + } } }; // 处理缓存读取错误 -const handleCacheReadError = (res) => { - - // 增加缓存读取错误次数 - viewsInfo.cacheReadError++; - - if (!res.headersSent) { - res.writeHead(500, { 'Content-Type': 'text/plain;charset=UTF-8' }); - res.end('Internal Server Error: Unable to read cache content file'); - } +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'); }; // 启动服务器