From 1ac4faa8f2ac6e9704d160a0cf5bc3c673eb61f2 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Sat, 30 May 2026 19:13:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=8A=93=E5=8C=85xea?= =?UTF-8?q?pi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 8 +-- pnpm-lock.yaml | 138 +++++++++++++++++++++++++++---------------- src/server/crypto.js | 24 ++++++++ src/server/hook.js | 34 ++++++++++- 4 files changed, 147 insertions(+), 57 deletions(-) diff --git a/package.json b/package.json index 8a22ceb..e336cf5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "api-clawer", - "version": "0.1.1", + "version": "0.2.0", "description": "网易云音乐客户端抓包工具", "main": "src/server/app.js", "scripts": { @@ -16,12 +16,12 @@ "license": "MIT", "packageManager": "pnpm@10.28.1", "dependencies": { - "axios": "^1.14.0", + "axios": "^1.16.1", "concurrently": "^8.2.2", "dotenv": "^16.6.1", - "express": "^4.22.1", + "express": "^4.22.2", "pino": "^6.14.0", "pino-pretty": "^7.6.1", - "ws": "^8.20.0" + "ws": "^8.21.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2e1c85..9633e60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: axios: - specifier: ^1.14.0 - version: 1.14.0 + specifier: ^1.16.1 + version: 1.16.1 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -18,8 +18,8 @@ importers: specifier: ^16.6.1 version: 16.6.1 express: - specifier: ^4.22.1 - version: 4.22.1 + specifier: ^4.22.2 + version: 4.22.2 pino: specifier: ^6.14.0 version: 6.14.0 @@ -27,19 +27,23 @@ importers: specifier: ^7.6.1 version: 7.6.1 ws: - specifier: ^8.20.0 - version: 8.20.0 + specifier: ^8.21.0 + version: 8.21.0 packages: - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -66,11 +70,11 @@ packages: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - axios@1.14.0: - resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} bytes@3.1.2: @@ -156,6 +160,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -200,8 +213,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: @@ -223,8 +236,8 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} engines: {node: '>= 0.10.0'} fast-redact@3.5.0: @@ -241,8 +254,8 @@ packages: flatstr@1.0.12: resolution: {integrity: sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -297,14 +310,18 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -422,8 +439,8 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} quick-format-unescaped@4.0.4: @@ -471,12 +488,12 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} side-channel-map@1.0.1: @@ -575,8 +592,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -601,13 +618,19 @@ packages: snapshots: - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ansi-regex@5.0.1: {} ansi-styles@3.2.1: @@ -631,15 +654,17 @@ snapshots: atomic-sleep@1.0.0: {} - axios@1.14.0: + axios@1.16.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 form-data: 4.0.5 + https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color - body-parser@1.20.4: + body-parser@1.20.5: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -649,7 +674,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.2 + qs: 6.15.2 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -711,7 +736,7 @@ snapshots: date-fns: 2.30.0 lodash: 4.18.1 rxjs: 7.8.2 - shell-quote: 1.8.3 + shell-quote: 1.8.4 spawn-command: 0.0.2 supports-color: 8.1.1 tree-kill: 1.2.2 @@ -729,7 +754,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 dateformat@4.6.3: {} @@ -737,6 +762,10 @@ snapshots: dependencies: ms: 2.0.0 + debug@4.4.3: + dependencies: + ms: 2.1.3 + delayed-stream@1.0.0: {} depd@2.0.0: {} @@ -772,7 +801,7 @@ snapshots: es-errors@1.3.0: {} - es-object-atoms@1.1.1: + es-object-atoms@1.1.2: dependencies: es-errors: 1.3.0 @@ -781,7 +810,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.4 escalade@3.2.0: {} @@ -791,11 +820,11 @@ snapshots: etag@1.8.1: {} - express@4.22.1: + express@4.22.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4 + body-parser: 1.20.5 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 @@ -814,7 +843,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.13 proxy-addr: 2.0.7 - qs: 6.14.2 + qs: 6.15.2 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.2 @@ -845,14 +874,14 @@ snapshots: flatstr@1.0.12: {} - follow-redirects@1.15.11: {} + follow-redirects@1.16.0: {} form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.4 mime-types: 2.1.35 forwarded@0.2.0: {} @@ -868,18 +897,18 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 gopd@1.2.0: {} @@ -893,7 +922,7 @@ snapshots: dependencies: has-symbols: 1.1.0 - hasown@2.0.2: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -905,6 +934,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -1008,7 +1044,7 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - qs@6.14.2: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -1072,9 +1108,9 @@ snapshots: setprototypeof@1.2.0: {} - shell-quote@1.8.3: {} + shell-quote@1.8.4: {} - side-channel-list@1.0.0: + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -1098,7 +1134,7 @@ snapshots: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 - side-channel-list: 1.0.0 + side-channel-list: 1.0.1 side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 @@ -1174,7 +1210,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.0: {} + ws@8.21.0: {} y18n@5.0.8: {} diff --git a/src/server/crypto.js b/src/server/crypto.js index 2116ca0..9045470 100644 --- a/src/server/crypto.js +++ b/src/server/crypto.js @@ -6,6 +6,7 @@ const bodyify = require('querystring').stringify; const eapiKey = 'e82ckenh8dichen8'; const linuxapiKey = 'rFgB&h#%2?^eDg:Q'; +const xeapiKey = '723f08a8d77c4a3698a9722b71b3607b'; const decrypt = (buffer, key) => { const decipher = crypto.createDecipheriv('aes-128-ecb', key, null); @@ -41,6 +42,29 @@ module.exports = { }; }, }, + xeapi: { + encrypt: (buffer) => encrypt(buffer, xeapiKey), + decrypt: (buffer) => decrypt(buffer, xeapiKey), + encryptRequest: (url, object) => { + url = parse(url); + const text = JSON.stringify(object); + const message = `nobody${url.path}use${text}md5forencrypt`; + const digest = crypto + .createHash('md5') + .update(message) + .digest('hex'); + const data = `${url.path}-36cd479b6b5-${text}-36cd479b6b5-${digest}`; + return { + url: url.href.replace(/\w*api/, 'xeapi'), + body: bodyify({ + params: module.exports.xeapi + .encrypt(Buffer.from(data)) + .toString('hex') + .toUpperCase(), + }), + }; + }, + }, api: { encryptRequest: (url, object) => { url = parse(url); diff --git a/src/server/hook.js b/src/server/hook.js index 06f3de9..c575237 100644 --- a/src/server/hook.js +++ b/src/server/hook.js @@ -133,6 +133,7 @@ hook.request.before = (ctx) => { ) && req.method === 'POST' && (url.path.startsWith('/eapi/') || // eapi + url.path.startsWith('/xeapi/') || // xeapi url.path.startsWith('/api/linux/forward')) // linuxapi ) { return request @@ -157,6 +158,8 @@ hook.request.before = (ctx) => { netease.crypto = 'linuxapi'; } else if (url.path.startsWith('/eapi/')) { netease.crypto = 'eapi'; + } else if (url.path.startsWith('/xeapi/')) { + netease.crypto = 'xeapi'; } else if (url.path.startsWith('/api/')) { netease.crypto = 'api'; } @@ -205,6 +208,32 @@ hook.request.before = (ctx) => { netease.e_r = false; } break; + case 'xeapi': + data = crypto.xeapi + .decrypt( + Buffer.from( + body.slice( + 7, + body.length - netease.pad.length + ), + 'hex' + ) + ) + .toString() + .split('-36cd479b6b5-'); + netease.path = data[0]; + netease.param = JSON.parse(data[1]); + if ( + netease.param.hasOwnProperty('e_r') && + (netease.param.e_r == 'true' || + netease.param.e_r == true) + ) { + // eapi's e_r is true, needs to be encrypted + netease.e_r = true; + } else { + netease.e_r = false; + } + break; case 'api': data = {}; decodeURIComponent(body) @@ -285,9 +314,10 @@ hook.request.after = (ctx) => { ); // for js precision if (netease.e_r) { - // eapi's e_r is true, needs to be encrypted + // e_r is true, response body is encrypted + const decryptCrypto = netease.crypto === 'xeapi' ? crypto.xeapi : crypto.eapi; netease.jsonBody = JSON.parse( - patch(crypto.eapi.decrypt(buffer).toString()) + patch(decryptCrypto.decrypt(buffer).toString()) ); } else { netease.jsonBody = JSON.parse(patch(buffer.toString()));