@casual-simulation/aux-vm-browser
Version:
A set of utilities required to securely run an AUX in a web browser.
243 lines • 8.36 kB
JavaScript
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences.
*
* Copyright (c) 2019-2025 Casual Simulation, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import '@casual-simulation/aux-vm/globalThis-polyfill';
import { Observable } from 'rxjs';
/**
* Creates a new message channel and sends port2 to the iframe in a message.
* @param iframeWindow The window to send the port to.
*/
export function setupChannel(iframeWindow) {
const channel = new MessageChannel();
if (iframeWindow instanceof Worker) {
iframeWindow.postMessage({
type: 'init_port',
port: channel.port2,
}, [channel.port2]);
}
else {
iframeWindow.postMessage({
type: 'init_port',
port: channel.port2,
}, '*', [channel.port2]);
}
return channel;
}
/**
* Listens for the init_port event from the global context.
* @param origin The origin that the channels should be recieved from.
*/
export function listenForChannels(origin) {
return new Observable((observer) => {
let listener = (msg) => {
if (msg.data.type === 'init_port') {
if (!origin || msg.origin === origin) {
observer.next(msg.data.port);
}
}
};
globalThis.addEventListener('message', listener);
return () => {
globalThis.removeEventListener('message', listener);
};
});
}
/**
* Listens for the init_port event from the global context.
* @param origin The origin that the channel should be recieved from.
*/
export function listenForChannel(origin) {
return new Promise((resolve) => {
let listener = (msg) => {
if (msg.data.type === 'init_port') {
if (!origin || msg.origin === origin) {
globalThis.removeEventListener('message', listener);
msg.data.port.origin = origin;
resolve(msg.data.port);
}
}
};
globalThis.addEventListener('message', listener);
});
}
export function waitForLoad(iframe) {
return new Promise((resolve) => {
let listener = () => {
iframe.removeEventListener('load', listener);
resolve();
};
iframe.addEventListener('load', listener);
});
}
/**
* Loads the script at the given URL into the given iframe window.
* @param iframeWindow The iframe.
* @param id The ID of the script.
* @param source The source code to load.
*/
export function loadScript(iframeWindow, id, source) {
return new Promise((resolve, reject) => {
const listener = (message) => {
if (message.source !== iframeWindow) {
return;
}
if (message.data.type === 'script_loaded' &&
message.data.id === id) {
globalThis.removeEventListener('message', listener);
resolve();
}
};
globalThis.addEventListener('message', listener);
iframeWindow.postMessage({
type: 'load_script',
id,
source,
}, '*');
});
}
/**
* Injects the given message port with the given ID into the iframe.
* @param iframeWindow The iframe that the message port should be injected into.
* @param id The ID of the message port.
* @param port The port to inject.
*/
export function injectPort(iframeWindow, id, port) {
return new Promise((resolve, reject) => {
const listener = (message) => {
if (message.source !== iframeWindow) {
return;
}
if (message.data.type === 'port_injected' &&
message.data.id === id) {
globalThis.removeEventListener('message', listener);
resolve();
}
};
globalThis.addEventListener('message', listener);
iframeWindow.postMessage({
type: 'inject_port',
id,
port,
}, '*', [port]);
});
}
/**
* Loads the script at the given URL into the given iframe window.
* @param iframeWindow The iframe.
* @param id The ID of the script.
* @param text The text to load.
* @param element The HTML element the text should be loaded in.
*/
export function loadText(iframeWindow, id, text, element) {
return new Promise((resolve, reject) => {
const listener = (message) => {
if (message.source !== iframeWindow) {
return;
}
if (message.data.type === 'text_loaded' && message.data.id === id) {
globalThis.removeEventListener('message', listener);
resolve();
}
};
globalThis.addEventListener('message', listener);
iframeWindow.postMessage({
type: 'load_text',
id,
text,
element,
}, '*');
});
}
/**
* Reloads the iframe.
* @param iframeWindow The iframe to reload.
*/
export function reload(iframeWindow) {
const promise = waitForLoad(iframeWindow);
iframeWindow.contentWindow.postMessage({
type: 'reload',
}, '*');
return promise;
}
/**
* Creates
* @param options
* @param properties
*/
export async function setupCustomIframe(options, properties) {
const origin = options.vmOrigin || location.origin;
const iframeUrl = new URL('/aux-vm-iframe.html', origin).href;
const iframe = document.createElement('iframe');
iframe.src = iframeUrl;
iframe.style.display = 'none';
if (properties) {
for (let key in properties) {
iframe[key] = properties[key];
}
}
// Allow the iframe to run scripts, but do nothing else.
// Because we're not allowing the same origin, this prevents the VM from talking to
// storage like IndexedDB and therefore prevents different VMs from affecting each other.
// iframe.sandbox.add('allow-scripts');
// const bowserResult = Bowser.parse(navigator.userAgent);
// Safari requires the allow-same-origin option in order to load
// web workers using a blob.
// if (
// bowserResult.browser.name === 'Safari' ||
// bowserResult.os.name === 'iOS'
// ) {
// console.warn('[AuxVMImpl] Adding allow-same-origin for Safari');
// iframe.sandbox.add('allow-same-origin');
// }
let promise = waitForLoad(iframe);
document.body.insertBefore(iframe, document.body.firstChild);
await promise;
return iframe;
}
// /**
// * Loads the script into the iframe window as a portal.
// * @param iframeWindow The iframe.
// * @param id The ID of the portal.
// * @param source The source code to load.
// */
// export function registerIFramePortal(iframeWindow: Window, id: string, source: string) {
// return new Promise<void>((resolve, reject) => {
// const listener = (message: MessageEvent) => {
// if (message.source !== iframeWindow) {
// debugger;
// return;
// }
// if (
// message.data.type === 'portal_registered' &&
// message.data.id === id
// ) {
// globalThis.removeEventListener('message', listener);
// resolve();
// }
// };
// globalThis.addEventListener('message', listener);
// iframeWindow.postMessage(
// {
// type: 'register_portal',
// id,
// source
// },
// '*'
// );
// });
// }
//# sourceMappingURL=IFrameHelpers.js.map