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 || ''; 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, path, headers, uniqid } = apiData.data; const data = { realUrl, cloudtype, expiration: expiration * 1000, path, headers, uniqid }; if (expiration > 0) { fs.writeFileSync(cacheMetaFile, JSON.stringify(data)); } 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' }); const isVideo = data.path.includes('.mp4'); data.headers['content-length'] = realRes.headers['content-length']; res.writeHead(realRes.statusCode, { ...data.headers, '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(), }); 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) => { if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Bad Gateway: ${data.realUrl}`); } if (fs.existsSync(tempCacheContentFile)) { fs.unlinkSync(tempCacheContentFile); } }); }).on('error', (e) => { if (!res.headersSent) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end(`Bad Gateway: ${data.realUrl}`); } if (fs.existsSync(tempCacheContentFile)) { fs.unlinkSync(tempCacheContentFile); } }); }; const serveFromCache = (cacheMetaFile, cacheContentFile, res) => { const cacheData = JSON.parse(fs.readFileSync(cacheMetaFile, 'utf8')); const readStream = fs.createReadStream(cacheContentFile); const isVideo = cacheData.path.includes('.mp4'); cacheData.headers['content-length'] = fs.statSync(cacheContentFile).size; readStream.on('open', () => { res.writeHead(200, { ...cacheData.headers, '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(), }); 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); }); setTimeout(() => { console.error('Forcing shutdown...'); process.exit(1); }, 10000); });