2023-11-05 15:14:42 +08:00

529 lines
18 KiB
JavaScript
Raw 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.

'use strict'
globalThis.lx_setup = (key, id, name, description, rawScript) => {
delete globalThis.setup
const _nativeCall = globalThis.__lx_native_call__
delete globalThis.__lx_native_call__
const set_timeout = globalThis.__lx_native_call__set_timeout
delete globalThis.__lx_native_call__set_timeout
const utils_str2b64 = globalThis.__lx_native_call__utils_str2b64
delete globalThis.__lx_native_call__utils_str2b64
const utils_b642buf = globalThis.__lx_native_call__utils_b642buf
delete globalThis.__lx_native_call__utils_b642buf
const utils_str2md5 = globalThis.__lx_native_call__utils_str2md5
delete globalThis.__lx_native_call__utils_str2md5
const utils_aes_encrypt = globalThis.__lx_native_call__utils_aes_encrypt
delete globalThis.__lx_native_call__utils_aes_encrypt
const utils_rsa_encrypt = globalThis.__lx_native_call__utils_rsa_encrypt
delete globalThis.__lx_native_call__utils_rsa_encrypt
const KEY_PREFIX = {
publicKeyStart: '-----BEGIN PUBLIC KEY-----',
publicKeyEnd: '-----END PUBLIC KEY-----',
privateKeyStart: '-----BEGIN PRIVATE KEY-----',
privateKeyEnd: '-----END PRIVATE KEY-----',
}
const RSA_PADDING = {
OAEPWithSHA1AndMGF1Padding: 'RSA/ECB/OAEPWithSHA1AndMGF1Padding',
NoPadding: 'RSA/ECB/NoPadding',
}
const AES_MODE = {
CBC_128_PKCS7Padding: 'AES/CBC/PKCS7Padding',
ECB_128_NoPadding: 'AES',
}
const nativeCall = (action, data) => {
data = JSON.stringify(data)
// console.log('nativeCall', action, data)
_nativeCall(key, action, data)
}
const callbacks = new Map()
let timeoutId = 0
const _setTimeout = (callback, timeout = 0, ...params) => {
if (typeof timeout !== 'number' || timeout < 0) throw new Error('timeout required number')
if (timeoutId > 90000000000) throw new Error('max timeout')
const id = timeoutId++
callbacks.set(id, {
callback,
params,
})
set_timeout(id, parseInt(timeout))
return id
}
const _clearTimeout = (id) => {
const tagret = callbacks.get(id)
if (!tagret) return
callbacks.delete(id)
}
const handleSetTimeout = (id) => {
const tagret = callbacks.get(id)
if (!tagret) return
callbacks.delete(id)
tagret.callback(...tagret.params)
}
// 将字节数组解码为字符串UTF-8
function bytesToString(bytes) {
let result = ''
let i = 0
while (i < bytes.length) {
const byte = bytes[i]
if (byte < 128) {
result += String.fromCharCode(byte)
i++
} else if (byte >= 192 && byte < 224) {
result += String.fromCharCode(((byte & 31) << 6) | (bytes[i + 1] & 63))
i += 2
} else {
result += String.fromCharCode(((byte & 15) << 12) | ((bytes[i + 1] & 63) << 6) | (bytes[i + 2] & 63))
i += 3
}
}
return result
}
// 将字符串编码为字节数组UTF-8
function stringToBytes(inputString) {
const bytes = []
for (let i = 0; i < inputString.length; i++) {
const charCode = inputString.charCodeAt(i)
if (charCode < 128) {
bytes.push(charCode)
} else if (charCode < 2048) {
bytes.push((charCode >> 6) | 192)
bytes.push((charCode & 63) | 128)
} else {
bytes.push((charCode >> 12) | 224)
bytes.push(((charCode >> 6) & 63) | 128)
bytes.push((charCode & 63) | 128)
}
}
return bytes
}
const NATIVE_EVENTS_NAMES = {
init: 'init',
showUpdateAlert: 'showUpdateAlert',
request: 'request',
cancelRequest: 'cancelRequest',
response: 'response',
// 'utils.crypto.aesEncrypt': 'utils.crypto.aesEncrypt',
// 'utils.crypto.rsaEncrypt': 'utils.crypto.rsaEncrypt',
// 'utils.crypto.randomBytes': 'utils.crypto.randomBytes',
// 'utils.crypto.md5': 'utils.crypto.md5',
// 'utils.buffer.from': 'utils.buffer.from',
// 'utils.buffer.bufToString': 'utils.buffer.bufToString',
// 'utils.zlib.inflate': 'utils.zlib.inflate',
// 'utils.zlib.deflate': 'utils.zlib.deflate',
}
const EVENT_NAMES = {
request: 'request',
inited: 'inited',
updateAlert: 'updateAlert',
}
const eventNames = Object.values(EVENT_NAMES)
const events = {
request: null,
}
const allSources = ['kw', 'kg', 'tx', 'wy', 'mg']
const supportQualitys = {
kw: ['128k', '320k', 'flac', 'flac24bit'],
kg: ['128k', '320k', 'flac', 'flac24bit'],
tx: ['128k', '320k', 'flac', 'flac24bit'],
wy: ['128k', '320k', 'flac', 'flac24bit'],
mg: ['128k', '320k', 'flac', 'flac24bit'],
}
const supportActions = {
kw: ['musicUrl'],
kg: ['musicUrl'],
tx: ['musicUrl'],
wy: ['musicUrl'],
mg: ['musicUrl'],
xm: ['musicUrl'],
}
const requestQueue = new Map()
let isInitedApi = false
let isShowedUpdateAlert = false
const sendNativeRequest = (url, options, callback) => {
const requestKey = Math.random().toString()
const requestInfo = {
aborted: false,
abort: () => {
nativeCall(NATIVE_EVENTS_NAMES.cancelRequest, requestKey)
},
}
requestQueue.set(requestKey, {
callback,
// timeout: setTimeout(() => {
// const req = requestQueue.get(requestKey)
// if (req) req.timeout = null
// nativeCall(NATIVE_EVENTS_NAMES.cancelRequest, requestKey)
// }, 30000),
requestInfo,
})
nativeCall(NATIVE_EVENTS_NAMES.request, { requestKey, url, options })
return requestInfo
}
const handleNativeResponse = ({ requestKey, error, response }) => {
const targetRequest = requestQueue.get(requestKey)
if (!targetRequest) return
requestQueue.delete(requestKey)
targetRequest.requestInfo.aborted = true
// if (targetRequest.timeout) clearTimeout(targetRequest.timeout)
if (error == null) targetRequest.callback(null, response)
else targetRequest.callback(new Error(error), null)
}
const handleRequest = ({ requestKey, data }) => {
// console.log(data)
if (!events.request) return nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: 'Request event is not defined' })
try {
events.request.call(globalThis.lx, { source: data.source, action: data.action, info: data.info }).then(response => {
let result
switch (data.action) {
case 'musicUrl':
result = {
source: data.source,
action: data.action,
data: {
type: data.info.type,
url: response,
},
}
break
}
nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: true, result })
}).catch(err => {
// console.log('handleRequest err', err)
nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: err.message })
})
} catch (err) {
// console.log('handleRequest call err', err)
nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: err.message })
}
}
const jsCall = (action, data) => {
// console.log('jsCall', action, data)
switch (action) {
case '__set_timeout__':
handleSetTimeout(data)
break
case 'request':
handleRequest(data)
break
case 'response':
handleNativeResponse(data)
break
case NATIVE_EVENTS_NAMES['utils.buffer.from']:
break
case NATIVE_EVENTS_NAMES['utils.buffer.bufToString']:
break
default:
break
}
return 'Unknown action: ' + action
}
Object.defineProperty(globalThis, '__lx_native__', {
enumerable: false,
configurable: false,
writable: false,
value: (_key, action, data) => {
if (key != _key) return 'Invalid key'
return jsCall(action, JSON.parse(data))
},
})
/**
*
* @param {*} info {
* openDevTools: false,
* status: true,
* message: 'xxx',
* sources: {
* kw: ['128k', '320k', 'flac', 'flac24bit'],
* kg: ['128k', '320k', 'flac', 'flac24bit'],
* tx: ['128k', '320k', 'flac', 'flac24bit'],
* wy: ['128k', '320k', 'flac', 'flac24bit'],
* mg: ['128k', '320k', 'flac', 'flac24bit'],
* }
* }
*/
const handleInit = (info) => {
if (!info) {
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: null, status: false, errorMessage: 'Init failed' })
// sendMessage(NATIVE_EVENTS_NAMES.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
if (!info.status) {
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: null, status: false, errorMessage: 'Init failed' })
// sendMessage(NATIVE_EVENTS_NAMES.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
const sourceInfo = {
sources: {},
}
try {
for (const source of allSources) {
const userSource = info.sources[source]
if (!userSource || userSource.type !== 'music') continue
const qualitys = supportQualitys[source]
const actions = supportActions[source]
sourceInfo.sources[source] = {
type: 'music',
actions: actions.filter(a => userSource.actions.includes(a)),
qualitys: qualitys.filter(q => userSource.qualitys.includes(q)),
}
}
} catch (error) {
// console.log(error)
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: null, status: false, errorMessage: error.message })
return
}
nativeCall(NATIVE_EVENTS_NAMES.init, { id, info: sourceInfo, status: true })
}
const handleShowUpdateAlert = (data, resolve, reject) => {
if (!data || typeof data != 'object') return reject(new Error('parameter format error.'))
if (!data.log || typeof data.log != 'string') return reject(new Error('log is required.'))
if (data.updateUrl && !/^https?:\/\/[^\s$.?#].[^\s]*$/.test(data.updateUrl) && data.updateUrl.length > 1024) delete data.updateUrl
if (data.log.length > 1024) data.log = data.log.substring(0, 1024) + '...'
nativeCall(NATIVE_EVENTS_NAMES.showUpdateAlert, { log: data.log, updateUrl: data.updateUrl, name })
resolve()
}
const dataToB64 = (data) => {
if (typeof data === 'string') return utils_str2b64(data)
else if (Array.isArray(data) || ArrayBuffer.isView(data)) return utils.buffer.bufToString(data, 'base64')
throw new Error('data type error: ' + typeof data + ' raw data: ' + data)
}
const utils = {
crypto: {
aesEncrypt(buffer, mode, key, iv) {
// console.log('aesEncrypt', buffer, mode, key, iv)
switch (mode) {
case 'aes-128-cbc':
return utils.buffer.from(utils_aes_encrypt(dataToB64(buffer), dataToB64(key), dataToB64(iv), AES_MODE.CBC_128_PKCS7Padding), 'base64')
case 'aes-128-ecb':
return utils.buffer.from(utils_aes_encrypt(dataToB64(buffer), dataToB64(key), '', AES_MODE.ECB_128_NoPadding), 'base64')
default:
throw new Error('Binary encoding is not supported for input strings')
}
},
rsaEncrypt(buffer, key) {
// console.log('rsaEncrypt', buffer, key)
if (typeof key !== 'string') throw new Error('Invalid RSA key')
key = key.replace(KEY_PREFIX.publicKeyStart, '')
.replace(KEY_PREFIX.publicKeyEnd, '')
return utils.buffer.from(utils_rsa_encrypt(dataToB64(buffer), key, RSA_PADDING.NoPadding), 'base64')
},
randomBytes(size) {
const byteArray = new Uint8Array(size)
for (let i = 0; i < size; i++) {
byteArray[i] = Math.floor(Math.random() * 256) // 随机生成一个字节的值0-255
}
return byteArray
},
md5(str) {
if (typeof str !== 'string') throw new Error('param required a string')
const md5 = utils_str2md5(str)
// console.log('md5', str, md5)
return md5
},
},
buffer: {
from(input, encoding) {
// console.log('buffer.from', input, encoding)
if (typeof input === 'string') {
switch (encoding) {
case 'binary':
throw new Error('Binary encoding is not supported for input strings')
case 'base64':
return new Uint8Array(JSON.parse(utils_b642buf(input)))
case 'hex':
return new Uint8Array(input.match(/.{1,2}/g).map(byte => parseInt(byte, 16)))
default:
return new Uint8Array(stringToBytes(input))
}
} else if (Array.isArray(input)) {
return new Uint8Array(input)
} else {
throw new Error('Unsupported input type: ' + input + ' encoding: ' + encoding)
}
},
bufToString(buf, format) {
// console.log('buffer.bufToString', buf, format)
if (Array.isArray(buf) || ArrayBuffer.isView(buf)) {
switch (format) {
case 'binary':
// return new TextDecoder('latin1').decode(new Uint8Array(buf))
return buf
case 'hex':
return new Uint8Array(buf).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')
case 'base64':
return utils_str2b64(bytesToString(Array.from(buf)))
case 'utf8':
case 'utf-8':
default:
return bytesToString(Array.from(buf))
}
} else {
throw new Error('Input is not a valid buffer: ' + buf + ' format: ' + format)
}
},
},
// zlib: {
// inflate(buf) {
// return new Promise((resolve, reject) => {
// zlib.inflate(buf, (err, data) => {
// if (err) reject(new Error(err.message))
// else resolve(data)
// })
// })
// },
// deflate(data) {
// return new Promise((resolve, reject) => {
// zlib.deflate(data, (err, buf) => {
// if (err) reject(new Error(err.message))
// else resolve(buf)
// })
// })
// },
// }),
}
globalThis.lx = {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData, binary }, callback) {
let options = { headers, binary: binary === true }
// let data
// if (body) {
// data = body
// } else if (form) {
// data = form
// // data.content_type = 'application/x-www-form-urlencoded'
// options.json = false
// } else if (formData) {
// data = formData
// // data.content_type = 'multipart/form-data'
// options.json = false
// }
if (timeout && typeof timeout == 'number') options.timeout = Math.min(options.timeout, 60_000)
let request = sendNativeRequest(url, { method, body, form, formData, ...options }, (err, resp) => {
callback(err, {
statusCode: resp.statusCode,
statusMessage: resp.statusMessage,
headers: resp.headers,
// bytes: resp.bytes,
// raw: resp.raw,
body: resp.body,
}, resp.body)
})
return () => {
if (!request.aborted) request.abort()
request = null
}
},
send(eventName, data) {
return new Promise((resolve, reject) => {
if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.inited:
if (isInitedApi) return reject(new Error('Script is inited'))
isInitedApi = true
handleInit(data)
resolve()
break
case EVENT_NAMES.updateAlert:
if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.'))
isShowedUpdateAlert = true
handleShowUpdateAlert(data, resolve, reject)
break
default:
reject(new Error('Unknown event name: ' + eventName))
}
})
},
on(eventName, handler) {
if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.request:
events.request = handler
break
default: return Promise.reject(new Error('The event is not supported: ' + eventName))
}
return Promise.resolve()
},
utils,
currentScriptInfo: {
name,
description,
},
version: '1.0.0',
env: 'mobile',
}
globalThis.setTimeout = _setTimeout
globalThis.clearTimeout = _clearTimeout
globalThis.window = globalThis
globalThis.document = {
getElementsByTagName(name) {
if (name == 'script') {
return [
Object.freeze({
innerText: rawScript,
}),
]
}
return null
},
}
const freezeObject = (obj) => {
if (typeof obj != 'object') return
Object.freeze(obj)
for (const subObj of Object.values(obj)) freezeObject(subObj)
}
freezeObject(globalThis.lx)
const _toString = Function.prototype.toString
// eslint-disable-next-line no-extend-native
Function.prototype.toString = function() {
return Object.getOwnPropertyDescriptors(this).name.configurable
? _toString.apply(this)
: `function ${this.name}() { [native code] }`
}
const excludes = [
Function.prototype.toString,
Function.prototype.toLocaleString,
Object.prototype.toString,
]
const freezeObjectProperty = (obj, freezedObj = new Set()) => {
if (obj == null) return
switch (typeof obj) {
case 'object':
case 'function':
if (freezedObj.has(obj)) return
// Object.freeze(obj)
freezedObj.add(obj)
for (const [name, { ...config }] of Object.entries(Object.getOwnPropertyDescriptors(obj))) {
if (!excludes.includes(config.value)) {
if (config.writable) config.writable = false
if (config.configurable) config.configurable = false
Object.defineProperty(obj, name, config)
}
freezeObjectProperty(config.value, freezedObj)
}
}
}
freezeObjectProperty(globalThis)
console.log('Preload finished.')
}