workerboxjs
Advanced tools
Comparing version 3.4.0 to 4.0.0
196
lib/index.js
@@ -1,6 +0,35 @@ | ||
let workerBoxCount = 0; | ||
export function createWorkerBox (scriptUrl, options) { | ||
import createCallbackStore from './createCallbackStore.js'; | ||
import scopeToString from './scopeToString.js'; | ||
import stringToArgs from './stringToArgs.js'; | ||
import argsToString from './argsToString.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;'; | ||
iframe.src = url; | ||
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 (scriptUrl, options) { | ||
options = { | ||
appendVersion: true, | ||
randomiseSubdomain: false, | ||
...options | ||
@@ -14,3 +43,3 @@ }; | ||
if (options.appendVersion) { | ||
scriptUrl = scriptUrl + '/v3.4.0/'; | ||
scriptUrl = scriptUrl + '/v4.0.0/'; | ||
} | ||
@@ -30,139 +59,46 @@ | ||
if (options.randomiseSubdomain) { | ||
const subdomain = [...Array(30)] | ||
.map(() => Math.random().toString(36)[2]) | ||
.join(''); | ||
scriptUrl.host = `${subdomain}.${scriptUrl.host}`; | ||
} | ||
const callbacks = createCallbackStore(); | ||
workerBoxCount = workerBoxCount + 1; | ||
return new Promise((resolve) => { | ||
const iframe = document.createElement('iframe'); | ||
iframe.sandbox = 'allow-scripts allow-same-origin'; | ||
iframe.id = `workerBox${workerBoxCount}`; | ||
iframe.style = | ||
'position: fixed; height: 0; width: 0; opacity: 0; top: -100px;'; | ||
iframe.src = scriptUrl.href; | ||
document.body.appendChild(iframe); | ||
const run = (id, args) => | ||
new Promise(resolve => { | ||
instance.postMessage(['callback', { id, args, resolve: callbacks.add(resolve) }]); | ||
}); | ||
const worker = document.getElementById( | ||
`workerBox${workerBoxCount}` | ||
).contentWindow; | ||
const instance = await createWorkerboxInstance(scriptUrl.href, async message => { | ||
const [action, { id, args, resolve }] = message.data; | ||
const promises = {}; | ||
const callbacks = {}; | ||
let currentCallbackId = 0; | ||
const parsedArgs = stringToArgs(args, callbacks.add, run); | ||
function prepareArgs (args) { | ||
const newArgs = []; | ||
for (const arg of args) { | ||
if (typeof arg === 'function') { | ||
currentCallbackId = currentCallbackId + 1; | ||
callbacks[currentCallbackId] = arg; | ||
newArgs.push(['callback', currentCallbackId]); | ||
} else if (typeof arg === 'object') { | ||
newArgs.push(['object', prepareArgs(arg)]); | ||
} else { | ||
newArgs.push(['literal', arg]); | ||
} | ||
} | ||
return newArgs; | ||
if (action === 'error') { | ||
callbacks.get(id)?.(new Error(parsedArgs[0])); | ||
return; | ||
} | ||
window.addEventListener('message', async (event) => { | ||
if (event.data.ready) { | ||
let messageNumber = 0; | ||
const run = (code, scope) => { | ||
messageNumber = messageNumber + 1; | ||
const currentMessageNumber = messageNumber; | ||
function prepareScope (scope) { | ||
const newScope = {}; | ||
for (const key in scope) { | ||
if (typeof scope[key] === 'function') { | ||
newScope[key] = ['function', key]; | ||
} else if (typeof scope[key] === 'object') { | ||
newScope[key] = ['object', prepareScope(scope[key])]; | ||
} else { | ||
newScope[key] = ['literal', scope[key]]; | ||
} | ||
} | ||
return newScope; | ||
} | ||
if (action === 'return') { | ||
callbacks.get(id)?.(parsedArgs[0]); | ||
return; | ||
} | ||
function parseArgs (args) { | ||
const newArgs = []; | ||
for (const arg of args) { | ||
if (arg[0] === 'callback') { | ||
newArgs.push((...rawArgs) => { | ||
return new Promise((resolve) => { | ||
const args = prepareArgs([...rawArgs, resolve]); | ||
worker.postMessage( | ||
{ | ||
messageNumber: currentMessageNumber, | ||
callbackKey: arg[1], | ||
callbackArgs: args | ||
}, | ||
'*' | ||
); | ||
}); | ||
}); | ||
} else if (arg[0] === 'object') { | ||
newArgs.push(parseArgs(arg[1])); | ||
} else { | ||
newArgs.push(arg[1]); | ||
} | ||
} | ||
return newArgs; | ||
} | ||
const fn = callbacks.get(id); | ||
if (!fn) { | ||
return; | ||
} | ||
const callFunction = async (key, args) => { | ||
const [resolve] = parseArgs(args).slice(-1); | ||
const result = await scope[key].call( | ||
null, | ||
...parseArgs(args).slice(0, -1) | ||
); | ||
resolve(result); | ||
}; | ||
const result = await fn(...parsedArgs); | ||
instance.postMessage(['callback', { id: resolve, args: argsToString([result]) }]); | ||
}); | ||
worker.postMessage( | ||
{ | ||
messageNumber: currentMessageNumber, | ||
code, | ||
scope: prepareScope(scope) | ||
}, | ||
'*' | ||
); | ||
return new Promise((resolve, reject) => { | ||
promises[currentMessageNumber] = { resolve, reject, callFunction }; | ||
}); | ||
}; | ||
run.destroy = () => { | ||
iframe.remove(); | ||
}; | ||
resolve(run); | ||
return; | ||
} | ||
const { messageNumber, error, result } = event.data; | ||
if (event.data.functionKey) { | ||
promises[messageNumber].callFunction( | ||
event.data.functionKey, | ||
event.data.functionArgs | ||
); | ||
return; | ||
} | ||
if (error) { | ||
promises[messageNumber]?.reject(error); | ||
return; | ||
} | ||
promises[messageNumber]?.resolve(result); | ||
}); | ||
}); | ||
return { | ||
run: async (code, originalScope) => { | ||
return new Promise((resolve, reject) => { | ||
const id = callbacks.add(resolve); | ||
const errorId = callbacks.add(reject); | ||
const scope = scopeToString(originalScope, callbacks.add, run); | ||
instance.postMessage(['execute', { id, errorId, code, scope }]); | ||
}); | ||
}, | ||
destroy: () => instance.destroy() | ||
}; | ||
} | ||
export default createWorkerBox; |
{ | ||
"name": "workerboxjs", | ||
"version": "3.4.0", | ||
"version": "4.0.0", | ||
"type": "module", | ||
@@ -11,3 +11,3 @@ "description": "A secure sandbox to execute untrusted user JavaScript, in a web browser, without any risk to your own domain/site/page.", | ||
"build": "node build.js", | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "node test" | ||
}, | ||
@@ -38,11 +38,11 @@ "files": [ | ||
"devDependencies": { | ||
"@markwylde/ftp-deploy": "^1.2.0", | ||
"@markwylde/ftp-deploy": "^2.0.1", | ||
"basictap": "^3.4.3", | ||
"chokidar": "^3.5.3", | ||
"debounce": "^1.2.1", | ||
"esbuild": "^0.15.12", | ||
"esbuild": "^0.15.15", | ||
"just-tap": "^1.5.1", | ||
"minify": "^9.1.0", | ||
"process": "^0.11.10", | ||
"puppeteer": "^19.2.0", | ||
"puppeteer": "^19.2.2", | ||
"servatron": "^2.4.2" | ||
@@ -49,0 +49,0 @@ }, |
@@ -18,3 +18,3 @@ # WorkerBox | ||
const run = await createWorkerBox('https://sandbox.workerbox.net/'); | ||
const { run, destroy } = await createWorkerBox('https://sandbox.workerbox.net/'); | ||
@@ -36,3 +36,3 @@ const scope = { | ||
run.destroy() // Destroys the worker box, terminating any running workers | ||
destroy() // Destroys the worker box, terminating any running workers | ||
``` | ||
@@ -46,15 +46,1 @@ | ||
Because the only communication between the user code and the workerbox instead is done through messaging, the argument inputs and outputs must all be JSON serializable. | ||
## Caveats | ||
### Storage | ||
Web workers can't use cookies or localStorage, but even if they could they would be isolated to third party domain that is running the code. | ||
However, there are some ways to store data. For example, indexDB. | ||
While your unsafe user code can not access the indexDB of your own site, it can use the instance on the server's site. | ||
But remember, anyone can run untrusted user code on the workerbox site. So if your users store data on the workerbox domain, technically anyone can view that data. | ||
Therefore, you should advise your users not to store any data using the web workers API. | ||
Of course, you could provide an abstraction on the `scope` that would safely allow you to store data on your own domain. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
0
6702
86
44