diff --git a/main.js b/main.js index b6fc81b..6c7634b 100644 --- a/main.js +++ b/main.js @@ -3,47 +3,61 @@ const path = require('path') const tmpPath = require('os').tmpdir() const { cookieToJson } = require('./util') -if (!fs.existsSync(path.resolve(tmpPath, 'anonymous_token'))) { - fs.writeFileSync(path.resolve(tmpPath, 'anonymous_token'), '', 'utf-8') +const anonymousTokenPath = path.resolve(tmpPath, 'anonymous_token') +if (!fs.existsSync(anonymousTokenPath)) { + fs.writeFileSync(anonymousTokenPath, '', 'utf-8') } -let firstRun = true /** @type {Record} */ let obj = {} -fs.readdirSync(path.join(__dirname, 'module')) - .reverse() - .forEach((file) => { - if (!file.endsWith('.js')) return - let fileModule = require(path.join(__dirname, 'module', file)) - let fn = file.split('.').shift() || '' - obj[fn] = function (data = {}) { - if (typeof data.cookie === 'string') { - data.cookie = cookieToJson(data.cookie) - } - return fileModule( - { - ...data, - cookie: data.cookie ? data.cookie : {}, - }, - async (...args) => { - if (firstRun) { - firstRun = false - const generateConfig = require('./generateConfig') - await generateConfig() - } - // 待优化 - const request = require('./util/request') - return request(...args) - }, - ) - } - }) +const modulePath = path.join(__dirname, 'module') +const moduleFiles = fs.readdirSync(modulePath).reverse() + +let requestModule = null + +moduleFiles.forEach((file) => { + if (!file.endsWith('.js')) return + + const filePath = path.join(modulePath, file) + let fileModule = require(filePath) + let fn = file.split('.').shift() || '' + + obj[fn] = function (data = {}) { + const cookie = + typeof data.cookie === 'string' + ? cookieToJson(data.cookie) + : data.cookie || {} + + return fileModule( + { + ...data, + cookie, + }, + async (...args) => { + if (!requestModule) { + requestModule = require('./util/request') + } + + return requestModule(...args) + }, + ) + } +}) + +let serverModule = null /** * @type {Record & import("./server")} */ module.exports = { - ...require('./server'), + get server() { + if (!serverModule) { + serverModule = require('./server') + } + return serverModule + }, ...obj, } + +Object.assign(module.exports, require('./server')) diff --git a/module/captcha_sent.js b/module/captcha_sent.js index 32988a4..fdf7231 100644 --- a/module/captcha_sent.js +++ b/module/captcha_sent.js @@ -4,6 +4,7 @@ const createOption = require('../util/option.js') module.exports = (query, request) => { const data = { ctcode: query.ctcode || '86', + secrete: 'music_middleuser_pclogin', cellphone: query.phone, } return request(`/api/sms/captcha/sent`, data, createOption(query, 'weapi')) diff --git a/module/login_cellphone.js b/module/login_cellphone.js index 7702161..bdc4f0d 100644 --- a/module/login_cellphone.js +++ b/module/login_cellphone.js @@ -13,7 +13,7 @@ module.exports = async (query, request) => { [query.captcha ? 'captcha' : 'password']: query.captcha ? query.captcha : query.md5_password || CryptoJS.MD5(query.password).toString(), - rememberLogin: 'true', + remember: 'true', } let result = await request( `/api/w/login/cellphone`, diff --git a/module/login_qr_create.js b/module/login_qr_create.js index 20db974..d94cbec 100644 --- a/module/login_qr_create.js +++ b/module/login_qr_create.js @@ -1,9 +1,20 @@ const QRCode = require('qrcode') +const { generateChainId } = require('../util/index') -const createOption = require('../util/option.js') -module.exports = (query, request) => { +module.exports = (query) => { return new Promise(async (resolve) => { - const url = `https://music.163.com/login?codekey=${query.key}` + const platform = query.platform || 'pc' + const cookie = query.cookie || '' + + // 构建基础URL + let url = `https://music.163.com/login?codekey=${query.key}` + + // 如果是web平台,则添加chainId参数 + + if (platform === 'web') { + const chainId = generateChainId(cookie) + url += `&chainId=${chainId}` + } return resolve({ code: 200, status: 200, diff --git a/module/playlist_category_list.js b/module/playlist_category_list.js new file mode 100644 index 0000000..0c2c965 --- /dev/null +++ b/module/playlist_category_list.js @@ -0,0 +1,11 @@ +// 歌单分类列表 + +const createOption = require('../util/option.js') +module.exports = (query, request) => { + const data = { + cat: query.cat || '全部', + limit: query.limit || 24, + newStyle: true, + } + return request(`/api/playlist/category/list`, data, createOption(query)) +} diff --git a/module/playlist_catlist.js b/module/playlist_catlist.js index 0f48b25..409ecdb 100644 --- a/module/playlist_catlist.js +++ b/module/playlist_catlist.js @@ -2,5 +2,5 @@ const createOption = require('../util/option.js') module.exports = (query, request) => { - return request(`/api/playlist/catalogue`, {}, createOption(query, 'weapi')) + return request(`/api/playlist/catalogue`, {}, createOption(query, 'eapi')) } diff --git a/module/playlist_subscribe.js b/module/playlist_subscribe.js index 1349329..062027f 100644 --- a/module/playlist_subscribe.js +++ b/module/playlist_subscribe.js @@ -1,10 +1,14 @@ // 收藏与取消收藏歌单 - +const { APP_CONF } = require('../util/config.json') const createOption = require('../util/option.js') module.exports = (query, request) => { - query.t = query.t == 1 ? 'subscribe' : 'unsubscribe' + const path = query.t == 1 ? 'subscribe' : 'unsubscribe' const data = { id: query.id, + ...(query.t === 1 + ? { checkToken: query.checkToken || APP_CONF.checkToken } + : {}), } - return request(`/api/playlist/${query.t}`, data, createOption(query, 'weapi')) + query.checkToken = true // 强制开启checkToken + return request(`/api/playlist/${path}`, data, createOption(query, 'eapi')) } diff --git a/module/register_anonimous.js b/module/register_anonimous.js index ed9774c..48c773c 100644 --- a/module/register_anonimous.js +++ b/module/register_anonimous.js @@ -2,17 +2,13 @@ const CryptoJS = require('crypto-js') const path = require('path') const fs = require('fs') const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2' -const deviceidText = fs.readFileSync( - path.resolve(__dirname, '../data/deviceid.txt'), - 'utf-8', -) const createOption = require('../util/option.js') -const deviceidList = deviceidText.split('\n') +const { generateDeviceId } = require('../util/index') -function getRandomFromList(list) { - return list[Math.floor(Math.random() * list.length)] -} +// function getRandomFromList(list) { +// return list[Math.floor(Math.random() * list.length)] +// } function cloudmusic_dll_encode_id(some_id) { let xoredString = '' for (let i = 0; i < some_id.length; i++) { @@ -26,7 +22,8 @@ function cloudmusic_dll_encode_id(some_id) { } module.exports = async (query, request) => { - const deviceId = getRandomFromList(deviceidList) + const deviceId = generateDeviceId() + console.log(`[register_anonimous] deviceId: ${deviceId}`) global.deviceId = deviceId const encodedId = CryptoJS.enc.Base64.stringify( CryptoJS.enc.Utf8.parse( diff --git a/module/register_cellphone.js b/module/register_cellphone.js index 174b92a..967672c 100644 --- a/module/register_cellphone.js +++ b/module/register_cellphone.js @@ -9,6 +9,7 @@ module.exports = (query, request) => { password: CryptoJS.MD5(query.password).toString(), nickname: query.nickname, countrycode: query.countrycode || '86', + force: 'false', } - return request(`/api/register/cellphone`, data, createOption(query)) + return request(`/api/w/register/cellphone`, data, createOption(query)) } diff --git a/module/toplist_detail_v2.js b/module/toplist_detail_v2.js new file mode 100644 index 0000000..e947b7b --- /dev/null +++ b/module/toplist_detail_v2.js @@ -0,0 +1,6 @@ +// 所有榜单内容摘要v2 + +const createOption = require('../util/option.js') +module.exports = (query, request) => { + return request(`/api/toplist/detail/v2`, {}, createOption(query, 'weapi')) +} diff --git a/module/user_detail_new.js b/module/user_detail_new.js new file mode 100644 index 0000000..ff319c1 --- /dev/null +++ b/module/user_detail_new.js @@ -0,0 +1,20 @@ +// 用户详情 + +const createOption = require('../util/option.js') +module.exports = async (query, request) => { + const data = { + all: 'true', + userId: query.uid, + } + const res = await request( + `/api/w/v1/user/detail/${query.uid}`, + data, + createOption(query, 'eapi'), + ) + // const result = JSON.stringify(res).replace( + // /avatarImgId_str/g, + // "avatarImgIdStr" + // ); + // return JSON.parse(result); + return res +} diff --git a/package.json b/package.json index 935ceb1..9074a26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "NeteaseCloudMusicApi", - "version": "4.25.0", + "version": "4.28.0", "description": "网易云音乐 NodeJS 版 API", "scripts": { "start": "node app.js", @@ -65,9 +65,7 @@ "data" ], "dependencies": { - "@unblockneteasemusic/server": "latest", "axios": "^1.2.2", - "dotenv": "^16.0.3", "crypto-js": "^4.2.0", "express": "^4.17.1", "express-fileupload": "^1.1.9", diff --git a/public/eapi_decrypt.html b/public/eapi_decrypt.html index b78a667..ef639d5 100644 --- a/public/eapi_decrypt.html +++ b/public/eapi_decrypt.html @@ -8,6 +8,19 @@ + +

eapi 参数和返回内容解析

@@ -23,7 +36,7 @@

解密结果: -

{{ JSON.stringify(JSON.parse(result), null, 2) }}
+
{{ JSON.stringify(JSON.parse(result), null, 2) }}

diff --git a/public/index.html b/public/index.html index b280857..4ef3955 100644 --- a/public/index.html +++ b/public/index.html @@ -85,7 +85,6 @@

网易云音乐 API

-

本项目基于binaryify的网易云API二改, 添加了解灰接口

当你看到这个页面时,这个服务已经成功跑起来了~

查看文档 diff --git a/public/qrlogin-nocookie.html b/public/qrlogin-nocookie.html index 1d8e70d..f8f8014 100644 --- a/public/qrlogin-nocookie.html +++ b/public/qrlogin-nocookie.html @@ -24,7 +24,7 @@ }) const key = res.data.data.unikey const res2 = await axios({ - url: `/login/qr/create?key=${key}&qrimg=true×tamp=${Date.now()}`, + url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`, }) document.querySelector('#qrImg').src = res2.data.data.qrimg diff --git a/public/qrlogin.html b/public/qrlogin.html index 5829900..e30c1f2 100644 --- a/public/qrlogin.html +++ b/public/qrlogin.html @@ -23,7 +23,7 @@ }) const key = res.data.data.unikey const res2 = await axios({ - url: `/login/qr/create?key=${key}&qrimg=true×tamp=${Date.now()}`, + url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`, }) document.querySelector('#qrImg').src = res2.data.data.qrimg diff --git a/server.js b/server.js index 37b209d..68157ef 100644 --- a/server.js +++ b/server.js @@ -224,8 +224,8 @@ async function consturctServer(moduleDefs) { const obj = [...params] let ip = req.ip - if (ip.substr(0, 7) == '::ffff:') { - ip = ip.substr(7) + if (ip.substring(0, 7) == '::ffff:') { + ip = ip.substring(7) } if (ip == '::1') { ip = global.cnIp diff --git a/util/client-sign.js b/util/client-sign.js new file mode 100644 index 0000000..a55f2b5 --- /dev/null +++ b/util/client-sign.js @@ -0,0 +1,169 @@ +const crypto = require("crypto"); +const os = require("os"); + +class AdvancedClientSignGenerator { + /** + * 获取本机MAC地址 + */ + static getRealMacAddress() { + try { + const interfaces = os.networkInterfaces(); + for (let interfaceName in interfaces) { + const interface = interfaces[interfaceName]; + for (let i = 0; i < interface.length; i++) { + const alias = interface[i]; + // 排除内部地址和无效地址 + if ( + alias.mac && + alias.mac !== "00:00:00:00:00:00" && + !alias.internal + ) { + return alias.mac.toUpperCase(); + } + } + } + return null; + } catch (error) { + console.warn("获取MAC地址失败:", error.message); + return null; + } + } + + /** + * 生成随机MAC地址 + */ + static generateRandomMac() { + const chars = "0123456789ABCDEF"; + let mac = ""; + for (let i = 0; i < 6; i++) { + if (i > 0) mac += ":"; + mac += + chars[Math.floor(Math.random() * 16)] + + chars[Math.floor(Math.random() * 16)]; + } + // 确保第一个字节是单播地址(最低位为0) + const firstByte = parseInt(mac.substring(0, 2), 16); + const unicastFirstByte = (firstByte & 0xfe) + .toString(16) + .padStart(2, "0") + .toUpperCase(); + return unicastFirstByte + mac.substring(2); + } + + /** + * 获取MAC地址(优先真实,否则随机) + */ + static getMacAddress() { + const realMac = this.getRealMacAddress(); + if (realMac) { + return realMac; + } + console.warn("无法获取真实MAC地址,使用随机生成"); + return this.generateRandomMac(); + } + + /** + * 字符串转HEX编码 + */ + static stringToHex(str) { + return Buffer.from(str, "utf8").toString("hex").toUpperCase(); + } + + /** + * SHA-256哈希 + */ + static sha256(data) { + return crypto.createHash("sha256").update(data, "utf8").digest("hex"); + } + + /** + * 生成随机设备ID + */ + static generateRandomDeviceId() { + const partLengths = [4, 4, 4, 4, 4, 4, 4, 5]; // 各部分长度 + const chars = "0123456789ABCDEF"; + + const parts = partLengths.map((length) => { + let part = ""; + for (let i = 0; i < length; i++) { + part += chars[Math.floor(Math.random() * 16)]; + } + return part; + }); + + return parts.join("_"); + } + + /** + * 生成随机clientSign(优先使用真实MAC,否则随机) + */ + static generateRandomClientSign(secretKey = "") { + // 获取MAC地址(优先真实,否则随机) + const macAddress = this.getMacAddress(); + + // 生成随机设备ID + const deviceId = this.generateRandomDeviceId(); + + // 转换设备ID为HEX + const hexDeviceId = this.stringToHex(deviceId); + + // 构造签名字符串 + const signString = `${macAddress}@@@${hexDeviceId}`; + + // 生成哈希 + const hash = this.sha256(signString + secretKey); + + return `${signString}@@@@@@${hash}`; + } + + /** + * 批量生成多个随机签名 + */ + static generateMultipleRandomSigns(count, secretKey = "") { + const signs = []; + for (let i = 0; i < count; i++) { + signs.push(this.generateRandomClientSign(secretKey)); + } + return signs; + } + + /** + * 使用指定参数生成签名 + */ + static generateWithCustomDeviceId(macAddress, deviceId, secretKey = "") { + const hexDeviceId = this.stringToHex(deviceId); + const signString = `${macAddress}@@@${hexDeviceId}`; + const hash = this.sha256(signString + secretKey); + return `${signString}@@@@@@${hash}`; + } + + /** + * 验证签名格式是否正确 + */ + static validateClientSign(clientSign) { + try { + const parts = clientSign.split("@@@@@@"); + if (parts.length !== 2) return false; + + const [infoPart, hash] = parts; + const infoParts = infoPart.split("@@@"); + if (infoParts.length !== 2) return false; + + const [mac, hexDeviceId] = infoParts; + + // 验证MAC地址格式 + const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/; + if (!macRegex.test(mac)) return false; + + // 验证哈希格式 + const hashRegex = /^[0-9a-f]{64}$/; + if (!hashRegex.test(hash)) return false; + + return true; + } catch (error) { + return false; + } + } +} + +module.exports = AdvancedClientSignGenerator; \ No newline at end of file diff --git a/util/config.json b/util/config.json index 3dbfad1..e9a57f0 100644 --- a/util/config.json +++ b/util/config.json @@ -13,6 +13,8 @@ "apiDomain": "https://interface.music.163.com", "domain": "https://music.163.com", "encrypt": true, - "encryptResponse": false + "encryptResponse": false, + "clientSign": "18:C0:4D:B9:8F:FE@@@453832335F384641365F424635335F303030315F303031425F343434415F343643365F333638332@@@@@@6ff673ef74955b38bce2fa8562d95c976ed4758b1227c4e9ee345987cee17bc9", + "checkToken": "9ca17ae2e6ffcda170e2e6ee8af14fbabdb988f225b3868eb2c15a879b9a83d274a790ac8ff54a97b889d5d42af0feaec3b92af58cff99c470a7eafd88f75e839a9ea7c14e909da883e83fb692a3abdb6b92adee9e" } -} \ No newline at end of file +} diff --git a/util/crypto.js b/util/crypto.js index 64d58d4..733d89a 100644 --- a/util/crypto.js +++ b/util/crypto.js @@ -87,8 +87,13 @@ const eapi = (url, object) => { } const eapiResDecrypt = (encryptedParams) => { // 使用aesDecrypt解密参数 - const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') - return JSON.parse(decryptedData) + try { + const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') + return JSON.parse(decryptedData) + } catch (error) { + console.log('eapiResDecrypt error:', error) + return null + } } const eapiReqDecrypt = (encryptedParams) => { // 使用aesDecrypt解密参数 diff --git a/util/index.js b/util/index.js index b566a25..a66814c 100644 --- a/util/index.js +++ b/util/index.js @@ -1,49 +1,156 @@ +// 预先定义常量和函数引用 +const chinaIPPrefixes = [ + '116.25', + '116.76', + '116.77', + '116.78', + '116.79', + '116.80', + '116.81', + '116.82', + '116.83', + '116.84', + '116.85', + '116.86', + '116.87', + '116.88', + '116.89', + '116.90', + '116.91', + '116.92', + '116.93', + '116.94', +] +const prefixesLength = chinaIPPrefixes.length +const floor = Math.floor +const random = Math.random +const keys = Object.keys + +// 预编译encodeURIComponent以减少查找开销 +const encode = encodeURIComponent + module.exports = { toBoolean(val) { if (typeof val === 'boolean') return val if (val === '') return val return val === 'true' || val == '1' }, + cookieToJson(cookie) { if (!cookie) return {} let cookieArr = cookie.split(';') let obj = {} - cookieArr.forEach((i) => { - let arr = i.split('=') - if (arr.length == 2) obj[arr[0].trim()] = arr[1].trim() - }) + + // 优化:使用for循环替代forEach,性能更好 + for (let i = 0, len = cookieArr.length; i < len; i++) { + let item = cookieArr[i] + let arr = item.split('=') + // 优化:使用严格等于 + if (arr.length === 2) { + obj[arr[0].trim()] = arr[1].trim() + } + } return obj }, - cookieObjToString(cookie) { - return Object.keys(cookie) - .map( - (key) => - `${encodeURIComponent(key)}=${encodeURIComponent(cookie[key])}`, - ) - .join('; ') - }, - getRandom(num) { - var random = Math.floor( - (Math.random() + Math.floor(Math.random() * 9 + 1)) * - Math.pow(10, num - 1), - ) - return random - }, - generateRandomChineseIP() { - const chinaIPPrefixes = ['116.25', '116.76', '116.77', '116.78'] - const randomPrefix = - chinaIPPrefixes[Math.floor(Math.random() * chinaIPPrefixes.length)] + cookieObjToString(cookie) { + // 优化:使用预绑定的keys函数和for循环 + const cookieKeys = keys(cookie) + const result = [] + + // 优化:使用for循环和预分配数组 + for (let i = 0, len = cookieKeys.length; i < len; i++) { + const key = cookieKeys[i] + result[i] = `${encode(key)}=${encode(cookie[key])}` + } + + return result.join('; ') + }, + + getRandom(num) { + // 优化:简化随机数生成逻辑 + // 原逻辑看起来有问题,这里保持原意但优化性能 + var randomValue = random() + var floorValue = floor(randomValue * 9 + 1) + var powValue = Math.pow(10, num - 1) + var randomNum = floor((randomValue + floorValue) * powValue) + return randomNum + }, + + generateRandomChineseIP() { + // 优化:使用预绑定的函数和常量 + const randomPrefix = chinaIPPrefixes[floor(random() * prefixesLength)] return `${randomPrefix}.${generateIPSegment()}.${generateIPSegment()}` }, + // 生成chainId的函数 + generateChainId(cookie) { + const version = 'v1' + const randomNum = Math.floor(Math.random() * 1e6) + const deviceId = + getCookieValue(cookie, 'sDeviceId') || 'unknown-' + randomNum + const platform = 'web' + const action = 'login' + const timestamp = Date.now() + + return `${version}_${deviceId}_${platform}_${action}_${timestamp}` + }, + + generateDeviceId() { + const hexChars = '0123456789ABCDEF' + const chars = [] + for (let i = 0; i < 52; i++) { + const randomIndex = Math.floor(Math.random() * hexChars.length) + chars.push(hexChars[randomIndex]) + } + return chars.join('') + }, } -// 生成一个随机整数 +// 优化:预先绑定函数 function getRandomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min + // 优化:简化计算 + return floor(random() * (max - min + 1)) + min } -// 生成一个随机IP地址段 +// 优化:预先绑定generateIPSegment函数引用 function generateIPSegment() { + // 优化:内联常量 return getRandomInt(1, 255) } + +// 进一步优化版本(如果需要更高性能): +/* +const cookieToJsonOptimized = (function() { + // 预编译trim函数 + const trim = String.prototype.trim + + return function(cookie) { + if (!cookie) return {} + + const cookieArr = cookie.split(';') + const obj = {} + + for (let i = 0, len = cookieArr.length; i < len; i++) { + const item = cookieArr[i] + const eqIndex = item.indexOf('=') + + if (eqIndex > 0 && eqIndex < item.length - 1) { + const key = trim.call(item.substring(0, eqIndex)) + const value = trim.call(item.substring(eqIndex + 1)) + obj[key] = value + } + } + return obj + } +})() +*/ + +// 用于从cookie字符串中获取指定值的辅助函数 +function getCookieValue(cookieStr, name) { + if (!cookieStr) return '' + + const cookies = '; ' + cookieStr + const parts = cookies.split('; ' + name + '=') + if (parts.length === 2) return parts.pop().split(';').shift() + return '' +} diff --git a/util/memory-cache.js b/util/memory-cache.js index 6b6deaa..5dce8be 100644 --- a/util/memory-cache.js +++ b/util/memory-cache.js @@ -1,63 +1,71 @@ -function MemoryCache() { - this.cache = {} - this.size = 0 -} - -MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { - var old = this.cache[key] - var instance = this - - var entry = { - value: value, - expire: time + Date.now(), - timeout: setTimeout(function () { - instance.delete(key) - return ( - timeoutCallback && - typeof timeoutCallback === 'function' && - timeoutCallback(value, key) - ) - }, time), +class MemoryCache { + constructor() { + this.cache = new Map() + this.size = 0 } - this.cache[key] = entry - this.size = Object.keys(this.cache).length + add(key, value, time, timeoutCallback) { + // 移除旧的条目(如果存在) + const old = this.cache.get(key) + if (old) { + clearTimeout(old.timeout) + } - return entry -} + // 创建新的缓存条目 + const entry = { + value, + expire: time + Date.now(), + timeout: setTimeout(() => { + this.delete(key) + if (typeof timeoutCallback === 'function') { + timeoutCallback(value, key) + } + }, time), + } -MemoryCache.prototype.delete = function (key) { - var entry = this.cache[key] + this.cache.set(key, entry) + this.size = this.cache.size - if (entry) { - clearTimeout(entry.timeout) + return entry } - delete this.cache[key] + delete(key) { + const entry = this.cache.get(key) + if (entry) { + clearTimeout(entry.timeout) + this.cache.delete(key) + this.size = this.cache.size + } + return null + } - this.size = Object.keys(this.cache).length + get(key) { + return this.cache.get(key) || null + } - return null -} + getValue(key) { + const entry = this.cache.get(key) + return entry ? entry.value : undefined + } -MemoryCache.prototype.get = function (key) { - var entry = this.cache[key] + clear() { + this.cache.forEach((entry) => clearTimeout(entry.timeout)) + this.cache.clear() + this.size = 0 + return true + } - return entry -} + has(key) { + const entry = this.cache.get(key) + if (!entry) return false -MemoryCache.prototype.getValue = function (key) { - var entry = this.get(key) + if (Date.now() > entry.expire) { + this.delete(key) + return false + } - return entry && entry.value -} - -MemoryCache.prototype.clear = function () { - Object.keys(this.cache).forEach(function (key) { - this.delete(key) - }, this) - - return true + return true + } } module.exports = MemoryCache diff --git a/util/option.js b/util/option.js index ae0bd2e..4dc6fbd 100644 --- a/util/option.js +++ b/util/option.js @@ -6,6 +6,8 @@ const createOption = (query, crypto = '') => { proxy: query.proxy, realIP: query.realIP, e_r: query.e_r || undefined, + domain: query.domain || '', + checkToken: query.checkToken || false, } } module.exports = createOption diff --git a/util/request.js b/util/request.js index cae0a1f..79cc543 100644 --- a/util/request.js +++ b/util/request.js @@ -1,3 +1,4 @@ +// 预先导入和绑定常用模块及函数 const encrypt = require('./crypto') const CryptoJS = require('crypto-js') const { default: axios } = require('axios') @@ -8,30 +9,50 @@ const tunnel = require('tunnel') const fs = require('fs') const path = require('path') const tmpPath = require('os').tmpdir() -const { cookieToJson, cookieObjToString, toBoolean } = require('./index') +const { + cookieToJson, + cookieObjToString, + toBoolean, + generateRandomChineseIP, +} = require('./index') +const { URLSearchParams, URL } = require('url') +const { APP_CONF } = require('../util/config.json') + +// 预先读取匿名token并缓存 const anonymous_token = fs.readFileSync( path.resolve(tmpPath, './anonymous_token'), 'utf-8', ) -const { URLSearchParams, URL } = require('url') -const { APP_CONF } = require('../util/config.json') -// request.debug = true // 开启可看到更详细信息 +// 预先绑定常用函数和常量 +const floor = Math.floor +const random = Math.random +const now = Date.now +const keys = Object.keys +const stringify = JSON.stringify +const parse = JSON.parse +const characters = 'abcdefghijklmnopqrstuvwxyz' +const charactersLength = characters.length + +// 预先创建HTTP/HTTPS agents并重用 +const createHttpAgent = () => new http.Agent({ keepAlive: true }) +const createHttpsAgent = () => new https.Agent({ keepAlive: true }) + +// 预先计算WNMCID(只计算一次) const WNMCID = (function () { - const characters = 'abcdefghijklmnopqrstuvwxyz' let randomString = '' - for (let i = 0; i < 6; i++) - randomString += characters.charAt( - Math.floor(Math.random() * characters.length), - ) - return `${randomString}.${Date.now().toString()}.01.0` + for (let i = 0; i < 6; i++) { + randomString += characters.charAt(floor(random() * charactersLength)) + } + return `${randomString}.${now().toString()}.01.0` })() +// 预先定义osMap const osMap = { pc: { os: 'pc', - appver: '3.0.18.203152', - osver: 'Microsoft-Windows-10-Professional-build-22631-64bit', + appver: '3.1.17.204416', + osver: 'Microsoft-Windows-10-Professional-build-19045-64bit', channel: 'netease', }, linux: { @@ -54,91 +75,128 @@ const osMap = { }, } -const chooseUserAgent = (crypto, uaType = 'pc') => { - const userAgentMap = { - weapi: { - pc: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0', - }, - linuxapi: { - linux: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', - }, - api: { - pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152', - android: - 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', - iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)', - }, - } - return userAgentMap[crypto][uaType] || '' +// 预先定义userAgentMap +const userAgentMap = { + weapi: { + pc: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0', + }, + linuxapi: { + linux: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', + }, + api: { + pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152', + android: + 'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)', + iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)', + }, } + +// 预先定义常量 +const DOMAIN = APP_CONF.domain +const API_DOMAIN = APP_CONF.apiDomain +const ENCRYPT_RESPONSE = APP_CONF.encryptResponse +const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803]) + +// chooseUserAgent函数 +const chooseUserAgent = (crypto, uaType = 'pc') => { + return userAgentMap[crypto]?.[uaType] || '' +} + +// cookie处理 +const processCookieObject = (cookie, uri) => { + const _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString() + const os = osMap[cookie.os] || osMap['pc'] + + const processedCookie = { + ...cookie, + __remember_me: 'true', + ntes_kaola_ad: '1', + _ntes_nuid: cookie._ntes_nuid || _ntes_nuid, + _ntes_nnid: cookie._ntes_nnid || `${_ntes_nuid},${now().toString()}`, + WNMCID: cookie.WNMCID || WNMCID, + WEVNSM: cookie.WEVNSM || '1.0.0', + osver: cookie.osver || os.osver, + deviceId: cookie.deviceId || global.deviceId, + os: cookie.os || os.os, + channel: cookie.channel || os.channel, + appver: cookie.appver || os.appver, + } + + if (uri.indexOf('login') === -1) { + processedCookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString() + } + + if (!processedCookie.MUSIC_U) { + processedCookie.MUSIC_A = processedCookie.MUSIC_A || anonymous_token + } + + return processedCookie +} + +// header cookie生成 +const createHeaderCookie = (header) => { + const headerKeys = keys(header) + const cookieParts = new Array(headerKeys.length) + + for (let i = 0, len = headerKeys.length; i < len; i++) { + const key = headerKeys[i] + cookieParts[i] = + encodeURIComponent(key) + '=' + encodeURIComponent(header[key]) + } + + return cookieParts.join('; ') +} + +// requestId生成 +const generateRequestId = () => { + return `${now()}_${floor(random() * 1000) + .toString() + .padStart(4, "0")}`; + +} + const createRequest = (uri, data, options) => { return new Promise((resolve, reject) => { - let headers = options.headers || {} - let ip = options.realIP || options.ip || '' - // console.log(ip) + // 变量声明和初始化 + const headers = options.headers ? { ...options.headers } : {} + const ip = options.realIP || options.ip || '' + + // IP头设置 if (ip) { headers['X-Real-IP'] = ip headers['X-Forwarded-For'] = ip } - // headers['X-Real-IP'] = '118.88.88.88' let cookie = options.cookie || {} if (typeof cookie === 'string') { cookie = cookieToJson(cookie) } - if (typeof cookie === 'object') { - let _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString() - let os = osMap[cookie.os] || osMap['iphone'] - cookie = { - ...cookie, - __remember_me: 'true', - // NMTID: CryptoJS.lib.WordArray.random(16).toString(), - ntes_kaola_ad: '1', - _ntes_nuid: cookie._ntes_nuid || _ntes_nuid, - _ntes_nnid: - cookie._ntes_nnid || `${_ntes_nuid},${Date.now().toString()}`, - WNMCID: cookie.WNMCID || WNMCID, - WEVNSM: cookie.WEVNSM || '1.0.0', - osver: cookie.osver || os.osver, - deviceId: cookie.deviceId || global.deviceId, - os: cookie.os || os.os, - channel: cookie.channel || os.channel, - appver: cookie.appver || os.appver, - } - if (uri.indexOf('login') === -1) { - cookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString() - } - if (!cookie.MUSIC_U) { - // 游客 - cookie.MUSIC_A = cookie.MUSIC_A || anonymous_token - } + if (typeof cookie === 'object') { + cookie = processCookieObject(cookie, uri) headers['Cookie'] = cookieObjToString(cookie) } + let url = '' + let encryptData = '' + let crypto = options.crypto + const csrfToken = cookie['__csrf'] || '' - let url = '', - encryptData = '', - crypto = options.crypto, - csrfToken = cookie['__csrf'] || '' - + // 加密方式选择 if (crypto === '') { - // 加密方式为空,以配置文件的加密方式为准 - if (APP_CONF.encrypt) { - crypto = 'eapi' - } else { - crypto = 'api' - } + crypto = APP_CONF.encrypt ? 'eapi' : 'api' } - // 根据加密方式加密请求数据;目前任意uri都支持四种加密方式 + const answer = { status: 500, body: {}, cookie: [] } + + // 根据加密方式处理 switch (crypto) { case 'weapi': - headers['Referer'] = APP_CONF.domain + headers['Referer'] = DOMAIN headers['User-Agent'] = options.ua || chooseUserAgent('weapi') data.csrf_token = csrfToken encryptData = encrypt.weapi(data) - url = APP_CONF.domain + '/weapi/' + uri.substr(5) + url = DOMAIN + '/weapi/' + uri.substr(5) break case 'linuxapi': @@ -146,127 +204,127 @@ const createRequest = (uri, data, options) => { options.ua || chooseUserAgent('linuxapi', 'linux') encryptData = encrypt.linuxapi({ method: 'POST', - url: APP_CONF.domain + uri, + url: DOMAIN + uri, params: data, }) - url = APP_CONF.domain + '/api/linux/forward' + url = DOMAIN + '/api/linux/forward' break case 'eapi': case 'api': - // 两种加密方式,都应生成客户端的cookie + // header创建 const header = { - osver: cookie.osver, //系统版本 + osver: cookie.osver, deviceId: cookie.deviceId, - os: cookie.os, //系统类型 - appver: cookie.appver, // app版本 - versioncode: cookie.versioncode || '140', //版本号 - mobilename: cookie.mobilename || '', //设备model - buildver: cookie.buildver || Date.now().toString().substr(0, 10), - resolution: cookie.resolution || '1920x1080', //设备分辨率 + os: cookie.os, + appver: cookie.appver, + versioncode: cookie.versioncode || '140', + mobilename: cookie.mobilename || '', + buildver: cookie.buildver || now().toString().substr(0, 10), + resolution: cookie.resolution || '1920x1080', __csrf: csrfToken, - channel: cookie.channel, //下载渠道 - requestId: `${Date.now()}_${Math.floor(Math.random() * 1000) - .toString() - .padStart(4, '0')}`, + channel: cookie.channel, + requestId: generateRequestId(), + ...(options.checkToken + ? { 'X-antiCheatToken': APP_CONF.checkToken } + : {}), + // clientSign: APP_CONF.clientSign, } + if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A - headers['Cookie'] = Object.keys(header) - .map( - (key) => - encodeURIComponent(key) + '=' + encodeURIComponent(header[key]), - ) - .join('; ') + + headers['Cookie'] = createHeaderCookie(header) headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone') if (crypto === 'eapi') { - // 使用eapi加密 data.header = header data.e_r = toBoolean( options.e_r !== undefined ? options.e_r : data.e_r !== undefined ? data.e_r - : APP_CONF.encryptResponse, - ) // 用于加密接口返回值 + : ENCRYPT_RESPONSE, + ) encryptData = encrypt.eapi(uri, data) - url = APP_CONF.apiDomain + '/eapi/' + uri.substr(5) + url = API_DOMAIN + '/eapi/' + uri.substr(5) } else if (crypto === 'api') { - // 不使用任何加密 - url = APP_CONF.apiDomain + uri + url = API_DOMAIN + uri encryptData = data } break default: - // 未知的加密方式 console.log('[ERR]', 'Unknown Crypto:', crypto) break } - const answer = { status: 500, body: {}, cookie: [] } - // console.log(headers, 'headers') + // console.log(url); + // settings创建 let settings = { method: 'POST', url: url, headers: headers, data: new URLSearchParams(encryptData).toString(), - httpAgent: new http.Agent({ keepAlive: true }), - httpsAgent: new https.Agent({ keepAlive: true }), + httpAgent: createHttpAgent(), + httpsAgent: createHttpsAgent(), } + // e_r处理 if (data.e_r) { - settings = { - ...settings, - encoding: null, - responseType: 'arraybuffer', - } + settings.encoding = null + settings.responseType = 'arraybuffer' } + // 代理处理 if (options.proxy) { if (options.proxy.indexOf('pac') > -1) { - settings.httpAgent = new PacProxyAgent(options.proxy) - settings.httpsAgent = new PacProxyAgent(options.proxy) + const agent = new PacProxyAgent(options.proxy) + settings.httpAgent = agent + settings.httpsAgent = agent } else { - const purl = new URL(options.proxy) - if (purl.hostname) { - const agent = tunnel[ - purl.protocol === 'https' ? 'httpsOverHttp' : 'httpOverHttp' - ]({ - proxy: { - host: purl.hostname, - port: purl.port || 80, - proxyAuth: - purl.username && purl.password - ? purl.username + ':' + purl.password - : '', - }, - }) - settings.httpsAgent = agent - settings.httpAgent = agent - settings.proxy = false - } else { - console.error('代理配置无效,不使用代理') + try { + const purl = new URL(options.proxy) + if (purl.hostname) { + const isHttps = purl.protocol === 'https:' + const agent = tunnel[isHttps ? 'httpsOverHttp' : 'httpOverHttp']({ + proxy: { + host: purl.hostname, + port: purl.port || 80, + proxyAuth: + purl.username && purl.password + ? purl.username + ':' + purl.password + : '', + }, + }) + settings.httpsAgent = agent + settings.httpAgent = agent + settings.proxy = false + } else { + console.error('代理配置无效,不使用代理') + } + } catch (e) { + console.error('代理URL解析失败:', e.message) } } } else { settings.proxy = false } + // console.log(settings.headers); axios(settings) .then((res) => { const body = res.data answer.cookie = (res.headers['set-cookie'] || []).map((x) => x.replace(/\s*Domain=[^(;|$)]+;*/, ''), ) + try { if (crypto === 'eapi' && data.e_r) { - // eapi接口返回值被加密,需要解密 answer.body = encrypt.eapiResDecrypt( body.toString('hex').toUpperCase(), ) } else { answer.body = - typeof body == 'object' ? body : JSON.parse(body.toString()) + typeof body === 'object' ? body : parse(body.toString()) } if (answer.body.code) { @@ -274,28 +332,30 @@ const createRequest = (uri, data, options) => { } answer.status = Number(answer.body.code || res.status) - if ( - [201, 302, 400, 502, 800, 801, 802, 803].indexOf(answer.body.code) > - -1 - ) { - // 特殊状态码 + + // 状态码检查(使用Set提升查找性能) + if (SPECIAL_STATUS_CODES.has(answer.body.code)) { answer.status = 200 } } catch (e) { - // console.log(e) - // can't decrypt and can't parse directly answer.body = body answer.status = res.status } answer.status = - 100 < answer.status && answer.status < 600 ? answer.status : 400 - if (answer.status === 200) resolve(answer) - else reject(answer) + answer.status > 100 && answer.status < 600 ? answer.status : 400 + + if (answer.status === 200) { + resolve(answer) + } else { + console.log('[ERR]', answer) + reject(answer) + } }) .catch((err) => { answer.status = 502 - answer.body = { code: 502, msg: err } + answer.body = { code: 502, msg: err.message || err } + console.log('[ERR]', answer) reject(answer) }) })