You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							540 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
	
	
							540 lines
						
					
					
						
							14 KiB
						
					
					
				| '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 modulusedRequiree 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 pa = require('path');
 | |
| const {
 | |
| 	Script,
 | |
| 	createContext
 | |
| } = require('vm');
 | |
| const {
 | |
| 	EventEmitter
 | |
| } = require('events');
 | |
| const {
 | |
| 	INSPECT_MAX_BYTES
 | |
| } = require('buffer');
 | |
| const {
 | |
| 	createBridge,
 | |
| 	VMError
 | |
| } = require('./bridge');
 | |
| const {
 | |
| 	transformer,
 | |
| 	INTERNAL_STATE_NAME
 | |
| } = require('./transformer');
 | |
| const {
 | |
| 	lookupCompiler
 | |
| } = require('./compiler');
 | |
| const {
 | |
| 	VMScript
 | |
| } = require('./script');
 | |
| 
 | |
| const objectDefineProperties = Object.defineProperties;
 | |
| 
 | |
| /**
 | |
|  * Host objects
 | |
|  *
 | |
|  * @private
 | |
|  */
 | |
| const HOST = Object.freeze({
 | |
| 	Buffer,
 | |
| 	Function,
 | |
| 	Object,
 | |
| 	transformAndCheck,
 | |
| 	INSPECT_MAX_BYTES,
 | |
| 	INTERNAL_STATE_NAME
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * Compile a script.
 | |
|  *
 | |
|  * @private
 | |
|  * @param {string} filename - Filename of the script.
 | |
|  * @param {string} script - Script.
 | |
|  * @return {vm.Script} The compiled script.
 | |
|  */
 | |
| function compileScript(filename, script) {
 | |
| 	return new Script(script, {
 | |
| 		__proto__: null,
 | |
| 		filename,
 | |
| 		displayErrors: false
 | |
| 	});
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Default run options for vm.Script.runInContext
 | |
|  *
 | |
|  * @private
 | |
|  */
 | |
| const DEFAULT_RUN_OPTIONS = Object.freeze({__proto__: null, displayErrors: false});
 | |
| 
 | |
| function checkAsync(allow) {
 | |
| 	if (!allow) throw new VMError('Async not available');
 | |
| }
 | |
| 
 | |
| function transformAndCheck(args, code, isAsync, isGenerator, allowAsync) {
 | |
| 	const ret = transformer(args, code, isAsync, isGenerator, undefined);
 | |
| 	checkAsync(allowAsync || !ret.hasAsync);
 | |
| 	return ret.code;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * This callback will be called and has a specific time to finish.<br>
 | |
|  * No parameters will be supplied.<br>
 | |
|  * If parameters are required, use a closure.
 | |
|  *
 | |
|  * @private
 | |
|  * @callback runWithTimeout
 | |
|  * @return {*}
 | |
|  *
 | |
|  */
 | |
| 
 | |
| let cacheTimeoutContext = null;
 | |
| let cacheTimeoutScript = null;
 | |
| 
 | |
| /**
 | |
|  * 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 (!cacheTimeoutContext) {
 | |
| 		cacheTimeoutContext = createContext();
 | |
| 		cacheTimeoutScript = new Script('fn()', {
 | |
| 			__proto__: null,
 | |
| 			filename: 'timeout_bridge.js',
 | |
| 			displayErrors: false
 | |
| 		});
 | |
| 	}
 | |
| 	cacheTimeoutContext.fn = fn;
 | |
| 	try {
 | |
| 		return cacheTimeoutScript.runInContext(cacheTimeoutContext, {
 | |
| 			__proto__: null,
 | |
| 			displayErrors: false,
 | |
| 			timeout
 | |
| 		});
 | |
| 	} finally {
 | |
| 		cacheTimeoutContext.fn = null;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| const bridgeScript = compileScript(`${__dirname}/bridge.js`,
 | |
| 	`(function(global) {"use strict"; const exports = {};${fs.readFileSync(`${__dirname}/bridge.js`, 'utf8')}\nreturn exports;})`);
 | |
| const setupSandboxScript = compileScript(`${__dirname}/setup-sandbox.js`,
 | |
| 	`(function(global, host, bridge, data, context) { ${fs.readFileSync(`${__dirname}/setup-sandbox.js`, 'utf8')}\n})`);
 | |
| const getGlobalScript = compileScript('get_global.js', 'this');
 | |
| 
 | |
| let getGeneratorFunctionScript = null;
 | |
| let getAsyncFunctionScript = null;
 | |
| let getAsyncGeneratorFunctionScript = null;
 | |
| try {
 | |
| 	getGeneratorFunctionScript = compileScript('get_generator_function.js', '(function*(){}).constructor');
 | |
| } catch (ex) {}
 | |
| try {
 | |
| 	getAsyncFunctionScript = compileScript('get_async_function.js', '(async function(){}).constructor');
 | |
| } catch (ex) {}
 | |
| try {
 | |
| 	getAsyncGeneratorFunctionScript = compileScript('get_async_generator_function.js', '(async function*(){}).constructor');
 | |
| } catch (ex) {}
 | |
| 
 | |
| /**
 | |
|  * Class VM.
 | |
|  *
 | |
|  * @public
 | |
|  */
 | |
| class VM extends EventEmitter {
 | |
| 
 | |
| 	/**
 | |
| 	 * The timeout for {@link VM#run} calls.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @since v3.9.0
 | |
| 	 * @member {number} timeout
 | |
| 	 * @memberOf VM#
 | |
| 	 */
 | |
| 
 | |
| 	/**
 | |
| 	 * Get the global sandbox object.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @readonly
 | |
| 	 * @since v3.9.0
 | |
| 	 * @member {Object} sandbox
 | |
| 	 * @memberOf VM#
 | |
| 	 */
 | |
| 
 | |
| 	/**
 | |
| 	 * The compiler to use to get the JavaScript code.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @readonly
 | |
| 	 * @since v3.9.0
 | |
| 	 * @member {(string|compileCallback)} compiler
 | |
| 	 * @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)().<br>
 | |
| 	 * Only available for node v10+.
 | |
| 	 * @param {boolean} [options.wasm=true] - Allow to run wasm code.<br>
 | |
| 	 * Only available for node v10+.
 | |
| 	 * @param {boolean} [options.allowAsync=true] - Allows for async functions.
 | |
| 	 * @throws {VMError} If the compiler is unknown.
 | |
| 	 */
 | |
| 	constructor(options = {}) {
 | |
| 		super();
 | |
| 
 | |
| 		// Read all options
 | |
| 		const {
 | |
| 			timeout,
 | |
| 			sandbox,
 | |
| 			compiler = 'javascript',
 | |
| 			allowAsync: optAllowAsync = true
 | |
| 		} = options;
 | |
| 		const allowEval = options.eval !== false;
 | |
| 		const allowWasm = options.wasm !== false;
 | |
| 		const allowAsync = optAllowAsync && !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 = createContext(undefined, {
 | |
| 			__proto__: null,
 | |
| 			codeGeneration: {
 | |
| 				__proto__: null,
 | |
| 				strings: allowEval,
 | |
| 				wasm: allowWasm
 | |
| 			}
 | |
| 		});
 | |
| 
 | |
| 		const sandboxGlobal = getGlobalScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
 | |
| 
 | |
| 		// Initialize the sandbox bridge
 | |
| 		const {
 | |
| 			createBridge: sandboxCreateBridge
 | |
| 		} = bridgeScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal);
 | |
| 
 | |
| 		// Initialize the bridge
 | |
| 		const bridge = createBridge(sandboxCreateBridge, () => {});
 | |
| 
 | |
| 		const data = {
 | |
| 			__proto__: null,
 | |
| 			allowAsync
 | |
| 		};
 | |
| 
 | |
| 		if (getGeneratorFunctionScript) {
 | |
| 			data.GeneratorFunction = getGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
 | |
| 		}
 | |
| 		if (getAsyncFunctionScript) {
 | |
| 			data.AsyncFunction = getAsyncFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
 | |
| 		}
 | |
| 		if (getAsyncGeneratorFunctionScript) {
 | |
| 			data.AsyncGeneratorFunction = getAsyncGeneratorFunctionScript.runInContext(_context, DEFAULT_RUN_OPTIONS);
 | |
| 		}
 | |
| 
 | |
| 		// Create the bridge between the host and the sandbox.
 | |
| 		const internal = setupSandboxScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal, HOST, bridge.other, data, _context);
 | |
| 
 | |
| 		const runScript = (script) => {
 | |
| 			// This closure is intentional to hide _context and bridge since the allow to access the sandbox directly which is unsafe.
 | |
| 			let ret;
 | |
| 			try {
 | |
| 				ret = script.runInContext(_context, DEFAULT_RUN_OPTIONS);
 | |
| 			} catch (e) {
 | |
| 				throw bridge.from(e);
 | |
| 			}
 | |
| 			return bridge.from(ret);
 | |
| 		};
 | |
| 
 | |
| 		const makeReadonly = (value, mock) => {
 | |
| 			try {
 | |
| 				internal.readonly(value, mock);
 | |
| 			} catch (e) {
 | |
| 				throw bridge.from(e);
 | |
| 			}
 | |
| 			return value;
 | |
| 		};
 | |
| 
 | |
| 		const makeProtected = (value) => {
 | |
| 			const sandboxBridge = bridge.other;
 | |
| 			try {
 | |
| 				sandboxBridge.fromWithFactory(sandboxBridge.protectedFactory, value);
 | |
| 			} catch (e) {
 | |
| 				throw bridge.from(e);
 | |
| 			}
 | |
| 			return value;
 | |
| 		};
 | |
| 
 | |
| 		const addProtoMapping = (hostProto, sandboxProto) => {
 | |
| 			const sandboxBridge = bridge.other;
 | |
| 			let otherProto;
 | |
| 			try {
 | |
| 				otherProto = sandboxBridge.from(sandboxProto);
 | |
| 				sandboxBridge.addProtoMapping(otherProto, hostProto);
 | |
| 			} catch (e) {
 | |
| 				throw bridge.from(e);
 | |
| 			}
 | |
| 			bridge.addProtoMapping(hostProto, otherProto);
 | |
| 		};
 | |
| 
 | |
| 		const addProtoMappingFactory = (hostProto, sandboxProtoFactory) => {
 | |
| 			const sandboxBridge = bridge.other;
 | |
| 			const factory = () => {
 | |
| 				const proto = sandboxProtoFactory(this);
 | |
| 				bridge.addProtoMapping(hostProto, proto);
 | |
| 				return proto;
 | |
| 			};
 | |
| 			try {
 | |
| 				const otherProtoFactory = sandboxBridge.from(factory);
 | |
| 				sandboxBridge.addProtoMappingFactory(otherProtoFactory, hostProto);
 | |
| 			} catch (e) {
 | |
| 				throw bridge.from(e);
 | |
| 			}
 | |
| 		};
 | |
| 
 | |
| 		// Define the properties of this object.
 | |
| 		// Use Object.defineProperties here to be able to
 | |
| 		// hide and set properties read-only.
 | |
| 		objectDefineProperties(this, {
 | |
| 			__proto__: null,
 | |
| 			timeout: {
 | |
| 				__proto__: null,
 | |
| 				value: timeout,
 | |
| 				writable: true,
 | |
| 				enumerable: true
 | |
| 			},
 | |
| 			compiler: {
 | |
| 				__proto__: null,
 | |
| 				value: compiler,
 | |
| 				enumerable: true
 | |
| 			},
 | |
| 			sandbox: {
 | |
| 				__proto__: null,
 | |
| 				value: bridge.from(sandboxGlobal),
 | |
| 				enumerable: true
 | |
| 			},
 | |
| 			_runScript: {__proto__: null, value: runScript},
 | |
| 			_makeReadonly: {__proto__: null, value: makeReadonly},
 | |
| 			_makeProtected: {__proto__: null, value: makeProtected},
 | |
| 			_addProtoMapping: {__proto__: null, value: addProtoMapping},
 | |
| 			_addProtoMappingFactory: {__proto__: null, value: addProtoMappingFactory},
 | |
| 			_compiler: {__proto__: null, value: resolvedCompiler},
 | |
| 			_allowAsync: {__proto__: null, value: allowAsync}
 | |
| 		});
 | |
| 
 | |
| 		// prepare global sandbox
 | |
| 		if (sandbox) {
 | |
| 			this.setGlobals(sandbox);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Adds all the values to the globals.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @since v3.9.0
 | |
| 	 * @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.sandbox[name] = values[name];
 | |
| 			}
 | |
| 		}
 | |
| 		return this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Set a global value.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @since v3.9.0
 | |
| 	 * @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.sandbox[name] = value;
 | |
| 		return this;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Get a global value.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @since v3.9.0
 | |
| 	 * @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.sandbox[name];
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Freezes the object inside VM making it read-only. Not available for primitive values.
 | |
| 	 *
 | |
| 	 * @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.readonly(value);
 | |
| 		if (globalName) this.sandbox[globalName] = value;
 | |
| 		return value;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Freezes the object inside VM making it read-only. Not available for primitive values.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @param {*} value - Object to freeze.
 | |
| 	 * @param {*} [mock] - When the object does not have a property the mock is used before prototype lookup.
 | |
| 	 * @return {*} Object to freeze.
 | |
| 	 */
 | |
| 	readonly(value, mock) {
 | |
| 		return this._makeReadonly(value, mock);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Protects the object inside VM making impossible to set functions as it's properties. Not available for primitive values.
 | |
| 	 *
 | |
| 	 * @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._makeProtected(value);
 | |
| 		if (globalName) this.sandbox[globalName] = value;
 | |
| 		return value;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Run the code in VM.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @param {(string|VMScript)} 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.<br>
 | |
| 	 * 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, options) {
 | |
| 		let script;
 | |
| 		let filename;
 | |
| 
 | |
| 		if (typeof options === 'object') {
 | |
| 			filename = options.filename;
 | |
| 		} else {
 | |
| 			filename = options;
 | |
| 		}
 | |
| 
 | |
| 		if (code instanceof VMScript) {
 | |
| 			script = code._compileVM();
 | |
| 			checkAsync(this._allowAsync || !code._hasAsync);
 | |
| 		} else {
 | |
| 			const useFileName = filename || 'vm.js';
 | |
| 			let scriptCode = this._compiler(code, useFileName);
 | |
| 			const ret = transformer(null, scriptCode, false, false, useFileName);
 | |
| 			scriptCode = ret.code;
 | |
| 			checkAsync(this._allowAsync || !ret.hasAsync);
 | |
| 			// Compile the script here so that we don't need to create a instance of VMScript.
 | |
| 			script = new Script(scriptCode, {
 | |
| 				__proto__: null,
 | |
| 				filename: useFileName,
 | |
| 				displayErrors: false
 | |
| 			});
 | |
| 		}
 | |
| 
 | |
| 		if (!this.timeout) {
 | |
| 			return this._runScript(script);
 | |
| 		}
 | |
| 
 | |
| 		return doWithTimeout(() => {
 | |
| 			return this._runScript(script);
 | |
| 		}, this.timeout);
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Run the code in VM.
 | |
| 	 *
 | |
| 	 * @public
 | |
| 	 * @since v3.9.0
 | |
| 	 * @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);
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| exports.VM = VM;
 |