存档:TS 重构前基线(lx 聚合源 = isolated-vm 加载器,签名方案已移除)
This commit is contained in:
commit
f289df4cd7
124 files changed
+14110
No files matched your search
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user