123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539 |
- '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;
|