UNPKG

baset-vm

Version:

VM package for BaseT project.

312 lines 13.1 kB
"use strict"; // tslint:disable-next-line:no-unused-expression (function sandbox(vm, host) { 'use strict'; const { Script } = host.require('vm'); const fs = host.require('fs'); const pa = host.require('path'); // tslint:disable-next-line:no-this-assignment const global = this; // tslint:disable-next-line:forin for (const name in host) { global[name] = host[name]; } delete global.require; Object.setPrototypeOf(global, Object.prototype); Object.defineProperties(global, { global: { value: global }, GLOBAL: { value: global }, root: { value: global }, isVM: { value: true }, }); const ERROR_CST = Error.captureStackTrace; /** * VMError definition. */ class VMError extends Error { constructor(message, code) { super(message); this.code = code; ERROR_CST(this, this.constructor); } } global.VMError = VMError; // TODO: check if really exists in runtime // tslint:disable-next-line:no-any const BUILTIN_MODULES = host.process.binding('natives'); const JSON_PARSE = JSON.parse; const BUILTINS = {}; const CACHE = {}; const EXTENSIONS = { ['.json'](module, filename) { module.exports = JSON_PARSE(fs.readFileSync(filename, 'utf8')); }, ['.node'](module, filename) { module.exports = host.require(filename); }, }; for (const ext of vm.options.sourceExtensions) { EXTENSIONS[ext] = (module, filename, dirname) => { if (typeof vm.options.require === 'boolean' || vm.options.require.context !== 'sandbox') { module.exports = host.require(filename); } else { // Load module let contents = fs.readFileSync(filename, 'utf8'); if (filename.endsWith(`tcomb${pa.sep}lib${pa.sep}isArray.js`)) { contents = `module.exports = function isArray(x) { return Array.isArray ? Array.isArray(x) : x instanceof Array; };`; } if (typeof vm.options.compiler === 'function') { contents = vm.options.compiler(contents, filename); } const code = `(function (exports, require, module, __filename, __dirname) { 'use strict'; ${contents} \n});`; // Precompile script const script = new Script(code, { filename: filename || 'vm.js', displayErrors: false, }); const closure = script.runInContext(global, { filename: filename || 'vm.js', displayErrors: false, }); // run script closure(module.exports, module.require, module, filename, dirname); } }; } /** * Resolve filename. */ function originalResolveFilename(path) { const resolvedPath = pa.resolve(path); const exists = fs.existsSync(resolvedPath); const isDir = exists ? fs.statSync(resolvedPath).isDirectory() : false; // direct file match if (exists && !isDir) return resolvedPath; // load as file for (const ext of vm.options.sourceExtensions) { if (fs.existsSync(`${resolvedPath}${ext}`)) { const stat = fs.statSync(`${resolvedPath}${ext}`); // some directories could be named like `asn1.js`, so we have to check it here if (stat && !stat.isDirectory()) { return `${resolvedPath}${ext}`; } } } if (fs.existsSync(`${resolvedPath}.node`)) return `${resolvedPath}.node`; if (fs.existsSync(`${resolvedPath}.json`)) return `${resolvedPath}.json`; // load as directory if (fs.existsSync(`${resolvedPath}/package.json`)) { try { const pkg = JSON.parse(fs.readFileSync(`${resolvedPath}/package.json`, 'utf8')); if (pkg.main === undefined) pkg.main = 'index.js'; return resolveFilename(`${resolvedPath}/${pkg.main}`); } catch (ex) { throw new VMError(`Module '${resolvedPath}' has invalid package.json`, 'EMODULEINVALID'); } } for (const ext of vm.options.sourceExtensions) { if (fs.existsSync(`${resolvedPath}/index${ext}`)) return `${resolvedPath}/index${ext}`; } if (fs.existsSync(`${resolvedPath}/index.node`)) return `${resolvedPath}/index.node`; return null; } const { resolveFilename: providedResolveFilename } = vm.options; const resolveFilename = providedResolveFilename ? (path) => providedResolveFilename(originalResolveFilename, path) : originalResolveFilename; function resolveFilenameLikeNode(modulename, currentDirname) { // it could be found by custom resolver let filename = resolveFilename(modulename); if (!filename) { // Check node_modules in path if (!currentDirname) throw new VMError('You must specify script path to load relative modules.', 'ENOPATH'); if (typeof vm.options.require !== 'boolean' && Array.isArray(vm.options.require.external)) { const isWhitelisted = vm.options.require.external.indexOf(modulename) !== -1; if (!isWhitelisted) throw new VMError(`The module '${modulename}' is not whitelisted in VM.`, 'EDENIED'); } const paths = currentDirname.split(pa.sep); while (paths.length) { const path = paths.join(pa.sep); // console.log(`${path}${pa.sep}node_modules${pa.sep}${modulename}`) filename = resolveFilename(`${path}${pa.sep}node_modules${pa.sep}${modulename}`); if (filename) break; paths.pop(); } } return filename; } function createModule(id, require, filename = id, exports = {}) { return { id, filename, exports, require, loaded: true, parent: null, children: [], paths: [id, filename], }; } /** * Builtin require. */ function getBuiltIn(modulename) { function requireBuiltin(builtInName) { if (BUILTINS[builtInName]) return BUILTINS[builtInName].exports; // Only compiled builtins are stored here if (builtInName === 'events') { try { const script = new Script(`(function (exports, require, module, process) { 'use strict'; ${BUILTIN_MODULES[builtInName]} \n});`, { filename: `${builtInName}.vm.js`, }); // setup module scope const module = createModule(builtInName, requireBuiltin, `${builtInName}.vm.js`); BUILTINS[builtInName] = module; // run script script.runInContext(global)(module.exports, module.require, module, host.process); return module.exports; } catch (e) { throw e; } } return host.require(builtInName); } if (typeof vm.options.require !== 'boolean' && host.Array.isArray(vm.options.require.builtin)) { if (vm.options.require.builtin.indexOf('*') >= 0) { if (vm.options.require.builtin.indexOf(`-${modulename}`) >= 0) { throw new VMError(`Access denied to require '${modulename}'`, 'EDENIED'); } } else if (vm.options.require.builtin.indexOf(modulename) === -1) { throw new VMError(`Access denied to require '${modulename}'`, 'EDENIED'); } } else if (typeof vm.options.require !== 'boolean' && vm.options.require.builtin && !host.Array.isArray(vm.options.require.builtin)) { if (!vm.options.require.builtin[modulename]) { throw new VMError(`Access denied to require '${modulename}'`, 'EDENIED'); } } else { throw new VMError(`Access denied to require '${modulename}'`, 'EDENIED'); } return requireBuiltin(modulename); } function getMock(modulename, skipedMock) { return (modulename !== skipedMock && typeof vm.options.require !== 'boolean' && vm.options.require.mock && vm.options.require.mock[modulename]); } /** * Prepare require. */ function prepareRequire(currentDirname, skipedMock) { return function require(modulename, mockedModuleName) { if (vm.options.nesting && modulename === 'baset-vm') return { NodeVM: host.NodeVM }; if (!vm.options.require) throw new VMError(`Access denied to require '${modulename}'`, 'EDENIED'); if (modulename === undefined) throw new VMError("Module '' not found.", 'ENOTFOUND'); if (typeof modulename !== 'string') throw new VMError(`Invalid module name '${modulename}'`, 'EINVALIDNAME'); // Mock? const mock = getMock(modulename, skipedMock); if (mock) { return require(mock, modulename); } // Builtin? if (BUILTIN_MODULES[modulename]) return getBuiltIn(modulename); // External? if (typeof vm.options.require === 'boolean' || !vm.options.require.external) { throw new VMError(`Access denied to require '${modulename}'`, 'EDENIED'); } let filename = null; if (/^(\.|\.\/|\.\.\/)/.exec(modulename)) { // Module is relative file, e.g. ./script.js or ../script.js if (!currentDirname) throw new VMError('You must specify script path to load relative modules.', 'ENOPATH'); filename = resolveFilename(`${currentDirname}/${modulename}`); } else { // Module is absolute file, e.g. /script.js or //server/script.js or C:\script.js filename = (/^(\/|\\|[a-zA-Z]:\\)/.exec(modulename)) ? resolveFilename(modulename) : resolveFilenameLikeNode(modulename, currentDirname); } if (!filename) throw new VMError(`Cannot find module '${modulename}'`, 'ENOTFOUND'); // return cache whenever possible if (CACHE[filename]) return CACHE[filename].exports; const dirname = pa.dirname(filename); const extname = pa.extname(filename); if (vm.options.require.root) { const requiredPath = pa.resolve(vm.options.require.root); if (dirname.indexOf(requiredPath) !== 0) { throw new VMError(`Module '${modulename}' is not allowed to be required. The path is outside the border!`, 'EDENIED'); } } const module = createModule(filename, prepareRequire(dirname, mockedModuleName)); CACHE[filename] = module; // lookup extensions if (EXTENSIONS[extname]) { EXTENSIONS[extname](module, filename, dirname); return module.exports; } throw new VMError(`Failed to load '${modulename}': Unknown type.`, 'ELOADFAIL'); }; } /** * Prepare sandbox. */ let processCwd; global.process = Object.assign({}, host.process, { argv: [], env: {}, cwd() { return processCwd || host.process.cwd(); }, chdir(directory) { processCwd = directory; } }); if (vm.options.console === 'inherit') { global.console = host.console; } else if (vm.options.console === 'redirect') { global.console = Object.assign({}, host.console, { log(...args) { vm.emit('console.log', ...args); }, info(...args) { vm.emit('console.info', ...args); }, warn(...args) { vm.emit('console.warn', ...args); }, error(...args) { vm.emit('console.error', ...args); }, dir(...args) { vm.emit('console.dir', ...args); }, time: () => { }, timeEnd: () => { }, trace(...args) { vm.emit('console.trace', ...args); } }); } /* Return contextized require. */ return prepareRequire; }); //# sourceMappingURL=sandbox.js.map