存档:TS 重构前基线(lx 聚合源 = isolated-vm 加载器,签名方案已移除)

This commit is contained in:
archive committed 2026-08-28 23:24:49 +08:00
commit f289df4cd7
124 files changed
+14110

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
// lx-music-source-v5 复现/追踪脚本(与生产加载器 src/apis/lx-source-ivm.js 相互独立)
// 1) 将去混淆脚本末尾的字符串缓存 [] 替换为 LOGCACHE(Proxy),按序记录 JSVMP 解码的全部字符串
// 2) 模拟 lx-music-desktop preload.js 的 globalThis.lx 环境,观察脚本的注册与取流行为
// 运行:node tmp/reproduce-lx-source-v5.mjs
import crypto from 'node:crypto'
import fs from 'node:fs'
import vm from 'node:vm'
import zlib from 'node:zlib'
import { promisify } from 'node:util'
import { fileURLToPath } from 'node:url'
const inflate = promisify(zlib.inflate)
const deflate = promisify(zlib.deflate)
const md5 = (s) => crypto.createHash('md5').update(s).digest('hex')
const deobPath = fileURLToPath(new URL('../lx-music-source-v5-deobfuscated.js', import.meta.url))
const obfPath = fileURLToPath(new URL('../lx-music-source-v5.js', import.meta.url))
let code = fs.readFileSync(deobPath, 'utf8')
// 字符串缓存:文件末尾 IIFE 最后一个参数 []
const cacheLog = []
const LOGCACHE = new Proxy([], {
set(t, prop, val) {
if (typeof val === 'string') cacheLog.push(val)
t[prop] = val
return true
},
})
const idx = code.lastIndexOf(', []);')
code = code.slice(0, idx) + ', LOGCACHE);'
// ---- lx 环境(复刻 preload.js) ----
const handlers = {}
const requestCalls = []
let rconfigBody = process.env.RCONFIG_BODY ?? JSON.stringify({ code: 0, msg: 'success', data: 'https://example.test/fake.mp3' })
const lx = {
EVENT_NAMES: { request: 'request', inited: 'inited', updateAlert: 'updateAlert' },
request(url, options = {}, callback) {
const rec = { url, options: { method: options.method ?? 'get', timeout: options.timeout, headers: options.headers, body: options.body, form: options.form, formData: options.formData } }
requestCalls.push(rec)
console.log('[lx.request]', url)
console.log(' options:', JSON.stringify(rec.options))
const isRconfig = url.includes('/rconfig')
const raw = isRconfig ? rconfigBody : JSON.stringify({ code: 0, msg: 'success', data: 'https://example.test/fake.mp3' })
const parsed = (() => { try { return JSON.parse(raw) } catch { return raw } })()
setTimeout(() => {
callback(null, { statusCode: 200, statusMessage: 'OK', headers: { 'content-type': 'application/json' }, raw: Buffer.from(raw), body: parsed }, parsed)
}, 5)
return () => {}
},
send(eventName, data) {
console.log('[lx.send]', eventName, JSON.stringify(data))
return Promise.resolve()
},
on(eventName, handler) {
console.log('[lx.on]', eventName, typeof handler, handler && handler.length)
handlers[eventName] = handler
return Promise.resolve()
},
utils: {
crypto: {
aesEncrypt(b, mode, key, iv) { const c = crypto.createCipheriv(mode, key, iv); return Buffer.concat([c.update(b), c.final()]) },
rsaEncrypt(b, key) { b = Buffer.concat([Buffer.alloc(128 - b.length), b]); return crypto.publicEncrypt({ key, padding: crypto.constants.RSA_NO_PADDING }, b) },
randomBytes: (n) => crypto.randomBytes(n),
md5,
},
buffer: { from: (...a) => Buffer.from(...a), bufToString: (b, f) => Buffer.from(b, 'binary').toString(f) },
zlib: { inflate: (b) => inflate(b), deflate: (d) => deflate(d) },
},
currentScriptInfo: { name: '独家音源', description: '', version: '5', author: 'w', homepage: '', rawScript: code },
version: '2.0.0',
env: 'desktop',
}
const ctx = {
console, setTimeout, clearTimeout, setInterval, clearInterval,
Promise, Error, TypeError, RangeError, SyntaxError, EvalError, URIError,
Array, Object, String, Number, Boolean, Date, Math, JSON, RegExp, BigInt, Symbol,
Function, parseInt, parseFloat, isNaN, isFinite,
encodeURIComponent, decodeURIComponent, escape, unescape,
Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, DataView, Map, Set, WeakMap, WeakSet,
TextEncoder, TextDecoder, URL, URLSearchParams,
Buffer,
atob: (s) => Buffer.from(s, 'base64').toString('binary'),
btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
SCRIPT_MD5: md5(fs.readFileSync(obfPath)),
LOGCACHE,
lx,
}
ctx.globalThis = ctx
ctx.window = ctx
ctx.self = ctx
ctx.global = ctx
process.on('uncaughtException', (e) => console.error('[uncaught]', e.stack?.split('\n').slice(0, 6).join('\n')))
process.on('unhandledRejection', (e) => console.error('[unhandledRejection]', e?.stack?.split('\n').slice(0, 6).join('\n') || e))
const context = vm.createContext(ctx)
console.error('[run]')
try {
vm.runInContext(code, context, { filename: 'lx-source-v5.js', timeout: 15000 })
console.error('[run ok]')
} catch (e) {
console.error('[vm error]', e.stack?.split('\n').slice(0, 8).join('\n'))
}
await new Promise((r) => setTimeout(r, 1500))
console.error('\n===== decoded strings (%d) =====', cacheLog.length)
console.log(cacheLog.map((s, i) => `${i}\t${JSON.stringify(s)}`).join('\n'))
console.error('\n===== summary =====')
console.error('events registered:', Object.keys(handlers))
console.error('requests made:', requestCalls.length)
// 触发 musicUrl 请求
const handler = handlers.request
if (handler) {
console.error('\n[trigger] musicUrl kw/128k')
const markBefore = cacheLog.length
try {
const p = handler.call(lx, { source: 'kw', action: 'musicUrl', info: { type: '128k', musicInfo: { songmid: '28423286', hash: 'fakehash123', id: 'kw_28423286' } } })
console.error('[trigger] handler returned:', p && p.constructor?.name)
if (p && typeof p.then === 'function') {
const res = await Promise.race([p.then((v) => ({ ok: true, v }), (e) => ({ ok: false, msg: e?.message, name: e?.name })), new Promise((r) => setTimeout(() => r({ timeout: true }), 8000))])
console.error('[trigger result]', JSON.stringify(res, null, 1))
}
} catch (e) {
console.error('[trigger sync error]', e?.message)
}
console.error('\n[strings used during trigger]')
console.error(cacheLog.slice(markBefore).map((s) => JSON.stringify(s)).join('\n'))
await new Promise((r) => setTimeout(r, 500))
console.error('\n[requests during trigger]')
for (const rc of requestCalls) console.log(rc.url, JSON.stringify(rc.options))
}
fs.writeFileSync(fileURLToPath(new URL('./trace-strings.json', import.meta.url)), JSON.stringify(cacheLog, null, 1))
process.exit(0)
+81
View File
@@ -0,0 +1,81 @@
// isolated-vm 加载器测试(离线,不真实联网)
// 运行:node tmp/test-lx-source-ivm.mjs
import { createIvmLxSource } from '../src/apis/lx-source-ivm.js'
let failed = 0
const check = (name, cond, detail = '') => {
console.log(`${cond ? '✅' : '❌'} ${name}${detail ? ` — ${detail}` : ''}`)
if (!cond) failed++
}
const makeMock = () => {
const seenUrls = []
const mock = (url) => {
seenUrls.push(url)
return {
promise: Promise.resolve({
statusCode: 200, statusMessage: 'OK',
headers: { 'content-type': 'application/json' },
body: { code: 0, msg: 'success', data: 'https://example.com/fake.mp3' },
}),
cancelHttp() {},
}
}
mock.seenUrls = seenUrls
return mock
}
// ===== 常规流程(默认白名单:脚本 apiUrl 同源放行)=====
const mock = makeMock()
const lx = await createIvmLxSource({ http: mock })
check('脚本 inited 上报', !!lx.inited, JSON.stringify(lx.inited?.sources ? Object.keys(lx.inited.sources) : null))
check('scriptInfo 解析', lx.scriptInfo.version === '5', JSON.stringify(lx.scriptInfo))
check('白名单来自脚本声明', lx.allowedOrigins.includes('https://88.lxmusic.xn--fiqs8s'), lx.allowedOrigins.join(','))
// hash 优先
const r1 = lx.apis.kw.getMusicUrl({ songmid: '28423286', hash: 'fakehash123' }, '320k')
const u1 = await r1.promise
check('musicUrl hash 优先', u1.url === 'https://example.com/fake.mp3', u1.url)
check('取流路径/hash', mock.seenUrls.some(u => u.includes('/v4s/url/kw/fakehash123/320k')), mock.seenUrls.at(-1)?.split('?')[0])
// songmid 兜底
const r2 = lx.apis.tx.getMusicUrl({ songmid: '003C9fBv2K4x8b' }, '128k')
const u2 = await r2.promise
check('musicUrl songmid 兜底', u2.url === 'https://example.com/fake.mp3', u2.url)
check('取流路径/songmid', mock.seenUrls.some(u => u.includes('/v4s/url/tx/003C9fBv2K4x8b/128k')))
// 签名参数齐全
const lastUrl = mock.seenUrls.at(-1)
check('签名参数齐全', ['e=', 'to=', 'fp=', 's2=', 'v=5', 'si=', 's3='].every(k => lastUrl.includes(k)))
// rconfig 心跳(脚本加载时自动发出,走白名单放行)
check('启动 rconfig 心跳放行', mock.seenUrls.some(u => u.includes('/rconfig?')))
// ===== HTTP 默认阻止:白名单置空,脚本请求全部拦截 =====
const mockBlocked = makeMock()
const lb = await createIvmLxSource({ http: mockBlocked, allowedOrigins: [], initTimeout: 8000, log: false })
let blockedBehavior = 'init-failed'
try {
const rb = lb.apis.kw.getMusicUrl({ songmid: '28423286' }, '128k')
await rb.promise
blockedBehavior = 'unexpected-success'
} catch (e) {
blockedBehavior = `rejected: ${e.message}`
}
check('白名单置空后请求被拦截', !mockBlocked.seenUrls.length, `mock 被调用 ${mockBlocked.seenUrls.length} 次; ${blockedBehavior}`)
await lb.destroy()
// ===== 失败路径:code != 0 → 脚本内部错误码映射后 reject =====
let errSeen = ''
const mockFail = () => ({ promise: Promise.resolve({ statusCode: 200, statusMessage: 'OK', headers: {}, body: { code: 1, msg: '签名过期' } }), cancelHttp() {} })
const lx2 = await createIvmLxSource({ http: mockFail, log: false })
try {
await lx2.apis.kg.getMusicUrl({ hash: 'abc' }, 'flac').promise
} catch (e) { errSeen = e.message }
check('code!=0 reject(脚本错误映射)', !!errSeen && errSeen !== 'undefined', errSeen)
await lx2.destroy()
await lx.destroy()
console.log(failed ? `\n${failed} 项失败` : '\n全部通过')
process.exit(failed ? 1 : 0)
+261
View File
@@ -0,0 +1,261 @@
[
"ps://88.lxmusi",
"c.xn--fiqs8s",
"ci",
"sumxl",
"par",
"es",
"}]\"tib42calf\",\"calf\",\"k023\",\"k821\"[:\"gm\",]\"tib42calf\",\"calf\",\"k023\",\"k821\"[:\"yw\",]\"tib42calf\",\"calf\",\"k023\",\"k821\"[:\"xt\",]\"tib42calf\",\"calf\",\"k023\",\"k821\"[:\"gk\",]\"tib42calf\",\"calf\",\"k023\",\"k8",
"21\"[:\"wk\"{",
"sye",
"k",
"hs",
"up",
"l",
"acol",
"x",
"SEMAN_",
"TNEVE",
"tseuq",
"e",
"r",
"n",
"o",
"d",
"sli",
"vn",
"noisre",
"v",
"GIFNOC_TPIRCS_REVR",
"ES",
"litu",
"otpy",
"rc",
"m",
"5",
"ofnItpircStn",
"erruc",
"rev",
"sion",
"undefin",
"ed",
"u",
"ndefined",
"noitc",
"nuf",
"ro",
"tareti",
"gaTgnirt",
"Sot",
"getPrototype",
"Of",
"pytotorp",
"prot",
"et",
"aerc",
"epytoto",
"rp",
"construct",
"or",
"ytreporPeni",
"f",
"",
"value",
"enumerable",
"configurable",
"writable",
"rotcur",
"tsnoc",
"emaNy",
"alp",
"sid",
"oitcnuFrotareneG",
"noitcnuFro",
"tareneG",
"xt",
"wor",
"h",
"nrute",
"rotar",
"eneG",
"gn",
"irtSot",
"w",
"fOepytoto",
"fOe",
"p",
"ytotorPtes",
"rototype",
"eta",
"erc",
"appl",
"prototy",
"pe",
"eate",
"protot",
"epy",
"_invok",
"a",
"in",
"_i",
"nvoke",
"lac",
"app",
"ly",
"tacn",
"oc",
"/rc",
"onfig",
"T",
"G",
"method",
"nos",
"j/noitacilppa",
"Content-Type",
"co",
"-cisum-",
"lx",
"conca",
"/",
"t",
"acnoc",
"User-Agent",
"headers",
"follow_max",
"length",
"on",
"toStrin",
"g",
"ush",
"ni",
"oj",
"toStri",
"ahc",
"rCodeAt",
"ot",
"gnir",
"0",
"taep",
"er",
"ecolons",
"time",
"lps",
"it",
"am",
"tch",
"^https?:\\/\\/[^/]+(.*)$",
"sedul",
"c",
"?",
"fingerprin",
"current",
"ScriptInfo",
"tpir",
"cSwar",
"toLowerCa",
"se",
"Nd7Sk/2Mj9s/",
"Vdk=",
"ilp",
"s",
"sup",
"jo",
"gnirtS",
"dap",
"Start",
"esaCreppUo",
"til",
"ps",
"ioj",
"​",
"tla",
"Sngis",
"|",
"tArah",
"charA",
"=",
"noloce",
"ot&",
"ti",
"&f",
"=p",
"=2s",
"&",
"&v",
"=is",
"si",
"gn3",
"done",
"valu",
"resolv",
"th",
"en",
"hcta",
"yd",
"ob",
"dat",
"tad",
"ause",
"Vnim",
"y",
"ilauQqh",
"hc",
"aErof",
"name",
"mus",
"type",
"ic",
"lrUci",
"sum",
"actions",
"qualitys",
"reque",
"st",
"i",
"detin",
"status",
"openDevTools",
"sources",
"no",
"itca",
"ec",
"ru",
"os",
"inf",
"lrUcis",
"um",
"fnIcisum",
"pyt",
"ppa",
"lo",
"cal",
"ha",
"sh",
"tac",
"/s4v/",
"noc",
"EG",
"application/jso",
"tacno",
"lx-music",
"-",
"source",
"songId",
"quality",
"sign3",
"sig",
"n3",
"ecr",
"3n",
"gnos",
"qu",
"ality",
"rever",
"=3",
"s&",
"3ngi",
"bo",
"cod",
"at",
"ad"
]
+10
View File
@@ -0,0 +1,10 @@
// 端到端验证:经 src/apis/lx-source.js 懒加载封装真实取流一次
import lx from '../src/apis/lx-source.js'
const r = lx.kw.getMusicUrl({ songmid: '28423286' }, '320k')
const res = await Promise.race([
r.promise,
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout 25s')), 25000)),
])
console.log('[结果]', JSON.stringify(res).slice(0, 160))
process.exit(0)