From 5e61608f8051f6330c3f3dda08924c464cc2580f Mon Sep 17 00:00:00 2001 From: Link Date: Fri, 4 Dec 2020 11:49:16 +0800 Subject: [PATCH] style: format code --- .eslintignore | 2 +- .prettierignore | 1 + packages/api/app.js | 8 +- packages/api/app.test.js | 4 +- packages/api/main.js | 4 +- packages/api/module/batch.js | 2 +- packages/api/module/check_music.js | 2 +- packages/api/module/like.js | 5 +- packages/api/module/login.js | 5 +- packages/api/module/login_cellphone.js | 5 +- packages/api/module/login_status.js | 2 +- packages/api/module/playlist_track_add.js | 2 +- packages/api/module/playlist_track_delete.js | 2 +- packages/api/module/register_cellphone.js | 5 +- packages/api/module/related_playlist.js | 2 +- packages/api/module/song_detail.js | 2 +- packages/api/util/apicache.js | 74 +- packages/api/util/crypto.js | 13 +- packages/api/util/index.js | 2 +- packages/api/util/memory-cache.js | 14 +- packages/api/util/request.js | 16 +- packages/unblock/app.js | 2 +- packages/unblock/bridge.js | 2 +- packages/unblock/endpoint.worker.js | 8 +- packages/unblock/src/app.js | 163 ++- packages/unblock/src/bridge.js | 62 +- packages/unblock/src/browser/background.js | 80 +- packages/unblock/src/browser/convert.js | 76 +- packages/unblock/src/browser/crypto.js | 77 +- packages/unblock/src/browser/inject.js | 148 ++- packages/unblock/src/browser/request.js | 66 +- packages/unblock/src/browser/script.js | 12 +- packages/unblock/src/cache.js | 53 +- packages/unblock/src/cli.js | 321 +++-- packages/unblock/src/crypto.js | 233 ++-- packages/unblock/src/hook.js | 703 ++++++---- packages/unblock/src/kwDES.js | 1233 ++++++++++++++---- packages/unblock/src/provider/baidu.js | 64 +- packages/unblock/src/provider/find.js | 60 +- packages/unblock/src/provider/insure.js | 35 +- packages/unblock/src/provider/joox.js | 95 +- packages/unblock/src/provider/kugou.js | 78 +- packages/unblock/src/provider/kuwo.js | 133 +- packages/unblock/src/provider/match.js | 264 ++-- packages/unblock/src/provider/migu.js | 80 +- packages/unblock/src/provider/netease.js | 32 +- packages/unblock/src/provider/qq.js | 279 ++-- packages/unblock/src/provider/select.js | 3 +- packages/unblock/src/provider/xiami.js | 111 +- packages/unblock/src/provider/youtube.js | 135 +- packages/unblock/src/request.js | 184 ++- packages/unblock/src/server.js | 325 ++--- packages/unblock/src/sni.js | 98 +- 53 files changed, 3452 insertions(+), 1935 deletions(-) diff --git a/.eslintignore b/.eslintignore index 8d18b0b9..23053de0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -NeteaseCloudMusicApi/ +packages/ diff --git a/.prettierignore b/.prettierignore index e69de29b..3c3629e6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/api/app.js b/packages/api/app.js index 4ac4764e..2eaec497 100644 --- a/packages/api/app.js +++ b/packages/api/app.js @@ -26,7 +26,7 @@ app.use((req, res, next) => { // cookie parser app.use((req, res, next) => { req.cookies = {} - ;(req.headers.cookie || '').split(/\s*;\s*/).forEach((pair) => { + ;(req.headers.cookie || '').split(/\s*;\s*/).forEach(pair => { let crack = pair.indexOf('=') if (crack < 1 || crack == pair.length - 1) return req.cookies[ @@ -56,7 +56,7 @@ const special = { fs.readdirSync(path.join(__dirname, 'module')) .reverse() - .forEach((file) => { + .forEach(file => { if (!file.endsWith('.js')) return let route = file in special @@ -77,12 +77,12 @@ fs.readdirSync(path.join(__dirname, 'module')) ) question(query, request) - .then((answer) => { + .then(answer => { console.log('[OK]', decodeURIComponent(req.originalUrl)) res.append('Set-Cookie', answer.cookie) res.status(answer.status).send(answer.body) }) - .catch((answer) => { + .catch(answer => { console.log('[ERR]', decodeURIComponent(req.originalUrl), { status: answer.status, body: answer.body, diff --git a/packages/api/app.test.js b/packages/api/app.test.js index 94846b28..e6ddbd0b 100644 --- a/packages/api/app.test.js +++ b/packages/api/app.test.js @@ -6,10 +6,10 @@ before(() => { app = require('./app.js') global.host = 'http://localhost:' + app.server.address().port }) -after((done) => { +after(done => { app.server.close(done) }) -fs.readdirSync(path.join(__dirname, 'test')).forEach((file) => { +fs.readdirSync(path.join(__dirname, 'test')).forEach(file => { require(path.join(__dirname, 'test', file)) }) diff --git a/packages/api/main.js b/packages/api/main.js index 0b214902..02dd6e2b 100644 --- a/packages/api/main.js +++ b/packages/api/main.js @@ -6,10 +6,10 @@ const { cookieToJson } = require('./util/index') let obj = {} fs.readdirSync(path.join(__dirname, 'module')) .reverse() - .forEach((file) => { + .forEach(file => { if (!file.endsWith('.js')) return let fileModule = require(path.join(__dirname, 'module', file)) - obj[file.split('.').shift()] = function (data) { + obj[file.split('.').shift()] = function(data) { if (typeof data.cookie === 'string') { data.cookie = cookieToJson(data.cookie) } diff --git a/packages/api/module/batch.js b/packages/api/module/batch.js index 2e950700..93937e8e 100644 --- a/packages/api/module/batch.js +++ b/packages/api/module/batch.js @@ -4,7 +4,7 @@ module.exports = (query, request) => { const data = { e_r: true, } - Object.keys(query).forEach((i) => { + Object.keys(query).forEach(i => { if (/^\/api\//.test(i)) { data[i] = query[i] } diff --git a/packages/api/module/check_music.js b/packages/api/module/check_music.js index 0e32ef9d..76361cab 100644 --- a/packages/api/module/check_music.js +++ b/packages/api/module/check_music.js @@ -15,7 +15,7 @@ module.exports = (query, request) => { proxy: query.proxy, realIP: query.realIP, }, - ).then((response) => { + ).then(response => { let playable = false if (response.body.code == 200) { if (response.body.data[0].code == 200) { diff --git a/packages/api/module/like.js b/packages/api/module/like.js index 03b60bab..8b4bc61a 100644 --- a/packages/api/module/like.js +++ b/packages/api/module/like.js @@ -9,9 +9,8 @@ module.exports = (query, request) => { } return request( 'POST', - `https://music.163.com/weapi/radio/like?alg=${ - query.alg || 'itembased' - }&trackId=${query.id}&time=${query.time || 25}`, + `https://music.163.com/weapi/radio/like?alg=${query.alg || + 'itembased'}&trackId=${query.id}&time=${query.time || 25}`, data, { crypto: 'weapi', diff --git a/packages/api/module/login.js b/packages/api/module/login.js index 44787ce5..423f6e5a 100644 --- a/packages/api/module/login.js +++ b/packages/api/module/login.js @@ -8,7 +8,10 @@ module.exports = async (query, request) => { username: query.email, password: query.md5_password || - crypto.createHash('md5').update(query.password).digest('hex'), + crypto + .createHash('md5') + .update(query.password) + .digest('hex'), rememberLogin: 'true', } let result = await request( diff --git a/packages/api/module/login_cellphone.js b/packages/api/module/login_cellphone.js index 0d2e7aed..5442a2d5 100644 --- a/packages/api/module/login_cellphone.js +++ b/packages/api/module/login_cellphone.js @@ -9,7 +9,10 @@ module.exports = async (query, request) => { countrycode: query.countrycode || '86', password: query.md5_password || - crypto.createHash('md5').update(query.password).digest('hex'), + crypto + .createHash('md5') + .update(query.password) + .digest('hex'), rememberLogin: 'true', } let result = await request( diff --git a/packages/api/module/login_status.js b/packages/api/module/login_status.js index 3a38665c..367de88c 100644 --- a/packages/api/module/login_status.js +++ b/packages/api/module/login_status.js @@ -6,7 +6,7 @@ module.exports = (query, request) => { `https://music.163.com`, {}, { cookie: query.cookie, proxy: query.proxy, realIP: query.realIP }, - ).then((response) => { + ).then(response => { try { let profile = eval(`(${/GUser\s*=\s*([^;]+);/.exec(response.body)[1]})`) let bindings = eval(`(${/GBinds\s*=\s*([^;]+);/.exec(response.body)[1]})`) diff --git a/packages/api/module/playlist_track_add.js b/packages/api/module/playlist_track_add.js index c2391f3c..b31c6a28 100644 --- a/packages/api/module/playlist_track_add.js +++ b/packages/api/module/playlist_track_add.js @@ -4,7 +4,7 @@ module.exports = async (query, request) => { const data = { id: query.pid, tracks: JSON.stringify( - query.ids.split(',').map((item) => { + query.ids.split(',').map(item => { return { type: 3, id: item } }), ), diff --git a/packages/api/module/playlist_track_delete.js b/packages/api/module/playlist_track_delete.js index 54ef409a..2adb8b27 100644 --- a/packages/api/module/playlist_track_delete.js +++ b/packages/api/module/playlist_track_delete.js @@ -6,7 +6,7 @@ module.exports = async (query, request) => { const data = { id: query.id, tracks: JSON.stringify( - query.ids.split(',').map((item) => { + query.ids.split(',').map(item => { return { type: 3, id: item } }), ), diff --git a/packages/api/module/register_cellphone.js b/packages/api/module/register_cellphone.js index 602e0fad..3386f5b3 100644 --- a/packages/api/module/register_cellphone.js +++ b/packages/api/module/register_cellphone.js @@ -6,7 +6,10 @@ module.exports = (query, request) => { const data = { captcha: query.captcha, phone: query.phone, - password: crypto.createHash('md5').update(query.password).digest('hex'), + password: crypto + .createHash('md5') + .update(query.password) + .digest('hex'), nickname: query.nickname, } return request('POST', `https://music.163.com/api/register/cellphone`, data, { diff --git a/packages/api/module/related_playlist.js b/packages/api/module/related_playlist.js index 6f64b3d4..34c8aa2f 100644 --- a/packages/api/module/related_playlist.js +++ b/packages/api/module/related_playlist.js @@ -11,7 +11,7 @@ module.exports = (query, request) => { proxy: query.proxy, realIP: query.realIP, }, - ).then((response) => { + ).then(response => { try { const pattern = /
[\s\S]*?[\s\S]*?]*>([^<]+?)<\/a>[\s\S]*?]*>([^<]+?)<\/a>/g let result, diff --git a/packages/api/module/song_detail.js b/packages/api/module/song_detail.js index 0a7d5068..17ebe55e 100644 --- a/packages/api/module/song_detail.js +++ b/packages/api/module/song_detail.js @@ -3,7 +3,7 @@ module.exports = (query, request) => { query.ids = query.ids.split(/\s*,\s*/) const data = { - c: '[' + query.ids.map((id) => '{"id":' + id + '}').join(',') + ']', + c: '[' + query.ids.map(id => '{"id":' + id + '}').join(',') + ']', ids: '[' + query.ids.join(',') + ']', } return request('POST', `https://music.163.com/weapi/v3/song/detail`, data, { diff --git a/packages/api/util/apicache.js b/packages/api/util/apicache.js index a71c9987..7b50a97a 100644 --- a/packages/api/util/apicache.js +++ b/packages/api/util/apicache.js @@ -13,19 +13,19 @@ var t = { var instances = [] -var matches = function (a) { - return function (b) { +var matches = function(a) { + return function(b) { return a === b } } -var doesntMatch = function (a) { - return function (b) { +var doesntMatch = function(a) { + return function(b) { return !matches(a)(b) } } -var logDuration = function (d, prefix) { +var logDuration = function(d, prefix) { var str = d > 1000 ? (d / 1000).toFixed(2) + 'sec' : d + 'ms' return '\x1b[33m- ' + (prefix ? prefix + ' ' : '') + str + '\x1b[0m' } @@ -68,7 +68,7 @@ function ApiCache() { this.id = instances.length function debug(a, b, c, d) { - var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function (arg) { + var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function(arg) { return arg !== undefined }) var debugEnv = @@ -118,10 +118,10 @@ function ApiCache() { function filterBlacklistedHeaders(headers) { return Object.keys(headers) - .filter(function (key) { + .filter(function(key) { return globalOptions.headerBlacklist.indexOf(key) === -1 }) - .reduce(function (acc, header) { + .reduce(function(acc, header) { acc[header] = headers[header] return acc }, {}) @@ -145,7 +145,7 @@ function ApiCache() { try { redis.hset(key, 'response', JSON.stringify(value)) redis.hset(key, 'duration', duration) - redis.expire(key, duration / 1000, expireCallback || function () {}) + redis.expire(key, duration / 1000, expireCallback || function() {}) } catch (err) { debug('[apicache] error in redis.hset()') } @@ -154,7 +154,7 @@ function ApiCache() { } // add automatic cache clearing from duration, includes max limit on setTimeout - timers[key] = setTimeout(function () { + timers[key] = setTimeout(function() { instance.clear(key, true) }, Math.min(duration, 2147483647)) } @@ -205,11 +205,11 @@ function ApiCache() { } // append header overwrites if applicable - Object.keys(globalOptions.headers).forEach(function (name) { + Object.keys(globalOptions.headers).forEach(function(name) { res.setHeader(name, globalOptions.headers[name]) }) - res.writeHead = function () { + res.writeHead = function() { // add cache control headers if (!globalOptions.headers['cache-control']) { if (shouldCacheResponse(req, res, toggle)) { @@ -227,13 +227,13 @@ function ApiCache() { } // patch res.write - res.write = function (content) { + res.write = function(content) { accumulateContent(res, content) return res._apicache.write.apply(this, arguments) } // patch res.end - res.end = function (content, encoding) { + res.end = function(content, encoding) { if (shouldCacheResponse(req, res, toggle)) { accumulateContent(res, content) @@ -332,14 +332,14 @@ function ApiCache() { } } - this.clear = function (target, isAutomatic) { + this.clear = function(target, isAutomatic) { var group = index.groups[target] var redis = globalOptions.redisClient if (group) { debug('clearing group "' + target + '"') - group.forEach(function (key) { + group.forEach(function(key) { debug('clearing cached entry for "' + key + '"') clearTimeout(timers[key]) delete timers[key] @@ -381,7 +381,7 @@ function ApiCache() { index.all = index.all.filter(doesntMatch(target)) // remove target from each group that it may exist in - Object.keys(index.groups).forEach(function (groupName) { + Object.keys(index.groups).forEach(function(groupName) { index.groups[groupName] = index.groups[groupName].filter( doesntMatch(target), ) @@ -398,7 +398,7 @@ function ApiCache() { memCache.clear() } else { // clear redis keys one by one from internal index to prevent clearing non-apicache entries - index.all.forEach(function (key) { + index.all.forEach(function(key) { clearTimeout(timers[key]) delete timers[key] try { @@ -434,7 +434,7 @@ function ApiCache() { return defaultDuration } - this.getDuration = function (duration) { + this.getDuration = function(duration) { return parseDuration(duration, globalOptions.defaultDuration) } @@ -446,13 +446,13 @@ function ApiCache() { * }) * */ - this.getPerformance = function () { - return performanceArray.map(function (p) { + this.getPerformance = function() { + return performanceArray.map(function(p) { return p.report() }) } - this.getIndex = function (group) { + this.getIndex = function(group) { if (group) { return index.groups[group] } else { @@ -472,9 +472,9 @@ function ApiCache() { options: opt, }) - var options = function (localOptions) { + var options = function(localOptions) { if (localOptions) { - middlewareOptions.find(function (middleware) { + middlewareOptions.find(function(middleware) { return middleware.options === opt }).localOptions = localOptions } @@ -490,7 +490,7 @@ function ApiCache() { * A Function for non tracking performance */ function NOOPCachePerformance() { - this.report = this.hit = this.miss = function () {} // noop; + this.report = this.hit = this.miss = function() {} // noop; } /** @@ -544,7 +544,7 @@ function ApiCache() { /** * Return performance statistics */ - this.report = function () { + this.report = function() { return { lastCacheHit: this.lastCacheHit, lastCacheMiss: this.lastCacheMiss, @@ -564,7 +564,7 @@ function ApiCache() { * @param {Uint8Array} array An array representing hits and misses. * @returns a number between 0 and 1, or null if the array has no hits or misses */ - this.hitRate = function (array) { + this.hitRate = function(array) { var hits = 0 var misses = 0 for (var i = 0; i < array.length; i++) { @@ -597,7 +597,7 @@ function ApiCache() { * 01 encodes a hit * 10 encodes a miss */ - this.recordHitInArray = function (array, hit) { + this.recordHitInArray = function(array, hit) { var arrayIndex = ~~(this.callCount / 4) % array.length var bitOffset = (this.callCount % 4) * 2 // 2 bits per record, 4 records per uint8 array element var clearMask = ~(3 << bitOffset) @@ -609,7 +609,7 @@ function ApiCache() { * Records the hit or miss in the tracking arrays and increments the call count. * @param {boolean} hit true records a hit, false records a miss */ - this.recordHit = function (hit) { + this.recordHit = function(hit) { this.recordHitInArray(this.hitsLast100, hit) this.recordHitInArray(this.hitsLast1000, hit) this.recordHitInArray(this.hitsLast10000, hit) @@ -622,7 +622,7 @@ function ApiCache() { * Records a hit event, setting lastCacheMiss to the given key * @param {string} key The key that had the cache hit */ - this.hit = function (key) { + this.hit = function(key) { this.recordHit(true) this.lastCacheHit = key } @@ -631,7 +631,7 @@ function ApiCache() { * Records a miss event, setting lastCacheMiss to the given key * @param {string} key The key that had the cache miss */ - this.miss = function (key) { + this.miss = function(key) { this.recordHit(false) this.lastCacheMiss = key } @@ -643,7 +643,7 @@ function ApiCache() { performanceArray.push(perf) - var cache = function (req, res, next) { + var cache = function(req, res, next) { function bypass() { debug('bypass detected, skipping cache.') return next() @@ -714,7 +714,7 @@ function ApiCache() { // send if cache hit from redis if (redis && redis.connected) { try { - redis.hgetall(key, function (err, obj) { + redis.hgetall(key, function(err, obj) { if (!err && obj && obj.response) { var elapsed = new Date() - req.apicacheTimer debug( @@ -777,7 +777,7 @@ function ApiCache() { return cache } - this.options = function (options) { + this.options = function(options) { if (options) { Object.assign(globalOptions, options) syncOptions() @@ -802,14 +802,14 @@ function ApiCache() { } } - this.resetIndex = function () { + this.resetIndex = function() { index = { all: [], groups: {}, } } - this.newInstance = function (config) { + this.newInstance = function(config) { var instance = new ApiCache() if (config) { @@ -819,7 +819,7 @@ function ApiCache() { return instance } - this.clone = function () { + this.clone = function() { return this.newInstance(this.options()) } diff --git a/packages/api/util/crypto.js b/packages/api/util/crypto.js index 3f99e858..1601851f 100644 --- a/packages/api/util/crypto.js +++ b/packages/api/util/crypto.js @@ -20,11 +20,11 @@ const rsaEncrypt = (buffer, key) => { ) } -const weapi = (object) => { +const weapi = object => { const text = JSON.stringify(object) const secretKey = crypto .randomBytes(16) - .map((n) => base62.charAt(n % 62).charCodeAt()) + .map(n => base62.charAt(n % 62).charCodeAt()) return { params: aesEncrypt( Buffer.from( @@ -38,7 +38,7 @@ const weapi = (object) => { } } -const linuxapi = (object) => { +const linuxapi = object => { const text = JSON.stringify(object) return { eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '') @@ -50,7 +50,10 @@ const linuxapi = (object) => { const eapi = (url, object) => { const text = typeof object === 'object' ? JSON.stringify(object) : object const message = `nobody${url}use${text}md5forencrypt` - const digest = crypto.createHash('md5').update(message).digest('hex') + const digest = crypto + .createHash('md5') + .update(message) + .digest('hex') const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}` return { params: aesEncrypt(Buffer.from(data), 'ecb', eapiKey, '') @@ -59,7 +62,7 @@ const eapi = (url, object) => { } } -const decrypt = (cipherBuffer) => { +const decrypt = cipherBuffer => { const decipher = crypto.createDecipheriv('aes-128-ecb', eapiKey, '') return Buffer.concat([decipher.update(cipherBuffer), decipher.final()]) } diff --git a/packages/api/util/index.js b/packages/api/util/index.js index 06aade52..89334780 100644 --- a/packages/api/util/index.js +++ b/packages/api/util/index.js @@ -8,7 +8,7 @@ module.exports = { if (!cookie) return {} let cookieArr = cookie.split(';') let obj = {} - cookieArr.forEach((i) => { + cookieArr.forEach(i => { let arr = i.split('=') obj[arr[0]] = arr[1] }) diff --git a/packages/api/util/memory-cache.js b/packages/api/util/memory-cache.js index 6b6deaa2..4030c7a4 100644 --- a/packages/api/util/memory-cache.js +++ b/packages/api/util/memory-cache.js @@ -3,14 +3,14 @@ function MemoryCache() { this.size = 0 } -MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { +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 () { + timeout: setTimeout(function() { instance.delete(key) return ( timeoutCallback && @@ -26,7 +26,7 @@ MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { return entry } -MemoryCache.prototype.delete = function (key) { +MemoryCache.prototype.delete = function(key) { var entry = this.cache[key] if (entry) { @@ -40,20 +40,20 @@ MemoryCache.prototype.delete = function (key) { return null } -MemoryCache.prototype.get = function (key) { +MemoryCache.prototype.get = function(key) { var entry = this.cache[key] return entry } -MemoryCache.prototype.getValue = function (key) { +MemoryCache.prototype.getValue = function(key) { var entry = this.get(key) return entry && entry.value } -MemoryCache.prototype.clear = function () { - Object.keys(this.cache).forEach(function (key) { +MemoryCache.prototype.clear = function() { + Object.keys(this.cache).forEach(function(key) { this.delete(key) }, this) diff --git a/packages/api/util/request.js b/packages/api/util/request.js index c7187a36..135c1517 100644 --- a/packages/api/util/request.js +++ b/packages/api/util/request.js @@ -54,7 +54,7 @@ const createRequest = (method, url, data, options) => { if (typeof options.cookie === 'object') headers['Cookie'] = Object.keys(options.cookie) .map( - (key) => + key => encodeURIComponent(key) + '=' + encodeURIComponent(options.cookie[key]), @@ -88,7 +88,11 @@ const createRequest = (method, url, data, options) => { appver: cookie.appver || '6.1.1', // app版本 versioncode: cookie.versioncode || '140', //版本号 mobilename: cookie.mobilename, //设备model - buildver: cookie.buildver || Date.now().toString().substr(0, 10), + buildver: + cookie.buildver || + Date.now() + .toString() + .substr(0, 10), resolution: cookie.resolution || '1920x1080', //设备分辨率 __csrf: csrfToken, os: cookie.os || 'android', @@ -101,7 +105,7 @@ const createRequest = (method, url, data, options) => { if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A headers['Cookie'] = Object.keys(header) .map( - (key) => + key => encodeURIComponent(key) + '=' + encodeURIComponent(header[key]), ) .join('; ') @@ -145,9 +149,9 @@ const createRequest = (method, url, data, options) => { } axios(settings) - .then((res) => { + .then(res => { const body = res.data - answer.cookie = (res.headers['set-cookie'] || []).map((x) => + answer.cookie = (res.headers['set-cookie'] || []).map(x => x.replace(/\s*Domain=[^(;|$)]+;*/, ''), ) try { @@ -166,7 +170,7 @@ const createRequest = (method, url, data, options) => { if (answer.status == 200) resolve(answer) else reject(answer) }) - .catch((err) => { + .catch(err => { answer.status = 502 answer.body = { code: 502, msg: err } reject(answer) diff --git a/packages/unblock/app.js b/packages/unblock/app.js index 2fc26a5e..f0d33cac 100644 --- a/packages/unblock/app.js +++ b/packages/unblock/app.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('./src/app') \ No newline at end of file +require('./src/app') diff --git a/packages/unblock/bridge.js b/packages/unblock/bridge.js index 246fdbf0..c7e07d43 100644 --- a/packages/unblock/bridge.js +++ b/packages/unblock/bridge.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('./src/bridge') \ No newline at end of file +require('./src/bridge') diff --git a/packages/unblock/endpoint.worker.js b/packages/unblock/endpoint.worker.js index 150cdad2..f6df5a60 100644 --- a/packages/unblock/endpoint.worker.js +++ b/packages/unblock/endpoint.worker.js @@ -10,11 +10,15 @@ const handleRequest = async request => { const [matched, base64Url, fileName] = pattern.exec(path || '') || [] if (!matched) return notFound let url = base64Url.replace(/-/g, '+').replace(/_/g, '/') - try { url = new URL(atob(url)) } catch(_) { url = null } + try { + url = new URL(atob(url)) + } catch (_) { + url = null + } if (!url) return notFound const headers = new Headers(request.headers) headers.set('host', url.host) headers.delete('cookie') const { method, body } = request return fetch(url, { method, headers, body }) -} \ No newline at end of file +} diff --git a/packages/unblock/src/app.js b/packages/unblock/src/app.js index 9f69d619..4dc52a6d 100644 --- a/packages/unblock/src/app.js +++ b/packages/unblock/src/app.js @@ -2,54 +2,88 @@ const package = require('../package.json') const config = require('./cli.js') -.program({name: package.name.replace(/@.+\//, ''), version: package.version}) -.option(['-v', '--version'], {action: 'version'}) -.option(['-p', '--port'], {metavar: 'port', help: 'specify server port'}) -.option(['-a', '--address'], {metavar: 'address', help: 'specify server host'}) -.option(['-u', '--proxy-url'], {metavar: 'url', help: 'request through upstream proxy'}) -.option(['-f', '--force-host'], {metavar: 'host', help: 'force the netease server ip'}) -.option(['-o', '--match-order'], {metavar: 'source', nargs: '+', help: 'set priority of sources'}) -.option(['-t', '--token'], {metavar: 'token', help: 'set up proxy authentication'}) -.option(['-e', '--endpoint'], {metavar: 'url', help: 'replace virtual endpoint with public host'}) -.option(['-s', '--strict'], {action: 'store_true', help: 'enable proxy limitation'}) -.option(['-h', '--help'], {action: 'help'}) -.parse(process.argv) + .program({ + name: package.name.replace(/@.+\//, ''), + version: package.version + }) + .option(['-v', '--version'], { action: 'version' }) + .option(['-p', '--port'], { metavar: 'port', help: 'specify server port' }) + .option(['-a', '--address'], { + metavar: 'address', + help: 'specify server host' + }) + .option(['-u', '--proxy-url'], { + metavar: 'url', + help: 'request through upstream proxy' + }) + .option(['-f', '--force-host'], { + metavar: 'host', + help: 'force the netease server ip' + }) + .option(['-o', '--match-order'], { + metavar: 'source', + nargs: '+', + help: 'set priority of sources' + }) + .option(['-t', '--token'], { + metavar: 'token', + help: 'set up proxy authentication' + }) + .option(['-e', '--endpoint'], { + metavar: 'url', + help: 'replace virtual endpoint with public host' + }) + .option(['-s', '--strict'], { + action: 'store_true', + help: 'enable proxy limitation' + }) + .option(['-h', '--help'], { action: 'help' }) + .parse(process.argv) global.address = config.address config.port = (config.port || '8080').split(':').map(string => parseInt(string)) -const invalid = value => (isNaN(value) || value < 1 || value > 65535) +const invalid = value => isNaN(value) || value < 1 || value > 65535 if (config.port.some(invalid)) { - console.log('Port must be a number higher than 0 and lower than 65535.') - process.exit(1) + console.log('Port must be a number higher than 0 and lower than 65535.') + process.exit(1) } if (config.proxyUrl && !/http(s?):\/\/.+:\d+/.test(config.proxyUrl)) { - console.log('Please check the proxy url.') - process.exit(1) + console.log('Please check the proxy url.') + process.exit(1) } if (config.endpoint && !/http(s?):\/\/.+/.test(config.endpoint)) { - console.log('Please check the endpoint host.') - process.exit(1) + console.log('Please check the endpoint host.') + process.exit(1) } if (config.forceHost && require('net').isIP(config.forceHost) === 0) { - console.log('Please check the server host.') - process.exit(1) + console.log('Please check the server host.') + process.exit(1) } if (config.matchOrder) { - const provider = new Set(['netease', 'qq', 'xiami', 'baidu', 'kugou', 'kuwo', 'migu', 'joox', 'youtube']) - const candidate = config.matchOrder - if (candidate.some((key, index) => index != candidate.indexOf(key))) { - console.log('Please check the duplication in match order.') - process.exit(1) - } - else if (candidate.some(key => !provider.has(key))) { - console.log('Please check the availability of match sources.') - process.exit(1) - } - global.source = candidate + const provider = new Set([ + 'netease', + 'qq', + 'xiami', + 'baidu', + 'kugou', + 'kuwo', + 'migu', + 'joox', + 'youtube' + ]) + const candidate = config.matchOrder + if (candidate.some((key, index) => index != candidate.indexOf(key))) { + console.log('Please check the duplication in match order.') + process.exit(1) + } else if (candidate.some(key => !provider.has(key))) { + console.log('Please check the availability of match sources.') + process.exit(1) + } + global.source = candidate } if (config.token && !/\S+:\S+/.test(config.token)) { - console.log('Please check the authentication token.') - process.exit(1) + console.log('Please check the authentication token.') + process.exit(1) } const parse = require('url').parse @@ -60,7 +94,10 @@ const target = Array.from(hook.target.host) global.port = config.port global.proxy = config.proxyUrl ? parse(config.proxyUrl) : null -global.hosts = target.reduce((result, host) => Object.assign(result, {[host]: config.forceHost}), {}) +global.hosts = target.reduce( + (result, host) => Object.assign(result, { [host]: config.forceHost }), + {} +) server.whitelist = ['://[\\w.]*music\\.126\\.net', '://[\\w.]*vod\\.126\\.net'] if (config.strict) server.blacklist.push('.*') server.authentication = config.token || null @@ -70,17 +107,47 @@ if (config.endpoint) server.whitelist.push(escape(config.endpoint)) hosts['music.httpdns.c.163.com'] = random(['59.111.181.35', '59.111.181.38']) hosts['httpdns.n.netease.com'] = random(['59.111.179.213', '59.111.179.214']) -const dns = host => new Promise((resolve, reject) => require('dns').lookup(host, {all: true}, (error, records) => error ? reject(error) : resolve(records.map(record => record.address)))) -const httpdns = host => require('./request')('POST', 'https://music.httpdns.c.163.com/d', {}, host).then(response => response.json()).then(jsonBody => jsonBody.dns.reduce((result, domain) => result.concat(domain.ips), [])) -const httpdns2 = host => require('./request')('GET', 'https://httpdns.n.netease.com/httpdns/v2/d?domain=' + host).then(response => response.json()).then(jsonBody => Object.keys(jsonBody.data).map(key => jsonBody.data[key]).reduce((result, value) => result.concat(value.ip || []), [])) +const dns = host => + new Promise((resolve, reject) => + require('dns').lookup(host, { all: true }, (error, records) => + error ? reject(error) : resolve(records.map(record => record.address)) + ) + ) +const httpdns = host => + require('./request')('POST', 'https://music.httpdns.c.163.com/d', {}, host) + .then(response => response.json()) + .then(jsonBody => + jsonBody.dns.reduce((result, domain) => result.concat(domain.ips), []) + ) +const httpdns2 = host => + require('./request')( + 'GET', + 'https://httpdns.n.netease.com/httpdns/v2/d?domain=' + host + ) + .then(response => response.json()) + .then(jsonBody => + Object.keys(jsonBody.data) + .map(key => jsonBody.data[key]) + .reduce((result, value) => result.concat(value.ip || []), []) + ) -Promise.all([httpdns, httpdns2].map(query => query(target.join(','))).concat(target.map(dns))) -.then(result => { - const {host} = hook.target - result.forEach(array => array.forEach(host.add, host)) - server.whitelist = server.whitelist.concat(Array.from(host).map(escape)) - const log = type => console.log(`${['HTTP', 'HTTPS'][type]} Server running @ http://${address || '0.0.0.0'}:${port[type]}`) - if (port[0]) server.http.listen(port[0], address).once('listening', () => log(0)) - if (port[1]) server.https.listen(port[1], address).once('listening', () => log(1)) -}) -.catch(error => console.log(error)) \ No newline at end of file +Promise.all( + [httpdns, httpdns2] + .map(query => query(target.join(','))) + .concat(target.map(dns)) +) + .then(result => { + const { host } = hook.target + result.forEach(array => array.forEach(host.add, host)) + server.whitelist = server.whitelist.concat(Array.from(host).map(escape)) + const log = type => + console.log( + `${['HTTP', 'HTTPS'][type]} Server running @ http://${address || + '0.0.0.0'}:${port[type]}` + ) + if (port[0]) + server.http.listen(port[0], address).once('listening', () => log(0)) + if (port[1]) + server.https.listen(port[1], address).once('listening', () => log(1)) + }) + .catch(error => console.log(error)) diff --git a/packages/unblock/src/bridge.js b/packages/unblock/src/bridge.js index 7be3b4c1..da206be1 100644 --- a/packages/unblock/src/bridge.js +++ b/packages/unblock/src/bridge.js @@ -4,35 +4,41 @@ const parse = require('url').parse require('./provider/insure').disable = true const router = { - qq: require('./provider/qq'), - xiami: require('./provider/xiami'), - baidu: require('./provider/baidu'), - kugou: require('./provider/kugou'), - kuwo: require('./provider/kuwo'), - migu: require('./provider/migu'), - joox: require('./provider/joox') + qq: require('./provider/qq'), + xiami: require('./provider/xiami'), + baidu: require('./provider/baidu'), + kugou: require('./provider/kugou'), + kuwo: require('./provider/kuwo'), + migu: require('./provider/migu'), + joox: require('./provider/joox') } const distribute = (url, router) => - Promise.resolve() - .then(() => { - const route = url.pathname.slice(1).split('/').map(path => decodeURIComponent(path)) - let pointer = router, argument = decodeURIComponent(url.query) - try {argument = JSON.parse(argument)} catch(e) {} - const miss = route.some(path => { - if (path in pointer) pointer = pointer[path] - else return true - }) - if (miss || typeof pointer != 'function') return Promise.reject() - // return pointer.call(null, argument) - return cache(pointer, argument, 15 * 60 * 1000) - }) + Promise.resolve().then(() => { + const route = url.pathname + .slice(1) + .split('/') + .map(path => decodeURIComponent(path)) + let pointer = router, + argument = decodeURIComponent(url.query) + try { + argument = JSON.parse(argument) + } catch (e) {} + const miss = route.some(path => { + if (path in pointer) pointer = pointer[path] + else return true + }) + if (miss || typeof pointer != 'function') return Promise.reject() + // return pointer.call(null, argument) + return cache(pointer, argument, 15 * 60 * 1000) + }) -require('http').createServer() -.listen(parseInt(process.argv[2]) || 9000) -.on('request', (req, res) => - distribute(parse(req.url), router) - .then(data => res.write(data)) - .catch(() => res.writeHead(404)) - .then(() => res.end()) -) \ No newline at end of file +require('http') + .createServer() + .listen(parseInt(process.argv[2]) || 9000) + .on('request', (req, res) => + distribute(parse(req.url), router) + .then(data => res.write(data)) + .catch(() => res.writeHead(404)) + .then(() => res.end()) + ) diff --git a/packages/unblock/src/browser/background.js b/packages/unblock/src/browser/background.js index 650e8e5c..2ef6796c 100644 --- a/packages/unblock/src/browser/background.js +++ b/packages/unblock/src/browser/background.js @@ -1,34 +1,56 @@ import match from './provider/match.js' const self = chrome.runtime.id -chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => { - match(request.match, ['qq']) - .then(song => sendResponse(song)) - .catch(console.log) - return true -}) +chrome.runtime.onMessageExternal.addListener( + (request, sender, sendResponse) => { + match(request.match, ['qq']) + .then(song => sendResponse(song)) + .catch(console.log) + return true + } +) -chrome.webRequest.onBeforeSendHeaders.addListener(details => { - let headers = details.requestHeaders - if(details.url.includes('//music.163.com/')){ - headers.push({name: 'X-Real-IP', value: '118.88.88.88'}) - } - if(details.initiator == `chrome-extension://${self}`){ - let index = headers.findIndex(item => item.name.toLowerCase() === 'additional-headers') - if(index === -1) return - Object.entries(JSON.parse(atob(headers[index].value))).forEach(entry => headers.push({name: entry[0], value: entry[1]})) - headers.splice(index, 1) - } - if(details.initiator == 'https://music.163.com' && (details.type == 'media' || details.url.includes('.mp3'))){ - headers = headers.filter(item => !['referer', 'origin'].includes(item.name.toLowerCase())) - } - return {requestHeaders: headers} -}, {urls: ['*://*/*']}, ['blocking', 'requestHeaders', 'extraHeaders']) +chrome.webRequest.onBeforeSendHeaders.addListener( + details => { + let headers = details.requestHeaders + if (details.url.includes('//music.163.com/')) { + headers.push({ name: 'X-Real-IP', value: '118.88.88.88' }) + } + if (details.initiator == `chrome-extension://${self}`) { + let index = headers.findIndex( + item => item.name.toLowerCase() === 'additional-headers' + ) + if (index === -1) return + Object.entries(JSON.parse(atob(headers[index].value))).forEach(entry => + headers.push({ name: entry[0], value: entry[1] }) + ) + headers.splice(index, 1) + } + if ( + details.initiator == 'https://music.163.com' && + (details.type == 'media' || details.url.includes('.mp3')) + ) { + headers = headers.filter( + item => !['referer', 'origin'].includes(item.name.toLowerCase()) + ) + } + return { requestHeaders: headers } + }, + { urls: ['*://*/*'] }, + ['blocking', 'requestHeaders', 'extraHeaders'] +) -chrome.webRequest.onHeadersReceived.addListener(details => { - let headers = details.responseHeaders - if(details.initiator == 'https://music.163.com' && (details.type == 'media' || details.url.includes('.mp3'))){ - headers.push({name: 'Access-Control-Allow-Origin', value: '*'}) - } - return {responseHeaders: headers} -}, {urls: ['*://*/*']}, ['blocking', 'responseHeaders']) \ No newline at end of file +chrome.webRequest.onHeadersReceived.addListener( + details => { + let headers = details.responseHeaders + if ( + details.initiator == 'https://music.163.com' && + (details.type == 'media' || details.url.includes('.mp3')) + ) { + headers.push({ name: 'Access-Control-Allow-Origin', value: '*' }) + } + return { responseHeaders: headers } + }, + { urls: ['*://*/*'] }, + ['blocking', 'responseHeaders'] +) diff --git a/packages/unblock/src/browser/convert.js b/packages/unblock/src/browser/convert.js index fc7e8d8b..79d85dc4 100644 --- a/packages/unblock/src/browser/convert.js +++ b/packages/unblock/src/browser/convert.js @@ -2,42 +2,62 @@ const fs = require('fs') const path = require('path') const importReplacer = (match, state, alias, file) => { - file = file + (file.endsWith('.js') ? '' : '.js') - return `import ${alias} from '${file}'` + file = file + (file.endsWith('.js') ? '' : '.js') + return `import ${alias} from '${file}'` } const converter = (input, output, processor) => { - let data = fs.readFileSync(input).toString() - if(processor){ - data = processor(data) - } - else{ - data = data.replace(/global\./g, 'window.') - data = data.replace(/(const|let|var)\s+(\w+)\s*=\s*require\(\s*['|"](.+)['|"]\s*\)/g, importReplacer) - data = data.replace(/module\.exports\s*=\s*/g, 'export default ') - } - fs.writeFileSync(output, data) + let data = fs.readFileSync(input).toString() + if (processor) { + data = processor(data) + } else { + data = data.replace(/global\./g, 'window.') + data = data.replace( + /(const|let|var)\s+(\w+)\s*=\s*require\(\s*['|"](.+)['|"]\s*\)/g, + importReplacer + ) + data = data.replace(/module\.exports\s*=\s*/g, 'export default ') + } + fs.writeFileSync(output, data) } -converter(path.resolve(__dirname, '..', 'cache.js'), path.resolve(__dirname, '.', 'cache.js')) +converter( + path.resolve(__dirname, '..', 'cache.js'), + path.resolve(__dirname, '.', 'cache.js') +) -if(!fs.existsSync(path.resolve(__dirname, 'provider'))) fs.mkdirSync(path.resolve(__dirname, 'provider')) +if (!fs.existsSync(path.resolve(__dirname, 'provider'))) + fs.mkdirSync(path.resolve(__dirname, 'provider')) -fs.readdirSync(path.resolve(__dirname, '..', 'provider')).filter(file => !file.includes('test')).forEach(file => { - converter(path.resolve(__dirname, '..', 'provider', file), path.resolve(__dirname, 'provider', file)) -}) +fs.readdirSync(path.resolve(__dirname, '..', 'provider')) + .filter(file => !file.includes('test')) + .forEach(file => { + converter( + path.resolve(__dirname, '..', 'provider', file), + path.resolve(__dirname, 'provider', file) + ) + }) const providerReplacer = (match, state, data) => { - let provider = [] - let imports = data.match(/\w+\s*:\s*require\(['|"].+['|"]\)/g).map(line => { - line = line.match(/(\w+)\s*:\s*require\(['|"](.+)['|"]\)/) - provider.push(line[1]) - return importReplacer(null, null, line[1], line[2]) - }) - return imports.join('\n') + '\n\n' + `${state} provider = {${provider.join(', ')}}` + let provider = [] + let imports = data.match(/\w+\s*:\s*require\(['|"].+['|"]\)/g).map(line => { + line = line.match(/(\w+)\s*:\s*require\(['|"](.+)['|"]\)/) + provider.push(line[1]) + return importReplacer(null, null, line[1], line[2]) + }) + return ( + imports.join('\n') + '\n\n' + `${state} provider = {${provider.join(', ')}}` + ) } -converter(path.resolve(__dirname, 'provider', 'match.js'), path.resolve(__dirname, 'provider', 'match.js'), data => { - data = data.replace(/(const|let|var)\s+provider\s*=\s*{([^}]+)}/g, providerReplacer) - return data -}) \ No newline at end of file +converter( + path.resolve(__dirname, 'provider', 'match.js'), + path.resolve(__dirname, 'provider', 'match.js'), + data => { + data = data.replace( + /(const|let|var)\s+provider\s*=\s*{([^}]+)}/g, + providerReplacer + ) + return data + } +) diff --git a/packages/unblock/src/browser/crypto.js b/packages/unblock/src/browser/crypto.js index baef8b2d..6870c3b4 100644 --- a/packages/unblock/src/browser/crypto.js +++ b/packages/unblock/src/browser/crypto.js @@ -1,32 +1,53 @@ +const bodyify = object => + Object.entries(object) + .map(entry => entry.map(encodeURIComponent).join('=')) + .join('&') -const bodyify = object => Object.entries(object).map(entry => entry.map(encodeURIComponent).join('=')).join('&') - -const toBuffer = string => (new TextEncoder()).encode(string) -const toHex = arrayBuffer => Array.from(arrayBuffer).map(n => n.toString(16).padStart(2, '0')).join('') +const toBuffer = string => new TextEncoder().encode(string) +const toHex = arrayBuffer => + Array.from(arrayBuffer) + .map(n => n.toString(16).padStart(2, '0')) + .join('') const toBase64 = arrayBuffer => btoa(arrayBuffer) export default { - uri: { - retrieve: id => { - id = id.toString().trim() - const key = '3go8&$8*3*3h0k(2)2' - let string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ key.charCodeAt(index % key.length))).join('') - let result = CryptoJS.MD5(string).toString(CryptoJS.enc.Base64).replace(/\//g, '_').replace(/\+/g, '-') - return `http://p1.music.126.net/${result}/${id}` - } - }, - md5: { - digest: value => CryptoJS.MD5(value).toString() - }, - miguapi: { - encrypt: object => { - let text = JSON.stringify(object), signer = new JSEncrypt() - let password = Array.from(window.crypto.getRandomValues(new Uint8Array(32))).map(n => n.toString(16).padStart(2, '0')).join('') - signer.setPublicKey('-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----') - return bodyify({ - data: CryptoJS.AES.encrypt(text, password).toString(), - secKey: signer.encrypt(password) - }) - } - } -} \ No newline at end of file + uri: { + retrieve: id => { + id = id.toString().trim() + const key = '3go8&$8*3*3h0k(2)2' + let string = Array.from(Array(id.length).keys()) + .map(index => + String.fromCharCode( + id.charCodeAt(index) ^ key.charCodeAt(index % key.length) + ) + ) + .join('') + let result = CryptoJS.MD5(string) + .toString(CryptoJS.enc.Base64) + .replace(/\//g, '_') + .replace(/\+/g, '-') + return `http://p1.music.126.net/${result}/${id}` + } + }, + md5: { + digest: value => CryptoJS.MD5(value).toString() + }, + miguapi: { + encrypt: object => { + let text = JSON.stringify(object), + signer = new JSEncrypt() + let password = Array.from( + window.crypto.getRandomValues(new Uint8Array(32)) + ) + .map(n => n.toString(16).padStart(2, '0')) + .join('') + signer.setPublicKey( + '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----' + ) + return bodyify({ + data: CryptoJS.AES.encrypt(text, password).toString(), + secKey: signer.encrypt(password) + }) + } + } +} diff --git a/packages/unblock/src/browser/inject.js b/packages/unblock/src/browser/inject.js index 5d5e6cfb..6a09be2a 100644 --- a/packages/unblock/src/browser/inject.js +++ b/packages/unblock/src/browser/inject.js @@ -1,73 +1,83 @@ -(() => { - const remote = 'oleomikdicccalekkpcbfgdmpjehnpkp' - const remoteMatch = id => new Promise(resolve => { - chrome.runtime.sendMessage(remote, {match: id}, response => { - resolve(response) - }) - }) +;(() => { + const remote = 'oleomikdicccalekkpcbfgdmpjehnpkp' + const remoteMatch = id => + new Promise(resolve => { + chrome.runtime.sendMessage(remote, { match: id }, response => { + resolve(response) + }) + }) - const waitTimeout = wait => new Promise(resolve => { - setTimeout(() => { - resolve() - }, wait) - }) + const waitTimeout = wait => + new Promise(resolve => { + setTimeout(() => { + resolve() + }, wait) + }) - const searchFunction = (object, keyword) => - Object.keys(object) - .filter(key => object[key] && typeof object[key] == 'function') - .find(key => String(object[key]).match(keyword)) + const searchFunction = (object, keyword) => + Object.keys(object) + .filter(key => object[key] && typeof object[key] == 'function') + .find(key => String(object[key]).match(keyword)) - if(self.frameElement && self.frameElement.tagName == 'IFRAME'){ //in iframe - const keyOne = searchFunction(window.nej.e, '\\.dataset;if') - const keyTwo = searchFunction(window.nm.x, '\\.copyrightId==') - const keyThree = searchFunction(window.nm.x, '\\.privilege;if') - const functionOne = window.nej.e[keyOne] + if (self.frameElement && self.frameElement.tagName == 'IFRAME') { + //in iframe + const keyOne = searchFunction(window.nej.e, '\\.dataset;if') + const keyTwo = searchFunction(window.nm.x, '\\.copyrightId==') + const keyThree = searchFunction(window.nm.x, '\\.privilege;if') + const functionOne = window.nej.e[keyOne] - window.nej.e[keyOne] = (z, name) => { - if (name == 'copyright' || name == 'resCopyright') return 1 - return functionOne(z, name) - } - window.nm.x[keyTwo] = () => false - window.nm.x[keyThree] = song => { - song.status = 0 - if (song.privilege) song.privilege.pl = 320000 - return 0 - } - const table = document.querySelector('table tbody') - if(table) Array.from(table.childNodes) - .filter(element => element.classList.contains('js-dis')) - .forEach(element => element.classList.remove('js-dis')) - } - else{ - const keyAjax = searchFunction(window.nej.j, '\\.replace\\("api","weapi') - const functionAjax = window.nej.j[keyAjax] - window.nej.j[keyAjax] = (url, param) => { - const onload = param.onload - param.onload = data => { - Promise.resolve() - .then(() => { - if(url.includes('enhance/player/url')){ - if(data.data[0].url){ - data.data[0].url = data.data[0].url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net') - } - else{ - return Promise.race([remoteMatch(data.data[0].id), waitTimeout(4000)]) - .then(result => { - if(result){ - data.data[0].code = 200 - data.data[0].br = 320000 - data.data[0].type = 'mp3' - data.data[0].size = result.size - data.data[0].md5 = result.md5 - data.data[0].url = result.url.replace(/http:\/\//, 'https://') - } - }) - } - } - }) - .then(() => onload(data)) - } - functionAjax(url, param) - } - } -})() \ No newline at end of file + window.nej.e[keyOne] = (z, name) => { + if (name == 'copyright' || name == 'resCopyright') return 1 + return functionOne(z, name) + } + window.nm.x[keyTwo] = () => false + window.nm.x[keyThree] = song => { + song.status = 0 + if (song.privilege) song.privilege.pl = 320000 + return 0 + } + const table = document.querySelector('table tbody') + if (table) + Array.from(table.childNodes) + .filter(element => element.classList.contains('js-dis')) + .forEach(element => element.classList.remove('js-dis')) + } else { + const keyAjax = searchFunction(window.nej.j, '\\.replace\\("api","weapi') + const functionAjax = window.nej.j[keyAjax] + window.nej.j[keyAjax] = (url, param) => { + const onload = param.onload + param.onload = data => { + Promise.resolve() + .then(() => { + if (url.includes('enhance/player/url')) { + if (data.data[0].url) { + data.data[0].url = data.data[0].url.replace( + /(m\d+?)(?!c)\.music\.126\.net/, + '$1c.music.126.net' + ) + } else { + return Promise.race([ + remoteMatch(data.data[0].id), + waitTimeout(4000) + ]).then(result => { + if (result) { + data.data[0].code = 200 + data.data[0].br = 320000 + data.data[0].type = 'mp3' + data.data[0].size = result.size + data.data[0].md5 = result.md5 + data.data[0].url = result.url.replace( + /http:\/\//, + 'https://' + ) + } + }) + } + } + }) + .then(() => onload(data)) + } + functionAjax(url, param) + } + } +})() diff --git a/packages/unblock/src/browser/request.js b/packages/unblock/src/browser/request.js index a257a6ba..e64f9fbd 100644 --- a/packages/unblock/src/browser/request.js +++ b/packages/unblock/src/browser/request.js @@ -1,21 +1,45 @@ -export default (method, url, headers, body) => new Promise((resolve, reject) => { - headers = headers || {} - const xhr = new XMLHttpRequest() - xhr.onreadystatechange = () => {if (xhr.readyState == 4) resolve(xhr)} - xhr.onerror = error => reject(error) - xhr.open(method, url, true) - const safe = {}, unsafe = {} - Object.keys(headers).filter(key => (['origin', 'referer'].includes(key.toLowerCase()) ? unsafe : safe)[key] = headers[key]) - Object.entries(safe).forEach(entry => xhr.setRequestHeader.apply(xhr, entry)) - if (Object.keys(unsafe)) xhr.setRequestHeader('Additional-Headers', btoa(JSON.stringify(unsafe))) - xhr.send(body) -}).then(xhr => Object.assign(xhr, { - statusCode: xhr.status, - headers: - xhr.getAllResponseHeaders().split('\r\n').filter(line => line).map(line => line.split(/\s*:\s*/)) - .reduce((result, pair) => Object.assign(result, {[pair[0].toLowerCase()]: pair[1]}), {}), - url: {href: xhr.responseURL}, - body: () => xhr.responseText, - json: () => JSON.parse(xhr.responseText), - jsonp: () => JSON.parse(xhr.responseText.slice(xhr.responseText.indexOf('(') + 1, -')'.length)) -})) \ No newline at end of file +export default (method, url, headers, body) => + new Promise((resolve, reject) => { + headers = headers || {} + const xhr = new XMLHttpRequest() + xhr.onreadystatechange = () => { + if (xhr.readyState == 4) resolve(xhr) + } + xhr.onerror = error => reject(error) + xhr.open(method, url, true) + const safe = {}, + unsafe = {} + Object.keys(headers).filter( + key => + ((['origin', 'referer'].includes(key.toLowerCase()) ? unsafe : safe)[ + key + ] = headers[key]) + ) + Object.entries(safe).forEach(entry => + xhr.setRequestHeader.apply(xhr, entry) + ) + if (Object.keys(unsafe)) + xhr.setRequestHeader('Additional-Headers', btoa(JSON.stringify(unsafe))) + xhr.send(body) + }).then(xhr => + Object.assign(xhr, { + statusCode: xhr.status, + headers: xhr + .getAllResponseHeaders() + .split('\r\n') + .filter(line => line) + .map(line => line.split(/\s*:\s*/)) + .reduce( + (result, pair) => + Object.assign(result, { [pair[0].toLowerCase()]: pair[1] }), + {} + ), + url: { href: xhr.responseURL }, + body: () => xhr.responseText, + json: () => JSON.parse(xhr.responseText), + jsonp: () => + JSON.parse( + xhr.responseText.slice(xhr.responseText.indexOf('(') + 1, -')'.length) + ) + }) + ) diff --git a/packages/unblock/src/browser/script.js b/packages/unblock/src/browser/script.js index 01eb7d84..0094c93c 100644 --- a/packages/unblock/src/browser/script.js +++ b/packages/unblock/src/browser/script.js @@ -1,5 +1,7 @@ -(() => { - let script = (document.head || document.documentElement).appendChild(document.createElement('script')) - script.src = chrome.extension.getURL('inject.js') - script.onload = script.parentNode.removeChild(script) -})() \ No newline at end of file +;(() => { + let script = (document.head || document.documentElement).appendChild( + document.createElement('script') + ) + script.src = chrome.extension.getURL('inject.js') + script.onload = script.parentNode.removeChild(script) +})() diff --git a/packages/unblock/src/cache.js b/packages/unblock/src/cache.js index 0a69371e..815292c8 100644 --- a/packages/unblock/src/cache.js +++ b/packages/unblock/src/cache.js @@ -1,27 +1,32 @@ const collector = (job, cycle) => - setTimeout(() => { - let keep = false - Object.keys(job.cache || {}) - .forEach(key => { - if (!job.cache[key]) return - job.cache[key].expiration < Date.now() - ? job.cache[key] = null - : keep = keep || true - }) - keep ? collector(job, cycle) : job.collector = null - }, cycle) + setTimeout(() => { + let keep = false + Object.keys(job.cache || {}).forEach(key => { + if (!job.cache[key]) return + job.cache[key].expiration < Date.now() + ? (job.cache[key] = null) + : (keep = keep || true) + }) + keep ? collector(job, cycle) : (job.collector = null) + }, cycle) module.exports = (job, parameter, live = 30 * 60 * 1000) => { - const cache = job.cache ? job.cache : job.cache = {} - if (!job.collector) job.collector = collector(job, live / 2) - const key = parameter == null ? 'default' : (typeof(parameter) === 'object' ? (parameter.id || parameter.key || JSON.stringify(parameter)) : parameter) - const done = (status, result) => cache[key].execution = Promise[status](result) - if (!cache[key] || cache[key].expiration < Date.now()) - cache[key] = { - expiration: Date.now() + live, - execution: job(parameter) - .then(result => done('resolve', result)) - .catch(result => done('reject', result)) - } - return cache[key].execution -} \ No newline at end of file + const cache = job.cache ? job.cache : (job.cache = {}) + if (!job.collector) job.collector = collector(job, live / 2) + const key = + parameter == null + ? 'default' + : typeof parameter === 'object' + ? parameter.id || parameter.key || JSON.stringify(parameter) + : parameter + const done = (status, result) => + (cache[key].execution = Promise[status](result)) + if (!cache[key] || cache[key].expiration < Date.now()) + cache[key] = { + expiration: Date.now() + live, + execution: job(parameter) + .then(result => done('resolve', result)) + .catch(result => done('reject', result)) + } + return cache[key].execution +} diff --git a/packages/unblock/src/cli.js b/packages/unblock/src/cli.js index 96b129e5..3ee499e2 100644 --- a/packages/unblock/src/cli.js +++ b/packages/unblock/src/cli.js @@ -1,163 +1,208 @@ const cli = { - width: 80, - _program: {}, - _options: [], - program: (information = {}) => { - cli._program = information - return cli - }, - option: (flags, addition = {}) => { - // name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo. - // dest - The name of the attribute to be added to the object returned by parse_options(). + width: 80, + _program: {}, + _options: [], + program: (information = {}) => { + cli._program = information + return cli + }, + option: (flags, addition = {}) => { + // name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo. + // dest - The name of the attribute to be added to the object returned by parse_options(). - // nargs - The number of command-line arguments that should be consumed. // N, ?, *, +, REMAINDER - // action - The basic type of action to be taken when this argument is encountered at the command line. // store, store_true, store_false, append, append_const, count, help, version + // nargs - The number of command-line arguments that should be consumed. // N, ?, *, +, REMAINDER + // action - The basic type of action to be taken when this argument is encountered at the command line. // store, store_true, store_false, append, append_const, count, help, version - // const - A constant value required by some action and nargs selections. (supporting store_const and append_const action) + // const - A constant value required by some action and nargs selections. (supporting store_const and append_const action) - // metavar - A name for the argument in usage messages. - // help - A brief description of what the argument does. + // metavar - A name for the argument in usage messages. + // help - A brief description of what the argument does. - // required - Whether or not the command-line option may be omitted (optionals only). - // default - The value produced if the argument is absent from the command line. - // type - The type to which the command-line argument should be converted. - // choices - A container of the allowable values for the argument. + // required - Whether or not the command-line option may be omitted (optionals only). + // default - The value produced if the argument is absent from the command line. + // type - The type to which the command-line argument should be converted. + // choices - A container of the allowable values for the argument. - flags = Array.isArray(flags) ? flags : [flags] - addition.dest = addition.dest || flags.slice(-1)[0].toLowerCase().replace(/^-+/, '').replace(/-[a-z]/g, character => character.slice(1).toUpperCase()) - addition.help = addition.help || {'help': 'output usage information', 'version': 'output the version number'}[addition.action] - cli._options.push(Object.assign(addition, {flags: flags, positional: !flags[0].startsWith('-')})) - return cli - }, - parse: argv => { - const positionals = cli._options.map((option, index) => option.positional ? index : null).filter(index => index !== null), optionals = {} - cli._options.forEach((option, index) => option.positional ? null : option.flags.forEach(flag => optionals[flag] = index)) + flags = Array.isArray(flags) ? flags : [flags] + addition.dest = + addition.dest || + flags + .slice(-1)[0] + .toLowerCase() + .replace(/^-+/, '') + .replace(/-[a-z]/g, character => character.slice(1).toUpperCase()) + addition.help = + addition.help || + { + help: 'output usage information', + version: 'output the version number' + }[addition.action] + cli._options.push( + Object.assign(addition, { + flags: flags, + positional: !flags[0].startsWith('-') + }) + ) + return cli + }, + parse: argv => { + const positionals = cli._options + .map((option, index) => (option.positional ? index : null)) + .filter(index => index !== null), + optionals = {} + cli._options.forEach((option, index) => + option.positional + ? null + : option.flags.forEach(flag => (optionals[flag] = index)) + ) - cli._program.name = cli._program.name || require('path').parse(argv[1]).base - const args = argv.slice(2).reduce((result, part) => /^-[^-]/.test(part) ? result.concat(part.slice(1).split('').map(string => '-' + string)) : result.concat(part), []) + cli._program.name = cli._program.name || require('path').parse(argv[1]).base + const args = argv.slice(2).reduce( + (result, part) => + /^-[^-]/.test(part) + ? result.concat( + part + .slice(1) + .split('') + .map(string => '-' + string) + ) + : result.concat(part), + [] + ) - let pointer = 0 - while (pointer < args.length) { - let value = null - const part = args[pointer] - const index = part.startsWith('-') ? optionals[part] : positionals.shift() - if (index == undefined) part.startsWith('-') ? error(`no such option: ${part}`) : error(`extra arguments found: ${part}`) - if (part.startsWith('-')) pointer += 1 - const {action} = cli._options[index] + let pointer = 0 + while (pointer < args.length) { + let value = null + const part = args[pointer] + const index = part.startsWith('-') ? optionals[part] : positionals.shift() + if (index == undefined) + part.startsWith('-') + ? error(`no such option: ${part}`) + : error(`extra arguments found: ${part}`) + if (part.startsWith('-')) pointer += 1 + const { action } = cli._options[index] - if (['help', 'version'].includes(action)) { - if (action === 'help') help() - else if (action === 'version') version() - } - else if (['store_true', 'store_false'].includes(action)) { - value = action === 'store_true' - } - else { - const gap = args.slice(pointer).findIndex(part => part in optionals) - const next = gap === -1 ? args.length : pointer + gap - value = args.slice(pointer, next) - if (value.length === 0) { - if (cli._options[index].positional) - error(`the following arguments are required: ${part}`) - else if (cli._options[index].nargs === '+') - error(`argument ${part}: expected at least one argument`) - else - error(`argument ${part}: expected one argument`) - } - if (cli._options[index].nargs !== '+') { - value = value[0] - pointer += 1 - } - else { - pointer = next - } - } - cli[cli._options[index].dest] = value - } - if (positionals.length) error(`the following arguments are required: ${positionals.map(index => cli._options[index].flags[0]).join(', ')}`) - // cli._options.forEach(option => console.log(option.dest, cli[option.dest])) - return cli - } + if (['help', 'version'].includes(action)) { + if (action === 'help') help() + else if (action === 'version') version() + } else if (['store_true', 'store_false'].includes(action)) { + value = action === 'store_true' + } else { + const gap = args.slice(pointer).findIndex(part => part in optionals) + const next = gap === -1 ? args.length : pointer + gap + value = args.slice(pointer, next) + if (value.length === 0) { + if (cli._options[index].positional) + error(`the following arguments are required: ${part}`) + else if (cli._options[index].nargs === '+') + error(`argument ${part}: expected at least one argument`) + else error(`argument ${part}: expected one argument`) + } + if (cli._options[index].nargs !== '+') { + value = value[0] + pointer += 1 + } else { + pointer = next + } + } + cli[cli._options[index].dest] = value + } + if (positionals.length) + error( + `the following arguments are required: ${positionals + .map(index => cli._options[index].flags[0]) + .join(', ')}` + ) + // cli._options.forEach(option => console.log(option.dest, cli[option.dest])) + return cli + } } -const pad = length => (new Array(length + 1)).join(' ') +const pad = length => new Array(length + 1).join(' ') const usage = () => { - const options = cli._options.map(option => { - const flag = option.flags.sort((a, b) => a.length - b.length)[0] - const name = option.metavar || option.dest - if (option.positional) { - if (option.nargs === '+') - return `${name} [${name} ...]` - else - return `${name}` - } - else { - if (['store_true', 'store_false', 'help', 'version'].includes(option.action)) - return `[${flag}]` - else if (option.nargs === '+') - return `[${flag} ${name} [${name} ...]]` - else - return `[${flag} ${name}]` - } - }) - const maximum = cli.width - const title = `usage: ${cli._program.name}` - const lines = [title] + const options = cli._options.map(option => { + const flag = option.flags.sort((a, b) => a.length - b.length)[0] + const name = option.metavar || option.dest + if (option.positional) { + if (option.nargs === '+') return `${name} [${name} ...]` + else return `${name}` + } else { + if ( + ['store_true', 'store_false', 'help', 'version'].includes(option.action) + ) + return `[${flag}]` + else if (option.nargs === '+') return `[${flag} ${name} [${name} ...]]` + else return `[${flag} ${name}]` + } + }) + const maximum = cli.width + const title = `usage: ${cli._program.name}` + const lines = [title] - options.map(name => ' ' + name).forEach(option => { - lines[lines.length - 1].length + option.length < maximum - ? lines[lines.length - 1] += option - : lines.push(pad(title.length) + option) - }) - console.log(lines.join('\n')) + options + .map(name => ' ' + name) + .forEach(option => { + lines[lines.length - 1].length + option.length < maximum + ? (lines[lines.length - 1] += option) + : lines.push(pad(title.length) + option) + }) + console.log(lines.join('\n')) } const help = () => { - usage() - const positionals = cli._options.filter(option => option.positional) - .map(option => [option.metavar || option.dest, option.help]) - const optionals = cli._options.filter(option => !option.positional) - .map(option => { - const {flags} = option - const name = option.metavar || option.dest - let use = '' - if (['store_true', 'store_false', 'help', 'version'].includes(option.action)) - use = flags.map(flag => `${flag}`).join(', ') - else if (option.nargs === '+') - use = flags.map(flag => `${flag} ${name} [${name} ...]`).join(', ') - else - use = flags.map(flag => `${flag} ${name}`).join(', ') - return [use, option.help] - }) - let align = Math.max.apply(null, positionals.concat(optionals).map(option => option[0].length)) - align = align > 30 ? 30 : align - const rest = cli.width - align - 4 - const publish = option => { - const slice = string => - Array.from(Array(Math.ceil(string.length / rest)).keys()) - .map(index => string.slice(index * rest, (index + 1) * rest)) - .join('\n' + pad(align + 4)) - option[0].length < align - ? console.log(` ${option[0]}${pad(align - option[0].length)} ${slice(option[1])}`) - : console.log(` ${option[0]}\n${pad(align + 4)}${slice(option[1])}`) - } - if (positionals.length) console.log('\npositional arguments:') - positionals.forEach(publish) - if (optionals.length) console.log('\noptional arguments:') - optionals.forEach(publish) - process.exit() + usage() + const positionals = cli._options + .filter(option => option.positional) + .map(option => [option.metavar || option.dest, option.help]) + const optionals = cli._options + .filter(option => !option.positional) + .map(option => { + const { flags } = option + const name = option.metavar || option.dest + let use = '' + if ( + ['store_true', 'store_false', 'help', 'version'].includes(option.action) + ) + use = flags.map(flag => `${flag}`).join(', ') + else if (option.nargs === '+') + use = flags.map(flag => `${flag} ${name} [${name} ...]`).join(', ') + else use = flags.map(flag => `${flag} ${name}`).join(', ') + return [use, option.help] + }) + let align = Math.max.apply( + null, + positionals.concat(optionals).map(option => option[0].length) + ) + align = align > 30 ? 30 : align + const rest = cli.width - align - 4 + const publish = option => { + const slice = string => + Array.from(Array(Math.ceil(string.length / rest)).keys()) + .map(index => string.slice(index * rest, (index + 1) * rest)) + .join('\n' + pad(align + 4)) + option[0].length < align + ? console.log( + ` ${option[0]}${pad(align - option[0].length)} ${slice(option[1])}` + ) + : console.log(` ${option[0]}\n${pad(align + 4)}${slice(option[1])}`) + } + if (positionals.length) console.log('\npositional arguments:') + positionals.forEach(publish) + if (optionals.length) console.log('\noptional arguments:') + optionals.forEach(publish) + process.exit() } const version = () => { - console.log(cli._program.version) - process.exit() + console.log(cli._program.version) + process.exit() } const error = message => { - usage() - console.log(cli._program.name + ':', 'error:', message) - process.exit(1) + usage() + console.log(cli._program.name + ':', 'error:', message) + process.exit(1) } -module.exports = cli \ No newline at end of file +module.exports = cli diff --git a/packages/unblock/src/crypto.js b/packages/unblock/src/crypto.js index 1b08c010..b7db1f5f 100644 --- a/packages/unblock/src/crypto.js +++ b/packages/unblock/src/crypto.js @@ -8,95 +8,160 @@ const eapiKey = 'e82ckenh8dichen8' const linuxapiKey = 'rFgB&h#%2?^eDg:Q' const decrypt = (buffer, key) => { - const decipher = crypto.createDecipheriv('aes-128-ecb', key, '') - return Buffer.concat([decipher.update(buffer), decipher.final()]) + const decipher = crypto.createDecipheriv('aes-128-ecb', key, '') + return Buffer.concat([decipher.update(buffer), decipher.final()]) } const encrypt = (buffer, key) => { - const cipher = crypto.createCipheriv('aes-128-ecb', key, '') - return Buffer.concat([cipher.update(buffer), cipher.final()]) + const cipher = crypto.createCipheriv('aes-128-ecb', key, '') + return Buffer.concat([cipher.update(buffer), cipher.final()]) } module.exports = { - eapi: { - encrypt: buffer => encrypt(buffer, eapiKey), - decrypt: buffer => decrypt(buffer, eapiKey), - encryptRequest: (url, object) => { - url = parse(url) - const text = JSON.stringify(object) - const message = `nobody${url.path}use${text}md5forencrypt` - const digest = crypto.createHash('md5').update(message).digest('hex') - const data = `${url.path}-36cd479b6b5-${text}-36cd479b6b5-${digest}` - return { - url: url.href.replace(/\w*api/, 'eapi'), - body: bodyify({ - params: module.exports.eapi.encrypt(Buffer.from(data)).toString('hex').toUpperCase() - }) - } - } - }, - linuxapi: { - encrypt: buffer => encrypt(buffer, linuxapiKey), - decrypt: buffer => decrypt(buffer, linuxapiKey), - encryptRequest: (url, object) => { - url = parse(url) - const text = JSON.stringify({method: 'POST', url: url.href, params: object}) - return { - url: url.resolve('/api/linux/forward'), - body: bodyify({ - eparams: module.exports.linuxapi.encrypt(Buffer.from(text)).toString('hex').toUpperCase() - }) - } - } - }, - miguapi: { - encryptBody: object => { - const text = JSON.stringify(object) - const derive = (password, salt, keyLength, ivSize) => { // EVP_BytesToKey - salt = salt || Buffer.alloc(0) - const keySize = keyLength / 8 - const repeat = Math.ceil((keySize + ivSize * 8) / 32) - const buffer = Buffer.concat(Array(repeat).fill(null).reduce( - result => result.concat(crypto.createHash('md5').update(Buffer.concat([result.slice(-1)[0], password, salt])).digest()), - [Buffer.alloc(0)] - )) - return { - key: buffer.slice(0, keySize), - iv: buffer.slice(keySize, keySize + ivSize) - } - } - const password = Buffer.from(crypto.randomBytes(32).toString('hex')), salt = crypto.randomBytes(8) - const key = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----' - const secret = derive(password, salt, 256, 16) - const cipher = crypto.createCipheriv('aes-256-cbc', secret.key, secret.iv) - return bodyify({ - data: Buffer.concat([Buffer.from('Salted__'), salt, cipher.update(Buffer.from(text)), cipher.final()]).toString('base64'), - secKey: crypto.publicEncrypt({key, padding: crypto.constants.RSA_PKCS1_PADDING}, password).toString('base64') - }) - } - }, - base64: { - encode: (text, charset) => Buffer.from(text, charset).toString('base64').replace(/\+/g, '-').replace(/\//g, '_'), - decode: (text, charset) => Buffer.from(text.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(charset) - }, - uri: { - retrieve: id => { - id = id.toString().trim() - const key = '3go8&$8*3*3h0k(2)2' - const string = Array.from(Array(id.length).keys()).map(index => String.fromCharCode(id.charCodeAt(index) ^ key.charCodeAt(index % key.length))).join('') - const result = crypto.createHash('md5').update(string).digest('base64').replace(/\//g, '_').replace(/\+/g, '-') - return `http://p1.music.126.net/${result}/${id}` - } - }, - md5: { - digest: value => crypto.createHash('md5').update(value).digest('hex'), - pipe: source => new Promise((resolve, reject) => { - const digest = crypto.createHash('md5').setEncoding('hex') - source.pipe(digest) - .on('error', error => reject(error)) - .once('finish', () => resolve(digest.read())) - }) - } + eapi: { + encrypt: buffer => encrypt(buffer, eapiKey), + decrypt: buffer => decrypt(buffer, eapiKey), + encryptRequest: (url, object) => { + url = parse(url) + const text = JSON.stringify(object) + const message = `nobody${url.path}use${text}md5forencrypt` + const digest = crypto + .createHash('md5') + .update(message) + .digest('hex') + const data = `${url.path}-36cd479b6b5-${text}-36cd479b6b5-${digest}` + return { + url: url.href.replace(/\w*api/, 'eapi'), + body: bodyify({ + params: module.exports.eapi + .encrypt(Buffer.from(data)) + .toString('hex') + .toUpperCase() + }) + } + } + }, + linuxapi: { + encrypt: buffer => encrypt(buffer, linuxapiKey), + decrypt: buffer => decrypt(buffer, linuxapiKey), + encryptRequest: (url, object) => { + url = parse(url) + const text = JSON.stringify({ + method: 'POST', + url: url.href, + params: object + }) + return { + url: url.resolve('/api/linux/forward'), + body: bodyify({ + eparams: module.exports.linuxapi + .encrypt(Buffer.from(text)) + .toString('hex') + .toUpperCase() + }) + } + } + }, + miguapi: { + encryptBody: object => { + const text = JSON.stringify(object) + const derive = (password, salt, keyLength, ivSize) => { + // EVP_BytesToKey + salt = salt || Buffer.alloc(0) + const keySize = keyLength / 8 + const repeat = Math.ceil((keySize + ivSize * 8) / 32) + const buffer = Buffer.concat( + Array(repeat) + .fill(null) + .reduce( + result => + result.concat( + crypto + .createHash('md5') + .update( + Buffer.concat([result.slice(-1)[0], password, salt]) + ) + .digest() + ), + [Buffer.alloc(0)] + ) + ) + return { + key: buffer.slice(0, keySize), + iv: buffer.slice(keySize, keySize + ivSize) + } + } + const password = Buffer.from(crypto.randomBytes(32).toString('hex')), + salt = crypto.randomBytes(8) + const key = + '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKWVJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exReZosTByYp4Xwpb1+WAQIDAQAB\n-----END PUBLIC KEY-----' + const secret = derive(password, salt, 256, 16) + const cipher = crypto.createCipheriv('aes-256-cbc', secret.key, secret.iv) + return bodyify({ + data: Buffer.concat([ + Buffer.from('Salted__'), + salt, + cipher.update(Buffer.from(text)), + cipher.final() + ]).toString('base64'), + secKey: crypto + .publicEncrypt( + { key, padding: crypto.constants.RSA_PKCS1_PADDING }, + password + ) + .toString('base64') + }) + } + }, + base64: { + encode: (text, charset) => + Buffer.from(text, charset) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_'), + decode: (text, charset) => + Buffer.from( + text.replace(/-/g, '+').replace(/_/g, '/'), + 'base64' + ).toString(charset) + }, + uri: { + retrieve: id => { + id = id.toString().trim() + const key = '3go8&$8*3*3h0k(2)2' + const string = Array.from(Array(id.length).keys()) + .map(index => + String.fromCharCode( + id.charCodeAt(index) ^ key.charCodeAt(index % key.length) + ) + ) + .join('') + const result = crypto + .createHash('md5') + .update(string) + .digest('base64') + .replace(/\//g, '_') + .replace(/\+/g, '-') + return `http://p1.music.126.net/${result}/${id}` + } + }, + md5: { + digest: value => + crypto + .createHash('md5') + .update(value) + .digest('hex'), + pipe: source => + new Promise((resolve, reject) => { + const digest = crypto.createHash('md5').setEncoding('hex') + source + .pipe(digest) + .on('error', error => reject(error)) + .once('finish', () => resolve(digest.read())) + }) + } } -try {module.exports.kuwoapi = require('./kwDES')} catch(e) {} \ No newline at end of file +try { + module.exports.kuwoapi = require('./kwDES') +} catch (e) {} diff --git a/packages/unblock/src/hook.js b/packages/unblock/src/hook.js index db7d42d3..cf9b7438 100644 --- a/packages/unblock/src/hook.js +++ b/packages/unblock/src/hook.js @@ -6,335 +6,458 @@ const match = require('./provider/match') const querystring = require('querystring') const hook = { - request: { - before: () => {}, - after: () => {}, - }, - connect: { - before: () => {} - }, - negotiate: { - before: () => {} - }, - target: { - host: new Set(), - path: new Set() - } + request: { + before: () => {}, + after: () => {} + }, + connect: { + before: () => {} + }, + negotiate: { + before: () => {} + }, + target: { + host: new Set(), + path: new Set() + } } hook.target.host = new Set([ - 'music.163.com', - 'interface.music.163.com', - 'interface3.music.163.com', - 'apm.music.163.com', - 'apm3.music.163.com', - // 'mam.netease.com', - // 'api.iplay.163.com', // look living - // 'ac.dun.163yun.com', - // 'crash.163.com', - // 'clientlog.music.163.com', - // 'clientlog3.music.163.com' + 'music.163.com', + 'interface.music.163.com', + 'interface3.music.163.com', + 'apm.music.163.com', + 'apm3.music.163.com' + // 'mam.netease.com', + // 'api.iplay.163.com', // look living + // 'ac.dun.163yun.com', + // 'crash.163.com', + // 'clientlog.music.163.com', + // 'clientlog3.music.163.com' ]) hook.target.path = new Set([ - '/api/v3/playlist/detail', - '/api/v3/song/detail', - '/api/v6/playlist/detail', - '/api/album/play', - '/api/artist/privilege', - '/api/album/privilege', - '/api/v1/artist', - '/api/v1/artist/songs', - '/api/artist/top/song', - '/api/v1/album', - '/api/album/v3/detail', - '/api/playlist/privilege', - '/api/song/enhance/player/url', - '/api/song/enhance/player/url/v1', - '/api/song/enhance/download/url', - '/api/song/enhance/privilege', - '/batch', - '/api/batch', - '/api/v1/search/get', - '/api/v1/search/song/get', - '/api/search/complex/get', - '/api/cloudsearch/pc', - '/api/v1/playlist/manipulate/tracks', - '/api/song/like', - '/api/v1/play/record', - '/api/playlist/v4/detail', - '/api/v1/radio/get', - '/api/v1/discovery/recommend/songs' + '/api/v3/playlist/detail', + '/api/v3/song/detail', + '/api/v6/playlist/detail', + '/api/album/play', + '/api/artist/privilege', + '/api/album/privilege', + '/api/v1/artist', + '/api/v1/artist/songs', + '/api/artist/top/song', + '/api/v1/album', + '/api/album/v3/detail', + '/api/playlist/privilege', + '/api/song/enhance/player/url', + '/api/song/enhance/player/url/v1', + '/api/song/enhance/download/url', + '/api/song/enhance/privilege', + '/batch', + '/api/batch', + '/api/v1/search/get', + '/api/v1/search/song/get', + '/api/search/complex/get', + '/api/cloudsearch/pc', + '/api/v1/playlist/manipulate/tracks', + '/api/song/like', + '/api/v1/play/record', + '/api/playlist/v4/detail', + '/api/v1/radio/get', + '/api/v1/discovery/recommend/songs' ]) const domainList = [ - 'music.163.com', - 'music.126.net', - 'iplay.163.com', - 'look.163.com', - 'y.163.com', + 'music.163.com', + 'music.126.net', + 'iplay.163.com', + 'look.163.com', + 'y.163.com' ] hook.request.before = ctx => { - const {req} = ctx - req.url = (req.url.startsWith('http://') ? '' : (req.socket.encrypted ? 'https:' : 'http:') + '//' + (domainList.some(domain => (req.headers.host || '').endsWith(domain)) ? req.headers.host : null)) + req.url - const url = parse(req.url) - if ([url.hostname, req.headers.host].some(host => host.includes('music.163.com'))) ctx.decision = 'proxy' - if ([url.hostname, req.headers.host].some(host => hook.target.host.has(host)) && req.method == 'POST' && (url.path == '/api/linux/forward' || url.path.startsWith('/eapi/'))) { - return request.read(req) - .then(body => req.body = body) - .then(body => { - if ('x-napm-retry' in req.headers) delete req.headers['x-napm-retry'] - req.headers['X-Real-IP'] = '118.88.88.88' - if (req.url.includes('stream')) return // look living eapi can not be decrypted - if (body) { - let data = null - const netease = {} - netease.pad = (body.match(/%0+$/) || [''])[0] - netease.forward = (url.path == '/api/linux/forward') - if (netease.forward) { - data = JSON.parse(crypto.linuxapi.decrypt(Buffer.from(body.slice(8, body.length - netease.pad.length), 'hex')).toString()) - netease.path = parse(data.url).path - netease.param = data.params - } - else { - data = crypto.eapi.decrypt(Buffer.from(body.slice(7, body.length - netease.pad.length), 'hex')).toString().split('-36cd479b6b5-') - netease.path = data[0] - netease.param = JSON.parse(data[1]) - } - netease.path = netease.path.replace(/\/\d*$/, '') - ctx.netease = netease - // console.log(netease.path, netease.param) + const { req } = ctx + req.url = + (req.url.startsWith('http://') + ? '' + : (req.socket.encrypted ? 'https:' : 'http:') + + '//' + + (domainList.some(domain => (req.headers.host || '').endsWith(domain)) + ? req.headers.host + : null)) + req.url + const url = parse(req.url) + if ( + [url.hostname, req.headers.host].some(host => + host.includes('music.163.com') + ) + ) + ctx.decision = 'proxy' + if ( + [url.hostname, req.headers.host].some(host => hook.target.host.has(host)) && + req.method == 'POST' && + (url.path == '/api/linux/forward' || url.path.startsWith('/eapi/')) + ) { + return request + .read(req) + .then(body => (req.body = body)) + .then(body => { + if ('x-napm-retry' in req.headers) delete req.headers['x-napm-retry'] + req.headers['X-Real-IP'] = '118.88.88.88' + if (req.url.includes('stream')) return // look living eapi can not be decrypted + if (body) { + let data = null + const netease = {} + netease.pad = (body.match(/%0+$/) || [''])[0] + netease.forward = url.path == '/api/linux/forward' + if (netease.forward) { + data = JSON.parse( + crypto.linuxapi + .decrypt( + Buffer.from( + body.slice(8, body.length - netease.pad.length), + 'hex' + ) + ) + .toString() + ) + netease.path = parse(data.url).path + netease.param = data.params + } else { + data = crypto.eapi + .decrypt( + Buffer.from( + body.slice(7, body.length - netease.pad.length), + 'hex' + ) + ) + .toString() + .split('-36cd479b6b5-') + netease.path = data[0] + netease.param = JSON.parse(data[1]) + } + netease.path = netease.path.replace(/\/\d*$/, '') + ctx.netease = netease + // console.log(netease.path, netease.param) - if (netease.path == '/api/song/enhance/download/url') - return pretendPlay(ctx) - } - }) - .catch(error => console.log(error, req.url)) - } - else if ((hook.target.host.has(url.hostname)) && (url.path.startsWith('/weapi/') || url.path.startsWith('/api/'))) { - req.headers['X-Real-IP'] = '118.88.88.88' - ctx.netease = {web: true, path: url.path.replace(/^\/weapi\//, '/api/').replace(/\?.+$/, '').replace(/\/\d*$/, '')} - } - else if (req.url.includes('package')) { - try { - const data = req.url.split('package/').pop().split('/') - const url = parse(crypto.base64.decode(data[0])) - const id = data[1].replace(/\.\w+/, '') - req.url = url.href - req.headers['host'] = url.hostname - req.headers['cookie'] = null - ctx.package = {id} - ctx.decision = 'proxy' - // if (url.href.includes('google')) - // return request('GET', req.url, req.headers, null, parse('http://127.0.0.1:1080')) - // .then(response => (ctx.res.writeHead(response.statusCode, response.headers), response.pipe(ctx.res))) - } - catch(error) { - ctx.error = error - ctx.decision = 'close' - } - } + if (netease.path == '/api/song/enhance/download/url') + return pretendPlay(ctx) + } + }) + .catch(error => console.log(error, req.url)) + } else if ( + hook.target.host.has(url.hostname) && + (url.path.startsWith('/weapi/') || url.path.startsWith('/api/')) + ) { + req.headers['X-Real-IP'] = '118.88.88.88' + ctx.netease = { + web: true, + path: url.path + .replace(/^\/weapi\//, '/api/') + .replace(/\?.+$/, '') + .replace(/\/\d*$/, '') + } + } else if (req.url.includes('package')) { + try { + const data = req.url + .split('package/') + .pop() + .split('/') + const url = parse(crypto.base64.decode(data[0])) + const id = data[1].replace(/\.\w+/, '') + req.url = url.href + req.headers['host'] = url.hostname + req.headers['cookie'] = null + ctx.package = { id } + ctx.decision = 'proxy' + // if (url.href.includes('google')) + // return request('GET', req.url, req.headers, null, parse('http://127.0.0.1:1080')) + // .then(response => (ctx.res.writeHead(response.statusCode, response.headers), response.pipe(ctx.res))) + } catch (error) { + ctx.error = error + ctx.decision = 'close' + } + } } hook.request.after = ctx => { - const {req, proxyRes, netease, package} = ctx - if (req.headers.host === 'tyst.migu.cn' && proxyRes.headers['content-range'] && proxyRes.statusCode === 200) proxyRes.statusCode = 206 - if (netease && hook.target.path.has(netease.path) && proxyRes.statusCode == 200) { - return request.read(proxyRes, true) - .then(buffer => buffer.length ? proxyRes.body = buffer : Promise.reject()) - .then(buffer => { - const patch = string => string.replace(/([^\\]"\s*:\s*)(\d{16,})(\s*[}|,])/g, '$1"$2L"$3') // for js precision - try { - netease.encrypted = false - netease.jsonBody = JSON.parse(patch(buffer.toString())) - } - catch(error) { - netease.encrypted = true - netease.jsonBody = JSON.parse(patch(crypto.eapi.decrypt(buffer).toString())) - } + const { req, proxyRes, netease, package } = ctx + if ( + req.headers.host === 'tyst.migu.cn' && + proxyRes.headers['content-range'] && + proxyRes.statusCode === 200 + ) + proxyRes.statusCode = 206 + if ( + netease && + hook.target.path.has(netease.path) && + proxyRes.statusCode == 200 + ) { + return request + .read(proxyRes, true) + .then(buffer => + buffer.length ? (proxyRes.body = buffer) : Promise.reject() + ) + .then(buffer => { + const patch = string => + string.replace(/([^\\]"\s*:\s*)(\d{16,})(\s*[}|,])/g, '$1"$2L"$3') // for js precision + try { + netease.encrypted = false + netease.jsonBody = JSON.parse(patch(buffer.toString())) + } catch (error) { + netease.encrypted = true + netease.jsonBody = JSON.parse( + patch(crypto.eapi.decrypt(buffer).toString()) + ) + } - if (new Set([401, 512]).has(netease.jsonBody.code) && !netease.web) { - if (netease.path.includes('manipulate')) return tryCollect(ctx) - else if (netease.path == '/api/song/like') return tryLike(ctx) - } - else if (netease.path.includes('url')) return tryMatch(ctx) - }) - .then(() => { - ['transfer-encoding', 'content-encoding', 'content-length'].filter(key => key in proxyRes.headers).forEach(key => delete proxyRes.headers[key]) + if (new Set([401, 512]).has(netease.jsonBody.code) && !netease.web) { + if (netease.path.includes('manipulate')) return tryCollect(ctx) + else if (netease.path == '/api/song/like') return tryLike(ctx) + } else if (netease.path.includes('url')) return tryMatch(ctx) + }) + .then(() => { + ;['transfer-encoding', 'content-encoding', 'content-length'] + .filter(key => key in proxyRes.headers) + .forEach(key => delete proxyRes.headers[key]) - const inject = (key, value) => { - if (typeof(value) === 'object' && value != null) { - if ('fee' in value) value['fee'] = 0 - if ('st' in value && 'pl' in value && 'dl' in value && 'subp' in value) { // batch modify - value['st'] = 0 - value['subp'] = 1 - value['pl'] = (value['pl'] == 0) ? 320000 : value['pl'] - value['dl'] = (value['dl'] == 0) ? 320000 : value['dl'] - } - } - return value - } + const inject = (key, value) => { + if (typeof value === 'object' && value != null) { + if ('fee' in value) value['fee'] = 0 + if ( + 'st' in value && + 'pl' in value && + 'dl' in value && + 'subp' in value + ) { + // batch modify + value['st'] = 0 + value['subp'] = 1 + value['pl'] = value['pl'] == 0 ? 320000 : value['pl'] + value['dl'] = value['dl'] == 0 ? 320000 : value['dl'] + } + } + return value + } - let body = JSON.stringify(netease.jsonBody, inject) - body = body.replace(/([^\\]"\s*:\s*)"(\d{16,})L"(\s*[}|,])/g, '$1$2$3') // for js precision - proxyRes.body = (netease.encrypted ? crypto.eapi.encrypt(Buffer.from(body)) : body) - }) - .catch(error => error ? console.log(error, req.url) : null) - } - else if (package) { - if (new Set([201, 301, 302, 303, 307, 308]).has(proxyRes.statusCode)) { - return request(req.method, parse(req.url).resolve(proxyRes.headers.location), req.headers) - .then(response => ctx.proxyRes = response) - } - else if (/p\d+c*.music.126.net/.test(req.url)) { - proxyRes.headers['content-type'] = 'audio/*' - } - } + let body = JSON.stringify(netease.jsonBody, inject) + body = body.replace(/([^\\]"\s*:\s*)"(\d{16,})L"(\s*[}|,])/g, '$1$2$3') // for js precision + proxyRes.body = netease.encrypted + ? crypto.eapi.encrypt(Buffer.from(body)) + : body + }) + .catch(error => (error ? console.log(error, req.url) : null)) + } else if (package) { + if (new Set([201, 301, 302, 303, 307, 308]).has(proxyRes.statusCode)) { + return request( + req.method, + parse(req.url).resolve(proxyRes.headers.location), + req.headers + ).then(response => (ctx.proxyRes = response)) + } else if (/p\d+c*.music.126.net/.test(req.url)) { + proxyRes.headers['content-type'] = 'audio/*' + } + } } hook.connect.before = ctx => { - const {req} = ctx - const url = parse('https://' + req.url) - if ([url.hostname, req.headers.host].some(host => hook.target.host.has(host))) { - if (url.port == 80) { - req.url = `${global.address || 'localhost'}:${global.port[0]}` - req.local = true - } - else if (global.port[1]) { - req.url = `${global.address || 'localhost'}:${global.port[1]}` - req.local = true - } - else { - ctx.decision = 'blank' - } - } - else if (url.href.includes(global.endpoint)) ctx.decision = 'proxy' + const { req } = ctx + const url = parse('https://' + req.url) + if ( + [url.hostname, req.headers.host].some(host => hook.target.host.has(host)) + ) { + if (url.port == 80) { + req.url = `${global.address || 'localhost'}:${global.port[0]}` + req.local = true + } else if (global.port[1]) { + req.url = `${global.address || 'localhost'}:${global.port[1]}` + req.local = true + } else { + ctx.decision = 'blank' + } + } else if (url.href.includes(global.endpoint)) ctx.decision = 'proxy' } hook.negotiate.before = ctx => { - const {req, socket, decision} = ctx - const url = parse('https://' + req.url) - const target = hook.target.host - if (req.local || decision) return - if (target.has(socket.sni) && !target.has(url.hostname)) { - target.add(url.hostname) - ctx.decision = 'blank' - } + const { req, socket, decision } = ctx + const url = parse('https://' + req.url) + const target = hook.target.host + if (req.local || decision) return + if (target.has(socket.sni) && !target.has(url.hostname)) { + target.add(url.hostname) + ctx.decision = 'blank' + } } const pretendPlay = ctx => { - const {req, netease} = ctx - const turn = 'http://music.163.com/api/song/enhance/player/url' - let query = null - if (netease.forward) { - const {id, br} = netease.param - netease.param = {ids: `["${id}"]`, br} - query = crypto.linuxapi.encryptRequest(turn, netease.param) - } - else { - const {id, br, e_r, header} = netease.param - netease.param = {ids: `["${id}"]`, br, e_r, header} - query = crypto.eapi.encryptRequest(turn, netease.param) - } - req.url = query.url - req.body = query.body + netease.pad + const { req, netease } = ctx + const turn = 'http://music.163.com/api/song/enhance/player/url' + let query = null + if (netease.forward) { + const { id, br } = netease.param + netease.param = { ids: `["${id}"]`, br } + query = crypto.linuxapi.encryptRequest(turn, netease.param) + } else { + const { id, br, e_r, header } = netease.param + netease.param = { ids: `["${id}"]`, br, e_r, header } + query = crypto.eapi.encryptRequest(turn, netease.param) + } + req.url = query.url + req.body = query.body + netease.pad } const tryCollect = ctx => { - const {req, netease} = ctx - const {trackIds, pid, op} = netease.param - const trackId = (Array.isArray(trackIds) ? trackIds : JSON.parse(trackIds))[0] - return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=${op}`).then(response => response.json()) - .then(jsonBody => { - netease.jsonBody = jsonBody - }) - .catch(() => {}) + const { req, netease } = ctx + const { trackIds, pid, op } = netease.param + const trackId = (Array.isArray(trackIds) ? trackIds : JSON.parse(trackIds))[0] + return request( + 'POST', + 'http://music.163.com/api/playlist/manipulate/tracks', + req.headers, + `trackIds=[${trackId},${trackId}]&pid=${pid}&op=${op}` + ) + .then(response => response.json()) + .then(jsonBody => { + netease.jsonBody = jsonBody + }) + .catch(() => {}) } const tryLike = ctx => { - const {req, netease} = ctx - const {trackId} = netease.param - let pid = 0, userId = 0 - return request('GET', 'http://music.163.com/api/v1/user/info', req.headers).then(response => response.json()) - .then(jsonBody => { - userId = jsonBody.userPoint.userId - return request('GET', `http://music.163.com/api/user/playlist?uid=${userId}&limit=1`, req.headers).then(response => response.json()) - }) - .then(jsonBody => { - pid = jsonBody.playlist[0].id - return request('POST', 'http://music.163.com/api/playlist/manipulate/tracks', req.headers, `trackIds=[${trackId},${trackId}]&pid=${pid}&op=add`).then(response => response.json()) - }) - .then(jsonBody => { - if (new Set([200, 502]).has(jsonBody.code)) { - netease.jsonBody = {code: 200, playlistId: pid} - } - }) - .catch(() => {}) + const { req, netease } = ctx + const { trackId } = netease.param + let pid = 0, + userId = 0 + return request('GET', 'http://music.163.com/api/v1/user/info', req.headers) + .then(response => response.json()) + .then(jsonBody => { + userId = jsonBody.userPoint.userId + return request( + 'GET', + `http://music.163.com/api/user/playlist?uid=${userId}&limit=1`, + req.headers + ).then(response => response.json()) + }) + .then(jsonBody => { + pid = jsonBody.playlist[0].id + return request( + 'POST', + 'http://music.163.com/api/playlist/manipulate/tracks', + req.headers, + `trackIds=[${trackId},${trackId}]&pid=${pid}&op=add` + ).then(response => response.json()) + }) + .then(jsonBody => { + if (new Set([200, 502]).has(jsonBody.code)) { + netease.jsonBody = { code: 200, playlistId: pid } + } + }) + .catch(() => {}) } -const computeHash = task => request('GET', task.url).then(response => crypto.md5.pipe(response)) +const computeHash = task => + request('GET', task.url).then(response => crypto.md5.pipe(response)) const tryMatch = ctx => { - const {req, netease} = ctx - const {jsonBody} = netease - let tasks = [], target = 0 + const { req, netease } = ctx + const { jsonBody } = netease + let tasks = [], + target = 0 - const inject = item => { - item.flag = 0 - if ((item.code != 200 || item.freeTrialInfo) && (target == 0 || item.id == target)) { - return match(item.id) - .then(song => { - item.type = song.br === 999000 ? 'flac' : 'mp3' - item.url = global.endpoint ? `${global.endpoint}/package/${crypto.base64.encode(song.url)}/${item.id}.${item.type}` : song.url - item.md5 = song.md5 || crypto.md5.digest(song.url) - item.br = song.br || 128000 - item.size = song.size - item.code = 200 - item.freeTrialInfo = null - return song - }) - .then(song => { - if (!netease.path.includes('download') || song.md5) return - const newer = (base, target) => { - const difference = - Array.from([base, target]) - .map(version => version.split('.').slice(0, 3).map(number => parseInt(number) || 0)) - .reduce((aggregation, current) => !aggregation.length ? current.map(element => [element]) : aggregation.map((element, index) => element.concat(current[index])), []) - .filter(pair => pair[0] != pair[1])[0] - return !difference || difference[0] <= difference[1] - } - const limit = {android: '0.0.0', osx: '0.0.0'} - const task = {key: song.url.replace(/\?.*$/, '').replace(/(?<=kugou\.com\/)\w+\/\w+\//, '').replace(/(?<=kuwo\.cn\/)\w+\/\w+\/resource\//, ''), url: song.url} - try { - let {header} = netease.param - header = typeof(header) === 'string' ? JSON.parse(header) : header - const cookie = querystring.parse(req.headers.cookie.replace(/\s/g, ''), ';') - const os = header.os || cookie.os, version = header.appver || cookie.appver - if (os in limit && newer(limit[os], version)) - return cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then(value => item.md5 = value) - } - catch(e) {} - }) - .catch(() => {}) - } - else if (item.code == 200 && netease.web) { - item.url = item.url.replace(/(m\d+?)(?!c)\.music\.126\.net/, '$1c.music.126.net') - } - } + const inject = item => { + item.flag = 0 + if ( + (item.code != 200 || item.freeTrialInfo) && + (target == 0 || item.id == target) + ) { + return match(item.id) + .then(song => { + item.type = song.br === 999000 ? 'flac' : 'mp3' + item.url = global.endpoint + ? `${global.endpoint}/package/${crypto.base64.encode(song.url)}/${ + item.id + }.${item.type}` + : song.url + item.md5 = song.md5 || crypto.md5.digest(song.url) + item.br = song.br || 128000 + item.size = song.size + item.code = 200 + item.freeTrialInfo = null + return song + }) + .then(song => { + if (!netease.path.includes('download') || song.md5) return + const newer = (base, target) => { + const difference = Array.from([base, target]) + .map(version => + version + .split('.') + .slice(0, 3) + .map(number => parseInt(number) || 0) + ) + .reduce( + (aggregation, current) => + !aggregation.length + ? current.map(element => [element]) + : aggregation.map((element, index) => + element.concat(current[index]) + ), + [] + ) + .filter(pair => pair[0] != pair[1])[0] + return !difference || difference[0] <= difference[1] + } + const limit = { android: '0.0.0', osx: '0.0.0' } + const task = { + key: song.url + .replace(/\?.*$/, '') + .replace(/(?<=kugou\.com\/)\w+\/\w+\//, '') + .replace(/(?<=kuwo\.cn\/)\w+\/\w+\/resource\//, ''), + url: song.url + } + try { + let { header } = netease.param + header = typeof header === 'string' ? JSON.parse(header) : header + const cookie = querystring.parse( + req.headers.cookie.replace(/\s/g, ''), + ';' + ) + const os = header.os || cookie.os, + version = header.appver || cookie.appver + if (os in limit && newer(limit[os], version)) + return cache(computeHash, task, 7 * 24 * 60 * 60 * 1000).then( + value => (item.md5 = value) + ) + } catch (e) {} + }) + .catch(() => {}) + } else if (item.code == 200 && netease.web) { + item.url = item.url.replace( + /(m\d+?)(?!c)\.music\.126\.net/, + '$1c.music.126.net' + ) + } + } - if (!Array.isArray(jsonBody.data)) { - tasks = [inject(jsonBody.data)] - } - else if (netease.path.includes('download')) { - jsonBody.data = jsonBody.data[0] - tasks = [inject(jsonBody.data)] - } - else { - target = netease.web ? 0 : parseInt(((Array.isArray(netease.param.ids) ? netease.param.ids : JSON.parse(netease.param.ids))[0] || 0).toString().replace('_0', '')) // reduce time cost - tasks = jsonBody.data.map(item => inject(item)) - } - return Promise.all(tasks).catch(() => {}) + if (!Array.isArray(jsonBody.data)) { + tasks = [inject(jsonBody.data)] + } else if (netease.path.includes('download')) { + jsonBody.data = jsonBody.data[0] + tasks = [inject(jsonBody.data)] + } else { + target = netease.web + ? 0 + : parseInt( + ( + (Array.isArray(netease.param.ids) + ? netease.param.ids + : JSON.parse(netease.param.ids))[0] || 0 + ) + .toString() + .replace('_0', '') + ) // reduce time cost + tasks = jsonBody.data.map(item => inject(item)) + } + return Promise.all(tasks).catch(() => {}) } -module.exports = hook \ No newline at end of file +module.exports = hook diff --git a/packages/unblock/src/kwDES.js b/packages/unblock/src/kwDES.js index ebdcaab3..96647d4b 100644 --- a/packages/unblock/src/kwDES.js +++ b/packages/unblock/src/kwDES.js @@ -4,63 +4,237 @@ https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java */ -const Long = ( - typeof(BigInt) === 'function' // BigInt support in Node 10+ - ? n => (n = BigInt(n), ({ - low: Number(n), - valueOf: () => n.valueOf(), - toString: () => n.toString(), - not: () => Long(~n), - isNegative: () => n < 0, - or: x => Long(n | BigInt(x)), - and: x => Long(n & BigInt(x)), - xor: x => Long(n ^ BigInt(x)), - equals: x => n === BigInt(x), - multiply: x => Long(n * BigInt(x)), - shiftLeft: x => Long(n << BigInt(x)), - shiftRight: x => Long(n >> BigInt(x)), - })) - : (...args) => new (require('long'))(...args) -) +const Long = + typeof BigInt === 'function' // BigInt support in Node 10+ + ? n => ( + (n = BigInt(n)), + { + low: Number(n), + valueOf: () => n.valueOf(), + toString: () => n.toString(), + not: () => Long(~n), + isNegative: () => n < 0, + or: x => Long(n | BigInt(x)), + and: x => Long(n & BigInt(x)), + xor: x => Long(n ^ BigInt(x)), + equals: x => n === BigInt(x), + multiply: x => Long(n * BigInt(x)), + shiftLeft: x => Long(n << BigInt(x)), + shiftRight: x => Long(n >> BigInt(x)) + } + ) + : (...args) => new (require('long'))(...args) const range = n => Array.from(new Array(n).keys()) -const power = (base, index) => Array(index).fill().reduce((result) => result.multiply(base), Long(1)) -const LongArray = (...array) => array.map(n => n === -1 ? Long(-1, -1) : Long(n)) +const power = (base, index) => + Array(index) + .fill() + .reduce(result => result.multiply(base), Long(1)) +const LongArray = (...array) => + array.map(n => (n === -1 ? Long(-1, -1) : Long(n))) // EXPANSION const arrayE = LongArray( - 31, 0, 1, 2, 3, 4, -1, -1, - 3, 4, 5, 6, 7, 8, -1, -1, - 7, 8, 9, 10, 11, 12, -1, -1, - 11, 12, 13, 14, 15, 16, -1, -1, - 15, 16, 17, 18, 19, 20, -1, -1, - 19, 20, 21, 22, 23, 24, -1, -1, - 23, 24, 25, 26, 27, 28, -1, -1, - 27, 28, 29, 30, 31, 30, -1, -1 + 31, + 0, + 1, + 2, + 3, + 4, + -1, + -1, + 3, + 4, + 5, + 6, + 7, + 8, + -1, + -1, + 7, + 8, + 9, + 10, + 11, + 12, + -1, + -1, + 11, + 12, + 13, + 14, + 15, + 16, + -1, + -1, + 15, + 16, + 17, + 18, + 19, + 20, + -1, + -1, + 19, + 20, + 21, + 22, + 23, + 24, + -1, + -1, + 23, + 24, + 25, + 26, + 27, + 28, + -1, + -1, + 27, + 28, + 29, + 30, + 31, + 30, + -1, + -1 ) // INITIAL_PERMUTATION const arrayIP = LongArray( - 57, 49, 41, 33, 25, 17, 9, 1, - 59, 51, 43, 35, 27, 19, 11, 3, - 61, 53, 45, 37, 29, 21, 13, 5, - 63, 55, 47, 39, 31, 23, 15, 7, - 56, 48, 40, 32, 24, 16, 8, 0, - 58, 50, 42, 34, 26, 18, 10, 2, - 60, 52, 44, 36, 28, 20, 12, 4, - 62, 54, 46, 38, 30, 22, 14, 6 + 57, + 49, + 41, + 33, + 25, + 17, + 9, + 1, + 59, + 51, + 43, + 35, + 27, + 19, + 11, + 3, + 61, + 53, + 45, + 37, + 29, + 21, + 13, + 5, + 63, + 55, + 47, + 39, + 31, + 23, + 15, + 7, + 56, + 48, + 40, + 32, + 24, + 16, + 8, + 0, + 58, + 50, + 42, + 34, + 26, + 18, + 10, + 2, + 60, + 52, + 44, + 36, + 28, + 20, + 12, + 4, + 62, + 54, + 46, + 38, + 30, + 22, + 14, + 6 ) // INVERSE_PERMUTATION const arrayIP_1 = LongArray( - 39, 7, 47, 15, 55, 23, 63, 31, - 38, 6, 46, 14, 54, 22, 62, 30, - 37, 5, 45, 13, 53, 21, 61, 29, - 36, 4, 44, 12, 52, 20, 60, 28, - 35, 3, 43, 11, 51, 19, 59, 27, - 34, 2, 42, 10, 50, 18, 58, 26, - 33, 1, 41, 9, 49, 17, 57, 25, - 32, 0, 40, 8, 48, 16, 56, 24 + 39, + 7, + 47, + 15, + 55, + 23, + 63, + 31, + 38, + 6, + 46, + 14, + 54, + 22, + 62, + 30, + 37, + 5, + 45, + 13, + 53, + 21, + 61, + 29, + 36, + 4, + 44, + 12, + 52, + 20, + 60, + 28, + 35, + 3, + 43, + 11, + 51, + 19, + 59, + 27, + 34, + 2, + 42, + 10, + 50, + 18, + 58, + 26, + 33, + 1, + 41, + 9, + 49, + 17, + 57, + 25, + 32, + 0, + 40, + 8, + 48, + 16, + 56, + 24 ) // ROTATES @@ -71,216 +245,823 @@ arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1) // PERMUTATION const arrayP = LongArray( - 15, 6, 19, 20, 28, 11, 27, 16, - 0, 14, 22, 25, 4, 17, 30, 9, - 1, 7, 23, 13, 31, 26, 2, 8, - 18, 12, 29, 5, 21, 10, 3, 24 + 15, + 6, + 19, + 20, + 28, + 11, + 27, + 16, + 0, + 14, + 22, + 25, + 4, + 17, + 30, + 9, + 1, + 7, + 23, + 13, + 31, + 26, + 2, + 8, + 18, + 12, + 29, + 5, + 21, + 10, + 3, + 24 ) // PERMUTED_CHOICE1 const arrayPC_1 = LongArray( - 56, 48, 40, 32, 24, 16, 8, 0, - 57, 49, 41, 33, 25, 17, 9, 1, - 58, 50, 42, 34, 26, 18, 10, 2, - 59, 51, 43, 35, 62, 54, 46, 38, - 30, 22, 14, 6, 61, 53, 45, 37, - 29, 21, 13, 5, 60, 52, 44, 36, - 28, 20, 12, 4, 27, 19, 11, 3 + 56, + 48, + 40, + 32, + 24, + 16, + 8, + 0, + 57, + 49, + 41, + 33, + 25, + 17, + 9, + 1, + 58, + 50, + 42, + 34, + 26, + 18, + 10, + 2, + 59, + 51, + 43, + 35, + 62, + 54, + 46, + 38, + 30, + 22, + 14, + 6, + 61, + 53, + 45, + 37, + 29, + 21, + 13, + 5, + 60, + 52, + 44, + 36, + 28, + 20, + 12, + 4, + 27, + 19, + 11, + 3 ) // PERMUTED_CHOICE2 const arrayPC_2 = LongArray( - 13, 16, 10, 23, 0, 4, -1, -1, - 2, 27, 14, 5, 20, 9, -1, -1, - 22, 18, 11, 3, 25, 7, -1, -1, - 15, 6, 26, 19, 12, 1, -1, -1, - 40, 51, 30, 36, 46, 54, -1, -1, - 29, 39, 50, 44, 32, 47, -1, -1, - 43, 48, 38, 55, 33, 52, -1, -1, - 45, 41, 49, 35, 28, 31, -1, -1 + 13, + 16, + 10, + 23, + 0, + 4, + -1, + -1, + 2, + 27, + 14, + 5, + 20, + 9, + -1, + -1, + 22, + 18, + 11, + 3, + 25, + 7, + -1, + -1, + 15, + 6, + 26, + 19, + 12, + 1, + -1, + -1, + 40, + 51, + 30, + 36, + 46, + 54, + -1, + -1, + 29, + 39, + 50, + 44, + 32, + 47, + -1, + -1, + 43, + 48, + 38, + 55, + 33, + 52, + -1, + -1, + 45, + 41, + 49, + 35, + 28, + 31, + -1, + -1 ) -const matrixNSBox = [[ - 14, 4, 3, 15, 2, 13, 5, 3, - 13, 14, 6, 9, 11, 2, 0, 5, - 4, 1, 10, 12, 15, 6, 9, 10, - 1, 8, 12, 7, 8, 11, 7, 0, - 0, 15, 10, 5, 14, 4, 9, 10, - 7, 8, 12, 3, 13, 1, 3, 6, - 15, 12, 6, 11, 2, 9, 5, 0, - 4, 2, 11, 14, 1, 7, 8, 13, ], [ - 15, 0, 9, 5, 6, 10, 12, 9, - 8, 7, 2, 12, 3, 13, 5, 2, - 1, 14, 7, 8, 11, 4, 0, 3, - 14, 11, 13, 6, 4, 1, 10, 15, - 3, 13, 12, 11, 15, 3, 6, 0, - 4, 10, 1, 7, 8, 4, 11, 14, - 13, 8, 0, 6, 2, 15, 9, 5, - 7, 1, 10, 12, 14, 2, 5, 9, ], [ - 10, 13, 1, 11, 6, 8, 11, 5, - 9, 4, 12, 2, 15, 3, 2, 14, - 0, 6, 13, 1, 3, 15, 4, 10, - 14, 9, 7, 12, 5, 0, 8, 7, - 13, 1, 2, 4, 3, 6, 12, 11, - 0, 13, 5, 14, 6, 8, 15, 2, - 7, 10, 8, 15, 4, 9, 11, 5, - 9, 0, 14, 3, 10, 7, 1, 12, ], [ - 7, 10, 1, 15, 0, 12, 11, 5, - 14, 9, 8, 3, 9, 7, 4, 8, - 13, 6, 2, 1, 6, 11, 12, 2, - 3, 0, 5, 14, 10, 13, 15, 4, - 13, 3, 4, 9, 6, 10, 1, 12, - 11, 0, 2, 5, 0, 13, 14, 2, - 8, 15, 7, 4, 15, 1, 10, 7, - 5, 6, 12, 11, 3, 8, 9, 14, ], [ - 2, 4, 8, 15, 7, 10, 13, 6, - 4, 1, 3, 12, 11, 7, 14, 0, - 12, 2, 5, 9, 10, 13, 0, 3, - 1, 11, 15, 5, 6, 8, 9, 14, - 14, 11, 5, 6, 4, 1, 3, 10, - 2, 12, 15, 0, 13, 2, 8, 5, - 11, 8, 0, 15, 7, 14, 9, 4, - 12, 7, 10, 9, 1, 13, 6, 3, ], [ - 12, 9, 0, 7, 9, 2, 14, 1, - 10, 15, 3, 4, 6, 12, 5, 11, - 1, 14, 13, 0, 2, 8, 7, 13, - 15, 5, 4, 10, 8, 3, 11, 6, - 10, 4, 6, 11, 7, 9, 0, 6, - 4, 2, 13, 1, 9, 15, 3, 8, - 15, 3, 1, 14, 12, 5, 11, 0, - 2, 12, 14, 7, 5, 10, 8, 13, ], [ - 4, 1, 3, 10, 15, 12, 5, 0, - 2, 11, 9, 6, 8, 7, 6, 9, - 11, 4, 12, 15, 0, 3, 10, 5, - 14, 13, 7, 8, 13, 14, 1, 2, - 13, 6, 14, 9, 4, 1, 2, 14, - 11, 13, 5, 0, 1, 10, 8, 3, - 0, 11, 3, 5, 9, 4, 15, 2, - 7, 8, 12, 15, 10, 7, 6, 12, ], [ - 13, 7, 10, 0, 6, 9, 5, 15, - 8, 4, 3, 10, 11, 14, 12, 5, - 2, 11, 9, 6, 15, 12, 0, 3, - 4, 1, 14, 13, 1, 2, 7, 8, - 1, 2, 12, 15, 10, 4, 0, 3, - 13, 14, 6, 9, 7, 8, 9, 6, - 15, 1, 5, 12, 3, 10, 14, 5, - 8, 7, 11, 0, 4, 13, 2, 11, ], +const matrixNSBox = [ + [ + 14, + 4, + 3, + 15, + 2, + 13, + 5, + 3, + 13, + 14, + 6, + 9, + 11, + 2, + 0, + 5, + 4, + 1, + 10, + 12, + 15, + 6, + 9, + 10, + 1, + 8, + 12, + 7, + 8, + 11, + 7, + 0, + 0, + 15, + 10, + 5, + 14, + 4, + 9, + 10, + 7, + 8, + 12, + 3, + 13, + 1, + 3, + 6, + 15, + 12, + 6, + 11, + 2, + 9, + 5, + 0, + 4, + 2, + 11, + 14, + 1, + 7, + 8, + 13 + ], + [ + 15, + 0, + 9, + 5, + 6, + 10, + 12, + 9, + 8, + 7, + 2, + 12, + 3, + 13, + 5, + 2, + 1, + 14, + 7, + 8, + 11, + 4, + 0, + 3, + 14, + 11, + 13, + 6, + 4, + 1, + 10, + 15, + 3, + 13, + 12, + 11, + 15, + 3, + 6, + 0, + 4, + 10, + 1, + 7, + 8, + 4, + 11, + 14, + 13, + 8, + 0, + 6, + 2, + 15, + 9, + 5, + 7, + 1, + 10, + 12, + 14, + 2, + 5, + 9 + ], + [ + 10, + 13, + 1, + 11, + 6, + 8, + 11, + 5, + 9, + 4, + 12, + 2, + 15, + 3, + 2, + 14, + 0, + 6, + 13, + 1, + 3, + 15, + 4, + 10, + 14, + 9, + 7, + 12, + 5, + 0, + 8, + 7, + 13, + 1, + 2, + 4, + 3, + 6, + 12, + 11, + 0, + 13, + 5, + 14, + 6, + 8, + 15, + 2, + 7, + 10, + 8, + 15, + 4, + 9, + 11, + 5, + 9, + 0, + 14, + 3, + 10, + 7, + 1, + 12 + ], + [ + 7, + 10, + 1, + 15, + 0, + 12, + 11, + 5, + 14, + 9, + 8, + 3, + 9, + 7, + 4, + 8, + 13, + 6, + 2, + 1, + 6, + 11, + 12, + 2, + 3, + 0, + 5, + 14, + 10, + 13, + 15, + 4, + 13, + 3, + 4, + 9, + 6, + 10, + 1, + 12, + 11, + 0, + 2, + 5, + 0, + 13, + 14, + 2, + 8, + 15, + 7, + 4, + 15, + 1, + 10, + 7, + 5, + 6, + 12, + 11, + 3, + 8, + 9, + 14 + ], + [ + 2, + 4, + 8, + 15, + 7, + 10, + 13, + 6, + 4, + 1, + 3, + 12, + 11, + 7, + 14, + 0, + 12, + 2, + 5, + 9, + 10, + 13, + 0, + 3, + 1, + 11, + 15, + 5, + 6, + 8, + 9, + 14, + 14, + 11, + 5, + 6, + 4, + 1, + 3, + 10, + 2, + 12, + 15, + 0, + 13, + 2, + 8, + 5, + 11, + 8, + 0, + 15, + 7, + 14, + 9, + 4, + 12, + 7, + 10, + 9, + 1, + 13, + 6, + 3 + ], + [ + 12, + 9, + 0, + 7, + 9, + 2, + 14, + 1, + 10, + 15, + 3, + 4, + 6, + 12, + 5, + 11, + 1, + 14, + 13, + 0, + 2, + 8, + 7, + 13, + 15, + 5, + 4, + 10, + 8, + 3, + 11, + 6, + 10, + 4, + 6, + 11, + 7, + 9, + 0, + 6, + 4, + 2, + 13, + 1, + 9, + 15, + 3, + 8, + 15, + 3, + 1, + 14, + 12, + 5, + 11, + 0, + 2, + 12, + 14, + 7, + 5, + 10, + 8, + 13 + ], + [ + 4, + 1, + 3, + 10, + 15, + 12, + 5, + 0, + 2, + 11, + 9, + 6, + 8, + 7, + 6, + 9, + 11, + 4, + 12, + 15, + 0, + 3, + 10, + 5, + 14, + 13, + 7, + 8, + 13, + 14, + 1, + 2, + 13, + 6, + 14, + 9, + 4, + 1, + 2, + 14, + 11, + 13, + 5, + 0, + 1, + 10, + 8, + 3, + 0, + 11, + 3, + 5, + 9, + 4, + 15, + 2, + 7, + 8, + 12, + 15, + 10, + 7, + 6, + 12 + ], + [ + 13, + 7, + 10, + 0, + 6, + 9, + 5, + 15, + 8, + 4, + 3, + 10, + 11, + 14, + 12, + 5, + 2, + 11, + 9, + 6, + 15, + 12, + 0, + 3, + 4, + 1, + 14, + 13, + 1, + 2, + 7, + 8, + 1, + 2, + 12, + 15, + 10, + 4, + 0, + 3, + 13, + 14, + 6, + 9, + 7, + 8, + 9, + 6, + 15, + 1, + 5, + 12, + 3, + 10, + 14, + 5, + 8, + 7, + 11, + 0, + 4, + 13, + 2, + 11 + ] ] -const bitTransform = (arrInt, n, l) => { // int[], int, long : long - let l2 = Long(0) - range(n).forEach(i => { - if (arrInt[i].isNegative() || (l.and(arrayMask[arrInt[i].low]).equals(0))) - return - l2 = l2.or(arrayMask[i]) - }) - return l2 +const bitTransform = (arrInt, n, l) => { + // int[], int, long : long + let l2 = Long(0) + range(n).forEach(i => { + if (arrInt[i].isNegative() || l.and(arrayMask[arrInt[i].low]).equals(0)) + return + l2 = l2.or(arrayMask[i]) + }) + return l2 } -const DES64 = (longs, l) => { // long[], long - let out = Long(0) - let SOut = Long(0) - const pR = range(8).map(() => Long(0)) - const pSource = [Long(0), Long(0)] - let L = Long(0) - let R = Long(0) - out = bitTransform(arrayIP, 64, l) - pSource[0] = out.and(0xFFFFFFFF) - pSource[1] = out.and(-4294967296).shiftRight(32) +const DES64 = (longs, l) => { + // long[], long + let out = Long(0) + let SOut = Long(0) + const pR = range(8).map(() => Long(0)) + const pSource = [Long(0), Long(0)] + let L = Long(0) + let R = Long(0) + out = bitTransform(arrayIP, 64, l) + pSource[0] = out.and(0xffffffff) + pSource[1] = out.and(-4294967296).shiftRight(32) - range(16).forEach(i => { - R = Long(pSource[1]) - R = bitTransform(arrayE, 64, R) - R = R.xor(longs[i]) - range(8).forEach(j => { - pR[j] = R.shiftRight(j * 8).and(255) - }) - SOut = Long(0) - range(8).reverse().forEach(sbi => { - SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]) - }) - R = bitTransform(arrayP, 32, SOut) - L = Long(pSource[0]) - pSource[0] = Long(pSource[1]) - pSource[1] = L.xor(R) - }) - pSource.reverse() - out = pSource[1].shiftLeft(32).and(-4294967296).or( - pSource[0].and(0xFFFFFFFF) - ) - out = bitTransform(arrayIP_1, 64, out) - return out + range(16).forEach(i => { + R = Long(pSource[1]) + R = bitTransform(arrayE, 64, R) + R = R.xor(longs[i]) + range(8).forEach(j => { + pR[j] = R.shiftRight(j * 8).and(255) + }) + SOut = Long(0) + range(8) + .reverse() + .forEach(sbi => { + SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]) + }) + R = bitTransform(arrayP, 32, SOut) + L = Long(pSource[0]) + pSource[0] = Long(pSource[1]) + pSource[1] = L.xor(R) + }) + pSource.reverse() + out = pSource[1] + .shiftLeft(32) + .and(-4294967296) + .or(pSource[0].and(0xffffffff)) + out = bitTransform(arrayIP_1, 64, out) + return out } - -const subKeys = (l, longs, n) => { // long, long[], int - let l2 = bitTransform(arrayPC_1, 56, l) - range(16).forEach(i => { - l2 = ( - l2.and(arrayLsMask[arrayLs[i]]).shiftLeft(28 - arrayLs[i]).or( - l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i]) - ) - ) - longs[i] = bitTransform(arrayPC_2, 64, l2) - }) - if (n === 1) { - range(8).forEach(j => { - [longs[j], longs[15 - j]] = [longs[15 - j], longs[j]] - }) - } +const subKeys = (l, longs, n) => { + // long, long[], int + let l2 = bitTransform(arrayPC_1, 56, l) + range(16).forEach(i => { + l2 = l2 + .and(arrayLsMask[arrayLs[i]]) + .shiftLeft(28 - arrayLs[i]) + .or(l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i])) + longs[i] = bitTransform(arrayPC_2, 64, l2) + }) + if (n === 1) { + range(8).forEach(j => { + ;[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]] + }) + } } const crypt = (msg, key, mode) => { - // 处理密钥块 - let l = Long(0) - range(8).forEach(i => { - l = Long(key[i]).shiftLeft(i * 8).or(l) - }) + // 处理密钥块 + let l = Long(0) + range(8).forEach(i => { + l = Long(key[i]) + .shiftLeft(i * 8) + .or(l) + }) + + const j = Math.floor(msg.length / 8) + // arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了 - const j = Math.floor(msg.length / 8) - // arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了 - - const arrLong1 = range(16).map(() => Long(0)) - subKeys(l, arrLong1, mode) + const arrLong1 = range(16).map(() => Long(0)) + subKeys(l, arrLong1, mode) - // arrLong2 存放的是前部分的明文 - const arrLong2 = range(j).map(() => Long(0)) + // arrLong2 存放的是前部分的明文 + const arrLong2 = range(j).map(() => Long(0)) - range(j).forEach(m => { - range(8).forEach(n => { - arrLong2[m] = Long(msg[n + m * 8]).shiftLeft(n * 8).or(arrLong2[m]) - }) - }) + range(j).forEach(m => { + range(8).forEach(n => { + arrLong2[m] = Long(msg[n + m * 8]) + .shiftLeft(n * 8) + .or(arrLong2[m]) + }) + }) - // 用于存放密文 - const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0)) + // 用于存放密文 + const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0)) - // 计算前部的数据块(除了最后一部分) - range(j).forEach(i1 => { - arrLong3[i1] = DES64(arrLong1, arrLong2[i1]) - }) + // 计算前部的数据块(除了最后一部分) + range(j).forEach(i1 => { + arrLong3[i1] = DES64(arrLong1, arrLong2[i1]) + }) - // 保存多出来的字节 - const arrByte1 = msg.slice(j * 8) - let l2 = Long(0) + // 保存多出来的字节 + const arrByte1 = msg.slice(j * 8) + let l2 = Long(0) - range(msg.length % 8).forEach(i1 => { - l2 = Long(arrByte1[i1]).shiftLeft(i1 * 8).or(l2) - }) + range(msg.length % 8).forEach(i1 => { + l2 = Long(arrByte1[i1]) + .shiftLeft(i1 * 8) + .or(l2) + }) - // 计算多出的那一位(最后一位) - if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2) // 解密不需要 + // 计算多出的那一位(最后一位) + if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2) // 解密不需要 - // 将密文转为字节型 - const arrByte2 = range(8 * arrLong3.length).map(() => 0) - let i4 = 0 - arrLong3.forEach(l3 => { - range(8).forEach(i6 => { - arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low - i4 += 1 - }) - }) - return Buffer.from(arrByte2) + // 将密文转为字节型 + const arrByte2 = range(8 * arrLong3.length).map(() => 0) + let i4 = 0 + arrLong3.forEach(l3 => { + range(8).forEach(i6 => { + arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low + i4 += 1 + }) + }) + return Buffer.from(arrByte2) } const SECRET_KEY = Buffer.from('ylzsxkwm') @@ -288,4 +1069,4 @@ const encrypt = msg => crypt(msg, SECRET_KEY, 0) const decrypt = msg => crypt(msg, SECRET_KEY, 1) const encryptQuery = query => encrypt(Buffer.from(query)).toString('base64') -module.exports = {encrypt, decrypt, encryptQuery} \ No newline at end of file +module.exports = { encrypt, decrypt, encryptQuery } diff --git a/packages/unblock/src/provider/baidu.js b/packages/unblock/src/provider/baidu.js index 1fac1a8d..8dc0aabd 100644 --- a/packages/unblock/src/provider/baidu.js +++ b/packages/unblock/src/provider/baidu.js @@ -4,47 +4,47 @@ const select = require('./select') const request = require('../request') const format = song => { - const artistId = song.all_artist_id.split(',') - return { - id: song.song_id, - name: song.title, - album: {id: song.album_id, name: song.album_title}, - artists: song.author.split(',').map((name, index) => ({id: artistId[index], name})) - } + const artistId = song.all_artist_id.split(',') + return { + id: song.song_id, + name: song.title, + album: { id: song.album_id, name: song.album_title }, + artists: song.author + .split(',') + .map((name, index) => ({ id: artistId[index], name })) + } } const search = info => { - const url = - 'http://musicapi.taihe.com/v1/restserver/ting?' + - 'from=qianqianmini&method=baidu.ting.search.merge&' + - 'isNew=1&platform=darwin&page_no=1&page_size=30&' + - `query=${encodeURIComponent(info.keyword)}&version=11.2.1` + const url = + 'http://musicapi.taihe.com/v1/restserver/ting?' + + 'from=qianqianmini&method=baidu.ting.search.merge&' + + 'isNew=1&platform=darwin&page_no=1&page_size=30&' + + `query=${encodeURIComponent(info.keyword)}&version=11.2.1` - return request('GET', url) - .then(response => response.json()) - .then(jsonBody => { - const list = jsonBody.result.song_info.song_list.map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + const list = jsonBody.result.song_info.song_list.map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) } const track = id => { - const url = - 'http://music.taihe.com/data/music/fmlink?' + - 'songIds=' + id + '&type=mp3' + const url = + 'http://music.taihe.com/data/music/fmlink?' + 'songIds=' + id + '&type=mp3' - return request('GET', url) - .then(response => response.json()) - .then(jsonBody => { - if ('songList' in jsonBody.data) - return jsonBody.data.songList[0].songLink || Promise.reject() - else - return Promise.reject() - }) - .catch(() => insure().baidu.track(id)) + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + if ('songList' in jsonBody.data) + return jsonBody.data.songList[0].songLink || Promise.reject() + else return Promise.reject() + }) + .catch(() => insure().baidu.track(id)) } const check = info => cache(search, info).then(track) -module.exports = {check} \ No newline at end of file +module.exports = { check } diff --git a/packages/unblock/src/provider/find.js b/packages/unblock/src/provider/find.js index 162f6349..e16ca374 100644 --- a/packages/unblock/src/provider/find.js +++ b/packages/unblock/src/provider/find.js @@ -1,34 +1,50 @@ const cache = require('../cache') const request = require('../request') -const filter = (object, keys) => Object.keys(object).reduce((result, key) => Object.assign(result, keys.includes(key) && {[key]: object[key]}), {}) +const filter = (object, keys) => + Object.keys(object).reduce( + (result, key) => + Object.assign(result, keys.includes(key) && { [key]: object[key] }), + {} + ) // Object.keys(object).filter(key => !keys.includes(key)).forEach(key => delete object[key]) const limit = text => { - const output = [text[0]] - const length = () => output.reduce((sum, token) => sum + token.length, 0) - text.slice(1).some(token => length() > 15 ? true : (output.push(token), false)) - return output + const output = [text[0]] + const length = () => output.reduce((sum, token) => sum + token.length, 0) + text + .slice(1) + .some(token => (length() > 15 ? true : (output.push(token), false))) + return output } const find = id => { - const url = - 'https://music.163.com/api/song/detail?ids=[' + id + ']' + const url = 'https://music.163.com/api/song/detail?ids=[' + id + ']' - return request('GET', url) - .then(response => response.json()) - .then(jsonBody => { - const info = filter(jsonBody.songs[0], ['id', 'name', 'alias', 'duration']) - info.name = (info.name || '') - .replace(/(\s*cover[::\s][^)]+)/i, '') - .replace(/\(\s*cover[::\s][^\)]+\)/i, '') - .replace(/(\s*翻自[::\s][^)]+)/, '') - .replace(/\(\s*翻自[::\s][^\)]+\)/, '') - info.album = filter(jsonBody.songs[0].album, ['id', 'name']) - info.artists = jsonBody.songs[0].artists.map(artist => filter(artist, ['id', 'name'])) - info.keyword = info.name + ' - ' + limit(info.artists.map(artist => artist.name)).join(' / ') - return info.name ? info : Promise.reject() - }) + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + const info = filter(jsonBody.songs[0], [ + 'id', + 'name', + 'alias', + 'duration' + ]) + info.name = (info.name || '') + .replace(/(\s*cover[::\s][^)]+)/i, '') + .replace(/\(\s*cover[::\s][^\)]+\)/i, '') + .replace(/(\s*翻自[::\s][^)]+)/, '') + .replace(/\(\s*翻自[::\s][^\)]+\)/, '') + info.album = filter(jsonBody.songs[0].album, ['id', 'name']) + info.artists = jsonBody.songs[0].artists.map(artist => + filter(artist, ['id', 'name']) + ) + info.keyword = + info.name + + ' - ' + + limit(info.artists.map(artist => artist.name)).join(' / ') + return info.name ? info : Promise.reject() + }) } -module.exports = id => cache(find, id) \ No newline at end of file +module.exports = id => cache(find, id) diff --git a/packages/unblock/src/provider/insure.js b/packages/unblock/src/provider/insure.js index 8fba78e1..4e54d0a5 100644 --- a/packages/unblock/src/provider/insure.js +++ b/packages/unblock/src/provider/insure.js @@ -2,19 +2,22 @@ const request = require('../request') const host = null // 'http://localhost:9000' module.exports = () => { - const proxy = new Proxy(() => {}, { - get: (target, property) => { - target.route = (target.route || []).concat(property) - return proxy - }, - apply: (target, _, payload) => { - if (module.exports.disable || !host) return Promise.reject() - const path = target.route.join('/') - const query = typeof(payload[0]) === 'object' ? JSON.stringify(payload[0]) : payload[0] - // if (path != 'qq/ticket') return Promise.reject() - return request('GET', `${host}/${path}?${encodeURIComponent(query)}`) - .then(response => response.body()) - } - }) - return proxy -} \ No newline at end of file + const proxy = new Proxy(() => {}, { + get: (target, property) => { + target.route = (target.route || []).concat(property) + return proxy + }, + apply: (target, _, payload) => { + if (module.exports.disable || !host) return Promise.reject() + const path = target.route.join('/') + const query = + typeof payload[0] === 'object' ? JSON.stringify(payload[0]) : payload[0] + // if (path != 'qq/ticket') return Promise.reject() + return request( + 'GET', + `${host}/${path}?${encodeURIComponent(query)}` + ).then(response => response.body()) + } + }) + return proxy +} diff --git a/packages/unblock/src/provider/joox.js b/packages/unblock/src/provider/joox.js index 321c2c40..63690017 100644 --- a/packages/unblock/src/provider/joox.js +++ b/packages/unblock/src/provider/joox.js @@ -5,63 +5,74 @@ const crypto = require('../crypto') const request = require('../request') const headers = { - 'origin': 'http://www.joox.com', - 'referer': 'http://www.joox.com' + origin: 'http://www.joox.com', + referer: 'http://www.joox.com' } const fit = info => { - if (/[\u0800-\u4e00]/.test(info.name)) //is japanese - return info.name - else - return info.keyword + if (/[\u0800-\u4e00]/.test(info.name)) + //is japanese + return info.name + else return info.keyword } const format = song => { - const {decode} = crypto.base64 - return { - id: song.songid, - name: decode(song.info1 || ''), - duration: song.playtime * 1000, - album: {id: song.albummid, name: decode(song.info3 || '')}, - artists: song.singer_list.map(({id, name}) => ({id, name: decode(name || '')})) - } + const { decode } = crypto.base64 + return { + id: song.songid, + name: decode(song.info1 || ''), + duration: song.playtime * 1000, + album: { id: song.albummid, name: decode(song.info3 || '') }, + artists: song.singer_list.map(({ id, name }) => ({ + id, + name: decode(name || '') + })) + } } const search = info => { - const keyword = fit(info) - const url = - 'http://api-jooxtt.sanook.com/web-fcgi-bin/web_search?' + - 'country=hk&lang=zh_TW&' + - 'search_input=' + encodeURIComponent(keyword) + '&sin=0&ein=30' + const keyword = fit(info) + const url = + 'http://api-jooxtt.sanook.com/web-fcgi-bin/web_search?' + + 'country=hk&lang=zh_TW&' + + 'search_input=' + + encodeURIComponent(keyword) + + '&sin=0&ein=30' - return request('GET', url, headers) - .then(response => response.body()) - .then(body => { - const jsonBody = JSON.parse(body.replace(/'/g, '"')) - const list = jsonBody.itemlist.map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) + return request('GET', url, headers) + .then(response => response.body()) + .then(body => { + const jsonBody = JSON.parse(body.replace(/'/g, '"')) + const list = jsonBody.itemlist.map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) } const track = id => { - const url = - 'http://api.joox.com/web-fcgi-bin/web_get_songinfo?' + - 'songid=' + id + '&country=hk&lang=zh_cn&from_type=-1&' + - 'channel_id=-1&_=' + (new Date).getTime() + const url = + 'http://api.joox.com/web-fcgi-bin/web_get_songinfo?' + + 'songid=' + + id + + '&country=hk&lang=zh_cn&from_type=-1&' + + 'channel_id=-1&_=' + + new Date().getTime() - return request('GET', url, headers) - .then(response => response.jsonp()) - .then(jsonBody => { - const songUrl = (jsonBody.r320Url || jsonBody.r192Url || jsonBody.mp3Url || jsonBody.m4aUrl).replace(/M\d00([\w]+).mp3/, 'M800$1.mp3') - if (songUrl) - return songUrl - else - return Promise.reject() - }) - .catch(() => insure().joox.track(id)) + return request('GET', url, headers) + .then(response => response.jsonp()) + .then(jsonBody => { + const songUrl = ( + jsonBody.r320Url || + jsonBody.r192Url || + jsonBody.mp3Url || + jsonBody.m4aUrl + ).replace(/M\d00([\w]+).mp3/, 'M800$1.mp3') + if (songUrl) return songUrl + else return Promise.reject() + }) + .catch(() => insure().joox.track(id)) } const check = info => cache(search, info).then(track) -module.exports = {check, track} \ No newline at end of file +module.exports = { check, track } diff --git a/packages/unblock/src/provider/kugou.js b/packages/unblock/src/provider/kugou.js index a911be5e..8fa1d0e1 100644 --- a/packages/unblock/src/provider/kugou.js +++ b/packages/unblock/src/provider/kugou.js @@ -5,49 +5,55 @@ const crypto = require('../crypto') const request = require('../request') const format = song => { - const SingerName = song.SingerName.split('、') - return { - id: song.FileHash, - name: song.SongName, - duration: song.Duration * 1000, - album: {id: song.AlbumID, name: song.AlbumName}, - artists: song.SingerId.map((id, index) => ({id, name: SingerName[index]})) - } + const SingerName = song.SingerName.split('、') + return { + id: song.FileHash, + name: song.SongName, + duration: song.Duration * 1000, + album: { id: song.AlbumID, name: song.AlbumName }, + artists: song.SingerId.map((id, index) => ({ id, name: SingerName[index] })) + } } const search = info => { - const url = - 'http://songsearch.kugou.com/song_search_v2?' + - 'keyword=' + encodeURIComponent(info.keyword) + '&page=1' - - return request('GET', url) - .then(response => response.json()) - .then(jsonBody => { - const list = jsonBody.data.lists.map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) - .catch(() => insure().kugou.search(info)) + const url = + 'http://songsearch.kugou.com/song_search_v2?' + + 'keyword=' + + encodeURIComponent(info.keyword) + + '&page=1' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + const list = jsonBody.data.lists.map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) + .catch(() => insure().kugou.search(info)) } const track = id => { - // const url = - // 'http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=' + id - - // return request('GET', url) - // .then(response => response.json()) - // .then(jsonBody => jsonBody.url || Promise.reject()) - - const url = - 'http://trackercdn.kugou.com/i/v2/?' + - 'key=' + crypto.md5.digest(`${id}kgcloudv2`) + '&hash=' + id + '&' + - 'br=hq&appid=1005&pid=2&cmd=25&behavior=play' - - return request('GET', url) - .then(response => response.json()) - .then(jsonBody => jsonBody.url[0] || Promise.reject()) + // const url = + // 'http://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash=' + id + + // return request('GET', url) + // .then(response => response.json()) + // .then(jsonBody => jsonBody.url || Promise.reject()) + + const url = + 'http://trackercdn.kugou.com/i/v2/?' + + 'key=' + + crypto.md5.digest(`${id}kgcloudv2`) + + '&hash=' + + id + + '&' + + 'br=hq&appid=1005&pid=2&cmd=25&behavior=play' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => jsonBody.url[0] || Promise.reject()) } const check = info => cache(search, info).then(track) -module.exports = {check, search} \ No newline at end of file +module.exports = { check, search } diff --git a/packages/unblock/src/provider/kuwo.js b/packages/unblock/src/provider/kuwo.js index 229c5421..504a0f4c 100644 --- a/packages/unblock/src/provider/kuwo.js +++ b/packages/unblock/src/provider/kuwo.js @@ -5,73 +5,94 @@ const crypto = require('../crypto') const request = require('../request') const format = song => ({ - id: song.musicrid.split('_').pop(), - name: song.name, - duration: song.songTimeMinutes.split(':').reduce((minute, second) => minute * 60 + parseFloat(second), 0) * 1000, - album: {id: song.albumid, name: song.album}, - artists: song.artist.split('&').map((name, index) => ({id: index ? null : song.artistid, name})) + id: song.musicrid.split('_').pop(), + name: song.name, + duration: + song.songTimeMinutes + .split(':') + .reduce((minute, second) => minute * 60 + parseFloat(second), 0) * 1000, + album: { id: song.albumid, name: song.album }, + artists: song.artist + .split('&') + .map((name, index) => ({ id: index ? null : song.artistid, name })) }) const search = info => { - // const url = - // // 'http://search.kuwo.cn/r.s?' + - // // 'ft=music&itemset=web_2013&client=kt&' + - // // 'rformat=json&encoding=utf8&' + - // // 'all=' + encodeURIComponent(info.keyword) + '&pn=0&rn=20' - // 'http://search.kuwo.cn/r.s?' + - // 'ft=music&rformat=json&encoding=utf8&' + - // 'rn=8&callback=song&vipver=MUSIC_8.0.3.1&' + - // 'SONGNAME=' + encodeURIComponent(info.name) + '&' + - // 'ARTIST=' + encodeURIComponent(info.artists[0].name) + // const url = + // // 'http://search.kuwo.cn/r.s?' + + // // 'ft=music&itemset=web_2013&client=kt&' + + // // 'rformat=json&encoding=utf8&' + + // // 'all=' + encodeURIComponent(info.keyword) + '&pn=0&rn=20' + // 'http://search.kuwo.cn/r.s?' + + // 'ft=music&rformat=json&encoding=utf8&' + + // 'rn=8&callback=song&vipver=MUSIC_8.0.3.1&' + + // 'SONGNAME=' + encodeURIComponent(info.name) + '&' + + // 'ARTIST=' + encodeURIComponent(info.artists[0].name) - // return request('GET', url) - // .then(response => response.body()) - // .then(body => { - // const jsonBody = eval( - // '(' + body - // .replace(/\n/g, '') - // .match(/try\s*\{[^=]+=\s*(.+?)\s*\}\s*catch/)[1] - // .replace(/;\s*song\s*\(.+\)\s*;\s*/, '') + ')' - // ) - // const matched = jsonBody.abslist[0] - // if (matched) - // return matched.MUSICRID.split('_').pop() - // else - // return Promise.reject() - // }) + // return request('GET', url) + // .then(response => response.body()) + // .then(body => { + // const jsonBody = eval( + // '(' + body + // .replace(/\n/g, '') + // .match(/try\s*\{[^=]+=\s*(.+?)\s*\}\s*catch/)[1] + // .replace(/;\s*song\s*\(.+\)\s*;\s*/, '') + ')' + // ) + // const matched = jsonBody.abslist[0] + // if (matched) + // return matched.MUSICRID.split('_').pop() + // else + // return Promise.reject() + // }) - const keyword = encodeURIComponent(info.keyword.replace(' - ', '')) - const url = `http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=${keyword}&pn=1&rn=30` + const keyword = encodeURIComponent(info.keyword.replace(' - ', '')) + const url = `http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=${keyword}&pn=1&rn=30` - return request('GET', `http://kuwo.cn/search/list?key=${keyword}`) - .then(response => response.headers['set-cookie'].find(line => line.includes('kw_token')).replace(/;.*/, '').split('=').pop()) - .then(token => request('GET', url, {referer: `http://www.kuwo.cn/search/list?key=${keyword}`, csrf: token, cookie: `kw_token=${token}`})) - .then(response => response.json()) - .then(jsonBody => { - const list = jsonBody.data.list.map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) + return request('GET', `http://kuwo.cn/search/list?key=${keyword}`) + .then(response => + response.headers['set-cookie'] + .find(line => line.includes('kw_token')) + .replace(/;.*/, '') + .split('=') + .pop() + ) + .then(token => + request('GET', url, { + referer: `http://www.kuwo.cn/search/list?key=${keyword}`, + csrf: token, + cookie: `kw_token=${token}` + }) + ) + .then(response => response.json()) + .then(jsonBody => { + const list = jsonBody.data.list.map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) } const track = id => { - const url = (crypto.kuwoapi - ? 'http://mobi.kuwo.cn/mobi.s?f=kuwo&q=' + crypto.kuwoapi.encryptQuery( - 'corp=kuwo&p2p=1&type=convert_url2&sig=0&format=' + ['flac', 'mp3'].slice(select.ENABLE_FLAC ? 0 : 1).join('|') + '&rid=' + id - ) - : 'http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_' + id // flac refuse - // : 'http://www.kuwo.cn/url?format=mp3&response=url&type=convert_url3&br=320kmp3&rid=' + id // flac refuse - ) + const url = crypto.kuwoapi + ? 'http://mobi.kuwo.cn/mobi.s?f=kuwo&q=' + + crypto.kuwoapi.encryptQuery( + 'corp=kuwo&p2p=1&type=convert_url2&sig=0&format=' + + ['flac', 'mp3'].slice(select.ENABLE_FLAC ? 0 : 1).join('|') + + '&rid=' + + id + ) + : 'http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_' + + id // flac refuse + // : 'http://www.kuwo.cn/url?format=mp3&response=url&type=convert_url3&br=320kmp3&rid=' + id // flac refuse - return request('GET', url, {'user-agent': 'okhttp/3.10.0'}) - .then(response => response.body()) - .then(body => { - const url = (body.match(/http[^\s$"]+/) || [])[0] - return url || Promise.reject() - }) - .catch(() => insure().kuwo.track(id)) + return request('GET', url, { 'user-agent': 'okhttp/3.10.0' }) + .then(response => response.body()) + .then(body => { + const url = (body.match(/http[^\s$"]+/) || [])[0] + return url || Promise.reject() + }) + .catch(() => insure().kuwo.track(id)) } const check = info => cache(search, info).then(track) -module.exports = {check, track} +module.exports = { check, track } diff --git a/packages/unblock/src/provider/match.js b/packages/unblock/src/provider/match.js index 973787f0..f690bc0e 100644 --- a/packages/unblock/src/provider/match.js +++ b/packages/unblock/src/provider/match.js @@ -2,96 +2,200 @@ const find = require('./find') const request = require('../request') const provider = { - netease: require('./netease'), - qq: require('./qq'), - xiami: require('./xiami'), - baidu: require('./baidu'), - kugou: require('./kugou'), - kuwo: require('./kuwo'), - migu: require('./migu'), - joox: require('./joox'), - youtube: require('./youtube') + netease: require('./netease'), + qq: require('./qq'), + xiami: require('./xiami'), + baidu: require('./baidu'), + kugou: require('./kugou'), + kuwo: require('./kuwo'), + migu: require('./migu'), + joox: require('./joox'), + youtube: require('./youtube') } const match = (id, source) => { - let meta = {} - const candidate = (source || global.source || ['qq', 'kuwo', 'migu']).filter(name => name in provider) - return find(id) - .then(info => { - meta = info - return Promise.all(candidate.map(name => provider[name].check(info).catch(() => {}))) - }) - .then(urls => { - urls = urls.filter(url => url) - return Promise.all(urls.map(url => check(url))) - }) - .then(songs => { - songs = songs.filter(song => song.url) - if (!songs.length) return Promise.reject() - console.log(`[${meta.id}] ${meta.name}\n${songs[0].url}`) - return songs[0] - }) + let meta = {} + const candidate = (source || global.source || ['qq', 'kuwo', 'migu']).filter( + name => name in provider + ) + return find(id) + .then(info => { + meta = info + return Promise.all( + candidate.map(name => provider[name].check(info).catch(() => {})) + ) + }) + .then(urls => { + urls = urls.filter(url => url) + return Promise.all(urls.map(url => check(url))) + }) + .then(songs => { + songs = songs.filter(song => song.url) + if (!songs.length) return Promise.reject() + console.log(`[${meta.id}] ${meta.name}\n${songs[0].url}`) + return songs[0] + }) } const check = url => { - const song = {size: 0, br: null, url: null, md5: null} - return Promise.race([request('GET', url, {'range': 'bytes=0-8191'}), new Promise((_, reject) => setTimeout(() => reject(504), 5 * 1000))]) - .then(response => { - if (!response.statusCode.toString().startsWith('2')) return Promise.reject() - if (url.includes('qq.com')) - song.md5 = response.headers['server-md5'] - else if (url.includes('xiami.net') || url.includes('qianqian.com')) - song.md5 = response.headers['etag'].replace(/"/g, '').toLowerCase() - song.size = parseInt((response.headers['content-range'] || '').split('/').pop() || response.headers['content-length']) || 0 - song.url = response.url.href - return response.headers['content-length'] === '8192' ? response.body(true) : Promise.reject() - }) - .then(data => { - const bitrate = decode(data) - song.br = (bitrate && !isNaN(bitrate)) ? bitrate * 1000 : null - }) - .catch(() => {}) - .then(() => song) + const song = { size: 0, br: null, url: null, md5: null } + return Promise.race([ + request('GET', url, { range: 'bytes=0-8191' }), + new Promise((_, reject) => setTimeout(() => reject(504), 5 * 1000)) + ]) + .then(response => { + if (!response.statusCode.toString().startsWith('2')) + return Promise.reject() + if (url.includes('qq.com')) song.md5 = response.headers['server-md5'] + else if (url.includes('xiami.net') || url.includes('qianqian.com')) + song.md5 = response.headers['etag'].replace(/"/g, '').toLowerCase() + song.size = + parseInt( + (response.headers['content-range'] || '').split('/').pop() || + response.headers['content-length'] + ) || 0 + song.url = response.url.href + return response.headers['content-length'] === '8192' + ? response.body(true) + : Promise.reject() + }) + .then(data => { + const bitrate = decode(data) + song.br = bitrate && !isNaN(bitrate) ? bitrate * 1000 : null + }) + .catch(() => {}) + .then(() => song) } const decode = buffer => { - const map = { - 3: { - 3: ['free', 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 'bad'], - 2: ['free', 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 'bad'], - 1: ['free', 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 'bad'] - }, - 2: { - 3: ['free', 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 'bad'], - 2: ['free', 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 'bad'] - } - } - map[2][1] = map[2][2] - map[0] = map[2] + const map = { + 3: { + 3: [ + 'free', + 32, + 64, + 96, + 128, + 160, + 192, + 224, + 256, + 288, + 320, + 352, + 384, + 416, + 448, + 'bad' + ], + 2: [ + 'free', + 32, + 48, + 56, + 64, + 80, + 96, + 112, + 128, + 160, + 192, + 224, + 256, + 320, + 384, + 'bad' + ], + 1: [ + 'free', + 32, + 40, + 48, + 56, + 64, + 80, + 96, + 112, + 128, + 160, + 192, + 224, + 256, + 320, + 'bad' + ] + }, + 2: { + 3: [ + 'free', + 32, + 48, + 56, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 224, + 256, + 'bad' + ], + 2: [ + 'free', + 8, + 16, + 24, + 32, + 40, + 48, + 56, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 'bad' + ] + } + } + map[2][1] = map[2][2] + map[0] = map[2] - let pointer = 0 - if (buffer.slice(0, 4).toString() === 'fLaC') return 999 - if (buffer.slice(0, 3).toString() === 'ID3') { - pointer = 6 - const size = buffer.slice(pointer, pointer + 4).reduce((summation, value, index) => summation + (value & 0x7f) << (7 * (3 - index)), 0) - pointer = 10 + size - } - const header = buffer.slice(pointer, pointer + 4) + let pointer = 0 + if (buffer.slice(0, 4).toString() === 'fLaC') return 999 + if (buffer.slice(0, 3).toString() === 'ID3') { + pointer = 6 + const size = buffer + .slice(pointer, pointer + 4) + .reduce( + (summation, value, index) => + (summation + (value & 0x7f)) << (7 * (3 - index)), + 0 + ) + pointer = 10 + size + } + const header = buffer.slice(pointer, pointer + 4) - // https://www.allegro.cc/forums/thread/591512/674023 - if ( - header.length === 4 && - header[0] === 0xff && - ((header[1] >> 5) & 0x7) === 0x7 && - ((header[1] >> 1) & 0x3) !== 0 && - ((header[2] >> 4) & 0xf) !== 0xf && - ((header[2] >> 2) & 0x3) !== 0x3 - ) { - const version = (header[1] >> 3) & 0x3 - const layer = (header[1] >> 1) & 0x3 - const bitrate = header[2] >> 4 - return map[version][layer][bitrate] - } + // https://www.allegro.cc/forums/thread/591512/674023 + if ( + header.length === 4 && + header[0] === 0xff && + ((header[1] >> 5) & 0x7) === 0x7 && + ((header[1] >> 1) & 0x3) !== 0 && + ((header[2] >> 4) & 0xf) !== 0xf && + ((header[2] >> 2) & 0x3) !== 0x3 + ) { + const version = (header[1] >> 3) & 0x3 + const layer = (header[1] >> 1) & 0x3 + const bitrate = header[2] >> 4 + return map[version][layer][bitrate] + } } -module.exports = match \ No newline at end of file +module.exports = match diff --git a/packages/unblock/src/provider/migu.js b/packages/unblock/src/provider/migu.js index 1af696bd..3b35a907 100644 --- a/packages/unblock/src/provider/migu.js +++ b/packages/unblock/src/provider/migu.js @@ -5,56 +5,60 @@ const crypto = require('../crypto') const request = require('../request') const headers = { - 'origin': 'http://music.migu.cn/', - 'referer': 'http://music.migu.cn/' + origin: 'http://music.migu.cn/', + referer: 'http://music.migu.cn/' } const format = song => { - const singerId = song.singerId.split(/\s*,\s*/) - const singerName = song.singerName.split(/\s*,\s*/) - return { - id: song.copyrightId, - name: song.title, - album: {id: song.albumId, name: song.albumName}, - artists: singerId.map((id, index) => ({id, name: singerName[index]})) - } + const singerId = song.singerId.split(/\s*,\s*/) + const singerName = song.singerName.split(/\s*,\s*/) + return { + id: song.copyrightId, + name: song.title, + album: { id: song.albumId, name: song.albumName }, + artists: singerId.map((id, index) => ({ id, name: singerName[index] })) + } } const search = info => { - const url = - 'http://m.music.migu.cn/migu/remoting/scr_search_tag?' + - 'keyword=' + encodeURIComponent(info.keyword) + '&type=2&rows=20&pgc=1' - - return request('GET', url) - .then(response => response.json()) - .then(jsonBody => { - const list = ((jsonBody || {}).musics || []).map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) + const url = + 'http://m.music.migu.cn/migu/remoting/scr_search_tag?' + + 'keyword=' + + encodeURIComponent(info.keyword) + + '&type=2&rows=20&pgc=1' + + return request('GET', url) + .then(response => response.json()) + .then(jsonBody => { + const list = ((jsonBody || {}).musics || []).map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) } const single = (id, format) => { - const url = - 'http://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?' + - 'dataType=2&' + crypto.miguapi.encryptBody({copyrightId: id.toString(), type: format}) - - return request('GET', url, headers) - .then(response => response.json()) - .then(jsonBody => { - const {playUrl} = jsonBody.data - return playUrl ? encodeURI(playUrl) : Promise.reject() - }) + const url = + 'http://music.migu.cn/v3/api/music/audioPlayer/getPlayInfo?' + + 'dataType=2&' + + crypto.miguapi.encryptBody({ copyrightId: id.toString(), type: format }) + + return request('GET', url, headers) + .then(response => response.json()) + .then(jsonBody => { + const { playUrl } = jsonBody.data + return playUrl ? encodeURI(playUrl) : Promise.reject() + }) } const track = id => - Promise.all( - [3, 2, 1].slice(select.ENABLE_FLAC ? 0 : 1) - .map(format => single(id, format).catch(() => null)) - ) - .then(result => result.find(url => url) || Promise.reject()) - .catch(() => insure().migu.track(id)) + Promise.all( + [3, 2, 1] + .slice(select.ENABLE_FLAC ? 0 : 1) + .map(format => single(id, format).catch(() => null)) + ) + .then(result => result.find(url => url) || Promise.reject()) + .catch(() => insure().migu.track(id)) const check = info => cache(search, info).then(track) -module.exports = {check, track} \ No newline at end of file +module.exports = { check, track } diff --git a/packages/unblock/src/provider/netease.js b/packages/unblock/src/provider/netease.js index 004b437f..4fd6ca09 100644 --- a/packages/unblock/src/provider/netease.js +++ b/packages/unblock/src/provider/netease.js @@ -3,26 +3,28 @@ const crypto = require('../crypto') const request = require('../request') const search = info => { - const url = - 'http://music.163.com/api/album/' + info.album.id + const url = 'http://music.163.com/api/album/' + info.album.id - return request('GET', url) - .then(response => response.body()) - .then(body => { - const jsonBody = JSON.parse(body.replace(/"dfsId":(\d+)/g, '"dfsId":"$1"')) // for js precision - const matched = jsonBody.album.songs.find(song => song.id === info.id) - if (matched) - return matched.hMusic.dfsId || matched.mMusic.dfsId || matched.lMusic.dfsId - else - return Promise.reject() - }) + return request('GET', url) + .then(response => response.body()) + .then(body => { + const jsonBody = JSON.parse( + body.replace(/"dfsId":(\d+)/g, '"dfsId":"$1"') + ) // for js precision + const matched = jsonBody.album.songs.find(song => song.id === info.id) + if (matched) + return ( + matched.hMusic.dfsId || matched.mMusic.dfsId || matched.lMusic.dfsId + ) + else return Promise.reject() + }) } const track = id => { - if (!id || id === '0') return Promise.reject() - return crypto.uri.retrieve(id) + if (!id || id === '0') return Promise.reject() + return crypto.uri.retrieve(id) } const check = info => cache(search, info).then(track) -module.exports = {check} \ No newline at end of file +module.exports = { check } diff --git a/packages/unblock/src/provider/qq.js b/packages/unblock/src/provider/qq.js index bd641471..3a5711a3 100644 --- a/packages/unblock/src/provider/qq.js +++ b/packages/unblock/src/provider/qq.js @@ -4,150 +4,173 @@ const select = require('./select') const request = require('../request') const headers = { - 'origin': 'http://y.qq.com/', - 'referer': 'http://y.qq.com/', - 'cookie': process.env.QQ_COOKIE || null // 'uin=; qm_keyst=', + origin: 'http://y.qq.com/', + referer: 'http://y.qq.com/', + cookie: process.env.QQ_COOKIE || null // 'uin=; qm_keyst=', } const playable = song => { - const switchFlag = song['switch'].toString(2).split('') - switchFlag.pop() - switchFlag.reverse() - const playFlag = switchFlag[0] - const tryFlag = switchFlag[13] - return ((playFlag == 1) || ((playFlag == 1) && (tryFlag == 1))) + const switchFlag = song['switch'].toString(2).split('') + switchFlag.pop() + switchFlag.reverse() + const playFlag = switchFlag[0] + const tryFlag = switchFlag[13] + return playFlag == 1 || (playFlag == 1 && tryFlag == 1) } const format = song => ({ - id: {song: song.mid, file: song.file.media_mid}, - name: song.name, - duration: song.interval * 1000, - album: {id: song.album.mid, name: song.album.name}, - artists: song.singer.map(({mid, name}) => ({id: mid, name})) + id: { song: song.mid, file: song.file.media_mid }, + name: song.name, + duration: song.interval * 1000, + album: { id: song.album.mid, name: song.album.name }, + artists: song.singer.map(({ mid, name }) => ({ id: mid, name })) }) const search = info => { - const url = - 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + - 'ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&' + - 'searchid=46804741196796149&t=0&aggr=1&cr=1&catZhida=1&lossless=0&' + - 'flag_qc=0&p=1&n=20&w=' + encodeURIComponent(info.keyword) + '&' + - 'g_tk=5381&jsonpCallback=MusicJsonCallback10005317669353331&loginUin=0&hostUin=0&' + - 'format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0' - - return request('GET', url) - .then(response => response.jsonp()) - .then(jsonBody => { - const list = jsonBody.data.song.list.map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) + const url = + 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp?' + + 'ct=24&qqmusic_ver=1298&new_json=1&remoteplace=txt.yqq.center&' + + 'searchid=46804741196796149&t=0&aggr=1&cr=1&catZhida=1&lossless=0&' + + 'flag_qc=0&p=1&n=20&w=' + + encodeURIComponent(info.keyword) + + '&' + + 'g_tk=5381&jsonpCallback=MusicJsonCallback10005317669353331&loginUin=0&hostUin=0&' + + 'format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0' + + return request('GET', url) + .then(response => response.jsonp()) + .then(jsonBody => { + const list = jsonBody.data.song.list.map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) } const single = (id, format) => { - // const classic = ['001yS0N33yPm1B', '000bog5B2DYgHN', '002bongo1BDtKz', '004RDW5Q2ol2jj', '001oEME64eXNbp', '001e9dH11YeXGp', '0021onBk2QNjBu', '001YoUs11jvsIK', '000SNxc91Mw3UQ', '002k94ea4379uy'] - // id = id || classic[Math.floor(classic.length * Math.random())] - const uin = ((headers.cookie || '').match(/uin=(\d+)/) || [])[1] || '0' - - const concatenate = vkey => { - if (!vkey) return Promise.reject() - const host = ['streamoc.music.tc.qq.com', 'mobileoc.music.tc.qq.com', 'isure.stream.qqmusic.qq.com', 'dl.stream.qqmusic.qq.com', 'aqqmusic.tc.qq.com/amobile.music.tc.qq.com'][3] - return `http://${host}/${format.join(id.file)}?vkey=${vkey}&uin=0&fromtag=8&guid=7332953645` - } - - // const url = - // 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' + - // '?g_tk=0&loginUin=0&hostUin=0&format=json&inCharset=utf8' + - // '&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0' + - // '&cid=205361747&uin=0&guid=7332953645' + - // '&songmid='+ id.song + '&filename='+ format.join(id.file) - - // return request('GET', url, headers) - // .then(response => response.json()) - // .then(jsonBody => { - // const {vkey} = jsonBody.data.items[0] - // return concatenate(vkey) - // }) - - const url = - 'https://u.y.qq.com/cgi-bin/musicu.fcg?data=' + - encodeURIComponent(JSON.stringify({ - // req: { - // method: 'GetCdnDispatch', - // module: 'CDN.SrfCdnDispatchServer', - // param: { - // calltype: 0, - // guid: '7332953645', - // userip: '' - // } - // }, - req_0: { - module: 'vkey.GetVkeyServer', - method: 'CgiGetVkey', - param: { - guid: '7332953645', - loginflag: 1, - filename: [format.join(id.file)], - songmid: [id.song], - songtype: [0], - uin, - platform: '20' - } - } - })) - - return request('GET', url, headers) - .then(response => response.json()) - .then(jsonBody => { - const { sip, midurlinfo } = jsonBody.req_0.data - // const vkey = - // jsonBody.req_0.data.midurlinfo[0].vkey || - // (jsonBody.req_0.data.testfile2g.match(/vkey=(\w+)/) || [])[1] - // return concatenate(vkey) - return midurlinfo[0].purl ? sip[0] + midurlinfo[0].purl : Promise.reject() - }) + // const classic = ['001yS0N33yPm1B', '000bog5B2DYgHN', '002bongo1BDtKz', '004RDW5Q2ol2jj', '001oEME64eXNbp', '001e9dH11YeXGp', '0021onBk2QNjBu', '001YoUs11jvsIK', '000SNxc91Mw3UQ', '002k94ea4379uy'] + // id = id || classic[Math.floor(classic.length * Math.random())] + const uin = ((headers.cookie || '').match(/uin=(\d+)/) || [])[1] || '0' + + const concatenate = vkey => { + if (!vkey) return Promise.reject() + const host = [ + 'streamoc.music.tc.qq.com', + 'mobileoc.music.tc.qq.com', + 'isure.stream.qqmusic.qq.com', + 'dl.stream.qqmusic.qq.com', + 'aqqmusic.tc.qq.com/amobile.music.tc.qq.com' + ][3] + return `http://${host}/${format.join( + id.file + )}?vkey=${vkey}&uin=0&fromtag=8&guid=7332953645` + } + + // const url = + // 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' + + // '?g_tk=0&loginUin=0&hostUin=0&format=json&inCharset=utf8' + + // '&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0' + + // '&cid=205361747&uin=0&guid=7332953645' + + // '&songmid='+ id.song + '&filename='+ format.join(id.file) + + // return request('GET', url, headers) + // .then(response => response.json()) + // .then(jsonBody => { + // const {vkey} = jsonBody.data.items[0] + // return concatenate(vkey) + // }) + + const url = + 'https://u.y.qq.com/cgi-bin/musicu.fcg?data=' + + encodeURIComponent( + JSON.stringify({ + // req: { + // method: 'GetCdnDispatch', + // module: 'CDN.SrfCdnDispatchServer', + // param: { + // calltype: 0, + // guid: '7332953645', + // userip: '' + // } + // }, + req_0: { + module: 'vkey.GetVkeyServer', + method: 'CgiGetVkey', + param: { + guid: '7332953645', + loginflag: 1, + filename: [format.join(id.file)], + songmid: [id.song], + songtype: [0], + uin, + platform: '20' + } + } + }) + ) + + return request('GET', url, headers) + .then(response => response.json()) + .then(jsonBody => { + const { sip, midurlinfo } = jsonBody.req_0.data + // const vkey = + // jsonBody.req_0.data.midurlinfo[0].vkey || + // (jsonBody.req_0.data.testfile2g.match(/vkey=(\w+)/) || [])[1] + // return concatenate(vkey) + return midurlinfo[0].purl ? sip[0] + midurlinfo[0].purl : Promise.reject() + }) } const track = id => { - id.key = id.file - return Promise.all( - [['F000', '.flac'], ['M800', '.mp3'], ['M500', '.mp3']].slice((headers.cookie || typeof(window) !== 'undefined') ? (select.ENABLE_FLAC ? 0 : 1) : 2) - .map(format => single(id, format).catch(() => null)) - ) - .then(result => result.find(url => url) || Promise.reject()) - .catch(() => insure().qq.track(id)) - - // return request( - // 'POST', 'http://acc.music.qq.com/base/fcgi-bin/fcg_music_express_mobile2.fcg', {}, - // ` - // 9070003190 - // 4600100100105 - // 02123456782 - // 352iosM800${id}.mp30 - // `.replace(/\s/, '') - // ) - // .then(response => response.body(true)) - // .then(body => { - // const xml = require('zlib').inflateSync(body.slice(5)).toString() - // const focus = xml.match(/(.+)<\/item>/) - // return `http://streamoc.music.tc.qq.com/${focus[1]}?vkey=${focus[2]}&guid=0&uin=12345678&fromtag=6` - // }) - - // const url = - // 'https://i.y.qq.com/v8/playsong.html?ADTAG=newyqq.song&songmid=' + id - - // const mobile = {'user-agent': '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'} - // return request('GET', url, mobile) - // .then(response => response.body()) - // .then(body => { - // const audio = body.match(/]+src="([^"]+)"[^>]*>/) - // if (audio) - // return audio[1].replace(/C400(\w+)\.m4a/, 'M500$1.mp3') - // else - // return Promise.reject() - // }) + id.key = id.file + return Promise.all( + [ + ['F000', '.flac'], + ['M800', '.mp3'], + ['M500', '.mp3'] + ] + .slice( + headers.cookie || typeof window !== 'undefined' + ? select.ENABLE_FLAC + ? 0 + : 1 + : 2 + ) + .map(format => single(id, format).catch(() => null)) + ) + .then(result => result.find(url => url) || Promise.reject()) + .catch(() => insure().qq.track(id)) + + // return request( + // 'POST', 'http://acc.music.qq.com/base/fcgi-bin/fcg_music_express_mobile2.fcg', {}, + // ` + // 9070003190 + // 4600100100105 + // 02123456782 + // 352iosM800${id}.mp30 + // `.replace(/\s/, '') + // ) + // .then(response => response.body(true)) + // .then(body => { + // const xml = require('zlib').inflateSync(body.slice(5)).toString() + // const focus = xml.match(/(.+)<\/item>/) + // return `http://streamoc.music.tc.qq.com/${focus[1]}?vkey=${focus[2]}&guid=0&uin=12345678&fromtag=6` + // }) + + // const url = + // 'https://i.y.qq.com/v8/playsong.html?ADTAG=newyqq.song&songmid=' + id + + // const mobile = {'user-agent': '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'} + // return request('GET', url, mobile) + // .then(response => response.body()) + // .then(body => { + // const audio = body.match(/]+src="([^"]+)"[^>]*>/) + // if (audio) + // return audio[1].replace(/C400(\w+)\.m4a/, 'M500$1.mp3') + // else + // return Promise.reject() + // }) } const check = info => cache(search, info).then(track) -module.exports = {check, track} \ No newline at end of file +module.exports = { check, track } diff --git a/packages/unblock/src/provider/select.js b/packages/unblock/src/provider/select.js index 8119c29e..243df6d6 100644 --- a/packages/unblock/src/provider/select.js +++ b/packages/unblock/src/provider/select.js @@ -1,3 +1,4 @@ module.exports = list => list[0] -module.exports.ENABLE_FLAC = (process.env.ENABLE_FLAC || '').toLowerCase() === 'true' \ No newline at end of file +module.exports.ENABLE_FLAC = + (process.env.ENABLE_FLAC || '').toLowerCase() === 'true' diff --git a/packages/unblock/src/provider/xiami.js b/packages/unblock/src/provider/xiami.js index 3df2c02e..a46d91b0 100644 --- a/packages/unblock/src/provider/xiami.js +++ b/packages/unblock/src/provider/xiami.js @@ -5,42 +5,51 @@ const crypto = require('../crypto') const request = require('../request') const headers = { - // 'origin': 'http://www.xiami.com/', - // 'referer': 'http://www.xiami.com/' - 'referer': 'https://h.xiami.com/' + // 'origin': 'http://www.xiami.com/', + // 'referer': 'http://www.xiami.com/' + referer: 'https://h.xiami.com/' } const format = song => ({ - id: song.song_id, - name: song.song_name, - album: {id: song.album_id, name: song.album_name}, - artists: [{id: song.artist_id, name: song.artist_name}] + id: song.song_id, + name: song.song_name, + album: { id: song.album_id, name: song.album_name }, + artists: [{ id: song.artist_id, name: song.artist_name }] }) const caesar = pattern => { - const height = parseInt(pattern[0]) - pattern = pattern.slice(1) - const width = Math.ceil(pattern.length / height) - const unpad = height - (width * height - pattern.length) - - const matrix = Array.from(Array(height).keys()).map(i => - pattern.slice(i < unpad ? i * width : unpad * width + (i - unpad) * (width - 1)).slice(0, i < unpad ? width : width - 1) - ) - - const transpose = Array.from(Array(width).keys()).map(x => - Array.from(Array(height).keys()).map(y => matrix[y][x]).join('') - ) - - return unescape(transpose.join('')).replace(/\^/g, '0') + const height = parseInt(pattern[0]) + pattern = pattern.slice(1) + const width = Math.ceil(pattern.length / height) + const unpad = height - (width * height - pattern.length) + + const matrix = Array.from(Array(height).keys()).map(i => + pattern + .slice(i < unpad ? i * width : unpad * width + (i - unpad) * (width - 1)) + .slice(0, i < unpad ? width : width - 1) + ) + + const transpose = Array.from(Array(width).keys()).map(x => + Array.from(Array(height).keys()) + .map(y => matrix[y][x]) + .join('') + ) + + return unescape(transpose.join('')).replace(/\^/g, '0') } const token = () => { - return request('GET', 'https://www.xiami.com') - .then(response => - response.headers['set-cookie'].map(line => line.replace(/;.+$/, '')).reduce( - (cookie, line) => (line = line.split(/\s*=\s*/).map(decodeURIComponent), Object.assign(cookie, {[line[0]]: line[1]})), {} - ) - ) + return request('GET', 'https://www.xiami.com').then(response => + response.headers['set-cookie'] + .map(line => line.replace(/;.+$/, '')) + .reduce( + (cookie, line) => ( + (line = line.split(/\s*=\s*/).map(decodeURIComponent)), + Object.assign(cookie, { [line[0]]: line[1] }) + ), + {} + ) + ) } // const search = info => { @@ -64,18 +73,20 @@ const token = () => { // } const search = info => { - const url = - 'http://api.xiami.com/web?v=2.0&app_key=1' + - '&key=' + encodeURIComponent(info.keyword) + '&page=1' + - '&limit=20&callback=jsonp&r=search/songs' - - return request('GET', url, headers) - .then(response => response.jsonp()) - .then(jsonBody => { - const list = jsonBody.data.songs.map(format) - const matched = select(list, info) - return matched ? matched.id : Promise.reject() - }) + const url = + 'http://api.xiami.com/web?v=2.0&app_key=1' + + '&key=' + + encodeURIComponent(info.keyword) + + '&page=1' + + '&limit=20&callback=jsonp&r=search/songs' + + return request('GET', url, headers) + .then(response => response.jsonp()) + .then(jsonBody => { + const list = jsonBody.data.songs.map(format) + const matched = select(list, info) + return matched ? matched.id : Promise.reject() + }) } // const track = id => { @@ -105,18 +116,18 @@ const search = info => { // } const track = id => { - const url = - 'https://api.xiami.com/web?v=2.0&app_key=1' + - '&id=' + id + '&callback=jsonp&r=song/detail' - - return request('GET', url, headers) - .then(response => response.jsonp()) - .then(jsonBody => - jsonBody.data.song.listen_file || Promise.reject() - ) - .catch(() => insure().xiami.track(id)) + const url = + 'https://api.xiami.com/web?v=2.0&app_key=1' + + '&id=' + + id + + '&callback=jsonp&r=song/detail' + + return request('GET', url, headers) + .then(response => response.jsonp()) + .then(jsonBody => jsonBody.data.song.listen_file || Promise.reject()) + .catch(() => insure().xiami.track(id)) } const check = info => cache(search, info).then(track) -module.exports = {check, track} \ No newline at end of file +module.exports = { check, track } diff --git a/packages/unblock/src/provider/youtube.js b/packages/unblock/src/provider/youtube.js index c23ae92a..b8c1c169 100644 --- a/packages/unblock/src/provider/youtube.js +++ b/packages/unblock/src/provider/youtube.js @@ -1,78 +1,107 @@ const cache = require('../cache') const request = require('../request') -const parse = query => (query || '').split('&').reduce((result, item) => (item = item.split('=').map(decodeURIComponent), Object.assign({}, result, {[item[0]]: item[1]})), {}) +const parse = query => + (query || '') + .split('&') + .reduce( + (result, item) => ( + (item = item.split('=').map(decodeURIComponent)), + Object.assign({}, result, { [item[0]]: item[1] }) + ), + {} + ) // const proxy = require('url').parse('http://127.0.0.1:1080') const proxy = undefined const key = process.env.YOUTUBE_KEY || null // YouTube Data API v3 const signature = (id = '-tKVN2mAKRI') => { - const url = - `https://www.youtube.com/watch?v=${id}` + const url = `https://www.youtube.com/watch?v=${id}` - return request('GET', url, {}, null, proxy) - .then(response => response.body()) - .then(body => { - let assets = /"assets":{[^}]+}/.exec(body)[0] - assets = JSON.parse(`{${assets}}`).assets - return request('GET', 'https://youtube.com' + assets.js, {}, null, proxy).then(response => response.body()) - }) - .then(body => { - const [_, funcArg, funcBody] = /function\((\w+)\)\s*{([^}]+split\(""\)[^}]+join\(""\))};/.exec(body) - const helperName = /;(.+?)\..+?\(/.exec(funcBody)[1] - const helperContent = new RegExp(`var ${helperName}={[\\s\\S]+?};`).exec(body)[0] - return new Function([funcArg], helperContent + '\n' + funcBody) - }) + return request('GET', url, {}, null, proxy) + .then(response => response.body()) + .then(body => { + let assets = /"assets":{[^}]+}/.exec(body)[0] + assets = JSON.parse(`{${assets}}`).assets + return request( + 'GET', + 'https://youtube.com' + assets.js, + {}, + null, + proxy + ).then(response => response.body()) + }) + .then(body => { + const [ + _, + funcArg, + funcBody + ] = /function\((\w+)\)\s*{([^}]+split\(""\)[^}]+join\(""\))};/.exec(body) + const helperName = /;(.+?)\..+?\(/.exec(funcBody)[1] + const helperContent = new RegExp(`var ${helperName}={[\\s\\S]+?};`).exec( + body + )[0] + return new Function([funcArg], helperContent + '\n' + funcBody) + }) } const apiSearch = info => { - const url = - `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(info.keyword)}&type=video&key=${key}` + const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent( + info.keyword + )}&type=video&key=${key}` - return request('GET', url, {accept: 'application/json'}, null, proxy) - .then(response => response.json()) - .then(jsonBody => { - const matched = jsonBody.items[0] - if (matched) - return matched.id.videoId - else - return Promise.reject() - }) + return request('GET', url, { accept: 'application/json' }, null, proxy) + .then(response => response.json()) + .then(jsonBody => { + const matched = jsonBody.items[0] + if (matched) return matched.id.videoId + else return Promise.reject() + }) } const search = info => { - const url = - `https://www.youtube.com/results?search_query=${encodeURIComponent(info.keyword)}` + const url = `https://www.youtube.com/results?search_query=${encodeURIComponent( + info.keyword + )}` - return request('GET', url, {}, null, proxy) - .then(response => response.body()) - .then(body => { - const initialData = JSON.parse(body.match(/window\["ytInitialData"\]\s*=\s*([^;]+);/)[1]) - const matched = initialData.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents[0].itemSectionRenderer.contents[0] - if (matched) - return matched.videoRenderer.videoId - else - return Promise.reject() - }) + return request('GET', url, {}, null, proxy) + .then(response => response.body()) + .then(body => { + const initialData = JSON.parse( + body.match(/window\["ytInitialData"\]\s*=\s*([^;]+);/)[1] + ) + const matched = + initialData.contents.twoColumnSearchResultsRenderer.primaryContents + .sectionListRenderer.contents[0].itemSectionRenderer.contents[0] + if (matched) return matched.videoRenderer.videoId + else return Promise.reject() + }) } const track = id => { - const url = - `https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage` + const url = `https://www.youtube.com/get_video_info?video_id=${id}&el=detailpage` - return request('GET', url, {}, null, proxy) - .then(response => response.body()) - .then(body => JSON.parse(parse(body).player_response).streamingData) - .then(streamingData => { - const stream = streamingData.formats.concat(streamingData.adaptiveFormats) - .find(format => format.itag === 140) - // .filter(format => [249, 250, 140, 251].includes(format.itag)) // NetaseMusic PC client do not support webm format - // .sort((a, b) => b.bitrate - a.bitrate)[0] - const target = parse(stream.signatureCipher) - return stream.url || (target.sp.includes('sig') ? cache(signature, undefined, 24 * 60 * 60 * 1000).then(sign => target.url + '&sig=' + sign(target.s)) : target.url) - }) + return request('GET', url, {}, null, proxy) + .then(response => response.body()) + .then(body => JSON.parse(parse(body).player_response).streamingData) + .then(streamingData => { + const stream = streamingData.formats + .concat(streamingData.adaptiveFormats) + .find(format => format.itag === 140) + // .filter(format => [249, 250, 140, 251].includes(format.itag)) // NetaseMusic PC client do not support webm format + // .sort((a, b) => b.bitrate - a.bitrate)[0] + const target = parse(stream.signatureCipher) + return ( + stream.url || + (target.sp.includes('sig') + ? cache(signature, undefined, 24 * 60 * 60 * 1000).then( + sign => target.url + '&sig=' + sign(target.s) + ) + : target.url) + ) + }) } const check = info => cache(key ? apiSearch : search, info).then(track) -module.exports = {check, track} +module.exports = { check, track } diff --git a/packages/unblock/src/request.js b/packages/unblock/src/request.js index a8d6627d..c882a423 100644 --- a/packages/unblock/src/request.js +++ b/packages/unblock/src/request.js @@ -5,93 +5,135 @@ const parse = require('url').parse const translate = host => (global.hosts || {})[host] || host -const create = (url, proxy) => (((typeof(proxy) === 'undefined' ? global.proxy : proxy) || url).protocol === 'https:' ? https : http).request +const create = (url, proxy) => + (((typeof proxy === 'undefined' ? global.proxy : proxy) || url).protocol === + 'https:' + ? https + : http + ).request const configure = (method, url, headers, proxy) => { - headers = headers || {} - proxy = typeof(proxy) === 'undefined' ? global.proxy : proxy - if ('content-length' in headers) delete headers['content-length'] + headers = headers || {} + proxy = typeof proxy === 'undefined' ? global.proxy : proxy + if ('content-length' in headers) delete headers['content-length'] - const options = {} - options._headers = headers - if (proxy && url.protocol === 'https:') { - options.method = 'CONNECT' - options.headers = Object.keys(headers).reduce((result, key) => Object.assign(result, ['host', 'user-agent'].includes(key) && {[key]: headers[key]}), {}) - } - else { - options.method = method - options.headers = headers - } + const options = {} + options._headers = headers + if (proxy && url.protocol === 'https:') { + options.method = 'CONNECT' + options.headers = Object.keys(headers).reduce( + (result, key) => + Object.assign( + result, + ['host', 'user-agent'].includes(key) && { [key]: headers[key] } + ), + {} + ) + } else { + options.method = method + options.headers = headers + } - if (proxy) { - options.hostname = translate(proxy.hostname) - options.port = proxy.port || ((proxy.protocol === 'https:') ? 443 : 80) - options.path = (url.protocol === 'https:') ? (translate(url.hostname) + ':' + (url.port || 443)) : ('http://' + translate(url.hostname) + url.path) - } - else { - options.hostname = translate(url.hostname) - options.port = url.port || ((url.protocol === 'https:') ? 443 : 80) - options.path = url.path - } - return options + if (proxy) { + options.hostname = translate(proxy.hostname) + options.port = proxy.port || (proxy.protocol === 'https:' ? 443 : 80) + options.path = + url.protocol === 'https:' + ? translate(url.hostname) + ':' + (url.port || 443) + : 'http://' + translate(url.hostname) + url.path + } else { + options.hostname = translate(url.hostname) + options.port = url.port || (url.protocol === 'https:' ? 443 : 80) + options.path = url.path + } + return options } const request = (method, url, headers, body, proxy) => { - url = parse(url) - headers = headers || {} - const options = configure(method, url, Object.assign({ - 'host': url.hostname, - 'accept': 'application/json, text/plain, */*', - 'accept-encoding': 'gzip, deflate', - 'accept-language': 'zh-CN,zh;q=0.9', - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36' - }, headers), proxy) + url = parse(url) + headers = headers || {} + const options = configure( + method, + url, + Object.assign( + { + host: url.hostname, + accept: 'application/json, text/plain, */*', + 'accept-encoding': 'gzip, deflate', + 'accept-language': 'zh-CN,zh;q=0.9', + 'user-agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36' + }, + headers + ), + proxy + ) - return new Promise((resolve, reject) => { - create(url, proxy)(options) - .on('response', response => resolve(response)) - .on('connect', (_, socket) => - https.request({ - method: method, - path: url.path, - headers: options._headers, - socket: socket, - agent: false - }) - .on('response', response => resolve(response)) - .on('error', error => reject(error)) - .end(body) - ) - .on('error', error => reject(error)) - .end(options.method.toUpperCase() === 'CONNECT' ? undefined : body) - }) - .then(response => { - if (new Set([201, 301, 302, 303, 307, 308]).has(response.statusCode)) - return request(method, url.resolve(response.headers.location || url.href), (delete headers.host, headers), body, proxy) - else - return Object.assign(response, {url: url, body: raw => read(response, raw), json: () => json(response), jsonp: () => jsonp(response)}) - }) + return new Promise((resolve, reject) => { + create( + url, + proxy + )(options) + .on('response', response => resolve(response)) + .on('connect', (_, socket) => + https + .request({ + method: method, + path: url.path, + headers: options._headers, + socket: socket, + agent: false + }) + .on('response', response => resolve(response)) + .on('error', error => reject(error)) + .end(body) + ) + .on('error', error => reject(error)) + .end(options.method.toUpperCase() === 'CONNECT' ? undefined : body) + }).then(response => { + if (new Set([201, 301, 302, 303, 307, 308]).has(response.statusCode)) + return request( + method, + url.resolve(response.headers.location || url.href), + (delete headers.host, headers), + body, + proxy + ) + else + return Object.assign(response, { + url: url, + body: raw => read(response, raw), + json: () => json(response), + jsonp: () => jsonp(response) + }) + }) } const read = (connect, raw) => - new Promise((resolve, reject) => { - const chunks = [] - connect - .on('data', chunk => chunks.push(chunk)) - .on('end', () => resolve(Buffer.concat(chunks))) - .on('error', error => reject(error)) - }) - .then(buffer => { - buffer = (buffer.length && ['gzip', 'deflate'].includes(connect.headers['content-encoding'])) ? zlib.unzipSync(buffer) : buffer - return raw ? buffer : buffer.toString() - }) + new Promise((resolve, reject) => { + const chunks = [] + connect + .on('data', chunk => chunks.push(chunk)) + .on('end', () => resolve(Buffer.concat(chunks))) + .on('error', error => reject(error)) + }).then(buffer => { + buffer = + buffer.length && + ['gzip', 'deflate'].includes(connect.headers['content-encoding']) + ? zlib.unzipSync(buffer) + : buffer + return raw ? buffer : buffer.toString() + }) const json = connect => read(connect, false).then(body => JSON.parse(body)) -const jsonp = connect => read(connect, false).then(body => JSON.parse(body.slice(body.indexOf('(') + 1, -')'.length))) +const jsonp = connect => + read(connect, false).then(body => + JSON.parse(body.slice(body.indexOf('(') + 1, -')'.length)) + ) request.read = read request.create = create request.translate = translate request.configure = configure -module.exports = request \ No newline at end of file +module.exports = request diff --git a/packages/unblock/src/server.js b/packages/unblock/src/server.js index 44994505..5ee83de8 100644 --- a/packages/unblock/src/server.js +++ b/packages/unblock/src/server.js @@ -8,167 +8,200 @@ const hook = require('./hook') const request = require('./request') const proxy = { - core: { - mitm: (req, res) => { - if (req.url == '/proxy.pac') { - const url = parse('http://' + req.headers.host) - res.writeHead(200, {'Content-Type': 'application/x-ns-proxy-autoconfig'}) - res.end(` + core: { + mitm: (req, res) => { + if (req.url == '/proxy.pac') { + const url = parse('http://' + req.headers.host) + res.writeHead(200, { + 'Content-Type': 'application/x-ns-proxy-autoconfig' + }) + res.end(` function FindProxyForURL(url, host) { - if (${Array.from(hook.target.host).map(host => (`host == '${host}'`)).join(' || ')}) { + if (${Array.from(hook.target.host) + .map(host => `host == '${host}'`) + .join(' || ')}) { return 'PROXY ${url.hostname}:${url.port || 80}' } return 'DIRECT' } `) - } - else { - const ctx = {res, req} - Promise.resolve() - .then(() => proxy.protect(ctx)) - .then(() => proxy.authenticate(ctx)) - .then(() => hook.request.before(ctx)) - .then(() => proxy.filter(ctx)) - .then(() => proxy.log(ctx)) - .then(() => proxy.mitm.request(ctx)) - .then(() => hook.request.after(ctx)) - .then(() => proxy.mitm.response(ctx)) - .catch(() => proxy.mitm.close(ctx)) - } - }, - tunnel: (req, socket, head) => { - const ctx = {req, socket, head} - Promise.resolve() - .then(() => proxy.protect(ctx)) - .then(() => proxy.authenticate(ctx)) - .then(() => hook.connect.before(ctx)) - .then(() => proxy.filter(ctx)) - .then(() => proxy.log(ctx)) - .then(() => proxy.tunnel.connect(ctx)) - .then(() => proxy.tunnel.dock(ctx)) - .then(() => hook.negotiate.before(ctx)) - .then(() => proxy.tunnel.pipe(ctx)) - .catch(() => proxy.tunnel.close(ctx)) - } - }, - abort: (socket, from) => { - // console.log('call abort', from) - if (socket) socket.end() - if (socket && !socket.destroyed) socket.destroy() - }, - protect: ctx => { - const {req, res, socket} = ctx - if (req) req.on('error', () => proxy.abort(req.socket, 'req')) - if (res) res.on('error', () => proxy.abort(res.socket, 'res')) - if (socket) socket.on('error', () => proxy.abort(socket, 'socket')) - }, - log: ctx => { - const {req, socket, decision} = ctx - const mark = {close: '|', blank: '-', proxy: '>'}[decision] || '>' - if (socket) - console.log('TUNNEL', mark, req.url) - else - console.log('MITM', mark, parse(req.url).host, req.socket.encrypted ? '(ssl)' : '') - }, - authenticate: ctx => { - const {req, res, socket} = ctx - const credential = Buffer.from((req.headers['proxy-authorization'] || '').split(/\s+/).pop() || '', 'base64').toString() - if ('proxy-authorization' in req.headers) delete req.headers['proxy-authorization'] - if (server.authentication && credential != server.authentication && (socket || req.url.startsWith('http://'))) { - if (socket) - socket.write('HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="realm"\r\n\r\n') - else - res.writeHead(407, {'proxy-authenticate': 'Basic realm="realm"'}) - return Promise.reject(ctx.error = 'authenticate') - } - }, - filter: ctx => { - if (ctx.decision || ctx.req.local) return - const url = parse((ctx.socket ? 'https://' : '') + ctx.req.url) - const match = pattern => url.href.search(new RegExp(pattern, 'g')) != -1 - try { - const allow = server.whitelist.some(match) - const deny = server.blacklist.some(match) - // console.log('allow', allow, 'deny', deny) - if (!allow && deny) { - return Promise.reject(ctx.error = 'filter') - } - } - catch(error) { - ctx.error = error - } - }, - mitm: { - request: ctx => new Promise((resolve, reject) => { - if (ctx.decision === 'close') return reject(ctx.error = ctx.decision) - const {req} = ctx - const url = parse(req.url) - const options = request.configure(req.method, url, req.headers) - ctx.proxyReq = request.create(url)(options) - .on('response', proxyRes => resolve(ctx.proxyRes = proxyRes)) - .on('error', error => reject(ctx.error = error)) - req.readable ? req.pipe(ctx.proxyReq) : ctx.proxyReq.end(req.body) - }), - response: ctx => { - const {res, proxyRes} = ctx - proxyRes.on('error', () => proxy.abort(proxyRes.socket, 'proxyRes')) - res.writeHead(proxyRes.statusCode, proxyRes.headers) - proxyRes.readable ? proxyRes.pipe(res) : res.end(proxyRes.body) - }, - close: ctx => { - proxy.abort(ctx.res.socket, 'mitm') - } - }, - tunnel: { - connect: ctx => new Promise((resolve, reject) => { - if (ctx.decision === 'close') return reject(ctx.error = ctx.decision) - const {req} = ctx - const url = parse('https://' + req.url) - if (global.proxy && !req.local) { - const options = request.configure(req.method, url, req.headers) - request.create(proxy)(options) - .on('connect', (_, proxySocket) => resolve(ctx.proxySocket = proxySocket)) - .on('error', error => reject(ctx.error = error)) - .end() - } - else { - const proxySocket = net.connect(url.port || 443, request.translate(url.hostname)) - .on('connect', () => resolve(ctx.proxySocket = proxySocket)) - .on('error', error => reject(ctx.error = error)) - } - }), - dock: ctx => new Promise(resolve => { - const {req, head, socket} = ctx - socket - .once('data', data => resolve(ctx.head = Buffer.concat([head, data]))) - .write(`HTTP/${req.httpVersion} 200 Connection established\r\n\r\n`) - }).then(data => ctx.socket.sni = sni(data)).catch(() => {}), - pipe: ctx => { - if (ctx.decision === 'blank') return Promise.reject(ctx.error = ctx.decision) - const {head, socket, proxySocket} = ctx - proxySocket.on('error', () => proxy.abort(ctx.proxySocket, 'proxySocket')) - proxySocket.write(head) - socket.pipe(proxySocket) - proxySocket.pipe(socket) - }, - close: ctx => { - proxy.abort(ctx.socket, 'tunnel') - } - } + } else { + const ctx = { res, req } + Promise.resolve() + .then(() => proxy.protect(ctx)) + .then(() => proxy.authenticate(ctx)) + .then(() => hook.request.before(ctx)) + .then(() => proxy.filter(ctx)) + .then(() => proxy.log(ctx)) + .then(() => proxy.mitm.request(ctx)) + .then(() => hook.request.after(ctx)) + .then(() => proxy.mitm.response(ctx)) + .catch(() => proxy.mitm.close(ctx)) + } + }, + tunnel: (req, socket, head) => { + const ctx = { req, socket, head } + Promise.resolve() + .then(() => proxy.protect(ctx)) + .then(() => proxy.authenticate(ctx)) + .then(() => hook.connect.before(ctx)) + .then(() => proxy.filter(ctx)) + .then(() => proxy.log(ctx)) + .then(() => proxy.tunnel.connect(ctx)) + .then(() => proxy.tunnel.dock(ctx)) + .then(() => hook.negotiate.before(ctx)) + .then(() => proxy.tunnel.pipe(ctx)) + .catch(() => proxy.tunnel.close(ctx)) + } + }, + abort: (socket, from) => { + // console.log('call abort', from) + if (socket) socket.end() + if (socket && !socket.destroyed) socket.destroy() + }, + protect: ctx => { + const { req, res, socket } = ctx + if (req) req.on('error', () => proxy.abort(req.socket, 'req')) + if (res) res.on('error', () => proxy.abort(res.socket, 'res')) + if (socket) socket.on('error', () => proxy.abort(socket, 'socket')) + }, + log: ctx => { + const { req, socket, decision } = ctx + const mark = { close: '|', blank: '-', proxy: '>' }[decision] || '>' + if (socket) console.log('TUNNEL', mark, req.url) + else + console.log( + 'MITM', + mark, + parse(req.url).host, + req.socket.encrypted ? '(ssl)' : '' + ) + }, + authenticate: ctx => { + const { req, res, socket } = ctx + const credential = Buffer.from( + (req.headers['proxy-authorization'] || '').split(/\s+/).pop() || '', + 'base64' + ).toString() + if ('proxy-authorization' in req.headers) + delete req.headers['proxy-authorization'] + if ( + server.authentication && + credential != server.authentication && + (socket || req.url.startsWith('http://')) + ) { + if (socket) + socket.write( + 'HTTP/1.1 407 Proxy Auth Required\r\nProxy-Authenticate: Basic realm="realm"\r\n\r\n' + ) + else res.writeHead(407, { 'proxy-authenticate': 'Basic realm="realm"' }) + return Promise.reject((ctx.error = 'authenticate')) + } + }, + filter: ctx => { + if (ctx.decision || ctx.req.local) return + const url = parse((ctx.socket ? 'https://' : '') + ctx.req.url) + const match = pattern => url.href.search(new RegExp(pattern, 'g')) != -1 + try { + const allow = server.whitelist.some(match) + const deny = server.blacklist.some(match) + // console.log('allow', allow, 'deny', deny) + if (!allow && deny) { + return Promise.reject((ctx.error = 'filter')) + } + } catch (error) { + ctx.error = error + } + }, + mitm: { + request: ctx => + new Promise((resolve, reject) => { + if (ctx.decision === 'close') return reject((ctx.error = ctx.decision)) + const { req } = ctx + const url = parse(req.url) + const options = request.configure(req.method, url, req.headers) + ctx.proxyReq = request + .create(url)(options) + .on('response', proxyRes => resolve((ctx.proxyRes = proxyRes))) + .on('error', error => reject((ctx.error = error))) + req.readable ? req.pipe(ctx.proxyReq) : ctx.proxyReq.end(req.body) + }), + response: ctx => { + const { res, proxyRes } = ctx + proxyRes.on('error', () => proxy.abort(proxyRes.socket, 'proxyRes')) + res.writeHead(proxyRes.statusCode, proxyRes.headers) + proxyRes.readable ? proxyRes.pipe(res) : res.end(proxyRes.body) + }, + close: ctx => { + proxy.abort(ctx.res.socket, 'mitm') + } + }, + tunnel: { + connect: ctx => + new Promise((resolve, reject) => { + if (ctx.decision === 'close') return reject((ctx.error = ctx.decision)) + const { req } = ctx + const url = parse('https://' + req.url) + if (global.proxy && !req.local) { + const options = request.configure(req.method, url, req.headers) + request + .create(proxy)(options) + .on('connect', (_, proxySocket) => + resolve((ctx.proxySocket = proxySocket)) + ) + .on('error', error => reject((ctx.error = error))) + .end() + } else { + const proxySocket = net + .connect(url.port || 443, request.translate(url.hostname)) + .on('connect', () => resolve((ctx.proxySocket = proxySocket))) + .on('error', error => reject((ctx.error = error))) + } + }), + dock: ctx => + new Promise(resolve => { + const { req, head, socket } = ctx + socket + .once('data', data => + resolve((ctx.head = Buffer.concat([head, data]))) + ) + .write(`HTTP/${req.httpVersion} 200 Connection established\r\n\r\n`) + }) + .then(data => (ctx.socket.sni = sni(data))) + .catch(() => {}), + pipe: ctx => { + if (ctx.decision === 'blank') + return Promise.reject((ctx.error = ctx.decision)) + const { head, socket, proxySocket } = ctx + proxySocket.on('error', () => proxy.abort(ctx.proxySocket, 'proxySocket')) + proxySocket.write(head) + socket.pipe(proxySocket) + proxySocket.pipe(socket) + }, + close: ctx => { + proxy.abort(ctx.socket, 'tunnel') + } + } } const options = { - key: fs.readFileSync(path.join(__dirname, '..', 'server.key')), - cert: fs.readFileSync(path.join(__dirname, '..', 'server.crt')) + key: fs.readFileSync(path.join(__dirname, '..', 'server.key')), + cert: fs.readFileSync(path.join(__dirname, '..', 'server.crt')) } const server = { - http: require('http').createServer().on('request', proxy.core.mitm).on('connect', proxy.core.tunnel), - https: require('https').createServer(options).on('request', proxy.core.mitm).on('connect', proxy.core.tunnel) + http: require('http') + .createServer() + .on('request', proxy.core.mitm) + .on('connect', proxy.core.tunnel), + https: require('https') + .createServer(options) + .on('request', proxy.core.mitm) + .on('connect', proxy.core.tunnel) } server.whitelist = [] server.blacklist = ['://127\\.\\d+\\.\\d+\\.\\d+', '://localhost'] server.authentication = null -module.exports = server \ No newline at end of file +module.exports = server diff --git a/packages/unblock/src/sni.js b/packages/unblock/src/sni.js index b7040a99..89d1a8d9 100644 --- a/packages/unblock/src/sni.js +++ b/packages/unblock/src/sni.js @@ -1,52 +1,52 @@ // Thanks to https://github.com/buschtoens/sni module.exports = data => { - let end = data.length - let pointer = 5 + 1 + 3 + 2 + 32 - const nan = (number = pointer) => isNaN(number) - - if (pointer + 1 > end || nan()) return null - pointer += 1 + data[pointer] - - if (pointer + 2 > end || nan()) return null - pointer += 2 + data.readInt16BE(pointer) - - if (pointer + 1 > end || nan()) return null - pointer += 1 + data[pointer] - - if (pointer + 2 > end || nan()) return null - const extensionsLength = data.readInt16BE(pointer) - pointer += 2 - const extensionsEnd = pointer + extensionsLength - - if (extensionsEnd > end || nan(extensionsEnd)) return null - end = extensionsEnd - - while (pointer + 4 <= end || nan()) { - const extensionType = data.readInt16BE(pointer) - const extensionSize = data.readInt16BE(pointer + 2) - pointer += 4 - if (extensionType !== 0) { - pointer += extensionSize - continue - } - if (pointer + 2 > end || nan()) return null - const nameListLength = data.readInt16BE(pointer) - pointer += 2 - if (pointer + nameListLength > end) return null - - while (pointer + 3 <= end || nan()) { - const nameType = data[pointer] - const nameLength = data.readInt16BE(pointer + 1) - pointer += 3 - if (nameType !== 0) { - pointer += nameLength - continue - } - if (pointer + nameLength > end || nan()) return null - return data.toString('ascii', pointer, pointer + nameLength) - } - } - - return null -} \ No newline at end of file + let end = data.length + let pointer = 5 + 1 + 3 + 2 + 32 + const nan = (number = pointer) => isNaN(number) + + if (pointer + 1 > end || nan()) return null + pointer += 1 + data[pointer] + + if (pointer + 2 > end || nan()) return null + pointer += 2 + data.readInt16BE(pointer) + + if (pointer + 1 > end || nan()) return null + pointer += 1 + data[pointer] + + if (pointer + 2 > end || nan()) return null + const extensionsLength = data.readInt16BE(pointer) + pointer += 2 + const extensionsEnd = pointer + extensionsLength + + if (extensionsEnd > end || nan(extensionsEnd)) return null + end = extensionsEnd + + while (pointer + 4 <= end || nan()) { + const extensionType = data.readInt16BE(pointer) + const extensionSize = data.readInt16BE(pointer + 2) + pointer += 4 + if (extensionType !== 0) { + pointer += extensionSize + continue + } + if (pointer + 2 > end || nan()) return null + const nameListLength = data.readInt16BE(pointer) + pointer += 2 + if (pointer + nameListLength > end) return null + + while (pointer + 3 <= end || nan()) { + const nameType = data[pointer] + const nameLength = data.readInt16BE(pointer + 1) + pointer += 3 + if (nameType !== 0) { + pointer += nameLength + continue + } + if (pointer + nameLength > end || nan()) return null + return data.toString('ascii', pointer, pointer + nameLength) + } + } + + return null +}