UNPKG

module-sandbox

Version:

A v8 isolate sandbox with require support

137 lines (115 loc) 5.14 kB
const fs = require('fs') const { NanoresourcePromise: Nanoresource } = require('nanoresource-promise/emitter') const { unflatten } = require('flat') const ivm = require('@andrewosh/isolated-vm') const RequireController = require('./lib/requires') const { __makeRequire, __attachNodeShims, __requireBuild, __requireLoad, __requireDirname, __getExportedFunctions } = require('./lib/guest') module.exports = class Sandbox extends Nanoresource { constructor (path, opts = {}) { super() this.path = path this.fs = opts.fs || fs.promises this.isolate = new ivm.Isolate(opts) this.script = null this.rpc = null this._requireController = null this._hostcalls = mapOpt(opts.hostcalls) this._requires = mapOpt(opts.requires) this.ready = this.open.bind(this) } // Nanoresource Methods async _open () { const context = await this.isolate.createContext() const jail = context.global // This make the global object available in the context as `global`. We use `derefInto()` here // because otherwise `global` would actually be a Reference{} object in the new isolate. await jail.set('global', jail.derefInto()) const defaultFs = (typeof this.fs === 'function') ? (await this.fs()).fs : this.fs this._requireController = new RequireController(this.fs, this.path, { overrides: this._requires }) await this._configureEnvironment(context, this._requireController) const scriptSource = await defaultFs.readFile(this.path, { encoding: 'utf-8' }) this.script = await this.isolate.compileScript(scriptSource) await this.script.run(context) // Attach exported stuff to this.exports await this._attachExports(context) } async _close () { // TODO: Any other cleanup required? return this.isolate.dispose() } // Private Methods async _attachExports (context) { let exportedFunctions = await context.eval('__getExportedFunctions()', { copy: true }) if (!exportedFunctions || !Array.isArray(exportedFunctions.result)) return exportedFunctions = exportedFunctions.result.map(path => [path, async (...args) => { const res = await context.evalClosure(`return module.exports.${path}.apply(undefined, arguments)`, [...args], { arguments: { copy: true }, result: { promise: true, copy: true } }) return res && res.result }]) exportedFunctions = exportedFunctions.reduce((obj, pair) => { obj[pair[0]] = pair[1] return obj }, {}) this.rpc = unflatten(exportedFunctions) } async _attachHostcalls (context) { for (const [name, func] of this._hostcalls) { const wrappedFunc = (...args) => { return func(context, ...args) } await context.evalClosure(`global.hostcalls.${name} = function (...args) { return $0.apply(undefined, args, { result: { promise: true, copy: true }, arguments: { copy: true } }) }`, [new ivm.Reference(wrappedFunc)], { result: { copy: true }, arguments: { copy: true } }) } } async _configureEnvironment (context, controller) { // Enable logging. await context.evalClosure(`global.console.log = function(...args) { $0.applyIgnored(undefined, args, { arguments: { copy: true } }); }`, [(...args) => console.log('sandbox:', ...args)], { arguments: { reference: true } }) await context.global.set('__requireSignalBuf', new ivm.ExternalCopy(controller.signal).copyInto({ release: true })) const env = ` global.__requireSignal = new Int32Array(__requireSignalBuf) global.__requireUnsupported = new Set(['fs', 'net', 'tls', 'http', 'https']) global.__requireCache = {} global.__requireCompile = new Function('module', 'exports', '__filename', '__dirname', 'require', '__src', 'eval(__src)') global.__requireRootContext = '${controller.rootContext}' global.__makeRequire = ${__makeRequire} global.__requireLoad = ${__requireLoad} global.__requireBuild = ${__requireBuild} global.__requireDirname = ${__requireDirname} ${__attachNodeShims} ${__getExportedFunctions} __attachNodeShims() ` await context.eval(env) await context.evalClosure(`global.__requireControllerLoad = function (...args) { $0.applyIgnored(undefined, [...args, Object.keys(__requireCache), false], { arguments: { copy: true } }) Atomics.wait(__requireSignal, 0, 0) __requireSignal[0] = 0 }`, [controller.load.bind(controller)], { arguments: { reference: true } }) await context.evalClosure(`global.__requireControllerFetch = function (...args) { return $0.applySync(undefined, args, { arguments: { copy: true } }) }`, [controller.fetch.bind(controller)], { arguments: { reference: true } }) await context.eval('global.require = __makeRequire()') await context.eval('global.Buffer = require(\'buffer\').Buffer') // Hook up any provided hostcalls. if (this._hostcalls) await this._attachHostcalls(context) } } function mapOpt (opt) { if (!opt) return null if (opt instanceof Map) return opt return new Map(Object.entries(opt)) }