From 50cc26f29714acb106ea6ad50dd08eb27374a804 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Sat, 6 Jun 2026 14:52:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BAxeapi=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3=E4=BB=A5=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AF=86=E9=92=A5=E8=8E=B7=E5=8F=96=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.MD | 4 +- module/register_xeapikey.js | 74 +++++++++++++++++++++++++++++++++++++ util/config.json | 1 + util/request.js | 2 +- util/xeapiKey.js | 60 ++++-------------------------- 5 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 module/register_xeapikey.js diff --git a/README.MD b/README.MD index fc7d8e4..d95c7d3 100644 --- a/README.MD +++ b/README.MD @@ -30,7 +30,7 @@ ## 项目简介 -网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署(如果原版诈尸, 我会及时同步 or 归档)。 +网易云音乐第三方 Node.js API, 支持丰富的音乐相关接口,适合自建服务、二次开发和多平台部署 > [!IMPORTANT] > @@ -215,7 +215,7 @@ pnpm test 原作者 [Binaryify/NeteaseCloudMusicApi](https://github.com/binaryify/NeteaseCloudMusicApi) 项目为本项目基础 (该项目在`npmjs`网站上仍持续维护, 但 github 仓库已不再更新) -感谢大佬们为逆向eapi, weapi等加密算法所做的贡献 +感谢大佬们为逆向eapi, weapi, xeapi等加密算法所做的贡献 项目参考: diff --git a/module/register_xeapikey.js b/module/register_xeapikey.js new file mode 100644 index 0000000..9372f35 --- /dev/null +++ b/module/register_xeapikey.js @@ -0,0 +1,74 @@ +const { default: axios } = require('axios') +const encrypt = require('../util/crypto') +const { APP_CONF } = require('../util/config.json') + +const generateNonce = () => { + let nonce = '' + for (let i = 0; i < 16; i++) { + nonce += Math.floor(Math.random() * 10).toString() + } + return nonce +} + +module.exports = async (query, request) => { + const nonce = generateNonce() + const timestamp = String(Date.now()) + const deviceId = query.deviceId || global.deviceId || '' + const currentKeyVersion = query.currentKeyVersion || '' + + const data = { + appVersion: '9.1.65', + currentKeyVersion, + deviceId, + nonce, + os: 'android', + requestType: 'active', + signature: encrypt.xeapiSign(timestamp, nonce), + t1: '', + t2: '', + timestamp, + uid: '', + } + + const res = await axios({ + method: 'POST', + url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get', + headers: { + 'User-Agent': + 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', + Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '', + }, + data: new URLSearchParams(data).toString(), + proxy: false, + }) + + if ( + !res.data || + res.data.code !== 200 || + !res.data.data || + !res.data.data.encryptedData + ) { + throw new Error('xeapi public key request failed') + } + if ( + !res.data.data.signature || + encrypt.xeapiSign(res.data.data.timestamp, nonce) !== + res.data.data.signature + ) { + throw new Error('xeapi public key response signature mismatch') + } + + const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData) + if (!publicKey.sk) { + throw new Error('xeapi public key response missing sk') + } + + return { + status: 200, + body: { + ...publicKey, + deviceId, + }, + cookie: [], + } +} diff --git a/util/config.json b/util/config.json index e9a57f0..189e5fb 100644 --- a/util/config.json +++ b/util/config.json @@ -11,6 +11,7 @@ }, "APP_CONF": { "apiDomain": "https://interface.music.163.com", + "xeapiDomain": "https://interface3.music.163.com", "domain": "https://music.163.com", "encrypt": true, "encryptResponse": false, diff --git a/util/request.js b/util/request.js index c697fea..1b12d2e 100644 --- a/util/request.js +++ b/util/request.js @@ -110,7 +110,7 @@ const userAgentMap = { // 预先定义常量 const DOMAIN = APP_CONF.domain const API_DOMAIN = APP_CONF.apiDomain -const XEAPI_DOMAIN = 'https://interface3.music.163.com' +const XEAPI_DOMAIN = APP_CONF.xeapiDomain const ENCRYPT_RESPONSE = APP_CONF.encryptResponse const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803]) diff --git a/util/xeapiKey.js b/util/xeapiKey.js index ba6ced4..d308df5 100644 --- a/util/xeapiKey.js +++ b/util/xeapiKey.js @@ -1,59 +1,15 @@ -const { default: axios } = require('axios') -const encrypt = require('./crypto') -const { APP_CONF } = require('./config.json') - -const generateNonce = () => { - let nonce = '' - for (let i = 0; i < 16; i++) { - nonce += Math.floor(Math.random() * 10).toString() - } - return nonce -} +const registerXeapiKey = require('../module/register_xeapikey') const getXeapiPublicKey = async (currentPublicKey = {}, deviceId = '') => { - const nonce = generateNonce() - const timestamp = String(Date.now()) - const data = { - appVersion: '9.1.65', - currentKeyVersion: currentPublicKey.version || '', - deviceId, - nonce, - os: 'android', - requestType: 'active', - signature: encrypt.xeapiSign(timestamp, nonce), - t1: '', - t2: '', - timestamp, - uid: '', - } - const res = await axios({ - method: 'POST', - url: APP_CONF.apiDomain + '/api/gorilla/anti/crawler/security/key/get', - headers: { - 'User-Agent': - 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', - Cookie: deviceId ? `deviceId=${encodeURIComponent(deviceId)}` : '', + const result = await registerXeapiKey( + { + deviceId, + currentKeyVersion: currentPublicKey.version || '', }, - data: new URLSearchParams(data).toString(), - proxy: false, - }) - if ( - !res.data || - res.data.code !== 200 || - !res.data.data || - !res.data.data.encryptedData - ) { - throw new Error('xeapi public key request failed') - } - if ( - !res.data.data.signature || - encrypt.xeapiSign(res.data.data.timestamp, nonce) !== - res.data.data.signature - ) { - throw new Error('xeapi public key response signature mismatch') - } + null, + ) - const publicKey = encrypt.xeapiDecryptPublicKey(res.data.data.encryptedData) + const publicKey = result.body if (!publicKey.sk && currentPublicKey.sk) { publicKey.sk = currentPublicKey.sk }