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 requestTimeout = 20000; // 10 seconds const cacheDir = pathModule.join(__dirname, '.cache'); const args = process.argv.slice(2); const pathIndex = {}; // 增加访问计数器 const viewsInfo = { // 请求次数 request: 0, // 缓存命中次数 cacheHit: 0, // API调用次数 apiCall: 0, // 缓存调用次数 cacheCall: 0, }; // 默认端口号和 API 地址 let port = 9001; let apiEndpoint = 'https://x-mo.cn:9001/get/'; // 解析命令行参数 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); } // 定时清理过期缓存数据 setInterval(() => { const currentTime = Date.now(); for (const key in pathIndex) { if (currentTime - pathIndex[key].timestamp > 24 * 60 * 60 * 1000) { delete pathIndex[key]; } } }, 60 * 60 * 1000); // 每隔 1 小时执行一次 // 处理请求并返回数据 const server = http.createServer(async (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 token = parsedUrl.pathname.split('/').slice(2).join('/'); // 处理根路径请求 if (reqPath === 'favicon.ico') { res.writeHead(204); res.end(); return; } // 返回 endpoint, 缓存目录, 缓存数量, 用于监听服务是否正常运行 if (reqPath === 'endpoint') { 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; } // 清理缓存 if (reqPath === 'clean') { // 删除缓存目录下的所有文件 const files = fs.readdirSync(cacheDir); for (const file of files) { const filePath = pathModule.join(cacheDir, file); fs.unlinkSync(filePath); } res.writeHead(200, { 'Content-Type': 'text/plain;charset=UTF-8' }); res.end(JSON.stringify({ code: 200, data: { cacheDir: cacheDir, pathIndexCount: Object.keys(pathIndex).length, files: files.length, } })); return; } // 当没有 token 或 undefined if (token === '' || typeof token === 'undefined') { token = reqPath; reqPath = 'go'; } // 检查第一个路径只能是 avatar,endpoint,go,bbs,www if (!['avatar', 'go', 'bbs', 'www', 'url', 'thumb'].includes(reqPath)) { res.writeHead(404, { 'Content-Type': 'text/plain;charset=UTF-8' }); res.end('Not Found'); return; } 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++; const uniqidhex = crypto.createHash('md5').update(reqPath + token).digest('hex'); let cacheMetaFile = ''; let cacheContentFile = ''; let tempCacheContentFile = ''; if (pathIndex[uniqidhex]) { cacheMetaFile = pathModule.join(cacheDir, `${pathIndex[uniqidhex].uniqid}.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(304); res.end(); } else { // 增加缓存命中次数 viewsInfo.cacheHit++; serveFromCache(cacheData, cacheContentFile, cacheMetaFile, res); } } else { try { // 增加 API 调用次数 viewsInfo.apiCall++; const apiData = await fetchApiData(reqPath, token, sign); 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)) { serveFromCache(data, cacheContentFile, cacheMetaFile, res); } else { fetchAndServe(data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res); } } else { // utf8 解码 res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8;' }); res.end(apiData.message || 'Bad Gateway'); } } catch (error) { res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8' }); res.end('Bad Gateway: Failed to decode JSON ' + error); } } }); // 检查缓存头并返回是否为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); if (lastModified <= modifiedSince) { isNotModified = true; } } return { cacheData, isNotModified }; }; // 检查缓存是否有效 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(); }; // 从 API 获取数据 const fetchApiData = (reqPath, token, sign) => { return new Promise((resolve, reject) => { // 将请求路径和参数进行编码 const queryParams = querystring.stringify({ type: reqPath, sign: sign }); const apiUrl = `${apiEndpoint}?${queryParams}`; const apiReq = https.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 }, timeout: requestTimeout, rejectUnauthorized: false }, (apiRes) => { let data = ''; apiRes.on('data', chunk => data += chunk); apiRes.on('end', () => { try { resolve(JSON.parse(data)); } catch (error) { reject(error); } }); }); apiReq.on('error', reject); apiReq.end(); }); }; // 从真实 URL 获取数据并写入缓存 const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, cacheMetaFile, res) => { https.get(data.realUrl, { timeout: requestTimeout * 10, rejectUnauthorized: false }, (realRes) => { const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: 'w' }); 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.writeFileSync(cacheMetaFile, JSON.stringify(data)); } else { console.warn('Warning: content-length is undefined for the response from:', data.realUrl); } const defaultHeaders = { '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', 'Expires': new Date(Date.now() + 31536000000).toUTCString(), 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive', 'Date': new Date().toUTCString(), 'Last-Modified': new Date().toUTCString(), }; res.writeHead(realRes.statusCode, Object.assign({}, defaultHeaders, data.headers)); 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}`); } } }); realRes.on('error', (e) => { handleResponseError(res, tempCacheContentFile, data.realUrl); }); }).on('error', (e) => { handleResponseError(res, tempCacheContentFile, data.realUrl); }); }; // 从缓存中读取数据并返回 const serveFromCache = (cacheData, cacheContentFile, cacheMetaFile, res) => { // 增加缓存调用次数 viewsInfo.cacheCall++; const readStream = fs.createReadStream(cacheContentFile); let isVideo = cacheData.path && typeof cacheData.path === 'string' && cacheData.path.includes('.mp4'); // 查询 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); } } 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', 'Expires': new Date(Date.now() + 31536000000).toUTCString(), 'Accept-Ranges': 'bytes', 'Connection': 'keep-alive', 'Date': new Date().toUTCString(), 'Last-Modified': new Date().toUTCString(), } res.writeHead(200, Object.assign({}, defaultHeaders, cacheData.headers)); readStream.pipe(res); }); readStream.on('error', (err) => { handleCacheReadError(res); }); }; // 处理响应错误 const handleResponseError = (res, tempCacheContentFile, realUrl) => { if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain;charset=UTF-8' }); res.end(`Bad Gateway: ${realUrl}`); } if (fs.existsSync(tempCacheContentFile)) { fs.unlinkSync(tempCacheContentFile); } }; // 处理缓存读取错误 const handleCacheReadError = (res) => { if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'text/plain;charset=UTF-8' }); res.end('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); });