147 lines
4.6 KiB
JavaScript
147 lines
4.6 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 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: env,
|
|
};
|
|
}
|
|
|
|
const result = await Promise.resolve(userFn(requestObject));
|
|
return { status: States.SUCCESS, result, logs, environment: env };
|
|
} catch (err) {
|
|
const status = /timed out/i.test(err.message)
|
|
? States.TIMEOUT
|
|
: States.SCRIPT_ERROR;
|
|
return { status, result: err.message ?? err, logs, environment: env };
|
|
}
|
|
}
|
|
|
|
// ────── 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}`));
|