const express = require('express'); const path = require('path'); const fs = require('fs'); const axios = require('axios'); const { randomUUID } = require('crypto'); require('dotenv').config(); const CAPTURE_FILE = path.join(__dirname, '..', '..', 'captures.jsonl'); let capturedData = []; let clients = []; function ensureCaptureId(data) { if (!data || typeof data !== 'object') return data; if (typeof data.id !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(data.id)) { data.id = randomUUID(); } return data; } function rewriteCaptureFile() { fs.writeFileSync( CAPTURE_FILE, capturedData.map(data => JSON.stringify(data)).join('\n') + (capturedData.length ? '\n' : ''), 'utf-8' ); } // ======================== 数据持久化 ======================== /** * 从文件加载历史数据 */ function loadFromFile() { try { if (fs.existsSync(CAPTURE_FILE)) { const content = fs.readFileSync(CAPTURE_FILE, 'utf-8').trim(); if (content) { capturedData = content.split('\n') .filter(line => line.trim()) .map(line => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean) .map(ensureCaptureId); // 旧记录没有稳定 ID;补齐后写回,确保选择与删除跨刷新保持一致。 rewriteCaptureFile(); console.log(`Loaded ${capturedData.length} historical records from ${CAPTURE_FILE}`); } } } catch (e) { console.error('Failed to load capture file:', e.message); } } /** * 追加一条数据到文件 */ function appendToFile(data) { try { fs.appendFileSync(CAPTURE_FILE, JSON.stringify(data) + '\n', 'utf-8'); } catch (e) { console.error('Failed to append capture to file:', e.message); } } /** * 清空文件 */ function clearFile() { try { fs.writeFileSync(CAPTURE_FILE, '', 'utf-8'); } catch (e) { console.error('Failed to clear capture file:', e.message); } } // ======================== 请求重放 ======================== /** * 重放一个被抓包的请求 * @param {object} param0 { path, method, params, crypto, rawPath, requestHeaders } * @returns {Promise} 重放结果 */ async function replayRequest({ path: apiPath, method, params, crypto, rawPath, requestHeaders }) { const cryptoModule = require('../server/crypto'); const url = require('url'); const baseUrl = 'https://music.163.com'; const origPath = rawPath || apiPath; // 根据加密类型构造请求 let requestUrl, requestBody, requestHeadersObj = {}; switch (crypto) { case 'eapi': { const eapiPath = '/eapi' + apiPath.replace(/^\/api/, ''); const encrypted = cryptoModule.eapi.encryptRequest(baseUrl + eapiPath, params); requestUrl = baseUrl + eapiPath; requestBody = encrypted.body; break; } case 'linuxapi': { const encrypted = cryptoModule.linuxapi.encryptRequest(apiPath, params); requestUrl = baseUrl + '/api/linux/forward'; requestBody = encrypted.body; break; } case 'api': { requestUrl = baseUrl + apiPath; requestBody = new url.URLSearchParams(params).toString(); requestHeadersObj['Content-Type'] = 'application/x-www-form-urlencoded'; break; } default: { // weapi / 未知 -> 直接 POST JSON requestUrl = baseUrl + apiPath; requestBody = params; requestHeadersObj['Content-Type'] = 'application/json'; break; } } // 合并原始请求头 if (requestHeaders) { ['cookie', 'user-agent', 'referer', 'origin', 'x-real-ip'].forEach(key => { if (requestHeaders[key]) requestHeadersObj[key] = requestHeaders[key]; }); } if (!requestHeadersObj['User-Agent'] && !requestHeadersObj['user-agent']) { requestHeadersObj['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; } requestHeadersObj['X-Real-IP'] = '118.88.88.88'; const startTime = Date.now(); const response = await axios.post(requestUrl, requestBody, { headers: requestHeadersObj, responseType: 'arraybuffer', timeout: 15000, httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }), }); const duration = Date.now() - startTime; // 尝试解密响应 let responseData = null; const resBuffer = Buffer.from(response.data); const contentType = response.headers['content-type'] || ''; if (contentType.includes('json') || contentType.includes('text')) { try { responseData = JSON.parse(resBuffer.toString()); } catch { // 非 JSON } } // 尝试 eapi 解密 if (!responseData && resBuffer.length > 0) { try { const decrypted = cryptoModule.eapi.decrypt(resBuffer).toString(); responseData = JSON.parse(decrypted); } catch { // 无法解密 } } return { timestamp: new Date().toISOString(), path: apiPath, rawPath: origPath, crypto: crypto || 'replay', param: params, response: responseData, statusCode: response.status, method: method || 'POST', duration, requestHeaders: requestHeadersObj, responseHeaders: { ...response.headers }, replay: true, }; } // ======================== Express App ======================== /** * 创建前端 Express app (共享 capturedData 和 clients) */ function createApp() { const app = express(); // 启动时加载历史数据 loadFromFile(); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ limit: '10mb', extended: true })); app.use(express.static(path.join(__dirname, 'public'))); app.post('/api/capture', (req, res) => { const data = ensureCaptureId(req.body); capturedData.push(data); console.log('Captured data:', data.path); // 持久化 appendToFile(data); broadcastData(); res.status(200).send('OK'); }); app.get('/api/data', (req, res) => { res.json(capturedData); }); app.get('/api/version', (req, res) => { try { const packageJson = require('../../package.json'); res.json({ version: packageJson.version }); } catch (error) { console.error('Failed to read package.json:', error); res.json({ version: '0.1.0' }); } }); app.post('/api/clear', (req, res) => { capturedData = []; clearFile(); broadcastData(); res.json({ success: true }); }); app.post('/api/delete', (req, res) => { const ids = Array.isArray(req.body.ids) ? [...new Set(req.body.ids.filter(id => typeof id === 'string'))] : []; if (ids.length === 0) return res.status(400).json({ error: 'ids must be a non-empty string array' }); const idSet = new Set(ids); const previousData = capturedData; const nextData = capturedData.filter(item => !idSet.has(item.id)); const deleted = previousData.length - nextData.length; try { capturedData = nextData; rewriteCaptureFile(); broadcastData(); res.json({ success: true, deleted }); } catch (e) { capturedData = previousData; console.error('Failed to persist deleted captures:', e.message); res.status(500).json({ error: 'Failed to persist deleted captures' }); } }); // 请求重放端点 app.post('/api/replay', async (req, res) => { const { path: apiPath, method, params, crypto, rawPath, requestHeaders } = req.body; if (!apiPath) { return res.status(400).json({ error: 'Missing path' }); } try { if (!['eapi', 'linuxapi', 'api'].includes(crypto)) { return res.status(400).json({ error: `Unsupported replay crypto: ${crypto || 'unknown'}` }); } const result = ensureCaptureId(await replayRequest({ path: apiPath, method, params, crypto, rawPath, requestHeaders })); // 将重放结果也加入抓包列表 capturedData.push(result); appendToFile(result); broadcastData(); res.json(result); } catch (e) { console.error('Replay failed:', e.message); res.status(500).json({ error: e.message }); } }); // 获取/设置完整抓包模式 app.get('/api/settings', (req, res) => { res.json({ fullCapture: global.fullCapture === true, }); }); app.post('/api/settings', (req, res) => { const { fullCapture } = req.body; if (typeof fullCapture === 'boolean') { global.fullCapture = fullCapture; console.log(`Full capture mode: ${fullCapture ? 'ON' : 'OFF'}`); res.json({ success: true, fullCapture }); } else { res.status(400).json({ error: 'fullCapture must be boolean' }); } }); // 获取数据统计信息 app.get('/api/stats', (req, res) => { const total = capturedData.length; const methods = {}; const cryptos = {}; const statusGroups = {}; capturedData.forEach(item => { const m = (item.method || 'UNKNOWN').toUpperCase(); methods[m] = (methods[m] || 0) + 1; const c = item.crypto || 'unknown'; cryptos[c] = (cryptos[c] || 0) + 1; const sg = String(item.statusCode || '?')[0] + 'xx'; statusGroups[sg] = (statusGroups[sg] || 0) + 1; }); res.json({ total, methods, cryptos, statusGroups }); }); app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.write(`data: ${JSON.stringify(capturedData)}\n\n`); clients.push(res); req.on('close', () => { clients = clients.filter(client => client !== res); }); }); return app; } function broadcastData() { clients.forEach(client => { try { client.write(`data: ${JSON.stringify(capturedData)}\n\n`); } catch (e) { clients = clients.filter(c => c !== client); } }); } /** * 启动前端服务器 * @returns {Promise} */ function startClient(port) { return new Promise((resolve, reject) => { const PORT = port || process.env.PORT || 3000; const app = createApp(); const server = app.listen(PORT, () => { console.log(`Frontend server running at http://localhost:${PORT}`); resolve(server); }); server.on('error', reject); }); } // 直接运行时启动 if (require.main === module) { startClient(); } module.exports = { startClient, createApp };