const http = require('http'); const https = require('https'); const url = require('url'); const querystring = require('querystring'); const fs = require('fs'); const path = require('path'); const requestTimeout = 10000; // 10 seconds const cacheDir = path.join(__dirname, '.cache'); const args = process.argv.slice(2); let port = 9001; let apiEndpoint = 'https://oss.x-php.com/alist/link'; // 解析命令行参数 args.forEach(arg => { 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); } const server = http.createServer(async (req, res) => { if (req.url === '/favicon.ico') { res.writeHead(204); res.end(); return; } const parsedUrl = url.parse(req.url, true); const reqPath = parsedUrl.pathname; const sign = parsedUrl.query.sign || ''; // 只要reqPath的文件名,不要路径 const reqName = parsedUrl.pathname.split('/').pop(); const cacheMetaFile = path.join(cacheDir, `${reqName.replace(/\//g, '_')}.meta`); const cacheContentFile = path.join(cacheDir, `${reqName.replace(/\//g, '_')}.content`); const tempCacheContentFile = path.join(cacheDir, `${reqName.replace(/\//g, '_')}.temp`); if (!sign || reqPath === '/') { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Bad Request: Missing sign or path'); return; } if (isCacheValid(cacheMetaFile, cacheContentFile)) { serveFromCache(cacheMetaFile, cacheContentFile, res); } else { try { const apiData = await fetchApiData(reqPath, sign); if (apiData.code === 200 && apiData.data && apiData.data.url) { const { url: realUrl, cloudtype, expiration } = apiData.data; const data = { realUrl, cloudtype, expiration: expiration * 1000 }; if (expiration > 0) { fs.writeFileSync(cacheMetaFile, JSON.stringify(data)); } // 如果 cacheContentFile 存在 直接调用它 if (fs.existsSync(cacheContentFile)) { serveFromCache(cacheMetaFile, cacheContentFile, res); return; } fetchAndServe(data, tempCacheContentFile, cacheContentFile, res); } else { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(apiData.message || 'Bad Gateway'); } } catch (error) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end('Bad Gateway: Failed to decode JSON' + error); } } }); 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(); }; const fetchApiData = (reqPath, sign) => { return new Promise((resolve, reject) => { const postData = querystring.stringify({ path: reqPath, sign }); const apiReq = https.request(apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'Content-Length': Buffer.byteLength(postData), 'sign': sign }, timeout: requestTimeout }, (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.write(postData); apiReq.end(); }); }; const fetchAndServe = (data, tempCacheContentFile, cacheContentFile, res) => { https.get(data.realUrl, { timeout: requestTimeout * 10 }, (realRes) => { // 创建临时缓存文件流 const cacheStream = fs.createWriteStream(tempCacheContentFile, { flags: 'w' }); res.writeHead(realRes.statusCode, { ...realRes.headers, 'Cloud-Type': data.cloudtype, 'Cloud-Expiration': data.expiration, }); realRes.pipe(cacheStream); realRes.pipe(res); realRes.on('end', () => { // 下载完成后,将临时文件重命名为最终缓存文件 fs.renameSync(tempCacheContentFile, cacheContentFile); cacheStream.end(); }); realRes.on('error', (e) => { if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Bad Gateway: ${data.realUrl}`); } fs.unlinkSync(tempCacheContentFile); // 删除临时文件 }); }).on('error', (e) => { if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Bad Gateway: ${data.realUrl}`); } fs.unlinkSync(tempCacheContentFile); // 删除临时文件 }); }; const serveFromCache = (cacheMetaFile, cacheContentFile, res) => { const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')); const readStream = fs.createReadStream(cacheContentFile); readStream.on('open', () => { res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Cloud-Type': cacheData.cloudtype, 'Cloud-Expiration': cacheData.expiration, }); readStream.pipe(res); }); readStream.on('error', (err) => { if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error: Unable to read cache content file'); } }); }; server.listen(port, () => { console.log(`Proxy server is running on http://localhost:${port}`); }); // Graceful shutdown process.on('SIGINT', () => { console.log('Received SIGINT. Shutting down gracefully...'); server.close(() => { console.log('Server closed.'); process.exit(0); }); // Force shutdown after 10 seconds if not closed setTimeout(() => { console.error('Forcing shutdown...'); process.exit(1); }, 10000); });