mirror of
https://gitlab.com/Binaryify/neteasecloudmusicapi.git
synced 2025-05-23 22:37:41 +08:00
refactor encryption (support linux api)
This commit is contained in:
parent
9be0feccd8
commit
6421a6c9e3
156
app.js
156
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');
|
||||
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
|
||||
|
||||
// version check
|
||||
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 +
|
||||
',请及时更新'
|
||||
)
|
||||
}
|
||||
if(!err){
|
||||
let version = stdout.trim()
|
||||
if(package.version < version){
|
||||
console.log(`最新版本: ${version}, 当前版本: ${package.version}, 请及时更新`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 跨域设置
|
||||
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()
|
||||
})
|
||||
const app = express()
|
||||
|
||||
const onlyStatus200 = (req, res) => res.statusCode === 200
|
||||
|
||||
app.use(cache('2 minutes', onlyStatus200))
|
||||
|
||||
app.use(express.static(path.resolve(__dirname, 'public')))
|
||||
|
||||
// 补全缺失的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)
|
||||
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)
|
||||
})
|
||||
})
|
||||
.catch(answer => {
|
||||
console.log('[ERR]', decodeURIComponent(req.originalUrl))
|
||||
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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
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-----'
|
||||
|
||||
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
|
||||
const aesEncrypt = (buffer, mode, key, iv) => {
|
||||
const cipher = crypto.createCipheriv('aes-128-' + mode, key, iv)
|
||||
return Buffer.concat([cipher.update(buffer),cipher.final()])
|
||||
}
|
||||
|
||||
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
|
||||
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 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 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 zfill(str, size) {
|
||||
while (str.length < size) str = '0' + str
|
||||
return str
|
||||
const linuxapi = (object) => {
|
||||
const text = JSON.stringify(object)
|
||||
return {
|
||||
eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '').toString('hex').toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Encrypt
|
||||
module.exports = {weapi, linuxapi}
|
30
util/init.js
30
util/init.js
@ -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
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user