From 9e2b60511e83e38d996c00ac1aaeb2adea54e3b8 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Sat, 8 Apr 2023 17:10:23 +0200 Subject: [PATCH 1/3] Make resolver API public --- index.d.ts | 95 +++++++++++- lib/builtin.js | 147 ++++++++++++++++++ lib/nodevm.js | 126 +++++++++++----- lib/resolver-compat.js | 307 ++++++++++++-------------------------- lib/resolver.js | 235 ++++++++++++++--------------- lib/setup-node-sandbox.js | 66 ++++---- package-lock.json | 4 +- 7 files changed, 573 insertions(+), 407 deletions(-) create mode 100644 lib/builtin.js diff --git a/index.d.ts b/index.d.ts index ceb1c0d..1640f88 100644 --- a/index.d.ts +++ b/index.d.ts @@ -59,6 +59,87 @@ export class VMFileSystem implements VMFileSystemInterface { isSeparator(char: string): boolean; } +export type BuiltinLoad = (vm: NodeVM) => any; +export type Builtin = BuiltinLoad | {init: (vm: NodeVM)=>void, load: BuiltinLoad}; +export type Builtins = Map; +export type HostRequire = (id: string) => any; +export type JSONValue = null | boolean | number | string | readonly JSONValue[] | {[key: string]: JSONValue}; +export interface Package { + name: JSONValue, + main: JSONValue, + exports: JSONValue, + imports: JSONValue, + type: JSONValue +}; + +export function makeBuiltins(builtins: string[], hostRequire: HostRequire): Builtins; +export function makeBuiltinsFromLegacyOptions(builtins: string[], hostRequire: HostRequire, mocks?: {[key: string]: any}, overrides?: {[key: string]: Builtin}): Builtins; +export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver; + +export abstract class Resolver { + constructor(readonly fs: VMFileSystemInterface, readonly globalPaths: readonly string[], readonly builtins: Builtins); + init(vm: NodeVM): void; + abstract isPathAllowed(path: string): boolean; + checkAccess(mod: any, filename: string): void; + pathIsRelative(path: string): boolean; + pathIsAbsolute(path: string): boolean; + lookupPaths(mod: any, id: string): readonly string[]; + getBuiltinModulesList(vm: NodeVM): readonly string[]; + loadBuiltinModule(vm: NodeVM, id: string): any; + makeExtensionHandler(vm: NodeVM, name: string): (mod: any, filename: string) => void; + getExtensions(vm: NodeVM): {[key: string]: (mod: any, filename: string) => void}; + loadJS(vm: NodeVM, mod: any, filename: string): void; + loadJSON(vm: NodeVM, mod: any, filename: string): void; + loadNode(vm: NodeVM, mod: any, filename: string): void; + registerModule(mod: any, filename: string, path: string, parent: any, direct: boolean): void; + resolve(mod: any, id: string, options: {paths?: readonly string[], unsafeOptions: any}, extList: readonly string[], direct: boolean): string; + resolveFull(mod: any, id: string, options: {paths?: readonly string[], unsafeOptions: any}, extList: readonly string[], direct: boolean): string; + genLookupPaths(path: string): readonly string[]; +} + +export abstract class DefaultResolver extends Resolver { + private packageCache: Map; + private scriptCache: Map; + constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Builtins); + getCompiler(filename: string): CompilerFunction; + isStrict(filename: string): boolean; + readScript(filename: string): string; + customResolve(id: string, path: string, extList: readonly string[]): string | undefined; + loadAsFileOrDirectory(x: string, extList: readonly string[]): string | undefined; + tryFile(x: string): string | undefined; + tryWithExtension(x: string, extList: readonly string[]): string | undefined; + readPackage(path: string): Package | undefined; + readPackageScope(path: string): {data?: Package, scope?: string}; + // LOAD_AS_FILE(X) + loadAsFile(x: string, extList: readonly string[]): string | undefined; + // LOAD_INDEX(X) + loadIndex(x: string, extList: readonly string[]): string | undefined; + // LOAD_AS_DIRECTORY(X) + loadAsPackage(x: string, pack: Package | undefined, extList: readonly string[]): string | undefined; + // LOAD_AS_DIRECTORY(X) + loadAsDirectory(x: string, extList: readonly string[]): string | undefined; + // LOAD_NODE_MODULES(X, START) + loadNodeModules(x: string, dirs: readonly string[], extList: readonly string[]): string | undefined; + // LOAD_PACKAGE_IMPORTS(X, DIR) + loadPackageImports(x: string, dir: string, extList: readonly string[]): string | undefined; + // LOAD_PACKAGE_EXPORTS(X, DIR) + loadPackageExports(x: string, dir: string, extList: readonly string[]): string | undefined; + // LOAD_PACKAGE_SELF(X, DIR) + loadPackageSelf(x: string, dir: string, extList: readonly string[]): string | undefined; + // RESOLVE_ESM_MATCH(MATCH) + resolveEsmMatch(match: string, x: string, extList: readonly string[]): string; + // PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions) + packageExportsResolve(packageURL: string, subpath: string, rexports: JSONValue, conditions: readonly string[], extList: readonly string[]): string; + // PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions) + packageImportsExportsResolve(matchKey: string, matchObj: {[key: string]: JSONValue}, packageURL: string, isImports: boolean, conditions: readonly string[], extList: readonly string[]): string | undefined | null; + // PATTERN_KEY_COMPARE(keyA, keyB) + patternKeyCompare(keyA: string, keyB: string): number; + // PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions) + packageTargetResolve(packageURL: string, target: JSONValue, subpath: string, pattern: boolean, internal: boolean, conditions: readonly string[], extList: readonly string[]): string | undefined | null; + // PACKAGE_RESOLVE(packageSpecifier, parentURL) + packageResolve(packageSpecifier: string, parentURL: string, conditions: readonly string[], extList: readonly string[]): string; +} + /** * Require options for a VM */ @@ -67,18 +148,18 @@ export interface VMRequire { * Array of allowed built-in modules, accepts ["*"] for all. Using "*" increases the attack surface and potential * new modules allow to escape the sandbox. (default: none) */ - 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 */ context?: "host" | "sandbox"; /** `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). */ mock?: any; /* An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. */ @@ -141,7 +222,7 @@ export interface NodeVMOptions extends VMOptions { /** `inherit` to enable console, `redirect` to redirect to events, `off` to disable console (default: `inherit`). */ console?: "inherit" | "redirect" | "off"; /** `true` or an object to enable `require` options (default: `false`). */ - require?: boolean | VMRequire; + require?: boolean | VMRequire | Resolver; /** * **WARNING**: This should be disabled. It allows to create a NodeVM form within the sandbox which could return any host module. * `true` to enable VMs nesting (default: `false`). @@ -150,7 +231,7 @@ export interface NodeVMOptions extends VMOptions { /** `commonjs` (default) to wrap script into CommonJS wrapper, `none` to retrieve value returned by the script. */ wrapper?: "commonjs" | "none"; /** File extensions that the internal module resolver should accept. */ - sourceExtensions?: string[]; + sourceExtensions?: readonly string[]; /** * Array of arguments passed to `process.argv`. * This object will not be copied and the script can change this object. @@ -224,6 +305,8 @@ export class NodeVM extends EventEmitter implements VM { readonly sandbox: any; /** Only here because of implements VM. Does nothing. */ timeout?: number; + /** The resolver used to resolve modules */ + readonly resolver: Resolver; /** Runs the code */ run(js: string | VMScript, options?: string | { filename?: string, wrapper?: "commonjs" | "none", strict?: boolean }): any; /** Runs the code in the specific file */ diff --git a/lib/builtin.js b/lib/builtin.js new file mode 100644 index 0000000..499e1f6 --- /dev/null +++ b/lib/builtin.js @@ -0,0 +1,147 @@ + +const fs = require('fs'); +const nmod = require('module'); +const {EventEmitter} = require('events'); +const util = require('util'); +const {VMScript} = require('./script'); +const {VM} = require('./vm'); + +const eventsModules = new WeakMap(); + +function defaultBuiltinLoaderEvents(vm) { + return eventsModules.get(vm); +} + +let cacheBufferScript; + +function defaultBuiltinLoaderBuffer(vm) { + 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(vm) { + 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; + } + get eventEmitter() { + return this[kEventEmitter]; + } + } + EventEmitterReferencingAsyncResourceClass = EventEmitterReferencingAsyncResource; +} + +let cacheEventsScript; + +const SPECIAL_MODULES = { + events: { + init(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); + }, + load: defaultBuiltinLoaderEvents + }, + buffer: defaultBuiltinLoaderBuffer, + util: defaultBuiltinLoaderUtil +}; + +function addDefaultBuiltin(builtins, key, hostRequire) { + if (builtins.has(key)) return; + const special = SPECIAL_MODULES[key]; + builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key))); +} + + +function makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) { + const res = new Map(); + if (mocks) { + const keys = Object.getOwnPropertyNames(mocks); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + res.set(key, (tvm) => tvm.readonly(mocks[key])); + } + } + if (overrides) { + const keys = Object.getOwnPropertyNames(overrides); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + res.set(key, overrides[key]); + } + } + if (Array.isArray(builtins)) { + const def = builtins.indexOf('*') >= 0; + if (def) { + for (let i = 0; i < BUILTIN_MODULES.length; i++) { + const name = BUILTIN_MODULES[i]; + if (builtins.indexOf(`-${name}`) === -1) { + addDefaultBuiltin(res, name, hostRequire); + } + } + } else { + for (let i = 0; i < BUILTIN_MODULES.length; i++) { + const name = BUILTIN_MODULES[i]; + if (builtins.indexOf(name) !== -1) { + addDefaultBuiltin(res, name, hostRequire); + } + } + } + } else if (builtins) { + for (let i = 0; i < BUILTIN_MODULES.length; i++) { + const name = BUILTIN_MODULES[i]; + if (builtins[name]) { + addDefaultBuiltin(res, name, hostRequire); + } + } + } + return res; +} + +function makeBuiltins(builtins, hostRequire) { + const res = new Map(); + for (let i = 0; i < builtins.length; i++) { + const name = builtins[i]; + addDefaultBuiltin(res, name, hostRequire); + } + return res; +} + +exports.makeBuiltinsFromLegacyOptions = makeBuiltinsFromLegacyOptions; +exports.makeBuiltins = makeBuiltins; diff --git a/lib/nodevm.js b/lib/nodevm.js index a55a0e2..c40a68a 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -38,8 +38,9 @@ const { VM } = require('./vm'); const { - resolverFromOptions + makeResolverFromLegacyOptions } = require('./resolver-compat'); +const { Resolver } = require('./resolver'); const objectDefineProperty = Object.defineProperty; const objectDefineProperties = Object.defineProperties; @@ -86,6 +87,36 @@ const NESTING_OVERRIDE = Object.freeze({ vm2: vm2NestingLoader }); +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) + }; +} + /** * Event caused by a console.debug call if options.console="redirect" is specified. * @@ -168,7 +199,7 @@ class NodeVM extends VM { * Only available for node v10+. * @param {("inherit"|"redirect"|"off")} [options.console="inherit"] - Sets the behavior of the console in the sandbox. * inherit to enable console, redirect to redirect to events, off 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] - WARNING: When allowing require the option options.require.root * 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 {(string[])} [options.require.external.modules] - Array of allowed external modules. Also supports wildcards, so specifying ['@scope/*-ver-??], @@ -221,6 +252,9 @@ class NodeVM extends VM { super({__proto__: null, compiler: compiler, eval: allowEval, wasm}); + const customResolver = requireOpts instanceof Resolver; + const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(requireOpts, nesting && NESTING_OVERRIDE, this._compiler); + // This is only here for backwards compatibility. objectDefineProperty(this, 'options', {__proto__: null, value: { console: consoleType, @@ -231,9 +265,7 @@ class NodeVM extends VM { strict }}); - const resolver = resolverFromOptions(this, requireOpts, nesting && NESTING_OVERRIDE, this._compiler); - - objectDefineProperty(this, '_resolver', {__proto__: null, value: resolver}); + objectDefineProperty(this, 'resolver', {__proto__: null, value: resolver, enumerable: true}); if (!cacheSandboxScript) { cacheSandboxScript = compileScript(`${__dirname}/setup-node-sandbox.js`, @@ -242,23 +274,9 @@ class NodeVM extends VM { const closure = this._runScript(cacheSandboxScript); - const extensions = { - __proto__: null - }; - - 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); - + const extensions = customResolver ? resolver.getExtensions(this) : makeCustomExtensions(this, resolver, sourceExtensions); this.readonly(HOST); - this.readonly(resolver); - this.readonly(this); const { Module, @@ -270,9 +288,41 @@ class NodeVM extends VM { argv, env, 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); + } }); objectDefineProperties(this, { @@ -292,7 +342,7 @@ class NodeVM extends VM { this.setGlobals(sandbox); } - if (requireOpts && requireOpts.import) { + if (!customResolver && requireOpts && requireOpts.import) { if (Array.isArray(requireOpts.import)) { for (let i = 0, l = requireOpts.import.length; i < l; i++) { this.require(requireOpts.import[i]); @@ -303,6 +353,14 @@ class NodeVM extends VM { } } + /** + * @ignore + * @deprecated + */ + get _resolver() { + return this.resolver; + } + /** * @ignore * @deprecated Just call the method yourself like method(args); @@ -331,12 +389,12 @@ class NodeVM extends VM { * @throws {*} If the module couldn't be found or loading it threw an error. */ 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; } return this._requireImpl(mod, module, true); @@ -391,19 +449,19 @@ class NodeVM extends VM { if (code instanceof VMScript) { script = strict ? code._compileNodeVMStrict() : code._compileNodeVM(); 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); } } else { const unresolvedFilename = filename || 'vm.js'; if (!sandboxModule) { 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 { sandboxModule = new (this._Module)(null, null); sandboxModule.id = unresolvedFilename; @@ -493,7 +551,7 @@ class NodeVM extends VM { } } -function vm2NestingLoader(resolver, vm, id) { +function vm2NestingLoader(vm) { if (!cacheMakeNestingScript) { cacheMakeNestingScript = compileScript('nesting.js', '(vm, nodevm) => ({VM: vm, NodeVM: nodevm})'); } diff --git a/lib/resolver-compat.js b/lib/resolver-compat.js index 0ef30ee..ce6b4cf 100644 --- a/lib/resolver-compat.js +++ b/lib/resolver-compat.js @@ -1,20 +1,14 @@ '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 { Resolver, DefaultResolver } = 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'); /** * Require wrapper to be able to annotate require with webpackIgnore. @@ -44,11 +38,66 @@ function makeExternalMatcher(obj) { return new RegExp(`[\\\\/]node_modules[\\\\/]${regexString}(?:[\\\\/](?!(?:.*[\\\\/])?node_modules[\\\\/]).*)?$`); } -class LegacyResolver extends DefaultResolver { +class CustomResolver extends DefaultResolver { + + constructor(fileSystem, globalPaths, builtinModules, rootPaths, context, customResolver, hostRequire, compiler, strict) { + super(fileSystem, globalPaths, builtinModules); + this.rootPaths = rootPaths; + this.context = context; + 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.context === 'sandbox') return super.loadJS(vm, mod, filename); + const m = this.hostRequire(filename); + mod.exports = vm.readonly(m); + } + + loadNode(vm, mod, filename) { + if (this.context === 'sandbox') return super.loadNode(vm, mod, filename); + const m = this.hostRequire(filename); + mod.exports = vm.readonly(m); + } - 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; + 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, context, customResolver, hostRequire, compiler, strict, externals, allowTransitive) { + super(fileSystem, globalPaths, builtinModules, rootPaths, context, customResolver, hostRequire, compiler, strict); + this.externals = externals.map(makeExternalMatcher); + this.externalCache = externals.map(pattern => new RegExp(makeExternalMatcherRegex(pattern))); this.currMod = undefined; this.trustedMods = new WeakMap(); this.allowTransitive = allowTransitive; @@ -81,23 +130,21 @@ class LegacyResolver extends DefaultResolver { }); } - 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 { this.currMod = undefined; } @@ -111,9 +158,7 @@ class LegacyResolver extends DefaultResolver { } loadJS(vm, mod, filename) { - filename = this.pathResolve(filename); - this.checkAccess(mod, filename); - if (this.pathContext(filename, 'js') === 'sandbox') { + if (this.context === 'sandbox') { 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}); @@ -123,158 +168,33 @@ class LegacyResolver extends DefaultResolver { } } -} - -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; - } - get eventEmitter() { - return this[kEventEmitter]; - } - } - 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]; + 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; } - } - 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); - } + 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); } - 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); } const { @@ -289,62 +209,27 @@ function resolverFromOptions(vm, options, override, compiler) { fs: fsOpt = DEFAULT_FS, } = options; - const builtins = genBuiltinsFromOptions(vm, builtinOpt, mockOpt, override); - - if (!externalOpt) return new Resolver(fsOpt, builtins, [], hostRequire); - - 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; - } + const builtins = makeBuiltinsFromLegacyOptions(builtinOpt, hostRequire, mockOpt, override); - 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); - }; - } + if (!externalOpt) return new Resolver(fsOpt, [], builtins); + + if (!compiler) compiler = jsCompiler; + + const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined; if (typeof externalOpt !== 'object') { - return new DefaultResolver(fsOpt, builtins, checkPath, [], () => context, newCustomResolver, hostRequire, compiler, strict); + return new CustomResolver(fsOpt, [], builtins, checkedRootPaths, context, customResolver, hostRequire, compiler, strict); } let transitive = false; + let external = undefined; if (Array.isArray(externalOpt)) { external = externalOpt; } else { external = externalOpt.modules; transitive = context === 'sandbox' && 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, context, customResolver, hostRequire, compiler, strict, external, transitive); } -exports.resolverFromOptions = resolverFromOptions; +exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions; diff --git a/lib/resolver.js b/lib/resolver.js index 22f9061..fd86aff 100644 --- a/lib/resolver.js +++ b/lib/resolver.js @@ -6,6 +6,7 @@ const { VMError } = require('./bridge'); const { VMScript } = require('./script'); +const { jsCopmiler } = require('./compiler'); // This should match. Note that '\', '%' are invalid characters // 1. name/.* @@ -21,19 +22,24 @@ function isArrayIndex(key) { class Resolver { - constructor(fs, builtinModules, globalPaths, hostRequire) { + constructor(fs, globalPaths, builtins) { this.fs = fs; - this.builtinModules = builtinModules; this.globalPaths = globalPaths; - this.hostRequire = hostRequire; + this.builtins = builtins; } init(vm) { } - 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) { @@ -48,31 +54,45 @@ class Resolver { return path !== '' && (this.fs.isSeparator(path[0]) || this.fs.isAbsolute(path)); } - pathConcat(...paths) { - return this.fs.join(...paths); - } - - pathBasename(path) { - return this.fs.basename(path); - } - - pathDirname(path) { - return this.fs.dirname(path); - } - 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]; } - getBuiltinModulesList() { - return Object.getOwnPropertyNames(this.builtinModules); + 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; } loadBuiltinModule(vm, id) { - const handler = this.builtinModules[id]; - return handler && handler(this, 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); + } + + makeExtensionHandler(vm, name) { + return (mod, filename) => { + filename = this.fs.resolve(filename); + this.checkAccess(mod, filename); + this[name](vm, mod, filename); + }; + } + + 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'), + }; } loadJS(vm, mod, filename) { @@ -91,19 +111,17 @@ class Resolver { } - 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 // b. STOP return x; } - 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" throw new VMError(`Cannot find module '${x}'`, 'ENOTFOUND'); } @@ -116,14 +134,14 @@ class Resolver { const dirs = []; // 4. while I >= 0, while (true) { - const name = this.pathBasename(path); + const name = this.fs.basename(path); // a. if PARTS[I] = "node_modules" CONTINUE if (name !== 'node_modules') { // b. DIR = path join(PARTS[0 .. I] + "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; // d. let I = I - 1 path = dir; @@ -136,96 +154,79 @@ class Resolver { } -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); } 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') { - const script = this.readScript(filename); - vm.run(script, {filename, strict: this.strict, module: mod, wrapper: 'none', dirname: mod.path}); - } else { - const m = this.hostRequire(filename); - mod.exports = vm.readonly(m); - } + const script = this.readScript(filename); + vm.run(script, {filename, strict: this.isStrict(filename), module: mod, wrapper: 'none', dirname: mod.path}); } 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); } loadNode(vm, mod, filename) { - filename = this.pathResolve(filename); - this.checkAccess(mod, filename); - if (this.pathContext(filename, 'node') === 'sandbox') throw new VMError('Native modules can be required only with context set to \'host\'.'); - const m = this.hostRequire(filename); - mod.exports = vm.readonly(m); + throw new VMError('Native modules can be required only with context set to \'host\'.'); + } + + 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 || '.'; // 5. LOAD_PACKAGE_SELF(X, dirname(Y)) @@ -256,13 +257,13 @@ class DefaultResolver extends Resolver { for (let i = 0; i < paths.length; i++) { // a. LOAD_AS_FILE(Y + X) // 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; } } else if (paths === undefined) { // a. LOAD_AS_FILE(Y + X) // 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; } else { throw new VMError('Invalid options.paths option.'); @@ -270,7 +271,7 @@ class DefaultResolver extends Resolver { } else { // a. LOAD_AS_FILE(Y + X) // 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; } @@ -309,10 +310,10 @@ class DefaultResolver extends Resolver { f = this.loadNodeModules(x, dirs, extList); if (f) return f; - 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); } loadAsFileOrDirectory(x, extList) { @@ -324,14 +325,14 @@ class DefaultResolver extends Resolver { } 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; } tryWithExtension(x, extList) { for (let i = 0; i < extList.length; i++) { const ext = extList[i]; - if (ext !== this.pathBasename(ext)) continue; + if (ext !== this.fs.basename(ext)) continue; const f = this.tryFile(x + ext); if (f) return f; } @@ -339,15 +340,15 @@ class DefaultResolver extends Resolver { } 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; } @@ -367,15 +368,15 @@ class DefaultResolver extends Resolver { imports: parsed.imports, type: parsed.type }; - this.packageCache[packagePath] = filtered; + this.packageCache.set(packagePath, filtered); return filtered; } readPackageScope(path) { 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; const pack = this.readPackage(dir); if (pack) return {data: pack, scope: dir}; @@ -400,7 +401,7 @@ class DefaultResolver extends Resolver { // 1. If X/index.js is a file, load X/index.js as JavaScript text. STOP // 2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP // 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); } // LOAD_AS_DIRECTORY(X) @@ -412,7 +413,7 @@ class DefaultResolver extends Resolver { // b. If "main" is a falsy value, GOTO 2. if (typeof pack.main === 'string') { // 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) let f = this.loadAsFile(m, extList); if (f) return f; @@ -507,7 +508,7 @@ class DefaultResolver extends Resolver { // 2. If X does not match this pattern or DIR/NAME/package.json is not a file, // return. if (!res) return undefined; - const scope = this.pathConcat(dir, res[1]); + const scope = this.fs.join(dir, res[1]); const pack = this.readPackage(scope); if (!pack) return undefined; // 3. Parse DIR/NAME/package.json, and look for "exports" field. @@ -707,7 +708,7 @@ class DefaultResolver extends Resolver { return this.packageResolve(target.replace(/\*/g, subpath), packageURL, conditions, extList); } // 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); } } // Otherwise, throw an Invalid Package Target error. @@ -720,7 +721,7 @@ class DefaultResolver extends Resolver { throw new VMError(`Invalid package target for '${subpath}'`, 'ERR_INVALID_PACKAGE_TARGET'); } // 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. subpath = decodeURI(subpath); // f. If subpath split on "/" or "\" contains any ".", ".." or "node_modules" segments, case insensitive and including percent @@ -735,7 +736,7 @@ class DefaultResolver extends Resolver { } // h. Otherwise, // 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 } else if (Array.isArray(target)) { // a. If target.length is zero, return null. @@ -806,7 +807,7 @@ class DefaultResolver extends Resolver { throw new VMError(`Invalid package specifier '${packageSpecifier}'`, 'ERR_INVALID_MODULE_SPECIFIER'); } // 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. return 'node:' + packageSpecifier; } @@ -845,11 +846,11 @@ class DefaultResolver extends Resolver { let packageURL; while (true) { // 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. if (parentParentURL === parentURL) { // 12. Throw a Module Not Found error. @@ -872,7 +873,7 @@ class DefaultResolver extends Resolver { } // g. Otherwise, // 1. Return the URL resolution of packageSubpath in packageURL. - return this.pathConcat(packageURL, packageSubpath); + return this.fs.join(packageURL, packageSubpath); } } diff --git a/lib/setup-node-sandbox.js b/lib/setup-node-sandbox.js index 05f38ef..c4fa5cb 100644 --- a/lib/setup-node-sandbox.js +++ b/lib/setup-node-sandbox.js @@ -37,17 +37,23 @@ const { argv: optionArgv, env: optionEnv, console: optionConsole, - vm, - resolver, - extensions + extensions, + emitArgs, + globalPaths, + getLookupPathsFor, + resolve: resolve0, + lookupPaths, + loadBuiltinModule, + registerModule, + builtinModules, + dirname, + basename } = data; function ensureSandboxArray(a) { return localArrayPrototypeSlice(a); } -const globalPaths = ensureSandboxArray(resolver.globalPaths); - class Module { constructor(id, path, parent) { @@ -56,7 +62,7 @@ class Module { this.path = path; this.parent = parent; this.loaded = false; - this.paths = path ? ensureSandboxArray(resolver.genLookupPaths(path)) : []; + this.paths = path ? ensureSandboxArray(getLookupPathsFor(path)) : []; this.children = []; this.exports = {}; } @@ -81,12 +87,12 @@ function requireImpl(mod, id, direct) { if (direct && mod.require !== originalRequire) { return mod.require(id); } - const filename = resolver.resolve(mod, id, undefined, Module._extensions, direct); + const filename = resolve0(mod, id, undefined, Module._extensions, direct); if (localStringPrototypeStartsWith(filename, 'node:')) { id = localStringPrototypeSlice(filename, 5); let nmod = cacheBuiltins[id]; if (!nmod) { - nmod = resolver.loadBuiltinModule(vm, id); + nmod = loadBuiltinModule(id); if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND'); cacheBuiltins[id] = nmod; } @@ -101,15 +107,15 @@ function requireImpl(mod, id, direct) { let nmod = cacheBuiltins[id]; if (nmod) return nmod; - nmod = resolver.loadBuiltinModule(vm, id); + nmod = loadBuiltinModule(id); if (nmod) { cacheBuiltins[id] = nmod; return 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); try { Module._cache[filename] = module; @@ -131,8 +137,8 @@ function requireImpl(mod, id, direct) { return module.exports; } -Module.builtinModules = ensureSandboxArray(resolver.getBuiltinModulesList()); -Module.globalPaths = globalPaths; +Module.builtinModules = ensureSandboxArray(builtinModules); +Module.globalPaths = ensureSandboxArray(globalPaths); Module._extensions = {__proto__: null}; Module._cache = {__proto__: null}; @@ -146,7 +152,7 @@ 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;) { const ext = localStringPrototypeSlice(name, i); const handler = Module._extensions[ext]; @@ -165,11 +171,11 @@ function createRequireForModule(mod) { return requireImpl(mod, id, true); } 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)); } resolve.paths = paths; @@ -277,20 +283,6 @@ global.clearImmediate = function clearImmediate(immediate) { const localProcess = host.process; -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(); const LISTENER_HANDLER = new LocalWeakMap(); @@ -435,27 +427,27 @@ if (optionConsole === 'inherit') { } else if (optionConsole === 'redirect') { global.console = { 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); }, time() {}, timeEnd() {}, trace(...args) { - vmEmitArgs('console.trace', args); + emitArgs('console.trace', args); } }; } diff --git a/package-lock.json b/package-lock.json index 5e61b0d..8ed2787 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vm2", - "version": "3.9.11", + "version": "3.9.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vm2", - "version": "3.9.11", + "version": "3.9.15", "license": "MIT", "dependencies": { "acorn": "^8.7.0", From e5cfcdca028757296780547e53b71b34718debe9 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Tue, 11 Apr 2023 23:15:01 +0200 Subject: [PATCH 2/3] Reduce resolver API and add docu --- README.md | 24 +++++++++++- index.d.ts | 103 ++++++++++++-------------------------------------- lib/main.js | 8 ++++ lib/nodevm.js | 2 +- 4 files changed, 56 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 3eac28c..fe72773 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Unlike `VM`, `NodeVM` allows you to require modules in the same way that you wou * `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`). * `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`). * `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. * `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.external.transitive` - Boolean which indicates if transitive dependencies of external modules are allowed (default: `false`). **WARNING**: When a module is required transitively, any module is then able to require it normally, even if this was not possible before it was loaded. @@ -211,6 +211,28 @@ const script = new VMScript('require("foobar")', {filename: '/data/myvmscript.js vm.run(script); ``` +### 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 You can increase performance by using precompiled scripts. The precompiled VMScript can be run multiple times. It is important to note that the code is not bound to any VM (context); rather, it is bound before each run, just for that run. diff --git a/index.d.ts b/index.d.ts index 1640f88..4ad0508 100644 --- a/index.d.ts +++ b/index.d.ts @@ -59,86 +59,18 @@ export class VMFileSystem implements VMFileSystemInterface { isSeparator(char: string): boolean; } +/** + * 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}; -export type Builtins = Map; +/** + * Require method + */ export type HostRequire = (id: string) => any; -export type JSONValue = null | boolean | number | string | readonly JSONValue[] | {[key: string]: JSONValue}; -export interface Package { - name: JSONValue, - main: JSONValue, - exports: JSONValue, - imports: JSONValue, - type: JSONValue -}; - -export function makeBuiltins(builtins: string[], hostRequire: HostRequire): Builtins; -export function makeBuiltinsFromLegacyOptions(builtins: string[], hostRequire: HostRequire, mocks?: {[key: string]: any}, overrides?: {[key: string]: Builtin}): Builtins; -export function makeResolverFromLegacyOptions(options: VMRequire, override?: {[key: string]: Builtin}, compiler?: CompilerFunction): Resolver; - -export abstract class Resolver { - constructor(readonly fs: VMFileSystemInterface, readonly globalPaths: readonly string[], readonly builtins: Builtins); - init(vm: NodeVM): void; - abstract isPathAllowed(path: string): boolean; - checkAccess(mod: any, filename: string): void; - pathIsRelative(path: string): boolean; - pathIsAbsolute(path: string): boolean; - lookupPaths(mod: any, id: string): readonly string[]; - getBuiltinModulesList(vm: NodeVM): readonly string[]; - loadBuiltinModule(vm: NodeVM, id: string): any; - makeExtensionHandler(vm: NodeVM, name: string): (mod: any, filename: string) => void; - getExtensions(vm: NodeVM): {[key: string]: (mod: any, filename: string) => void}; - loadJS(vm: NodeVM, mod: any, filename: string): void; - loadJSON(vm: NodeVM, mod: any, filename: string): void; - loadNode(vm: NodeVM, mod: any, filename: string): void; - registerModule(mod: any, filename: string, path: string, parent: any, direct: boolean): void; - resolve(mod: any, id: string, options: {paths?: readonly string[], unsafeOptions: any}, extList: readonly string[], direct: boolean): string; - resolveFull(mod: any, id: string, options: {paths?: readonly string[], unsafeOptions: any}, extList: readonly string[], direct: boolean): string; - genLookupPaths(path: string): readonly string[]; -} - -export abstract class DefaultResolver extends Resolver { - private packageCache: Map; - private scriptCache: Map; - constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Builtins); - getCompiler(filename: string): CompilerFunction; - isStrict(filename: string): boolean; - readScript(filename: string): string; - customResolve(id: string, path: string, extList: readonly string[]): string | undefined; - loadAsFileOrDirectory(x: string, extList: readonly string[]): string | undefined; - tryFile(x: string): string | undefined; - tryWithExtension(x: string, extList: readonly string[]): string | undefined; - readPackage(path: string): Package | undefined; - readPackageScope(path: string): {data?: Package, scope?: string}; - // LOAD_AS_FILE(X) - loadAsFile(x: string, extList: readonly string[]): string | undefined; - // LOAD_INDEX(X) - loadIndex(x: string, extList: readonly string[]): string | undefined; - // LOAD_AS_DIRECTORY(X) - loadAsPackage(x: string, pack: Package | undefined, extList: readonly string[]): string | undefined; - // LOAD_AS_DIRECTORY(X) - loadAsDirectory(x: string, extList: readonly string[]): string | undefined; - // LOAD_NODE_MODULES(X, START) - loadNodeModules(x: string, dirs: readonly string[], extList: readonly string[]): string | undefined; - // LOAD_PACKAGE_IMPORTS(X, DIR) - loadPackageImports(x: string, dir: string, extList: readonly string[]): string | undefined; - // LOAD_PACKAGE_EXPORTS(X, DIR) - loadPackageExports(x: string, dir: string, extList: readonly string[]): string | undefined; - // LOAD_PACKAGE_SELF(X, DIR) - loadPackageSelf(x: string, dir: string, extList: readonly string[]): string | undefined; - // RESOLVE_ESM_MATCH(MATCH) - resolveEsmMatch(match: string, x: string, extList: readonly string[]): string; - // PACKAGE_EXPORTS_RESOLVE(packageURL, subpath, exports, conditions) - packageExportsResolve(packageURL: string, subpath: string, rexports: JSONValue, conditions: readonly string[], extList: readonly string[]): string; - // PACKAGE_IMPORTS_EXPORTS_RESOLVE(matchKey, matchObj, packageURL, isImports, conditions) - packageImportsExportsResolve(matchKey: string, matchObj: {[key: string]: JSONValue}, packageURL: string, isImports: boolean, conditions: readonly string[], extList: readonly string[]): string | undefined | null; - // PATTERN_KEY_COMPARE(keyA, keyB) - patternKeyCompare(keyA: string, keyB: string): number; - // PACKAGE_TARGET_RESOLVE(packageURL, target, subpath, pattern, internal, conditions) - packageTargetResolve(packageURL: string, target: JSONValue, subpath: string, pattern: boolean, internal: boolean, conditions: readonly string[], extList: readonly string[]): string | undefined | null; - // PACKAGE_RESOLVE(packageSpecifier, parentURL) - packageResolve(packageSpecifier: string, parentURL: string, conditions: readonly string[], extList: readonly string[]): string; -} /** * Require options for a VM @@ -165,7 +97,7 @@ export interface VMRequire { /* An additional lookup function in case a module wasn't found in one of the traditional node lookup paths. */ resolve?: (moduleName: string, parentDirname: string) => string | { path: string, module?: string } | undefined; /** Custom require to require host and built-in modules. */ - customRequire?: (id: string) => any; + customRequire?: HostRequire; /** Load modules in strict mode. (default: true) */ strict?: boolean; /** FileSystem to load files from */ @@ -178,6 +110,19 @@ export interface VMRequire { */ export type CompilerFunction = (code: string, filename: string) => string; +export abstract class Resolver { + private constructor(fs: VMFileSystemInterface, globalPaths: readonly string[], builtins: Map); +} + +/** + * 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 */ @@ -190,7 +135,7 @@ export interface VMOptions { /** VM's global object. */ sandbox?: any; /** - * 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. */ timeout?: number; diff --git a/lib/main.js b/lib/main.js index de5ffc9..15e5163 100644 --- a/lib/main.js +++ b/lib/main.js @@ -15,9 +15,17 @@ const { const { VMFileSystem } = require('./filesystem'); +const { + Resolver +} = require('./resolver'); +const { + makeResolverFromLegacyOptions +} = require('./resolver-compat'); exports.VMError = VMError; exports.VMScript = VMScript; exports.NodeVM = NodeVM; exports.VM = VM; exports.VMFileSystem = VMFileSystem; +exports.Resolver = Resolver; +exports.makeResolverFromLegacyOptions = makeResolverFromLegacyOptions; diff --git a/lib/nodevm.js b/lib/nodevm.js index c40a68a..c224758 100644 --- a/lib/nodevm.js +++ b/lib/nodevm.js @@ -274,7 +274,7 @@ class NodeVM extends VM { const closure = this._runScript(cacheSandboxScript); - const extensions = customResolver ? resolver.getExtensions(this) : makeCustomExtensions(this, resolver, sourceExtensions); + const extensions = makeCustomExtensions(this, resolver, sourceExtensions); this.readonly(HOST); From 7b4eeab673ab22a7bda0526e7b68ccff63aec2b7 Mon Sep 17 00:00:00 2001 From: XmiliaH Date: Tue, 11 Apr 2023 23:15:15 +0200 Subject: [PATCH 3/3] Add tests --- test/nodevm.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/nodevm.js b/test/nodevm.js index 58addb3..843ce82 100644 --- a/test/nodevm.js +++ b/test/nodevm.js @@ -7,7 +7,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); const {EventEmitter} = require('events'); -const {NodeVM, VMScript} = require('..'); +const {NodeVM, VMScript, makeResolverFromLegacyOptions} = require('..'); // const NODE_VERSION = parseInt(process.versions.node.split('.')[0]); global.isHost = true; @@ -601,6 +601,19 @@ function getStack(error) { }); }); +describe('resolver', () => { + it('use resolver', () => { + const resolver = makeResolverFromLegacyOptions({ + external: true + }); + const vm = new NodeVM({ + require: resolver + }); + + vm.run("require('mocha')", __filename); + }); +}); + describe('source extensions', () => { it('does not find a TS module with the default settings', () => { const vm = new NodeVM({