UNPKG

@casual-simulation/aux-vm-browser

Version:

A set of utilities required to securely run an AUX in a web browser.

321 lines 11.8 kB
import { ProxyBridgePartitionImpl } from '@casual-simulation/aux-common'; import { Subject } from 'rxjs'; import { wrap, proxy, expose, transfer, createEndpoint } from 'comlink'; import { setupChannel, waitForLoad } from '../html/IFrameHelpers'; import { remapProgressPercent } from '@casual-simulation/aux-common'; import { RemoteAuxVM } from '@casual-simulation/aux-vm-client/vm/RemoteAuxVM'; import { getBaseOrigin, getVMOrigin } from './AuxVMUtils'; export const DEFAULT_IFRAME_ALLOW_ATTRIBUTE = 'accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking'; export const DEFAULT_IFRAME_SANDBOX_ATTRIBUTE = 'allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts allow-downloads'; /** * Defines an interface for an AUX that is run inside a virtual machine. * That is, the AUX is run inside a web worker. */ export default class AuxVMImpl { /** * The ID of the simulation. */ get id() { return this._id; } get configBotId() { return this._config.configBotId; } /** * Creates a new Simulation VM. * @param id The ID of the simulation. * @param origin The origin of the simulation. * @param config The config that should be used. * @param relaxOrigin Whether to relax the origin of the VM. */ constructor(id, origin, config, relaxOrigin = false) { this._batchPending = false; this._autoBatch = true; this._batchedEvents = []; this._id = id; this._origin = origin; this._config = config; this._relaxOrigin = relaxOrigin; this._batchedEvents = []; this._localEvents = new Subject(); this._deviceEvents = new Subject(); this._stateUpdated = new Subject(); this._versionUpdated = new Subject(); this._connectionStateChanged = new Subject(); this._onError = new Subject(); this._subVMAdded = new Subject(); this._subVMRemoved = new Subject(); this._subVMMap = new Map(); this._onAuthMessage = new Subject(); } get origin() { return this._origin; } get subVMAdded() { return this._subVMAdded; } get subVMRemoved() { return this._subVMRemoved; } get connectionStateChanged() { return this._connectionStateChanged; } get onError() { return this._onError; } get onAuthMessage() { return this._onAuthMessage; } /** * Initaializes the VM. */ async init() { return await this._init(); } async _init() { let origin = getVMOrigin(this._config.config.vmOrigin, location.origin, this._id); let enableDom = this._config.config.enableDom; if (enableDom && getBaseOrigin(location.origin) === getBaseOrigin(origin) && !this._config.config.debug) { console.error(`[AuxVMImpl] Cannot use DOM when base origin is the same as the VM origin. ${origin} should not share the same base domain as ${location.origin}.`); console.error('[AuxVMImpl] Using WebWorker isolated VM.'); console.error('[AuxVMImpl] To use DOM, enable debug mode or use a separate VM origin.'); enableDom = false; } if (this._relaxOrigin) { const baseOrigin = getBaseOrigin(origin); console.log('[AuxVMImpl] Relaxing origin to:', baseOrigin); origin = baseOrigin; } console.log('origin', origin); const iframeUrl = new URL(enableDom ? '/aux-vm-iframe-dom.html' : '/aux-vm-iframe.html', origin).href; this._connectionStateChanged.next({ type: 'progress', message: 'Getting web manifest...', progress: 0.05, }); this._connectionStateChanged.next({ type: 'progress', message: 'Initializing web worker...', progress: 0.1, }); this._iframe = document.createElement('iframe'); this._iframe.src = iframeUrl; if (!enableDom) { this._iframe.style.display = 'none'; } this._iframe.style.position = 'absolute'; this._iframe.style.height = '100%'; this._iframe.style.width = '100%'; this._iframe.setAttribute('allow', DEFAULT_IFRAME_ALLOW_ATTRIBUTE); this._iframe.setAttribute('sandbox', DEFAULT_IFRAME_SANDBOX_ATTRIBUTE); let promise = waitForLoad(this._iframe); const iframeContainer = document.querySelector('.vm-iframe-container'); if (iframeContainer) { iframeContainer.appendChild(this._iframe); } else { document.body.insertBefore(this._iframe, document.body.firstChild); } await promise; this._channel = setupChannel(this._iframe.contentWindow); this._connectionStateChanged.next({ type: 'progress', message: 'Creating VM...', progress: 0.2, }); const wrapper = wrap(this._channel.port1); this._proxy = await new wrapper(location.origin, processPartitions(this._config)); let statusMapper = remapProgressPercent(0.2, 1); return await this._proxy.init(proxy((events) => this._localEvents.next(events)), proxy((events) => this._deviceEvents.next(events)), proxy((state) => this._stateUpdated.next(state)), proxy((version) => this._versionUpdated.next(version)), proxy((state) => this._connectionStateChanged.next(statusMapper(state))), proxy((err) => this._onError.next(err)), proxy((channel) => this._handleAddedSubChannel(channel)), proxy((id) => this._handleRemovedSubChannel(id)), proxy((message) => this._onAuthMessage.next(message))); } /** * The observable list of events that should be produced locally. */ get localEvents() { return this._localEvents; } get deviceEvents() { return this._deviceEvents; } /** * The observable list of bot state updates from this simulation. */ get stateUpdated() { return this._stateUpdated; } get versionUpdated() { return this._versionUpdated; } /** * Sends the given list of events to the simulation. * @param events The events to send to the simulation. */ async sendEvents(events) { if (!this._proxy) return null; this._batchOrSendEvents(events); } _batchOrSendEvents(events) { if (this._autoBatch) { for (let event of events) { this._batchedEvents.push(event); } this._scheduleBatch(); } else { this._sendEventsToProxy(events); } } _scheduleBatch() { if (!this._batchPending && this._batchedEvents.length > 0) { this._batchPending = true; queueMicrotask(() => { this._processBatch(); }); } } _processBatch() { this._batchPending = false; let events = this._batchedEvents; this._batchedEvents = []; this._sendEventsToProxy(events); } _sendEventsToProxy(events) { if (events && events.length) { const transferables = []; for (let event of events) { if (event.type === 'async_result') { if (event.result instanceof OffscreenCanvas) { console.log(`[AuxVMImpl] marked OffscreenCanvas as transferable in AsyncResultAction`, event); transferables.push(event.result); } } } if (transferables.length > 0) { console.log(`[AuxVMImpl] sendEvents marking transferables from events`, events, transferables); events = transfer(events, transferables); } } this._proxy.sendEvents(events); } /** * Executes a shout with the given event name on the given bot IDs with the given argument. * Also dispatches any actions and errors that occur. * Returns the results from the event. * @param eventName The name of the event. * @param botIds The IDs of the bots that the shout is being sent to. * @param arg The argument to include in the shout. */ async shout(eventName, botIds, arg) { if (!this._proxy) return null; return await this._proxy.shout(eventName, botIds, arg); } async formulaBatch(formulas) { if (!this._proxy) return null; return await this._proxy.formulaBatch(formulas); } async forkAux(newId) { if (!this._proxy) return null; return await this._proxy.forkAux(newId); } async exportBots(botIds) { if (!this._proxy) return null; return await this._proxy.exportBots(botIds); } /** * Exports the causal tree for the simulation. */ async export() { if (!this._proxy) return null; return await this._proxy.export(); } async getTags() { if (!this._proxy) return null; return await this._proxy.getTags(); } async updateDevice(device) { if (!this._proxy) return null; return await this._proxy.updateDevice(device); } /** * Gets a new endpoint for the aux channel. * Can then be used with a ConnectableAuxVM. */ createEndpoint() { return this._proxy[createEndpoint](); } sendAuthMessage(message) { return this._proxy.sendAuthMessage(message); } unsubscribe() { if (this.closed) { return; } this.closed = true; this._channel = null; this._proxy = null; if (this._iframe) { if (this._iframe.parentNode) { this._iframe.parentNode.removeChild(this._iframe); } this._iframe = null; } this._connectionStateChanged.unsubscribe(); this._connectionStateChanged = null; this._localEvents.unsubscribe(); this._localEvents = null; } _createSubVM(id, origin, configBotId, channel) { return new RemoteAuxVM(id, origin, configBotId, channel); } async _handleAddedSubChannel(subChannel) { const { id, configBotId } = await subChannel.getInfo(); const channel = (await subChannel.getChannel()); const subVM = { id: id, vm: this._createSubVM(id, this.origin, configBotId, channel), channel, }; this._subVMMap.set(id, subVM); this._subVMAdded.next(subVM); } async _handleRemovedSubChannel(channelId) { const vm = this._subVMMap.get(channelId); if (vm) { this._subVMMap.delete(channelId); this._subVMRemoved.next(vm); } } } export function processPartitions(config) { let transferrables = []; for (let key in config.partitions) { const partition = config.partitions[key]; if (!partition) { delete config.partitions[key]; } else if (partition.type === 'proxy') { const bridge = new ProxyBridgePartitionImpl(partition.partition); const channel = new MessageChannel(); expose(bridge, channel.port1); transferrables.push(channel.port2); config.partitions[key] = { type: 'proxy_client', editStrategy: partition.partition.realtimeStrategy, private: partition.partition.private, port: channel.port2, }; } } return transfer(config, transferrables); } //# sourceMappingURL=AuxVMImpl.js.map