mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 14:41:23 +08:00
@@ -0,0 +1,832 @@
|
||||
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.hostname +
|
||||
(req.originalUrl || req.url) +
|
||||
JSON.stringify(req.cookies)
|
||||
// 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()
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"resourceTypeMap": {
|
||||
"0": "R_SO_4_",
|
||||
"1": "R_MV_5_",
|
||||
"2": "A_PL_0_",
|
||||
"3": "R_AL_3_",
|
||||
"4": "A_DJ_1_",
|
||||
"5": "R_VI_62_",
|
||||
"6": "A_EV_2_",
|
||||
"7": "A_DR_14_"
|
||||
},
|
||||
"APP_CONF": {
|
||||
"apiDomain": "https://interface.music.163.com",
|
||||
"domain": "https://music.163.com",
|
||||
"encrypt": true,
|
||||
"encryptResponse": false
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
const CryptoJS = require('crypto-js')
|
||||
const forge = require('node-forge')
|
||||
const iv = '0102030405060708'
|
||||
const presetKey = '0CoJUm6Qyw8W8jud'
|
||||
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
||||
const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
const eapiKey = 'e82ckenh8dichen8'
|
||||
|
||||
const aesEncrypt = (text, mode, key, iv, format = 'base64') => {
|
||||
let encrypted = CryptoJS.AES.encrypt(
|
||||
CryptoJS.enc.Utf8.parse(text),
|
||||
CryptoJS.enc.Utf8.parse(key),
|
||||
{
|
||||
iv: CryptoJS.enc.Utf8.parse(iv),
|
||||
mode: CryptoJS.mode[mode.toUpperCase()],
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
},
|
||||
)
|
||||
if (format === 'base64') {
|
||||
return encrypted.toString()
|
||||
}
|
||||
|
||||
return encrypted.ciphertext.toString().toUpperCase()
|
||||
}
|
||||
const aesDecrypt = (ciphertext, key, iv, format = 'base64') => {
|
||||
let bytes
|
||||
if (format === 'base64') {
|
||||
bytes = CryptoJS.AES.decrypt(ciphertext, CryptoJS.enc.Utf8.parse(key), {
|
||||
iv: CryptoJS.enc.Utf8.parse(iv),
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
})
|
||||
} else {
|
||||
bytes = CryptoJS.AES.decrypt(
|
||||
{ ciphertext: CryptoJS.enc.Hex.parse(ciphertext) },
|
||||
CryptoJS.enc.Utf8.parse(key),
|
||||
{
|
||||
iv: CryptoJS.enc.Utf8.parse(iv),
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
},
|
||||
)
|
||||
}
|
||||
return bytes.toString(CryptoJS.enc.Utf8)
|
||||
}
|
||||
const rsaEncrypt = (str, key) => {
|
||||
const forgePublicKey = forge.pki.publicKeyFromPem(key)
|
||||
const encrypted = forgePublicKey.encrypt(str, 'NONE')
|
||||
return forge.util.bytesToHex(encrypted)
|
||||
}
|
||||
|
||||
const weapi = (object) => {
|
||||
const text = JSON.stringify(object)
|
||||
let secretKey = ''
|
||||
for (let i = 0; i < 16; i++) {
|
||||
secretKey += base62.charAt(Math.round(Math.random() * 61))
|
||||
}
|
||||
return {
|
||||
params: aesEncrypt(
|
||||
aesEncrypt(text, 'cbc', presetKey, iv),
|
||||
'cbc',
|
||||
secretKey,
|
||||
iv,
|
||||
),
|
||||
encSecKey: rsaEncrypt(secretKey.split('').reverse().join(''), publicKey),
|
||||
}
|
||||
}
|
||||
|
||||
const linuxapi = (object) => {
|
||||
const text = JSON.stringify(object)
|
||||
return {
|
||||
eparams: aesEncrypt(text, 'ecb', linuxapiKey, '', 'hex'),
|
||||
}
|
||||
}
|
||||
|
||||
const eapi = (url, object) => {
|
||||
const text = typeof object === 'object' ? JSON.stringify(object) : object
|
||||
const message = `nobody${url}use${text}md5forencrypt`
|
||||
const digest = CryptoJS.MD5(message).toString()
|
||||
const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`
|
||||
return {
|
||||
params: aesEncrypt(data, 'ecb', eapiKey, '', 'hex'),
|
||||
}
|
||||
}
|
||||
const eapiResDecrypt = (encryptedParams) => {
|
||||
// 使用aesDecrypt解密参数
|
||||
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
|
||||
return JSON.parse(decryptedData)
|
||||
}
|
||||
const eapiReqDecrypt = (encryptedParams) => {
|
||||
// 使用aesDecrypt解密参数
|
||||
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
|
||||
// 使用正则表达式解析出URL和数据
|
||||
const match = decryptedData.match(/(.*?)-36cd479b6b5-(.*?)-36cd479b6b5-(.*)/)
|
||||
if (match) {
|
||||
const url = match[1]
|
||||
const data = JSON.parse(match[2])
|
||||
return { url, data }
|
||||
}
|
||||
|
||||
// 如果没有匹配到,返回null
|
||||
return null
|
||||
}
|
||||
const decrypt = (cipher) => {
|
||||
const decipher = CryptoJS.AES.decrypt(
|
||||
{
|
||||
ciphertext: CryptoJS.enc.Hex.parse(cipher),
|
||||
},
|
||||
eapiKey,
|
||||
{
|
||||
mode: CryptoJS.mode.ECB,
|
||||
},
|
||||
)
|
||||
const decryptedBytes = CryptoJS.enc.Utf8.stringify(decipher)
|
||||
return decryptedBytes
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
weapi,
|
||||
linuxapi,
|
||||
eapi,
|
||||
decrypt,
|
||||
aesEncrypt,
|
||||
aesDecrypt,
|
||||
eapiReqDecrypt,
|
||||
eapiResDecrypt,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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('=')
|
||||
if (arr.length == 2) obj[arr[0].trim()] = arr[1].trim()
|
||||
})
|
||||
return obj
|
||||
},
|
||||
cookieObjToString(cookie) {
|
||||
return Object.keys(cookie)
|
||||
.map(
|
||||
(key) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(cookie[key])}`,
|
||||
)
|
||||
.join('; ')
|
||||
},
|
||||
getRandom(num) {
|
||||
var random = Math.floor(
|
||||
(Math.random() + Math.floor(Math.random() * 9 + 1)) *
|
||||
Math.pow(10, num - 1),
|
||||
)
|
||||
return random
|
||||
},
|
||||
generateRandomChineseIP() {
|
||||
const chinaIPPrefixes = ['116.25', '116.76', '116.77', '116.78']
|
||||
|
||||
const randomPrefix =
|
||||
chinaIPPrefixes[Math.floor(Math.random() * chinaIPPrefixes.length)]
|
||||
return `${randomPrefix}.${generateIPSegment()}.${generateIPSegment()}`
|
||||
},
|
||||
}
|
||||
|
||||
// 生成一个随机整数
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
// 生成一个随机IP地址段
|
||||
function generateIPSegment() {
|
||||
return getRandomInt(1, 255)
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
const createOption = (query, crypto = '') => {
|
||||
return {
|
||||
crypto: query.crypto || crypto || '',
|
||||
cookie: query.cookie,
|
||||
ua: query.ua || '',
|
||||
proxy: query.proxy,
|
||||
realIP: query.realIP,
|
||||
e_r: query.e_r || undefined,
|
||||
}
|
||||
}
|
||||
module.exports = createOption
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
const encrypt = require('./crypto')
|
||||
const CryptoJS = require('crypto-js')
|
||||
const { default: axios } = require('axios')
|
||||
const { PacProxyAgent } = require('pac-proxy-agent')
|
||||
const http = require('http')
|
||||
const https = require('https')
|
||||
const tunnel = require('tunnel')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const tmpPath = require('os').tmpdir()
|
||||
const { cookieToJson, cookieObjToString, toBoolean } = require('./index')
|
||||
const anonymous_token = fs.readFileSync(
|
||||
path.resolve(tmpPath, './anonymous_token'),
|
||||
'utf-8',
|
||||
)
|
||||
const { URLSearchParams, URL } = require('url')
|
||||
const { APP_CONF } = require('../util/config.json')
|
||||
// request.debug = true // 开启可看到更详细信息
|
||||
|
||||
const WNMCID = (function () {
|
||||
const characters = 'abcdefghijklmnopqrstuvwxyz'
|
||||
let randomString = ''
|
||||
for (let i = 0; i < 6; i++)
|
||||
randomString += characters.charAt(
|
||||
Math.floor(Math.random() * characters.length),
|
||||
)
|
||||
return `${randomString}.${Date.now().toString()}.01.0`
|
||||
})()
|
||||
|
||||
const osMap = {
|
||||
pc: {
|
||||
os: 'pc',
|
||||
appver: '3.0.18.203152',
|
||||
osver: 'Microsoft-Windows-10-Professional-build-22631-64bit',
|
||||
channel: 'netease',
|
||||
},
|
||||
linux: {
|
||||
os: 'linux',
|
||||
appver: '1.2.1.0428',
|
||||
osver: 'Deepin 20.9',
|
||||
channel: 'netease',
|
||||
},
|
||||
android: {
|
||||
os: 'android',
|
||||
appver: '8.20.20.231215173437',
|
||||
osver: '14',
|
||||
channel: 'xiaomi',
|
||||
},
|
||||
iphone: {
|
||||
os: 'iPhone OS',
|
||||
appver: '9.0.90',
|
||||
osver: '16.2',
|
||||
channel: 'distribution',
|
||||
},
|
||||
}
|
||||
|
||||
const chooseUserAgent = (crypto, uaType = 'pc') => {
|
||||
const userAgentMap = {
|
||||
weapi: {
|
||||
pc: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
|
||||
},
|
||||
linuxapi: {
|
||||
linux:
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
|
||||
},
|
||||
api: {
|
||||
pc: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/3.0.18.203152',
|
||||
android:
|
||||
'NeteaseMusic/9.1.65.240927161425(9001065);Dalvik/2.1.0 (Linux; U; Android 14; 23013RK75C Build/UKQ1.230804.001)',
|
||||
iphone: 'NeteaseMusic 9.0.90/5038 (iPhone; iOS 16.2; zh_CN)',
|
||||
},
|
||||
}
|
||||
return userAgentMap[crypto][uaType] || ''
|
||||
}
|
||||
const createRequest = (uri, data, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let headers = options.headers || {}
|
||||
let ip = options.realIP || options.ip || ''
|
||||
// console.log(ip)
|
||||
if (ip) {
|
||||
headers['X-Real-IP'] = ip
|
||||
headers['X-Forwarded-For'] = ip
|
||||
}
|
||||
// headers['X-Real-IP'] = '118.88.88.88'
|
||||
|
||||
let cookie = options.cookie || {}
|
||||
if (typeof cookie === 'string') {
|
||||
cookie = cookieToJson(cookie)
|
||||
}
|
||||
if (typeof cookie === 'object') {
|
||||
let _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString()
|
||||
let os = osMap[cookie.os] || osMap['iphone']
|
||||
cookie = {
|
||||
...cookie,
|
||||
__remember_me: 'true',
|
||||
// NMTID: CryptoJS.lib.WordArray.random(16).toString(),
|
||||
ntes_kaola_ad: '1',
|
||||
_ntes_nuid: cookie._ntes_nuid || _ntes_nuid,
|
||||
_ntes_nnid:
|
||||
cookie._ntes_nnid || `${_ntes_nuid},${Date.now().toString()}`,
|
||||
WNMCID: cookie.WNMCID || WNMCID,
|
||||
WEVNSM: cookie.WEVNSM || '1.0.0',
|
||||
|
||||
osver: cookie.osver || os.osver,
|
||||
deviceId: cookie.deviceId || global.deviceId,
|
||||
os: cookie.os || os.os,
|
||||
channel: cookie.channel || os.channel,
|
||||
appver: cookie.appver || os.appver,
|
||||
}
|
||||
if (uri.indexOf('login') === -1) {
|
||||
cookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString()
|
||||
}
|
||||
if (!cookie.MUSIC_U) {
|
||||
// 游客
|
||||
cookie.MUSIC_A = cookie.MUSIC_A || anonymous_token
|
||||
}
|
||||
headers['Cookie'] = cookieObjToString(cookie)
|
||||
}
|
||||
|
||||
let url = '',
|
||||
encryptData = '',
|
||||
crypto = options.crypto,
|
||||
csrfToken = cookie['__csrf'] || ''
|
||||
|
||||
if (crypto === '') {
|
||||
// 加密方式为空,以配置文件的加密方式为准
|
||||
if (APP_CONF.encrypt) {
|
||||
crypto = 'eapi'
|
||||
} else {
|
||||
crypto = 'api'
|
||||
}
|
||||
}
|
||||
|
||||
// 根据加密方式加密请求数据;目前任意uri都支持四种加密方式
|
||||
switch (crypto) {
|
||||
case 'weapi':
|
||||
headers['Referer'] = APP_CONF.domain
|
||||
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
|
||||
data.csrf_token = csrfToken
|
||||
encryptData = encrypt.weapi(data)
|
||||
url = APP_CONF.domain + '/weapi/' + uri.substr(5)
|
||||
break
|
||||
|
||||
case 'linuxapi':
|
||||
headers['User-Agent'] =
|
||||
options.ua || chooseUserAgent('linuxapi', 'linux')
|
||||
encryptData = encrypt.linuxapi({
|
||||
method: 'POST',
|
||||
url: APP_CONF.domain + uri,
|
||||
params: data,
|
||||
})
|
||||
url = APP_CONF.domain + '/api/linux/forward'
|
||||
break
|
||||
|
||||
case 'eapi':
|
||||
case 'api':
|
||||
// 两种加密方式,都应生成客户端的cookie
|
||||
const header = {
|
||||
osver: cookie.osver, //系统版本
|
||||
deviceId: cookie.deviceId,
|
||||
os: cookie.os, //系统类型
|
||||
appver: cookie.appver, // app版本
|
||||
versioncode: cookie.versioncode || '140', //版本号
|
||||
mobilename: cookie.mobilename || '', //设备model
|
||||
buildver: cookie.buildver || Date.now().toString().substr(0, 10),
|
||||
resolution: cookie.resolution || '1920x1080', //设备分辨率
|
||||
__csrf: csrfToken,
|
||||
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('; ')
|
||||
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone')
|
||||
|
||||
if (crypto === 'eapi') {
|
||||
// 使用eapi加密
|
||||
data.header = header
|
||||
data.e_r =
|
||||
options.e_r != undefined
|
||||
? options.e_r
|
||||
: data.e_r != undefined
|
||||
? data.e_r
|
||||
: APP_CONF.encryptResponse // 用于加密接口返回值
|
||||
data.e_r = toBoolean(data.e_r)
|
||||
encryptData = encrypt.eapi(uri, data)
|
||||
url = APP_CONF.apiDomain + '/eapi/' + uri.substr(5)
|
||||
} else if (crypto === 'api') {
|
||||
// 不使用任何加密
|
||||
url = APP_CONF.apiDomain + uri
|
||||
encryptData = data
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
// 未知的加密方式
|
||||
console.log('[ERR]', 'Unknown Crypto:', crypto)
|
||||
break
|
||||
}
|
||||
const answer = { status: 500, body: {}, cookie: [] }
|
||||
// console.log(headers, 'headers')
|
||||
let settings = {
|
||||
method: 'POST',
|
||||
url: url,
|
||||
headers: headers,
|
||||
data: new URLSearchParams(encryptData).toString(),
|
||||
httpAgent: new http.Agent({ keepAlive: true }),
|
||||
httpsAgent: new https.Agent({ keepAlive: true }),
|
||||
}
|
||||
|
||||
if (data.e_r) {
|
||||
settings = {
|
||||
...settings,
|
||||
encoding: null,
|
||||
responseType: 'arraybuffer',
|
||||
}
|
||||
}
|
||||
|
||||
if (options.proxy) {
|
||||
if (options.proxy.indexOf('pac') > -1) {
|
||||
settings.httpAgent = new PacProxyAgent(options.proxy)
|
||||
settings.httpsAgent = new PacProxyAgent(options.proxy)
|
||||
} else {
|
||||
const purl = new URL(options.proxy)
|
||||
if (purl.hostname) {
|
||||
const agent = tunnel[
|
||||
purl.protocol === 'https' ? 'httpsOverHttp' : 'httpOverHttp'
|
||||
]({
|
||||
proxy: {
|
||||
host: purl.hostname,
|
||||
port: purl.port || 80,
|
||||
proxyAuth:
|
||||
purl.username && purl.password
|
||||
? purl.username + ':' + purl.password
|
||||
: '',
|
||||
},
|
||||
})
|
||||
settings.httpsAgent = agent
|
||||
settings.httpAgent = agent
|
||||
settings.proxy = false
|
||||
} else {
|
||||
console.error('代理配置无效,不使用代理')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
settings.proxy = false
|
||||
}
|
||||
axios(settings)
|
||||
.then((res) => {
|
||||
const body = res.data
|
||||
answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
|
||||
x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
|
||||
)
|
||||
try {
|
||||
if (data.e_r) {
|
||||
// eapi接口返回值被加密,需要解密
|
||||
answer.body = encrypt.eapiResDecrypt(
|
||||
body.toString('hex').toUpperCase(),
|
||||
)
|
||||
} else {
|
||||
answer.body =
|
||||
typeof body == 'object' ? body : JSON.parse(body.toString())
|
||||
}
|
||||
|
||||
if (answer.body.code) {
|
||||
answer.body.code = Number(answer.body.code)
|
||||
}
|
||||
|
||||
answer.status = Number(answer.body.code || res.status)
|
||||
if (
|
||||
[201, 302, 400, 502, 800, 801, 802, 803].indexOf(answer.body.code) >
|
||||
-1
|
||||
) {
|
||||
// 特殊状态码
|
||||
answer.status = 200
|
||||
}
|
||||
} catch (e) {
|
||||
// console.log(e)
|
||||
// can't decrypt and can't parse directly
|
||||
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
|
||||
Reference in New Issue
Block a user