feat: testing quickjs sanbox

This commit is contained in:
Anoop M D 2024-08-13 17:22:08 +05:30
parent bde2c57a23
commit 81d660afd7
29 changed files with 1334 additions and 133 deletions

View File

@ -14,7 +14,8 @@
"url": "git+https://github.com/usebruno/bruno.git"
},
"scripts": {
"test": "jest"
"test": "jest",
"postinstall": "cd node_modules/isolated-vm && npm rebuild"
},
"files": [
"src",

View File

@ -42,7 +42,7 @@ const runSingleRequest = async function (
// todo: allow to override from cli args
// we will default to vm2 (developer-mode) for 1.x version for backward compatibility
// 2.x will default to isolated-vm (safe mode)
scriptingConfig.runtime = 'vm2';
scriptingConfig.runtime = 'isolated-vm';
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427

View File

@ -12,8 +12,7 @@
},
"scripts": {
"test": "jest --testPathIgnorePatterns test.js",
"rebuild:electron": "cd ./node_modules/isolated-vm && npx electron-rebuild",
"postinstall": "npm run isolated-vm:install && npm run isolated-vm:prebuild:dev && npm run isolated-vm:bundle-libraries",
"postinstall": "npm run isolated-vm:install && npm run isolated-vm:bundle-libraries",
"isolated-vm:install": "cd ./node_modules/isolated-vm && npm install",
"isolated-vm:prebuild:dev": "node ./scripts/prebuild-isolated-vm-for-dev.js",
"isolated-vm:prebuild:prod": "node ./scripts/prebuild-isolated-vm-for-prod-builds.js",
@ -37,6 +36,7 @@
"nanoid": "3.3.4",
"node-fetch": "2.*",
"node-vault": "^0.10.2",
"quickjs-emscripten": "^0.29.2",
"uuid": "^9.0.0"
},
"devDependencies": {

View File

@ -108,7 +108,7 @@ class TestRuntime {
};
}
if (this.mode == 'safe') {
if (this.runtime === 'isolated-vm') {
await executeInIsolatedVMAsync({
script: testsFile,
context: context,

View File

@ -0,0 +1,167 @@
const ivm = require('isolated-vm');
const addBruShimToContext = require('./shims/bru');
const addBrunoRequestShimToContext = require('./shims/bruno-request');
const addConsoleShimToContext = require('./shims/console');
const addBrunoResponseShimToContext = require('./shims/bruno-response');
const addTestShimToContext = require('./shims/test');
const addLibraryShimsToContext = require('./shims/lib');
// execute `npm run build:isolated-vm:inbuilt-modules` if the below file doesn't exist
const getBundledCode = require('../../bundle-browser-rollup');
const addSleepShimToContext = require('./shims/sleep');
const toNumber = (value) => {
const num = Number(value);
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const executeInIsolatedVMStrict = ({ script: externalScript, context: externalContext, scriptType = 'script' }) => {
if (!isNaN(Number(externalScript))) {
return Number(externalScript);
}
let result;
const isolate = new ivm.Isolate();
try {
const context = isolate.createContextSync();
context.global.setSync('global', context.global.derefInto());
const { bru, req, res } = externalContext;
context.evalSync(`
let bru = {};
let req = {};
let res = {};
`);
bru && addBruShimToContext(context, bru);
req && addBrunoRequestShimToContext(context, req);
res && addBrunoResponseShimToContext(context, res);
context.global.setSync('setResult', function (arg) {
result = arg;
});
const templateLiteralText = `
let value = \`${externalScript}\`;
setResult(value);
`;
const jsExpressionText = `
let value = ${externalScript};
setResult(value);
`;
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
const script = isolate.compileScriptSync(scriptText);
script.runSync(context);
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
isolate.dispose();
};
const executeInIsolatedVMAsync = async ({
script: externalScript,
context: externalContext,
modules = {},
scriptType = 'script'
}) => {
if (!isNaN(Number(externalScript))) {
return toNumber(externalScript);
}
let result;
const isolate = new ivm.Isolate();
try {
const context = await isolate.createContext();
await context.global.set('global', context.global.derefInto());
context.evalSync(`
let bru = {};
let req = {};
let res = {};
let console = {};
global.requireObject = {};
`);
context.global.setSync('log', function (...args) {
console.debug(...args);
});
try {
const bundledCode = getBundledCode?.toString() || '';
await context.eval(`(${bundledCode})()`);
} catch (err) {
console.debug('Error bundling libraries', err);
}
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
bru && addBruShimToContext(context, bru);
req && addBrunoRequestShimToContext(context, req);
res && addBrunoResponseShimToContext(context, res);
consoleFn && addConsoleShimToContext(context, consoleFn);
addSleepShimToContext(context);
await context.eval(
`
global.require = (module) => {
return global.requireObject[module];
}
`
);
await addLibraryShimsToContext(context);
test && __brunoTestResults && (await addTestShimToContext(context, __brunoTestResults));
context.global.setSync('setResult', function (arg) {
result = arg;
});
const jsScriptText = `
new Promise(async (resolve, reject) => {
// modify the setTimeout function with the shim to work-around the callback-function clone issues
setTimeout = global.setTimeout;
console?.debug && console.debug('isolated-vm:execution-start:');
try {
${externalScript}
} catch (error) {
console?.debug && console.debug('isolated-vm:execution-end:with-error', error?.message);
}
console?.debug && console.debug('isolated-vm:execution-end:');
resolve();
});
`;
const templateLiteralText = `
let value = \`${externalScript}\`;
setResult(value);
`;
const jsExpressionText = `
let value = ${externalScript};
setResult(value);
`;
let scriptText =
scriptType === 'template-literal'
? templateLiteralText
: scriptType === 'expression'
? jsExpressionText
: jsScriptText;
const script = await isolate.compileScript(scriptText);
await script.run(context);
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
isolate.dispose();
};
module.exports = {
executeInIsolatedVMStrict,
executeInIsolatedVMAsync
};

View File

@ -0,0 +1,59 @@
const addBruShimToContext = (context, bru) => {
context.global.setSync('cwd', function () {
return bru.cwd();
});
context.global.setSync('getEnvName', function () {
return bru.getEnvName();
});
context.global.setSync('getProcessEnv', function (key) {
return bru.getProcessEnv(key);
});
context.global.setSync('getEnvVar', function (key) {
return bru.getEnvVar(key);
});
context.global.setSync('setEnvVar', function (key, value) {
bru.setEnvVar(key, value);
});
context.global.setSync('setVar', function (key, value) {
bru.setVar(key, value);
});
context.global.setSync('getVar', function (key) {
return bru.getVar(key);
});
context.global.setSync('setNextRequest', function (nextRequest) {
bru.setNextRequest(nextRequest);
});
context.global.setSync('visualize', function (htmlString) {
bru.visualize(htmlString);
});
context.global.setSync('getSecretVar', function (key) {
return bru.getSecretVar(key);
});
context.evalSync(`
bru = {
...bru || {},
cwd: global.cwd,
getEnvName: global.getEnvName,
getProcessEnv: global.getProcessEnv,
getEnvVar: global.getEnvVar,
setEnvVar: global.setEnvVar,
setVar: global.setVar,
getVar: global.getVar,
setNextRequest: global.setNextRequest,
visualize: global.visualize,
getSecretVar: global.getSecretVar
}
`);
};
module.exports = addBruShimToContext;

View File

@ -0,0 +1,79 @@
const addBrunoRequestShimToContext = (context, req) => {
context.global.setSync('getUrl', function () {
return req.getUrl();
});
context.global.setSync('setUrl', function (url) {
req.setUrl(url);
});
context.global.setSync('getMethod', function () {
return req.getMethod();
});
context.global.setSync('getAuthMode', function () {
return req.getAuthMode();
});
context.global.setSync('setMethod', function (method) {
req.setMethod(method);
});
context.global.setSync('getHeaders', function () {
return req.getHeaders();
});
context.global.setSync('setHeaders', function (headers) {
req.setHeaders(headers);
});
context.global.setSync('getHeader', function (name) {
return req.getHeader(name);
});
context.global.setSync('setHeader', function (name, value) {
req.setHeader(name, value);
});
context.global.setSync('getBody', function () {
return req.getBody();
});
context.global.setSync('setBody', function (data) {
req.setBody(data);
});
context.global.setSync('setMaxRedirects', function (maxRedirects) {
req.setMaxRedirects(maxRedirects);
});
context.global.setSync('getTimeout', function () {
return req.getTimeout();
});
context.global.setSync('setTimeout', function (timeout) {
req.setTimeout(timeout);
});
context.evalSync(`
req = {
...req || {},
getUrl: global.getUrl,
setUrl: global.setUrl,
getMethod: global.getMethod,
getAuthMode: global.getAuthMode,
setMethod: global.setMethod,
getHeaders: global.getHeaders,
setHeaders: global.setHeaders,
getHeader: global.getHeader,
setHeader: global.setHeader,
getBody: global.getBody,
setBody: global.setBody,
setMaxRedirects: global.setMaxRedirects,
getTimeout: global.getTimeout,
setTimeout: global.setTimeout
}
`);
};
module.exports = addBrunoRequestShimToContext;

View File

@ -0,0 +1,46 @@
const ivm = require('isolated-vm');
const addBrunoResponseShimToContext = (context, res) => {
context.global.setSync('status', new ivm.ExternalCopy(res?.status).copyInto());
context.global.setSync('headers', new ivm.ExternalCopy(res?.headers).copyInto());
context.global.setSync('body', new ivm.ExternalCopy(res?.body).copyInto());
context.global.setSync('responseTime', new ivm.ExternalCopy(res?.responseTime).copyInto());
context.global.setSync('getStatus', function () {
return res?.getStatus();
});
context.global.setSync('getHeader', function (name) {
return res?.getHeader(name);
});
context.global.setSync('getHeaders', function () {
return res?.getHeaders();
});
context.global.setSync('getBody', function () {
return res?.getBody();
});
context.global.setSync('getResponseTime', function () {
return res?.getResponseTime();
});
context.evalSync(`
res = {
...res || {},
status: global.status,
statusText: global.statusText,
headers: global.headers,
body: global.body,
responseTime: global.responseTime,
getStatus: global.getStatus,
getHeader: global.getHeader,
getHeaders: global.getHeaders,
getBody: global.getBody,
getResponseTime: global.getResponseTime
}
`);
};
module.exports = addBrunoResponseShimToContext;

View File

@ -0,0 +1,39 @@
const addConsoleShimToContext = (context, console) => {
context.global.setSync('log', function (...args) {
console?.log && console.log(...args);
return args;
});
context.global.setSync('debug', function (...args) {
console?.debug && console.debug(...args);
return args;
});
context.global.setSync('info', function (...args) {
console?.info && console.info(...args);
return args;
});
context.global.setSync('warn', function (...args) {
console?.warn && console.warn(...args);
return args;
});
context.global.setSync('error', function (...args) {
console?.error && console.error(...args);
return args;
});
context.evalSync(`
console = {
...console || {},
log: global.log,
debug: global.debug,
info: global.info,
warn: global.warn,
error: global.error
}
`);
};
module.exports = addConsoleShimToContext;

View File

@ -0,0 +1,57 @@
const axios = require('axios');
const ivm = require('isolated-vm');
const { cleanJson } = require('../../../../utils');
const addAxiosShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.axios = (...args) => $0.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
${['get', 'post', 'put', 'patch', 'delete']
?.map(
(method, idx) =>
`globalThis.axios.${method} = (...args) => $${
idx + 1
}.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)))`
)
.join('\n')}
globalThis.requireObject = {
...globalThis.requireObject,
axios: globalThis.axios,
}
`,
[
async (...argStrings) => {
console.log(argStrings);
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios(...args)
.then((response) => {
return cleanJson(response?.data);
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
...['get', 'post', 'put', 'patch', 'delete']?.map((method) => async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios[method](...args)
.then((response) => {
return cleanJson(response?.data);
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
})
],
{ arguments: { reference: true } }
);
};
module.exports = addAxiosShimToContext;

View File

@ -0,0 +1,121 @@
const axios = require('axios');
const ivm = require('isolated-vm');
const { cleanJson } = require('../../../../utils');
const addAxiosShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.axios = (...args) => $0.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.get = (...args) => $1.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.post = (...args) => $2.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.put = (...args) => $3.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.delete = (...args) => $4.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.patch = (...args) => $5.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.requireObject = {
...globalThis.requireObject,
axios: globalThis.axios,
}
`,
[
async (...argStrings) => {
console.log(argStrings);
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.get(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.post(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.put(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.delete(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.patch(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
};
module.exports = addAxiosShimToContext;

View File

@ -0,0 +1,11 @@
const addAxiosShimToContext = require('./axios');
const addNanoidShimToContext = require('./nanoid');
const addUuidShimToContext = require('./uuid');
const addLibraryShimsToContext = async (context) => {
await addAxiosShimToContext(context);
await addNanoidShimToContext(context);
await addUuidShimToContext(context);
};
module.exports = addLibraryShimsToContext;

View File

@ -0,0 +1,23 @@
const ivm = require('isolated-vm');
const { nanoid } = require('nanoid');
const addNanoidShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.nanoid = {};
globalThis.nanoid.nanoid = () => $0.applySync(undefined);
globalThis.requireObject = {
...globalThis.requireObject,
nanoid: globalThis.nanoid
}
`,
[
() => {
return new ivm.ExternalCopy(nanoid()).copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
};
module.exports = addNanoidShimToContext;

View File

@ -0,0 +1,39 @@
const ivm = require('isolated-vm');
const uuid = require('uuid');
const { MAX, NIL } = uuid;
const addUuidShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.uuid = {};
globalThis.uuid.MAX = $0;
globalThis.uuid.NIL = $1;
${['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']
?.map(
(fn, idx) =>
`globalThis.uuid.${fn} = (...args) => $${
idx + 2
}.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));`
)
.join('\n')}
globalThis.requireObject = {
...globalThis.requireObject,
uuid: globalThis.uuid,
}
`,
[
new ivm.ExternalCopy(MAX).copyInto({ release: true }),
new ivm.ExternalCopy(NIL).copyInto({ release: true }),
...['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']?.map(
(fn) =>
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(uuid[fn](...args)).copyInto({ release: true });
}
)
],
{ arguments: { reference: true } }
);
};
module.exports = addUuidShimToContext;

View File

@ -0,0 +1,83 @@
const ivm = require('isolated-vm');
const { MAX, NIL, parse, stringify, v1, v1ToV6, v3, v4, v5, v6, v6ToV1, v7, validate, version } = require('uuid');
const addUuidShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.uuid = {};
globalThis.uuid.MAX = $0;
globalThis.uuid.NIL = $1;
globalThis.uuid.version = (...args) => $2.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.parse = (...args) => $3.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.stringify = (...args) => $4.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v1 = (...args) => $5.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v1ToV6 = (...args) => $6.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v3 = (...args) => $7.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v4 = (...args) => $8.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v5 = (...args) => $9.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v6 = (...args) => $10.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v6ToV1 = (...args) => $11.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v7 = (...args) => $12.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.validate = (...args) => $13.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.requireObject = {
...globalThis.requireObject,
uuid: globalThis.uuid,
}
`,
[
new ivm.ExternalCopy(MAX).copyInto({ release: true }),
new ivm.ExternalCopy(NIL).copyInto({ release: true }),
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(version(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(parse(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(stringify(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v1(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v1ToV6(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v3(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v4(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v5(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v6(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v6ToV1(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v7(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(validate(...args)).copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
};
module.exports = addUuidShimToContext;

View File

@ -0,0 +1,32 @@
const ivm = require('isolated-vm');
const addSleepShimToContext = (context, console) => {
context.evalClosureSync(
`
global.sleep = (...args) => $0.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
`,
[
async (...argStrings) => {
await new Promise((resolve) => {
const timer = Number(argStrings?.[0]);
if (!Number.isInteger(timer) || timer < 0) {
resolve();
}
setTimeout(() => {
resolve();
}, timer);
});
return new ivm.ExternalCopy('done').copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
context.evalSync(`
global.setTimeout = async (fn, timer) => {
await sleep(timer);
await fn.apply();
}
`);
};
module.exports = addSleepShimToContext;

View File

@ -0,0 +1,57 @@
const addTestShimToContext = async (context, __brunoTestResults) => {
context.global.setSync('addResult', function (v) {
__brunoTestResults.addResult(v);
});
context.global.setSync('getResults', function () {
return __brunoTestResults.getResults();
});
context.evalSync(`
global.expect = require('chai').expect;
global.assert = require('chai').assert;
global.__brunoTestResults = {
addResult: global.addResult,
getResults: global.getResults,
}
global.DummyChaiAssertionError = class DummyChaiAssertionError extends Error {
constructor(message, props, ssf) {
super(message);
this.name = "AssertionError";
Object.assign(this, props);
}
}
global.Test = (__brunoTestResults) => async (description, callback) => {
try {
await callback();
__brunoTestResults.addResult({ description, status: "pass" });
} catch (error) {
if (error instanceof DummyChaiAssertionError) {
const { message, actual, expected } = error;
__brunoTestResults.addResult({
description,
status: "fail",
error: message,
actual,
expected,
});
} else {
__brunoTestResults.addResult({
description,
status: "fail",
error: error.message || "An unexpected error occurred.",
});
}
console.log(error);
}
};
global.test = Test(__brunoTestResults);
`);
};
module.exports = addTestShimToContext;

View File

@ -0,0 +1,84 @@
const rollup = require('rollup');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const fs = require('fs');
const { terser } = require('rollup-plugin-terser');
const bundleLibraries = async () => {
const codeScript = `
import { expect, assert } from 'chai';
import { Buffer } from "buffer";
import moment from "moment";
import btoa from "btoa";
import atob from "atob";
globalThis.expect = expect;
globalThis.assert = assert;
globalThis.moment = moment;
globalThis.btoa = btoa;
globalThis.atob = atob;
globalThis.Buffer = Buffer;
globalThis.requireObject = {
'chai': { expect, assert },
'moment': moment,
'buffer': { Buffer },
'btoa': btoa,
'atob': atob,
};
`;
const config = {
input: {
input: 'inline-code',
plugins: [
{
name: 'inline-code-plugin',
resolveId(id) {
if (id === 'inline-code') {
return id;
}
return null;
},
load(id) {
if (id === 'inline-code') {
return codeScript;
}
return null;
}
},
nodeResolve({
preferBuiltins: false,
browser: false
}),
commonjs(),
terser()
]
},
output: {
file: './src/bundle-browser-rollup.js',
format: 'iife',
name: 'MyBundle'
}
};
try {
const bundle = await rollup.rollup(config.input);
const { output } = await bundle.generate(config.output);
fs.writeFileSync(
'./src/bundle-browser-rollup.js',
`
const getBundledCode = () => {
return function(){
${output?.map((o) => o.code).join('\n')}
}()
}
module.exports = getBundledCode;
`
);
} catch (error) {
console.error('Error while bundling:', error);
}
};
bundleLibraries();
module.exports = bundleLibraries;

View File

@ -1,10 +1,17 @@
const ivm = require('isolated-vm');
const addBruShimToContext = require('./shims/bru');
const addQuickBruShimToContext = require('./shims/bru-quick');
const addBrunoRequestShimToContext = require('./shims/bruno-request');
const addConsoleShimToContext = require('./shims/console');
const addQuickBrunoRequestShimToContext = require('./shims/bruno-request-quick');
const addBrunoResponseShimToContext = require('./shims/bruno-response');
const addTestShimToContext = require('./shims/test');
const addQuickBrunoResponseShimToContext = require('./shims/bruno-response-quick');
const addConsoleShimToContext = require('./shims/console');
const addTestShimToContext = require('./shims/test-quick');
const addLibraryShimsToContext = require('./shims/lib');
const { getQuickJS, newQuickJSAsyncWASMModule } = require('quickjs-emscripten');
// execute `npm run build:isolated-vm:inbuilt-modules` if the below file doesn't exist
const getBundledCode = require('../../bundle-browser-rollup');
@ -72,93 +79,151 @@ const executeInIsolatedVMAsync = async ({
return toNumber(externalScript);
}
let result;
const isolate = new ivm.Isolate();
try {
const context = await isolate.createContext();
await context.global.set('global', context.global.derefInto());
// const QuickJS = await getQuickJS()
// const vm = QuickJS.newContext();
context.evalSync(`
let bru = {};
let req = {};
let res = {};
const module = await newQuickJSAsyncWASMModule()
const runtime = module.newRuntime()
const vm = runtime.newContext()
// context.evalSync(`
// let bru = {};
// let req = {};
// let res = {};
// let console = {};
// global.requireObject = {};
// `);
const bundledCode = getBundledCode?.toString() || '';
let bundledScript = `(${bundledCode})()`;
bundledScript += `
globalThis.require = (module) => {
return globalThis.requireObject[module];
}
`;
bundledScript += `
let bru = {
cwd: __bruno__cwd,
getEnvName: __bruno__getEnvName,
getProcessEnv: __bruno__getProcessEnv,
getEnvVar: __bruno__getEnvVar,
setEnvVar: __bruno__setEnvVar,
getVar: __bruno__getVar,
setVar: __bruno__setVar,
setNextRequest: __bruno__setNextRequest,
visualize: __bruno__visualize,
getSecretVar: __bruno__getSecretVar
};
let req = {
url: __bruno__req__url,
method: __bruno__req__method,
headers: __bruno__req__headers,
body: __bruno__req__body,
timeout: __bruno__req__timeout,
getUrl: __bruno__req__getUrl,
setUrl: __bruno__req__setUrl,
getMethod: __bruno__req__getMethod,
getAuthMode: __bruno__req__getAuthMode,
setMethod: __bruno__req__setMethod,
getHeaders: __bruno__req__getHeaders,
setHeaders: __bruno__req__setHeaders,
getHeader: __bruno__req__getHeader,
setHeader: __bruno__req__setHeader
getBody: __bruno__req__getBody,
setBody: __bruno__req__setBody,
setMaxRedirects: __bruno__req__setMaxRedirects,
getTimeout: __bruno__req__getTimeout,
setTimeout: __bruno__req__setTimeout
};
let res = {
status: __bruno__res__status,
headers: __bruno__res__headers,
body: __bruno__res__body,
responseTime: __bruno__res__responseTime,
getStatus: __bruno__res__getStatus,
getHeader: __bruno__res__getHeader,
getHeaders: __bruno__res__getHeaders,
getBody: __bruno__res__getBody,
getResponseTime: __bruno__res__getResponseTime
};
let console = {};
global.requireObject = {};
`);
context.global.setSync('log', function (...args) {
console.debug(...args);
});
try {
const bundledCode = getBundledCode?.toString() || '';
await context.eval(`(${bundledCode})()`);
} catch (err) {
console.debug('Error bundling libraries', err);
}
`;
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
bru && addBruShimToContext(context, bru);
req && addBrunoRequestShimToContext(context, req);
res && addBrunoResponseShimToContext(context, res);
consoleFn && addConsoleShimToContext(context, consoleFn);
addSleepShimToContext(context);
bru && addQuickBruShimToContext(vm, bru);
req && addQuickBrunoRequestShimToContext(vm, req);
res && addQuickBrunoResponseShimToContext(vm, res);
// consoleFn && addConsoleShimToContext(context, consoleFn);
// addSleepShimToContext(context);
await context.eval(
`
global.require = (module) => {
return global.requireObject[module];
// await addLibraryShimsToContext(context);
test && __brunoTestResults && addTestShimToContext(vm, __brunoTestResults);
bundledScript += `
globalThis.expect = require('chai').expect;
globalThis.assert = require('chai').assert;
globalThis.__brunoTestResults = {
addResult: globalThis.__bruno__addResult,
getResults: globalThis.__bruno__getResults,
}
globalThis.DummyChaiAssertionError = class DummyChaiAssertionError extends Error {
constructor(message, props, ssf) {
super(message);
this.name = "AssertionError";
Object.assign(this, props);
}
`
);
}
await addLibraryShimsToContext(context);
test && __brunoTestResults && (await addTestShimToContext(context, __brunoTestResults));
context.global.setSync('setResult', function (arg) {
result = arg;
});
const jsScriptText = `
new Promise(async (resolve, reject) => {
// modify the setTimeout function with the shim to work-around the callback-function clone issues
setTimeout = global.setTimeout;
console?.debug && console.debug('isolated-vm:execution-start:');
globalThis.Test = (__brunoTestResults) => (description, callback) => {
try {
${externalScript}
callback();
__brunoTestResults.addResult({ description, status: "pass" });
} catch (error) {
console?.debug && console.debug('isolated-vm:execution-end:with-error', error?.message);
if (error instanceof DummyChaiAssertionError) {
const { message, actual, expected } = error;
__brunoTestResults.addResult({
description,
status: "fail",
error: message,
actual,
expected,
});
} else {
globalThis.__bruno__addResult({
description,
status: "fail",
error: error.message || "An unexpected error occurred.",
});
}
// console.log(error);
}
console?.debug && console.debug('isolated-vm:execution-end:');
resolve();
});
};
globalThis.test = Test(__brunoTestResults);
`;
const templateLiteralText = `
let value = \`${externalScript}\`;
setResult(value);
`;
bundledScript += externalScript;
const jsExpressionText = `
let value = ${externalScript};
setResult(value);
`;
let scriptText =
scriptType === 'template-literal'
? templateLiteralText
: scriptType === 'expression'
? jsExpressionText
: jsScriptText;
const script = await isolate.compileScript(scriptText);
await script.run(context);
const result = await vm.evalCodeAsync(bundledScript);
console.log('Result:', result);
if (result.error) {
console.log("Execution failed:", vm.dump(result.error))
result.error.dispose()
} else {
console.log("Success:", vm.dump(result.value))
result.value.dispose();
}
vm.dispose();
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
isolate.dispose();
};
module.exports = {

View File

@ -0,0 +1,65 @@
const { marshallToVm } = require('../utils');
const addBruShimToContext = (vm, bru) => {
let cwd = vm.newFunction('cwd', function () {
return marshallToVm(bru.cwd(), vm);
});
vm.setProp(vm.global, "__bruno__cwd", cwd)
cwd.dispose();
let getEnvName = vm.newFunction('getEnvName', function () {
return marshallToVm(bru.getEnvName(), vm);
});
vm.setProp(vm.global, "__bruno__getEnvName", getEnvName);
getEnvName.dispose();
let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getProcessEnv", getProcessEnv);
getProcessEnv.dispose();
let getEnvVar = vm.newFunction('getEnvVar', function (key) {
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getEnvVar", getEnvVar);
getEnvVar.dispose();
let setEnvVar = vm.newFunction('setEnvVar', function (key, value) {
bru.setEnvVar(vm.dump(key), vm.dump(value));
});
vm.setProp(vm.global, "__bruno__setEnvVar", setEnvVar);
setEnvVar.dispose();
let getVar = vm.newFunction('getVar', function (key) {
return marshallToVm(bru.getVar(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getVar", getVar);
getVar.dispose();
let setVar = vm.newFunction('setVar', function (key, value) {
bru.setVar(vm.dump(key), vm.dump(value));
});
vm.setProp(vm.global, "__bruno__setVar", setVar);
setVar.dispose();
let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
bru.setNextRequest(vm.dump(nextRequest));
});
vm.setProp(vm.global, "__bruno__setNextRequest", setNextRequest);
setNextRequest.dispose();
let visualize = vm.newFunction('visualize', function (htmlString) {
bru.visualize(vm.dump(htmlString));
});
vm.setProp(vm.global, "__bruno__visualize", visualize);
visualize.dispose();
let getSecretVar = vm.newFunction('getSecretVar', function (key) {
return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getSecretVar", getSecretVar);
getSecretVar.dispose();
};
module.exports = addBruShimToContext;

View File

@ -0,0 +1,107 @@
const { marshallToVm } = require('../utils');
const addBrunoRequestShimToContext = (vm, req) => {
const url = marshallToVm(req.getUrl(), vm);
const method = marshallToVm(req.getMethod(), vm);
const headers = marshallToVm(req.getHeaders(), vm);
const body = marshallToVm(req.getBody(), vm);
const timeout = marshallToVm(req.getTimeout(), vm);
vm.setProp(vm.global, '__bruno__requrl', url);
vm.setProp(vm.global, '__bruno__reqmethod', method);
vm.setProp(vm.global, '__bruno__reqheaders', headers);
vm.setProp(vm.global, '__bruno__reqbody', body);
vm.setProp(vm.global, '__bruno__reqtimeout', timeout);
url.dispose();
method.dispose();
headers.dispose();
body.dispose();
timeout.dispose();
// let getUrl = vm.newFunction('__bruno__req__getUrl', function () {
// return marshallToVm(req.getUrl(), vm);
// });
// vm.setProp(vm.global, 'getUrl', getUrl);
// getUrl.dispose();
// let setUrl = vm.newFunction('__bruno__req__setUrl', function (url) {
// req.setUrl(vm.dump(url));
// });
// vm.setProp(vm.global, 'setUrl', setUrl);
// setUrl.dispose();
// let getMethod = vm.newFunction('__bruno__req__getMethod', function () {
// return marshallToVm(req.getMethod(), vm);
// });
// vm.setProp(vm.global, 'getMethod', getMethod);
// getMethod.dispose();
// let getAuthMode = vm.newFunction('__bruno__req__getAuthMode', function () {
// return marshallToVm(req.getAuthMode(), vm);
// });
// vm.setProp(vm.global, 'getAuthMode', getAuthMode);
// getAuthMode.dispose();
// let setMethod = vm.newFunction('__bruno__req__setMethod', function (method) {
// req.setMethod(vm.dump(method));
// });
// vm.setProp(vm.global, 'setMethod', setMethod);
// setMethod.dispose();
// let getHeaders = vm.newFunction('__bruno__req__getHeaders', function () {
// return marshallToVm(req.getHeaders(), vm);
// });
// vm.setProp(vm.global, 'getHeaders', getHeaders);
// getHeaders.dispose();
// let setHeaders = vm.newFunction('__bruno__req__setHeaders', function (headers) {
// req.setHeaders(vm.dump(headers));
// });
// vm.setProp(vm.global, 'setHeaders', setHeaders);
// setHeaders.dispose();
// let getHeader = vm.newFunction('__bruno__req__getHeader', function (name) {
// return marshallToVm(req.getHeader(vm.dump(name)), vm);
// });
// vm.setProp(vm.global, 'getHeader', getHeader);
// getHeader.dispose();
// let setHeader = vm.newFunction('__bruno__req__setHeader', function (name, value) {
// req.setHeader(vm.dump(name), vm.dump(value));
// });
// vm.setProp(vm.global, 'setHeader', setHeader);
// setHeader.dispose();
// let getBody = vm.newFunction('__bruno__req__getBody', function () {
// return marshallToVm(req.getBody(), vm);
// });
// vm.setProp(vm.global, 'getBody', getBody);
// getBody.dispose();
// let setBody = vm.newFunction('__bruno__req__setBody', function (data) {
// req.setBody(vm.dump(data));
// });
// vm.setProp(vm.global, 'setBody', setBody);
// setBody.dispose();
// let setMaxRedirects = vm.newFunction('__bruno__req__setMaxRedirects', function (maxRedirects) {
// req.setMaxRedirects(vm.dump(maxRedirects));
// });
// vm.setProp(vm.global, 'setMaxRedirects', setMaxRedirects);
// setMaxRedirects.dispose();
// let getTimeout = vm.newFunction('__bruno__req__getTimeout', function () {
// return marshallToVm(req.getTimeout(), vm);
// });
// vm.setProp(vm.global, 'getTimeout', getTimeout);
// getTimeout.dispose();
// let setTimeout = vm.newFunction('__bruno__req__setTimeout', function (timeout) {
// req.setTimeout(vm.dump(timeout));
// });
// vm.setProp(vm.global, 'setTimeout', setTimeout);
// setTimeout.dispose();
};
module.exports = addBrunoRequestShimToContext;

View File

@ -0,0 +1,51 @@
const { marshallToVm } = require('../utils');
const addBrunoResponseShimToContext = (vm, res) => {
const status = marshallToVm(res?.status, vm);
const headers = marshallToVm(res?.headers, vm);
const body = marshallToVm(res?.body, vm);
const responseTime = marshallToVm(res?.responseTime, vm);
vm.setProp(vm.global, '__bruno__res__status', status);
vm.setProp(vm.global, '__bruno__res__headers', headers);
vm.setProp(vm.global, '__bruno__res__body', body);
vm.setProp(vm.global, '__bruno__res__responseTime', responseTime);
status.dispose();
headers.dispose();
body.dispose();
responseTime.dispose();
let getStatus = vm.newFunction('getStatus', function () {
return marshallToVm(res.getStatus(), vm);
});
vm.setProp(vm.global, '__bruno__res__getStatus', getStatus);
getStatus.dispose();
let getHeader = vm.newFunction('getHeader', function (name) {
return marshallToVm(res.getHeader(vm.dump(name)), vm);
});
vm.setProp(vm.global, '__bruno__res__getHeader', getHeader);
getHeader.dispose();
let getHeaders = vm.newFunction('getHeaders', function () {
return marshallToVm(res.getHeaders(), vm);
});
vm.setProp(vm.global, '__bruno__res__getHeaders', getHeaders);
getHeaders.dispose();
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(res.getBody(), vm);
});
vm.setProp(vm.global, '__bruno__res__getBody', getBody);
getBody.dispose();
let getResponseTime = vm.newFunction('getResponseTime', function () {
return marshallToVm(res.getResponseTime(), vm);
});
vm.setProp(vm.global, '__bruno__res__getResponseTime', getResponseTime);
getResponseTime.dispose();
};
module.exports = addBrunoResponseShimToContext;

View File

@ -0,0 +1,17 @@
const { marshallToVm } = require('../utils');
const addBruShimToContext = (vm, __brunoTestResults) => {
let addResult = vm.newFunction('addResult', function (v) {
__brunoTestResults.addResult(vm.dump(v));
});
vm.setProp(vm.global, "__bruno__addResult", addResult);
addResult.dispose();
let getResults = vm.newFunction('getResults', function () {
return marshallToVm(__brunoTestResults.getResults(), vm);
});
vm.setProp(vm.global, "__bruno__getResults", getResults);
getResults.dispose();
};
module.exports = addBruShimToContext;

View File

@ -11,13 +11,13 @@ const bundleLibraries = async () => {
import moment from "moment";
import btoa from "btoa";
import atob from "atob";
global.expect = expect;
global.assert = assert;
global.moment = moment;
global.btoa = btoa;
global.atob = atob;
global.Buffer = Buffer;
global.requireObject = {
globalThis.expect = expect;
globalThis.assert = assert;
globalThis.moment = moment;
globalThis.btoa = btoa;
globalThis.atob = atob;
globalThis.Buffer = Buffer;
globalThis.requireObject = {
'chai': { expect, assert },
'moment': moment,
'buffer': { Buffer },

View File

@ -0,0 +1,31 @@
const marshallToVm = (value, vm) => {
if (value === undefined) {
return vm.undefined;
}
if (typeof value === "string") {
return vm.newString(value);
} else if (typeof value === "number") {
return vm.newNumber(value);
} else if (typeof value === "boolean") {
return vm.newBoolean(value);
} else if (typeof value === "object") {
if (Array.isArray(value)) {
const arr = vm.newArray();
for (let i = 0; i < value.length; i++) {
vm.setElement(arr, i, marshallToVm(value[i]));
}
return arr;
} else {
const obj = vm.newObject();
for (const key in value) {
vm.setProp(obj, key, marshallToVm(value[key]));
}
return obj;
}
}
};
module.exports = {
marshallToVm
};

View File

@ -8,6 +8,3 @@ vars {
foo: bar
testSetEnvVar: bruno-29653
}
vars:secret [
bruno
]

View File

@ -5,56 +5,28 @@ meta {
}
get {
url: {{host}}/ping
url: https://testbench-sanity.usebruno.com/ping
body: none
auth: none
}
auth:awsv4 {
accessKeyId: a
secretAccessKey: b
sessionToken: c
service: d
region: e
profileName: f
}
vars:pre-request {
m4: true
pong: pong
}
assert {
res.status: eq 200
res.responseTime: lte 2000
res.body: eq {{pong}}
script:pre-request {
// const moment = require("moment");
// function a() {
// return moment().format("hh:mm");
// return test;
// }
// a();
}
tests {
test("should ping pong", function() {
const data = res.getBody();
expect(data).to.equal(bru.getRequestVar("pong"));
test("should get var in scripts", function() {
expect("bruno-test-87267").to.equal("bruno-test-87267");
});
}
docs {
# API Documentation
## Introduction
Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
## Authentication
Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
### API Key
To use API key authentication, include your API key in the request headers as follows:
```http
GET /api/endpoint
Host: api.example.com
Authorization: Bearer YOUR_API_KEY
// const moment = require("moment");
// function a() {
// return moment().format("hh:mm");
// return test;
// }
// a();
}

View File

@ -10,7 +10,6 @@ get {
auth: none
}
script:pre-request {
const envName = bru.getEnvName();
bru.setVar("testEnvName", envName);
@ -21,4 +20,4 @@ tests {
const testEnvName = bru.getVar("testEnvName");
expect(testEnvName).to.equal("Prod");
});
}
}

View File

@ -10,7 +10,6 @@ get {
auth: none
}
script:post-response {
bru.setVar("testSetVar", "bruno-test-87267")
}
@ -20,4 +19,4 @@ tests {
const testSetVar = bru.getVar("testSetVar");
expect(testSetVar).to.equal("bruno-test-87267");
});
}
}