'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 _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 freezeObject = (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) } freezeObject(config.value, freezedObj) } } } freezeObject(globalThis) console.log('Preload finished.') }