@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
JavaScript
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