diff --git a/app.js b/app.js index 8445832db05994e865d3aa6f9299d9123a3fdddc..ee75e047db38c7c958c32dad77ad847946f3985a 100644 --- a/app.js +++ b/app.js @@ -1,114 +1,86 @@ -const express = require('express') -const apicache = require('apicache') -const path = require('path') const fs = require('fs') -const app = express() -let cache = apicache.middleware -const { exec } = require('child_process'); -exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => { - if (err) { - console.error(err); - return; - } - const onlinePackageVersion = stdout.trim(); - const package = require('./package.json') - if (package.version < onlinePackageVersion) { - console.log( - '最新版:Version:' + - onlinePackageVersion + - ',当前版本:' + - package.version + - ',请及时更新' - ) - } -}) +const path = require('path') +const express = require('express') +const request = require('./util/request') +const package = require('./package.json') +const exec = require('child_process').exec +const cache = require('apicache').middleware -// 跨域设置 -app.all('*', function(req, res, next) { - if (req.path !== '/' && !req.path.includes('.')) { - res.header('Access-Control-Allow-Credentials', true) - // 这里获取 origin 请求头 而不是用 * - res.header('Access-Control-Allow-Origin', req.headers['origin'] || '*') - res.header('Access-Control-Allow-Headers', 'X-Requested-With') - res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS') - res.header('Content-Type', 'application/json;charset=utf-8') - } - next() +// version check +exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => { + if(!err){ + let version = stdout.trim() + if(package.version < version){ + console.log(`最新版本: ${version}, 当前版本: ${package.version}, 请及时更新`) + } + } }) -const onlyStatus200 = (req, res) => res.statusCode === 200 - -app.use(cache('2 minutes', onlyStatus200)) - -app.use(express.static(path.resolve(__dirname, 'public'))) +const app = express() -// 补全缺失的cookie -const { completeCookie } = require('./util/init') -app.use(function(req, res, next) { - let cookie = completeCookie(req.headers.cookie) - req.headers.cookie = cookie.map(x => x[0]).concat(req.headers.cookie || []).join('; ') - res.append('Set-Cookie', cookie.map(x => (x.concat('Path=/').join('; ')))) - next() +// CORS +app.use(function(req, res, next){ + if(req.path !== '/' && !req.path.includes('.')){ + res.header({ + 'Access-Control-Allow-Credentials': true, + 'Access-Control-Allow-Origin': req.headers.origin || '*', + 'Access-Control-Allow-Headers': 'X-Requested-With', + 'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS', + 'Content-Type': 'application/json; charset=utf-8' + }) + } + next() }) // cookie parser -app.use(function(req, res, next) { - req.cookies = {}, (req.headers.cookie || '').split(/\s*;\s*/).forEach(pair => { - let crack = pair.indexOf('=') - if(crack < 1 || crack == pair.length - 1) return - req.cookies[decodeURIComponent(pair.slice(0, crack)).trim()] = decodeURIComponent(pair.slice(crack + 1)).trim() - }) - next() +app.use(function(req, res, next){ + req.cookies = {}, (req.headers.cookie || '').split(/\s*;\s*/).forEach(pair => { + let crack = pair.indexOf('=') + if(crack < 1 || crack == pair.length - 1) return + req.cookies[decodeURIComponent(pair.slice(0, crack)).trim()] = decodeURIComponent(pair.slice(crack + 1)).trim() + }) + next() }) -app.use(function(req, res, next) { - const proxy = req.query.proxy - if (proxy) { - req.headers.cookie += `__proxy__${proxy}` - } - next() -}) +// cache +app.use(cache('2 minutes', ((req, res) => res.statusCode === 200))) -// 因为这几个文件对外所注册的路由 和 其他文件对外注册的路由规则不一样, 所以专门写个MAP对这些文件做特殊处理 -const UnusualRouteFileMap = { - // key 为文件名, value 为对外注册的路由 - 'daily_signin.js': '/daily_signin', - 'fm_trash.js': '/fm_trash', - 'personal_fm.js': '/personal_fm' -} +// static +app.use(express.static(path.join(__dirname, 'public'))) +// router +const special = { + 'daily_signin.js': '/daily_signin', + 'fm_trash.js': '/fm_trash', + 'personal_fm.js': '/personal_fm' +} -// 改写router为module -const requestMod = require('./util/request') -let dev = express() -fs.readdirSync(path.join(__dirname, 'module')) -.reverse() -.forEach(file => { - if (!(/\.js$/i.test(file))) return - let route = (file in UnusualRouteFileMap) ? UnusualRouteFileMap[file] : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/') - let question = require(path.join(__dirname, 'module', file)) - - dev.use(route, (req, res) => { - let query = {...req.query, cookie: req.cookies} - question(query, requestMod) - .then(answer => { - console.log('[OK]', decodeURIComponent(req.originalUrl)) - res.append('Set-Cookie', answer.cookie) - res.status(answer.status).send(answer.body) - }) - .catch(answer => { - console.log('[ERR]', decodeURIComponent(req.originalUrl)) - res.append('Set-Cookie', answer.cookie) - res.status(answer.status).send(answer.body) +fs.readdirSync(path.join(__dirname, 'module')).reverse().forEach(file => { + if(!(/\.js$/i.test(file))) return + let route = (file in special) ? special[file] : '/' + file.replace(/\.js$/i, '').replace(/_/g, '/') + let question = require(path.join(__dirname, 'module', file)) + + app.use(route, (req, res) => { + let query = {...req.query, ...req.body, cookie: req.cookies} + question(query, request) + .then(answer => { + console.log('[OK]', decodeURIComponent(req.originalUrl)) + res.append('Set-Cookie', answer.cookie) + res.status(answer.status).send(answer.body) + }) + .catch(answer => { + console.log('[ERR]', decodeURIComponent(req.originalUrl)) + if(answer.body.code =='301') answer.body.msg = '需要登录' + res.append('Set-Cookie', answer.cookie) + res.status(answer.status).send(answer.body) + }) }) - }) }) -app.use('/', dev) const port = process.env.PORT || 3000 app.server = app.listen(port, () => { - console.log(`server running @ http://localhost:${port}`) + console.log(`server running @ http://localhost:${port}`) }) module.exports = app diff --git a/package.json b/package.json index 501aad279080c0de63759a31a070f0e43e58148c..df33c6a79ee4d0543d60dd5f219a2db6b884b7cf 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "license": "MIT", "dependencies": { "apicache": "^1.2.1", - "big-integer": "^1.6.28", "express": "^4.16.3", "request": "^2.85.0" }, @@ -25,4 +24,4 @@ "mocha": "^5.1.1", "power-assert": "^1.5.0" } -} \ No newline at end of file +} diff --git a/util/crypto.js b/util/crypto.js index ade29b294ce17186dc963f7bb4cb59aab91f95f8..f916a853ecd9ea115a13615665069a725de9a8be 100644 --- a/util/crypto.js +++ b/util/crypto.js @@ -1,67 +1,34 @@ -// 参考 https://github.com/darknessomi/musicbox/wiki/ -'use strict' const crypto = require('crypto') -const bigInt = require('big-integer') -const modulus = - '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' -const nonce = '0CoJUm6Qyw8W8jud' -const pubKey = '010001' - -String.prototype.hexEncode = function() { - let hex, i - - let result = '' - for (i = 0; i < this.length; i++) { - hex = this.charCodeAt(i).toString(16) - result += ('' + hex).slice(-4) - } - return result -} - -function createSecretKey(size) { - const keys = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - let key = '' - for (let i = 0; i < size; i++) { - let pos = Math.random() * keys.length - pos = Math.floor(pos) - key = key + keys.charAt(pos) - } - return key -} - -function aesEncrypt(text, secKey) { - const _text = text - const lv = new Buffer('0102030405060708', 'binary') - const _secKey = new Buffer(secKey, 'binary') - const cipher = crypto.createCipheriv('AES-128-CBC', _secKey, lv) - let encrypted = cipher.update(_text, 'utf8', 'base64') - encrypted += cipher.final('base64') - return encrypted +const iv = Buffer.from('0102030405060708') +const presetKey = Buffer.from('0CoJUm6Qyw8W8jud') +const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q') +const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' +const publicKey = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----' + +const aesEncrypt = (buffer, mode, key, iv) => { + const cipher = crypto.createCipheriv('aes-128-' + mode, key, iv) + return Buffer.concat([cipher.update(buffer),cipher.final()]) } -function zfill(str, size) { - while (str.length < size) str = '0' + str - return str +const rsaEncrypt = (buffer, key) => { + buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) + return crypto.publicEncrypt({key: key, padding: crypto.constants.RSA_NO_PADDING}, buffer) } -function rsaEncrypt(text, pubKey, modulus) { - const _text = text.split('').reverse().join('') - const biText = bigInt(new Buffer(_text).toString('hex'), 16), - biEx = bigInt(pubKey, 16), - biMod = bigInt(modulus, 16), - biRet = biText.modPow(biEx, biMod) - return zfill(biRet.toString(16), 256) +const weapi = (object) => { + const text = JSON.stringify(object) + const secretKey = crypto.randomBytes(16).map(n => (base62.charAt(n % 62).charCodeAt())) + return { + params: aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64')), 'cbc', secretKey, iv).toString('base64'), + encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex') + } } -function Encrypt(obj) { - const text = JSON.stringify(obj) - const secKey = createSecretKey(16) - const encText = aesEncrypt(aesEncrypt(text, nonce), secKey) - const encSecKey = rsaEncrypt(secKey, pubKey, modulus) - return { - params: encText, - encSecKey: encSecKey - } +const linuxapi = (object) => { + const text = JSON.stringify(object) + return { + eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '').toString('hex').toUpperCase() + } } -module.exports = Encrypt +module.exports = {weapi, linuxapi} \ No newline at end of file diff --git a/util/init.js b/util/init.js deleted file mode 100644 index c6c10f87f77f46ce45321b5f045bcb98c133c363..0000000000000000000000000000000000000000 --- a/util/init.js +++ /dev/null @@ -1,30 +0,0 @@ -function randomString(pattern, length){ - return Array.apply(null, {length: length}).map(() => (pattern[Math.floor(Math.random() * pattern.length)])).join('') -} - -function completeCookie(cookie){ - let origin = (cookie || '').split(/;\s*/).map(element => (element.split('=')[0])), extra = [] - let now = Date.now() - - if(!origin.includes('JSESSIONID-WYYY')){ - let expire = new Date(now + 1800000) //30 minutes - let jessionid = randomString('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ\\/+',176) + ':' + expire.getTime() - extra.push(['JSESSIONID-WYYY=' + jessionid, 'Expires=' + expire.toGMTString()]) - } - if(!origin.includes('_iuqxldmzr_')){ - let expire = new Date(now + 157680000000) //5 years - extra.push(['_iuqxldmzr_=32', 'Expires=' + expire.toGMTString()]) - } - if((!origin.includes('_ntes_nnid'))||(!origin.includes('_ntes_nuid'))){ - let expire = new Date(now + 3153600000000) //100 years - let nnid = randomString('0123456789abcdefghijklmnopqrstuvwxyz',32) + ',' + now - extra.push(['_ntes_nnid=' + nnid, 'Expires=' + expire.toGMTString()]) - extra.push(['_ntes_nuid=' + nnid.slice(0,32), 'Expires=' + expire.toGMTString()]) - } - - return extra -} - -module.exports = { - completeCookie -} \ No newline at end of file diff --git a/util/request.js b/util/request.js index 7b2b76771d355b93a6cc7a21350c136267537bf7..97715d731ac825c032be69c74f1beadfbd5a0357 100644 --- a/util/request.js +++ b/util/request.js @@ -1,10 +1,10 @@ -const encrypt = require('./crypto.js') +const encrypt = require('./crypto') const request = require('request') const queryString = require('querystring') -// request.debug = false +request.debug = true -function chooseUserAgent(ua) { +function chooseUserAgent(ua){ const userAgentList = [ 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', @@ -38,7 +38,7 @@ function createRequest(method, url, data, options){ let headers = {'User-Agent': chooseUserAgent(options.ua)} if(method.toUpperCase() == 'POST') headers['Content-Type'] = 'application/x-www-form-urlencoded' - if(url.indexOf('music.163.com') != -1) headers['Referer'] = 'http://music.163.com' + if(url.includes('music.163.com')) headers['Referer'] = 'http://music.163.com' // headers['X-Real-IP'] = '118.88.88.88' if(typeof(options.cookie) === 'object') @@ -47,9 +47,15 @@ function createRequest(method, url, data, options){ headers['Cookie'] = options.cookie if(options.crypto == 'weapi'){ - const csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/) + let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/) data.csrf_token = (csrfToken ? csrfToken[1] : '') - data = encrypt(data) + data = encrypt.weapi(data) + url = url.replace(/\w*api/,'weapi') + } + else if(options.crypto == 'linuxapi'){ + data = encrypt.linuxapi({'method': method, url: url.replace(/\w*api/,'api'), 'params': data}) + headers['User-Agent'] = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36' + url = 'http://music.163.com/api/linux/forward' } const answer = {status: 500, body: {}, cookie: []} @@ -66,9 +72,6 @@ function createRequest(method, url, data, options){ try{ answer.body = JSON.parse(body) answer.status = answer.body.code || res.statusCode - if(answer.body.code=='301'){ - answer.body.apiMsg='需要登陆' - } } catch(e){ answer.body = body