Quickjs kapselt JavaScript-Sandbox-Details

Quickjs kapselt JavaScript-Sandbox-Details

1. Szenario

In der vorherigen JavaScript-Sandbox-Erkundung wurde die Sandbox-Schnittstelle deklariert und einige einfache Codes zum Ausführen von JS-Skripten von Drittanbietern bereitgestellt, die vollständige IJavaScriptShadowbox wurde jedoch nicht implementiert. Im Folgenden wird erläutert, wie sie basierend auf quickjs implementiert wird.

Die Kapselungsbibliothek von quickjs in js ist quickjs-emscripten. Das Grundprinzip besteht darin, C in wasm zu kompilieren und es dann im Browser und nodejs auszuführen. Es bietet die folgenden grundlegenden APIs.

Exportschnittstelle LowLevelJavascriptVm<VmHandle> {
  global: VmHandle;
  undefiniert: VmHandle;
  Typ von (Handle: VmHandle): Zeichenfolge;
  getNumber(handle: VmHandle): Nummer;
  getString(handle: VmHandle): Zeichenfolge;
  neueNummer(Wert: Nummer): VmHandle;
  newString(Wert: Zeichenfolge): VmHandle;
  neuesObjekt(Prototyp?: VmHandle): VmHandle;
  neueFunktion(
    Name: Zeichenfolge,
    Wert: VmFunctionImplementation<VmHandle>
  ): VmHandle;
  getProp(handle: VmHandle, Schlüssel: Zeichenfolge | VmHandle): VmHandle;
  setProp(Handle: VmHandle, Schlüssel: Zeichenfolge | VmHandle, Wert: VmHandle): void;
  definierenProp(
    Griff: VmHandle,
    Schlüssel: Zeichenfolge | VmHandle,
    Deskriptor: VmPropertyDescriptor<VmHandle>
  ): Leere;
  Aufruffunktion(
    Funktion: VmHandle,
    thisVal: VmHandle,
    ...Argumente: VmHandle[]
  ): VmCallResult<VmHandle>;
  evalCode(Code: Zeichenfolge): VmCallResult<VmHandle>;
}

Das Folgende ist ein offizielles Codebeispiel

importiere { getQuickJS } von "quickjs-emscripten";

asynchrone Funktion main() {
  const QuickJS = warte auf getQuickJS();
  const vm = QuickJS.createVm();

  const Welt = vm.newString("Welt");
  vm.setProp(vm.global, "NAME", Welt);
  welt.entsorgen();

  const result = vm.evalCode(`"Hallo " + NAME + "!"`);
  if (Ergebnis.Fehler) {
    console.log("Ausführung fehlgeschlagen:", vm.dump(result.error));
    Ergebnis.Fehler.Entsorgen();
  } anders {
    console.log("Erfolg:", vm.dump(Ergebnis.Wert));
    Ergebnis.Wert.Entsorgen();
  }

  vm.dispose();
}

hauptsächlich();

Wie Sie sehen, müssen Sie nach dem Erstellen der Variablen in der VM daran denken, dispose aufzurufen. Dies ist in etwa so, als würde man die Verbindung schließen, wenn das Backend eine Verbindung zur Datenbank herstellt. Tatsächlich ist dies ziemlich umständlich, insbesondere in komplexen Situationen. Kurz gesagt: Die API ist zu niedrigstufig. Jemand hat im github issue quickjs-emscripten-sync erstellt, was uns viel Inspiration gegeben hat. Daher haben wir einige auf quickjs-emscripten basierende Toolfunktionen gekapselt, um es zu unterstützen, nicht es zu ersetzen.

2. Vereinfachen Sie die zugrunde liegende API

Es gibt zwei Hauptzwecke:

  • Automatisch dispose dispose
  • Bieten Sie eine bessere Möglichkeit zum Erstellen von vm Werten

2.1 Dispose automatisch aufrufen

Die Grundidee besteht darin, alle Werte, die dispose werden müssen, automatisch zu erfassen und sie nach der Ausführung callback mithilfe einer Funktion höherer Ordnung automatisch aufzurufen.

Darüber hinaus müssen unnötige mehrschichtige verschachtelte Proxys vermieden werden, vor allem, weil darauf basierend mehrere zugrunde liegende APIs implementiert werden und es zwischen ihnen zu verschachtelten Aufrufen kommen kann.

importiere { QuickJSHandle, QuickJSVm } von "quickjs-emscripten";

const QuickJSVmScopeSymbol = Symbol("QuickJSVmScope");

/**
 * Fügen Sie QuickJSVm einen lokalen Bereich hinzu. Alle Methodenaufrufe im lokalen Bereich müssen den Speicher nicht mehr manuell freigeben. * @param vm
 * @param-Handle
 */
Exportfunktion mit Scope<F extends (vm: QuickJSVm) => any>(
  vm: QuickJSVm,
  Griff: F
): {
  Wert: ReturnType<F>;
  entsorgen(): ungültig;
} {
  lass entsorgt: (() => void)[] = [];

  Funktion Wrap(Handle: QuickJSHandle) {
    entsorgt.push(() => handle.alive && handle.dispose());
    Rückholgriff;
  }

  //Mehrschichtigen Proxy vermeiden const isProxy = !!Reflect.get(vm, QuickJSVmScopeSymbol);
  Funktion entsorgen() {
    wenn (istProxy) {
      Reflect.get(vm, QuickJSVmScopeSymbol)();
      zurückkehren;
    }
    entsorgt.forEach((entsorgen) => entsorgen());
    //Speicher der Closure-Variable manuell freigeben disposes.length = 0;
  }
  konstanter Wert = Griff(
    istProxy
      ? vm
      : neuer Proxy(vm, {
          erhalten(
            Ziel: QuickJSVm,
            p: Schlüssel von QuickJSVm | Typ von QuickJSVmScopeSymbol
          ): beliebig {
            wenn (p === QuickJSVmScopeSymbol) {
              zurückgeben, entsorgen;
            }
            //Sperren Sie den this-Wert aller Methoden auf das QuickJSVm-Objekt statt auf das Proxy-Objekt const res = Reflect.get(target, p, target);
            Wenn (
              p.startsWith("neu") ||
              ["getProp", "unwrapResult"].includes(p)
            ) {
              Rückgabewert (...args: beliebig[]): QuickJSHandle => {
                Rückgabewert für Wrap(Reflect.apply(res, Ziel, Argumente));
              };
            }
            wenn (["evalCode", "callFunction"].includes(p)) {
              return (...args: beliebig[]) => {
                const res = (Ziel[p] als beliebiges)(...args);
                entsorgt.push(() => {
                  const handle = res.Fehler ?? res.Wert;
                  handle.alive und handle.entsorgen();
                });
                Rückgabewert;
              };
            }
            wenn (Typ von res === "Funktion") {
              return (...args: beliebig[]) => {
                gibt Reflect.apply(res, Ziel, Argumente) zurück;
              };
            }
            Rückgabewert;
          },
        })
  );

  return { Wert, entsorgen };
}

verwenden

mitScope(vm, (vm) => {
  const _hello = vm.newFunction("hallo", () => {});
  const _object = vm.newObject();
  vm.setProp(_object, "hallo", _hallo);
  vm.setProp(_object, "name", vm.newString("liuli"));
  erwarten(vm.dump(vm.getProp(_object, "hallo"))).nicht.toBeNull();
  vm.setProp(vm.global, "VM_GLOBAL", _object);
}).entsorgen();

Es unterstützt sogar verschachtelte Aufrufe und muss dispose nur einheitlich auf der äußersten Ebene aufrufen.

mit Umfang(vm, (vm) =>
  mitScope(vm, (vm) => {
    Konsole.log(vm.dump(vm.unwrapResult(vm.evalCode("1+1"))));
  })
).entsorgen();

2.2 Bieten Sie eine bessere Möglichkeit zum Erstellen von VM-Werten

Die Hauptidee besteht darin, den Typ der erstellten vm Variable zu bestimmen, automatisch die entsprechende Funktion aufzurufen und dann die erstellte Variable zurückzugeben.

importiere { QuickJSHandle, QuickJSVm } von "quickjs-emscripten";
importiere { withScope } von "./withScope";

Typ MarshalValue = {Wert: QuickJSHandle; entsorgen: () => void };

/**
 * Vereinfachen Sie die Erstellung komplexer Objekte mit QuickJSVm * @param vm
 */
Exportfunktion Marshal (vm: QuickJSVm) {
  Funktion Marshal (Wert: (...Argumente: beliebig[]) => beliebig, Name: Zeichenfolge): MarshalValue;
  Funktion Marshal (Wert: beliebig): MarshalValue;
  Funktion Marshal (Wert: beliebig, Name?: Zeichenfolge): MarshalValue {
    returniere mit Umfang(vm, (vm) => {
      Funktion _f(Wert: beliebig, Name?: Zeichenfolge): QuickJSHandle {
        wenn (Typ des Wertes === "Zeichenfolge") {
          gibt vm.newString(Wert) zurück;
        }
        wenn (Typ des Wertes === "Zahl") {
          return vm.neueNummer(Wert);
        }
        wenn (Typ des Wertes === "Boolesch") {
          Geben Sie vm.unwrapResult(vm.evalCode(`${value}`)); zurück.
        }
        if (Wert === undefiniert) {
          gibt vm.undefined zurück;
        }
        wenn (Wert === null) {
          gibt vm.null zurück;
        }
        wenn (Typ des Wertes === "bigint") {
          return vm.unwrapResult(vm.evalCode(`BigInt(${value})`));
        }
        wenn (Typ des Wertes === "Funktion") {
          returniere vm.newFunction(Name!, Wert);
        }
        wenn (Typ des Wertes === "Objekt") {
          wenn (Array.isArray(Wert)) {
            const _array = vm.newArray();
            Wert.fürJeden((v) => {
              wenn (Typ von v === "Funktion") {
                throw new Error("Arrays dürfen keine Funktionen enthalten, da keine Namen angegeben werden können");
              }
              vm.callFunction(vm.getProp(_array, "push"), _array, _f(v));
            });
            gibt _array zurück;
          }
          if (Wert Instanz von Map) {
            const _map = vm.unwrapResult(vm.evalCode("neue Map()"));
            Wert.fürJeden((v, k) => {
              vm.unwrapResult(
                vm.callFunction(vm.getProp(_map, "set"), _map, _f(k), _f(v, k))
              );
            });
            gib _map zurück;
          }
          const _object = vm.newObject();
          Objekt.Einträge(Wert).fürJeden(([k, v]) => {
            vm.setProp(_object, k, _f(v, k));
          });
          gibt _Objekt zurück;
        }
        throw new Error("nicht unterstützter Typ");
      }
      return _f(Wert, Name);
    });
  }

  Rückkehrmarschall;
}

verwenden

const mockHello = jest.fn();
const jetzt = neues Datum();
const { Wert, entsorgen } = marshal(vm)({
  Name: "liuli",
  Alter: 1,
  Geschlecht: falsch,
  Hobby: [1, 2, 3],
  Konto:
    Benutzername: "li",
  },
  hallo: mockHallo,
  Karte: neue Map().set(1, "a"),
  Datum: jetzt,
});
vm.setProp(vm.global, "vm_global", Wert);
entsorgen();
Funktion evalCode(Code: Zeichenfolge) {
  returniere vm.unwrapResult(vm.evalCode(code)).consume(vm.dump.bind(vm));
}
erwarten(evalCode("vm_global.name")).toBe("liuli");
erwarten(evalCode("vm_global.age")).toBe(1);
erwarten(evalCode("vm_global.sex")).soll(false);
erwarten(evalCode("vm_global.hobby")).toEqual([1, 2, 3]);
erwarten(neues Datum(evalCode("vm_global.date"))).toEqual(jetzt);
erwarten(evalCode("vm_global.account.username")).toEqual("li");
evalCode("vm_global.hello()");
erwarte(mockHello.mock.calls.length).toBe(1);
erwarten(evalCode("vm_global.map.size")).toBe(1);
erwarten(evalCode("vm_global.map.get(1)")).toBe("a");

Die derzeit unterstützten Typen werden mit dem strukturierten Klonalgorithmus von JavaScript verglichen, der an vielen Stellen verwendet wird ( iframe/web worker/worker_threads ).

Objekttyp schnelljs Strukturiertes Klonen Beachten
Alle primitiven Typen Außer Symbole
Funktion
Anordnung
Objekt Nur einfache Objekte einschließen (z. B. Objektliterale)
Karte
Satz
Datum
Fehler
Boolescher Wert Objekt
Zeichenfolge Objekt
Regulärer Ausdruck Das Feld lastIndex bleibt nicht erhalten.
Klecks
Datei
Dateiliste
ArrayBuffer
ArrayBufferView Dies bedeutet im Wesentlichen, dass alle typisierten Arrays
Bilddaten

Die oben genannten ungewöhnlichen Typen werden von QuickJS nicht unterstützt, von Marshal jedoch noch nicht.

3. Implementieren Sie gängige APIs wie console/setTimeout/setInterval

Da console/setTimeout/setInterval keine APIs auf JS-Sprachebene sind (Browser und Node.JS sie aber implementiert haben), müssen wir sie manuell implementieren und einfügen.

3.1 Implementierung der Konsole

Grundidee: globales Konsolenobjekt in VM einfügen, Parameter ausgeben und an die echte Konsolen-API weiterleiten

importiere { QuickJSVm } von "quickjs-emscripten";
importiere { marshal } von "../util/marshal";

Schnittstelle IVmConsole exportieren {
  log(...args: beliebig[]): ungültig;
  info(...args: beliebig[]): ungültig;
  warnen(...args: any[]): void;
  Fehler(...Argumente: beliebig[]): ungültig;
}

/**
 * Definieren Sie die Konsolen-API in der VM
 * @param vm
 * @param logger
 */
Exportfunktion defineConsole(vm: QuickJSVm, logger: IVmConsole) {
  const fields = ["log", "info", "warn", "error"] als const;
  const dump = vm.dump.bind(vm);
  const { Wert, Entsorgung } = Marshal(vm)(
    Felder.reduzieren((res, k) => {
      res[k] = (...args: beliebig[]) => {
        logger[k](...args.map(dump));
      };
      Rückgabewert;
    }, {} als Datensatz<Zeichenfolge, Funktion>)
  );
  vm.setProp(vm.global, "Konsole", Wert);
  entsorgen();
}

Exportklasse BasicVmConsole implementiert IVmConsole {
  Fehler(...Argumente: beliebig[]): void {
    Konsole.Fehler(...Argumente);
  }

  info(...args: beliebig[]): void {
    Konsole.info(...args);
  }

  log(...args: beliebig[]): void {
    Konsole.log(...args);
  }

  warnen(...args: any[]): void {
    Konsole.warnen(...args);
  }
}

verwenden

Konsole definieren(vm, neue BasicVmConsole());

3.2 Implementierung von setTimeout

Grundgedanke:

Implementierung von setTimeout und clearTimeout basierend auf quickjs

Globale setTimeout/clearTimeout -Funktion in VM einfügen
setTimeout

  • Registrieren Sie die übergebene callbackFunc als globale VM-Variable.
  • Führen Sie setTimeout auf Systemebene aus
  • Schreiben Sie clearTimeoutId => timeoutId in die Karte und geben Sie eine clearTimeoutId
    zurück. clearTimeoutId
  • Führen Sie die gerade registrierte globale VM-Variable aus und löschen Sie den Rückruf

clearTimeout: Ruft das echte learTimeout auf Systemebene gemäß learTimeoutId auf

Der Grund dafür, dass der setTimeout-Rückgabewert nicht direkt zurückgegeben wird, besteht darin, dass der Rückgabewert in nodejs ein Objekt und keine Zahl ist, sodass Map-Kompatibilität erforderlich ist.

importiere { QuickJSVm } von "quickjs-emscripten";
importiere { withScope } von "../util/withScope";
importiere { VmSetInterval } aus "./defineSetInterval";
importiere { deleteKey } aus "../util/deleteKey";
importiere { CallbackIdGenerator } von "@webos/ipc-main";

/**
 * Fügen Sie die Methode setTimeout ein. * Sie müssen nach der Injektion {@link defineEventLoop} aufrufen, um die Ereignisschleife der VM auszuführen. * @param vm
 */
Exportfunktion defineSetTimeout(vm: QuickJSVm): VmSetInterval {
  const callbackMap = neue Map<Zeichenfolge, beliebig>();
  Funktion löschen (ID: Zeichenfolge) {
    mitScope(vm, (vm) => {
      Schlüssel löschen(
        vm,
        vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)),
        Ausweis
      );
    }).entsorgen();
    Löschen Sie das Intervall.
    RückrufMap.delete(id);
  }
  mitScope(vm, (vm) => {
    const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
    wenn (vm.typeof(vmGlobal) === "undefiniert") {
      throw new Error("VM_GLOBAL existiert nicht, Sie müssen zuerst defineVmGlobal ausführen");
    }
    vm.setProp(vmGlobal, "setTimeoutCallback", vm.newObject());
    vm.setProp(
      vm.global,
      "Zeitlimit festlegen",
      vm.newFunction("setTimeout", (Rückruf, ms) => {
        const id = CallbackIdGenerator.generate();
        //Dies ist bereits asynchron, daher müssen Sie es mit withScope(vm, (vm) => { umschließen.
          const Rückrufe = vm.unwrapResult(
            vm.evalCode("VM_GLOBAL.setTimeoutCallback")
          );
          vm.setProp(Rückrufe, ID, Rückruf);
          //Dies ist immer noch asynchron, daher müssen Sie es mit const timeout = setTimeout( umschließen.
            () =>
              mitScope(vm, (vm) => {
                const Rückrufe = vm.unwrapResult(
                  vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)
                );
                const callback = vm.getProp(callbacks, id);
                vm.callFunction(Rückruf, vm.null);
                RückrufMap.delete(id);
              }).entsorgen(),
            vm.dump(ms)
          );
          RückrufMap.set(id, Zeitüberschreitung);
        }).entsorgen();
        gibt vm.newString(id) zurück;
      })
    );
    vm.setProp(
      vm.global,
      "Zeitüberschreitung löschen",
      vm.newFunction("clearTimeout", (id) => löschen(vm.dump(id)))
    );
  }).entsorgen();

  zurückkehren {
    Rückrufkarte,
    klar() {
      [...callbackMap.keys()].fürEach(löschen);
    },
  };
}

verwenden

const vmSetTimeout = definierenSetTimeout(vm);
mitScope(vm, (vm) => {
  vm.evalCode(`
      const begin = Date.now()
      setzeIntervall(() => {
        console.log(Datum.jetzt() - beginnen)
      }, 100)
    `);
}).entsorgen();
vmSetTimeout.clear();

3.3 Implementierung von setInterval

Im Grunde ist es ähnlich wie die Implementierung des setTimeout -Prozesses

importiere { QuickJSVm } von "quickjs-emscripten";
importiere { withScope } von "../util/withScope";
importiere { deleteKey } aus "../util/deleteKey";
importiere { CallbackIdGenerator } von "@webos/ipc-main";

Exportschnittstelle VmSetInterval {
  RückrufMap: Map<Zeichenfolge, beliebig>;
  löschen(): ungültig;
}

/**
 * Fügen Sie die Methode setInterval ein. * Sie müssen nach der Injektion {@link defineEventLoop} aufrufen, um die Ereignisschleife der VM auszuführen. * @param vm
 */
Exportfunktion defineSetInterval(vm: QuickJSVm): VmSetInterval {
  const callbackMap = neue Map<Zeichenfolge, beliebig>();
  Funktion löschen (ID: Zeichenfolge) {
    mitScope(vm, (vm) => {
      Schlüssel löschen(
        vm,
        vm.unwrapResult(vm.evalCode(`VM_GLOBAL.setTimeoutCallback`)),
        Ausweis
      );
    }).entsorgen();
    Löschen Sie das Intervall.
    RückrufMap.delete(id);
  }
  mitScope(vm, (vm) => {
    const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
    wenn (vm.typeof(vmGlobal) === "undefiniert") {
      throw new Error("VM_GLOBAL existiert nicht, Sie müssen zuerst defineVmGlobal ausführen");
    }
    vm.setProp(vmGlobal, "setIntervalCallback", vm.newObject());
    vm.setProp(
      vm.global,
      "Intervall festlegen",
      vm.newFunction("setInterval", (Rückruf, ms) => {
        const id = CallbackIdGenerator.generate();
        //Dies ist bereits asynchron, daher müssen Sie es mit withScope(vm, (vm) => { umschließen.
          const Rückrufe = vm.unwrapResult(
            vm.evalCode("VM_GLOBAL.setIntervalCallback")
          );
          vm.setProp(Rückrufe, ID, Rückruf);
          const Intervall = setzeInterval(() => {
            mitScope(vm, (vm) => {
              vm.callFunktion(
                vm.unwrapResult(
                  vm.evalCode(`VM_GLOBAL.setIntervalCallback['${id}']`)
                ),
                vm.null
              );
            }).entsorgen();
          }, vm.dump(ms));
          RückrufMap.set(id, Intervall);
        }).entsorgen();
        gibt vm.newString(id) zurück;
      })
    );
    vm.setProp(
      vm.global,
      "Löschintervall",
      vm.newFunction("clearInterval", (id) => löschen(vm.dump(id)))
    );
  }).entsorgen();

  zurückkehren {
    Rückrufkarte,
    klar() {
      [...callbackMap.keys()].fürEach(löschen);
    },
  };
}

3.4 Implementierung der Ereignisschleife

Eine Sache ist jedoch, dass quickjs-emscripten die Ereignisschleife nicht automatisch ausführt, d. h. Promise führt nach resolve nicht automatisch den nächsten Schritt aus. Die offizielle Methode executePendingJobs ermöglicht es uns, die Ereignisschleife manuell auszuführen, wie unten gezeigt

const { log } = defineMockConsole(vm);
mitScope(vm, (vm) => {
  vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
}).entsorgen();
erwarte(log.mock.calls.length).toBe(0);
vm.executePendingJobs();
erwarte(log.mock.calls.length).toBe(1);

Wir können also eine Funktion verwenden, die automatisch executePendingJobs aufruft.

importiere { QuickJSVm } von "quickjs-emscripten";

Schnittstelle VmEventLoop exportieren {
  löschen(): ungültig;
}

/**
 * Definieren Sie den Ereignisschleifenmechanismus in der VM, versuchen Sie, eine Schleife zu erstellen und die wartenden asynchronen Operationen auszuführen * @param vm
 */
Exportfunktion defineEventLoop(vm: QuickJSVm) {
  const Intervall = setzeInterval(() => {
    vm.executePendingJobs();
  }, 100);
  zurückkehren {
    klar() {
      clearInterval(Intervall);
    },
  };
}

Rufen Sie jetzt einfach defineEventLoop auf, um die Funktion executePendingJobs in einer Schleife auszuführen.

const { log } = defineMockConsole(vm);
const eventLoop = defineEventLoop(vm);
versuchen {
  mitScope(vm, (vm) => {
    vm.evalCode(`Promise.resolve().then(()=>console.log(1))`);
  }).entsorgen();
  erwarte(log.mock.calls.length).toBe(0);
  warte warte(100);
  erwarte(log.mock.calls.length).toBe(1);
Endlich
  eventLoop.löschen();
}

4. Realisieren Sie die Kommunikation zwischen Sandbox und System

Was unserer Sandbox noch fehlt, ist ein Kommunikationsmechanismus, also implementieren wir einen EventEmiiter .

Der Kern besteht darin, EventEmitter sowohl in der Systemebene als auch in der Sandbox zu implementieren. quickjs ermöglicht es uns, Methoden in die Sandbox einzufügen, sodass wir eine Map- und emitMain -Funktion einfügen können. Die Sandbox kann Ereignisse in der Map registrieren, damit die Systemebene sie aufrufen kann, und kann außerdem über emitMain Ereignisse an die Systemebene senden.

Kommunikation zwischen Sandbox und System:

importiere { QuickJSHandle, QuickJSVm } von "quickjs-emscripten";
importiere { marshal } von "../util/marshal";
importiere { withScope } von "../util/withScope";
importiere { IEventEmitter } von "@webos/ipc-main";

Exporttyp VmMessageChannel = IEventEmitter & {
  listenerMap: Map<string, ((msg: any) => void)[]>;
};

/**
 * Nachrichtenkommunikation definieren * @param vm
 */
Exportfunktion defineMessageChannel(vm: QuickJSVm): VmMessageChannel {
  const res = mitScope(vm, (vm) => {
    const vmGlobal = vm.getProp(vm.global, "VM_GLOBAL");
    wenn (vm.typeof(vmGlobal) === "undefiniert") {
      throw new Error("VM_GLOBAL existiert nicht, Sie müssen zuerst defineVmGlobal ausführen");
    }
    const listenerMap = neue Map<string, ((msg: string) => void)[]>();
    const messagePort = marshal(vm)({
      //Region VM-Prozess Callback-Funktionsdefinition listenerMap: new Map(),
      //emitMain(channel: QuickJSHandle, msg: QuickJSHandle) für VM-Prozess {
        const key = vm.dump(channel);
        Konstantwert = vm.dump(msg);
        wenn (!listenerMap.has(Schlüssel)) {
          console.log("Der Hauptprozess hört nicht auf die API: ", Schlüssel, Wert);
          zurückkehren;
        }
        listenerMap.get(Schlüssel)!.forEach((fn) => {
          versuchen {
            fn(Wert);
          } fangen (e) {
            console.error("Beim Ausführen der Rückruffunktion ist ein Fehler aufgetreten: ", e);
          }
        });
      },
      //Ende der Region
    });
    vm.setProp(vmGlobal, "MessagePort", messagePort.Wert);
    //Funktion emitVM(channel: string, msg: string) für Hauptprozess {
      mitScope(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(Kanal))
        );
        wenn (!vm.dump(_array)) {
          zurückkehren;
        }
        für (
          sei i = 0, Länge = vm.dump(vm.getProp(_array, "Länge"));
          i < Länge;
          ich++
        ) {
          vm.callFunktion(
            vm.getProp(_array, vm.newNumber(i)),
            vm.null,
            Marshal (VM) (Nachricht).Wert
          );
        }
      }).entsorgen();
    }
    zurückkehren {
      ausgeben: emitVM,
      offByChannel(Kanal: Zeichenfolge): void {
        listenerMap.delete(Kanal);
      },
      ein(Kanal: Zeichenfolge, Handle: (Daten: beliebig) => void): void {
        wenn (!listenerMap.has(channel)) {
          listenerMap.set(Kanal, []);
        }
        listenerMap.get(Kanal)!.push(Handle);
      },
      ListenerMap,
    } als VmMessageChannel;
  });
  res.entsorgen();
  gibt Res.Wert zurück;
}

Wie Sie sehen, haben wir zusätzlich zur Implementierung von IEventEmitter auch ein zusätzliches Feld listenerMap hinzugefügt. Dies dient hauptsächlich dazu, der oberen Ebene mehr Details zugänglich zu machen, sodass diese bei Bedarf direkt implementiert werden können (z. B. zum Bereinigen aller registrierten Ereignisse).

verwenden

definiereVmGlobal(vm);
const messageChannel = defineMessageChannel(vm);
const mockFn = jest.fn();
messageChannel.on("hallo", mockFn);
mitScope(vm, (vm) => {
  vm.evalCode(`
Klasse QuickJSEventEmitter {
    emittieren(Kanal, Daten) {
        VM_GLOBAL.MessagePort.emitMain(Kanal, Daten);
    }
    auf(Kanal, Handle) {
        wenn (!VM_GLOBAL.MessagePort.listenerMap.has(channel)) {
            VM_GLOBAL.MessagePort.listenerMap.set(channel, []);
        }
        VM_GLOBAL.MessagePort.listenerMap.get(Kanal).push(Handle);
    }
    offByChannel(Kanal) {
        VM_GLOBAL.MessagePort.listenerMap.delete(Kanal);
    }
}

const em = neuer QuickJSEventEmitter()
em.emit('hallo', 'liuli')
`);
}).entsorgen();
erwarten(mockFn.mock.calls[0][0]).toBe("liuli");
messageChannel.listenerMap.clear();

5. Implementieren Sie IJavaScriptShadowbox

Abschließend fügen wir die oben implementierten Funktionen zusammen, um IJavaScriptShadowbox zu implementieren.

importiere { IJavaScriptShadowbox } aus "./IJavaScriptShadowbox";
importiere { getQuickJS, QuickJS, QuickJSVm } von "quickjs-emscripten";
importieren {
  Grundlegende VM-Konsole,
  Konsole definieren,
  definiereEventLoop,
  definiereMessageChannel,
  definiereSetInterval,
  definierenSetTimeout,
  definierenVmGlobal,
  VmEventLoop,
  VmMessageChannel,
  VmSetInterval,
  mit Umfang,
} von "@webos/quickjs-emscripten-utils";

Exportklasse QuickJSShadowbox implementiert IJavaScriptShadowbox {
  privater vmMessageChannel: VmMessageChannel;
  private vmEventLoop: VmEventLoop;
  privates vmSetInterval: VmSetInterval;
  privates vmSetTimeout: VmSetInterval;

  privater Konstruktor (schreibgeschützte VM: QuickJSVm) {
    Konsole definieren(vm, neue BasicVmConsole());
    definiereVmGlobal(vm);
    this.vmSetTimeout = definiereSetTimeout(vm);
    this.vmSetInterval = definiereSetInterval(vm);
    this.vmEventLoop = definiereEventLoop(vm);
    this.vmMessageChannel = definiereMessageChannel(vm);
  }

  zerstören(): void {
    this.vmMessageChannel.listenerMap.clear();
    this.vmEventLoop.clear();
    this.vmSetInterval.clear();
    Dies.vmSetTimeout.clear();
    dies.vm.dispose();
  }

  eval(Code: Zeichenfolge): void {
    mitScope(this.vm, (vm) => {
      vm.unwrapResult(vm.evalCode(code));
    }).entsorgen();
  }

  emittieren(Kanal: Zeichenfolge, Daten?: beliebig): void {
    this.vmMessageChannel.emit(Kanal, Daten);
  }

  ein(Kanal: Zeichenfolge, Handle: (Daten: beliebig) => void): void {
    this.vmMessageChannel.on(Kanal, Handle);
  }

  offByChannel(Kanal: Zeichenfolge) {
    this.vmMessageChannel.offByChannel(Kanal);
  }

  privates statisches QuickJS: QuickJS;

  statisch asynchron erstellen() {
    wenn (!QuickJSShadowbox.quickJS) {
      QuickJSShadowbox.quickJS = warte auf getQuickJS();
    }
    gibt eine neue QuickJSShadowbox zurück (QuickJSShadowbox.quickJS.createVm());
  }

  statisch zerstören() {
    QuickJSShadowbox.quickJS = null wie beliebig;
  }
}

Verwendung auf Systemebene

const shadowbox = warte auf QuickJSShadowbox.create();
const mockConsole = defineMockConsole(shadowbox.vm);
Schattenbox.eval(Code);
shadowbox.emit(AppChannelEnum.Open);
erwarten(mockConsole.log.mock.calls[0][0]).toBe("öffnen");
shadowbox.emit(WindowChannelEnum.AllClose);
expect(mockConsole.log.mock.calls[1][0]).toBe("alle schließen");
Schattenbox.zerstören();


Verwendung im Sandbox-Bereich

const eventEmitter = neuer QuickJSEventEmitter();
eventEmitter.on(AppChannelEnum.Open, async () => {
  console.log("öffnen");
});
eventEmitter.on(WindowChannelEnum.AllClose, async () => {
  console.log("alle schließen");
});

6. Aktuelle Einschränkungen der QuickJS-Sandbox

Im Folgenden sind einige der Einschränkungen der aktuellen Implementierung aufgeführt, die in Zukunft auch verbessert werden können

Die Konsole unterstützt nur allgemeine Protokoll-/Info-/Warn-/Fehlermethoden.
Die setTimeout/setInterval-Ereignisschleifenzeit ist nicht garantiert. Derzeit wird es etwa alle 100 ms aufgerufen. Chrome-Devtool-Debugging ist nicht möglich und Sourcemap-Verarbeitung ist nicht verfügbar (Figmas Entwicklungserfahrung ist bis heute noch dieselbe. Switches können später hinzugefügt werden, um das Debugging in Web Workern zu unterstützen)
Fehler in VM werden nicht ausgelöst und in der Konsole gedruckt. Die Reihenfolge der API-Aufrufe und der Bereinigungsreihenfolge muss manuell sichergestellt werden, dass sie entgegengesetzt sind. Beispielsweise muss die VM-Erstellung vor defineSetTimeout erfolgen, und der Bereinigungsfunktionsaufruf von defineSetTimeout muss vor vm.dispose erfolgen. vm.dispose kann im messageChannel.on-Rückruf nicht synchron aufgerufen werden, da es sich um einen synchronen Aufruf handelt.

Dies ist das Ende dieses Artikels über die Details der QuickJS-Kapselung von JavaScript-Sandboxen. Weitere relevante QuickJS-Kapselungsinhalte von JavaScript-Sandboxen finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder durchsuchen Sie die verwandten Artikel weiter unten. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird!

Das könnte Sie auch interessieren:
  • JavaScript-Sandbox-Erkundung
  • Ein kurzer Vortrag über JavaScript Sandbox
  • Eine kurze Diskussion über verschiedene Möglichkeiten zur Implementierung einer Front-End-JS-Sandbox
  • Eine kurze Diskussion über die Node.js-Sandbox-Umgebung
  • Einrichten einer sicheren Sandbox-Umgebung für Node.js-Anwendungen
  • Beispiel für den Sandbox-Modus beim Schließen der JS-Implementierung
  • Beispielanalyse des JS-Sandbox-Modus
  • Sicherheits-Sandbox-Modus des JavaScript-Entwurfsmusters
  • WebWorker kapselt JavaScript-Sandbox-Details

<<:  So zeigen Sie den Datenbankinstallationspfad in MySQL an

>>:  Python schreibt die Ausgabe in den CSV-Vorgang

Artikel empfehlen

Erläuterung der HTML-Tags

Erläuterung der HTML-Tags 1. HTML-Tags Tag: !DOCT...

Erstellen Sie ein Docker-Image mit Dockerfile

Inhaltsverzeichnis Erstellen Sie ein Docker-Image...

Implementierungscode des JQuery-Schrittfortschrittsachsen-Plug-Ins

Jeden Tag ein jQuery-Plugin - Schritt-Fortschritt...

Ein nützliches mobiles Scrolling-Plugin BetterScroll

Inhaltsverzeichnis Machen Sie das Scrollen flüssi...

MySQL-Anmelde- und Beendigungsbefehlsformat

Das Befehlsformat für die MySQL-Anmeldung ist: my...

So verwenden Sie Docker zum Bereitstellen von Front-End-Anwendungen

Docker erfreut sich immer größerer Beliebtheit. E...

Centos7 installiert mysql5.6.29 Shell-Skript

In diesem Artikel wird das Shell-Skript von mysql...