From bd0d1b436c378e6acdb1dca3e62e02426bde9509 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Fri, 13 Mar 2026 22:23:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8A=93=E5=8C=85?= =?UTF-8?q?=E4=B8=8D=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 +- server.crt | 16 --- server.key | 9 -- src/client/public/index.html | 35 +++--- src/index.js | 221 +++++++++++++++++++++++++++++++++++ src/server/hook.js | 32 +++-- 6 files changed, 265 insertions(+), 54 deletions(-) delete mode 100644 server.crt delete mode 100644 server.key create mode 100644 src/index.js diff --git a/package.json b/package.json index 5e72be9..24ccae1 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,10 @@ { "name": "api-clawer", - "version": "0.0.1", + "version": "0.1.0", "description": "网易云音乐客户端抓包工具", "main": "src/server/app.js", "scripts": { - "dev": "concurrently \"node src/server/app.js\" \"node src/client/app.js\"", - "start": "concurrently \"node src/server/app.js\" \"node src/client/app.js\"", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node src/index.js" }, "keywords": ["netease", "music", "api", "clawer"], "author": "", diff --git a/server.crt b/server.crt deleted file mode 100644 index d8391bd..0000000 --- a/server.crt +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICgjCCAgigAwIBAgIUafzDKFqhg8XS5YogLkI9oGsngkUwCgYIKoZIzj0EAwMw -RDELMAkGA1UEBhMCQ04xJDAiBgNVBAMMG1VuYmxvY2tOZXRlYXNlTXVzaWMgUm9v -dCBDQTEPMA0GA1UECgwGbm9ib2R5MB4XDTI1MDcyNDA4NDY1NloXDTI2MDcyNDA4 -NDY1NlowezELMAkGA1UEBhMCQ04xETAPBgNVBAcMCEhhbmd6aG91MSwwKgYDVQQK -DCNOZXRFYXNlIChIYW5nemhvdSkgTmV0d29yayBDby4sIEx0ZDERMA8GA1UECwwI -SVQgRGVwdC4xGDAWBgNVBAMMDyoubXVzaWMuMTYzLmNvbTB2MBAGByqGSM49AgEG -BSuBBAAiA2IABACkvqL3b68Pv11nQVwm/9BNWGuh1fBKlqYZqnyr9Gy1xEQtf3we -WlwgVmX9rqu9nNEiAbbxIt9aGliHurKkE22+FiOKC4vduYlyyIXKlCAFXigHKr/k -6uKgc2jPfeYoC6OBgzCBgDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAg -gg1tdXNpYy4xNjMuY29tgg8qLm11c2ljLjE2My5jb20wHQYDVR0OBBYEFEeHCj4z -PzDre8No6wsWbrpmQ8hPMB8GA1UdIwQYMBaAFGW+hCo4x24AIMIYbUjR4NRlnzx1 -MAoGCCqGSM49BAMDA2gAMGUCMQDD0cOUV43GSLH8bbbkw4KD6kVoknm6z6sjE5vk -ZNmgjqduSn6XzCNiiwwhRrsmtVkCMCX8uRVIIG20mdcK2YSwlSgw5mHTJsNFRnfn -x8ft2l8Tk2yZcqFSsvuVKvBwZh7bWA== ------END CERTIFICATE----- diff --git a/server.key b/server.key deleted file mode 100644 index eeff0b6..0000000 --- a/server.key +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN EC PARAMETERS----- -BgUrgQQAIg== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- -MIGkAgEBBDDaWQTho9lbjl27ddVt9pC4req+U8iFhdMw0/RzWePrlF55fvmwRiff -GepKeBDdkx6gBwYFK4EEACKhZANiAAQApL6i92+vD79dZ0FcJv/QTVhrodXwSpam -Gap8q/RstcRELX98HlpcIFZl/a6rvZzRIgG28SLfWhpYh7qypBNtvhYjiguL3bmJ -csiFypQgBV4oByq/5OrioHNoz33mKAs= ------END EC PRIVATE KEY----- diff --git a/src/client/public/index.html b/src/client/public/index.html index 5553197..3a0bc6a 100644 --- a/src/client/public/index.html +++ b/src/client/public/index.html @@ -307,7 +307,7 @@
-

网易云音乐抓包工具 v0.0.1

+

网易云音乐抓包工具 v0.1.0

🔍 简易网易云音乐客户端抓包工具

@@ -489,20 +489,29 @@ const eventSource = new EventSource('/api/events'); eventSource.onmessage = function(event) { - const data = JSON.parse(event.data); - - // 更新状态为已连接 - updateStatus(); - - // 检查是否有新数据 - if (data.length !== capturedData.length) { - capturedData = data; - renderCaptureList(); + try { + const data = JSON.parse(event.data); - // 如果是新数据且当前没有选中,自动选中第一个 - if (selectedIndex === -1 && data.length > 0) { - selectCapture(0); + // 更新状态为已连接 + updateStatus(); + + // 检查是否有新数据(比较最后一个时间戳或数量) + const hasNewData = !capturedData.length || + data.length > capturedData.length || + (data.length > 0 && capturedData.length > 0 && + data[data.length - 1].timestamp !== capturedData[capturedData.length - 1].timestamp); + + if (hasNewData) { + capturedData = data; + renderCaptureList(); + + // 如果是新数据且当前没有选中,自动选中第一个 + if (selectedIndex === -1 && data.length > 0) { + selectCapture(0); + } } + } catch (e) { + console.error('Failed to parse SSE data:', e); } }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..cd6074d --- /dev/null +++ b/src/index.js @@ -0,0 +1,221 @@ +// 主入口文件,同时启动 server 和 client +// 确保 Ctrl+C 时能够优雅地关闭所有服务 + +require('dotenv').config(); + +const http = require('http'); +const https = require('https'); + +// 保存服务器引用以便关闭 +let servers = []; + +// 代理 server.app.js +const startServer = () => { + return new Promise((resolve, reject) => { + const packageJson = require('../package.json'); + const config = require('./server/cli.js') + .program({ + name: packageJson.name.replace(/@.+\//, ''), + version: packageJson.version, + }) + .option(['-v', '--version'], { action: 'version' }) + .option(['-p', '--port'], { + metavar: 'http[:https]', + help: 'specify server port', + }) + .option(['-a', '--address'], { + metavar: 'address', + help: 'specify server host', + }) + .option(['-h', '--help'], { action: 'help' }) + .parse(process.argv); + + global.address = config.address; + config.port = (config.port || process.env.HOOK_PORT || '9000') + .split(':') + .map((string) => parseInt(string)); + const invalid = (value) => isNaN(value) || value < 1 || value > 65535; + if (config.port.some(invalid)) { + console.log('Port must be a number higher than 0 and lower than 65535.'); + process.exit(1); + } + + if (!process.env.PORT) { + process.env.PORT = process.env.PORT || '3000'; + } + + const { logScope } = require('./server/logger'); + const escape = require('querystring').escape; + const hook = require('./server/hook'); + const server = require('./server/server'); + const logger = logScope('app'); + const target = Array.from(hook.target.host); + + global.port = config.port; + global.proxy = null; + global.hosts = {}; + global.endpoint = 'https://music.163.com'; + + server.whitelist = [ + '://[\w.]*music\\.126\\.net', + '://[\w.]*vod\\.126\\.net', + '://acstatic-dun.126.net', + '://[\w.]*\\.netease\\.com', + '://[\w.]*\\.163yun\\.com', + ]; + + if (config.endpoint) server.whitelist.push(escape(config.endpoint)); + + const dns = (host) => + new Promise((resolve, reject) => + require('dns').lookup(host, { all: true }, (error, records) => + error + ? reject(error) + : resolve(records.map((record) => record.address)) + ) + ); + + Promise.all(target.map(dns)) + .then((result) => { + const { host } = hook.target; + result.forEach((array) => array.forEach(host.add, host)); + server.whitelist = server.whitelist.concat( + Array.from(host).map(escape) + ); + const log = (type) => + logger.info( + `${['HTTP', 'HTTPS'][type]} Server running @ http://${ + address || '0.0.0.0' + }:${port[type]}` + ); + if (port[0]) { + const httpServer = server.http + .listen(port[0], address) + .once('listening', () => { + log(0); + servers.push(httpServer); + }); + } + if (port[1]) { + const httpsServer = server.https + .listen(port[1], address) + .once('listening', () => { + log(1); + servers.push(httpsServer); + }); + } + resolve(); + }) + .catch((error) => { + console.log(error); + reject(error); + }); + }); +}; + +// 启动 client +const startClient = () => { + return new Promise((resolve, reject) => { + const express = require('express'); + const path = require('path'); + + const app = express(); + const PORT = process.env.PORT || 3000; + + let capturedData = []; + let clients = []; + + app.use(express.json({ limit: '10mb' })); + app.use(express.urlencoded({ limit: '10mb', extended: true })); + app.use(express.static(path.join(__dirname, 'client/public'))); + + app.post('/api/capture', (req, res) => { + const data = req.body; + capturedData.push(data); + console.log('Captured data:', data.path); + + broadcastData(); + + res.status(200).send('OK'); + }); + + app.get('/api/data', (req, res) => { + res.json(capturedData); + }); + + 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); + }); + }); + + function broadcastData() { + clients.forEach(client => { + try { + client.write(`data: ${JSON.stringify(capturedData)}\n\n`); + } catch (e) { + clients = clients.filter(c => c !== client); + } + }); + } + + const clientServer = app.listen(PORT, () => { + console.log(`Frontend server running at http://localhost:${PORT}`); + servers.push(clientServer); + resolve(); + }); + + clientServer.on('error', reject); + }); +}; + +// 优雅关闭所有服务器 +const gracefulShutdown = async (signal) => { + console.log(`\n收到 ${signal} 信号,正在关闭服务器...`); + + // 关闭所有服务器 + const closePromises = servers.map(server => { + return new Promise((resolve) => { + server.close(() => { + console.log('服务器已关闭'); + resolve(); + }); + // 设置超时,强制关闭 + setTimeout(() => { + resolve(); + }, 5000); + }); + }); + + await Promise.all(closePromises); + console.log('所有服务已关闭'); + process.exit(0); +}; + +// 监听退出信号 +process.on('SIGINT', () => gracefulShutdown('SIGINT')); +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); + +// 启动所有服务 +const startAll = async () => { + try { + await Promise.all([ + startServer(), + startClient() + ]); + console.log('所有服务启动完成!'); + } catch (error) { + console.error('启动服务失败:', error); + process.exit(1); + } +}; + +startAll(); \ No newline at end of file diff --git a/src/server/hook.js b/src/server/hook.js index 1c316f9..e1e0a35 100644 --- a/src/server/hook.js +++ b/src/server/hook.js @@ -271,11 +271,6 @@ hook.request.after = (ctx) => { proxyRes.statusCode === 200 ) proxyRes.statusCode = 206; - if ( - netease && - hook.target.path.has(netease.path) && - proxyRes.statusCode === 200 - ) { return request .read(proxyRes, true) .then((buffer) => @@ -297,24 +292,37 @@ hook.request.after = (ctx) => { netease.jsonBody = JSON.parse(patch(buffer.toString())); } - // Send data to frontend + // Send data to frontend for all captured requests const dataToSend = { timestamp: new Date().toISOString(), path: netease.path, param: netease.param, - response: netease.jsonBody + response: netease.jsonBody, + statusCode: proxyRes.statusCode }; axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend) .catch(err => logger.error('Failed to send data to frontend:', err)); }) - .catch( - (error) => - error && + .catch((error) => { + // 即使读取响应体失败,也发送基本信息到前端 + const dataToSend = { + timestamp: new Date().toISOString(), + path: netease.path, + param: netease.param, + response: null, + statusCode: proxyRes.statusCode, + error: error.message + }; + axios.post(`http://localhost:${process.env.PORT || 3000}/api/capture`, dataToSend) + .catch(err => logger.error('Failed to send data to frontend:', err)); + + if (error) { logger.error( error, `A error occurred in hook.request.after when hooking ${req.url}.` - ) - ); + ); + } + }); } else if (pkg) { if (new Set([201, 301, 302, 303, 307, 308]).has(proxyRes.statusCode)) { return request(