quickjs封装JS沙箱的场景是什么,怎么实现
Admin 2022-06-22 群英技术资�
在前文JavaScript 沙箱探索 中声明了沙箱的接口,并且给出了一些简单的执行任意第三� js 脚本的代码,但并未实现完整的 IJavaScriptShadowbox
,下面便讲一下如何基� quickjs
实现它�
quickjs
� js 的封装库是quickjs-emscripten,基本原理是� c 编译� wasm
然后运行在浏览器�nodejs
上,它提供了以下基础� api�
export interface LowLevelJavascriptVm<VmHandle> { global: VmHandle; undefined: VmHandle; typeof(handle: VmHandle): string; getNumber(handle: VmHandle): number; getString(handle: VmHandle): string; newNumber(value: number): VmHandle; newString(value: string): VmHandle; newObject(prototype?: VmHandle): VmHandle; newFunction( name: string, value: VmFunctionImplementation<VmHandle> ): VmHandle; getProp(handle: VmHandle, key: string | VmHandle): VmHandle; setProp(handle: VmHandle, key: string | VmHandle, value: VmHandle): void; defineProp( handle: VmHandle, key: string | VmHandle, descriptor: VmPropertyDescriptor<VmHandle> ): void; callFunction( func: VmHandle, thisVal: VmHandle, ...args: VmHandle[] ): VmCallResult<VmHandle>; evalCode(code: string): VmCallResult<VmHandle>; }
下面是一段官方的代码示例
import { getQuickJS } from "quickjs-emscripten"; async function main() { const QuickJS = await getQuickJS(); const vm = QuickJS.createVm(); const world = vm.newString("world"); vm.setProp(vm.global, "NAME", world); world.dispose(); const result = vm.evalCode(`"Hello " + NAME + "!"`); if (result.error) { console.log("Execution failed:", vm.dump(result.error)); result.error.dispose(); } else { console.log("Success:", vm.dump(result.value)); result.value.dispose(); } vm.dispose(); } main();
可以看到,创� vm 中的变量后还必须留意调用 dispose
,有点像是后端连接数据库时必须注意关闭连接,而这其实是比较繁琐的,尤其是在复杂的情况下。简而言之,它的 api 太过于底层了。在 github issue
中有人创建了 quickjs-emscripten-sync
,这给了吾辈很多灵感,所以吾辈基于quickjs-emscripten 封装了一些工具函数,辅助而非替代它�
主要目的有两个:
dispose
vm
值的方法主要思路是自动收集所有需要调� dispose
的值,使用高阶函数� callback
执行完之后自动调用�
这里还需要注意避免不需要的多层嵌套代理,主要是考虑到下面更多的底层 api 基于它实现,而它们之间可能存在嵌套调用�
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten"; const QuickJSVmScopeSymbol = Symbol("QuickJSVmScope"); /** * � QuickJSVm 添加局部作用域,局部作用域的所有方法调用不再需要手动释放内� * @param vm * @param handle */ export function withScope<F extends (vm: QuickJSVm) => any>( vm: QuickJSVm, handle: F ): { value: ReturnType<F>; dispose(): void; } { let disposes: (() => void)[] = []; function wrap(handle: QuickJSHandle) { disposes.push(() => handle.alive && handle.dispose()); return handle; } //避免多层代理 const isProxy = !!Reflect.get(vm, QuickJSVmScopeSymbol); function dispose() { if (isProxy) { Reflect.get(vm, QuickJSVmScopeSymbol)(); return; } disposes.forEach((dispose) => dispose()); //手动释放闭包变量的内� disposes.length = 0; } const value = handle( isProxy ? vm : new Proxy(vm, { get( target: QuickJSVm, p: keyof QuickJSVm | typeof QuickJSVmScopeSymbol ): any { if (p === QuickJSVmScopeSymbol) { return dispose; } //锁定所有方法的 this 值为 QuickJSVm 对象而非 Proxy 对象 const res = Reflect.get(target, p, target); if ( p.startsWith("new") || ["getProp", "unwrapResult"].includes(p) ) { return (...args: any[]): QuickJSHandle => { return wrap(Reflect.apply(res, target, args)); }; } if (["evalCode", "callFunction"].includes(p)) { return (...args: any[]) => { const res = (target[p] as any)(...args); disposes.push(() => { const handle = res.error ?? res.value; handle.alive && handle.dispose(); }); return res; }; } if (typeof res === "function") { return (...args: any[]) => { return Reflect.apply(res, target, args); }; } return res; }, }) ); return { value, dispose }; }
使用
withScope(vm, (vm) => { const _hello = vm.newFunction("hello", () => {}); const _object = vm.newObject(); vm.setProp(_object, "hello", _hello); vm.setProp(_object, "name", vm.newString("liuli")); expect(vm.dump(vm.getProp(_object, "hello"))).not.toBeNull(); vm.setProp(vm.global, "VM_GLOBAL", _object); }).dispose();
甚至支持嵌套调用,而且仅需要在最外层统一调用 dispose
即可
withScope(vm, (vm) => withScope(vm, (vm) => { console.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1")))); }) ).dispose();
主要思路是判断创� vm
变量的类型,自动调用相应的函数,然后返回创建的变量�
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten"; import { withScope } from "./withScope"; type MarshalValue = { value: QuickJSHandle; dispose: () => void }; /** * 简化使� QuickJSVm 创建复杂对象的操� * @param vm */ export function marshal(vm: QuickJSVm) { function marshal(value: (...args: any[]) => any, name: string): MarshalValue; function marshal(value: any): MarshalValue; function marshal(value: any, name?: string): MarshalValue { return withScope(vm, (vm) => { function _f(value: any, name?: string): QuickJSHandle { if (typeof value === "string") { return vm.newString(value); } if (typeof value === "number") { return vm.newNumber(value); } if (typeof value === "boolean") { return vm.unwrapResult(vm.evalCode(`${value}`)); } if (value === undefined) { return vm.undefined; } if (value === null) { return vm.null; } if (typeof value === "bigint") { return vm.unwrapResult(vm.evalCode(`BigInt(${value})`)); } if (typeof value === "function") { return vm.newFunction(name!, value); } if (typeof value === "object") { if (Array.isArray(value)) { const _array = vm.newArray(); value.forEach((v) => { if (typeof v === "function") { throw new Error("数组中禁止包含函数,因为无法指定名字"); } vm.callFunction(vm.getProp(_array, "push"), _array, _f(v)); }); return _array; } if (value instanceof Map) { const _map = vm.unwrapResult(vm.evalCode("new Map()")); value.forEach((v, k) => { vm.unwrapResult( vm.callFunction(vm.getProp(_map, "set"), _map, _f(k), _f(v, k)) ); }); return _map; } const _object = vm.newObject(); Object.entries(value).forEach(([k, v]) => { vm.setProp(_object, k, _f(v, k)); }); return _object; } throw new Error("不支持的类型"); } return _f(value, name); }); } return marshal; }
使用
const mockHello = jest.fn(); const now = new Date(); const { value, dispose } = marshal(vm)({ name: "liuli", age: 1, sex: false, hobby: [1, 2, 3], account: { username: "li", }, hello: mockHello, map: new Map().set(1, "a"), date: now, }); vm.setProp(vm.global, "vm_global", value); dispose(); function evalCode(code: string) { return vm.unwrapResult(vm.evalCode(code)).consume(vm.dump.bind(vm)); } expect(evalCode("vm_global.name")).toBe("liuli"); expect(evalCode("vm_global.age")).toBe(1); expect(evalCode("vm_global.sex")).toBe(false); expect(evalCode("vm_global.hobby")).toEqual([1, 2, 3]); expect(new Date(evalCode("vm_global.date"))).toEqual(now); expect(evalCode("vm_global.account.username")).toEqual("li"); evalCode("vm_global.hello()"); expect(mockHello.mock.calls.length).toBe(1); expect(evalCode("vm_global.map.size")).toBe(1); expect(evalCode("vm_global.map.get(1)")).toBe("a");
目前支持的类型与 JavaScript 结构化克隆算� 对比,后者在很多地方�iframe/web worker/worker_threads
)均有使�
对象类型 | quickjs | 结构化克� | 注意 |
---|---|---|---|
所有的原始类型 | symbols 除外 | ||
Function | |||
Array | |||
Object | 仅包括普通对象(如对象字面量� | ||
Map | |||
Set | |||
Date | |||
Error | |||
Boolean | 对象 | ||
String | 对象 | ||
RegExp | lastIndex 字段不会被保留� | ||
Blob | |||
File | |||
FileList | |||
ArrayBuffer | |||
ArrayBufferView | 这基本上意味着所有的类型化数� | ||
ImageData |
以上不支持的非常见类型并� quickjs 不支持,仅仅� marshal 暂未支持�
由于 console/setTimeout/setInterval
均不� js 语言级别� api(但是浏览器、nodejs 均实现了),所以吾辈必须手动实现并注入它们�
基本思路�� vm 注入全局 console 对象,将参数 dump 之后转发到真正的 console api
import { QuickJSVm } from "quickjs-emscripten"; import { marshal } from "../util/marshal"; export interface IVmConsole { log(...args: any[]): void; info(...args: any[]): void; warn(...args: any[]): void; error(...args: any[]): void; } /** * 定义 vm 中的 console api * @param vm * @param logger */ export function defineConsole(vm: QuickJSVm, logger: IVmConsole) { const fields = ["log", "info", "warn", "error"] as const; const dump = vm.dump.bind(vm); const { value, dispose } = marshal(vm)( fields.reduce((res, k) => { res[k] = (...args: any[]) => { logger[k](...args.map(dump)); }; return res; }, {} as Record<string, Function>) ); vm.setProp(vm.global, "console", value); dispose(); } export class BasicVmConsole implements IVmConsole { error(...args: any[]): void { console.error(...args); } info(...args: any[]): void { console.info(...args); } log(...args: any[]): void { console.log(...args); } warn(...args: any[]): void { console.warn(...args); } }
使用
defineConsole(vm, new BasicVmConsole());
基本思路�
基于 quickjs 实现 setTimeout � clearTimeout
� vm 注入全局 setTimeout/clearTimeout
函数
setTimeout
callbackFunc
注册� vm 全局变量 setTimeout
clearTimeoutId => timeoutId
写到 map,返回一� clearTimeoutId
clearTimeout: 根据 clearTimeoutId
在系统层调用真实� clearTimeout
不直接返� setTimeout 返回值的原因在于� nodejs 中返回值是一个对象而非一个数字,所以需要使� map 兼容
import { QuickJSVm } from "quickjs-emscripten"; import { withScope } from "../util/withScope"; import { VmSetInterval } from "./defineSetInterval"; import { deleteKey } from "../util/deleteKey"; import { CallbackIdGenerator } from "@webos/ipc-main"; /** * 注入 setTimeout 方法 * 需要在注入后调� {@link defineEventLoop} � vm 的事件循环跑起来 * @param vm */ export function defineSetTimeout(vm: QuickJSVm): VmSetInterval { const callbackMap = new Map<string, any>(); function clear(id: string) { withScope(vm, (vm) => { deleteKey( vm, vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)), id ); }).dispose(); clearInterval(callbackMap.get(id)); callbackMap.delete(id); } withScope(vm, (vm) => { const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL"); if (vm.typeof(vmGlobal) === "undefined") { throw new Error("VM_GLOBAL 不存在,需要先执行 defineVmGlobal"); } vm.setProp(vmGlobal, "setTimeoutCallback", vm.newObject()); vm.setProp( vm.global, "setTimeout", vm.newFunction("setTimeout", (callback, ms) => { const id = CallbackIdGenerator.generate(); //此处已经是异步了,必须再包一� withScope(vm, (vm) => { const callbacks = vm.unwrapResult( vm.evalCode("VM_GLOBAL.setTimeoutCallback") ); vm.setProp(callbacks, id, callback); //此处还是异步的,必须再包一� const timeout = setTimeout( () => withScope(vm, (vm) => { const callbacks = vm.unwrapResult( vm.evalCode(`VM_GLOBAL.setTimeoutCallback`) ); const callback = vm.getProp(callbacks, id); vm.callFunction(callback, vm.null); callbackMap.delete(id); }).dispose(), vm.dump(ms) ); callbackMap.set(id, timeout); }).dispose(); return vm.newString(id); }) ); vm.setProp( vm.global, "clearTimeout", vm.newFunction("clearTimeout", (id) => clear(vm.dump(id))) ); }).dispose(); return { callbackMap, clear() { [...callbackMap.keys()].forEach(clear); }, }; }
使用
const vmSetTimeout = defineSetTimeout(vm); withScope(vm, (vm) => { vm.evalCode(` const begin = Date.now() setInterval(() => { console.log(Date.now() - begin) }, 100) `); }).dispose(); vmSetTimeout.clear();
基本上,与实� setTimeout
流程差不�
import { QuickJSVm } from "quickjs-emscripten"; import { withScope } from "../util/withScope"; import { deleteKey } from "../util/deleteKey"; import { CallbackIdGenerator } from "@webos/ipc-main"; export interface VmSetInterval { callbackMap: Map<string, any>; clear(): void; } /** * 注入 setInterval 方法 * 需要在注入后调� {@link defineEventLoop} � vm 的事件循环跑起来 * @param vm */ export function defineSetInterval(vm: QuickJSVm): VmSetInterval { const callbackMap = new Map<string, any>(); function clear(id: string) { withScope(vm, (vm) => { deleteKey( vm, vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)), id ); }).dispose(); clearInterval(callbackMap.get(id)); callbackMap.delete(id); } withScope(vm, (vm) => { const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL"); if (vm.typeof(vmGlobal) === "undefined") { throw new Error("VM_GLOBAL 不存在,需要先执行 defineVmGlobal"); } vm.setProp(vmGlobal, "setIntervalCallback", vm.newObject()); vm.setProp( vm.global, "setInterval", vm.newFunction("setInterval", (callback, ms) => { const id = CallbackIdGenerator.generate(); //此处已经是异步了,必须再包一� withScope(vm, (vm) => { const callbacks = vm.unwrapResult( vm.evalCode("VM_GLOBAL.setIntervalCallback") ); vm.setProp(callbacks, id, callback); const interval = setInterval(() => { withScope(vm, (vm) => { vm.callFunction( vm.unwrapResult( vm.evalCode(`VM_GLOBAL.setIntervalCallback['${id}']`) ), vm.null ); }).dispose(); }, vm.dump(ms)); callbackMap.set(id, interval); }).dispose(); return vm.newString(id); }) ); vm.setProp( vm.global, "clearInterval", vm.newFunction("clearInterval", (id) => clear(vm.dump(id))) ); }).dispose(); return { callbackMap, clear() { [...callbackMap.keys()].forEach(clear); }, }; }
但有一点麻烦的是,quickjs-emscripten
不会自动执行事件循环,即 Promise
� resolve
之后不会自动执行下一步。官方提供了 executePendingJobs
方法让我们手动执行事件循环,如下所�
const { log } = defineMockConsole(vm); withScope(vm, (vm) => { vm.evalCode(`Promise.resolve().then(()=>console.log(1))`); }).dispose(); expect(log.mock.calls.length).toBe(0); vm.executePendingJobs(); expect(log.mock.calls.length).toBe(1);
所以我们实现可以使用一个自动调� executePendingJobs
的函�
import { QuickJSVm } from "quickjs-emscripten"; export interface VmEventLoop { clear(): void; } /** * 定义 vm 中的事件循环机制,尝试循环执行等待的异步操作 * @param vm */ export function defineEventLoop(vm: QuickJSVm) { const interval = setInterval(() => { vm.executePendingJobs(); }, 100); return { clear() { clearInterval(interval); }, }; }
现在只要调用 defineEventLoop
即会循环执行 executePendingJobs
函数�
const { log } = defineMockConsole(vm); const eventLoop = defineEventLoop(vm); try { withScope(vm, (vm) => { vm.evalCode(`Promise.resolve().then(()=>console.log(1))`); }).dispose(); expect(log.mock.calls.length).toBe(0); await wait(100); expect(log.mock.calls.length).toBe(1); } finally { eventLoop.clear(); }
现在,我们沙箱还欠缺的就是通信机制了,下面我们便实现一� EventEmiiter
�
核心是让系统层和沙箱都实� EventEmitter
�quickjs
允许我们向沙箱中注入方法,所以我们可以注入一� Map � emitMain
函数。让沙箱既能够向 Map 中注册事件以供系统层调用,也能通过 emitMain
向系统层发送事件�
沙箱与系统之间的通信�
import { QuickJSHandle, QuickJSVm } from "quickjs-emscripten"; import { marshal } from "../util/marshal"; import { withScope } from "../util/withScope"; import { IEventEmitter } from "@webos/ipc-main"; export type VmMessageChannel = IEventEmitter & { listenerMap: Map<string, ((msg: any) => void)[]>; }; /** * 定义消息通信 * @param vm */ export function defineMessageChannel(vm: QuickJSVm): VmMessageChannel { const res = withScope(vm, (vm) => { const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL"); if (vm.typeof(vmGlobal) === "undefined") { throw new Error("VM_GLOBAL 不存在,需要先执行 defineVmGlobal"); } const listenerMap = new Map<string, ((msg: string) => void)[]>(); const messagePort = marshal(vm)({ //region vm 进程回调函数定义 listenerMap: new Map(), //� vm 进程用的 emitMain(channel: QuickJSHandle, msg: QuickJSHandle) { const key = vm.dump(channel); const value = vm.dump(msg); if (!listenerMap.has(key)) { console.log("主进程没有监� api: ", key, value); return; } listenerMap.get(key)!.forEach((fn) => { try { fn(value); } catch (e) { console.error("执行回调函数发生错误: ", e); } }); }, //endregion }); vm.setProp(vmGlobal, "MessagePort", messagePort.value); //给主进程用的 function emitVM(channel: string, msg: string) { withScope(vm, (vm) => { const _map = vm.unwrapResult( vm.evalCode("VM_GLOBAL.MessagePort.listenerMap") ); const _get = vm.getProp(_map, "get"); const _array = vm.unwrapResult( vm.callFunction(_get, _map, vm.newString(channel)) ); if (!vm.dump(_array)) { return; } for ( let i = 0, length = vm.dump(vm.getProp(_array, "length")); i < length; i++ ) { vm.callFunction( vm.getProp(_array, vm.newNumber(i)), vm.null, marshal(vm)(msg).value ); } }).dispose(); } return { emit: emitVM, offByChannel(channel: string): void { listenerMap.delete(channel); }, on(channel: string, handle: (data: any) => void): void { if (!listenerMap.has(channel)) { listenerMap.set(channel, []); } listenerMap.get(channel)!.push(handle); }, listenerMap, } as VmMessageChannel; }); res.dispose(); return res.value; }
可以看到,我们除了实现了 IEventEmitter,还额外添加了字� listenerMap,这主要是希望向上层暴露更多细节,便于在需要的时候(例如清理全部注册的事件)可以直接实现�
使用
defineVmGlobal(vm); const messageChannel = defineMessageChannel(vm); const mockFn = jest.fn(); messageChannel.on("hello", mockFn); withScope(vm, (vm) => { vm.evalCode(` class QuickJSEventEmitter { emit(channel, data) { VM_GLOBAL.MessagePort.emitMain(channel, data); } on(channel, handle) { if (!VM_GLOBAL.MessagePort.listenerMap.has(channel)) { VM_GLOBAL.MessagePort.listenerMap.set(channel, []); } VM_GLOBAL.MessagePort.listenerMap.get(channel).push(handle); } offByChannel(channel) { VM_GLOBAL.MessagePort.listenerMap.delete(channel); } } const em = new QuickJSEventEmitter() em.emit('hello', 'liuli') `); }).dispose(); expect(mockFn.mock.calls[0][0]).toBe("liuli"); messageChannel.listenerMap.clear();
最终,我们以上实现的功能集合起来,便实现了 IJavaScriptShadowbox
import { IJavaScriptShadowbox } from "./IJavaScriptShadowbox"; import { getQuickJS, QuickJS, QuickJSVm } from "quickjs-emscripten"; import { BasicVmConsole, defineConsole, defineEventLoop, defineMessageChannel, defineSetInterval, defineSetTimeout, defineVmGlobal, VmEventLoop, VmMessageChannel, VmSetInterval, withScope, } from "@webos/quickjs-emscripten-utils"; export class QuickJSShadowbox implements IJavaScriptShadowbox { private vmMessageChannel: VmMessageChannel; private vmEventLoop: VmEventLoop; private vmSetInterval: VmSetInterval; private vmSetTimeout: VmSetInterval; private constructor(readonly vm: QuickJSVm) { defineConsole(vm, new BasicVmConsole()); defineVmGlobal(vm); this.vmSetTimeout = defineSetTimeout(vm); this.vmSetInterval = defineSetInterval(vm); this.vmEventLoop = defineEventLoop(vm); this.vmMessageChannel = defineMessageChannel(vm); } destroy(): void { this.vmMessageChannel.listenerMap.clear(); this.vmEventLoop.clear(); this.vmSetInterval.clear(); this.vmSetTimeout.clear(); this.vm.dispose(); } eval(code: string): void { withScope(this.vm, (vm) => { vm.unwrapResult(vm.evalCode(code)); }).dispose(); } emit(channel: string, data?: any): void { this.vmMessageChannel.emit(channel, data); } on(channel: string, handle: (data: any) => void): void { this.vmMessageChannel.on(channel, handle); } offByChannel(channel: string) { this.vmMessageChannel.offByChannel(channel); } private static quickJS: QuickJS; static async create() { if (!QuickJSShadowbox.quickJS) { QuickJSShadowbox.quickJS = await getQuickJS(); } return new QuickJSShadowbox(QuickJSShadowbox.quickJS.createVm()); } static destroy() { QuickJSShadowbox.quickJS = null as any; } }
在系统层使用
const shadowbox = await QuickJSShadowbox.create(); const mockConsole = defineMockConsole(shadowbox.vm); shadowbox.eval(code); shadowbox.emit(AppChannelEnum.Open); expect(mockConsole.log.mock.calls[0][0]).toBe("open"); shadowbox.emit(WindowChannelEnum.AllClose); expect(mockConsole.log.mock.calls[1][0]).toBe("all close"); shadowbox.destroy();
在沙箱使�
const eventEmitter = new QuickJSEventEmitter(); eventEmitter.on(AppChannelEnum.Open, async () => { console.log("open"); }); eventEmitter.on(WindowChannelEnum.AllClose, async () => { console.log("all close"); });
下面是目前实现的一些限制,也是以后可以继续改进的点
console 仅支持常见的 log/info/warn/error 方法
setTimeout/setInterval 事件循环时间没有保证,目前大约在 100ms 调用一�
无法使用 chrome devtool 调试,也不会处理 sourcemap(figma 至今的开发体验仍然如此,后面可能添加开关支持在 web worker 中调试)
vm 中出现错误不会将错误抛出来并打印在控制台
各个 api 调用的顺序与清理顺序必须手动保证是相反的,例� vm 创建必须� defineSetTimeout 之前,� defineSetTimeout 的清理函数调用必须在 vm.dispose 之前
不能� messageChannel.on 回调中同步调� vm.dispose,因为是同步调用�
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:[email protected]进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容�
猜你喜欢
很多朋友向小编反映一个问题关于React onClick 传递参数的问题,当点击删除按钮需要执行删除操作,针对这个问题该如何处理呢?下面小编给大家带来了React onClick 传递参数的问题,感兴趣的朋友一起看看吧
对于新手来说,非负整数n的阶乘是比较难理解的一个算法,对此,这篇文章给大家分享的是有关js如何实现分解数字的内容、下面会介绍使用递归分解一个数字、使用WHILE循环分解一个数字和使用FOR循环分解数字�
本文主要介绍了react hooks组件间的传值方�(使用ts),文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一�
这篇文章主要为大家介绍了Promise静态四兄弟实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加�
这篇文章主要介绍了javaScript 事件冒泡,事件捕获和事件委托的相关资料,需要的朋友可以参考下,希望能够给你带来帮�
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所�
增值电信经营许可证 : B1.B2-20140078