u
This commit is contained in:
247
src/tools/javascript-worker.js
Normal file
247
src/tools/javascript-worker.js
Normal file
@@ -0,0 +1,247 @@
|
||||
'use strict';
|
||||
|
||||
import { parentPort, workerData } from 'worker_threads';
|
||||
import { NodeVM } from 'vm2';
|
||||
import sharp from 'sharp';
|
||||
import { validateObjects, validateObject } from 'react-canvas-label-editor';
|
||||
import { createVfs } from './vfs.js';
|
||||
|
||||
const { code, root, vmTimeout, bufferLimit, network, dashbackUrl, dashbackToken, vfsDir, googleAuthId } = workerData;
|
||||
|
||||
function resolveDashbackUrl(inputUrl, baseApiUrl) {
|
||||
const raw = String(inputUrl || '').trim();
|
||||
if (raw.startsWith('http://') || raw.startsWith('https://')) {
|
||||
return raw;
|
||||
}
|
||||
const cleanPath = raw.startsWith('/') ? raw : '/' + raw;
|
||||
const base = new URL(baseApiUrl || 'http://127.0.0.1:4208/api');
|
||||
const origin = base.origin;
|
||||
|
||||
if (cleanPath.startsWith('/api/') || cleanPath === '/api' || cleanPath.startsWith('/v1/') || cleanPath === '/v1') {
|
||||
return new URL(cleanPath, origin).toString();
|
||||
}
|
||||
const basePath = base.pathname.replace(/\/+$/, '');
|
||||
return new URL(`${basePath}${cleanPath}`, origin).toString();
|
||||
}
|
||||
|
||||
function isDashbackTarget(targetUrl, baseApiUrl) {
|
||||
if (!baseApiUrl) return false;
|
||||
try {
|
||||
const targetObj = new URL(targetUrl);
|
||||
const baseObj = new URL(baseApiUrl);
|
||||
return targetObj.origin === baseObj.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createSandboxFetch(apiUrl, token) {
|
||||
const rawFetch = globalThis.fetch;
|
||||
return async function sandboxFetch(input, init) {
|
||||
let url;
|
||||
let isDashback = false;
|
||||
|
||||
if (typeof input === 'string') {
|
||||
const resolved = resolveDashbackUrl(input, apiUrl);
|
||||
url = resolved;
|
||||
isDashback = isDashbackTarget(resolved, apiUrl);
|
||||
} else if (input instanceof globalThis.Request) {
|
||||
url = input.url;
|
||||
isDashback = isDashbackTarget(url, apiUrl);
|
||||
} else if (input instanceof globalThis.URL) {
|
||||
url = input.toString();
|
||||
isDashback = isDashbackTarget(url, apiUrl);
|
||||
} else {
|
||||
url = input;
|
||||
}
|
||||
|
||||
if (isDashback && token) {
|
||||
const existingHeaders = init?.headers || (typeof input === 'object' && input?.headers ? input.headers : {});
|
||||
const headers = new globalThis.Headers(existingHeaders);
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
if (!headers.has('Accept')) {
|
||||
headers.set('Accept', 'application/json, text/plain, */*');
|
||||
}
|
||||
return rawFetch(input instanceof globalThis.Request ? input : url, { ...init, headers });
|
||||
}
|
||||
|
||||
return rawFetch(url, init);
|
||||
};
|
||||
}
|
||||
|
||||
const NETWORK_GLOBALS = {
|
||||
fetch: createSandboxFetch(dashbackUrl, dashbackToken),
|
||||
Headers: globalThis.Headers,
|
||||
Request: globalThis.Request,
|
||||
Response: globalThis.Response,
|
||||
FormData: globalThis.FormData,
|
||||
AbortController: globalThis.AbortController,
|
||||
AbortSignal: globalThis.AbortSignal,
|
||||
URL: globalThis.URL,
|
||||
URLSearchParams: globalThis.URLSearchParams,
|
||||
Blob: globalThis.Blob,
|
||||
File: globalThis.File,
|
||||
TextEncoder: globalThis.TextEncoder,
|
||||
TextDecoder: globalThis.TextDecoder,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
};
|
||||
|
||||
const lines = [];
|
||||
const push = (prefix) => (...args) => {
|
||||
let text;
|
||||
try {
|
||||
text = args
|
||||
.map((a) => {
|
||||
if (typeof a === 'string') return a;
|
||||
if (Buffer.isBuffer(a) || ArrayBuffer.isView(a) || (a && a.constructor && a.constructor.name === 'Buffer')) {
|
||||
return `<Buffer ${(a.byteLength ?? a.length ?? 0)} bytes>`;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(a, (k, v) => {
|
||||
if (v && typeof v === 'object') {
|
||||
if (v.type === 'Buffer' && Array.isArray(v.data)) {
|
||||
return `<Buffer ${v.data.length} bytes>`;
|
||||
}
|
||||
if (Buffer.isBuffer(v) || ArrayBuffer.isView(v) || (v.constructor && v.constructor.name === 'Buffer')) {
|
||||
return `<Buffer ${(v.byteLength ?? v.length ?? 0)} bytes>`;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
});
|
||||
} catch {
|
||||
return String(a);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
} catch {
|
||||
text = String(args);
|
||||
}
|
||||
lines.push(prefix + text);
|
||||
};
|
||||
const sandboxConsole = {
|
||||
log: push(''),
|
||||
info: push(''),
|
||||
debug: push(''),
|
||||
warn: push('warn: '),
|
||||
error: push('error: '),
|
||||
trace: push('trace: '),
|
||||
};
|
||||
|
||||
const MAX_ITEMS = 200;
|
||||
|
||||
function sanitize(value, depth = 0) {
|
||||
if (value === null) return null;
|
||||
const t = typeof value;
|
||||
if (t === 'undefined') return { type: 'undefined' };
|
||||
if (t === 'boolean' || t === 'string') return value;
|
||||
if (t === 'number') return Number.isFinite(value) ? value : String(value);
|
||||
if (t === 'bigint') return `${value.toString()}n`;
|
||||
if (t === 'symbol') return String(value);
|
||||
if (t === 'function') return { type: 'function' };
|
||||
if (Buffer.isBuffer(value) || ArrayBuffer.isView(value) || (value && value.constructor && value.constructor.name === 'Buffer')) {
|
||||
const len = value.byteLength ?? value.length ?? 0;
|
||||
return `<Buffer ${len} bytes>`;
|
||||
}
|
||||
if (depth >= 6) return { type: t, note: 'too deeply nested to serialize' };
|
||||
if (Array.isArray(value)) {
|
||||
const arr = new Array();
|
||||
const len = Math.min(typeof value.length === 'number' ? value.length : 0, MAX_ITEMS);
|
||||
for (let i = 0; i < len; i++) {
|
||||
try { arr[i] = sanitize(value[i], depth + 1); }
|
||||
catch { arr[i] = { type: 'unreadable' }; }
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
if (t === 'object') {
|
||||
let keys;
|
||||
try { keys = Object.keys(value); } catch { return { type: 'object', note: 'not serializable' }; }
|
||||
const out = {};
|
||||
for (const k of keys.slice(0, MAX_ITEMS)) {
|
||||
try { out[k] = sanitize(value[k], depth + 1); }
|
||||
catch { out[k] = { type: 'unreadable' }; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return { type: t };
|
||||
}
|
||||
|
||||
function report(payload) {
|
||||
try {
|
||||
parentPort.postMessage(payload);
|
||||
} catch {
|
||||
try {
|
||||
parentPort.postMessage({ error: 'Sandbox produced a result that could not be serialized.' });
|
||||
} catch { /* thread exiting */ }
|
||||
}
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const vfs = createVfs(vfsDir, { googleAuthId });
|
||||
|
||||
const vm = new NodeVM({
|
||||
console: 'off',
|
||||
sandbox: {
|
||||
console: sandboxConsole,
|
||||
fs: vfs,
|
||||
vfs,
|
||||
sharp,
|
||||
validateLabelObjects: (objects) => validateObjects(JSON.parse(JSON.stringify(objects))),
|
||||
validateLabelObject: (object) => validateObject(JSON.parse(JSON.stringify(object))),
|
||||
...NETWORK_GLOBALS,
|
||||
},
|
||||
require: {
|
||||
root,
|
||||
external: false,
|
||||
builtin: [],
|
||||
},
|
||||
nesting: false,
|
||||
bufferAllocLimit: bufferLimit,
|
||||
timeout: vmTimeout,
|
||||
});
|
||||
|
||||
const started = Date.now();
|
||||
const done = () => ({
|
||||
stdout: lines.join('\n'),
|
||||
consoleLineCount: lines.length,
|
||||
durationMs: Date.now() - started,
|
||||
});
|
||||
|
||||
let value;
|
||||
let failed = null;
|
||||
try {
|
||||
value = vm.run(code, 'javascript_execute.js');
|
||||
if (value && typeof value.then === 'function') {
|
||||
const settleTimeout = Math.max(1000, vmTimeout - 500);
|
||||
value = await Promise.race([
|
||||
value,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Async snippet did not settle before the timeout.')), settleTimeout)
|
||||
),
|
||||
]);
|
||||
}
|
||||
} catch (err) {
|
||||
failed = {
|
||||
error: (err && err.message) || String(err),
|
||||
errorName: (err && err.name) || 'Error',
|
||||
cause: err && err.cause ? ((err.cause && err.cause.message) || String(err.cause)) : undefined,
|
||||
stack: err && typeof err.stack === 'string' ? err.stack : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
report({ ...done(), ...failed });
|
||||
} else {
|
||||
report({ result: sanitize(value), ...done() });
|
||||
}
|
||||
} catch (err) {
|
||||
report({
|
||||
error: (err && err.message) || String(err),
|
||||
errorName: (err && err.name) || 'Error',
|
||||
stack: err && typeof err.stack === 'string' ? err.stack : undefined,
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user