feat: integrate api in electron app

This commit is contained in:
kunkka 2020-10-27 23:08:38 +08:00
parent bd29570e31
commit 74bf714c28
240 changed files with 21003 additions and 112 deletions

830
napi/util/apicache.js Normal file
View file

@ -0,0 +1,830 @@
var url = require('url');
var MemoryCache = require('./memory-cache');
var t = {
ms: 1,
second: 1000,
minute: 60000,
hour: 3600000,
day: 3600000 * 24,
week: 3600000 * 24 * 7,
month: 3600000 * 24 * 30,
};
var instances = [];
var matches = function (a) {
return function (b) {
return a === b;
};
};
var doesntMatch = function (a) {
return function (b) {
return !matches(a)(b);
};
};
var logDuration = function (d, prefix) {
var str = d > 1000 ? (d / 1000).toFixed(2) + 'sec' : d + 'ms';
return '\x1b[33m- ' + (prefix ? prefix + ' ' : '') + str + '\x1b[0m';
};
function getSafeHeaders(res) {
return res.getHeaders ? res.getHeaders() : res._headers;
}
function ApiCache() {
var memCache = new MemoryCache();
var globalOptions = {
debug: false,
defaultDuration: 3600000,
enabled: true,
appendKey: [],
jsonp: false,
redisClient: false,
headerBlacklist: [],
statusCodes: {
include: [],
exclude: [],
},
events: {
expire: undefined,
},
headers: {
// 'cache-control': 'no-cache' // example of header overwrite
},
trackPerformance: false,
};
var middlewareOptions = [];
var instance = this;
var index = null;
var timers = {};
var performanceArray = []; // for tracking cache hit rate
instances.push(this);
this.id = instances.length;
function debug(a, b, c, d) {
var arr = ['\x1b[36m[apicache]\x1b[0m', a, b, c, d].filter(function (arg) {
return arg !== undefined;
});
var debugEnv =
process.env.DEBUG &&
process.env.DEBUG.split(',').indexOf('apicache') !== -1;
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr);
}
function shouldCacheResponse(request, response, toggle) {
var opt = globalOptions;
var codes = opt.statusCodes;
if (!response) return false;
if (toggle && !toggle(request, response)) {
return false;
}
if (
codes.exclude &&
codes.exclude.length &&
codes.exclude.indexOf(response.statusCode) !== -1
)
return false;
if (
codes.include &&
codes.include.length &&
codes.include.indexOf(response.statusCode) === -1
)
return false;
return true;
}
function addIndexEntries(key, req) {
var groupName = req.apicacheGroup;
if (groupName) {
debug('group detected "' + groupName + '"');
var group = (index.groups[groupName] = index.groups[groupName] || []);
group.unshift(key);
}
index.all.unshift(key);
}
function filterBlacklistedHeaders(headers) {
return Object.keys(headers)
.filter(function (key) {
return globalOptions.headerBlacklist.indexOf(key) === -1;
})
.reduce(function (acc, header) {
acc[header] = headers[header];
return acc;
}, {});
}
function createCacheObject(status, headers, data, encoding) {
return {
status: status,
headers: filterBlacklistedHeaders(headers),
data: data,
encoding: encoding,
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
};
}
function cacheResponse(key, value, duration) {
var redis = globalOptions.redisClient;
var expireCallback = globalOptions.events.expire;
if (redis && redis.connected) {
try {
redis.hset(key, 'response', JSON.stringify(value));
redis.hset(key, 'duration', duration);
redis.expire(key, duration / 1000, expireCallback || function () {});
} catch (err) {
debug('[apicache] error in redis.hset()');
}
} else {
memCache.add(key, value, duration, expireCallback);
}
// add automatic cache clearing from duration, includes max limit on setTimeout
timers[key] = setTimeout(function () {
instance.clear(key, true);
}, Math.min(duration, 2147483647));
}
function accumulateContent(res, content) {
if (content) {
if (typeof content == 'string') {
res._apicache.content = (res._apicache.content || '') + content;
} else if (Buffer.isBuffer(content)) {
var oldContent = res._apicache.content;
if (typeof oldContent === 'string') {
oldContent = !Buffer.from
? new Buffer(oldContent)
: Buffer.from(oldContent);
}
if (!oldContent) {
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0);
}
res._apicache.content = Buffer.concat(
[oldContent, content],
oldContent.length + content.length,
);
} else {
res._apicache.content = content;
}
}
}
function makeResponseCacheable(
req,
res,
next,
key,
duration,
strDuration,
toggle,
) {
// monkeypatch res.end to create cache object
res._apicache = {
write: res.write,
writeHead: res.writeHead,
end: res.end,
cacheable: true,
content: undefined,
};
// append header overwrites if applicable
Object.keys(globalOptions.headers).forEach(function (name) {
res.setHeader(name, globalOptions.headers[name]);
});
res.writeHead = function () {
// add cache control headers
if (!globalOptions.headers['cache-control']) {
if (shouldCacheResponse(req, res, toggle)) {
res.setHeader(
'cache-control',
'max-age=' + (duration / 1000).toFixed(0),
);
} else {
res.setHeader('cache-control', 'no-cache, no-store, must-revalidate');
}
}
res._apicache.headers = Object.assign({}, getSafeHeaders(res));
return res._apicache.writeHead.apply(this, arguments);
};
// patch res.write
res.write = function (content) {
accumulateContent(res, content);
return res._apicache.write.apply(this, arguments);
};
// patch res.end
res.end = function (content, encoding) {
if (shouldCacheResponse(req, res, toggle)) {
accumulateContent(res, content);
if (res._apicache.cacheable && res._apicache.content) {
addIndexEntries(key, req);
var headers = res._apicache.headers || getSafeHeaders(res);
var cacheObject = createCacheObject(
res.statusCode,
headers,
res._apicache.content,
encoding,
);
cacheResponse(key, cacheObject, duration);
// display log entry
var elapsed = new Date() - req.apicacheTimer;
debug(
'adding cache entry for "' + key + '" @ ' + strDuration,
logDuration(elapsed),
);
debug('_apicache.headers: ', res._apicache.headers);
debug('res.getHeaders(): ', getSafeHeaders(res));
debug('cacheObject: ', cacheObject);
}
}
return res._apicache.end.apply(this, arguments);
};
next();
}
function sendCachedResponse(
request,
response,
cacheObject,
toggle,
next,
duration,
) {
if (toggle && !toggle(request, response)) {
return next();
}
var headers = getSafeHeaders(response);
Object.assign(
headers,
filterBlacklistedHeaders(cacheObject.headers || {}),
{
// set properly-decremented max-age header. This ensures that max-age is in sync with the cache expiration.
'cache-control':
'max-age=' +
Math.max(
0,
(
duration / 1000 -
(new Date().getTime() / 1000 - cacheObject.timestamp)
).toFixed(0),
),
},
);
// only embed apicache headers when not in production environment
// unstringify buffers
var data = cacheObject.data;
if (data && data.type === 'Buffer') {
data =
typeof data.data === 'number'
? new Buffer.alloc(data.data)
: new Buffer.from(data.data);
}
// test Etag against If-None-Match for 304
var cachedEtag = cacheObject.headers.etag;
var requestEtag = request.headers['if-none-match'];
if (requestEtag && cachedEtag === requestEtag) {
response.writeHead(304, headers);
return response.end();
}
response.writeHead(cacheObject.status || 200, headers);
return response.end(data, cacheObject.encoding);
}
function syncOptions() {
for (var i in middlewareOptions) {
Object.assign(
middlewareOptions[i].options,
globalOptions,
middlewareOptions[i].localOptions,
);
}
}
this.clear = function (target, isAutomatic) {
var group = index.groups[target];
var redis = globalOptions.redisClient;
if (group) {
debug('clearing group "' + target + '"');
group.forEach(function (key) {
debug('clearing cached entry for "' + key + '"');
clearTimeout(timers[key]);
delete timers[key];
if (!globalOptions.redisClient) {
memCache.delete(key);
} else {
try {
redis.del(key);
} catch (err) {
console.log('[apicache] error in redis.del("' + key + '")');
}
}
index.all = index.all.filter(doesntMatch(key));
});
delete index.groups[target];
} else if (target) {
debug(
'clearing ' +
(isAutomatic ? 'expired' : 'cached') +
' entry for "' +
target +
'"',
);
clearTimeout(timers[target]);
delete timers[target];
// clear actual cached entry
if (!redis) {
memCache.delete(target);
} else {
try {
redis.del(target);
} catch (err) {
console.log('[apicache] error in redis.del("' + target + '")');
}
}
// remove from global index
index.all = index.all.filter(doesntMatch(target));
// remove target from each group that it may exist in
Object.keys(index.groups).forEach(function (groupName) {
index.groups[groupName] = index.groups[groupName].filter(
doesntMatch(target),
);
// delete group if now empty
if (!index.groups[groupName].length) {
delete index.groups[groupName];
}
});
} else {
debug('clearing entire index');
if (!redis) {
memCache.clear();
} else {
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
index.all.forEach(function (key) {
clearTimeout(timers[key]);
delete timers[key];
try {
redis.del(key);
} catch (err) {
console.log('[apicache] error in redis.del("' + key + '")');
}
});
}
this.resetIndex();
}
return this.getIndex();
};
function parseDuration(duration, defaultDuration) {
if (typeof duration === 'number') return duration;
if (typeof duration === 'string') {
var split = duration.match(/^([\d\.,]+)\s?(\w+)$/);
if (split.length === 3) {
var len = parseFloat(split[1]);
var unit = split[2].replace(/s$/i, '').toLowerCase();
if (unit === 'm') {
unit = 'ms';
}
return (len || 1) * (t[unit] || 0);
}
}
return defaultDuration;
}
this.getDuration = function (duration) {
return parseDuration(duration, globalOptions.defaultDuration);
};
/**
* Return cache performance statistics (hit rate). Suitable for putting into a route:
* <code>
* app.get('/api/cache/performance', (req, res) => {
* res.json(apicache.getPerformance())
* })
* </code>
*/
this.getPerformance = function () {
return performanceArray.map(function (p) {
return p.report();
});
};
this.getIndex = function (group) {
if (group) {
return index.groups[group];
} else {
return index;
}
};
this.middleware = function cache(
strDuration,
middlewareToggle,
localOptions,
) {
var duration = instance.getDuration(strDuration);
var opt = {};
middlewareOptions.push({
options: opt,
});
var options = function (localOptions) {
if (localOptions) {
middlewareOptions.find(function (middleware) {
return middleware.options === opt;
}).localOptions = localOptions;
}
syncOptions();
return opt;
};
options(localOptions);
/**
* A Function for non tracking performance
*/
function NOOPCachePerformance() {
this.report = this.hit = this.miss = function () {}; // noop;
}
/**
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above.
*/
function CachePerformance() {
/**
* Tracks the hit rate for the last 100 requests.
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
/**
* Tracks the hit rate for the last 1000 requests.
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
/**
* Tracks the hit rate for the last 10000 requests.
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
/**
* Tracks the hit rate for the last 100000 requests.
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened.
*/
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
/**
* The number of calls that have passed through the middleware since the server started.
*/
this.callCount = 0;
/**
* The total number of hits since the server started
*/
this.hitCount = 0;
/**
* The key from the last cache hit. This is useful in identifying which route these statistics apply to.
*/
this.lastCacheHit = null;
/**
* The key from the last cache miss. This is useful in identifying which route these statistics apply to.
*/
this.lastCacheMiss = null;
/**
* Return performance statistics
*/
this.report = function () {
return {
lastCacheHit: this.lastCacheHit,
lastCacheMiss: this.lastCacheMiss,
callCount: this.callCount,
hitCount: this.hitCount,
missCount: this.callCount - this.hitCount,
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount,
hitRateLast100: this.hitRate(this.hitsLast100),
hitRateLast1000: this.hitRate(this.hitsLast1000),
hitRateLast10000: this.hitRate(this.hitsLast10000),
hitRateLast100000: this.hitRate(this.hitsLast100000),
};
};
/**
* Computes a cache hit rate from an array of hits and misses.
* @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) {
var hits = 0;
var misses = 0;
for (var i = 0; i < array.length; i++) {
var n8 = array[i];
for (j = 0; j < 4; j++) {
switch (n8 & 3) {
case 1:
hits++;
break;
case 2:
misses++;
break;
}
n8 >>= 2;
}
}
var total = hits + misses;
if (total == 0) return null;
return hits / total;
};
/**
* Record a hit or miss in the given array. It will be recorded at a position determined
* by the current value of the callCount variable.
* @param {Uint8Array} array An array representing hits and misses.
* @param {boolean} hit true for a hit, false for a miss
* Each element in the array is 8 bits, and encodes 4 hit/miss records.
* Each hit or miss is encoded as to bits as follows:
* 00 means no hit or miss has been recorded in these bits
* 01 encodes a hit
* 10 encodes a miss
*/
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);
var record = (hit ? 1 : 2) << bitOffset;
array[arrayIndex] = (array[arrayIndex] & clearMask) | record;
};
/**
* 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.recordHitInArray(this.hitsLast100, hit);
this.recordHitInArray(this.hitsLast1000, hit);
this.recordHitInArray(this.hitsLast10000, hit);
this.recordHitInArray(this.hitsLast100000, hit);
if (hit) this.hitCount++;
this.callCount++;
};
/**
* 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.recordHit(true);
this.lastCacheHit = key;
};
/**
* 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.recordHit(false);
this.lastCacheMiss = key;
};
}
var perf = globalOptions.trackPerformance
? new CachePerformance()
: new NOOPCachePerformance();
performanceArray.push(perf);
var cache = function (req, res, next) {
function bypass() {
debug('bypass detected, skipping cache.');
return next();
}
// initial bypass chances
if (!opt.enabled) return bypass();
if (
req.headers['x-apicache-bypass'] ||
req.headers['x-apicache-force-fetch']
)
return bypass();
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
// if (typeof middlewareToggle === 'function') {
// if (!middlewareToggle(req, res)) return bypass()
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
// return bypass()
// }
// embed timer
req.apicacheTimer = new Date();
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
var key = req.originalUrl || req.url;
// Remove querystring from key if jsonp option is enabled
if (opt.jsonp) {
key = url.parse(key).pathname;
}
// add appendKey (either custom function or response path)
if (typeof opt.appendKey === 'function') {
key += '$$appendKey=' + opt.appendKey(req, res);
} else if (opt.appendKey.length > 0) {
var appendKey = req;
for (var i = 0; i < opt.appendKey.length; i++) {
appendKey = appendKey[opt.appendKey[i]];
}
key += '$$appendKey=' + appendKey;
}
// attempt cache hit
var redis = opt.redisClient;
var cached = !redis ? memCache.getValue(key) : null;
// send if cache hit from memory-cache
if (cached) {
var elapsed = new Date() - req.apicacheTimer;
debug(
'sending cached (memory-cache) version of',
key,
logDuration(elapsed),
);
perf.hit(key);
return sendCachedResponse(
req,
res,
cached,
middlewareToggle,
next,
duration,
);
}
// send if cache hit from redis
if (redis && redis.connected) {
try {
redis.hgetall(key, function (err, obj) {
if (!err && obj && obj.response) {
var elapsed = new Date() - req.apicacheTimer;
debug(
'sending cached (redis) version of',
key,
logDuration(elapsed),
);
perf.hit(key);
return sendCachedResponse(
req,
res,
JSON.parse(obj.response),
middlewareToggle,
next,
duration,
);
} else {
perf.miss(key);
return makeResponseCacheable(
req,
res,
next,
key,
duration,
strDuration,
middlewareToggle,
);
}
});
} catch (err) {
// bypass redis on error
perf.miss(key);
return makeResponseCacheable(
req,
res,
next,
key,
duration,
strDuration,
middlewareToggle,
);
}
} else {
perf.miss(key);
return makeResponseCacheable(
req,
res,
next,
key,
duration,
strDuration,
middlewareToggle,
);
}
};
cache.options = options;
return cache;
};
this.options = function (options) {
if (options) {
Object.assign(globalOptions, options);
syncOptions();
if ('defaultDuration' in options) {
// Convert the default duration to a number in milliseconds (if needed)
globalOptions.defaultDuration = parseDuration(
globalOptions.defaultDuration,
3600000,
);
}
if (globalOptions.trackPerformance) {
debug(
'WARNING: using trackPerformance flag can cause high memory usage!',
);
}
return this;
} else {
return globalOptions;
}
};
this.resetIndex = function () {
index = {
all: [],
groups: {},
};
};
this.newInstance = function (config) {
var instance = new ApiCache();
if (config) {
instance.options(config);
}
return instance;
};
this.clone = function () {
return this.newInstance(this.options());
};
// initialize index
this.resetIndex();
}
module.exports = new ApiCache();

67
napi/util/crypto.js Normal file
View file

@ -0,0 +1,67 @@
const crypto = require('crypto');
const iv = Buffer.from('0102030405060708');
const presetKey = Buffer.from('0CoJUm6Qyw8W8jud');
const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q');
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const publicKey =
'-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----';
const eapiKey = 'e82ckenh8dichen8';
const aesEncrypt = (buffer, mode, key, iv) => {
const cipher = crypto.createCipheriv('aes-128-' + mode, key, iv);
return Buffer.concat([cipher.update(buffer), cipher.final()]);
};
const rsaEncrypt = (buffer, key) => {
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]);
return crypto.publicEncrypt(
{ key: key, padding: crypto.constants.RSA_NO_PADDING },
buffer,
);
};
const weapi = (object) => {
const text = JSON.stringify(object);
const secretKey = crypto
.randomBytes(16)
.map((n) => base62.charAt(n % 62).charCodeAt());
return {
params: aesEncrypt(
Buffer.from(
aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64'),
),
'cbc',
secretKey,
iv,
).toString('base64'),
encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),
};
};
const linuxapi = (object) => {
const text = JSON.stringify(object);
return {
eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '')
.toString('hex')
.toUpperCase(),
};
};
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 data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`;
return {
params: aesEncrypt(Buffer.from(data), 'ecb', eapiKey, '')
.toString('hex')
.toUpperCase(),
};
};
const decrypt = (cipherBuffer) => {
const decipher = crypto.createDecipheriv('aes-128-ecb', eapiKey, '');
return Buffer.concat([decipher.update(cipherBuffer), decipher.final()]);
};
module.exports = { weapi, linuxapi, eapi, decrypt };

17
napi/util/index.js Normal file
View file

@ -0,0 +1,17 @@
module.exports = {
toBoolean(val) {
if (typeof val === 'boolean') return val;
if (val === '') return val;
return val === 'true' || val == '1';
},
cookieToJson(cookie) {
if (!cookie) return {};
let cookieArr = cookie.split(';');
let obj = {};
cookieArr.forEach((i) => {
let arr = i.split('=');
obj[arr[0]] = arr[1];
});
return obj;
},
};

63
napi/util/memory-cache.js Normal file
View file

@ -0,0 +1,63 @@
function MemoryCache() {
this.cache = {};
this.size = 0;
}
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) {
var old = this.cache[key];
var instance = this;
var entry = {
value: value,
expire: time + Date.now(),
timeout: setTimeout(function () {
instance.delete(key);
return (
timeoutCallback &&
typeof timeoutCallback === 'function' &&
timeoutCallback(value, key)
);
}, time),
};
this.cache[key] = entry;
this.size = Object.keys(this.cache).length;
return entry;
};
MemoryCache.prototype.delete = function (key) {
var entry = this.cache[key];
if (entry) {
clearTimeout(entry.timeout);
}
delete this.cache[key];
this.size = Object.keys(this.cache).length;
return null;
};
MemoryCache.prototype.get = function (key) {
var entry = this.cache[key];
return entry;
};
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) {
this.delete(key);
}, this);
return true;
};
module.exports = MemoryCache;

177
napi/util/request.js Normal file
View file

@ -0,0 +1,177 @@
const encrypt = require('./crypto');
const axios = require('axios');
const queryString = require('querystring');
const PacProxyAgent = require('pac-proxy-agent');
const http = require('http');
const https = require('https');
const tunnel = require('tunnel');
const qs = require('url');
// request.debug = true // 开启可看到更详细信息
const chooseUserAgent = (ua = false) => {
const userAgentList = {
mobile: [
// iOS 13.5.1 14.0 beta with safari
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1',
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.',
// iOS with qq micromsg
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML like Gecko) Mobile/14A456 QQ/6.5.7.408 V1_IPH_SQ_6.5.7_1_APP_A Pixel/750 Core/UIWebView NetType/4G Mem/103',
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/7.0.15(0x17000f27) NetType/WIFI Language/zh',
// Android -> Huawei Xiaomi
'Mozilla/5.0 (Linux; Android 9; PCT-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.64 HuaweiBrowser/10.0.3.311 Mobile Safari/537.36',
'Mozilla/5.0 (Linux; U; Android 9; zh-cn; Redmi Note 8 Build/PKQ1.190616.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.141 Mobile Safari/537.36 XiaoMi/MiuiBrowser/12.5.22',
// Android + qq micromsg
'Mozilla/5.0 (Linux; Android 10; YAL-AL00 Build/HUAWEIYAL-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/78.0.3904.62 XWEB/2581 MMWEBSDK/200801 Mobile Safari/537.36 MMWEBID/3027 MicroMessenger/7.0.18.1740(0x27001235) Process/toolsmp WeChat/arm64 NetType/WIFI Language/zh_CN ABI/arm64',
'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; BKK-AL10 Build/HONORBKK-AL10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/10.6 Mobile Safari/537.36',
],
pc: [
// macOS 10.15.6 Firefox / Chrome / Safari
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.30 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15',
// Windows 10 Firefox / Chrome / Edge
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.30 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586',
// Linux 就算了
],
};
let realUserAgentList =
userAgentList[ua] || userAgentList.mobile.concat(userAgentList.pc);
return ['mobile', 'pc', false].indexOf(ua) > -1
? realUserAgentList[Math.floor(Math.random() * realUserAgentList.length)]
: ua;
};
const createRequest = (method, url, data, options) => {
return new Promise((resolve, reject) => {
let headers = { 'User-Agent': chooseUserAgent(options.ua) };
if (method.toUpperCase() === 'POST')
headers['Content-Type'] = 'application/x-www-form-urlencoded';
if (url.includes('music.163.com'))
headers['Referer'] = 'https://music.163.com';
if (options.realIP) headers['X-Real-IP'] = options.realIP;
// headers['X-Real-IP'] = '118.88.88.88'
if (typeof options.cookie === 'object')
headers['Cookie'] = Object.keys(options.cookie)
.map(
(key) =>
encodeURIComponent(key) +
'=' +
encodeURIComponent(options.cookie[key]),
)
.join('; ');
else if (options.cookie) headers['Cookie'] = options.cookie;
if (!headers['Cookie']) {
headers['Cookie'] = options.token || '';
}
if (options.crypto === 'weapi') {
let csrfToken = (headers['Cookie'] || '').match(/_csrf=([^(;|$)]+)/);
data.csrf_token = csrfToken ? csrfToken[1] : '';
data = encrypt.weapi(data);
url = url.replace(/\w*api/, 'weapi');
} else if (options.crypto === 'linuxapi') {
data = encrypt.linuxapi({
method: method,
url: url.replace(/\w*api/, 'api'),
params: data,
});
headers['User-Agent'] =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36';
url = 'https://music.163.com/api/linux/forward';
} else if (options.crypto === 'eapi') {
const cookie = options.cookie || {};
const csrfToken = cookie['__csrf'] || '';
const header = {
osver: cookie.osver, //系统版本
deviceId: cookie.deviceId, //encrypt.base64.encode(imei + '\t02:00:00:00:00:00\t5106025eb79a5247\t70ffbaac7')
appver: cookie.appver || '6.1.1', // app版本
versioncode: cookie.versioncode || '140', //版本号
mobilename: cookie.mobilename, //设备model
buildver: cookie.buildver || Date.now().toString().substr(0, 10),
resolution: cookie.resolution || '1920x1080', //设备分辨率
__csrf: csrfToken,
os: cookie.os || 'android',
channel: cookie.channel,
requestId: `${Date.now()}_${Math.floor(Math.random() * 1000)
.toString()
.padStart(4, '0')}`,
};
if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U;
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A;
headers['Cookie'] = Object.keys(header)
.map(
(key) =>
encodeURIComponent(key) + '=' + encodeURIComponent(header[key]),
)
.join('; ');
data.header = header;
data = encrypt.eapi(options.url, data);
url = url.replace(/\w*api/, 'eapi');
}
const answer = { status: 500, body: {}, cookie: [] };
const settings = {
method: method,
url: url,
headers: headers,
data: queryString.stringify(data),
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
};
if (options.crypto === 'eapi') settings.encoding = null;
if (options.proxy) {
if (options.proxy.indexOf('pac') > -1) {
settings.httpAgent = new PacProxyAgent(options.proxy);
settings.httpsAgent = new PacProxyAgent(options.proxy);
} else {
var purl = qs.parse(options.proxy);
if (purl.hostname) {
const agent = tunnel.httpsOverHttp({
proxy: {
host: purl.hostname,
port: purl.port || 80,
},
});
settings.httpsAgent = agent;
settings.httpAgent = agent;
settings.proxy = false;
} else {
console.error('代理配置无效,不使用代理');
}
}
}
axios(settings)
.then((res) => {
const body = res.data;
answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
);
try {
answer.body = body;
answer.status = answer.body.code || res.status;
if (answer.body.code === 502) {
answer.status = 200;
}
} catch (e) {
answer.body = body;
answer.status = res.status;
}
answer.status =
100 < answer.status && answer.status < 600 ? answer.status : 400;
if (answer.status == 200) resolve(answer);
else reject(answer);
})
.catch((err) => {
answer.status = 502;
answer.body = { code: 502, msg: err };
reject(answer);
});
});
};
module.exports = createRequest;