From c5abb4f8462812b086d680fbaf9bbf4a09ca3c05 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 7 Feb 2024 18:07:23 +0100 Subject: [PATCH] Create wrapper --- packages/server/src/jsRunner/vm/index.ts | 154 +++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 packages/server/src/jsRunner/vm/index.ts diff --git a/packages/server/src/jsRunner/vm/index.ts b/packages/server/src/jsRunner/vm/index.ts new file mode 100644 index 0000000000..0f9b11e531 --- /dev/null +++ b/packages/server/src/jsRunner/vm/index.ts @@ -0,0 +1,154 @@ +import ivm from "isolated-vm" + +import url from "url" +import crypto from "crypto" +import querystring from "querystring" + +import { BundleType, loadBundle } from "../bundles" +import { VM } from "@budibase/types" + +export class IsolatedVM implements VM { + #isolate: ivm.Isolate + #vm: ivm.Context + #jail: ivm.Reference + #timeout: number + + #modules: Record< + string, + { + headCode: string + module: ivm.Module + } + > = {} + + readonly #resultKey = "results" + + constructor({ + memoryLimit, + timeout, + }: { + memoryLimit: number + timeout: number + }) { + this.#isolate = new ivm.Isolate({ memoryLimit }) + this.#vm = this.#isolate.createContextSync() + this.#jail = this.#vm.global + this.#jail.setSync("global", this.#jail.derefInto()) + + this.#addToContext({ + [this.#resultKey]: { out: "" }, + }) + + this.#timeout = timeout + } + + get cpuTime() { + return this.#isolate.cpuTime + } + + withHelpers() { + const injectedRequire = ` + const require = function(val){ + switch (val) { + case "url": + return { + resolve: (...params) => urlResolveCb(...params), + parse: (...params) => urlParseCb(...params), + } + case "querystring": + return { + escape: (...params) => querystringEscapeCb(...params), + } + } + };` + + const helpersSource = loadBundle(BundleType.HELPERS) + const helpersModule = this.#isolate.compileModuleSync( + `${injectedRequire};${helpersSource}` + ) + + this.#addToContext({ + urlResolveCb: new ivm.Callback( + (...params: Parameters) => url.resolve(...params) + ), + urlParseCb: new ivm.Callback((...params: Parameters) => + url.parse(...params) + ), + querystringEscapeCb: new ivm.Callback( + (...params: Parameters) => + querystring.escape(...params) + ), + helpersStripProtocol: new ivm.Callback((str: string) => { + var parsed = url.parse(str) as any + parsed.protocol = "" + return parsed.format() + }), + }) + + const cryptoModule = this.#isolate.compileModuleSync( + `export default { randomUUID: cryptoRandomUUIDCb }` + ) + cryptoModule.instantiateSync(this.#vm, specifier => { + throw new Error(`No imports allowed. Required: ${specifier}`) + }) + + this.#addToContext({ + cryptoRandomUUIDCb: new ivm.Callback( + (...params: Parameters) => { + return crypto.randomUUID(...params) + } + ), + }) + + helpersModule.instantiateSync(this.#vm, specifier => { + if (specifier === "crypto") { + return cryptoModule + } + throw new Error(`No imports allowed. Required: ${specifier}`) + }) + + this.#modules["compiled_module"] = { + headCode: 'import helpers from "compiled_module"', + module: helpersModule, + } + return this + } + + execute(code: string): string { + code = [ + ...Object.values(this.#modules).map(m => m.headCode), + `results.out=${code};`, + ].join(";") + + const script = this.#isolate.compileModuleSync(code) + + script.instantiateSync(this.#vm, specifier => { + if (specifier === "compiled_module") { + return this.#modules[specifier].module + } + + throw new Error(`"${specifier}" import not allowed`) + }) + + script.evaluateSync({ timeout: this.#timeout }) + + const result = this.#getResult() + return result + } + + #addToContext(context: Record) { + for (let key in context) { + this.#jail.setSync( + key, + new ivm.ExternalCopy(context[key]).copyInto({ release: true }) + ) + } + } + + #getResult() { + const ref = this.#vm.global.getSync(this.#resultKey, { reference: true }) + const result = ref.copySync() + ref.release() + return result.out + } +}