Feat/safe mode quickjs (#2848)

Safe Mode Sandbox using QuickJS
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
Co-authored-by: lohit <lohit.jiddimani@gmail.com>
This commit is contained in:
Anoop M D
2024-08-21 12:52:49 +05:30
committed by GitHub
parent 3ad4eda861
commit 753a576c3c
92 changed files with 8880 additions and 20951 deletions

1
packages/bruno-js/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
src/sandbox/bundle-browser-rollup.js

View File

@ -11,7 +11,8 @@
"@n8n/vm2": "^3.9.23"
},
"scripts": {
"test": "jest --testPathIgnorePatterns test.js"
"test": "node --experimental-vm-modules $(npx --no-install which jest) --testPathIgnorePatterns test.js",
"sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js"
},
"dependencies": {
"@usebruno/common": "0.1.0",
@ -24,12 +25,29 @@
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1",
"crypto-js-3.1.9-1": "npm:crypto-js@^3.1.9-1",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.4",
"node-fetch": "2.*",
"node-fetch": "^2.7.0",
"node-vault": "^0.10.2",
"path": "^0.12.7",
"quickjs-emscripten": "^0.29.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "3.2.5",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-polyfill-node": "^0.13.0",
"rollup-plugin-terser": "^7.0.2",
"stream": "^0.0.2",
"terser": "^5.31.1",
"uglify-js": "^3.18.0",
"util": "^0.12.5"
}
}

View File

@ -100,6 +100,10 @@ class Bru {
setNextRequest(nextRequest) {
this.nextRequest = nextRequest;
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
module.exports = Bru;

View File

@ -5,6 +5,7 @@ const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { interpolateString } = require('../interpolate-string');
const { executeQuickJsVm } = require('../sandbox/quickjs');
const { expect } = chai;
chai.use(require('chai-string'));
@ -161,7 +162,31 @@ const isUnaryOperator = (operator) => {
return unaryOperators.includes(operator);
};
const evaluateRhsOperand = (rhsOperand, operator, context) => {
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
script: literal,
context,
scriptType: 'template-literal'
});
}
return evaluateJsTemplateLiteral(literal, context);
};
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
script: expr,
context,
scriptType: 'expression'
});
}
return evaluateJsExpression(expr, context);
};
const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
if (isUnaryOperator(operator)) {
return;
}
@ -181,13 +206,17 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
return rhsOperand
.split(',')
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
.map((v) =>
evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
);
}
if (operator === 'between') {
const [lhs, rhs] = rhsOperand
.split(',')
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
.map((v) =>
evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)
);
return [lhs, rhs];
}
@ -200,10 +229,14 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
return interpolateString(rhsOperand, interpolationContext);
}
return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
return evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(rhsOperand, interpolationContext), context, runtime);
};
class AssertRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
}
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
const requestVariables = request?.requestVariables || {};
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
@ -238,8 +271,8 @@ class AssertRuntime {
const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);
try {
const lhs = evaluateJsExpression(lhsExpr, context);
const rhs = evaluateRhsOperand(rhsOperand, operator, context);
const lhs = evaluateJsExpressionBasedOnRuntime(lhsExpr, context, this.runtime);
const rhs = evaluateRhsOperand(rhsOperand, operator, context, this.runtime);
switch (operator) {
case 'eq':

View File

@ -28,9 +28,12 @@ const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class ScriptRuntime {
constructor() {}
constructor(props) {
this.runtime = props?.runtime || 'vm2';
}
// This approach is getting out of hand
// Need to refactor this to use a single arg (object) instead of 7
@ -86,6 +89,22 @@ class ScriptRuntime {
};
}
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: script,
context: context,
collectionPath
});
return {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
nextRequestName: bru.nextRequest
};
}
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
@ -123,6 +142,7 @@ class ScriptRuntime {
});
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
await asyncVM();
return {
request,
envVariables: cleanJson(envVariables),
@ -176,10 +196,27 @@ class ScriptRuntime {
log: customLogger('log'),
info: customLogger('info'),
warn: customLogger('warn'),
error: customLogger('error')
error: customLogger('error'),
debug: customLogger('debug')
};
}
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: script,
context: context,
collectionPath
});
return {
response,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
nextRequestName: bru.nextRequest
};
}
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {

View File

@ -15,7 +15,7 @@ const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const Test = require('../test');
const TestResults = require('../test-results');
const { cleanJson } = require('../utils');
const { cleanJson, appendAwaitToTestFunc } = require('../utils');
// Inbuilt Library Support
const ajv = require('ajv');
@ -30,9 +30,12 @@ const axios = require('axios');
const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class TestRuntime {
constructor() {}
constructor(props) {
this.runtime = props?.runtime || 'vm2';
}
async runTests(
testsFile,
@ -81,6 +84,9 @@ class TestRuntime {
};
}
// add 'await' prefix to the test function calls
testsFile = appendAwaitToTestFunc(testsFile);
const context = {
test,
bru,
@ -101,48 +107,56 @@ class TestRuntime {
log: customLogger('log'),
info: customLogger('info'),
warn: customLogger('warn'),
debug: customLogger('debug'),
error: customLogger('error')
};
}
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
btoa,
atob,
lodash,
moment,
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
if (this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: testsFile,
context: context
});
} else {
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
btoa,
atob,
lodash,
moment,
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
}
}
}
});
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
await asyncVM();
});
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
await asyncVM();
}
return {
request,

View File

@ -3,7 +3,38 @@ const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { executeQuickJsVm } = require('../sandbox/quickjs');
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
script: literal,
context,
scriptType: 'template-literal'
});
}
return evaluateJsTemplateLiteral(literal, context);
};
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
if (runtime === 'quickjs') {
return executeQuickJsVm({
script: expr,
context,
scriptType: 'expression'
});
}
return evaluateJsExpression(expr, context);
};
class VarsRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
this.mode = props?.mode || 'developer';
}
runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
if (!request?.requestVariables) {
request.requestVariables = {};
@ -28,7 +59,7 @@ class VarsRuntime {
};
_.each(enabledVars, (v) => {
const value = evaluateJsTemplateLiteral(v.value, context);
const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
request?.requestVariables && (request.requestVariables[v.name] = value);
});
}
@ -59,7 +90,7 @@ class VarsRuntime {
const errors = new Map();
_.each(enabledVars, (v) => {
try {
const value = evaluateJsExpression(v.value, context);
const value = evaluateJsExpressionBasedOnRuntime(v.value, context, this.runtime);
bru.setVar(v.name, value);
} catch (error) {
errors.set(v.name, error);

View File

@ -0,0 +1,88 @@
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";
import * as CryptoJS from "crypto-js-3.1.9-1";
globalThis.expect = expect;
globalThis.assert = assert;
globalThis.moment = moment;
globalThis.btoa = btoa;
globalThis.atob = atob;
globalThis.Buffer = Buffer;
globalThis.CryptoJS = CryptoJS;
globalThis.requireObject = {
...(globalThis.requireObject || {}),
'chai': { expect, assert },
'moment': moment,
'buffer': { Buffer },
'btoa': btoa,
'atob': atob,
'crypto-js': CryptoJS
};
`;
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/sandbox/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/sandbox/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

@ -0,0 +1,153 @@
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');
const addLocalModuleLoaderShimToContext = require('./shims/local-module');
const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscripten');
// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
const getBundledCode = require('../bundle-browser-rollup');
const addPathShimToContext = require('./shims/lib/path');
let QuickJSSyncContext;
const loader = memoizePromiseFactory(() => newQuickJSWASMModule());
const getContext = (opts) => loader().then((mod) => (QuickJSSyncContext = mod.newContext(opts)));
getContext();
const toNumber = (value) => {
const num = Number(value);
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {
if (!isNaN(Number(externalScript))) {
return Number(externalScript);
}
const vm = QuickJSSyncContext;
try {
const { bru, req, res } = externalContext;
bru && addBruShimToContext(vm, bru);
req && addBrunoRequestShimToContext(vm, req);
res && addBrunoResponseShimToContext(vm, res);
const templateLiteralText = `\`${externalScript}\`;`;
const jsExpressionText = `${externalScript};`;
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
const result = vm.evalCode(scriptText);
if (result.error) {
let e = vm.dump(result.error);
result.error.dispose();
return e;
} else {
let v = vm.dump(result.value);
let vString = v.toString();
result.value.dispose();
return v;
}
} catch (error) {
console.error('Error executing the script!', error);
}
};
const executeQuickJsVmAsync = async ({ script: externalScript, context: externalContext, collectionPath }) => {
if (!isNaN(Number(externalScript))) {
return toNumber(externalScript);
}
try {
const module = await newQuickJSWASMModule();
const vm = module.newContext();
const bundledCode = getBundledCode?.toString() || '';
const moduleLoaderCode = function () {
return `
globalThis.require = (mod) => {
let lib = globalThis.requireObject[mod];
let isModuleAPath = (module) => (module?.startsWith('.') || module?.startsWith?.(bru.cwd()))
if (lib) {
return lib;
}
else if (isModuleAPath(mod)) {
// fetch local module
let localModuleCode = globalThis.__brunoLoadLocalModule(mod);
// compile local module as iife
(function (){
const initModuleExportsCode = "const module = { exports: {} };"
const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;";
const patchedRequire = ${`
"\\n;" +
"let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)" +
"\\n;"
`}
eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode);
})();
// resolve module
return globalThis.requireObject[mod];
}
}
`;
};
vm.evalCode(
`
(${bundledCode})()
${moduleLoaderCode()}
`
);
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
bru && addBruShimToContext(vm, bru);
req && addBrunoRequestShimToContext(vm, req);
res && addBrunoResponseShimToContext(vm, res);
consoleFn && addConsoleShimToContext(vm, consoleFn);
addLocalModuleLoaderShimToContext(vm, collectionPath);
addPathShimToContext(vm);
await addLibraryShimsToContext(vm);
test && __brunoTestResults && addTestShimToContext(vm, __brunoTestResults);
const script = `
(async () => {
const setTimeout = async(fn, timer) => {
v = await bru.sleep(timer);
fn.apply();
}
await bru.sleep(0);
try {
${externalScript}
}
catch(error) {
console?.debug?.('quick-js:execution-end:with-error', error?.message);
}
return 'done';
})()
`;
const result = vm.evalCode(script);
const promiseHandle = vm.unwrapResult(result);
const resolvedResult = await vm.resolvePromise(promiseHandle);
promiseHandle.dispose();
const resolvedHandle = vm.unwrapResult(resolvedResult);
resolvedHandle.dispose();
// vm.dispose();
return;
} catch (error) {
console.error('Error executing the script!', error);
throw new Error(error);
}
};
module.exports = {
executeQuickJsVm,
executeQuickJsVmAsync
};

View File

@ -0,0 +1,81 @@
const { marshallToVm } = require('../utils');
const addBruShimToContext = (vm, bru) => {
const bruObject = vm.newObject();
let cwd = vm.newFunction('cwd', function () {
return marshallToVm(bru.cwd(), vm);
});
vm.setProp(bruObject, 'cwd', cwd);
cwd.dispose();
let getEnvName = vm.newFunction('getEnvName', function () {
return marshallToVm(bru.getEnvName(), vm);
});
vm.setProp(bruObject, 'getEnvName', getEnvName);
getEnvName.dispose();
let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);
getProcessEnv.dispose();
let getEnvVar = vm.newFunction('getEnvVar', function (key) {
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getEnvVar', getEnvVar);
getEnvVar.dispose();
let setEnvVar = vm.newFunction('setEnvVar', function (key, value) {
bru.setEnvVar(vm.dump(key), vm.dump(value));
});
vm.setProp(bruObject, 'setEnvVar', setEnvVar);
setEnvVar.dispose();
let getVar = vm.newFunction('getVar', function (key) {
return marshallToVm(bru.getVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getVar', getVar);
getVar.dispose();
let setVar = vm.newFunction('setVar', function (key, value) {
bru.setVar(vm.dump(key), vm.dump(value));
});
vm.setProp(bruObject, 'setVar', setVar);
setVar.dispose();
let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
bru.setNextRequest(vm.dump(nextRequest));
});
vm.setProp(bruObject, 'setNextRequest', setNextRequest);
setNextRequest.dispose();
let visualize = vm.newFunction('visualize', function (htmlString) {
bru.visualize(vm.dump(htmlString));
});
vm.setProp(bruObject, 'visualize', visualize);
visualize.dispose();
let getSecretVar = vm.newFunction('getSecretVar', function (key) {
return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);
});
vm.setProp(bruObject, 'getSecretVar', getSecretVar);
getSecretVar.dispose();
const sleep = vm.newFunction('sleep', (timer) => {
const t = vm.getString(timer);
const promise = vm.newPromise();
setTimeout(() => {
promise.resolve(vm.newString('slept'));
}, t);
promise.settled.then(vm.runtime.executePendingJobs);
return promise.handle;
});
sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle));
vm.setProp(vm.global, 'bru', bruObject);
bruObject.dispose();
};
module.exports = addBruShimToContext;

View File

@ -0,0 +1,112 @@
const { marshallToVm } = require('../utils');
const addBrunoRequestShimToContext = (vm, req) => {
const reqObject = vm.newObject();
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(reqObject, 'url', url);
vm.setProp(reqObject, 'method', method);
vm.setProp(reqObject, 'headers', headers);
vm.setProp(reqObject, 'body', body);
vm.setProp(reqObject, 'timeout', timeout);
url.dispose();
method.dispose();
headers.dispose();
body.dispose();
timeout.dispose();
let getUrl = vm.newFunction('getUrl', function () {
return marshallToVm(req.getUrl(), vm);
});
vm.setProp(reqObject, 'getUrl', getUrl);
getUrl.dispose();
let setUrl = vm.newFunction('setUrl', function (url) {
req.setUrl(vm.dump(url));
});
vm.setProp(reqObject, 'setUrl', setUrl);
setUrl.dispose();
let getMethod = vm.newFunction('getMethod', function () {
return marshallToVm(req.getMethod(), vm);
});
vm.setProp(reqObject, 'getMethod', getMethod);
getMethod.dispose();
let getAuthMode = vm.newFunction('getAuthMode', function () {
return marshallToVm(req.getAuthMode(), vm);
});
vm.setProp(reqObject, 'getAuthMode', getAuthMode);
getAuthMode.dispose();
let setMethod = vm.newFunction('setMethod', function (method) {
req.setMethod(vm.dump(method));
});
vm.setProp(reqObject, 'setMethod', setMethod);
setMethod.dispose();
let getHeaders = vm.newFunction('getHeaders', function () {
return marshallToVm(req.getHeaders(), vm);
});
vm.setProp(reqObject, 'getHeaders', getHeaders);
getHeaders.dispose();
let setHeaders = vm.newFunction('setHeaders', function (headers) {
req.setHeaders(vm.dump(headers));
});
vm.setProp(reqObject, 'setHeaders', setHeaders);
setHeaders.dispose();
let getHeader = vm.newFunction('getHeader', function (name) {
return marshallToVm(req.getHeader(vm.dump(name)), vm);
});
vm.setProp(reqObject, 'getHeader', getHeader);
getHeader.dispose();
let setHeader = vm.newFunction('setHeader', function (name, value) {
req.setHeader(vm.dump(name), vm.dump(value));
});
vm.setProp(reqObject, 'setHeader', setHeader);
setHeader.dispose();
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(req.getBody(), vm);
});
vm.setProp(reqObject, 'getBody', getBody);
getBody.dispose();
let setBody = vm.newFunction('setBody', function (data) {
req.setBody(vm.dump(data));
});
vm.setProp(reqObject, 'setBody', setBody);
setBody.dispose();
let setMaxRedirects = vm.newFunction('setMaxRedirects', function (maxRedirects) {
req.setMaxRedirects(vm.dump(maxRedirects));
});
vm.setProp(reqObject, 'setMaxRedirects', setMaxRedirects);
setMaxRedirects.dispose();
let getTimeout = vm.newFunction('getTimeout', function () {
return marshallToVm(req.getTimeout(), vm);
});
vm.setProp(reqObject, 'getTimeout', getTimeout);
getTimeout.dispose();
let setTimeout = vm.newFunction('setTimeout', function (timeout) {
req.setTimeout(vm.dump(timeout));
});
vm.setProp(reqObject, 'setTimeout', setTimeout);
setTimeout.dispose();
vm.setProp(vm.global, 'req', reqObject);
reqObject.dispose();
};
module.exports = addBrunoRequestShimToContext;

View File

@ -0,0 +1,55 @@
const { marshallToVm } = require('../utils');
const addBrunoResponseShimToContext = (vm, res) => {
const resObject = vm.newObject();
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(resObject, 'status', status);
vm.setProp(resObject, 'headers', headers);
vm.setProp(resObject, 'body', body);
vm.setProp(resObject, 'responseTime', responseTime);
status.dispose();
headers.dispose();
body.dispose();
responseTime.dispose();
let getStatus = vm.newFunction('getStatus', function () {
return marshallToVm(res.getStatus(), vm);
});
vm.setProp(resObject, 'getStatus', getStatus);
getStatus.dispose();
let getHeader = vm.newFunction('getHeader', function (name) {
return marshallToVm(res.getHeader(vm.dump(name)), vm);
});
vm.setProp(resObject, 'getHeader', getHeader);
getHeader.dispose();
let getHeaders = vm.newFunction('getHeaders', function () {
return marshallToVm(res.getHeaders(), vm);
});
vm.setProp(resObject, 'getHeaders', getHeaders);
getHeaders.dispose();
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(res.getBody(), vm);
});
vm.setProp(resObject, 'getBody', getBody);
getBody.dispose();
let getResponseTime = vm.newFunction('getResponseTime', function () {
return marshallToVm(res.getResponseTime(), vm);
});
vm.setProp(resObject, 'getResponseTime', getResponseTime);
getResponseTime.dispose();
vm.setProp(vm.global, 'res', resObject);
resObject.dispose();
};
module.exports = addBrunoResponseShimToContext;

View File

@ -0,0 +1,46 @@
const addConsoleShimToContext = (vm, console) => {
if (!console) return;
const consoleHandle = vm.newObject();
const logHandle = vm.newFunction('log', (...args) => {
const nativeArgs = args.map(vm.dump);
console?.log?.(...nativeArgs);
});
const debugHandle = vm.newFunction('debug', (...args) => {
const nativeArgs = args.map(vm.dump);
console?.debug?.(...nativeArgs);
});
const infoHandle = vm.newFunction('info', (...args) => {
const nativeArgs = args.map(vm.dump);
console?.info?.(...nativeArgs);
});
const warnHandle = vm.newFunction('warn', (...args) => {
const nativeArgs = args.map(vm.dump);
console?.warn?.(...nativeArgs);
});
const errorHandle = vm.newFunction('error', (...args) => {
const nativeArgs = args.map(vm.dump);
console?.error?.(...nativeArgs);
});
vm.setProp(consoleHandle, 'log', logHandle);
vm.setProp(consoleHandle, 'debug', debugHandle);
vm.setProp(consoleHandle, 'info', infoHandle);
vm.setProp(consoleHandle, 'warn', warnHandle);
vm.setProp(consoleHandle, 'error', errorHandle);
vm.setProp(vm.global, 'console', consoleHandle);
consoleHandle.dispose();
logHandle.dispose();
debugHandle.dispose();
infoHandle.dispose();
warnHandle.dispose();
errorHandle.dispose();
};
module.exports = addConsoleShimToContext;

View File

@ -0,0 +1,72 @@
const axios = require('axios');
const { cleanJson } = require('../../../../utils');
const { marshallToVm } = require('../../utils');
const methods = ['get', 'post', 'put', 'patch', 'delete'];
const addAxiosShimToContext = async (vm) => {
methods?.forEach((method) => {
const axiosHandle = vm.newFunction(method, (...args) => {
const nativeArgs = args.map(vm.dump);
const promise = vm.newPromise();
axios[method](...nativeArgs)
.then((response) => {
const { status, headers, data } = response || {};
promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));
})
.catch((err) => {
promise.resolve(
marshallToVm(
cleanJson({
message: err.message
}),
vm
)
);
});
promise.settled.then(vm.runtime.executePendingJobs);
return promise.handle;
});
axiosHandle.consume((handle) => vm.setProp(vm.global, `__bruno__axios__${method}`, handle));
});
const axiosHandle = vm.newFunction('axios', (...args) => {
const nativeArgs = args.map(vm.dump);
const promise = vm.newPromise();
axios(...nativeArgs)
.then((response) => {
const { status, headers, data } = response || {};
promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));
})
.catch((err) => {
promise.resolve(
marshallToVm(
cleanJson({
message: err.message
}),
vm
)
);
});
promise.settled.then(vm.runtime.executePendingJobs);
return promise.handle;
});
axiosHandle.consume((handle) => vm.setProp(vm.global, `__bruno__axios`, handle));
vm.evalCode(
`
globalThis.axios = __bruno__axios;
${methods
?.map((method) => {
return `globalThis.axios.${method} = __bruno__axios__${method};`;
})
?.join('\n')}
globalThis.requireObject = {
...globalThis.requireObject,
axios: globalThis.axios,
}
`
);
};
module.exports = addAxiosShimToContext;

View File

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

View File

@ -0,0 +1,24 @@
const { nanoid } = require('nanoid');
const { marshallToVm } = require('../../utils');
const addNanoidShimToContext = async (vm) => {
let _nanoid = vm.newFunction('nanoid', function () {
let v = nanoid();
return marshallToVm(v, vm);
});
vm.setProp(vm.global, '__bruno__nanoid', _nanoid);
_nanoid.dispose();
vm.evalCode(
`
globalThis.nanoid = {};
globalThis.nanoid.nanoid = globalThis.__bruno__nanoid;
globalThis.requireObject = {
...globalThis.requireObject,
'nanoid': globalThis.nanoid
}
`
);
};
module.exports = addNanoidShimToContext;

View File

@ -0,0 +1,28 @@
const path = require('path');
const { marshallToVm } = require('../../utils');
const fns = ['resolve'];
const addPathShimToContext = async (vm) => {
fns.forEach((fn) => {
let fnHandle = vm.newFunction(fn, function (...args) {
const nativeArgs = args.map(vm.dump);
return marshallToVm(path[fn](...nativeArgs), vm);
});
vm.setProp(vm.global, `__bruno__path__${fn}`, fnHandle);
fnHandle.dispose();
});
vm.evalCode(
`
globalThis.path = {};
${fns?.map((fn, idx) => `globalThis.path.${fn} = __bruno__path__${fn}`).join('\n')}
globalThis.requireObject = {
...(globalThis.requireObject || {}),
path: globalThis.path,
}
`
);
};
module.exports = addPathShimToContext;

View File

@ -0,0 +1,30 @@
const uuid = require('uuid');
const { marshallToVm } = require('../../utils');
const fns = ['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate'];
const addUuidShimToContext = async (vm) => {
fns.forEach((fn) => {
let fnHandle = vm.newFunction(fn, function (...args) {
const nativeArgs = args.map(vm.dump);
return marshallToVm(uuid[fn](...nativeArgs), vm);
});
vm.setProp(vm.global, `__bruno__uuid__${fn}`, fnHandle);
fnHandle.dispose();
});
vm.evalCode(
`
globalThis.uuid = {};
${['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']
?.map((fn, idx) => `globalThis.uuid.${fn} = __bruno__uuid__${fn}`)
.join('\n')}
globalThis.requireObject = {
...globalThis.requireObject,
uuid: globalThis.uuid,
}
`
);
};
module.exports = addUuidShimToContext;

View File

@ -0,0 +1,31 @@
const path = require('path');
const fs = require('fs');
const { marshallToVm } = require('../utils');
const addLocalModuleLoaderShimToContext = (vm, collectionPath) => {
let loadLocalModuleHandle = vm.newFunction('loadLocalModule', function (module) {
const filename = vm.dump(module);
// Check if the filename has an extension
const hasExtension = path.extname(filename) !== '';
const resolvedFilename = hasExtension ? filename : `${filename}.js`;
// Resolve the file path and check if it's within the collectionPath
const filePath = path.resolve(collectionPath, resolvedFilename);
const relativePath = path.relative(collectionPath, filePath);
// Ensure the resolved file path is inside the collectionPath
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
throw new Error('Access to files outside of the collectionPath is not allowed.');
}
let code = fs.readFileSync(filePath).toString();
return marshallToVm(code, vm);
});
vm.setProp(vm.global, '__brunoLoadLocalModule', loadLocalModuleHandle);
loadLocalModuleHandle.dispose();
};
module.exports = addLocalModuleLoaderShimToContext;

View File

@ -0,0 +1,63 @@
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();
vm.evalCode(
`
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);
}
}
globalThis.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 {
globalThis.__bruno__addResult({
description,
status: "fail",
error: error.message || "An unexpected error occurred.",
});
}
}
};
globalThis.test = Test(__brunoTestResults);
`
);
};
module.exports = addBruShimToContext;

View File

@ -0,0 +1,33 @@
const marshallToVm = (value, vm) => {
if (value === undefined) {
return vm.undefined;
}
if (value === null) {
return vm.null;
}
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.setProp(arr, i, marshallToVm(value[i], vm));
}
return arr;
} else {
const obj = vm.newObject();
for (const key in value) {
vm.setProp(obj, key, marshallToVm(value[key], vm));
}
return obj;
}
}
};
module.exports = {
marshallToVm
};

View File

@ -142,10 +142,15 @@ const cleanJson = (data) => {
}
};
const appendAwaitToTestFunc = (str) => {
return str.replace(/(?<!\.\s*)(?<!await\s)(test\()/g, 'await $1');
};
module.exports = {
evaluateJsExpression,
evaluateJsTemplateLiteral,
createResponseParser,
internalExpressionCache,
cleanJson
cleanJson,
appendAwaitToTestFunc
};

View File

@ -35,7 +35,7 @@ describe('runtime', () => {
})
`;
const runtime = new TestRuntime();
const runtime = new TestRuntime({ runtime: 'vm2' });
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
@ -71,7 +71,7 @@ describe('runtime', () => {
})
`;
const runtime = new TestRuntime();
const runtime = new TestRuntime({ runtime: 'vm2' });
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
@ -114,7 +114,7 @@ describe('runtime', () => {
bru.setVar('validation', validate(new Date().toISOString()))
`;
const runtime = new ScriptRuntime();
const runtime = new ScriptRuntime({ runtime: 'vm2' });
const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env);
expect(result.runtimeVariables.validation).toBeTruthy();
});
@ -160,7 +160,7 @@ describe('runtime', () => {
bru.setVar('validation', validate(new Date().toISOString()))
`;
const runtime = new ScriptRuntime();
const runtime = new ScriptRuntime({ runtime: 'vm2' });
const result = await runtime.runResponseScript(
script,
{ ...baseRequest },

View File

@ -1,5 +1,10 @@
const { describe, it, expect } = require('@jest/globals');
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require('../src/utils');
const {
evaluateJsExpression,
internalExpressionCache: cache,
createResponseParser,
appendAwaitToTestFunc
} = require('../src/utils');
describe('utils', () => {
describe('expression evaluation', () => {
@ -137,4 +142,70 @@ describe('utils', () => {
expect(value).toBe(20);
});
});
describe('appendAwaitToTestFunc function', () => {
it('example 1', () => {
const inputTestsString = `
test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`;
const ouutputTestsString = appendAwaitToTestFunc(inputTestsString);
expect(ouutputTestsString).toBe(`
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`);
});
it('example 2', () => {
const inputTestsString = `
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`;
const ouutputTestsString = appendAwaitToTestFunc(inputTestsString);
expect(ouutputTestsString).toBe(`
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
await test("should return json", function() {
const data = res.getBody();
expect(res.getBody()).to.eql({
"hello": "bruno"
});
});
`);
});
});
});