Files
isolator-js/server.js
2025-07-25 15:01:43 +10:00

166 lines
5.1 KiB
JavaScript

// speedy-functions.js
import express from "express";
import { VM, VMScript } from "vm2";
import { JSDOM } from "jsdom";
import cheerio from "cheerio";
import FileReader from "filereader";
// ────── 1. one-time constants ──────────────────────────────────────────
const PORT = +(process.env.PORT ?? 5000);
const TIMEOUT_MS = 5_000;
const HTTP_STATUS_CODES = Object.freeze({
OK: 200,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
});
const States = Object.freeze({
SUCCESS: "SUCCESS",
NOT_A_FUNCTION: "NOT_A_FUNCTION",
SCRIPT_ERROR: "ERROR",
TIMEOUT: "TIMEOUT",
});
// convenience helpers
const Response = (body = "", headers = {}, status = HTTP_STATUS_CODES.OK) => ({
body,
status,
headers,
});
const JsonResponse = (body = {}, headers = {}, status = HTTP_STATUS_CODES.OK) =>
Response(
JSON.stringify(body),
{ "Content-Type": "application/json", ...headers },
status
);
const HtmlResponse = (body = "", headers = {}, status = HTTP_STATUS_CODES.OK) =>
Response(body, { "Content-Type": "text/html", ...headers }, status);
const TextResponse = (body = "", headers = {}, status = HTTP_STATUS_CODES.OK) =>
Response(body, { "Content-Type": "text/plain", ...headers }, status);
// ────── 2. one-time imports ────────────────────────────────────────────
const fetch = (await import("node-fetch")).default;
// ────── 3. tiny script cache (precompiled) ─────────────────────────────
const scriptCache = new Map(); // code-string -> VMScript
const cachedScript = (code) => {
if (!scriptCache.has(code)) {
scriptCache.set(code, new VMScript(code));
}
return scriptCache.get(code);
};
// ────── 4. shared VM options (lightweight to clone) ────────────────────
const baseSandbox = {
fetch,
parseHTML: (html) => new JSDOM(html).window.document,
JSDOM,
cheerio,
HTTP_STATUS_CODES,
Response,
JsonResponse,
HtmlResponse,
TextResponse,
FileReader,
};
function createVm(logs, requestObject, env, funcName) {
return new VM({
timeout: TIMEOUT_MS,
sandbox: {
...baseSandbox,
console: {
log: (...args) => (
logs.push(args), /* bubble to host */ console.log(...args)
),
error: (...args) => (logs.push(args), console.error(...args)),
},
requestObject,
environment: env,
FUNCTION_NAME: funcName,
},
require: { external: true },
});
}
// ────── 5. evaluator ───────────────────────────────────────────────────
async function executeUserCode(
code,
requestObject,
env = {},
funcName = "userFunc"
) {
const logs = [];
const startTime = process.hrtime();
const vm = createVm(
logs,
requestObject,
JSON.parse(JSON.stringify(env)),
funcName
);
try {
const userFn = vm.run(cachedScript(code));
if (typeof userFn !== "function") {
return {
status: States.NOT_A_FUNCTION,
result: null,
logs,
environment: vm.sandbox.environment,
};
}
const result = await Promise.resolve(
userFn(requestObject, vm.sandbox.environment)
);
const endTime = process.hrtime(startTime);
const executionTime = (endTime[0] * 1e9 + endTime[1]) / 1e6; // in milliseconds
return {
status: States.SUCCESS,
result,
logs,
environment: vm.sandbox.environment,
execution_time: executionTime,
};
} catch (err) {
const status = /timed out/i.test(err.message)
? States.TIMEOUT
: States.SCRIPT_ERROR;
const endTime = process.hrtime(startTime);
const executionTime = (endTime[0] * 1e9 + endTime[1]) / 1e6; // in milliseconds
return {
status,
result: err.message ?? err,
logs,
environment: vm.sandbox.environment,
execution_time: executionTime,
};
}
}
// ────── 6. API surface ────────────────────────────────────────────────
const app = express();
app.use(express.json({ limit: "50mb" }));
app.use((_, res, next) => {
res.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"Origin, X-Requested-With, Content-Type, Accept",
});
next();
});
app.post("/execute", async ({ body }, res) => {
const { code = "", request = {}, environment = {}, name } = body;
const payload = await executeUserCode(code, request, environment, name);
res.json(payload);
});
app.listen(PORT, () => console.log(`⚡ server ready on :${PORT}`));