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'));
});
});