module-sandbox
Version:
A v8 isolate sandbox with require support
137 lines (115 loc) • 5.14 kB
JavaScript
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))
}