deno-vm
Version:
A VM module that provides a secure runtime environment via Deno.
852 lines (843 loc) • 30.2 kB
JavaScript
'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