Comparing version 3.9.17 to 3.9.18
@@ -0,1 +1,7 @@ | ||
v3.9.18 (2023-05-15) | ||
-------------------- | ||
[fix] Multiple security fixes. | ||
[new] Add resolver API to create a shared resolver for multiple `NodeVM` instances allowing to cache scripts and increase sandbox startup times. | ||
[new] Allow to pass a function to `require.context` which is called with the filename allowing to specify the context pre file. | ||
v3.9.17 (2023-04-17) | ||
@@ -2,0 +8,0 @@ -------------------- |
@@ -63,2 +63,20 @@ import { EventEmitter } from 'events'; | ||
/** | ||
* Function that will be called to load a built-in into a vm. | ||
*/ | ||
export type BuiltinLoad = (vm: NodeVM) => any; | ||
/** | ||
* Either a function that will be called to load a built-in into a vm or an object with a init method and a load method to load the built-in. | ||
*/ | ||
export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad}; | ||
/** | ||
* Require method | ||
*/ | ||
export type HostRequire = (id: string) => any; | ||
/** | ||
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided. | ||
*/ | ||
export type PathContextCallback = (modulePath: string, extensionType: string) => 'host' | 'sandbox'; | ||
/** | ||
* Require options for a VM | ||
@@ -71,14 +89,15 @@ */ | ||
*/ | ||
builtin?: string[]; | ||
builtin?: readonly string[]; | ||
/* | ||
* `host` (default) to require modules in host and proxy them to sandbox. `sandbox` to load, compile and | ||
* require modules in sandbox. Built-in modules except `events` always required in host and proxied to sandbox | ||
* require modules in sandbox or a callback which chooses the context based on the filename. | ||
* Built-in modules except `events` always required in host and proxied to sandbox | ||
*/ | ||
context?: "host" | "sandbox"; | ||
context?: "host" | "sandbox" | PathContextCallback; | ||
/** `true`, an array of allowed external modules or an object with external options (default: `false`) */ | ||
external?: boolean | string[] | { modules: string[], transitive: boolean }; | ||
external?: boolean | readonly string[] | { modules: readonly string[], transitive: boolean }; | ||
/** Array of modules to be loaded into NodeVM on start. */ | ||
import?: string[]; | ||
import?: readonly string[]; | ||
/** Restricted path(s) where local modules can be required (default: every path). */ | ||
root?: string | string[]; | ||
root?: string | readonly string[]; | ||
/** Collection of mock modules (both external or built-in). */ | ||
@@ -89,3 +108,3 @@ mock?: any; | ||
/** Custom require to require host and built-in modules. */ | ||
customRequire?: (id: string) => any; | ||
customRequire?: HostRequire; | ||
/** Load modules in strict mode. (default: true) */ | ||
@@ -103,3 +122,16 @@ strict?: boolean; | ||
export abstract class Resolver { | ||
private constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Map<string, Builtin>); | ||
} | ||
/** | ||
* Create a resolver as normal `NodeVM` does given `VMRequire` options. | ||
* | ||
* @param options The options that would have been given to `NodeVM`. | ||
* @param override Custom overrides for built-ins. | ||
* @param compiler Compiler to be used for loaded modules. | ||
*/ | ||
export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver; | ||
/** | ||
* Options for creating a VM | ||
@@ -116,3 +148,3 @@ */ | ||
/** | ||
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`. | ||
* Script timeout in milliseconds. Timeout is only effective on code you run through `run`. | ||
* Timeout is NOT effective on any method returned by VM. | ||
@@ -149,3 +181,3 @@ */ | ||
/** `true` or an object to enable `require` options (default: `false`). */ | ||
require?: boolean | VMRequire; | ||
require?: boolean | VMRequire | Resolver; | ||
/** | ||
@@ -159,3 +191,3 @@ * **WARNING**: This should be disabled. It allows to create a NodeVM form within the sandbox which could return any host module. | ||
/** File extensions that the internal module resolver should accept. */ | ||
sourceExtensions?: string[]; | ||
sourceExtensions?: readonly string[]; | ||
/** | ||
@@ -234,2 +266,4 @@ * Array of arguments passed to `process.argv`. | ||
timeout?: number; | ||
/** The resolver used to resolve modules */ | ||
readonly resolver: Resolver; | ||
/** Runs the code */ | ||
@@ -236,0 +270,0 @@ run(js: string | VMScript, options?: string | { filename?: string, wrapper?: "commonjs" | "none", strict?: boolean }): any; |
@@ -18,2 +18,8 @@ 'use strict'; | ||
} = require('./filesystem'); | ||
const { | ||
Resolver | ||
} = require('./resolver'); | ||
const { | ||
makeResolverFromLegacyOptions | ||
} = require('./resolver-compat'); | ||
@@ -25,1 +31,3 @@ exports.VMError = VMError; | ||
exports.VMFileSystem = VMFileSystem; | ||
exports.Resolver = Resolver; | ||
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions; |
@@ -20,2 +20,13 @@ 'use strict'; | ||
/** | ||
* This callback will be called to specify the context to use "per" module. Defaults to 'sandbox' if no return value provided. | ||
* | ||
* NOTE: many interoperating modules must live in the same context. | ||
* | ||
* @callback pathContextCallback | ||
* @param {string} modulePath - The full path to the module filename being requested. | ||
* @param {string} extensionType - The module type (node = native, js = cjs/esm module) | ||
* @return {("host"|"sandbox")} The context for this module. | ||
*/ | ||
const fs = require('fs'); | ||
@@ -42,4 +53,5 @@ const pa = require('path'); | ||
const { | ||
resolverFromOptions | ||
makeResolverFromLegacyOptions | ||
} = require('./resolver-compat'); | ||
const { Resolver } = require('./resolver'); | ||
@@ -91,2 +103,32 @@ const objectDefineProperty = Object.defineProperty; | ||
function makeCustomExtensions(vm, resolver, sourceExtensions) { | ||
const extensions = { __proto__: null }; | ||
const loadJS = resolver.makeExtensionHandler(vm, 'loadJS'); | ||
for (let i = 0; i < sourceExtensions.length; i++) { | ||
extensions['.' + sourceExtensions[i]] = loadJS; | ||
} | ||
if (!extensions['.json']) extensions['.json'] = resolver.makeExtensionHandler(vm, 'loadJSON'); | ||
if (!extensions['.node']) extensions['.node'] = resolver.makeExtensionHandler(vm, 'loadNode'); | ||
return extensions; | ||
} | ||
function makeSafePaths(unsafePaths) { | ||
if (unsafePaths === undefined) return undefined; | ||
if (!Array.isArray(unsafePaths)) return true; | ||
const paths = [...unsafePaths]; | ||
if (paths.some(path => typeof path !== 'string')) return true; | ||
return paths; | ||
} | ||
function makeSafeOptions(unsafeOptions) { | ||
if (unsafeOptions === undefined || unsafeOptions == null) return unsafeOptions; | ||
if (typeof unsafeOptions !== 'object' && typeof unsafeOptions !== 'function') return unsafeOptions; | ||
return { | ||
unsafeOptions, | ||
paths: makeSafePaths(unsafeOptions.paths) | ||
}; | ||
} | ||
/** | ||
@@ -174,3 +216,3 @@ * Event caused by a <code>console.debug</code> call if <code>options.console="redirect"</code> is specified. | ||
* <code>inherit</code> to enable console, <code>redirect</code> to redirect to events, <code>off</code> to disable console. | ||
* @param {Object|boolean} [options.require=false] - Allow require inside the sandbox. | ||
* @param {Object|boolean|Resolver} [options.require=false] - Allow require inside the sandbox. | ||
* @param {(boolean|string[]|Object)} [options.require.external=false] - <b>WARNING: When allowing require the option <code>options.require.root</code> | ||
@@ -184,4 +226,6 @@ * should be set to restrict the script from requiring any module. Values can be true, an array of allowed external modules or an object. | ||
* @param {Object} [options.require.mock] - Collection of mock modules (both external or built-in). | ||
* @param {("host"|"sandbox")} [options.require.context="host"] - <code>host</code> to require modules in host and proxy them to sandbox. | ||
* @param {("host"|"sandbox"|pathContextCallback)} [options.require.context="host"] - | ||
* <code>host</code> to require modules in host and proxy them to sandbox. | ||
* <code>sandbox</code> to load, compile and require modules in sandbox. | ||
* <code>pathContext(modulePath, ext)</code> to choose a mode per module (full path provided). | ||
* Builtin modules except <code>events</code> always required in host and proxied to sandbox. | ||
@@ -192,3 +236,3 @@ * @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start. | ||
* @param {customRequire} [options.require.customRequire=require] - Custom require to require host and built-in modules. | ||
* @param {boolean} [option.require.strict=true] - Load required modules in strict mode. | ||
* @param {boolean} [options.require.strict=true] - Load required modules in strict mode. | ||
* @param {boolean} [options.nesting=false] - | ||
@@ -230,2 +274,5 @@ * <b>WARNING: Allowing this is a security risk as scripts can create a NodeVM which can require any host module.</b> | ||
const customResolver = requireOpts instanceof Resolver; | ||
const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(requireOpts, nesting && NESTING_OVERRIDE, this._compiler); | ||
// This is only here for backwards compatibility. | ||
@@ -241,6 +288,4 @@ objectDefineProperty(this, 'options', {__proto__: null, value: { | ||
const resolver = resolverFromOptions(this, requireOpts, nesting && NESTING_OVERRIDE, this._compiler); | ||
objectDefineProperty(this, 'resolver', {__proto__: null, value: resolver, enumerable: true}); | ||
objectDefineProperty(this, '_resolver', {__proto__: null, value: resolver}); | ||
if (!cacheSandboxScript) { | ||
@@ -253,19 +298,5 @@ cacheSandboxScript = compileScript(`${__dirname}/setup-node-sandbox.js`, | ||
const extensions = { | ||
__proto__: null | ||
}; | ||
const extensions = makeCustomExtensions(this, resolver, sourceExtensions); | ||
const loadJS = (mod, filename) => resolver.loadJS(this, mod, filename); | ||
for (let i = 0; i < sourceExtensions.length; i++) { | ||
extensions['.' + sourceExtensions[i]] = loadJS; | ||
} | ||
if (!extensions['.json']) extensions['.json'] = (mod, filename) => resolver.loadJSON(this, mod, filename); | ||
if (!extensions['.node']) extensions['.node'] = (mod, filename) => resolver.loadNode(this, mod, filename); | ||
this.readonly(HOST); | ||
this.readonly(resolver); | ||
this.readonly(this); | ||
@@ -282,5 +313,37 @@ const { | ||
console: consoleType, | ||
vm: this, | ||
resolver, | ||
extensions | ||
extensions, | ||
emitArgs: (event, args) => { | ||
if (typeof event !== 'string' && typeof event !== 'symbol') throw new Error('Event is not a string'); | ||
return this.emit(event, ...args); | ||
}, | ||
globalPaths: [...resolver.globalPaths], | ||
getLookupPathsFor: (path) => { | ||
if (typeof path !== 'string') return []; | ||
return [...resolver.genLookupPaths(path)]; | ||
}, | ||
resolve: (mod, id, opt, ext, direct) => { | ||
if (typeof id !== 'string') throw new Error('Id is not a string'); | ||
const extList = Object.getOwnPropertyNames(ext); | ||
return resolver.resolve(mod, id, makeSafeOptions(opt), extList, !!direct); | ||
}, | ||
lookupPaths: (mod, id) => { | ||
if (typeof id !== 'string') throw new Error('Id is not a string'); | ||
return [...resolver.lookupPaths(mod, id)]; | ||
}, | ||
loadBuiltinModule: (id) => { | ||
if (typeof id !== 'string') throw new Error('Id is not a string'); | ||
return resolver.loadBuiltinModule(this, id); | ||
}, | ||
registerModule: (mod, filename, path, parent, direct) => { | ||
return resolver.registerModule(mod, filename, path, parent, direct); | ||
}, | ||
builtinModules: [...resolver.getBuiltinModulesList(this)], | ||
dirname: (path) => { | ||
if (typeof path !== 'string') return path; | ||
return resolver.fs.dirname(path); | ||
}, | ||
basename: (path) => { | ||
if (typeof path !== 'string') return path; | ||
return resolver.fs.basename(path); | ||
} | ||
}); | ||
@@ -305,3 +368,3 @@ | ||
if (requireOpts && requireOpts.import) { | ||
if (!customResolver && requireOpts && requireOpts.import) { | ||
if (Array.isArray(requireOpts.import)) { | ||
@@ -319,2 +382,10 @@ for (let i = 0, l = requireOpts.import.length; i < l; i++) { | ||
* @ignore | ||
* @deprecated | ||
*/ | ||
get _resolver() { | ||
return this.resolver; | ||
} | ||
/** | ||
* @ignore | ||
* @deprecated Just call the method yourself like <code>method(args);</code> | ||
@@ -346,8 +417,8 @@ * @param {function} method - Function to invoke. | ||
require(module) { | ||
const path = this._resolver.pathResolve('.'); | ||
const path = this.resolver.fs.resolve('.'); | ||
let mod = this._cacheRequireModule; | ||
if (!mod || mod.path !== path) { | ||
const filename = this._resolver.pathConcat(path, '/vm.js'); | ||
const filename = this.resolver.fs.join(path, '/vm.js'); | ||
mod = new (this._Module)(filename, path); | ||
this._resolver.registerModule(mod, filename, path, null, false); | ||
this.resolver.registerModule(mod, filename, path, null, false); | ||
this._cacheRequireModule = mod; | ||
@@ -407,6 +478,6 @@ } | ||
if (!sandboxModule) { | ||
const resolvedFilename = this._resolver.pathResolve(code.filename); | ||
dirname = this._resolver.pathDirname(resolvedFilename); | ||
const resolvedFilename = this.resolver.fs.resolve(code.filename); | ||
dirname = this.resolver.fs.dirname(resolvedFilename); | ||
sandboxModule = new (this._Module)(resolvedFilename, dirname); | ||
this._resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); | ||
this.resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); | ||
} | ||
@@ -417,6 +488,6 @@ } else { | ||
if (filename) { | ||
const resolvedFilename = this._resolver.pathResolve(filename); | ||
dirname = this._resolver.pathDirname(resolvedFilename); | ||
const resolvedFilename = this.resolver.fs.resolve(filename); | ||
dirname = this.resolver.fs.dirname(resolvedFilename); | ||
sandboxModule = new (this._Module)(resolvedFilename, dirname); | ||
this._resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); | ||
this.resolver.registerModule(sandboxModule, resolvedFilename, dirname, null, false); | ||
} else { | ||
@@ -511,3 +582,3 @@ sandboxModule = new (this._Module)(null, null); | ||
function vm2NestingLoader(resolver, vm, id) { | ||
function vm2NestingLoader(vm) { | ||
if (!cacheMakeNestingScript) { | ||
@@ -514,0 +585,0 @@ cacheMakeNestingScript = compileScript('nesting.js', '(vm, nodevm) => ({VM: vm, NodeVM: nodevm})'); |
'use strict'; | ||
// Translate the old options to the new Resolver functionality. | ||
const fs = require('fs'); | ||
const nmod = require('module'); | ||
const {EventEmitter} = require('events'); | ||
const util = require('util'); | ||
const { | ||
@@ -14,6 +8,6 @@ Resolver, | ||
} = require('./resolver'); | ||
const {VMScript} = require('./script'); | ||
const {VM} = require('./vm'); | ||
const {VMError} = require('./bridge'); | ||
const {DefaultFileSystem} = require('./filesystem'); | ||
const {makeBuiltinsFromLegacyOptions} = require('./builtin'); | ||
const {jsCompiler} = require('./compiler'); | ||
@@ -48,7 +42,62 @@ /** | ||
class LegacyResolver extends DefaultResolver { | ||
class CustomResolver extends DefaultResolver { | ||
constructor(fileSystem, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) { | ||
super(fileSystem, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict); | ||
this.externals = externals; | ||
constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict) { | ||
super(fileSystem, globalPaths, builtinModules); | ||
this.rootPaths = rootPaths; | ||
this.pathContext = pathContext; | ||
this.customResolver = customResolver; | ||
this.hostRequire = hostRequire; | ||
this.compiler = compiler; | ||
this.strict = strict; | ||
} | ||
isPathAllowed(filename) { | ||
return this.rootPaths === undefined || this.rootPaths.some(path => { | ||
if (!filename.startsWith(path)) return false; | ||
const len = path.length; | ||
if (filename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true; | ||
return this.fs.isSeparator(filename[len]); | ||
}); | ||
} | ||
loadJS(vm, mod, filename) { | ||
if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename); | ||
const m = this.hostRequire(filename); | ||
mod.exports = vm.readonly(m); | ||
} | ||
loadNode(vm, mod, filename) { | ||
if (this.pathContext(filename, 'node') !== 'host') return super.loadNode(vm, mod, filename); | ||
const m = this.hostRequire(filename); | ||
mod.exports = vm.readonly(m); | ||
} | ||
customResolve(x, path, extList) { | ||
if (this.customResolver === undefined) return undefined; | ||
const resolved = this.customResolver(x, path); | ||
if (!resolved) return undefined; | ||
if (typeof resolved === 'string') { | ||
return this.loadAsFileOrDirectory(resolved, extList); | ||
} | ||
const {module=x, path: resolvedPath} = resolved; | ||
return this.loadNodeModules(module, [resolvedPath], extList); | ||
} | ||
getCompiler(filename) { | ||
return this.compiler; | ||
} | ||
isStrict(filename) { | ||
return this.strict; | ||
} | ||
} | ||
class LegacyResolver extends CustomResolver { | ||
constructor(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict, externals, allowTransitive) { | ||
super(fileSystem, globalPaths, builtinModules, rootPaths, pathContext, customResolver, hostRequire, compiler, strict); | ||
this.externals = externals.map(makeExternalMatcher); | ||
this.externalCache = externals.map(pattern => new RegExp(makeExternalMatcherRegex(pattern))); | ||
this.currMod = undefined; | ||
@@ -86,19 +135,17 @@ this.trustedMods = new WeakMap(); | ||
resolveFull(mod, x, options, ext, direct) { | ||
resolveFull(mod, x, options, extList, direct) { | ||
this.currMod = undefined; | ||
if (!direct) return super.resolveFull(mod, x, options, ext, false); | ||
if (!direct) return super.resolveFull(mod, x, options, extList, false); | ||
const trustedMod = this.trustedMods.get(mod); | ||
if (!trustedMod || mod.path !== trustedMod.path) return super.resolveFull(mod, x, options, ext, false); | ||
if (!trustedMod || mod.path !== trustedMod.path) return super.resolveFull(mod, x, options, extList, false); | ||
const paths = [...mod.paths]; | ||
if (paths.length === trustedMod.length) { | ||
for (let i = 0; i < paths.length; i++) { | ||
if (paths[i] !== trustedMod.paths[i]) { | ||
return super.resolveFull(mod, x, options, ext, false); | ||
} | ||
if (paths.length !== trustedMod.paths.length) return super.resolveFull(mod, x, options, extList, false); | ||
for (let i = 0; i < paths.length; i++) { | ||
if (paths[i] !== trustedMod.paths[i]) { | ||
return super.resolveFull(mod, x, options, extList, false); | ||
} | ||
} | ||
const extCopy = Object.assign({__proto__: null}, ext); | ||
try { | ||
this.currMod = trustedMod; | ||
return super.resolveFull(trustedMod, x, undefined, extCopy, true); | ||
return super.resolveFull(trustedMod, x, options, extList, true); | ||
} finally { | ||
@@ -117,8 +164,6 @@ this.currMod = undefined; | ||
loadJS(vm, mod, filename) { | ||
filename = this.pathResolve(filename); | ||
this.checkAccess(mod, filename); | ||
if (this.pathContext(filename, 'js') === 'sandbox') { | ||
if (this.pathContext(filename, 'js') !== 'host') { | ||
const trustedMod = this.trustedMods.get(mod); | ||
const script = this.readScript(filename); | ||
vm.run(script, {filename, strict: true, module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path}); | ||
vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: trustedMod ? trustedMod.path : mod.path}); | ||
} else { | ||
@@ -130,154 +175,29 @@ const m = this.hostRequire(filename); | ||
} | ||
function defaultBuiltinLoader(resolver, vm, id) { | ||
const mod = resolver.hostRequire(id); | ||
return vm.readonly(mod); | ||
} | ||
const eventsModules = new WeakMap(); | ||
function defaultBuiltinLoaderEvents(resolver, vm, id) { | ||
return eventsModules.get(vm); | ||
} | ||
let cacheBufferScript; | ||
function defaultBuiltinLoaderBuffer(resolver, vm, id) { | ||
if (!cacheBufferScript) { | ||
cacheBufferScript = new VMScript('return buffer=>({Buffer: buffer});', {__proto__: null, filename: 'buffer.js'}); | ||
} | ||
const makeBuffer = vm.run(cacheBufferScript, {__proto__: null, strict: true, wrapper: 'none'}); | ||
return makeBuffer(Buffer); | ||
} | ||
let cacheUtilScript; | ||
function defaultBuiltinLoaderUtil(resolver, vm, id) { | ||
if (!cacheUtilScript) { | ||
cacheUtilScript = new VMScript(`return function inherits(ctor, superCtor) { | ||
ctor.super_ = superCtor; | ||
Object.setPrototypeOf(ctor.prototype, superCtor.prototype); | ||
}`, {__proto__: null, filename: 'util.js'}); | ||
} | ||
const inherits = vm.run(cacheUtilScript, {__proto__: null, strict: true, wrapper: 'none'}); | ||
const copy = Object.assign({}, util); | ||
copy.inherits = inherits; | ||
return vm.readonly(copy); | ||
} | ||
const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives'))).filter(s=>!s.startsWith('internal/')); | ||
let EventEmitterReferencingAsyncResourceClass = null; | ||
if (EventEmitter.EventEmitterAsyncResource) { | ||
// eslint-disable-next-line global-require | ||
const {AsyncResource} = require('async_hooks'); | ||
const kEventEmitter = Symbol('kEventEmitter'); | ||
class EventEmitterReferencingAsyncResource extends AsyncResource { | ||
constructor(ee, type, options) { | ||
super(type, options); | ||
this[kEventEmitter] = ee; | ||
customResolve(x, path, extList) { | ||
if (this.customResolver === undefined) return undefined; | ||
if (!(this.pathIsAbsolute(x) || this.pathIsRelative(x))) { | ||
if (!this.externalCache.some(regex => regex.test(x))) return undefined; | ||
} | ||
get eventEmitter() { | ||
return this[kEventEmitter]; | ||
const resolved = this.customResolver(x, path); | ||
if (!resolved) return undefined; | ||
if (typeof resolved === 'string') { | ||
this.externals.push(new RegExp('^' + escapeRegExp(resolved))); | ||
return this.loadAsFileOrDirectory(resolved, extList); | ||
} | ||
const {module=x, path: resolvedPath} = resolved; | ||
this.externals.push(new RegExp('^' + escapeRegExp(resolvedPath))); | ||
return this.loadNodeModules(module, [resolvedPath], extList); | ||
} | ||
EventEmitterReferencingAsyncResourceClass = EventEmitterReferencingAsyncResource; | ||
} | ||
let cacheEventsScript; | ||
const SPECIAL_MODULES = { | ||
events(vm) { | ||
if (!cacheEventsScript) { | ||
const eventsSource = fs.readFileSync(`${__dirname}/events.js`, 'utf8'); | ||
cacheEventsScript = new VMScript(`(function (fromhost) { const module = {}; module.exports={};{ ${eventsSource} | ||
} return module.exports;})`, {filename: 'events.js'}); | ||
} | ||
const closure = VM.prototype.run.call(vm, cacheEventsScript); | ||
const eventsInstance = closure(vm.readonly({ | ||
kErrorMonitor: EventEmitter.errorMonitor, | ||
once: EventEmitter.once, | ||
on: EventEmitter.on, | ||
getEventListeners: EventEmitter.getEventListeners, | ||
EventEmitterReferencingAsyncResource: EventEmitterReferencingAsyncResourceClass | ||
})); | ||
eventsModules.set(vm, eventsInstance); | ||
vm._addProtoMapping(EventEmitter.prototype, eventsInstance.EventEmitter.prototype); | ||
return defaultBuiltinLoaderEvents; | ||
}, | ||
buffer(vm) { | ||
return defaultBuiltinLoaderBuffer; | ||
}, | ||
util(vm) { | ||
return defaultBuiltinLoaderUtil; | ||
} | ||
}; | ||
function addDefaultBuiltin(builtins, key, vm) { | ||
if (builtins[key]) return; | ||
const special = SPECIAL_MODULES[key]; | ||
builtins[key] = special ? special(vm) : defaultBuiltinLoader; | ||
} | ||
function genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override) { | ||
const builtins = {__proto__: null}; | ||
if (mockOpt) { | ||
const keys = Object.getOwnPropertyNames(mockOpt); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
builtins[key] = (resolver, tvm, id) => tvm.readonly(mockOpt[key]); | ||
} | ||
} | ||
if (override) { | ||
const keys = Object.getOwnPropertyNames(override); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
builtins[key] = override[key]; | ||
} | ||
} | ||
if (Array.isArray(builtinOpt)) { | ||
const def = builtinOpt.indexOf('*') >= 0; | ||
if (def) { | ||
for (let i = 0; i < BUILTIN_MODULES.length; i++) { | ||
const name = BUILTIN_MODULES[i]; | ||
if (builtinOpt.indexOf(`-${name}`) === -1) { | ||
addDefaultBuiltin(builtins, name, vm); | ||
} | ||
} | ||
} else { | ||
for (let i = 0; i < BUILTIN_MODULES.length; i++) { | ||
const name = BUILTIN_MODULES[i]; | ||
if (builtinOpt.indexOf(name) !== -1) { | ||
addDefaultBuiltin(builtins, name, vm); | ||
} | ||
} | ||
} | ||
} else if (builtinOpt) { | ||
for (let i = 0; i < BUILTIN_MODULES.length; i++) { | ||
const name = BUILTIN_MODULES[i]; | ||
if (builtinOpt[name]) { | ||
addDefaultBuiltin(builtins, name, vm); | ||
} | ||
} | ||
} | ||
return builtins; | ||
} | ||
function defaultCustomResolver() { | ||
return undefined; | ||
} | ||
const DEFAULT_FS = new DefaultFileSystem(); | ||
const DENY_RESOLVER = new Resolver(DEFAULT_FS, {__proto__: null}, [], id => { | ||
throw new VMError(`Access denied to require '${id}'`, 'EDENIED'); | ||
}); | ||
const DENY_RESOLVER = new Resolver(DEFAULT_FS, [], new Map()); | ||
function resolverFromOptions(vm, options, override, compiler) { | ||
function makeResolverFromLegacyOptions(options, override, compiler) { | ||
if (!options) { | ||
if (!override) return DENY_RESOLVER; | ||
const builtins = genBuiltinsFromOptions(vm, undefined, undefined, override); | ||
return new Resolver(DEFAULT_FS, builtins, [], defaultRequire); | ||
const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override); | ||
return new Resolver(DEFAULT_FS, [], builtins); | ||
} | ||
@@ -297,50 +217,18 @@ | ||
const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override); | ||
const builtins = makeBuiltinsFromLegacyOptions(builtinOpt, hostRequire, mockOpt, override); | ||
if (!externalOpt) return new Resolver(fsOpt, builtins, [], hostRequire); | ||
if (!externalOpt) return new Resolver(fsOpt, [], builtins); | ||
let checkPath; | ||
if (rootPaths) { | ||
const checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)); | ||
checkPath = (filename) => { | ||
return checkedRootPaths.some(path => { | ||
if (!filename.startsWith(path)) return false; | ||
const len = path.length; | ||
if (filename.length === len || (len > 0 && fsOpt.isSeparator(path[len-1]))) return true; | ||
return fsOpt.isSeparator(filename[len]); | ||
}); | ||
}; | ||
} else { | ||
checkPath = () => true; | ||
} | ||
if (!compiler) compiler = jsCompiler; | ||
let newCustomResolver = defaultCustomResolver; | ||
let externals = undefined; | ||
let external = undefined; | ||
if (customResolver) { | ||
let externalCache; | ||
newCustomResolver = (resolver, x, path, extList) => { | ||
if (external && !(resolver.pathIsAbsolute(x) || resolver.pathIsRelative(x))) { | ||
if (!externalCache) { | ||
externalCache = external.map(ext => new RegExp(makeExternalMatcherRegex(ext))); | ||
} | ||
if (!externalCache.some(regex => regex.test(x))) return undefined; | ||
} | ||
const resolved = customResolver(x, path); | ||
if (!resolved) return undefined; | ||
if (typeof resolved === 'string') { | ||
if (externals) externals.push(new RegExp('^' + escapeRegExp(resolved))); | ||
return resolver.loadAsFileOrDirectory(resolved, extList); | ||
} | ||
const {module=x, path: resolvedPath} = resolved; | ||
if (externals) externals.push(new RegExp('^' + escapeRegExp(resolvedPath))); | ||
return resolver.loadNodeModules(module, [resolvedPath], extList); | ||
}; | ||
} | ||
const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined; | ||
const pathContext = typeof context === 'function' ? context : (() => context); | ||
if (typeof externalOpt !== 'object') { | ||
return new DefaultResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict); | ||
return new CustomResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict); | ||
} | ||
let transitive = false; | ||
let external = undefined; | ||
if (Array.isArray(externalOpt)) { | ||
@@ -350,8 +238,7 @@ external = externalOpt; | ||
external = externalOpt.modules; | ||
transitive = context === 'sandbox' && externalOpt.transitive; | ||
transitive = context !== 'host' && externalOpt.transitive; | ||
} | ||
externals = external.map(makeExternalMatcher); | ||
return new LegacyResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict, externals, transitive); | ||
return new LegacyResolver(fsOpt, [], builtins, checkedRootPaths, pathContext, customResolver, hostRequire, compiler, strict, external, transitive); | ||
} | ||
exports.resolverFromOptions = resolverFromOptions; | ||
exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions; |
@@ -9,2 +9,3 @@ 'use strict'; | ||
const { VMScript } = require('./script'); | ||
const { jsCopmiler } = require('./compiler'); | ||
@@ -25,7 +26,6 @@ // This should match. Note that '\', '%' are invalid characters | ||
constructor(fs, builtinModules, globalPaths, hostRequire) { | ||
constructor(fs, globalPaths, builtins) { | ||
this.fs = fs; | ||
this.builtinModules = builtinModules; | ||
this.globalPaths = globalPaths; | ||
this.hostRequire = hostRequire; | ||
this.builtins = builtins; | ||
} | ||
@@ -37,6 +37,12 @@ | ||
pathResolve(path) { | ||
return this.fs.resolve(path); | ||
isPathAllowed(path) { | ||
return false; | ||
} | ||
checkAccess(mod, filename) { | ||
if (!this.isPathAllowed(filename)) { | ||
throw new VMError(`Module '${filename}' is not allowed to be required. The path is outside the border!`, 'EDENIED'); | ||
} | ||
} | ||
pathIsRelative(path) { | ||
@@ -54,29 +60,43 @@ if (path === '' || path[0] !== '.') return false; | ||
pathConcat(...paths) { | ||
return this.fs.join(...paths); | ||
lookupPaths(mod, id) { | ||
if (this.pathIsRelative(id)) return [mod.path || '.']; | ||
return [...mod.paths, ...this.globalPaths]; | ||
} | ||
pathBasename(path) { | ||
return this.fs.basename(path); | ||
getBuiltinModulesList(vm) { | ||
if (this.builtins === undefined) return []; | ||
const res = []; | ||
this.builtins.forEach((value, key) => { | ||
if (typeof value === 'object') value.init(vm); | ||
res.push(key); | ||
}); | ||
return res; | ||
} | ||
pathDirname(path) { | ||
return this.fs.dirname(path); | ||
loadBuiltinModule(vm, id) { | ||
if (this.builtins === undefined) return undefined; | ||
const builtin = this.builtins.get(id); | ||
if (!builtin) return undefined; | ||
if (typeof builtin === 'function') return builtin(vm); | ||
return builtin.load(vm); | ||
} | ||
lookupPaths(mod, id) { | ||
if (typeof id === 'string') throw new Error('Id is not a string'); | ||
if (this.pathIsRelative(id)) return [mod.path || '.']; | ||
return [...mod.paths, ...this.globalPaths]; | ||
makeExtensionHandler(vm, name) { | ||
return (mod, filename) => { | ||
filename = this.fs.resolve(filename); | ||
this.checkAccess(mod, filename); | ||
this[name](vm, mod, filename); | ||
}; | ||
} | ||
getBuiltinModulesList() { | ||
return Object.getOwnPropertyNames(this.builtinModules); | ||
getExtensions(vm) { | ||
return { | ||
// eslint-disable-next-line quote-props | ||
__proto__: null, | ||
'.js': this.makeExtensionHandler(vm, 'loadJS'), | ||
'.json': this.makeExtensionHandler(vm, 'loadJSON'), | ||
' .node': this.makeExtensionHandler(vm, 'loadNode'), | ||
}; | ||
} | ||
loadBuiltinModule(vm, id) { | ||
const handler = this.builtinModules[id]; | ||
return handler && handler(this, vm, id); | ||
} | ||
loadJS(vm, mod, filename) { | ||
@@ -98,6 +118,4 @@ throw new VMError(`Access denied to require '${filename}'`, 'EDENIED'); | ||
resolve(mod, x, options, ext, direct) { | ||
if (typeof x !== 'string') throw new Error('Id is not a string'); | ||
if (x.startsWith('node:') || this.builtinModules[x]) { | ||
resolve(mod, x, options, extList, direct) { | ||
if (x.startsWith('node:') || this.builtins.has(x)) { | ||
// a. return the core module | ||
@@ -108,6 +126,6 @@ // b. STOP | ||
return this.resolveFull(mod, x, options, ext, direct); | ||
return this.resolveFull(mod, x, options, extList, direct); | ||
} | ||
resolveFull(mod, x, options, ext, direct) { | ||
resolveFull(mod, x, options, extList, direct) { | ||
// 7. THROW "not found" | ||
@@ -125,3 +143,3 @@ throw new VMError(`Cannot find module '${x}'`, 'ENOTFOUND'); | ||
while (true) { | ||
const name = this.pathBasename(path); | ||
const name = this.fs.basename(path); | ||
// a. if PARTS[I] = "node_modules" CONTINUE | ||
@@ -131,5 +149,5 @@ if (name !== 'node_modules') { | ||
// c. DIRS = DIR + DIRS // Note: this seems wrong. Should be DIRS + DIR | ||
dirs.push(this.pathConcat(path, 'node_modules')); | ||
dirs.push(this.fs.join(path, 'node_modules')); | ||
} | ||
const dir = this.pathDirname(path); | ||
const dir = this.fs.dirname(path); | ||
if (dir == path) break; | ||
@@ -147,50 +165,49 @@ // d. let I = I - 1 | ||
class DefaultResolver extends Resolver { | ||
constructor(fs, builtinModules, checkPath, globalPaths, pathContext, customResolver, hostRequire, compiler, strict) { | ||
super(fs, builtinModules, globalPaths, hostRequire); | ||
this.checkPath = checkPath; | ||
this.pathContext = pathContext; | ||
this.customResolver = customResolver; | ||
this.compiler = compiler; | ||
this.strict = strict; | ||
this.packageCache = {__proto__: null}; | ||
this.scriptCache = {__proto__: null}; | ||
function pathTestIsDirectory(fs, path) { | ||
try { | ||
const stat = fs.statSync(path, {__proto__: null, throwIfNoEntry: false}); | ||
return stat && stat.isDirectory(); | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
isPathAllowed(path) { | ||
return this.checkPath(path); | ||
function pathTestIsFile(fs, path) { | ||
try { | ||
const stat = fs.statSync(path, {__proto__: null, throwIfNoEntry: false}); | ||
return stat && stat.isFile(); | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
pathTestIsDirectory(path) { | ||
try { | ||
const stat = this.fs.statSync(path, {__proto__: null, throwIfNoEntry: false}); | ||
return stat && stat.isDirectory(); | ||
} catch (e) { | ||
return false; | ||
} | ||
} | ||
function readFile(fs, path) { | ||
return fs.readFileSync(path, {encoding: 'utf8'}); | ||
} | ||
pathTestIsFile(path) { | ||
try { | ||
const stat = this.fs.statSync(path, {__proto__: null, throwIfNoEntry: false}); | ||
return stat && stat.isFile(); | ||
} catch (e) { | ||
return false; | ||
} | ||
function readFileWhenExists(fs, path) { | ||
return pathTestIsFile(fs, path) ? readFile(fs, path) : undefined; | ||
} | ||
class DefaultResolver extends Resolver { | ||
constructor(fs, globalPaths, builtins) { | ||
super(fs, globalPaths, builtins); | ||
this.packageCache = new Map(); | ||
this.scriptCache = new Map(); | ||
} | ||
readFile(path) { | ||
return this.fs.readFileSync(path, {encoding: 'utf8'}); | ||
getCompiler(filename) { | ||
return jsCopmiler; | ||
} | ||
readFileWhenExists(path) { | ||
return this.pathTestIsFile(path) ? this.readFile(path) : undefined; | ||
isStrict(filename) { | ||
return true; | ||
} | ||
readScript(filename) { | ||
let script = this.scriptCache[filename]; | ||
let script = this.scriptCache.get(filename); | ||
if (!script) { | ||
script = new VMScript(this.readFile(filename), {filename, compiler: this.compiler}); | ||
this.scriptCache[filename] = script; | ||
script = new VMScript(readFile(this.fs, filename), {filename, compiler: this.getCompiler(filename)}); | ||
this.scriptCache.set(filename, script); | ||
} | ||
@@ -200,14 +217,8 @@ return script; | ||
checkAccess(mod, filename) { | ||
if (!this.isPathAllowed(filename)) { | ||
throw new VMError(`Module '${filename}' is not allowed to be required. The path is outside the border!`, 'EDENIED'); | ||
} | ||
} | ||
loadJS(vm, mod, filename) { | ||
filename = this.pathResolve(filename); | ||
this.checkAccess(mod, filename); | ||
if (this.pathContext(filename, 'js') === 'sandbox') { | ||
if (this.pathContext(filename, 'js') !== 'host') { | ||
const script = this.readScript(filename); | ||
vm.run(script, {filename, strict: this.strict, module: mod, wrapper: 'none', dirname: mod.path}); | ||
vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: mod.path}); | ||
} else { | ||
@@ -220,5 +231,3 @@ const m = this.hostRequire(filename); | ||
loadJSON(vm, mod, filename) { | ||
filename = this.pathResolve(filename); | ||
this.checkAccess(mod, filename); | ||
const json = this.readFile(filename); | ||
const json = readFile(this.fs, filename); | ||
mod.exports = vm._jsonParse(json); | ||
@@ -230,3 +239,3 @@ } | ||
this.checkAccess(mod, filename); | ||
if (this.pathContext(filename, 'node') === 'sandbox') throw new VMError('Native modules can be required only with context set to \'host\'.'); | ||
if (this.pathContext(filename, 'node') !== 'host') throw new VMError('Native modules can be required only with context set to \'host\'.'); | ||
const m = this.hostRequire(filename); | ||
@@ -236,7 +245,10 @@ mod.exports = vm.readonly(m); | ||
customResolve(x, path, extList) { | ||
return undefined; | ||
} | ||
// require(X) from module at path Y | ||
resolveFull(mod, x, options, ext, direct) { | ||
resolveFull(mod, x, options, extList, direct) { | ||
// Note: core module handled by caller | ||
const extList = Object.getOwnPropertyNames(ext); | ||
const path = mod.path || '.'; | ||
@@ -272,3 +284,3 @@ | ||
// b. LOAD_AS_DIRECTORY(Y + X) | ||
f = this.loadAsFileOrDirectory(this.pathConcat(paths[i], x), extList); | ||
f = this.loadAsFileOrDirectory(this.fs.join(paths[i], x), extList); | ||
if (f) return f; | ||
@@ -279,3 +291,3 @@ } | ||
// b. LOAD_AS_DIRECTORY(Y + X) | ||
f = this.loadAsFileOrDirectory(this.pathConcat(path, x), extList); | ||
f = this.loadAsFileOrDirectory(this.fs.join(path, x), extList); | ||
if (f) return f; | ||
@@ -288,3 +300,3 @@ } else { | ||
// b. LOAD_AS_DIRECTORY(Y + X) | ||
f = this.loadAsFileOrDirectory(this.pathConcat(path, x), extList); | ||
f = this.loadAsFileOrDirectory(this.fs.join(path, x), extList); | ||
if (f) return f; | ||
@@ -328,6 +340,6 @@ } | ||
f = this.customResolver(this, x, path, extList); | ||
f = this.customResolve(x, path, extList); | ||
if (f) return f; | ||
return super.resolveFull(mod, x, options, ext, direct); | ||
return super.resolveFull(mod, x, options, extList, direct); | ||
} | ||
@@ -344,4 +356,4 @@ | ||
tryFile(x) { | ||
x = this.pathResolve(x); | ||
return this.isPathAllowed(x) && this.pathTestIsFile(x) ? x : undefined; | ||
x = this.fs.resolve(x); | ||
return this.isPathAllowed(x) && pathTestIsFile(this.fs, x) ? x : undefined; | ||
} | ||
@@ -352,3 +364,3 @@ | ||
const ext = extList[i]; | ||
if (ext !== this.pathBasename(ext)) continue; | ||
if (ext !== this.fs.basename(ext)) continue; | ||
const f = this.tryFile(x + ext); | ||
@@ -361,11 +373,11 @@ if (f) return f; | ||
readPackage(path) { | ||
const packagePath = this.pathResolve(this.pathConcat(path, 'package.json')); | ||
const packagePath = this.fs.resolve(this.fs.join(path, 'package.json')); | ||
const cache = this.packageCache[packagePath]; | ||
const cache = this.packageCache.get(packagePath); | ||
if (cache !== undefined) return cache; | ||
if (!this.isPathAllowed(packagePath)) return undefined; | ||
const content = this.readFileWhenExists(packagePath); | ||
const content = readFileWhenExists(this.fs, packagePath); | ||
if (!content) { | ||
this.packageCache[packagePath] = false; | ||
this.packageCache.set(packagePath, false); | ||
return false; | ||
@@ -390,3 +402,3 @@ } | ||
}; | ||
this.packageCache[packagePath] = filtered; | ||
this.packageCache.set(packagePath, filtered); | ||
return filtered; | ||
@@ -397,5 +409,5 @@ } | ||
while (true) { | ||
const dir = this.pathDirname(path); | ||
const dir = this.fs.dirname(path); | ||
if (dir === path) break; | ||
const basename = this.pathBasename(dir); | ||
const basename = this.fs.basename(dir); | ||
if (basename === 'node_modules') break; | ||
@@ -425,3 +437,3 @@ const pack = this.readPackage(dir); | ||
// 3. If X/index.node is a file, load X/index.node as binary addon. STOP | ||
return this.tryWithExtension(this.pathConcat(x, 'index'), extList); | ||
return this.tryWithExtension(this.fs.join(x, 'index'), extList); | ||
} | ||
@@ -438,3 +450,3 @@ | ||
// c. let M = X + (json main field) | ||
const m = this.pathConcat(x, pack.main); | ||
const m = this.fs.join(x, pack.main); | ||
// d. LOAD_AS_FILE(M) | ||
@@ -534,3 +546,3 @@ let f = this.loadAsFile(m, extList); | ||
if (!res) return undefined; | ||
const scope = this.pathConcat(dir, res[1]); | ||
const scope = this.fs.join(dir, res[1]); | ||
const pack = this.readPackage(scope); | ||
@@ -735,3 +747,3 @@ if (!pack) return undefined; | ||
// b. Return PACKAGE_RESOLVE(target + subpath, packageURL + "/"). | ||
return this.packageResolve(this.pathConcat(target, subpath), packageURL, conditions, extList); | ||
return this.packageResolve(this.fs.join(target, subpath), packageURL, conditions, extList); | ||
} | ||
@@ -749,3 +761,3 @@ } | ||
// d. Let resolvedTarget be the URL resolution of the concatenation of packageURL and target. | ||
const resolvedTarget = this.pathConcat(packageURL, target); | ||
const resolvedTarget = this.fs.join(packageURL, target); | ||
// e. Assert: resolvedTarget is contained in packageURL. | ||
@@ -765,3 +777,3 @@ subpath = decodeURI(subpath); | ||
// 1. Return the URL resolution of the concatenation of subpath and resolvedTarget. | ||
return this.pathConcat(resolvedTarget, subpath); | ||
return this.fs.join(resolvedTarget, subpath); | ||
// 3. Otherwise, if target is an Array, then | ||
@@ -837,3 +849,3 @@ } else if (Array.isArray(target)) { | ||
// 3. If packageSpecifier is a Node.js builtin module name, then | ||
if (this.builtinModules[packageSpecifier]) { | ||
if (this.builtins.has(packageSpecifier)) { | ||
// a. Return the string "node:" concatenated with packageSpecifier. | ||
@@ -877,7 +889,7 @@ return 'node:' + packageSpecifier; | ||
// a. Let packageURL be the URL resolution of "node_modules/" concatenated with packageSpecifier, relative to parentURL. | ||
packageURL = this.pathResolve(this.pathConcat(parentURL, 'node_modules', packageSpecifier)); | ||
packageURL = this.fs.resolve(this.fs.join(parentURL, 'node_modules', packageSpecifier)); | ||
// b. Set parentURL to the parent folder URL of parentURL. | ||
const parentParentURL = this.pathDirname(parentURL); | ||
const parentParentURL = this.fs.dirname(parentURL); | ||
// c. If the folder at packageURL does not exist, then | ||
if (this.isPathAllowed(packageURL) && this.pathTestIsDirectory(packageURL)) break; | ||
if (this.isPathAllowed(packageURL) && pathTestIsDirectory(this.fs, packageURL)) break; | ||
// 1. Continue the next loop iteration. | ||
@@ -905,3 +917,3 @@ if (parentParentURL === parentURL) { | ||
// 1. Return the URL resolution of packageSubpath in packageURL. | ||
return this.pathConcat(packageURL, packageSubpath); | ||
return this.fs.join(packageURL, packageSubpath); | ||
} | ||
@@ -908,0 +920,0 @@ |
@@ -40,5 +40,13 @@ /* global host, data, VMError */ | ||
console: optionConsole, | ||
vm, | ||
resolver, | ||
extensions | ||
extensions, | ||
emitArgs, | ||
globalPaths, | ||
getLookupPathsFor, | ||
resolve: resolve0, | ||
lookupPaths, | ||
loadBuiltinModule, | ||
registerModule, | ||
builtinModules, | ||
dirname, | ||
basename | ||
} = data; | ||
@@ -50,4 +58,2 @@ | ||
const globalPaths = ensureSandboxArray(resolver.globalPaths); | ||
class Module { | ||
@@ -61,3 +67,3 @@ | ||
this.loaded = false; | ||
this.paths = path ? ensureSandboxArray(resolver.genLookupPaths(path)) : []; | ||
this.paths = path ? ensureSandboxArray(getLookupPathsFor(path)) : []; | ||
this.children = []; | ||
@@ -87,3 +93,3 @@ this.exports = {}; | ||
} | ||
const filename = resolver.resolve(mod, id, undefined, Module._extensions, direct); | ||
const filename = resolve0(mod, id, undefined, Module._extensions, direct); | ||
if (localStringPrototypeStartsWith(filename, 'node:')) { | ||
@@ -93,3 +99,3 @@ id = localStringPrototypeSlice(filename, 5); | ||
if (!nmod) { | ||
nmod = resolver.loadBuiltinModule(vm, id); | ||
nmod = loadBuiltinModule(id); | ||
if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND'); | ||
@@ -109,3 +115,3 @@ cacheBuiltins[id] = nmod; | ||
if (nmod) return nmod; | ||
nmod = resolver.loadBuiltinModule(vm, id); | ||
nmod = loadBuiltinModule(id); | ||
if (nmod) { | ||
@@ -116,5 +122,5 @@ cacheBuiltins[id] = nmod; | ||
const path = resolver.fs.dirname(filename); | ||
const path = dirname(filename); | ||
const module = new Module(filename, path, mod); | ||
resolver.registerModule(module, filename, path, mod, direct); | ||
registerModule(module, filename, path, mod, direct); | ||
mod._updateChildren(module, true); | ||
@@ -141,4 +147,4 @@ try { | ||
Module.builtinModules = ensureSandboxArray(resolver.getBuiltinModulesList()); | ||
Module.globalPaths = globalPaths; | ||
Module.builtinModules = ensureSandboxArray(builtinModules); | ||
Module.globalPaths = ensureSandboxArray(globalPaths); | ||
Module._extensions = {__proto__: null}; | ||
@@ -157,3 +163,3 @@ Module._cache = {__proto__: null}; | ||
function findBestExtensionHandler(filename) { | ||
const name = resolver.fs.basename(filename); | ||
const name = basename(filename); | ||
for (let i = 0; (i = localStringPrototypeIndexOf(name, '.', i + 1)) !== -1;) { | ||
@@ -177,7 +183,7 @@ const ext = localStringPrototypeSlice(name, i); | ||
function resolve(id, options) { | ||
return resolver.resolve(mod, id, options, Module._extensions, true); | ||
return resolve0(mod, id, options, Module._extensions, true); | ||
} | ||
require.resolve = resolve; | ||
function paths(id) { | ||
return ensureSandboxArray(resolver.lookupPaths(mod, id)); | ||
return ensureSandboxArray(lookupPaths(mod, id)); | ||
} | ||
@@ -290,16 +296,2 @@ resolve.paths = paths; | ||
function vmEmitArgs(event, args) { | ||
const allargs = [event]; | ||
for (let i = 0; i < args.length; i++) { | ||
if (!localReflectDefineProperty(allargs, i + 1, { | ||
__proto__: null, | ||
value: args[i], | ||
writable: true, | ||
enumerable: true, | ||
configurable: true | ||
})) throw new LocalError('Unexpected'); | ||
} | ||
return localReflectApply(vm.emit, vm, allargs); | ||
} | ||
const LISTENERS = new LocalWeakMap(); | ||
@@ -449,18 +441,18 @@ const LISTENER_HANDLER = new LocalWeakMap(); | ||
debug(...args) { | ||
vmEmitArgs('console.debug', args); | ||
emitArgs('console.debug', args); | ||
}, | ||
log(...args) { | ||
vmEmitArgs('console.log', args); | ||
emitArgs('console.log', args); | ||
}, | ||
info(...args) { | ||
vmEmitArgs('console.info', args); | ||
emitArgs('console.info', args); | ||
}, | ||
warn(...args) { | ||
vmEmitArgs('console.warn', args); | ||
emitArgs('console.warn', args); | ||
}, | ||
error(...args) { | ||
vmEmitArgs('console.error', args); | ||
emitArgs('console.error', args); | ||
}, | ||
dir(...args) { | ||
vmEmitArgs('console.dir', args); | ||
emitArgs('console.dir', args); | ||
}, | ||
@@ -470,3 +462,3 @@ time() {}, | ||
trace(...args) { | ||
vmEmitArgs('console.trace', args); | ||
emitArgs('console.trace', args); | ||
} | ||
@@ -473,0 +465,0 @@ }; |
@@ -24,2 +24,3 @@ /* global host, bridge, data, context */ | ||
apply: localReflectApply, | ||
construct: localReflectConstruct, | ||
deleteProperty: localReflectDeleteProperty, | ||
@@ -436,2 +437,59 @@ has: localReflectHas, | ||
function makeSafeHandlerArgs(args) { | ||
const sArgs = ensureThis(args); | ||
if (sArgs === args) return args; | ||
const a = []; | ||
for (let i=0; i < sArgs.length; i++) { | ||
localReflectDefineProperty(a, i, { | ||
__proto__: null, | ||
value: sArgs[i], | ||
enumerable: true, | ||
configurable: true, | ||
writable: true | ||
}); | ||
} | ||
return a; | ||
} | ||
const makeSafeArgs = Object.freeze({ | ||
__proto__: null, | ||
apply(target, thiz, args) { | ||
return localReflectApply(target, thiz, makeSafeHandlerArgs(args)); | ||
}, | ||
construct(target, args, newTarget) { | ||
return localReflectConstruct(target, makeSafeHandlerArgs(args), newTarget); | ||
} | ||
}); | ||
const proxyHandlerHandler = Object.freeze({ | ||
__proto__: null, | ||
get(target, name, receiver) { | ||
const value = target.handler[name]; | ||
if (typeof value !== 'function') return value; | ||
return new LocalProxy(value, makeSafeArgs); | ||
} | ||
}); | ||
function wrapProxyHandler(args) { | ||
if (args.length < 2) return args; | ||
const handler = args[1]; | ||
args[1] = new LocalProxy({__proto__: null, handler}, proxyHandlerHandler); | ||
return args; | ||
} | ||
const proxyHandler = Object.freeze({ | ||
__proto__: null, | ||
apply(target, thiz, args) { | ||
return localReflectApply(target, thiz, wrapProxyHandler(args)); | ||
}, | ||
construct(target, args, newTarget) { | ||
return localReflectConstruct(target, wrapProxyHandler(args), newTarget); | ||
} | ||
}); | ||
const proxiedProxy = new LocalProxy(LocalProxy, proxyHandler); | ||
overrideWithProxy(LocalProxy, 'revocable', LocalProxy.revocable, proxyHandler); | ||
global.Proxy = proxiedProxy; | ||
global.Function = proxiedFunction; | ||
@@ -438,0 +496,0 @@ global.eval = new LocalProxy(localEval, EvalHandler); |
@@ -47,2 +47,5 @@ 'use strict'; | ||
} = require('./script'); | ||
const { | ||
inspect | ||
} = require('util'); | ||
@@ -369,2 +372,4 @@ const objectDefineProperties = Object.defineProperties; | ||
this.readonly(inspect); | ||
// prepare global sandbox | ||
@@ -371,0 +376,0 @@ if (sandbox) { |
@@ -16,3 +16,3 @@ { | ||
], | ||
"version": "3.9.17", | ||
"version": "3.9.18", | ||
"main": "index.js", | ||
@@ -19,0 +19,0 @@ "sideEffects": false, |
@@ -137,3 +137,3 @@ # vm2 [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Package Quality][quality-image]][quality-url] [![Node.js CI](https://github.com/patriksimek/vm2/actions/workflows/node-test.yml/badge.svg)](https://github.com/patriksimek/vm2/actions/workflows/node-test.yml) [![Known Vulnerabilities][snyk-image]][snyk-url] | ||
* `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`). | ||
* `require` - `true` or object to enable `require` method (default: `false`). | ||
* `require` - `true`, an object or a Resolver to enable `require` method (default: `false`). | ||
* `require.external` - Values can be `true`, an array of allowed external modules, or an object (default: `false`). All paths matching `/node_modules/${any_allowed_external_module}/(?!/node_modules/)` are allowed to be required. | ||
@@ -145,3 +145,3 @@ * `require.external.modules` - Array of allowed external modules. Also supports wildcards, so specifying `['@scope/*-ver-??]`, for instance, will allow using all modules having a name of the form `@scope/something-ver-aa`, `@scope/other-ver-11`, etc. The `*` wildcard does not match path separators. | ||
* `require.mock` - Collection of mock modules (both external or built-in). | ||
* `require.context` - `host` (default) to require modules in the host and proxy them into the sandbox. `sandbox` to load, compile, and require modules in the sandbox. Except for `events`, built-in modules are always required in the host and proxied into the sandbox. | ||
* `require.context` - `host` (default) to require modules in the host and proxy them into the sandbox. `sandbox` to load, compile, and require modules in the sandbox. `callback(moduleFilename, ext)` to dynamically choose a context per module. The default will be sandbox is nothing is specified. Except for `events`, built-in modules are always required in the host and proxied into the sandbox. | ||
* `require.import` - An array of modules to be loaded into NodeVM on start. | ||
@@ -216,2 +216,24 @@ * `require.resolve` - An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. | ||
### Resolver | ||
A resolver can be created via `makeResolverFromLegacyOptions` and be used for multiple `NodeVM` instances allowing to share compiled module code potentially speeding up load times. The first example of `NodeVM` can be rewritten using `makeResolverFromLegacyOptions` as follows. | ||
```js | ||
const resolver = makeResolverFromLegacyOptions({ | ||
external: true, | ||
builtin: ['fs', 'path'], | ||
root: './', | ||
mock: { | ||
fs: { | ||
readFileSync: () => 'Nice try!' | ||
} | ||
} | ||
}); | ||
const vm = new NodeVM({ | ||
console: 'inherit', | ||
sandbox: {}, | ||
require: resolver | ||
}); | ||
``` | ||
## VMScript | ||
@@ -218,0 +240,0 @@ |
221272
24
5804
433