diff --git a/index.d.ts b/index.d.ts index 4c3a53b..714aa09 100644 --- a/index.d.ts +++ b/index.d.ts @@ -69,23 +69,44 @@ 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?: string[]; } /** - * A VM with behavior more similar to running inside Node. + * VM is a simple sandbox, without `require` feature, to synchronously run an untrusted code. + * Only JavaScript built-in objects + Buffer are available. Scheduling functions + * (`setInterval`, `setTimeout` and `setImmediate`) are not available by default. */ -export class NodeVM extends EventEmitter { - constructor(options?: NodeVMOptions); +export class VM { + constructor(options?: VMOptions); + /** Direct access to the global sandbox object */ + readonly sandbox: any; + /** Timeout to use for the run methods */ + timeout?: number; /** Runs the code */ - run(js: string, path: string): any; + run(js: string, path?: string): any; /** Runs the VMScript object */ - run(script: VMScript, path?: string): any; - + run(script: VMScript): any; + /** Runs the code in the specific file */ + runFile(filename: string): any; + /** Loads all the values into the global object with the same names */ + setGlobals(values: any): this; + /** Make a object visible as a global with a specific name */ + setGlobal(name: string, value: any): this; + /** Get the global object with the specific name */ + getGlobal(name: string): any; /** Freezes the object inside VM making it read-only. Not available for primitive values. */ - freeze(object: any, name: string): any; - /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. */ - protect(object: any, name: string): any; + freeze(object: any, name?: string): any; + /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values */ + protect(object: any, name?: string): any; +} + +/** + * A VM with behavior more similar to running inside Node. + */ +export class NodeVM extends EventEmitter implements VM { + constructor(options?: NodeVMOptions); + /** Require a module in VM and return it's exports. */ require(module: string): any; @@ -96,7 +117,7 @@ export class NodeVM extends EventEmitter { * @param {string} [filename] File name (used in stack traces only). * @param {Object} [options] VM options. */ - static code(script: string, filename: string, options: NodeVMOptions): NodeVM; + static code(script: string, filename?: string, options?: NodeVMOptions): any; /** * Create NodeVM and run script from file inside it. @@ -104,24 +125,28 @@ export class NodeVM extends EventEmitter { * @param {string} [filename] File name (used in stack traces only). * @param {Object} [options] VM options. */ - static file(filename: string, options: NodeVMOptions): NodeVM -} + static file(filename: string, options?: NodeVMOptions): any; -/** - * VM is a simple sandbox, without `require` feature, to synchronously run an untrusted code. - * Only JavaScript built-in objects + Buffer are available. Scheduling functions - * (`setInterval`, `setTimeout` and `setImmediate`) are not available by default. - */ -export class VM { - constructor(options?: VMOptions); + /** Direct access to the global sandbox object */ + readonly sandbox: any; + /** Only here because of implements VM. Does nothing. */ + timeout?: number; /** Runs the code */ - run(js: string): any; + run(js: string, path?: string): any; /** Runs the VMScript object */ run(script: VMScript): any; + /** Runs the code in the specific file */ + runFile(filename: string): any; + /** Loads all the values into the global object with the same names */ + setGlobals(values: any): this; + /** Make a object visible as a global with a specific name */ + setGlobal(name: string, value: any): this; + /** Get the global object with the specific name */ + getGlobal(name: string): any; /** Freezes the object inside VM making it read-only. Not available for primitive values. */ - freeze(object: any, name: string): any; + freeze(object: any, name?: string): any; /** Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values */ - protect(object: any, name: string): any; + protect(object: any, name?: string): any; } /** @@ -130,14 +155,29 @@ export class VM { * to any VM (context); rather, it is bound before each run, just for that run. */ export class VMScript { - constructor(code: string, path?: string, options?: { - lineOffset: number; - columnOffset: number; + constructor(code: string, path: string, options?: { + lineOffset?: number; + columnOffset?: number; + compiler?: "javascript" | "coffeescript" | CompilerFunction; + }); + constructor(code: string, options?: { + filename?: string, + lineOffset?: number; + columnOffset?: number; + compiler?: "javascript" | "coffeescript" | CompilerFunction; }); - /** Wraps the code */ - wrap(prefix: string, postfix: string): VMScript; + readonly code: string; + readonly filename: string; + readonly lineOffset: number; + readonly columnOffset: number; + readonly compiler: "javascript" | "coffeescript" | CompilerFunction; + /** + * Wraps the code + * @deprecated + */ + wrap(prefix: string, postfix: string): this; /** Compiles the code. If called multiple times, the code is only compiled once. */ - compile(): any; + compile(): this; } /** Custom Error class */ diff --git a/lib/contextify.js b/lib/contextify.js index 2457aa3..880547c 100644 --- a/lib/contextify.js +++ b/lib/contextify.js @@ -544,9 +544,9 @@ Decontextify.value = (value, traps, deepTraps, flags, mock) => { case 'object': if (value === null) { return null; - } else if (instanceOf(value, Number)) { return host.Number(value); - } else if (instanceOf(value, String)) { return host.String(value); - } else if (instanceOf(value, Boolean)) { return host.Boolean(value); + } else if (instanceOf(value, Number)) { return Decontextify.instance(value, host.Number, deepTraps, flags, 'Number'); + } else if (instanceOf(value, String)) { return Decontextify.instance(value, host.String, deepTraps, flags, 'String'); + } else if (instanceOf(value, Boolean)) { return Decontextify.instance(value, host.Boolean, deepTraps, flags, 'Boolean'); } else if (instanceOf(value, Date)) { return Decontextify.instance(value, host.Date, deepTraps, flags, 'Date'); } else if (instanceOf(value, RangeError)) { return Decontextify.instance(value, host.RangeError, deepTraps, flags, 'Error'); } else if (instanceOf(value, ReferenceError)) { return Decontextify.instance(value, host.ReferenceError, deepTraps, flags, 'Error'); @@ -871,9 +871,9 @@ Contextify.value = (value, traps, deepTraps, flags, mock) => { case 'object': if (value === null) { return null; - } else if (instanceOf(value, host.Number)) { return host.Number(value); - } else if (instanceOf(value, host.String)) { return host.String(value); - } else if (instanceOf(value, host.Boolean)) { return host.Boolean(value); + } else if (instanceOf(value, host.Number)) { return Contextify.instance(value, Number, deepTraps, flags, 'Number'); + } else if (instanceOf(value, host.String)) { return Contextify.instance(value, String, deepTraps, flags, 'String'); + } else if (instanceOf(value, host.Boolean)) { return Contextify.instance(value, Boolean, deepTraps, flags, 'Boolean'); } else if (instanceOf(value, host.Date)) { return Contextify.instance(value, Date, deepTraps, flags, 'Date'); } else if (instanceOf(value, host.RangeError)) { return Contextify.instance(value, RangeError, deepTraps, flags, 'Error'); } else if (instanceOf(value, host.ReferenceError)) { return Contextify.instance(value, ReferenceError, deepTraps, flags, 'Error'); @@ -910,8 +910,21 @@ Contextify.value = (value, traps, deepTraps, flags, mock) => { return null; } }; -Contextify.globalValue = (value, name) => { - return (global[name] = Contextify.value(value)); +Contextify.setGlobal = (name, value) => { + const prop = Contextify.value(name); + try { + global[prop] = Contextify.value(value); + } catch (e) { + throw Decontextify.value(e); + } +}; +Contextify.getGlobal = (name) => { + const prop = Contextify.value(name); + try { + return Decontextify.value(global[prop]); + } catch (e) { + throw Decontextify.value(e); + } }; Contextify.readonly = (value, mock) => { return Contextify.value(value, null, FROZEN_TRAPS, null, mock); @@ -923,6 +936,7 @@ Contextify.connect = (outer, inner) => { Decontextified.set(outer, inner); Contextified.set(inner, outer); }; +Contextify.makeModule = ()=>({exports: {}}); const BufferMock = host.Object.create(null); BufferMock.allocUnsafe = function allocUnsafe(size) { @@ -949,5 +963,6 @@ const exportsMap = host.Object.create(null); exportsMap.Contextify = Contextify; exportsMap.Decontextify = Decontextify; exportsMap.Buffer = LocalBuffer; +exportsMap.sandbox = Decontextify.value(global); return exportsMap; diff --git a/lib/main.js b/lib/main.js index 271d8f0..ab98a4d 100644 --- a/lib/main.js +++ b/lib/main.js @@ -2,31 +2,120 @@ 'use strict'; +/** + * This callback will be called to transform a script to JavaScript. + * + * @callback compileCallback + * @param {string} code - Script code to transform to JavaScript. + * @param {string} filename - Filename of this script. + * @return {string} JavaScript code that represents the script code. + */ + +/** + * This callback will be called to resolve a module if it couldn't be found. + * + * @callback resolveCallback + * @param {string} moduleName - Name of the module to resolve. + * @param {string} dirname - Name of the current directory. + * @return {(string|undefined)} The file or directory to use to load the requested module. + */ + const fs = require('fs'); const vm = require('vm'); const pa = require('path'); const {EventEmitter} = require('events'); const {INSPECT_MAX_BYTES} = require('buffer'); -const COFFEE_SCRIPT_COMPILER = {compiler: null}; +/** + * Load a script from a file and compile it. + * + * @private + * @param {string} filename - File to load and compile to a script. + * @param {string} prefix - Prefix for the script. + * @param {string} suffix - Suffix for the script. + * @return {vm.Script} The compiled script. + */ +function loadAndCompileScript(filename, prefix, suffix) { + const data = fs.readFileSync(filename, 'utf8'); + return new vm.Script(prefix + data + suffix, { + filename, + displayErrors: false + }); +} + +/** + * Cache where we can cache some things + * + * @private + * @property {?compileCallback} coffeeScriptCompiler - The coffee script compiler or null if not yet used. + * @property {?Object} timeoutContext - The context used for the timeout functionality of null if not yet used. + * @property {?vm.Script} timeoutScript - The compiled script used for the timeout functionality of null if not yet used. + * @property {vm.Script} contextifyScript - The compiled script used to setup a sandbox. + * @property {?vm.Script} sandboxScript - The compiled script used to setup the NodeVM require mechanism of null if not yet used. + */ +const CACHE = { + coffeeScriptCompiler: null, + timeoutContext: null, + timeoutScript: null, + contextifyScript: loadAndCompileScript(`${__dirname}/contextify.js`, '(function(require, host) { ', '\n})'), + sandboxScript: null, + fixAsyncScript: null, + getGlobalScript: null, + getGeneratorFunctionScript: null, + getAsyncFunctionScript: null, + getAsyncGeneratorFunctionScript: null, +}; + +/** + * Default run options for vm.Script.runInContext + * + * @private + */ +const DEFAULT_RUN_OPTIONS = {displayErrors: false}; + +/** + * Returns the cached coffee script compiler or loads it + * if it is not found in the cache. + * + * @private + * @return {compileCallback} The coffee script compiler. + * @throws {VMError} If the coffee-script module can't be found. + */ function getCoffeeScriptCompiler() { - if (!COFFEE_SCRIPT_COMPILER.compiler) { + if (!CACHE.coffeeScriptCompiler) { try { const coffeeScript = require('coffee-script'); - COFFEE_SCRIPT_COMPILER.compiler = (code, filename) => { + CACHE.coffeeScriptCompiler = (code, filename) => { return coffeeScript.compile(code, {header: false, bare: true}); }; } catch (e) { throw new VMError('Coffee-Script compiler is not installed.'); } } - return COFFEE_SCRIPT_COMPILER.compiler; + return CACHE.coffeeScriptCompiler; } +/** + * The JavaScript compiler, just a identity function. + * + * @private + * @type {compileCallback} + * @param {string} code - The JavaScript code. + * @param {string} filename - Filename of this script. + * @return {string} The code. + */ function jsCompiler(code, filename) { return code; } +/** + * Look up the compiler for a specific name. + * + * @private + * @param {(string|compileCallback)} compiler - A compile callback or the name of the compiler. + * @return {compileCallback} The resolved compiler. + * @throws {VMError} If the compiler is unknown or the coffee script module was needed and couldn't be found. + */ function lookupCompiler(compiler) { if ('function' === typeof compiler) return compiler; switch (compiler) { @@ -48,41 +137,223 @@ function lookupCompiler(compiler) { /** * Class Script * - * @class + * @public */ - class VMScript { + + /** + * The script code with wrapping. If set will invalidate the cache.
+ * Writable only for backwards compatibility. + * + * @public + * @readonly + * @member {string} code + * @memberOf VMScript# + */ + + /** + * The filename used for this script. + * + * @public + * @readonly + * @since v3.8.5 + * @member {string} filename + * @memberOf VMScript# + */ + + /** + * The line offset use for stack traces. + * + * @public + * @readonly + * @since v3.8.5 + * @member {number} lineOffset + * @memberOf VMScript# + */ + + /** + * The column offset use for stack traces. + * + * @public + * @readonly + * @since v3.8.5 + * @member {number} columnOffset + * @memberOf VMScript# + */ + + /** + * The compiler to use to get the JavaScript code. + * + * @public + * @readonly + * @since v3.8.5 + * @member {(string|compileCallback)} compiler + * @memberOf VMScript# + */ + + /** + * The prefix for the script. + * + * @private + * @member {string} _prefix + * @memberOf VMScript# + */ + + /** + * The suffix for the script. + * + * @private + * @member {string} _suffix + * @memberOf VMScript# + */ + + /** + * The compiled vm.Script for the VM or if not compiled null. + * + * @private + * @member {?vm.Script} _compiledVM + * @memberOf VMScript# + */ + + /** + * The compiled vm.Script for the NodeVM or if not compiled null. + * + * @private + * @member {?vm.Script} _compiledNodeVM + * @memberOf VMScript# + */ + + /** + * The resolved compiler to use to get the JavaScript code. + * + * @private + * @readonly + * @member {compileCallback} _compiler + * @memberOf VMScript# + */ + + /** + * The script to run without wrapping. + * + * @private + * @member {string} _code + * @memberOf VMScript# + */ + /** * Create VMScript instance. * - * @param {String} code Code to run. - * @param {String} [filename] Filename that shows up in any stack traces produced from this script. - * @param {{ lineOffset: number, columnOffset: number }} [options] Options that define vm.Script options. - * @return {VMScript} - */ - - constructor(code, filename, options) { - this._code = String(code); - this.options = options || {}; - this.filename = filename || this.options.filename || 'vm.js'; - this._prefix = ''; - this._suffix = ''; - this._compiledVM = null; - this._compiledNodeVM = null; - this._compiler = lookupCompiler(this.options.compiler || 'javascript'); - this._unresolvedFilename = this.options.filename || this.filename; + * @public + * @param {string} code - Code to run. + * @param {(string|Object)} [options] - Options map or filename. + * @param {string} [options.filename="vm.js"] - Filename that shows up in any stack traces produced from this script. + * @param {number} [options.lineOffset=0] - Passed to vm.Script options. + * @param {number} [options.columnOffset=0] - Passed to vm.Script options. + * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. + * @throws {VMError} If the compiler is unknown or if coffee-script was requested but the module not found. + */ + constructor(code, options) { + const sCode = `${code}`; + let useFileName; + let useOptions; + if (arguments.length === 2) { + if (typeof options === 'object' && options.toString === Object.prototype.toString) { + useOptions = options || {}; + useFileName = useOptions.filename; + } else { + useOptions = {}; + useFileName = options; + } + } else if (arguments.length > 2) { + // We do it this way so that there are no more arguments in the function. + // eslint-disable-next-line prefer-rest-params + useOptions = arguments[2] || {}; + useFileName = options || useOptions.filename; + } else { + useOptions = {}; + } + + const { + compiler = 'javascript', + lineOffset = 0, + columnOffset = 0 + } = useOptions; + + // Throw if the compiler is unknown. + const resolvedCompiler = lookupCompiler(compiler); + + Object.defineProperties(this, { + code: { + // Put this here so that it is enumerable, and looks like a property. + get() { + return this._prefix + this._code + this._suffix; + }, + set(value) { + const strNewCode = String(value); + if (strNewCode === this._code && this._prefix === '' && this._suffix === '') return; + this._code = strNewCode; + this._prefix = ''; + this._suffix = ''; + this._compiledVM = null; + this._compiledNodeVM = null; + }, + enumerable: true + }, + filename: { + value: useFileName || 'vm.js', + enumerable: true + }, + lineOffset: { + value: lineOffset, + enumerable: true + }, + columnOffset: { + value: columnOffset, + enumerable: true + }, + compiler: { + value: compiler, + enumerable: true + }, + _code: { + value: sCode, + writable: true + }, + _prefix: { + value: '', + writable: true + }, + _suffix: { + value: '', + writable: true + }, + _compiledVM: { + value: null, + writable: true + }, + _compiledNodeVM: { + value: null, + writable: true + }, + _compiler: {value: resolvedCompiler} + }); } /** - * Wraps the code. + * Wraps the code.
+ * This will replace the old wrapping.
* Will invalidate the code cache. * - * @return {VMScript} + * @public + * @deprecated Since v3.8.5. Wrap your code before passing it into the VMScript object. + * @param {string} prefix - String that will be appended before the script code. + * @param {script} suffix - String that will be appended behind the script code. + * @return {this} This for chaining. + * @throws {TypeError} If prefix or suffix is a Symbol. */ - wrap(prefix, suffix) { - const strPrefix = String(prefix); - const strSuffix = String(suffix); + const strPrefix = `${prefix}`; + const strSuffix = `${suffix}`; if (this._prefix === strPrefix && this._suffix === strSuffix) return this; this._prefix = strPrefix; this._suffix = strSuffix; @@ -92,285 +363,410 @@ class VMScript { } /** - * This code will be compiled to VM code. + * Compile this script.
+ * This is useful to detect syntax errors in the script. * - * @return {VMScript} + * @public + * @return {this} This for chaining. + * @throws {SyntaxError} If there is a syntax error in the script. */ - compile() { - return this._compileVM(); + this._compileVM(); + return this; } /** - * For backwards compatibility. + * Compiles this script to a vm.Script. * - * @return {String} The wrapped code + * @private + * @param {string} prefix - JavaScript code that will be used as prefix. + * @param {string} suffix - JavaScript code that will be used as suffix. + * @return {vm.Script} The compiled vm.Script. + * @throws {SyntaxError} If there is a syntax error in the script. */ - get code() { - return this._prefix + this._code + this._suffix; - } - - /** - * For backwards compatibility. - * Will invalidate the code cache. - * - * @param {String} newCode The new code to run. - */ - set code(newCode) { - const strNewCode = String(newCode); - if (strNewCode === this._prefix + this._code + this._suffix) return; - this._code = strNewCode; - this._prefix = ''; - this._suffix = ''; - this._compiledVM = null; - this._compiledNodeVM = null; + _compile(prefix, suffix) { + return new vm.Script(prefix + this._compiler(this._prefix + this._code + this._suffix, this.filename) + suffix, { + filename: this.filename, + displayErrors: false, + lineOffset: this.lineOffset, + columnOffset: this.columnOffset + }); } /** - * Will compile the code for VM and cache it + * Will return the cached version of the script intended for VM or compile it. * - * @return {VMScript} + * @private + * @return {vm.Script} The compiled script + * @throws {SyntaxError} If there is a syntax error in the script. */ _compileVM() { - if (this._compiledVM) return this; - - this._compiledVM = new vm.Script(this._compiler(this._prefix + this._code + this._suffix, this._unresolvedFilename), { - filename: this.filename, - displayErrors: false, - lineOffset: this.options.lineOffset || 0, - columnOffset: this.options.columnOffset || 0 - }); - - return this; + let script = this._compiledVM; + if (!script) { + this._compiledVM = script = this._compile('', ''); + } + return script; } /** - * Will compile the code for NodeVM and cache it + * Will return the cached version of the script intended for NodeVM or compile it. * - * @return {VMScript} + * @private + * @return {vm.Script} The compiled script + * @throws {SyntaxError} If there is a syntax error in the script. */ _compileNodeVM() { - if (this._compiledNodeVM) return this; - - this._compiledNodeVM = new vm.Script('(function (exports, require, module, __filename, __dirname) { ' + - this._compiler(this._prefix + this._code + this._suffix, this._unresolvedFilename) + '\n})', { - filename: this.filename, - displayErrors: false, - lineOffset: this.options.lineOffset || 0, - columnOffset: this.options.columnOffset || 0 - }); - - return this; - } - - _runInVM(context) { - return this._compiledVM.runInContext(context, { - filename: this.filename, - displayErrors: false - }); - } - - _runInNodeVM(context) { - return this._compiledNodeVM.runInContext(context, { - filename: this.filename, - displayErrors: false - }); + let script = this._compiledNodeVM; + if (!script) { + this._compiledNodeVM = script = this._compile('(function (exports, require, module, __filename, __dirname) { ', '\n})'); + } + return script; } } -function loadScript(filename) { - const data = fs.readFileSync(filename, 'utf8'); - return new VMScript(data, filename); -} - -const SCRIPT_CACHE = { - cf: loadScript(`${__dirname}/contextify.js`).wrap('(function(require, host) { ', '\n})')._compileVM(), - sb: loadScript(`${__dirname}/sandbox.js`).wrap('(function (vm, host, Contextify, Decontextify, Buffer) { ', '\n})')._compileVM(), - fa: loadScript(`${__dirname}/fixasync.js`).wrap('(function () { ', '\n})'), - getGlobal: new VMScript('this'), - getGeneratorFunction: new VMScript('(function*(){}).constructor'), - getAsyncFunction: new VMScript('(async function(){}).constructor'), - getAsyncGeneratorFunction: new VMScript('(async function*(){}).constructor'), - exp: new VMScript('({exports: {}})')._compileVM(), - runTimeout: new VMScript('fn()', 'timeout_bridge.js')._compileVM() -}; - -const TIMEOUT_CONTEXT = {context: null}; +/** + * + * This callback will be called and has a specific time to finish.
+ * No parameters will be supplied.
+ * If parameters are required, use a closure. + * + * @private + * @callback runWithTimeout + * @return {*} + * + */ +/** + * Run a function with a specific timeout. + * + * @private + * @param {runWithTimeout} fn - Function to run with the specific timeout. + * @param {number} timeout - The amount of time to give the function to finish. + * @return {*} The value returned by the function. + * @throws {Error} If the function took to long. + */ function doWithTimeout(fn, timeout) { - if (!TIMEOUT_CONTEXT.context) { - TIMEOUT_CONTEXT.context = vm.createContext(); + let ctx = CACHE.timeoutContext; + let script = CACHE.timeoutScript; + if (!ctx) { + CACHE.timeoutContext = ctx = vm.createContext(); + CACHE.timeoutScript = script = new vm.Script('fn()', { + filename: 'timeout_bridge.js', + displayErrors: false + }); } - TIMEOUT_CONTEXT.context.fn = fn; + ctx.fn = fn; try { - return SCRIPT_CACHE.runTimeout._compiledVM.runInContext(TIMEOUT_CONTEXT.context, { - filename: SCRIPT_CACHE.runTimeout.filename, + return script.runInContext(ctx, { displayErrors: false, timeout }); } finally { - TIMEOUT_CONTEXT.context.fn = null; + ctx.fn = null; } } /** * Class VM. * - * @property {Object} options VM options. + * @public */ - class VM extends EventEmitter { + + /** + * The timeout for {@link VM#run} calls. + * + * @public + * @since v3.8.5 + * @member {number} timeout + * @memberOf VM# + */ + /** - * Create VM instance. + * Get the global sandbox object. * - * @param {Object} [options] VM options. - * @return {VM} + * @public + * @readonly + * @since v3.8.5 + * @member {Object} sandbox + * @memberOf VM# + */ + + /** + * The compiler to use to get the JavaScript code. + * + * @public + * @readonly + * @since v3.8.5 + * @member {(string|compileCallback)} compiler + * @memberOf VM# */ + /** + * The context for this sandbox. + * + * @private + * @readonly + * @member {Object} _context + * @memberOf VM# + */ + + /** + * The internal methods for this sandbox. + * + * @private + * @readonly + * @member {{Contextify: Object, Decontextify: Object, Buffer: Object, sandbox:Object}} _internal + * @memberOf VM# + */ + + /** + * The resolved compiler to use to get the JavaScript code. + * + * @private + * @readonly + * @member {compileCallback} _compiler + * @memberOf VM# + */ + + /** + * Create a new VM instance. + * + * @public + * @param {Object} [options] - VM options. + * @param {number} [options.timeout] - The amount of time until a call to {@link VM#run} will timeout. + * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox. + * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. + * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().
+ * Only available for node v10+. + * @param {boolean} [options.wasm=true] - Allow to run wasm code.
+ * Only available for node v10+. + * @param {boolean} [options.fixAsync=false] - Filters for async functions. + * @throws {VMError} If the compiler is unknown. + */ constructor(options = {}) { super(); - // defaults - this.options = { - timeout: options.timeout, - sandbox: options.sandbox, - compiler: lookupCompiler(options.compiler || 'javascript'), - eval: options.eval === false ? false : true, - wasm: options.wasm === false ? false : true, - fixAsync: options.fixAsync - }; - - const host = { - version: parseInt(process.versions.node.split('.')[0]), - console, - String, - Number, - Buffer, - Boolean, - Array, - Date, - Error, - EvalError, - RangeError, - ReferenceError, - SyntaxError, - TypeError, - URIError, - RegExp, - Function, - Object, - VMError, - Proxy, - Reflect, - Map, - WeakMap, - Set, - WeakSet, - Promise, - Symbol, - INSPECT_MAX_BYTES - }; - - this._context = vm.createContext(undefined, { + // Read all options + const { + timeout, + sandbox, + compiler = 'javascript' + } = options; + const allowEval = options.eval !== false; + const allowWasm = options.wasm !== false; + const fixAsync = !!options.fixAsync; + + // Early error if sandbox is not an object. + if (sandbox && 'object' !== typeof sandbox) { + throw new VMError('Sandbox must be object.'); + } + + // Early error if compiler can't be found. + const resolvedCompiler = lookupCompiler(compiler); + + // Create a new context for this vm. + const _context = vm.createContext(undefined, { codeGeneration: { - strings: this.options.eval, - wasm: this.options.wasm + strings: allowEval, + wasm: allowWasm } }); - Reflect.defineProperty(this, '_internal', { - value: SCRIPT_CACHE.cf._runInVM(this._context).call(this._context, require, host) + // Create the bridge between the host and the sandbox. + const _internal = CACHE.contextifyScript.runInContext(_context, DEFAULT_RUN_OPTIONS).call(_context, require, HOST); + + // Define the properties of this object. + // Use Object.defineProperties here to be able to + // hide and set properties write only. + Object.defineProperties(this, { + timeout: { + value: timeout, + writable: true, + enumerable: true + }, + compiler: { + value: compiler, + enumerable: true + }, + sandbox: { + value: _internal.sandbox, + enumerable: true + }, + _context: {value: _context}, + _internal: {value: _internal}, + _compiler: {value: resolvedCompiler}, + _fixAsync: {value: fixAsync} }); - if (this.options.fixAsync) { - SCRIPT_CACHE.getGlobal._compileVM(); - SCRIPT_CACHE.fa._compileVM(); + if (fixAsync) { + if (!CACHE.fixAsyncScript) { + CACHE.fixAsyncScript = loadAndCompileScript(`${__dirname}/fixasync.js`, '(function() { ', '\n})'); + CACHE.getGlobalScript = new vm.Script('this', { + filename: 'get_global.js', + displayErrors: false + }); + try { + CACHE.getGeneratorFunctionScript = new vm.Script('(function*(){}).constructor', { + filename: 'get_generator_function.js', + displayErrors: false + }); + } catch (ex) {} + try { + CACHE.getAsyncFunctionScript = new vm.Script('(async function(){}).constructor', { + filename: 'get_async_function.js', + displayErrors: false + }); + } catch (ex) {} + try { + CACHE.getAsyncGeneratorFunctionScript = new vm.Script('(async function*(){}).constructor', { + filename: 'get_async_generator_function.js', + displayErrors: false + }); + } catch (ex) {} + } const internal = { __proto__: null, - global: SCRIPT_CACHE.getGlobal._runInVM(this._context), + global: CACHE.getGlobalScript.runInContext(this._context, DEFAULT_RUN_OPTIONS), Contextify: this._internal.Contextify, - host: host + host: HOST }; - try { - SCRIPT_CACHE.getGeneratorFunction._compileVM(); - internal.GeneratorFunction = SCRIPT_CACHE.getGeneratorFunction._runInVM(this._context); - } catch (ex) {} - try { - SCRIPT_CACHE.getAsyncFunction._compileVM(); - internal.AsyncFunction = SCRIPT_CACHE.getAsyncFunction._runInVM(this._context); - } catch (ex) {} - try { - SCRIPT_CACHE.getAsyncGeneratorFunction._compileVM(); - internal.AsyncGeneratorFunction = SCRIPT_CACHE.getAsyncGeneratorFunction._runInVM(this._context); - } catch (ex) {} - SCRIPT_CACHE.fa._runInVM(this._context).call(internal); + if (CACHE.getGeneratorFunctionScript) { + try { + internal.GeneratorFunction = CACHE.getGeneratorFunctionScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); + } catch (ex) {} + } + if (CACHE.getAsyncFunctionScript) { + try { + internal.AsyncFunction = CACHE.getAsyncFunctionScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); + } catch (ex) {} + } + if (CACHE.getAsyncGeneratorFunctionScript) { + try { + internal.AsyncGeneratorFunction = CACHE.getAsyncGeneratorFunctionScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); + } catch (ex) {} + } + CACHE.fixAsyncScript.runInContext(this._context, DEFAULT_RUN_OPTIONS).call(internal); } // prepare global sandbox - if (this.options.sandbox) { - if ('object' !== typeof this.options.sandbox) { - throw new VMError('Sandbox must be object.'); - } + if (sandbox) { + this.setGlobals(sandbox); + } + } - for (const name in this.options.sandbox) { - if (Object.prototype.hasOwnProperty.call(this.options.sandbox, name)) { - this._internal.Contextify.globalValue(this.options.sandbox[name], name); - } + /** + * Adds all the values to the globals. + * + * @public + * @since v3.8.5 + * @param {Object} values - All values that will be added to the globals. + * @return {this} This for chaining. + * @throws {*} If the setter of a global throws an exception it is propagated. And the remaining globals will not be written. + */ + setGlobals(values) { + for (const name in values) { + if (Object.prototype.hasOwnProperty.call(values, name)) { + this._internal.Contextify.setGlobal(name, values[name]); } } + return this; + } + + /** + * Set a global value. + * + * @public + * @since v3.8.5 + * @param {string} name - The name of the global. + * @param {*} value - The value of the global. + * @return {this} This for chaining. + * @throws {*} If the setter of the global throws an exception it is propagated. + */ + setGlobal(name, value) { + this._internal.Contextify.setGlobal(name, value); + return this; + } + + /** + * Get a global value. + * + * @public + * @since v3.8.5 + * @param {string} name - The name of the global. + * @return {*} The value of the global. + * @throws {*} If the getter of the global throws an exception it is propagated. + */ + getGlobal(name) { + return this._internal.Contextify.getGlobal(name); } /** * Freezes the object inside VM making it read-only. Not available for primitive values. * - * @static - * @param {*} object Object to freeze. - * @param {String} [globalName] Whether to add the object to global. + * @public + * @param {*} value - Object to freeze. + * @param {string} [globalName] - Whether to add the object to global. * @return {*} Object to freeze. + * @throws {*} If the setter of the global throws an exception it is propagated. */ - freeze(value, globalName) { this._internal.Contextify.readonly(value); - if (globalName) this._internal.Contextify.globalValue(value, globalName); + if (globalName) this._internal.Contextify.setGlobal(globalName, value); return value; } /** * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. * - * @static - * @param {*} object Object to protect. - * @param {String} [globalName] Whether to add the object to global. + * @public + * @param {*} value - Object to protect. + * @param {string} [globalName] - Whether to add the object to global. * @return {*} Object to protect. + * @throws {*} If the setter of the global throws an exception it is propagated. */ - protect(value, globalName) { this._internal.Contextify.protected(value); - if (globalName) this._internal.Contextify.globalValue(value, globalName); + if (globalName) this._internal.Contextify.globalValue(globalName, value); return value; } /** * Run the code in VM. * - * @param {String} code Code to run. - * @param {String} [filename] Filename that shows up in any stack traces produced from this script. + * @public + * @param {(string|VMScript)} code - Code to run. + * @param {string} [filename="vm.js"] - Filename that shows up in any stack traces produced from this script.
+ * This is only used if code is a String. * @return {*} Result of executed code. + * @throws {SyntaxError} If there is a syntax error in the script. + * @throws {Error} An error is thrown when the script took to long and there is a timeout. + * @throws {*} If the script execution terminated with an exception it is propagated. */ - run(code, filename) { - const script = code instanceof VMScript ? code : new VMScript(code, filename, {compiler: this.options.compiler}); - - if (this.options.fixAsync && /\basync\b/.test(script.code)) { - throw new VMError('Async not available'); + let script; + if (code instanceof VMScript) { + if (this._fixAsync && /\basync\b/.test(code.code)) { + throw new VMError('Async not available'); + } + script = code._compileVM(); + } else { + if (this._fixAsync && /\basync\b/.test(code)) { + throw new VMError('Async not available'); + } + const useFileName = filename || 'vm.js'; + // Compile the script here so that we don't need to create a instance of VMScript. + script = new vm.Script(this._compiler(code, useFileName), { + filename: useFileName, + displayErrors: false + }); } - script._compileVM(); - - if (!this.options.timeout) { + if (!this.timeout) { + // If no timeout is given, directly run the script. try { - return this._internal.Decontextify.value(script._runInVM(this._context)); + return this._internal.Decontextify.value(script.runInContext(this._context, DEFAULT_RUN_OPTIONS)); } catch (e) { throw this._internal.Decontextify.value(e); } @@ -378,183 +774,217 @@ class VM extends EventEmitter { return doWithTimeout(()=>{ try { - return this._internal.Decontextify.value(script._runInVM(this._context)); + return this._internal.Decontextify.value(script.runInContext(this._context, DEFAULT_RUN_OPTIONS)); } catch (e) { throw this._internal.Decontextify.value(e); } - }, this.options.timeout); + }, this.timeout); } + + /** + * Run the code in VM. + * + * @public + * @since v3.8.5 + * @param {string} filename - Filename of file to load and execute in a NodeVM. + * @return {*} Result of executed code. + * @throws {Error} If filename is not a valid filename. + * @throws {SyntaxError} If there is a syntax error in the script. + * @throws {Error} An error is thrown when the script took to long and there is a timeout. + * @throws {*} If the script execution terminated with an exception it is propagated. + */ + runFile(filename) { + const resolvedFilename = pa.resolve(filename); + + if (!fs.existsSync(resolvedFilename)) { + throw new VMError(`Script '${filename}' not found.`); + } + + if (fs.statSync(resolvedFilename).isDirectory()) { + throw new VMError('Script must be file, got directory.'); + } + + return this.run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); + } + } +/** + * Event caused by a console.debug call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.debug" + * @type {...*} + */ + +/** + * Event caused by a console.log call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.log" + * @type {...*} + */ + +/** + * Event caused by a console.info call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.info" + * @type {...*} + */ + +/** + * Event caused by a console.warn call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.warn" + * @type {...*} + */ + +/** + * Event caused by a console.error call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.error" + * @type {...*} + */ + +/** + * Event caused by a console.dir call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.dir" + * @type {...*} + */ + +/** + * Event caused by a console.trace call if options.console="redirect" is specified. + * + * @public + * @event NodeVM."console.trace" + * @type {...*} + */ + /** * Class NodeVM. * - * @class + * @public + * @extends {VM} * @extends {EventEmitter} - * @property {Object} module Pointer to main module. */ +class NodeVM extends VM { -class NodeVM extends EventEmitter { /** - * Create NodeVM instance. + * Create a new NodeVM instance.
* - * Unlike VM, NodeVM lets you use require same way like in regular node. + * Unlike VM, NodeVM lets you use require same way like in regular node.
+ * + * However, it does not use the timeout. * - * @param {Object} [options] VM options. - * @return {NodeVM} + * @public + * @param {Object} [options] - VM options. + * @param {Object} [options.sandbox] - Objects that will be copied into the global object of the sandbox. + * @param {(string|compileCallback)} [options.compiler="javascript"] - The compiler to use. + * @param {boolean} [options.eval=true] - Allow the dynamic evaluation of code via eval(code) or Function(code)().
+ * Only available for node v10+. + * @param {boolean} [options.wasm=true] - Allow to run wasm code.
+ * 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 {(boolean|string[]|Object)} [options.require.external=false] - 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-??], + * for instance, will allow using all modules having a name of the form @scope/something-ver-aa, @scope/other-ver-11, etc. + * @param {boolean} [options.require.external.transitive=false] - Boolean which indicates if transitive dependencies of external modules are allowed. + * @param {string[]} [options.require.builtin=[]] - Array of allowed builtin modules, accepts ["*"] for all. + * @param {(string|string[])} [options.require.root] - Restricted path(s) where local modules can be required. If omitted every path is allowed. + * @param {Object} [options.require.mock] - Collection of mock modules (both external or builtin). + * @param {("host"|"sandbox")} [options.require.context="host"] - host to require modules in host and proxy them to sandbox. + * sandbox to load, compile and require modules in sandbox. + * Builtin modules except events always required in host and proxied to sandbox. + * @param {string[]} [options.require.import] - Array of modules to be loaded into NodeVM on start. + * @param {resolveCallback} [options.require.resolve] - An additional lookup function in case a module wasn't + * found in one of the traditional node lookup paths. + * @param {boolean} [options.nesting=false] - Allow nesting of VMs. + * @param {("commonjs"|"none")} [options.wrapper="commonjs"] - commonjs to wrap script into CommonJS wrapper, + * none to retrieve value returned by the script. + * @param {string[]} [options.sourceExtensions=["js"]] - Array of file extensions to treat as source code. + * @throws {VMError} If the compiler is unknown. */ - constructor(options = {}) { - super(); + const sandbox = options.sandbox; + + // Throw this early + if (sandbox && 'object' !== typeof sandbox) { + throw new VMError('Sandbox must be object.'); + } + + super({compiler: options.compiler, eval: options.eval, wasm: options.wasm}); // defaults - this.options = { - sandbox: options.sandbox, + Object.defineProperty(this, 'options', {value: { console: options.console || 'inherit', require: options.require || false, - compiler: lookupCompiler(options.compiler || 'javascript'), - eval: options.eval === false ? false : true, - wasm: options.wasm === false ? false : true, nesting: options.nesting || false, wrapper: options.wrapper || 'commonjs', sourceExtensions: options.sourceExtensions || ['js'] - }; - - const host = { - version: parseInt(process.versions.node.split('.')[0]), - require, - process, - console, - setTimeout, - setInterval, - setImmediate, - clearTimeout, - clearInterval, - clearImmediate, - String, - Number, - Buffer, - Boolean, - Array, - Date, - Error, - EvalError, - RangeError, - ReferenceError, - SyntaxError, - TypeError, - URIError, - RegExp, - Function, - Object, - VMError, - Proxy, - Reflect, - Map, - WeakMap, - Set, - WeakSet, - Promise, - Symbol, - INSPECT_MAX_BYTES - }; - - if (this.options.nesting) { - host.VM = VM; - host.NodeVM = NodeVM; - } + }}); - this._context = vm.createContext(undefined, { - codeGeneration: { - strings: this.options.eval, - wasm: this.options.wasm - } - }); - - Object.defineProperty(this, '_internal', { - value: SCRIPT_CACHE.cf._runInVM(this._context).call(this._context, require, host) - }); + let sandboxScript = CACHE.sandboxScript; + if (!sandboxScript) { + CACHE.sandboxScript = sandboxScript = loadAndCompileScript(`${__dirname}/sandbox.js`, + '(function (vm, host, Contextify, Decontextify, Buffer) { ', '\n})'); + } - const closure = SCRIPT_CACHE.sb._runInVM(this._context); + const closure = sandboxScript.runInContext(this._context, DEFAULT_RUN_OPTIONS); Object.defineProperty(this, '_prepareRequire', { - value: closure.call(this._context, this, host, this._internal.Contextify, this._internal.Decontextify, this._internal.Buffer) + value: closure.call(this._context, this, HOST, this._internal.Contextify, this._internal.Decontextify, this._internal.Buffer) }); // prepare global sandbox - if (this.options.sandbox) { - if ('object' !== typeof this.options.sandbox) { - throw new VMError('Sandbox must be object.'); - } - - for (const name in this.options.sandbox) { - if (Object.prototype.hasOwnProperty.call(this.options.sandbox, name)) { - this._internal.Contextify.globalValue(this.options.sandbox[name], name); - } - } + if (sandbox) { + this.setGlobals(sandbox); } if (this.options.require && this.options.require.import) { - if (!Array.isArray(this.options.require.import)) { - this.options.require.import = [this.options.require.import]; - } - - for (let i = 0, l = this.options.require.import.length; i < l; i++) { - this.require(this.options.require.import[i]); + if (Array.isArray(this.options.require.import)) { + for (let i = 0, l = this.options.require.import.length; i < l; i++) { + this.require(this.options.require.import[i]); + } + } else { + this.require(this.options.require.import); } } } /** - * @deprecated + * @ignore + * @deprecated Just call the method yourself like method(args); + * @param {function} method - Function to invoke. + * @param {...*} args - Arguments to pass to the function. + * @return {*} Return value of the function. + * @todo Can we remove this function? It even had a bug that would use args as this parameter. + * @throws {*} Rethrows anything the method throws. + * @throws {VMError} If method is not a function. + * @throws {Error} If method is a class. */ - call(method, ...args) { if ('function' === typeof method) { - return method.apply(args); - + return method(...args); } else { throw new VMError('Unrecognized method type.'); } } - /** - * Freezes the object inside VM making it read-only. Not available for primitive values. - * - * @static - * @param {*} object Object to freeze. - * @param {String} [globalName] Whether to add the object to global. - * @return {*} Object to freeze. - */ - - freeze(value, globalName) { - this._internal.Contextify.readonly(value); - if (global) this._internal.Contextify.globalValue(value, globalName); - return value; - } - - /** - * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values. - * - * @static - * @param {*} object Object to protect. - * @param {String} [globalName] Whether to add the object to global. - * @return {*} Object to protect. - */ - - protect(value, globalName) { - this._internal.Contextify.protected(value); - if (global) this._internal.Contextify.globalValue(value, globalName); - return value; - } - /** * Require a module in VM and return it's exports. * - * @param {String} module Module name. + * @public + * @param {string} module - Module name. * @return {*} Exported module. + * @throws {*} If the module couldn't be found or loading it threw an error. */ - require(module) { return this.run(`module.exports = require('${module}');`, 'vm.js'); } @@ -565,112 +995,140 @@ class NodeVM extends EventEmitter { * First time you run this method, code is executed same way like in node's regular `require` - it's executed with * `module`, `require`, `exports`, `__dirname`, `__filename` variables and expect result in `module.exports'. * - * @param {String} code Code to run. - * @param {String} [filename] Filename that shows up in any stack traces produced from this script. + * @param {(string|VMScript)} code - Code to run. + * @param {string} [filename] - Filename that shows up in any stack traces produced from this script.
+ * This is only used if code is a String. * @return {*} Result of executed code. + * @throws {SyntaxError} If there is a syntax error in the script. + * @throws {*} If the script execution terminated with an exception it is propagated. + * @fires NodeVM."console.debug" + * @fires NodeVM."console.log" + * @fires NodeVM."console.info" + * @fires NodeVM."console.warn" + * @fires NodeVM."console.error" + * @fires NodeVM."console.dir" + * @fires NodeVM."console.trace" */ - run(code, filename) { let dirname; - let returned; let resolvedFilename; + let script; - if (filename) { - resolvedFilename = pa.resolve(filename); - dirname = pa.dirname(filename); + if (code instanceof VMScript) { + script = code._compileNodeVM(); + resolvedFilename = pa.resolve(code.filename); + dirname = pa.dirname(resolvedFilename); } else { - resolvedFilename = null; - dirname = null; + const unresolvedFilename = filename || 'vm.js'; + if (filename) { + resolvedFilename = pa.resolve(filename); + dirname = pa.dirname(resolvedFilename); + } else { + resolvedFilename = null; + dirname = null; + } + script = new vm.Script('(function (exports, require, module, __filename, __dirname) { ' + + this._compiler(code, unresolvedFilename) + '\n})', { + filename: unresolvedFilename, + displayErrors: false + }); } - const module = SCRIPT_CACHE.exp._runInVM(this._context); - - const script = code instanceof VMScript ? code : new VMScript(code, resolvedFilename, {compiler: this.options.compiler, filename}); - script._compileNodeVM(); + const wrapper = this.options.wrapper; + const module = this._internal.Contextify.makeModule(); try { - const closure = script._runInNodeVM(this._context); + const closure = script.runInContext(this._context, DEFAULT_RUN_OPTIONS); + + const returned = closure.call(this._context, module.exports, this._prepareRequire(dirname), module, resolvedFilename, dirname); - returned = closure.call(this._context, module.exports, this._prepareRequire(dirname), module, filename, dirname); + return this._internal.Decontextify.value(wrapper === 'commonjs' ? module.exports : returned); } catch (e) { throw this._internal.Decontextify.value(e); } - if (this.options.wrapper === 'commonjs') { - return this._internal.Decontextify.value(module.exports); - } else { - return this._internal.Decontextify.value(returned); - } } /** * Create NodeVM and run code inside it. * - * @param {String} script Javascript code. - * @param {String} [filename] File name (used in stack traces only). - * @param {Object} [options] VM options. - * @return {NodeVM} VM. + * @public + * @static + * @param {string} script - Code to execute. + * @param {string} [filename] - File name (used in stack traces only). + * @param {Object} [options] - VM options. + * @param {string} [options.filename] - File name (used in stack traces only). Used if filename is omitted. + * @return {*} Result of executed code. + * @see {@link NodeVM} for the options. + * @throws {SyntaxError} If there is a syntax error in the script. + * @throws {*} If the script execution terminated with an exception it is propagated. */ - static code(script, filename, options) { + let unresolvedFilename; if (filename != null) { if ('object' === typeof filename) { options = filename; - filename = null; + unresolvedFilename = options.filename; } else if ('string' === typeof filename) { - filename = pa.resolve(filename); + unresolvedFilename = filename; } else { throw new VMError('Invalid arguments.'); } + } else if ('object' === typeof options) { + unresolvedFilename = options.filename; } if (arguments.length > 3) { throw new VMError('Invalid number of arguments.'); } - return new NodeVM(options).run(script, filename); + const resolvedFilename = typeof unresolvedFilename === 'string' ? pa.resolve(unresolvedFilename) : undefined; + + return new NodeVM(options).run(script, resolvedFilename); } /** * Create NodeVM and run script from file inside it. * - * @param {String} [filename] File name (used in stack traces only). - * @param {Object} [options] VM options. - * @return {NodeVM} VM. + * @public + * @static + * @param {string} filename - Filename of file to load and execute in a NodeVM. + * @param {Object} [options] - NodeVM options. + * @return {*} Result of executed code. + * @see {@link NodeVM} for the options. + * @throws {Error} If filename is not a valid filename. + * @throws {SyntaxError} If there is a syntax error in the script. + * @throws {*} If the script execution terminated with an exception it is propagated. */ - static file(filename, options) { - filename = pa.resolve(filename); + const resolvedFilename = pa.resolve(filename); - if (!fs.existsSync(filename)) { + if (!fs.existsSync(resolvedFilename)) { throw new VMError(`Script '${filename}' not found.`); } - if (fs.statSync(filename).isDirectory()) { + if (fs.statSync(resolvedFilename).isDirectory()) { throw new VMError('Script must be file, got directory.'); } - return new NodeVM(options).run(fs.readFileSync(filename, 'utf8'), filename); + return new NodeVM(options).run(fs.readFileSync(resolvedFilename, 'utf8'), resolvedFilename); } } /** * VMError. * - * @class + * @public * @extends {Error} - * @property {String} stack Call stack. - * @property {String} message Error message. */ - class VMError extends Error { + /** * Create VMError instance. * - * @param {String} message Error message. - * @return {VMError} + * @public + * @param {string} message - Error message. */ - constructor(message) { super(message); @@ -680,6 +1138,52 @@ class VMError extends Error { } } +/** + * Host objects + * + * @private + */ +const HOST = { + version: parseInt(process.versions.node.split('.')[0]), + require, + process, + console, + setTimeout, + setInterval, + setImmediate, + clearTimeout, + clearInterval, + clearImmediate, + String, + Number, + Buffer, + Boolean, + Array, + Date, + Error, + EvalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError, + RegExp, + Function, + Object, + VMError, + Proxy, + Reflect, + Map, + WeakMap, + Set, + WeakSet, + Promise, + Symbol, + INSPECT_MAX_BYTES, + VM, + NodeVM +}; + exports.VMError = VMError; exports.NodeVM = NodeVM; exports.VM = VM; diff --git a/lib/sandbox.js b/lib/sandbox.js index e5080eb..7e27000 100644 --- a/lib/sandbox.js +++ b/lib/sandbox.js @@ -59,9 +59,7 @@ return ((vm, host) => { try { // Load module let contents = fs.readFileSync(filename, 'utf8'); - if (typeof vm.options.compiler === 'function') { - contents = vm.options.compiler(contents, filename); - } + contents = vm._compiler(contents, filename); const code = `(function (exports, require, module, __filename, __dirname) { 'use strict'; ${contents} \n});`; diff --git a/test/vm.js b/test/vm.js index af81a30..59192d3 100644 --- a/test/vm.js +++ b/test/vm.js @@ -128,11 +128,24 @@ describe('contextify', () => { assert.strictEqual(Object.prototype.toString.call(vm.run(`new Date`)), '[object Date]'); assert.strictEqual(Object.prototype.toString.call(vm.run(`new RangeError`)), '[object Error]'); assert.strictEqual(Object.prototype.toString.call(vm.run(`/a/g`)), '[object RegExp]'); + assert.strictEqual(Object.prototype.toString.call(vm.run(`new String`)), '[object String]'); + assert.strictEqual(Object.prototype.toString.call(vm.run(`new Number`)), '[object Number]'); + assert.strictEqual(Object.prototype.toString.call(vm.run(`new Boolean`)), '[object Boolean]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)([]), '[object Array]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new Date), '[object Date]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new RangeError), '[object Error]'); assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(/a/g), '[object RegExp]'); + assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new String), '[object String]'); + assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new Number), '[object Number]'); + assert.strictEqual(vm.run(`((obj) => Object.prototype.toString.call(obj))`)(new Boolean), '[object Boolean]'); + + assert.strictEqual(typeof vm.run(`new String`), 'object'); + assert.strictEqual(typeof vm.run(`new Number`), 'object'); + assert.strictEqual(typeof vm.run(`new Boolean`), 'object'); + assert.strictEqual(vm.run(`((obj) => typeof obj)`)(new String), 'object'); + assert.strictEqual(vm.run(`((obj) => typeof obj)`)(new Number), 'object'); + assert.strictEqual(vm.run(`((obj) => typeof obj)`)(new Boolean), 'object'); let o = vm.run('let x = {a: test.date, b: test.date};x'); assert.strictEqual(vm.run('x.valueOf().a instanceof Date'), true); @@ -160,15 +173,15 @@ describe('contextify', () => { it('string', () => { assert.strictEqual(vm.run('(test.string).constructor === String'), true); - assert.strictEqual(vm.run("typeof(test.stringO) === 'string' && test.string.valueOf instanceof Object"), true); + assert.strictEqual(vm.run("typeof(test.string) === 'string' && test.string.valueOf instanceof Object"), true); }); it('number', () => { - assert.strictEqual(vm.run("typeof(test.numberO) === 'number' && test.number.valueOf instanceof Object"), true); + assert.strictEqual(vm.run("typeof(test.number) === 'number' && test.number.valueOf instanceof Object"), true); }); it('boolean', () => { - assert.strictEqual(vm.run("typeof(test.booleanO) === 'boolean' && test.boolean.valueOf instanceof Object"), true); + assert.strictEqual(vm.run("typeof(test.boolean) === 'boolean' && test.boolean.valueOf instanceof Object"), true); }); it('date', () => { @@ -295,7 +308,13 @@ describe('VM', () => { }); it('globals', () => { + const dyn = {}; + vm.setGlobal('dyn', dyn); + vm.setGlobals({dyns: dyn}); assert.equal(vm.run('round(1.5)'), 2); + assert.equal(vm.getGlobal('dyn'), dyn); + assert.equal(vm.sandbox.dyn, dyn); + assert.equal(vm.sandbox.dyns, dyn); }); it('errors', () => { @@ -863,6 +882,10 @@ describe('precompiled scripts', () => { assert.ok('number' === typeof val1 && 'number' === typeof val2); assert.ok( val1 === 0 && val2 === 1); assert.throws(() => failScript.compile(), /SyntaxError/); + assert.ok(Object.keys(failScript).includes('code')); + assert.ok(Object.keys(failScript).includes('filename')); + assert.ok(Object.keys(failScript).includes('compiler')); + assert.ok(!Object.keys(failScript).includes('_code')); }); });