UNPKG

deno-vm

Version:

A VM module that provides a secure runtime environment via Deno.

852 lines (843 loc) 30.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var http = require('http'); var ws = require('ws'); var path = require('path'); var child_process = require('child_process'); var base64Js = require('base64-js'); var process$1 = _interopDefault(require('process')); var stream = require('stream'); // Global Channel ID counter. let channelIDCounter = 0; /** * Defines a class that implements the Channel Messaging API for the worker. */ class MessageChannel { constructor(channel) { const id = typeof channel !== 'undefined' ? channel : channelIDCounter++; this.port1 = new MessagePort(id); this.port2 = new MessagePort(id); MessagePort.link(this.port1, this.port2); } } /** * Defines a class that allows messages sent from one port to be recieved at the other port. */ class MessagePort { constructor(channelID) { /** * Represents an event handler for the "message" event, that is a function to be called when a message is recieved from the worker. */ this.onmessage = null; this._transferred = false; this._channelId = channelID; this._listeners = []; } get channelID() { return this._channelId; } get transferred() { return this._transferred; } addEventListener(type, listener) { if (type === 'message') { this._listeners.push(listener); } } removeEventListener(type, listener) { if (type === 'message') { const index = this._listeners.indexOf(listener); if (index >= 0) { this._listeners.splice(index, 1); } } } postMessage(data, transferrable) { if (this.transferred) { this._sendMessage(data, transferrable); } else { this._other._recieveMessage(data); } } start() { } close() { } transfer(sendMessage) { if (this.transferred) { throw new Error('Already transferred'); } this._transferred = true; this._other._transferred = true; this._other._sendMessage = sendMessage; return this._other._recieveMessage.bind(this._other); } _recieveMessage(data) { const event = { data, }; if (this.onmessage) { this.onmessage(event); } for (let onmessage of this._listeners) { onmessage(event); } } /** * Links the two message ports. * @param port1 The first port. * @param port2 The second port. */ static link(port1, port2) { port1._other = port2; port2._other = port1; } } const HAS_CIRCULAR_REF_OR_TRANSFERRABLE = Symbol('hasCircularRef'); /** * Serializes the given value into a new object that is flat and contains no circular references. * * The returned object is JSON-safe and contains a root which is the entry point to the data structure and optionally * contains a refs property which is a flat map of references. * * If the refs property is defined, then the data structure was circular. * * @param value The value to serialize. * @param transferrable The transferrable list. */ function serializeStructure(value, transferrable) { if ((typeof value !== 'object' && typeof value !== 'bigint') || value === null) { return { root: value, }; } else { let map = new Map(); const result = _serializeObject(value, map); if (map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] === true) { let refs = {}; for (let [key, ref] of map) { refs[ref.id] = ref.obj; } return { root: result, refs: refs, }; } return { root: value, }; } } /** * Deserializes the given structure into its original form. * @param value The structure to deserialize. */ function deserializeStructure(value) { if ('refs' in value) { let map = new Map(); let list = []; const result = _deserializeRef(value, value.root[0], map, list); return { data: result, transferred: list, }; } else { return { data: value.root, transferred: [], }; } } function _serializeObject(value, map) { if (typeof value !== 'object' && typeof value !== 'bigint') { return value; } if (map.has(value)) { const ref = map.get(value); map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; return [ref.id]; } let id = '$' + map.size; if (value instanceof Uint8Array || value instanceof Uint16Array || value instanceof Uint32Array || value instanceof Int8Array || value instanceof Int16Array || value instanceof Int32Array || value instanceof ArrayBuffer) { let ref = { root: base64Js.fromByteArray(value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength)), type: value.constructor.name, }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj: ref, }); return [id]; } else if (typeof value === 'bigint') { const root = value.toString(); const obj = { root, type: 'BigInt', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); return [id]; } else if (Array.isArray(value)) { let root = []; let obj = { root, }; map.set(value, { id, obj, }); for (let prop of value) { root.push(_serializeObject(prop, map)); } return [id]; } else if (value instanceof Date) { const obj = { root: value.toISOString(), type: 'Date', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); return [id]; } else if (value instanceof RegExp) { const obj = { root: { source: value.source, flags: value.flags, }, type: 'RegExp', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); return [id]; } else if (value instanceof Map) { let root = []; let obj = { root, type: 'Map', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); for (let prop of value) { root.push(_serializeObject(prop, map)); } return [id]; } else if (value instanceof Set) { let root = []; let obj = { root, type: 'Set', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); for (let prop of value) { root.push(_serializeObject(prop, map)); } return [id]; } else if (value instanceof Error) { let obj = { root: { name: value.name, message: value.message, stack: value.stack, }, type: 'Error', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); return [id]; } else if (value instanceof MessagePort) { if (!value.transferred) { throw new Error('Port must be transferred before serialization. Did you forget to add it to the transfer list?'); } let obj = { root: { channel: value.channelID, }, type: 'MessagePort', }; map[HAS_CIRCULAR_REF_OR_TRANSFERRABLE] = true; map.set(value, { id, obj, }); return [id]; } else if (value instanceof Object) { let root = {}; let ref = { root, }; map.set(value, { id, obj: ref, }); for (let prop in value) { if (Object.hasOwnProperty.call(value, prop)) { root[prop] = _serializeObject(value[prop], map); } } return [id]; } } function _deserializeRef(structure, ref, map, transfered) { if (map.has(ref)) { return map.get(ref); } const refData = structure.refs[ref]; if ('type' in refData) { const arrayTypes = [ 'ArrayBuffer', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array', ]; if (arrayTypes.indexOf(refData.type) >= 0) { const bytes = base64Js.toByteArray(refData.root); const final = refData.type == 'Uint8Array' ? bytes : refData.type === 'ArrayBuffer' ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : refData.type === 'Int8Array' ? new Int8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Int8Array.BYTES_PER_ELEMENT) : refData.type == 'Int16Array' ? new Int16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Int16Array.BYTES_PER_ELEMENT) : refData.type == 'Int32Array' ? new Int32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Int32Array.BYTES_PER_ELEMENT) : refData.type == 'Uint16Array' ? new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Uint16Array.BYTES_PER_ELEMENT) : refData.type == 'Uint32Array' ? new Uint32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Uint32Array.BYTES_PER_ELEMENT) : null; map.set(ref, final); return final; } else if (refData.type === 'BigInt') { const final = BigInt(refData.root); map.set(ref, final); return final; } else if (refData.type === 'Date') { const final = new Date(refData.root); map.set(ref, final); return final; } else if (refData.type === 'RegExp') { const final = new RegExp(refData.root.source, refData.root.flags); map.set(ref, final); return final; } else if (refData.type === 'Map') { let final = new Map(); map.set(ref, final); for (let value of refData.root) { const [key, val] = _deserializeRef(structure, value[0], map, transfered); final.set(key, val); } return final; } else if (refData.type === 'Set') { let final = new Set(); map.set(ref, final); for (let value of refData.root) { const val = Array.isArray(value) ? _deserializeRef(structure, value[0], map, transfered) : value; final.add(val); } return final; } else if (refData.type === 'Error') { let proto = Error.prototype; if (refData.root.name === 'EvalError') { proto = EvalError.prototype; } else if (refData.root.name === 'RangeError') { proto = RangeError.prototype; } else if (refData.root.name === 'ReferenceError') { proto = ReferenceError.prototype; } else if (refData.root.name === 'SyntaxError') { proto = SyntaxError.prototype; } else if (refData.root.name === 'TypeError') { proto = TypeError.prototype; } else if (refData.root.name === 'URIError') { proto = URIError.prototype; } let final = Object.create(proto); if (typeof refData.root.message !== 'undefined') { Object.defineProperty(final, 'message', { value: refData.root.message, writable: true, enumerable: false, configurable: true, }); } if (typeof refData.root.stack !== 'undefined') { Object.defineProperty(final, 'stack', { value: refData.root.stack, writable: true, enumerable: false, configurable: true, }); } return final; } else if (refData.type === 'MessagePort') { const channel = new MessageChannel(refData.root.channel); map.set(ref, channel.port1); transfered.push(channel.port2); return channel.port1; } } else if (Array.isArray(refData.root)) { let arr = []; map.set(ref, arr); for (let value of refData.root) { arr.push(Array.isArray(value) ? _deserializeRef(structure, value[0], map, transfered) : value); } return arr; } else if (typeof refData.root === 'object') { let obj = {}; map.set(ref, obj); for (let prop in refData.root) { if (Object.hasOwnProperty.call(refData.root, prop)) { const value = refData.root[prop]; obj[prop] = Array.isArray(value) ? _deserializeRef(structure, value[0], map, transfered) : value; } } return obj; } map.set(ref, refData.root); return refData.root; } function polyfillMessageChannel() { const anyGlobalThis = globalThis; if (typeof anyGlobalThis.MessageChannel === 'undefined') { anyGlobalThis.MessageChannel = MessageChannel; anyGlobalThis.MessagePort = MessagePort; } } /** * Forcefully kills the process with the given ID. * On Linux/Unix, this means sending the process the SIGKILL signal. * On Windows, this means using the taskkill executable to kill the process. * @param pid The ID of the process to kill. */ function forceKill(pid) { const isWindows = /^win/.test(process.platform); if (isWindows) { return killWindows(pid); } else { return killUnix(pid); } } function killWindows(pid) { child_process.execSync(`taskkill /PID ${pid} /T /F`); } function killUnix(pid) { try { const signal = 'SIGKILL'; process.kill(pid, signal); } catch (e) { // Allow this call to fail with // ESRCH, which meant that the process // to be killed was already dead. // But re-throw on other codes. if (e.code !== 'ESRCH') { throw e; } } } const DEFAULT_DENO_BOOTSTRAP_SCRIPT_PATH = __dirname.endsWith('src') ? path.resolve(__dirname, '../deno/index.ts') : path.resolve(__dirname, '../../deno/index.ts'); /** * The DenoWorker class is a WebWorker-like interface for interacting with Deno. * * Because Deno is an isolated environment, this worker gives you the ability to run untrusted JavaScript code without * potentially compromising your system. */ class DenoWorker { /** * Creates a new DenoWorker instance and injects the given script. * @param script The JavaScript that the worker should be started with. */ constructor(script, options) { /** * Represents an event handler for the "message" event, that is a function to be called when a message is recieved from the worker. */ this.onmessage = null; /** * Represents an event handler for the "exit" event. That is, a function to be called when the Deno worker process is terminated. */ this.onexit = null; this._onmessageListeners = []; this._onexitListeners = []; this._pendingMessages = []; this._available = false; this._socketClosed = false; this._stdout = new stream.Readable(); this._stdout.setEncoding('utf-8'); this._stderr = new stream.Readable(); this._stdout.setEncoding('utf-8'); this._stderr.setEncoding('utf-8'); this._options = Object.assign({ denoExecutable: 'deno', denoBootstrapScriptPath: DEFAULT_DENO_BOOTSTRAP_SCRIPT_PATH, reload: process$1.env.NODE_ENV !== 'production', logStdout: true, logStderr: true, denoUnstable: false, location: undefined, permissions: {}, denoV8Flags: [], denoImportMapPath: '', denoLockFilePath: '', denoCachedOnly: false, denoNoCheck: false, unsafelyIgnoreCertificateErrors: false, spawnOptions: {}, }, options || {}); this._ports = new Map(); this._httpServer = http.createServer(); this._server = new ws.Server({ server: this._httpServer, }); this._server.on('connection', (socket) => { if (this._socket) { socket.close(); return; } if (this._socketClosed) { socket.close(); this._socket = null; return; } this._socket = socket; socket.on('message', (message) => { if (typeof message === 'string') { const structuredData = JSON.parse(message); const channel = structuredData.channel; const deserialized = deserializeStructure(structuredData); const data = deserialized.data; if (deserialized.transferred) { this._handleTransferrables(deserialized.transferred); } if (!this._available && data && data.type === 'init') { this._available = true; let pendingMessages = this._pendingMessages; this._pendingMessages = []; for (let message of pendingMessages) { socket.send(message); } } else { if (typeof channel === 'number' || typeof channel === 'string') { const portData = this._ports.get(channel); if (portData) { portData.recieveData(data); } } else { const event = { data, }; if (this.onmessage) { this.onmessage(event); } for (let onmessage of this._onmessageListeners) { onmessage(event); } } } } }); socket.on('close', () => { this._available = false; this._socket = null; }); }); this._httpServer.listen({ host: '127.0.0.1', port: 0 }, () => { var _a; if (this._terminated) { this._httpServer.close(); return; } const addr = this._httpServer.address(); let connectAddress; let allowAddress; if (typeof addr === 'string') { connectAddress = addr; } else { connectAddress = `ws://${addr.address}:${addr.port}`; allowAddress = `${addr.address}:${addr.port}`; } let scriptArgs; if (typeof script === 'string') { scriptArgs = ['script', script]; } else { scriptArgs = ['import', script.href]; } let runArgs = []; addOption(runArgs, '--reload', this._options.reload); if (this._options.denoUnstable === true) { runArgs.push('--unstable'); } else if (this._options.denoUnstable) { for (let [key] of Object.entries(this._options.denoUnstable).filter(([_key, val]) => val)) { runArgs.push(`--unstable-${key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase())}`); } } addOption(runArgs, '--cached-only', this._options.denoCachedOnly); addOption(runArgs, '--no-check', this._options.denoNoCheck); addOption(runArgs, '--unsafely-ignore-certificate-errors', this._options.unsafelyIgnoreCertificateErrors); if (this._options.location) { addOption(runArgs, '--location', [this._options.location]); } if (this._options.denoV8Flags.length > 0) { addOption(runArgs, '--v8-flags', this._options.denoV8Flags); } if (this._options.denoImportMapPath) { addOption(runArgs, '--import-map', [ this._options.denoImportMapPath, ]); } if (this._options.denoLockFilePath) { addOption(runArgs, '--lock', [this._options.denoLockFilePath]); } if (this._options.permissions) { addOption(runArgs, '--allow-all', this._options.permissions.allowAll); if (!this._options.permissions.allowAll) { addOption(runArgs, '--allow-net', typeof this._options.permissions.allowNet === 'boolean' ? this._options.permissions.allowNet : this._options.permissions.allowNet ? [ ...this._options.permissions.allowNet, allowAddress, ] : [allowAddress]); // Ensures the `allowAddress` isn't denied const deniedAddresses = (_a = this._options.permissions.denyNet) === null || _a === void 0 ? void 0 : _a.filter((address) => address !== allowAddress); addOption(runArgs, '--deny-net', // Ensures an empty array isn't used (deniedAddresses === null || deniedAddresses === void 0 ? void 0 : deniedAddresses.length) ? deniedAddresses : false); addOption(runArgs, '--allow-read', this._options.permissions.allowRead); addOption(runArgs, '--allow-write', this._options.permissions.allowWrite); addOption(runArgs, '--allow-env', this._options.permissions.allowEnv); addOption(runArgs, '--allow-plugin', this._options.permissions.allowPlugin); addOption(runArgs, '--allow-hrtime', this._options.permissions.allowHrtime); } } this._process = child_process.spawn(this._options.denoExecutable, [ 'run', ...runArgs, this._options.denoBootstrapScriptPath, connectAddress, ...scriptArgs, ], this._options.spawnOptions); this._process.on('exit', (code, signal) => { this.terminate(); if (this.onexit) { this.onexit(code, signal); } for (let onexit of this._onexitListeners) { onexit(code, signal); } }); this._stdout = this._process.stdout; this._stderr = this._process.stderr; if (this._options.logStdout) { this.stdout.setEncoding('utf-8'); this.stdout.on('data', (data) => { console.log('[deno]', data); }); } if (this._options.logStderr) { this.stderr.setEncoding('utf-8'); this.stderr.on('data', (data) => { console.log('[deno]', data); }); } }); } get stdout() { return this._stdout; } get stderr() { return this._stderr; } /** * Sends a message to the worker. * @param data The data to be sent. Copied via the Structured Clone algorithm so circular references are supported in addition to typed arrays. * @param transfer Values that should be transferred. This should include any typed arrays that are referenced in the data. */ postMessage(data, transfer) { return this._postMessage(null, data, transfer); } /** * Closes the websocket, which may allow the process to exit natually. */ closeSocket() { this._socketClosed = true; if (this._socket) { this._socket.close(); this._socket = null; } } /** * Terminates the worker and cleans up unused resources. */ terminate() { this._terminated = true; this._socketClosed = true; if (this._process && this._process.exitCode === null) { // this._process.kill(); forceKill(this._process.pid); } this._process = null; if (this._httpServer) { this._httpServer.close(); } if (this._server) { this._server.close(); this._server = null; } this._socket = null; this._pendingMessages = null; } /** * Adds the given listener for the "message" or "exit" event. * @param type The type of the event. (Always either "message" or "exit") * @param listener The listener to add for the event. */ addEventListener(type, listener) { if (type === 'message') { this._onmessageListeners.push(listener); } else if (type === 'exit') { this._onexitListeners.push(listener); } } /** * Removes the given listener for the "message" or "exit" event. * @param type The type of the event. (Always either "message" or "exit") * @param listener The listener to remove for the event. */ removeEventListener(type, listener) { if (type === 'message') { const index = this._onmessageListeners.indexOf(listener); if (index >= 0) { this._onmessageListeners.splice(index, 1); } } if (type === 'exit') { const index = this._onexitListeners.indexOf(listener); if (index >= 0) { this._onexitListeners.splice(index, 1); } } } _postMessage(channel, data, transfer) { if (this._terminated) { return; } this._handleTransferrables(transfer); const structuredData = serializeStructure(data); if (channel !== null) { structuredData.channel = channel; } const json = JSON.stringify(structuredData); if (!this._available) { this._pendingMessages.push(json); } else if (this._socket) { this._socket.send(json); } } _handleTransferrables(transfer) { if (transfer) { for (let t of transfer) { if (t instanceof MessagePort) { if (!t.transferred) { const channelID = t.channelID; this._ports.set(t.channelID, { port: t, recieveData: t.transfer((data, transfer) => { this._postMessage(channelID, data, transfer); }), }); } } } } } } function addOption(list, name, option) { if (option === true) { list.push(`${name}`); } else if (Array.isArray(option)) { let values = option.join(','); list.push(`${name}=${values}`); } } exports.DenoWorker = DenoWorker; exports.MessageChannel = MessageChannel; exports.MessagePort = MessagePort; exports.forceKill = forceKill; exports.polyfillMessageChannel = polyfillMessageChannel; //# sourceMappingURL=index.js.map