From abd0c9ca916c1a9264e8e94239044ff0217d65e0 Mon Sep 17 00:00:00 2001 From: ImFurina <222616389+MoeFurina@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:08:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored main.js to optimize module and server loading, improving code clarity and lazy loading. Updated several dependencies in package.json to their latest versions for better security and compatibility. Updated documentation cover page and home page to reflect project branding changes. Co-Authored-By: binaryify --- main.js | 78 +++++---- package.json | 24 +-- pnpm-lock.yaml | 32 ++-- public/docs/_coverpage.md | 4 +- public/docs/home.md | 2 +- server.js | 14 +- util/client-sign.js | 169 ++++++++++++++++++ util/config.json | 6 +- util/crypto.js | 9 +- util/index.js | 159 ++++++++++++++--- util/memory-cache.js | 102 ++++++----- util/option.js | 2 + util/request.js | 349 ++++++++++++++++++++++---------------- 13 files changed, 656 insertions(+), 294 deletions(-) create mode 100644 util/client-sign.js 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/package.json b/package.json index bfed121..2ca5e45 100644 --- a/package.json +++ b/package.json @@ -66,25 +66,25 @@ ], "dependencies": { "@unblockneteasemusic/server": "^0.27.10", - "axios": "^1.2.2", + "axios": "^1.11.0", "crypto-js": "^4.2.0", - "dotenv": "^16.0.3", - "express": "^4.17.1", - "express-fileupload": "^1.1.9", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-fileupload": "^1.5.2", "md5": "^2.3.0", - "music-metadata": "^7.5.3", + "music-metadata": "^7.14.0", "node-forge": "^1.3.1", - "pac-proxy-agent": "^7.0.0", - "qrcode": "^1.4.4", + "pac-proxy-agent": "^7.2.0", + "qrcode": "^1.5.4", "safe-decode-uri-component": "^1.2.1", "tunnel": "^0.0.6", "xml2js": "^0.6.2", - "yargs": "^17.1.1" + "yargs": "^17.7.2" }, "devDependencies": { - "@types/express": "^4.17.13", - "@types/express-fileupload": "^1.2.2", - "@types/mocha": "^9.1.0", + "@types/express": "^4.17.23", + "@types/express-fileupload": "^1.5.1", + "@types/mocha": "^9.1.1", "@types/node": "16.11.19", "@typescript-eslint/eslint-plugin": "5.0.0", "@typescript-eslint/parser": "5.0.0", @@ -96,7 +96,7 @@ "intelli-espower-loader": "1.1.0", "lint-staged": "12.1.7", "mocha": "10.0.0", - "pkg": "^5.8.0", + "pkg": "^5.8.1", "power-assert": "1.6.1", "prettier": "2.7.1", "typescript": "4.5.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97a2d7e..12ceb1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,34 +12,34 @@ importers: specifier: ^0.27.10 version: 0.27.10 axios: - specifier: ^1.2.2 + specifier: ^1.11.0 version: 1.11.0 crypto-js: specifier: ^4.2.0 version: 4.2.0 dotenv: - specifier: ^16.0.3 + specifier: ^16.6.1 version: 16.6.1 express: - specifier: ^4.17.1 + specifier: ^4.21.2 version: 4.21.2 express-fileupload: - specifier: ^1.1.9 + specifier: ^1.5.2 version: 1.5.2 md5: specifier: ^2.3.0 version: 2.3.0 music-metadata: - specifier: ^7.5.3 + specifier: ^7.14.0 version: 7.14.0 node-forge: specifier: ^1.3.1 version: 1.3.1 pac-proxy-agent: - specifier: ^7.0.0 + specifier: ^7.2.0 version: 7.2.0 qrcode: - specifier: ^1.4.4 + specifier: ^1.5.4 version: 1.5.4 safe-decode-uri-component: specifier: ^1.2.1 @@ -51,17 +51,17 @@ importers: specifier: ^0.6.2 version: 0.6.2 yargs: - specifier: ^17.1.1 + specifier: ^17.7.2 version: 17.7.2 devDependencies: '@types/express': - specifier: ^4.17.13 + specifier: ^4.17.23 version: 4.17.23 '@types/express-fileupload': - specifier: ^1.2.2 + specifier: ^1.5.1 version: 1.5.1 '@types/mocha': - specifier: ^9.1.0 + specifier: ^9.1.1 version: 9.1.1 '@types/node': specifier: 16.11.19 @@ -97,7 +97,7 @@ importers: specifier: 10.0.0 version: 10.0.0 pkg: - specifier: ^5.8.0 + specifier: ^5.8.1 version: 5.8.1 power-assert: specifier: 1.6.1 @@ -1640,8 +1640,8 @@ packages: next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - node-abi@3.75.0: - resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + node-abi@3.77.0: + resolution: {integrity: sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==} engines: {node: '>=10'} node-fetch@2.7.0: @@ -4248,7 +4248,7 @@ snapshots: next-tick@1.1.0: {} - node-abi@3.75.0: + node-abi@3.77.0: dependencies: semver: 7.7.2 @@ -4534,7 +4534,7 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 1.0.2 - node-abi: 3.75.0 + node-abi: 3.77.0 pump: 3.0.3 rc: 1.2.8 simple-get: 4.0.1 diff --git a/public/docs/_coverpage.md b/public/docs/_coverpage.md index 017a98b..d251cb4 100644 --- a/public/docs/_coverpage.md +++ b/public/docs/_coverpage.md @@ -1,6 +1,6 @@ -# 网易云音乐 API Enhanced Reborn +# 网易云音乐 API Enhanced -> 为停更的网易云音乐 NodeJs API 提供持续的维护! +> 🔍 网易云音乐API Node.js服务的复兴项目 - 基于原版网易云API新增更多有趣的功能 - 具备登录接口,多达200多个接口 diff --git a/public/docs/home.md b/public/docs/home.md index 24cca89..8ab1cb8 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -1,6 +1,6 @@ # NeteaseCloudMusicApiEnhanced -网易云音乐 NodeJS API Enhanced(Reborn) +网易云音乐 NodeJS API Enhanced ## 灵感来自 diff --git a/server.js b/server.js index 5c1bce8..e9b1f16 100644 --- a/server.js +++ b/server.js @@ -146,16 +146,11 @@ async function consturctServer(moduleDefs) { * CORS & Preflight request */ app.use((req, res, next) => { - // 强制设置 Access-Control-Allow-Credentials: true if (req.path !== '/' && !req.path.includes('.')) { - let allowOrigin = CORS_ALLOW_ORIGIN || req.headers.origin - // 禁止为 *,必须为具体域名 - if (!allowOrigin || allowOrigin === '*') { - allowOrigin = req.headers.origin || '' - } res.set({ 'Access-Control-Allow-Credentials': true, - 'Access-Control-Allow-Origin': allowOrigin, + 'Access-Control-Allow-Origin': + CORS_ALLOW_ORIGIN || req.headers.origin || '*', 'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type', 'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS', 'Content-Type': 'application/json; charset=utf-8', @@ -232,8 +227,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 @@ -359,6 +354,7 @@ async function serveNcmApi(options) { - Node Version: ${process.version} - Process ID: ${process.pid}`) }) + return appExt } 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 62d48f5..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,31 +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') -const logger = require('./logger.js') -// 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: { @@ -55,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 || '' - // logger.info(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': @@ -147,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: - // 未知的加密方式 - logger.info('[ERR]', 'Unknown Crypto:', crypto) + console.log('[ERR]', 'Unknown Crypto:', crypto) break } - const answer = { status: 500, body: {}, cookie: [] } - // logger.info(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) { @@ -275,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) { - // logger.info(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) }) })