365 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 预先导入和绑定常用模块及函数
const encrypt = require('./crypto')
const CryptoJS = require('crypto-js')
const { default: axios } = require('axios')
const { PacProxyAgent } = require('pac-proxy-agent')
const logger = require('./logger')
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,
generateRandomChineseIP,
} = require('./index')
const { URLSearchParams, URL } = require('url')
const { APP_CONF } = require('../util/config.json')
// 预先读取匿名token并缓存
const anonymous_token = fs.readFileSync(
path.resolve(tmpPath, './anonymous_token'),
'utf-8',
)
// 预先绑定常用函数和常量
const floor = Math.floor
const random = Math.random
const now = Date.now
const keys = Object.keys
const stringify = JSON.stringify
const parse = JSON.parse
const characters = 'abcdefghijklmnopqrstuvwxyz'
const charactersLength = characters.length
// 预先创建HTTP/HTTPS agents并重用
const createHttpAgent = () => new http.Agent({ keepAlive: true })
const createHttpsAgent = () => new https.Agent({ keepAlive: true })
// 预先计算WNMCID只计算一次
const WNMCID = (function () {
let randomString = ''
for (let i = 0; i < 6; i++) {
randomString += characters.charAt(floor(random() * charactersLength))
}
return `${randomString}.${now().toString()}.01.0`
})()
// 预先定义osMap
const osMap = {
pc: {
os: 'pc',
appver: '3.1.17.204416',
osver: 'Microsoft-Windows-10-Professional-build-19045-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',
},
}
// 预先定义userAgentMap
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)',
},
}
// 预先定义常量
const DOMAIN = APP_CONF.domain
const API_DOMAIN = APP_CONF.apiDomain
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
// chooseUserAgent函数
const chooseUserAgent = (crypto, uaType = 'pc') => {
return userAgentMap[crypto]?.[uaType] || ''
}
// cookie处理
const processCookieObject = (cookie, uri) => {
const _ntes_nuid = CryptoJS.lib.WordArray.random(32).toString()
const os = osMap[cookie.os] || osMap['pc']
const processedCookie = {
...cookie,
__remember_me: 'true',
ntes_kaola_ad: '1',
_ntes_nuid: cookie._ntes_nuid || _ntes_nuid,
_ntes_nnid: cookie._ntes_nnid || `${_ntes_nuid},${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) {
processedCookie['NMTID'] = CryptoJS.lib.WordArray.random(16).toString()
}
if (!processedCookie.MUSIC_U) {
processedCookie.MUSIC_A = processedCookie.MUSIC_A || anonymous_token
}
return processedCookie
}
// header cookie生成
const createHeaderCookie = (header) => {
const headerKeys = keys(header)
const cookieParts = new Array(headerKeys.length)
for (let i = 0, len = headerKeys.length; i < len; i++) {
const key = headerKeys[i]
cookieParts[i] =
encodeURIComponent(key) + '=' + encodeURIComponent(header[key])
}
return cookieParts.join('; ')
}
// requestId生成
const generateRequestId = () => {
return `${now()}_${floor(random() * 1000)
.toString()
.padStart(4, "0")}`;
}
const createRequest = (uri, data, options) => {
return new Promise((resolve, reject) => {
// 变量声明和初始化
const headers = options.headers ? { ...options.headers } : {}
const ip = options.realIP || options.ip || (options.randomCNIP ? generateRandomChineseIP() : '')
// IP头设置
if (ip) {
headers['X-Real-IP'] = ip
headers['X-Forwarded-For'] = ip
}
let cookie = options.cookie || {}
if (typeof cookie === 'string') {
cookie = cookieToJson(cookie)
}
if (typeof cookie === 'object') {
cookie = processCookieObject(cookie, uri)
headers['Cookie'] = cookieObjToString(cookie)
}
let url = ''
let encryptData = ''
let crypto = options.crypto
const csrfToken = cookie['__csrf'] || ''
// 加密方式选择
if (crypto === '') {
crypto = APP_CONF.encrypt ? 'eapi' : 'api'
}
const answer = { status: 500, body: {}, cookie: [] }
// 根据加密方式处理
switch (crypto) {
case 'weapi':
headers['Referer'] = DOMAIN
headers['User-Agent'] = options.ua || chooseUserAgent('weapi')
data.csrf_token = csrfToken
encryptData = encrypt.weapi(data)
url = DOMAIN + '/weapi/' + uri.substr(5)
break
case 'linuxapi':
headers['User-Agent'] =
options.ua || chooseUserAgent('linuxapi', 'linux')
encryptData = encrypt.linuxapi({
method: 'POST',
url: DOMAIN + uri,
params: data,
})
url = DOMAIN + '/api/linux/forward'
break
case 'eapi':
case 'api':
// header创建
const header = {
osver: cookie.osver,
deviceId: cookie.deviceId,
os: cookie.os,
appver: cookie.appver,
versioncode: cookie.versioncode || '140',
mobilename: cookie.mobilename || '',
buildver: cookie.buildver || now().toString().substr(0, 10),
resolution: cookie.resolution || '1920x1080',
__csrf: csrfToken,
channel: cookie.channel,
requestId: generateRequestId(),
...(options.checkToken
? { 'X-antiCheatToken': APP_CONF.checkToken }
: {}),
// clientSign: APP_CONF.clientSign,
}
if (cookie.MUSIC_U) header['MUSIC_U'] = cookie.MUSIC_U
if (cookie.MUSIC_A) header['MUSIC_A'] = cookie.MUSIC_A
headers['Cookie'] = createHeaderCookie(header)
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone')
if (crypto === 'eapi') {
data.header = header
data.e_r = toBoolean(
options.e_r !== undefined
? options.e_r
: data.e_r !== undefined
? data.e_r
: ENCRYPT_RESPONSE,
)
encryptData = encrypt.eapi(uri, data)
url = API_DOMAIN + '/eapi/' + uri.substr(5)
} else if (crypto === 'api') {
url = API_DOMAIN + uri
encryptData = data
}
break
default:
logger.error('Unknown Crypto:', crypto)
break
}
// logger.info(url);
// settings创建
let settings = {
method: 'POST',
url: url,
headers: headers,
data: new URLSearchParams(encryptData).toString(),
httpAgent: createHttpAgent(),
httpsAgent: createHttpsAgent(),
}
// e_r处理
if (data.e_r) {
settings.encoding = null
settings.responseType = 'arraybuffer'
}
// 代理处理
if (options.proxy) {
if (options.proxy.indexOf('pac') > -1) {
const agent = new PacProxyAgent(options.proxy)
settings.httpAgent = agent
settings.httpsAgent = agent
} else {
try {
const purl = new URL(options.proxy)
if (purl.hostname) {
const isHttps = purl.protocol === 'https:'
const agent = tunnel[isHttps ? '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 {
logger.error('代理配置无效,不使用代理')
}
} catch (e) {
logger.error('代理URL解析失败:', e.message)
}
}
} else {
settings.proxy = false
}
// logger.info(settings.headers);
axios(settings)
.then((res) => {
const body = res.data
answer.cookie = (res.headers['set-cookie'] || []).map((x) =>
x.replace(/\s*Domain=[^(;|$)]+;*/, ''),
)
try {
if (crypto === 'eapi' && data.e_r) {
answer.body = encrypt.eapiResDecrypt(
body.toString('hex').toUpperCase(),
)
} else {
answer.body =
typeof body === 'object' ? body : parse(body.toString())
}
if (answer.body.code) {
answer.body.code = Number(answer.body.code)
}
answer.status = Number(answer.body.code || res.status)
// 状态码检查使用Set提升查找性能
if (SPECIAL_STATUS_CODES.has(answer.body.code)) {
answer.status = 200
}
} catch (e) {
answer.body = body
answer.status = res.status
}
answer.status =
answer.status > 100 && answer.status < 600 ? answer.status : 400
if (answer.status === 200) {
resolve(answer)
} else {
logger.error(answer)
reject(answer)
}
})
.catch((err) => {
answer.status = 502
answer.body = { code: 502, msg: err.message || err }
logger.error(answer)
reject(answer)
})
})
}
module.exports = createRequest