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) }) })