540 lines
14 KiB
JavaScript
540 lines
14 KiB
JavaScript
'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;
|