workerboxjs
Version:
A secure sandbox to execute untrusted user JavaScript, in a web browser, without any risk to your own domain/site/page.
104 lines (88 loc) • 2.97 kB
JavaScript
import createCallbackStore from './createCallbackStore.js';
import builtinWorker from './builtinWorker.html.js';
import createSuperJSON from './createSuperJSON.js';
const instances = {
count: 0
};
function createWorkerboxInstance (url, onMessage) {
instances.count = instances.count + 1;
const channel = new MessageChannel();
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts';
iframe.id = `workerBox-${instances.count}`;
iframe.style =
'position: fixed; height: 0; width: 0; opacity: 0; top: -100px;';
if (url) {
iframe.src = url;
} else {
iframe.srcdoc = url || builtinWorker;
}
document.body.appendChild(iframe);
channel.port1.onmessage = onMessage;
return new Promise(resolve => {
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage('OK', '*', [channel.port2]);
resolve({
postMessage: message => channel.port1.postMessage(message),
destroy: () => iframe.remove()
});
});
});
}
export async function createWorkerBox (options) {
options = {
serverUrl: null,
appendVersion: true,
...options
};
if (options.serverUrl && options.serverUrl.slice(-1) === '/') {
options.serverUrl = options.serverUrl.slice(0, -1);
}
if (options.serverUrl && options.appendVersion) {
options.serverUrl = options.serverUrl + '/v6.4.8/';
}
options.serverUrl = options.serverUrl && (new URL(options.serverUrl)).href;
const callbacks = createCallbackStore();
const run = (id, args) =>
new Promise((resolve, reject) => {
instance.postMessage(['callback', {
id, args, resolve: callbacks.add(resolve), reject: callbacks.add(reject)
}]);
});
const superjson = createSuperJSON(callbacks.add, run);
const instance = await createWorkerboxInstance(options.serverUrl, async message => {
const [action, { id, args, resolve, reject }] = message.data;
const parsedArgs = superjson.parse(args);
if (action === 'error') {
callbacks.get(id)?.(new Error(parsedArgs[0]));
return;
}
if (action === 'return') {
callbacks.get(id)?.(parsedArgs[0]);
return;
}
const fn = callbacks.get(id);
if (!fn) {
return;
}
try {
const result = await fn(...parsedArgs);
instance.postMessage(['callback', { id: resolve, args: superjson.stringify([result]) }]);
} catch (error) {
instance.postMessage(['callback', { id: reject, args: superjson.stringify([error.message]) }]);
}
});
return {
run: async (code, originalScope) => {
return new Promise((resolve, reject) => {
const id = callbacks.add(resolve);
const errorId = callbacks.add(reject);
const scope = superjson.stringify(originalScope || {});
instance.postMessage(['execute', { id, errorId, code, scope }]);
});
},
destroy: () => instance.destroy(),
options
};
}
export default createWorkerBox;