Merge branch 'isolated-vm-wrapper' into fix-bson
This commit is contained in:
commit
a55e75ae18
|
@ -13,11 +13,16 @@ export function init() {
|
|||
|
||||
let { vm } = bbCtx
|
||||
if (!vm) {
|
||||
// Can't copy the native helpers into the isolate. We just ignore them as they are handled properly from the helpersSource
|
||||
const { helpers, ...ctxToPass } = ctx
|
||||
|
||||
vm = new IsolatedVM({
|
||||
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
|
||||
timeout: env.JS_PER_EXECUTION_TIME_LIMIT_MS,
|
||||
perRequestLimit: env.JS_PER_REQUEST_TIME_LIMIT_MS,
|
||||
}).withHelpers()
|
||||
})
|
||||
.withContext(ctxToPass)
|
||||
.withHelpers()
|
||||
|
||||
bbCtx.vm = vm
|
||||
}
|
||||
|
|
|
@ -16,47 +16,47 @@ class ExecutionTimeoutError extends Error {
|
|||
}
|
||||
|
||||
class ModuleHandler {
|
||||
#modules: {
|
||||
private modules: {
|
||||
import: string
|
||||
moduleKey: string
|
||||
module: ivm.Module
|
||||
}[] = []
|
||||
|
||||
#generateRandomKey = () => `i${crypto.randomUUID().replace(/-/g, "")}`
|
||||
private generateRandomKey = () => `i${crypto.randomUUID().replace(/-/g, "")}`
|
||||
|
||||
registerModule(module: ivm.Module, imports: string) {
|
||||
this.#modules.push({
|
||||
moduleKey: this.#generateRandomKey(),
|
||||
this.modules.push({
|
||||
moduleKey: this.generateRandomKey(),
|
||||
import: imports,
|
||||
module: module,
|
||||
})
|
||||
}
|
||||
|
||||
generateImports() {
|
||||
return this.#modules
|
||||
return this.modules
|
||||
.map(m => `import ${m.import} from "${m.moduleKey}"`)
|
||||
.join(";")
|
||||
}
|
||||
|
||||
getModule(key: string) {
|
||||
const module = this.#modules.find(m => m.moduleKey === key)
|
||||
const module = this.modules.find(m => m.moduleKey === key)
|
||||
return module?.module
|
||||
}
|
||||
}
|
||||
|
||||
export class IsolatedVM implements VM {
|
||||
#isolate: ivm.Isolate
|
||||
#vm: ivm.Context
|
||||
#jail: ivm.Reference
|
||||
#timeout: number
|
||||
#perRequestLimit?: number
|
||||
private isolate: ivm.Isolate
|
||||
private vm: ivm.Context
|
||||
private jail: ivm.Reference
|
||||
private timeout: number
|
||||
private perRequestLimit?: number
|
||||
|
||||
// By default the wrapper returns itself
|
||||
#codeWrapper: (code: string) => string = code => code
|
||||
private codeWrapper: (code: string) => string = code => code
|
||||
|
||||
#moduleHandler = new ModuleHandler()
|
||||
private moduleHandler = new ModuleHandler()
|
||||
|
||||
readonly #resultKey = "results"
|
||||
private readonly resultKey = "results"
|
||||
|
||||
constructor({
|
||||
memoryLimit,
|
||||
|
@ -67,30 +67,30 @@ export class IsolatedVM implements VM {
|
|||
timeout: number
|
||||
perRequestLimit?: 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.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.addToContext({
|
||||
[this.resultKey]: { out: "" },
|
||||
})
|
||||
|
||||
this.#timeout = timeout
|
||||
this.#perRequestLimit = perRequestLimit
|
||||
this.timeout = timeout
|
||||
this.perRequestLimit = perRequestLimit
|
||||
}
|
||||
|
||||
withHelpers() {
|
||||
const urlModule = this.#registerCallbacks({
|
||||
const urlModule = this.registerCallbacks({
|
||||
resolve: url.resolve,
|
||||
parse: url.parse,
|
||||
})
|
||||
|
||||
const querystringModule = this.#registerCallbacks({
|
||||
const querystringModule = this.registerCallbacks({
|
||||
escape: querystring.escape,
|
||||
})
|
||||
|
||||
this.#addToContext({
|
||||
this.addToContext({
|
||||
helpersStripProtocol: new ivm.Callback((str: string) => {
|
||||
var parsed = url.parse(str) as any
|
||||
parsed.protocol = ""
|
||||
|
@ -105,19 +105,19 @@ export class IsolatedVM implements VM {
|
|||
}
|
||||
}`
|
||||
const helpersSource = loadBundle(BundleType.HELPERS)
|
||||
const helpersModule = this.#isolate.compileModuleSync(
|
||||
const helpersModule = this.isolate.compileModuleSync(
|
||||
`${injectedRequire};${helpersSource}`
|
||||
)
|
||||
|
||||
helpersModule.instantiateSync(this.#vm, specifier => {
|
||||
helpersModule.instantiateSync(this.vm, specifier => {
|
||||
if (specifier === "crypto") {
|
||||
const cryptoModule = this.#registerCallbacks({
|
||||
const cryptoModule = this.registerCallbacks({
|
||||
randomUUID: crypto.randomUUID,
|
||||
})
|
||||
const module = this.#isolate.compileModuleSync(
|
||||
const module = this.isolate.compileModuleSync(
|
||||
`export default ${cryptoModule}`
|
||||
)
|
||||
module.instantiateSync(this.#vm, specifier => {
|
||||
module.instantiateSync(this.vm, specifier => {
|
||||
throw new Error(`No imports allowed. Required: ${specifier}`)
|
||||
})
|
||||
return module
|
||||
|
@ -125,18 +125,18 @@ export class IsolatedVM implements VM {
|
|||
throw new Error(`No imports allowed. Required: ${specifier}`)
|
||||
})
|
||||
|
||||
this.#moduleHandler.registerModule(helpersModule, "helpers")
|
||||
this.moduleHandler.registerModule(helpersModule, "helpers")
|
||||
return this
|
||||
}
|
||||
|
||||
withContext(context: Record<string, any>) {
|
||||
this.#addToContext(context)
|
||||
this.addToContext(context)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
withParsingBson(data: any) {
|
||||
this.#addToContext({
|
||||
this.addToContext({
|
||||
bsonData: bson.BSON.serialize({ data }),
|
||||
})
|
||||
|
||||
|
@ -145,7 +145,7 @@ export class IsolatedVM implements VM {
|
|||
// 2. Deserialise the data within the isolate, to get the original data
|
||||
// 3. Process script
|
||||
// 4. Stringify the result in order to convert the result from BSON to json
|
||||
this.#codeWrapper = code =>
|
||||
this.codeWrapper = code =>
|
||||
`(function(){
|
||||
const data = deserialize(bsonData, { validation: { utf8: false } }).data;
|
||||
const result = ${code}
|
||||
|
@ -154,7 +154,7 @@ export class IsolatedVM implements VM {
|
|||
|
||||
const bsonSource = loadBundle(BundleType.BSON)
|
||||
|
||||
this.#addToContext({
|
||||
this.addToContext({
|
||||
textDecoderCb: new ivm.Callback(
|
||||
(args: {
|
||||
constructorArgs: any
|
||||
|
@ -184,23 +184,23 @@ export class IsolatedVM implements VM {
|
|||
})
|
||||
}
|
||||
}.toString()
|
||||
const bsonModule = this.#isolate.compileModuleSync(
|
||||
const bsonModule = this.isolate.compileModuleSync(
|
||||
`${textDecoderPolyfill};${bsonSource}`
|
||||
)
|
||||
bsonModule.instantiateSync(this.#vm, specifier => {
|
||||
bsonModule.instantiateSync(this.vm, specifier => {
|
||||
throw new Error(`No imports allowed. Required: ${specifier}`)
|
||||
})
|
||||
|
||||
this.#moduleHandler.registerModule(bsonModule, "{deserialize, toJson}")
|
||||
this.moduleHandler.registerModule(bsonModule, "{deserialize, toJson}")
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
execute(code: string): string {
|
||||
const perRequestLimit = this.#perRequestLimit
|
||||
const perRequestLimit = this.perRequestLimit
|
||||
|
||||
if (perRequestLimit) {
|
||||
const cpuMs = Number(this.#isolate.cpuTime) / 1e6
|
||||
const cpuMs = Number(this.isolate.cpuTime) / 1e6
|
||||
if (cpuMs > perRequestLimit) {
|
||||
throw new ExecutionTimeoutError(
|
||||
`CPU time limit exceeded (${cpuMs}ms > ${perRequestLimit}ms)`
|
||||
|
@ -208,14 +208,14 @@ export class IsolatedVM implements VM {
|
|||
}
|
||||
}
|
||||
|
||||
code = `${this.#moduleHandler.generateImports()};results.out=${this.#codeWrapper(
|
||||
code = `${this.moduleHandler.generateImports()};results.out=${this.codeWrapper(
|
||||
code
|
||||
)};`
|
||||
|
||||
const script = this.#isolate.compileModuleSync(code)
|
||||
const script = this.isolate.compileModuleSync(code)
|
||||
|
||||
script.instantiateSync(this.#vm, specifier => {
|
||||
const module = this.#moduleHandler.getModule(specifier)
|
||||
script.instantiateSync(this.vm, specifier => {
|
||||
const module = this.moduleHandler.getModule(specifier)
|
||||
if (module) {
|
||||
return module
|
||||
}
|
||||
|
@ -223,13 +223,13 @@ export class IsolatedVM implements VM {
|
|||
throw new Error(`"${specifier}" import not allowed`)
|
||||
})
|
||||
|
||||
script.evaluateSync({ timeout: this.#timeout })
|
||||
script.evaluateSync({ timeout: this.timeout })
|
||||
|
||||
const result = this.#getFromContext(this.#resultKey)
|
||||
const result = this.getFromContext(this.resultKey)
|
||||
return result.out
|
||||
}
|
||||
|
||||
#registerCallbacks(functions: Record<string, any>) {
|
||||
private registerCallbacks(functions: Record<string, any>) {
|
||||
const libId = crypto.randomUUID().replace(/-/g, "")
|
||||
|
||||
const x: Record<string, string> = {}
|
||||
|
@ -237,7 +237,7 @@ export class IsolatedVM implements VM {
|
|||
const key = `f${libId}${funcName}cb`
|
||||
x[funcName] = key
|
||||
|
||||
this.#addToContext({
|
||||
this.addToContext({
|
||||
[key]: new ivm.Callback((...params: any[]) => (func as any)(...params)),
|
||||
})
|
||||
}
|
||||
|
@ -251,17 +251,20 @@ export class IsolatedVM implements VM {
|
|||
return mod
|
||||
}
|
||||
|
||||
#addToContext(context: Record<string, any>) {
|
||||
private addToContext(context: Record<string, any>) {
|
||||
for (let key in context) {
|
||||
this.#jail.setSync(
|
||||
const value = context[key]
|
||||
this.jail.setSync(
|
||||
key,
|
||||
new ivm.ExternalCopy(context[key]).copyInto({ release: true })
|
||||
typeof value === "function"
|
||||
? value
|
||||
: new ivm.ExternalCopy(value).copyInto({ release: true })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#getFromContext(key: string) {
|
||||
const ref = this.#vm.global.getSync(key, { reference: true })
|
||||
private getFromContext(key: string) {
|
||||
const ref = this.vm.global.getSync(key, { reference: true })
|
||||
const result = ref.copySync()
|
||||
ref.release()
|
||||
return result
|
||||
|
|
|
@ -2,24 +2,25 @@ import env from "../environment"
|
|||
import { IsolatedVM } from "../jsRunner/vm"
|
||||
|
||||
const JS_TIMEOUT_MS = 1000
|
||||
|
||||
class ScriptRunner {
|
||||
#code: string
|
||||
#vm: IsolatedVM
|
||||
private code: string
|
||||
private vm: IsolatedVM
|
||||
|
||||
constructor(script: string, context: any, { parseBson = false } = {}) {
|
||||
this.#code = `(() => {${script}})();`
|
||||
this.#vm = new IsolatedVM({
|
||||
this.code = `(() => {${script}})();`
|
||||
this.vm = new IsolatedVM({
|
||||
memoryLimit: env.JS_RUNNER_MEMORY_LIMIT,
|
||||
timeout: JS_TIMEOUT_MS,
|
||||
}).withContext(context)
|
||||
|
||||
if (parseBson && context.data) {
|
||||
this.#vm = this.#vm.withParsingBson(context.data)
|
||||
this.vm = this.vm.withParsingBson(context.data)
|
||||
}
|
||||
}
|
||||
|
||||
execute() {
|
||||
const result = this.#vm.execute(this.#code)
|
||||
const result = this.vm.execute(this.code)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue