mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-10-20 22:51:23 +08:00
Merge branch 'main' into pr
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"apiDomain": "https://interface.music.163.com",
|
||||
"eapiDomain": "https://interfacepc.music.163.com",
|
||||
"xeapiDomain": "https://interface3.music.163.com",
|
||||
"neapiDomain": "https://interface3.music.163.com",
|
||||
"domain": "https://music.163.com",
|
||||
"clDomian": "https://clientlog.music.163.com",
|
||||
"clDomian3": "https://clientlog3.music.163.com",
|
||||
|
||||
@@ -2,6 +2,7 @@ const CryptoJS = require('crypto-js')
|
||||
const crypto = require('crypto')
|
||||
const forge = require('node-forge')
|
||||
const zlib = require('zlib')
|
||||
const zstd = require('./zstd')
|
||||
const iv = '0102030405060708'
|
||||
const presetKey = '0CoJUm6Qyw8W8jud'
|
||||
const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
|
||||
@@ -310,11 +311,86 @@ const xeapiDecryptPublicKey = (encryptedData) => {
|
||||
)
|
||||
}
|
||||
|
||||
const NEAPI_MAGIC = Buffer.from('CSJM')
|
||||
const NEAPI_NONCE_LEN = 12
|
||||
const NEAPI_AD_LEN = 16
|
||||
const NEAPI_TAG_LEN = 16
|
||||
const NEAPI_HEADER_LEN = 4 + 1 + 1 + 2 + NEAPI_NONCE_LEN + 1 + NEAPI_AD_LEN + 4
|
||||
|
||||
// Header: magic, the config version as two big-endian bytes, then the
|
||||
// nonce / associated data / sealed length.
|
||||
const neapi = (data, config) => {
|
||||
const nonce = crypto.randomBytes(NEAPI_NONCE_LEN)
|
||||
const ad = crypto.randomBytes(NEAPI_AD_LEN)
|
||||
const cipher = crypto.createCipheriv(
|
||||
'chacha20-poly1305',
|
||||
config.encryptKey,
|
||||
nonce,
|
||||
{ authTagLength: NEAPI_TAG_LEN },
|
||||
)
|
||||
cipher.setAAD(ad)
|
||||
|
||||
const sealed = Buffer.concat([
|
||||
cipher.update(zstd.compress(data)),
|
||||
cipher.final(),
|
||||
cipher.getAuthTag(),
|
||||
])
|
||||
|
||||
const header = Buffer.alloc(NEAPI_HEADER_LEN)
|
||||
NEAPI_MAGIC.copy(header, 0)
|
||||
header[4] = config.version >> 8
|
||||
header[5] = config.version & 0xff
|
||||
header.writeUInt16LE(nonce.length, 6)
|
||||
nonce.copy(header, 8)
|
||||
header[8 + nonce.length] = ad.length
|
||||
ad.copy(header, 9 + nonce.length)
|
||||
header.writeUInt32LE(sealed.length, 9 + nonce.length + ad.length)
|
||||
|
||||
return Buffer.concat([header, sealed]).toString('base64')
|
||||
}
|
||||
|
||||
// Some error responses repeat the same document back to back.
|
||||
const parseBody = (text) => {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
const boundary = text.indexOf('}{')
|
||||
if (boundary < 0) throw error
|
||||
return JSON.parse(text.slice(0, boundary + 1))
|
||||
}
|
||||
}
|
||||
|
||||
const neapiResDecrypt = (body, config) => {
|
||||
const raw = Buffer.from(String(body).trim(), 'base64')
|
||||
const nonceLen = raw.readUInt16LE(6)
|
||||
const nonce = raw.subarray(8, 8 + nonceLen)
|
||||
const adLen = raw[8 + nonceLen]
|
||||
const ad = raw.subarray(9 + nonceLen, 9 + nonceLen + adLen)
|
||||
const offset = 9 + nonceLen + adLen
|
||||
const sealed = raw.subarray(offset + 4, offset + 4 + raw.readUInt32LE(offset))
|
||||
|
||||
const decipher = crypto.createDecipheriv(
|
||||
'chacha20-poly1305',
|
||||
config.decryptKey,
|
||||
nonce,
|
||||
{ authTagLength: NEAPI_TAG_LEN },
|
||||
)
|
||||
decipher.setAAD(ad)
|
||||
decipher.setAuthTag(sealed.subarray(sealed.length - NEAPI_TAG_LEN))
|
||||
|
||||
const compressed = Buffer.concat([
|
||||
decipher.update(sealed.subarray(0, sealed.length - NEAPI_TAG_LEN)),
|
||||
decipher.final(),
|
||||
])
|
||||
return parseBody(zstd.decompress(compressed).toString())
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
weapi,
|
||||
linuxapi,
|
||||
eapi,
|
||||
xeapi,
|
||||
neapi,
|
||||
decrypt,
|
||||
aesEncrypt,
|
||||
aesDecrypt,
|
||||
@@ -323,4 +399,5 @@ module.exports = {
|
||||
xeapiSign,
|
||||
xeapiResDecrypt,
|
||||
xeapiDecryptPublicKey,
|
||||
neapiResDecrypt,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
const crypto = require('crypto')
|
||||
const zstd = require('./zstd')
|
||||
|
||||
// gorilla/algorithm/record 返回的 base64 解码后:
|
||||
// [0 .. 64) RSA 块 -> RSA_PKCS1 去填充得到 RC4 密钥
|
||||
// [64 .. 68) u32 LE 载荷长度
|
||||
// [68 .. ) RC4 载荷 -> zstd 解压得到 ECFG 容器
|
||||
const CONFIG_PUBLIC_KEY = Buffer.from(
|
||||
'3048024100e4e79c1cb27a9fab7d2c740e99e923ec51c009169ec3a380c60404' +
|
||||
'e01b060f0160cdc631d9fb7fe53b228a5ff11bee42b965d28c6c2036193f4ccd' +
|
||||
'c7ed853d110203010001',
|
||||
'hex',
|
||||
)
|
||||
|
||||
// 容器里的 Lua 区块对指令和字符串做了逐字节异或混淆
|
||||
const CODE_XOR = [
|
||||
0x94b4f825, 0x977c6795, 0x327de32b, 0x16da5ba1, 0xcf55a2eb, 0x080bb445,
|
||||
0x021af055, 0xfc56b8bd,
|
||||
]
|
||||
const STR_XOR = Buffer.from('aefda2d1832b518a7741c325fb2687f0', 'hex')
|
||||
const CHUNK_SIGNATURE = Buffer.from('7f454c46', 'hex')
|
||||
|
||||
// 脚本用这两个标记区分请求方向与响应方向
|
||||
const DIRECTION_ENCRYPT = 251
|
||||
const DIRECTION_DECRYPT = 503
|
||||
const KEY_LEN = 32
|
||||
|
||||
const T_NUMFLT = 3
|
||||
const T_NUMINT = 19
|
||||
const OP_LOADK = 1
|
||||
const OP_GETUPVAL = 5
|
||||
const OP_EQ = 31
|
||||
|
||||
const op = (ins) => ins & 0x3f
|
||||
const argA = (ins) => (ins >>> 6) & 0xff
|
||||
const argB = (ins) => (ins >>> 23) & 0x1ff
|
||||
const argC = (ins) => (ins >>> 14) & 0x1ff
|
||||
const argBx = (ins) => (ins >>> 14) & 0x3ffff
|
||||
|
||||
const rc4 = (key, data) => {
|
||||
const s = Buffer.alloc(256)
|
||||
for (let i = 0; i < 256; i++) s[i] = i
|
||||
|
||||
let j = 0
|
||||
for (let i = 0; i < 256; i++) {
|
||||
j = (j + s[i] + key[i % key.length]) & 0xff
|
||||
;[s[i], s[j]] = [s[j], s[i]]
|
||||
}
|
||||
|
||||
const out = Buffer.allocUnsafe(data.length)
|
||||
let i = 0
|
||||
j = 0
|
||||
for (let n = 0; n < data.length; n++) {
|
||||
i = (i + 1) & 0xff
|
||||
j = (j + s[i]) & 0xff
|
||||
;[s[i], s[j]] = [s[j], s[i]]
|
||||
out[n] = data[n] ^ s[(s[i] + s[j]) & 0xff]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const unwrapContainer = (raw) => {
|
||||
const key = crypto.publicDecrypt(
|
||||
crypto.createPublicKey({
|
||||
key: CONFIG_PUBLIC_KEY,
|
||||
format: 'der',
|
||||
type: 'pkcs1',
|
||||
}),
|
||||
raw.subarray(0, 64),
|
||||
)
|
||||
const size = raw.readUInt32LE(64)
|
||||
return zstd.decompress(rc4(key, raw.subarray(68, 68 + size)))
|
||||
}
|
||||
|
||||
class ChunkReader {
|
||||
constructor(buf, pos = 0) {
|
||||
this.buf = buf
|
||||
this.pos = pos
|
||||
}
|
||||
|
||||
u8() {
|
||||
return this.buf[this.pos++]
|
||||
}
|
||||
|
||||
u32() {
|
||||
const value = this.buf.readUInt32LE(this.pos)
|
||||
this.pos += 4
|
||||
return value
|
||||
}
|
||||
|
||||
i64() {
|
||||
const value = this.buf.readBigInt64LE(this.pos)
|
||||
this.pos += 8
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
f64() {
|
||||
const value = this.buf.readDoubleLE(this.pos)
|
||||
this.pos += 8
|
||||
return value
|
||||
}
|
||||
|
||||
string() {
|
||||
let size = this.u8()
|
||||
if (size === 0) return null
|
||||
if (size === 0xff) size = this.u32()
|
||||
// 长度比实际字节数多 1(标准 luac 的写法)
|
||||
const body = this.buf.subarray(this.pos, this.pos + size - 1)
|
||||
this.pos += size - 1
|
||||
const out = Buffer.allocUnsafe(body.length)
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
out[i] = body[i] ^ STR_XOR[i & 0x0f]
|
||||
}
|
||||
return out.toString('utf8')
|
||||
}
|
||||
}
|
||||
|
||||
const loadProto = (r) => {
|
||||
const proto = {
|
||||
source: r.string(),
|
||||
linedefined: r.u32(),
|
||||
lastlinedefined: r.u32(),
|
||||
numparams: r.u8(),
|
||||
isvararg: r.u8(),
|
||||
maxstack: r.u8(),
|
||||
}
|
||||
|
||||
let count = r.u32()
|
||||
proto.code = new Array(count)
|
||||
for (let i = 0; i < count; i++) {
|
||||
proto.code[i] = (r.u32() ^ CODE_XOR[i & 7]) >>> 0
|
||||
}
|
||||
|
||||
// 常量标签:0 nil / 1 boolean / 3 float / 19 integer / 4,20 string
|
||||
// 只保留用得到的整数与字符串
|
||||
count = r.u32()
|
||||
proto.consts = new Array(count)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const tag = r.u8()
|
||||
if (tag === 0) proto.consts[i] = null
|
||||
else if (tag === 1) {
|
||||
r.u8()
|
||||
proto.consts[i] = null
|
||||
} else if (tag === T_NUMFLT) {
|
||||
r.f64()
|
||||
proto.consts[i] = null
|
||||
} else if (tag === T_NUMINT) proto.consts[i] = r.i64()
|
||||
else proto.consts[i] = r.string()
|
||||
}
|
||||
|
||||
count = r.u32()
|
||||
proto.upvals = new Array(count)
|
||||
for (let i = 0; i < count; i++) {
|
||||
proto.upvals[i] = [r.u8(), r.u8()]
|
||||
}
|
||||
|
||||
count = r.u32()
|
||||
proto.protos = new Array(count)
|
||||
for (let i = 0; i < count; i++) {
|
||||
proto.protos[i] = loadProto(r)
|
||||
}
|
||||
|
||||
// 调试段跳过即可
|
||||
count = r.u32()
|
||||
r.pos += 4 * count
|
||||
count = r.u32()
|
||||
for (let i = 0; i < count; i++) {
|
||||
r.string()
|
||||
r.u32()
|
||||
r.u32()
|
||||
}
|
||||
count = r.u32()
|
||||
for (let i = 0; i < count; i++) {
|
||||
r.string()
|
||||
}
|
||||
return proto
|
||||
}
|
||||
|
||||
const loadChunk = (buf) => {
|
||||
if (!buf.subarray(0, 4).equals(CHUNK_SIGNATURE)) {
|
||||
throw new Error('neapi config is not a chunk')
|
||||
}
|
||||
const r = new ChunkReader(buf, 33)
|
||||
r.u8() // 主闭包 upvalue 数量
|
||||
return loadProto(r)
|
||||
}
|
||||
|
||||
// 主函数用一连串 LOADK 装载所有标量,据此可以把 upvalue 追回常量值
|
||||
const mainRegisters = (main) => {
|
||||
const regs = new Map()
|
||||
for (const ins of main.code) {
|
||||
if (op(ins) === OP_LOADK) regs.set(argA(ins), main.consts[argBx(ins)])
|
||||
}
|
||||
return regs
|
||||
}
|
||||
|
||||
// create_ctx 里方向 -> 密钥偏移的分支:
|
||||
// if direction == <a> then offset = <upvalue>
|
||||
// elseif direction == <b> then offset = <upvalue>
|
||||
const extractKeyOffsets = (script) => {
|
||||
const createCtx = script.protos[0]
|
||||
const regs = mainRegisters(script)
|
||||
const offsets = {}
|
||||
let pending = null
|
||||
|
||||
for (const ins of createCtx.code) {
|
||||
const code = op(ins)
|
||||
if (code === OP_EQ) {
|
||||
const c = argC(ins)
|
||||
const constant = c & 0x100 && createCtx.consts[c & 0xff]
|
||||
if (Number.isInteger(constant)) pending = constant
|
||||
} else if (code === OP_GETUPVAL && pending !== null) {
|
||||
const up = createCtx.upvals[argB(ins)]
|
||||
const constant = up && up[0] === 1 && regs.get(up[1])
|
||||
if (Number.isInteger(constant)) {
|
||||
offsets[pending] = constant
|
||||
pending = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(DIRECTION_ENCRYPT in offsets) || !(DIRECTION_DECRYPT in offsets)) {
|
||||
throw new Error('neapi config script has an unexpected direction table')
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
const extractKeyBlob = (script) => {
|
||||
const blob = script.consts
|
||||
.filter(
|
||||
(c) => typeof c === 'string' && /^[A-Za-z0-9+/]{64,}={0,2}$/.test(c),
|
||||
)
|
||||
.sort((a, b) => b.length - a.length)[0]
|
||||
return Buffer.from(blob, 'base64')
|
||||
}
|
||||
|
||||
const decodeConfig = (raw) => {
|
||||
const ecfg = unwrapContainer(raw)
|
||||
// ECFG 头 4 字节 + 大端 u16 版本,之后是 Lua 区块
|
||||
const version = ecfg.readUInt16BE(4)
|
||||
const script = loadChunk(ecfg.subarray(6))
|
||||
|
||||
const offsets = extractKeyOffsets(script)
|
||||
const keyBlob = extractKeyBlob(script)
|
||||
const slice = (direction) =>
|
||||
keyBlob.subarray(offsets[direction], offsets[direction] + KEY_LEN)
|
||||
|
||||
return {
|
||||
version,
|
||||
keyBlob,
|
||||
encryptKey: slice(DIRECTION_ENCRYPT),
|
||||
decryptKey: slice(DIRECTION_DECRYPT),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
decodeConfig,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// 配置会轮换,启动时由 /register/neapikey 拉取后缓存到这里
|
||||
const neapiKeyPath = path.resolve(require('os').tmpdir(), 'neapi_key')
|
||||
|
||||
let neapi_key = null
|
||||
|
||||
const loadNeapiKey = () => {
|
||||
if (!neapi_key && fs.existsSync(neapiKeyPath)) {
|
||||
try {
|
||||
const stored = JSON.parse(fs.readFileSync(neapiKeyPath, 'utf-8'))
|
||||
neapi_key = {
|
||||
version: stored.version,
|
||||
circleTime: stored.circleTime,
|
||||
fetchedAt: stored.fetchedAt,
|
||||
keyBlob: Buffer.from(stored.keyBlob, 'base64'),
|
||||
encryptKey: Buffer.from(stored.encryptKey, 'base64'),
|
||||
decryptKey: Buffer.from(stored.decryptKey, 'base64'),
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('[ERR]', error)
|
||||
}
|
||||
}
|
||||
return neapi_key
|
||||
}
|
||||
|
||||
const saveNeapiKey = (config, circleTime) => {
|
||||
const fetchedAt = Date.now()
|
||||
fs.writeFileSync(
|
||||
neapiKeyPath,
|
||||
JSON.stringify({
|
||||
version: config.version,
|
||||
circleTime,
|
||||
fetchedAt,
|
||||
keyBlob: config.keyBlob.toString('base64'),
|
||||
encryptKey: config.encryptKey.toString('base64'),
|
||||
decryptKey: config.decryptKey.toString('base64'),
|
||||
}),
|
||||
'utf-8',
|
||||
)
|
||||
neapi_key = { ...config, circleTime, fetchedAt }
|
||||
return neapi_key
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadNeapiKey,
|
||||
saveNeapiKey,
|
||||
}
|
||||
+97
-5
@@ -18,12 +18,14 @@ const {
|
||||
} = require('./index')
|
||||
const { URLSearchParams, URL } = require('url')
|
||||
const { APP_CONF } = require('./config.json')
|
||||
const { loadNeapiKey } = require('./neapiKey')
|
||||
const {
|
||||
getToken: antiCheatTokenV2,
|
||||
} = require('../module/register_checktoken_v2')
|
||||
const {
|
||||
getToken: antiCheatTokenV3,
|
||||
} = require('../module/register_checktoken_v3')
|
||||
const registerNeapikey = require('../module/register_neapikey')
|
||||
|
||||
// 预先读取匿名token并缓存
|
||||
const anonymous_token = fs.readFileSync(
|
||||
@@ -71,6 +73,20 @@ const WNMCID = (function () {
|
||||
let NMTID = ''
|
||||
let NMTID_RETRIES_LEFT = 3
|
||||
|
||||
// neapi 配置过期时只让一个请求去刷新,并发的其余请求共用同一次结果
|
||||
let neapiUserStatus = ''
|
||||
let neapiRefreshing = null
|
||||
const refreshNeapiKey = (version) => {
|
||||
if (!neapiRefreshing) {
|
||||
neapiRefreshing = registerNeapikey({ version, behavior: 'update' }, null)
|
||||
.catch((error) => console.log('[ERR]', error))
|
||||
.finally(() => {
|
||||
neapiRefreshing = null
|
||||
})
|
||||
}
|
||||
return neapiRefreshing
|
||||
}
|
||||
|
||||
// 预先定义osMap
|
||||
const osMap = {
|
||||
pc: {
|
||||
@@ -127,6 +143,7 @@ const DOMAIN = APP_CONF.domain
|
||||
const API_DOMAIN = APP_CONF.apiDomain
|
||||
const EAPI_DOMAIN = APP_CONF.eapiDomain
|
||||
const XEAPI_DOMAIN = APP_CONF.xeapiDomain
|
||||
const NEAPI_DOMAIN = APP_CONF.neapiDomain
|
||||
const ENCRYPT_RESPONSE = APP_CONF.encryptResponse
|
||||
const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
|
||||
|
||||
@@ -321,6 +338,47 @@ const createRequest = async (uri, data, options) => {
|
||||
})
|
||||
break
|
||||
|
||||
case 'neapi':
|
||||
const neapiKey = loadNeapiKey()
|
||||
if (!neapiKey) {
|
||||
throw new Error('neapi config is missing')
|
||||
}
|
||||
// 超过 circleTime 就后台刷新,本次仍用手上的配置
|
||||
if (now() - neapiKey.fetchedAt >= neapiKey.circleTime) {
|
||||
refreshNeapiKey(neapiKey.version)
|
||||
}
|
||||
const neapiCookie = {
|
||||
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(),
|
||||
}
|
||||
if (cookie.MUSIC_U) neapiCookie['MUSIC_U'] = cookie.MUSIC_U
|
||||
if (cookie.MUSIC_A) neapiCookie['MUSIC_A'] = cookie.MUSIC_A
|
||||
headers['Cookie'] = createHeaderCookie(neapiCookie)
|
||||
headers['User-Agent'] = options.ua || chooseUserAgent('api', 'android')
|
||||
headers['content-type'] =
|
||||
'application/x-www-form-urlencoded;charset=utf-8'
|
||||
// 服务端按 X-ER 选密钥,配置过期时由下面的 777/999 分支重拉并重试
|
||||
headers['X-ER'] = String(neapiKey.version)
|
||||
if (neapiUserStatus) headers['X-USER-STATUS'] = neapiUserStatus
|
||||
|
||||
const neapiBody = { ...data }
|
||||
delete neapiBody.e_r
|
||||
encryptData = encrypt.neapi(
|
||||
new URLSearchParams(neapiBody).toString(),
|
||||
neapiKey,
|
||||
)
|
||||
url = (options.domain || NEAPI_DOMAIN) + '/neapi/' + uri.substr(5)
|
||||
break
|
||||
|
||||
case 'eapi':
|
||||
case 'api':
|
||||
// header创建
|
||||
@@ -367,12 +425,19 @@ const createRequest = async (uri, data, options) => {
|
||||
console.log('[ERR]', 'Unknown Crypto:', crypto)
|
||||
break
|
||||
}
|
||||
const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
|
||||
const use_xeapi = crypto === 'xeapi'
|
||||
const use_neapi = crypto === 'neapi'
|
||||
|
||||
// settings创建
|
||||
let settings = {
|
||||
method: 'POST',
|
||||
url: url,
|
||||
headers: headers,
|
||||
data: new URLSearchParams(encryptData).toString(),
|
||||
// neapi 的请求体就是容器本身,不是表单字段
|
||||
data: use_neapi
|
||||
? encryptData
|
||||
: new URLSearchParams(encryptData).toString(),
|
||||
httpAgent: createHttpAgent(),
|
||||
httpsAgent: createHttpsAgent(),
|
||||
}
|
||||
@@ -383,9 +448,7 @@ const createRequest = async (uri, data, options) => {
|
||||
}
|
||||
|
||||
// 使用返回值加密
|
||||
const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
|
||||
const use_xeapi = crypto === 'xeapi'
|
||||
if (use_e_r || use_xeapi) {
|
||||
if (use_e_r || use_xeapi || use_neapi) {
|
||||
settings.encoding = null
|
||||
settings.responseType = 'arraybuffer'
|
||||
}
|
||||
@@ -426,7 +489,7 @@ const createRequest = async (uri, data, options) => {
|
||||
}
|
||||
// console.log(settings.headers);
|
||||
axios(settings)
|
||||
.then((res) => {
|
||||
.then(async (res) => {
|
||||
const body = res.data
|
||||
const setCookies = res.headers['set-cookie'] || []
|
||||
|
||||
@@ -457,6 +520,30 @@ const createRequest = async (uri, data, options) => {
|
||||
answer.cookie = setCookies.map(cleanCookie)
|
||||
}
|
||||
|
||||
// 空值同样要记下来,否则状态恢复后会一直带着上一次的值
|
||||
const userStatus = res.headers['x-user-status'] || ''
|
||||
if (use_neapi) {
|
||||
neapiUserStatus = userStatus
|
||||
if (
|
||||
(userStatus === '777' || userStatus === '999') &&
|
||||
!options.neapiRetried
|
||||
) {
|
||||
// 配置已轮换:刷新后重试一次
|
||||
await refreshNeapiKey(loadNeapiKey().version)
|
||||
try {
|
||||
resolve(
|
||||
await createRequest(uri, data, {
|
||||
...options,
|
||||
neapiRetried: true,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// debug: 统一注释块,需要时取消注释查看请求/返回的原始密文
|
||||
|
||||
// logger.debug(`[${crypto}]`, uri)
|
||||
@@ -475,6 +562,11 @@ const createRequest = async (uri, data, options) => {
|
||||
xeapiSessionKey = res.headers['x-encr-sskey']
|
||||
}
|
||||
answer.body = encrypt.xeapiResDecrypt(Buffer.from(body))
|
||||
} else if (use_neapi) {
|
||||
answer.body = encrypt.neapiResDecrypt(
|
||||
Buffer.from(body).toString(),
|
||||
loadNeapiKey(),
|
||||
)
|
||||
} else if (use_e_r) {
|
||||
answer.body = encrypt.eapiResDecrypt(
|
||||
body.toString('hex').toUpperCase(),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const zlib = require('zlib')
|
||||
const { decompress: fzstdDecompress } = require('fzstd')
|
||||
|
||||
// Node 22.15+ 自带 zstd。更早的版本(CI 含 18)用 fzstd 解压,压缩则退回到
|
||||
// raw block 帧:内容原样存储,任何 zstd 解码器都能读,只是没有压缩率。
|
||||
const hasNative = typeof zlib.zstdCompressSync === 'function'
|
||||
|
||||
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
||||
const WINDOW_DESCRIPTOR = 0x50 // windowLog 20
|
||||
const BLOCK_MAX = 0x20000 // zstd 单块上限 128 KiB
|
||||
|
||||
const compressRawFrame = (input) => {
|
||||
const parts = [MAGIC, Buffer.from([0x00, WINDOW_DESCRIPTOR])]
|
||||
let offset = 0
|
||||
do {
|
||||
const block = input.subarray(offset, offset + BLOCK_MAX)
|
||||
const last = offset + BLOCK_MAX >= input.length
|
||||
const header = Buffer.alloc(3)
|
||||
header.writeUIntLE((block.length << 3) | (last ? 1 : 0), 0, 3)
|
||||
parts.push(header, block)
|
||||
offset += BLOCK_MAX
|
||||
} while (offset < input.length)
|
||||
return Buffer.concat(parts)
|
||||
}
|
||||
|
||||
const compress = (input) => {
|
||||
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input)
|
||||
return hasNative ? zlib.zstdCompressSync(buf) : compressRawFrame(buf)
|
||||
}
|
||||
|
||||
const decompress = (input) => {
|
||||
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input)
|
||||
return hasNative
|
||||
? zlib.zstdDecompressSync(buf)
|
||||
: Buffer.from(fzstdDecompress(buf))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compress,
|
||||
decompress,
|
||||
}
|
||||
Reference in New Issue
Block a user