From 6391ee81a3401c4490e133d7995947866b60c328 Mon Sep 17 00:00:00 2001 From: Nzix Date: Fri, 19 Oct 2018 18:42:21 +0800 Subject: [PATCH 1/5] modify comments --- module/artist_sublist.js | 2 +- module/dj_sublist.js | 2 +- module/like.js | 2 +- module/mv_url.js | 2 ++ module/user_cloud_search.js | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/module/artist_sublist.js b/module/artist_sublist.js index d4d8096d0d3..ee0e8080632 100644 --- a/module/artist_sublist.js +++ b/module/artist_sublist.js @@ -1,4 +1,4 @@ -// 我的歌手列表 +// 关注歌手列表 module.exports = (query, request) => { const data = { diff --git a/module/dj_sublist.js b/module/dj_sublist.js index d42c83029ae..6eee3de1237 100644 --- a/module/dj_sublist.js +++ b/module/dj_sublist.js @@ -1,4 +1,4 @@ -// 我的电台列表 +// 订阅电台列表 module.exports = (query, request) => { const data = { diff --git a/module/like.js b/module/like.js index 3e29699f74b..3314087eadd 100644 --- a/module/like.js +++ b/module/like.js @@ -1,4 +1,4 @@ -// 红心取消红心歌曲 +// 红心与取消红心歌曲 module.exports = (query, request) => { query.like = (query.like ? true : false) diff --git a/module/mv_url.js b/module/mv_url.js index 669bc4f4a80..1338861ec86 100644 --- a/module/mv_url.js +++ b/module/mv_url.js @@ -1,3 +1,5 @@ +// MV链接 + module.exports = (query, request) => { const data = { id: query.id, diff --git a/module/user_cloud_search.js b/module/user_cloud_search.js index d17b5b98a3a..81872e6c2b6 100644 --- a/module/user_cloud_search.js +++ b/module/user_cloud_search.js @@ -1,4 +1,4 @@ -// 云盘数据详情?(暂时不要使用) +// 云盘数据详情(暂时不要使用) module.exports = (query, request) => { const data = { From 2b63eff404a297b6d478bb55bf2faecfcb5c3f21 Mon Sep 17 00:00:00 2001 From: Nzix Date: Sat, 20 Oct 2018 13:16:47 +0800 Subject: [PATCH 2/5] refactor encryption (support linux api) --- app.js | 156 ++++++++++++++++++++---------------------------- package.json | 3 +- util/crypto.js | 83 ++++++++------------------ util/init.js | 30 ---------- util/request.js | 21 ++++--- 5 files changed, 102 insertions(+), 191 deletions(-) delete mode 100644 util/init.js diff --git a/app.js b/app.js index 8445832db05..ee75e047db3 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 501aad27908..df33c6a79ee 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 ade29b294ce..f916a853ecd 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 c6c10f87f77..00000000000 --- 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 7b2b76771d3..97715d731ac 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 From 43e069e37c573ce01c5e8c77ab145f99fef41893 Mon Sep 17 00:00:00 2001 From: Nzix Date: Sat, 20 Oct 2018 13:17:58 +0800 Subject: [PATCH 3/5] internal modify --- module/banner.js | 16 ++-------------- module/check_music.js | 12 ++++++++---- module/top_song.js | 10 ++++++++-- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/module/banner.js b/module/banner.js index 869bd2a374a..a53bbbf28f5 100644 --- a/module/banner.js +++ b/module/banner.js @@ -2,19 +2,7 @@ module.exports = (query, request) => { return request( - 'GET', `http://music.163.com/discover`, {}, - {ua: 'pc', proxy: query.proxy} + 'POST', `http://music.163.com/api/v2/banner/get`, {clientType: "pc"}, + {crypto: 'linuxapi', proxy: query.proxy} ) - .then(response => { - try{ - const banners = eval(`(${/Gbanners\s*=\s*([^;]+);/.exec(response.body)[1]})`) - response.body = {code: 200, banners: banners} - return response - } - catch(err){ - response.status = 500 - response.body = {code: 500, msg: err.stack} - return Promise.reject(response) - } - }) } \ No newline at end of file diff --git a/module/check_music.js b/module/check_music.js index 8d11dffda28..c98f1ac34ac 100644 --- a/module/check_music.js +++ b/module/check_music.js @@ -10,12 +10,16 @@ module.exports = (query, request) => { {crypto: 'weapi', cookie: query.cookie, proxy: query.proxy} ) .then(response => { - if (response.body.code == 200) { - if (response.body.data[0].code == 200){ - response.body = {success: true, message: 'ok'} - return response + let playable = false + if(response.body.code == 200){ + if(response.body.data[0].code == 200){ + playable = true } } + if(playable){ + response.body = {success: true, message: 'ok'} + return response + } else{ response.status = 404 response.body = {success: false, message: '亲爱的,暂无版权'} diff --git a/module/top_song.js b/module/top_song.js index 304d94e519b..a8c2be8a097 100644 --- a/module/top_song.js +++ b/module/top_song.js @@ -1,8 +1,14 @@ -// 最新单曲(暂时废弃?) +// 新歌速递 module.exports = (query, request) => { + const data = { + areaId: query.type || 0, // 全部:0 华语:7 欧美:96 日本:8 韩国:16 + limit: query.limit || 100, + offset: query.offset || 0, + total: true + } return request( - 'POST', `http://music.163.com/weapi/v1/discovery/new/songs`, {}, + 'POST', `http://music.163.com/weapi/v1/discovery/new/songs`, data, {crypto: 'weapi', cookie: query.cookie, proxy: query.proxy} ) } \ No newline at end of file From f0d3b1f7bf153c8e0a3358757ccf27741c0e133d Mon Sep 17 00:00:00 2001 From: Nzix Date: Sat, 20 Oct 2018 13:41:23 +0800 Subject: [PATCH 4/5] individual patch --- module/send_playlist.js | 1 + module/send_text.js | 1 + 2 files changed, 2 insertions(+) diff --git a/module/send_playlist.js b/module/send_playlist.js index 1d9a3babb5a..9335e75959b 100644 --- a/module/send_playlist.js +++ b/module/send_playlist.js @@ -1,6 +1,7 @@ // 私信歌单 module.exports = (query, request) => { + query.cookie.os = 'pc' const data = { id: query.playlist, type: 'playlist', diff --git a/module/send_text.js b/module/send_text.js index 6cd8715d5ac..c68cf10947c 100644 --- a/module/send_text.js +++ b/module/send_text.js @@ -1,6 +1,7 @@ // 私信 module.exports = (query, request) => { + query.cookie.os = 'pc' const data = { id: query.playlist, type: 'text', From c37d5598ffe7c2a73affa9af489170207f19306b Mon Sep 17 00:00:00 2001 From: Nzix Date: Sun, 21 Oct 2018 03:23:46 +0800 Subject: [PATCH 5/5] unified code style --- app.js | 4 ++-- util/request.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index ee75e047db3..a27b237d502 100644 --- a/app.js +++ b/app.js @@ -19,7 +19,7 @@ exec('npm info NeteaseCloudMusicApi version', (err, stdout, stderr) => { const app = express() // CORS -app.use(function(req, res, next){ +app.use((req, res, next) => { if(req.path !== '/' && !req.path.includes('.')){ res.header({ 'Access-Control-Allow-Credentials': true, @@ -33,7 +33,7 @@ app.use(function(req, res, next){ }) // cookie parser -app.use(function(req, res, next){ +app.use((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 diff --git a/util/request.js b/util/request.js index 97715d731ac..e797fa5d79d 100644 --- a/util/request.js +++ b/util/request.js @@ -4,7 +4,7 @@ const queryString = require('querystring') request.debug = true -function chooseUserAgent(ua){ +const 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', @@ -33,7 +33,7 @@ function chooseUserAgent(ua){ return userAgentList[index] } -function createRequest(method, url, data, options){ +const createRequest = (method, url, data, options) => { return new Promise((resolve, reject) => { let headers = {'User-Agent': chooseUserAgent(options.ua)}