@usebruno/js 0.42.2 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -7
- package/src/bru.js +10 -11
- package/src/bruno-request.js +8 -8
- package/src/bruno-response.js +1 -2
- package/src/runtime/assert-runtime.js +5 -2
- package/src/runtime/script-runtime.js +18 -206
- package/src/runtime/test-runtime.js +12 -106
- package/src/runtime/vars-runtime.js +3 -2
- package/src/sandbox/node-vm/index.js +85 -34
- package/src/sandbox/node-vm/index.spec.js +252 -0
- package/src/sandbox/quickjs/index.js +2 -2
- package/src/sandbox/quickjs/shims/bru.js +3 -3
- package/src/sandbox/quickjs/shims/lib/crypto-utils.js +7 -9
- package/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js +6 -6
- package/src/sandbox/quickjs/shims/lib/utils.js +1 -1
- package/src/utils/results.js +4 -4
- package/src/utils.js +6 -6
|
@@ -1,47 +1,17 @@
|
|
|
1
|
-
const { NodeVM } = require('@usebruno/vm2');
|
|
2
|
-
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
3
1
|
const chai = require('chai');
|
|
4
|
-
const
|
|
5
|
-
const http = require('http');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const stream = require('stream');
|
|
8
|
-
const util = require('util');
|
|
9
|
-
const zlib = require('zlib');
|
|
10
|
-
const url = require('url');
|
|
11
|
-
const punycode = require('punycode');
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const { get } = require('lodash');
|
|
2
|
+
const uuid = require('uuid');
|
|
14
3
|
const Bru = require('../bru');
|
|
15
4
|
const BrunoRequest = require('../bruno-request');
|
|
16
5
|
const BrunoResponse = require('../bruno-response');
|
|
17
|
-
const Test = require('../test');
|
|
18
|
-
const TestResults = require('../test-results');
|
|
19
6
|
const { cleanJson } = require('../utils');
|
|
20
7
|
const { createBruTestResultMethods } = require('../utils/results');
|
|
21
|
-
|
|
22
|
-
// Inbuilt Library Support
|
|
23
|
-
const ajv = require('ajv');
|
|
24
|
-
const addFormats = require('ajv-formats');
|
|
25
|
-
const atob = require('atob');
|
|
26
|
-
const btoa = require('btoa');
|
|
27
|
-
const lodash = require('lodash');
|
|
28
|
-
const moment = require('moment');
|
|
29
|
-
const uuid = require('uuid');
|
|
30
|
-
const nanoid = require('nanoid');
|
|
31
|
-
const axios = require('axios');
|
|
32
|
-
const fetch = require('node-fetch');
|
|
33
|
-
const CryptoJS = require('crypto-js');
|
|
34
|
-
const NodeVault = require('node-vault');
|
|
35
|
-
const xml2js = require('xml2js');
|
|
36
|
-
const cheerio = require('cheerio');
|
|
37
|
-
const tv4 = require('tv4');
|
|
8
|
+
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
38
9
|
const jsonwebtoken = require('jsonwebtoken');
|
|
39
10
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
40
|
-
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
|
|
41
11
|
|
|
42
12
|
class TestRuntime {
|
|
43
13
|
constructor(props) {
|
|
44
|
-
this.runtime = props?.runtime || '
|
|
14
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
45
15
|
}
|
|
46
16
|
|
|
47
17
|
async runTests(
|
|
@@ -64,29 +34,12 @@ class TestRuntime {
|
|
|
64
34
|
const collectionVariables = request?.collectionVariables || {};
|
|
65
35
|
const folderVariables = request?.folderVariables || {};
|
|
66
36
|
const requestVariables = request?.requestVariables || {};
|
|
37
|
+
const promptVariables = request?.promptVariables || {};
|
|
67
38
|
const iterationDetails = request?.runnerIterationDetails || {};
|
|
68
39
|
const assertionResults = request?.assertionResults || [];
|
|
69
|
-
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName);
|
|
40
|
+
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
|
|
70
41
|
const req = new BrunoRequest(request, historyLogger);
|
|
71
42
|
const res = new BrunoResponse(response);
|
|
72
|
-
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
73
|
-
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
|
74
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
75
|
-
const additionalContextRootsAbsolute = lodash
|
|
76
|
-
.chain(additionalContextRoots)
|
|
77
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
78
|
-
.value();
|
|
79
|
-
|
|
80
|
-
const whitelistedModules = {};
|
|
81
|
-
|
|
82
|
-
for (let module of moduleWhitelist) {
|
|
83
|
-
try {
|
|
84
|
-
whitelistedModules[module] = require(module);
|
|
85
|
-
} catch (e) {
|
|
86
|
-
// Ignore
|
|
87
|
-
console.warn(e);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
43
|
|
|
91
44
|
// extend bru with result getter methods
|
|
92
45
|
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
|
@@ -113,10 +66,6 @@ class TestRuntime {
|
|
|
113
66
|
jwt: jsonwebtoken
|
|
114
67
|
};
|
|
115
68
|
|
|
116
|
-
if (this.runtime === 'vm2') {
|
|
117
|
-
mixinTypedArrays(context);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
69
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
121
70
|
const customLogger = (type) => {
|
|
122
71
|
return (...args) => {
|
|
@@ -132,20 +81,14 @@ class TestRuntime {
|
|
|
132
81
|
};
|
|
133
82
|
}
|
|
134
83
|
|
|
135
|
-
if(runRequestByItemPathname) {
|
|
84
|
+
if (runRequestByItemPathname) {
|
|
136
85
|
context.bru.runRequest = runRequestByItemPathname;
|
|
137
86
|
}
|
|
138
87
|
|
|
139
88
|
let scriptError = null;
|
|
140
89
|
|
|
141
90
|
try {
|
|
142
|
-
if (this.runtime === '
|
|
143
|
-
await executeQuickJsVmAsync({
|
|
144
|
-
script: testsFile,
|
|
145
|
-
context: context,
|
|
146
|
-
collectionPath
|
|
147
|
-
});
|
|
148
|
-
} else if (this.runtime === 'nodevm') {
|
|
91
|
+
if (this.runtime === 'nodevm') {
|
|
149
92
|
await runScriptInNodeVm({
|
|
150
93
|
script: testsFile,
|
|
151
94
|
context,
|
|
@@ -153,49 +96,12 @@ class TestRuntime {
|
|
|
153
96
|
scriptingConfig
|
|
154
97
|
});
|
|
155
98
|
} else {
|
|
156
|
-
// default runtime is
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
external: true,
|
|
162
|
-
builtin: ['*'],
|
|
163
|
-
root: [collectionPath, ...additionalContextRootsAbsolute],
|
|
164
|
-
mock: {
|
|
165
|
-
// node libs
|
|
166
|
-
path,
|
|
167
|
-
stream,
|
|
168
|
-
util,
|
|
169
|
-
url,
|
|
170
|
-
http,
|
|
171
|
-
https,
|
|
172
|
-
punycode,
|
|
173
|
-
zlib,
|
|
174
|
-
// 3rd party libs
|
|
175
|
-
ajv,
|
|
176
|
-
'ajv-formats': addFormats,
|
|
177
|
-
btoa,
|
|
178
|
-
atob,
|
|
179
|
-
lodash,
|
|
180
|
-
moment,
|
|
181
|
-
uuid,
|
|
182
|
-
nanoid,
|
|
183
|
-
axios,
|
|
184
|
-
chai,
|
|
185
|
-
'node-fetch': fetch,
|
|
186
|
-
'crypto-js': CryptoJS,
|
|
187
|
-
'xml2js': xml2js,
|
|
188
|
-
cheerio,
|
|
189
|
-
tv4,
|
|
190
|
-
'jsonwebtoken': jsonwebtoken,
|
|
191
|
-
...whitelistedModules,
|
|
192
|
-
fs: allowScriptFilesystemAccess ? fs : undefined,
|
|
193
|
-
'node-vault': NodeVault
|
|
194
|
-
}
|
|
195
|
-
}
|
|
99
|
+
// default runtime is `quickjs`
|
|
100
|
+
await executeQuickJsVmAsync({
|
|
101
|
+
script: testsFile,
|
|
102
|
+
context: context,
|
|
103
|
+
collectionPath
|
|
196
104
|
});
|
|
197
|
-
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
|
198
|
-
await asyncVM();
|
|
199
105
|
}
|
|
200
106
|
} catch (error) {
|
|
201
107
|
scriptError = error;
|
|
@@ -20,7 +20,7 @@ const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
|
|
|
20
20
|
|
|
21
21
|
class VarsRuntime {
|
|
22
22
|
constructor(props) {
|
|
23
|
-
this.runtime = props?.runtime || '
|
|
23
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
24
24
|
this.mode = props?.mode || 'developer';
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -36,7 +36,8 @@ class VarsRuntime {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
const
|
|
39
|
+
const promptVariables = request?.promptVariables || {};
|
|
40
|
+
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVars, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, undefined, promptVariables);
|
|
40
41
|
const req = new BrunoRequest(request, historyLogger);
|
|
41
42
|
const res = createResponseParser(response);
|
|
42
43
|
|
|
@@ -3,7 +3,6 @@ const fs = require('node:fs');
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { get } = require('lodash');
|
|
5
5
|
const lodash = require('lodash');
|
|
6
|
-
const { cleanJson } = require('../../utils');
|
|
7
6
|
const { mixinTypedArrays } = require('../mixins/typed-arrays');
|
|
8
7
|
|
|
9
8
|
class ScriptError extends Error {
|
|
@@ -37,6 +36,17 @@ async function runScriptInNodeVm({
|
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
try {
|
|
39
|
+
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
40
|
+
|
|
41
|
+
// Compute additional context roots
|
|
42
|
+
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
43
|
+
const additionalContextRootsAbsolute = lodash
|
|
44
|
+
.chain(additionalContextRoots)
|
|
45
|
+
.map((acr) => (path.isAbsolute(acr) ? acr : path.join(collectionPath, acr)))
|
|
46
|
+
.map((acr) => path.normalize(acr))
|
|
47
|
+
.value();
|
|
48
|
+
additionalContextRootsAbsolute.push(path.normalize(collectionPath));
|
|
49
|
+
|
|
40
50
|
// Create script context with all necessary variables
|
|
41
51
|
const scriptContext = {
|
|
42
52
|
// Bruno context
|
|
@@ -58,7 +68,12 @@ async function runScriptInNodeVm({
|
|
|
58
68
|
clearTimeout: global.clearTimeout,
|
|
59
69
|
clearInterval: global.clearInterval,
|
|
60
70
|
setImmediate: global.setImmediate,
|
|
61
|
-
clearImmediate: global.clearImmediate
|
|
71
|
+
clearImmediate: global.clearImmediate,
|
|
72
|
+
Error: global.Error,
|
|
73
|
+
TypeError: global.TypeError,
|
|
74
|
+
ReferenceError: global.ReferenceError,
|
|
75
|
+
SyntaxError: global.SyntaxError,
|
|
76
|
+
RangeError: global.RangeError
|
|
62
77
|
};
|
|
63
78
|
|
|
64
79
|
mixinTypedArrays(scriptContext);
|
|
@@ -72,7 +87,9 @@ async function runScriptInNodeVm({
|
|
|
72
87
|
collectionPath,
|
|
73
88
|
scriptContext,
|
|
74
89
|
currentModuleDir: collectionPath,
|
|
75
|
-
localModuleCache
|
|
90
|
+
localModuleCache,
|
|
91
|
+
allowScriptFilesystemAccess,
|
|
92
|
+
additionalContextRootsAbsolute
|
|
76
93
|
});
|
|
77
94
|
|
|
78
95
|
// Execute the script in an isolated VM context
|
|
@@ -91,7 +108,6 @@ async function runScriptInNodeVm({
|
|
|
91
108
|
return;
|
|
92
109
|
}
|
|
93
110
|
|
|
94
|
-
|
|
95
111
|
/**
|
|
96
112
|
* Creates a custom require function with enhanced security and local module support
|
|
97
113
|
* @param {Object} options - Configuration options
|
|
@@ -100,6 +116,8 @@ async function runScriptInNodeVm({
|
|
|
100
116
|
* @param {Object} options.scriptContext - Script execution context
|
|
101
117
|
* @param {string} options.currentModuleDir - Current module directory for relative imports
|
|
102
118
|
* @param {Map} options.localModuleCache - Cache for loaded local modules
|
|
119
|
+
* @param {boolean} options.allowScriptFilesystemAccess - Whether to allow fs module access
|
|
120
|
+
* @param {Array<string>} options.additionalContextRootsAbsolute - Pre-computed absolute context roots
|
|
103
121
|
* @returns {Function} Custom require function
|
|
104
122
|
*/
|
|
105
123
|
function createCustomRequire({
|
|
@@ -107,32 +125,54 @@ function createCustomRequire({
|
|
|
107
125
|
collectionPath,
|
|
108
126
|
scriptContext,
|
|
109
127
|
currentModuleDir = collectionPath,
|
|
110
|
-
localModuleCache = new Map()
|
|
128
|
+
localModuleCache = new Map(),
|
|
129
|
+
allowScriptFilesystemAccess = false,
|
|
130
|
+
additionalContextRootsAbsolute = []
|
|
111
131
|
}) {
|
|
112
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
113
|
-
const additionalContextRootsAbsolute = lodash
|
|
114
|
-
.chain(additionalContextRoots)
|
|
115
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
116
|
-
.value();
|
|
117
|
-
additionalContextRootsAbsolute.push(collectionPath);
|
|
118
|
-
|
|
119
132
|
return (moduleName) => {
|
|
120
|
-
// Check if it's a local module (starts with ./ or ../)
|
|
121
|
-
|
|
122
|
-
|
|
133
|
+
// Check if it's a local module (starts with ./ or ../ or .\ or ..\)
|
|
134
|
+
// Normalize backslashes to forward slashes for cross-platform compatibility
|
|
135
|
+
const normalizedModuleName = moduleName.replace(/\\/g, '/');
|
|
136
|
+
if (normalizedModuleName.startsWith('./') || normalizedModuleName.startsWith('../')) {
|
|
137
|
+
return loadLocalModule({ moduleName: normalizedModuleName, collectionPath, scriptContext, localModuleCache, currentModuleDir, additionalContextRootsAbsolute });
|
|
123
138
|
}
|
|
124
139
|
|
|
140
|
+
// Helper function to check if a module is the fs module or a submodule
|
|
141
|
+
const isFsModule = (module) => {
|
|
142
|
+
if (!module) return false;
|
|
143
|
+
const fsModule = require('fs');
|
|
144
|
+
// Check if it's the fs module itself
|
|
145
|
+
if (module === fsModule) return true;
|
|
146
|
+
// Check if it's fs/promises submodule
|
|
147
|
+
if (module === fsModule.promises) return true;
|
|
148
|
+
// Check if it's fs/promises by comparing with require('fs/promises')
|
|
149
|
+
try {
|
|
150
|
+
if (module === require('fs/promises')) return true;
|
|
151
|
+
} catch {
|
|
152
|
+
// fs/promises might not be available in all Node versions
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
|
|
125
157
|
// First try to require as a native/npm module
|
|
126
158
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
} catch (error) {
|
|
134
|
-
throw new Error(`Could not resolve module "${moduleName}": ${error.message}\n\nThis most likely means you did not install the module under "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map(root => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
|
|
159
|
+
const requiredModulePath = require.resolve(moduleName, { paths: [...additionalContextRootsAbsolute, ...module.paths] });
|
|
160
|
+
const requiredModule = require(requiredModulePath);
|
|
161
|
+
|
|
162
|
+
// Block filesystem module access if filesystem access is not allowed
|
|
163
|
+
if (!allowScriptFilesystemAccess && isFsModule(requiredModule)) {
|
|
164
|
+
throw new Error('Filesystem access is not allowed. Enable "filesystemAccess.allow" in scripting config to use the fs module.');
|
|
135
165
|
}
|
|
166
|
+
|
|
167
|
+
return requiredModule;
|
|
168
|
+
} catch (requireError) {
|
|
169
|
+
// Re-throw if it's our filesystem access error
|
|
170
|
+
if (requireError.message && requireError.message.includes('Enable "filesystemAccess.allow"')) {
|
|
171
|
+
throw requireError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If that fails, try to resolve from additionalContextRoots
|
|
175
|
+
throw new Error(`Could not resolve module "${moduleName}": ${requireError.message}\n\nThis most likely means you did not install the module under the collection or the "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
|
|
136
176
|
}
|
|
137
177
|
};
|
|
138
178
|
}
|
|
@@ -145,6 +185,7 @@ function createCustomRequire({
|
|
|
145
185
|
* @param {Object} options.scriptContext - Script execution context to inherit
|
|
146
186
|
* @param {Map} options.localModuleCache - Cache for loaded modules
|
|
147
187
|
* @param {string} options.currentModuleDir - Directory of the current module for relative resolution
|
|
188
|
+
* @param {Array<string>} options.additionalContextRootsAbsolute - Additional allowed context root paths
|
|
148
189
|
* @returns {*} The exported content of the loaded module
|
|
149
190
|
* @throws {Error} When module is outside collection path or cannot be loaded
|
|
150
191
|
*/
|
|
@@ -153,7 +194,8 @@ function loadLocalModule({
|
|
|
153
194
|
collectionPath,
|
|
154
195
|
scriptContext,
|
|
155
196
|
localModuleCache,
|
|
156
|
-
currentModuleDir
|
|
197
|
+
currentModuleDir,
|
|
198
|
+
additionalContextRootsAbsolute = []
|
|
157
199
|
}) {
|
|
158
200
|
// Check if the filename has an extension
|
|
159
201
|
const hasExtension = path.extname(moduleName) !== '';
|
|
@@ -162,12 +204,19 @@ function loadLocalModule({
|
|
|
162
204
|
// Resolve the file path relative to the current module's directory
|
|
163
205
|
const filePath = path.resolve(currentModuleDir, resolvedFilename);
|
|
164
206
|
const normalizedFilePath = path.normalize(filePath);
|
|
165
|
-
const normalizedCollectionPath = path.normalize(collectionPath);
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
208
|
+
const isWithinAllowedRoot = additionalContextRootsAbsolute.some((allowedRoot) => {
|
|
209
|
+
const normalizedAllowedRoot = path.normalize(allowedRoot);
|
|
210
|
+
const relativePath = path.relative(normalizedAllowedRoot, normalizedFilePath);
|
|
211
|
+
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!isWithinAllowedRoot) {
|
|
215
|
+
const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n');
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Access to files outside of the allowed context roots is not allowed: ${moduleName}\n\n`
|
|
218
|
+
+ `Allowed context roots:\n${allowedRootsDisplay}`
|
|
219
|
+
);
|
|
171
220
|
}
|
|
172
221
|
|
|
173
222
|
// Check cache first (use normalized path as key)
|
|
@@ -197,11 +246,13 @@ function loadLocalModule({
|
|
|
197
246
|
__dirname: moduleDir,
|
|
198
247
|
// Create a custom require function for this module that resolves relative to its directory
|
|
199
248
|
require: createCustomRequire({
|
|
200
|
-
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
201
|
-
collectionPath,
|
|
202
|
-
scriptContext,
|
|
203
|
-
currentModuleDir: moduleDir,
|
|
204
|
-
localModuleCache
|
|
249
|
+
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
250
|
+
collectionPath,
|
|
251
|
+
scriptContext,
|
|
252
|
+
currentModuleDir: moduleDir,
|
|
253
|
+
localModuleCache,
|
|
254
|
+
allowScriptFilesystemAccess: get(scriptContext.scriptingConfig, 'filesystemAccess.allow', false),
|
|
255
|
+
additionalContextRootsAbsolute
|
|
205
256
|
})
|
|
206
257
|
};
|
|
207
258
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { runScriptInNodeVm } = require('./index');
|
|
6
|
+
|
|
7
|
+
describe('node-vm sandbox', () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
let collectionPath;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create a temporary test directory
|
|
13
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));
|
|
14
|
+
collectionPath = path.join(testDir, 'collection');
|
|
15
|
+
fs.mkdirSync(collectionPath);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
// Clean up test directory
|
|
20
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('createCustomRequire - local modules', () => {
|
|
24
|
+
it('should load local module with ./ path', async () => {
|
|
25
|
+
// Create a local module
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(collectionPath, 'helper.js'),
|
|
28
|
+
'module.exports = { value: 42 };'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const script = `
|
|
32
|
+
const helper = require('./helper');
|
|
33
|
+
bru.setVar('result', helper.value);
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const context = {
|
|
37
|
+
bru: { setVar: jest.fn() },
|
|
38
|
+
console: console
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
42
|
+
|
|
43
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should load local module with ../ path', async () => {
|
|
47
|
+
// Create a subdirectory and modules
|
|
48
|
+
const subDir = path.join(collectionPath, 'subdir');
|
|
49
|
+
fs.mkdirSync(subDir);
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
path.join(collectionPath, 'parent.js'),
|
|
52
|
+
'module.exports = { name: "parent" };'
|
|
53
|
+
);
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(subDir, 'child.js'),
|
|
56
|
+
'const parent = require("../parent"); module.exports = parent;'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const script = `
|
|
60
|
+
const child = require('./subdir/child');
|
|
61
|
+
bru.setVar('result', child.name);
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
const context = {
|
|
65
|
+
bru: { setVar: jest.fn() },
|
|
66
|
+
console: console
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
70
|
+
|
|
71
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'parent');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle backslashes on Windows', async () => {
|
|
75
|
+
const subDir = path.join(collectionPath, 'utils');
|
|
76
|
+
fs.mkdirSync(subDir);
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
path.join(subDir, 'module.js'),
|
|
79
|
+
'module.exports = { platform: "cross-platform" };'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Simulate Windows-style path with backslashes
|
|
83
|
+
const script = `
|
|
84
|
+
const mod = require('.\\\\utils\\\\module');
|
|
85
|
+
bru.setVar('result', mod.platform);
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const context = {
|
|
89
|
+
bru: { setVar: jest.fn() },
|
|
90
|
+
console: console
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
94
|
+
|
|
95
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'cross-platform');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should block access outside collection path', async () => {
|
|
99
|
+
const script = `
|
|
100
|
+
const outside = require('../../outside');
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const context = { console: console };
|
|
104
|
+
|
|
105
|
+
await expect(
|
|
106
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
|
|
107
|
+
).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('createCustomRequire - additionalContextRoots', () => {
|
|
112
|
+
it('should allow module access from additionalContextRoots', async () => {
|
|
113
|
+
// Create an additional context root at same level as collection
|
|
114
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
115
|
+
fs.mkdirSync(additionalRoot);
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(additionalRoot, 'shared.js'),
|
|
118
|
+
'module.exports = { shared: true };'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// From collection, traverse up to testDir, then into shared directory
|
|
122
|
+
const script = `
|
|
123
|
+
const shared = require('../shared/shared');
|
|
124
|
+
bru.setVar('result', shared.shared);
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const context = {
|
|
128
|
+
bru: { setVar: jest.fn() },
|
|
129
|
+
console: console
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const scriptingConfig = {
|
|
133
|
+
additionalContextRoots: [additionalRoot]
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
137
|
+
|
|
138
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle relative additionalContextRoots path', async () => {
|
|
142
|
+
// Create a sibling directory to collection
|
|
143
|
+
const libsDir = path.join(testDir, 'libs');
|
|
144
|
+
fs.mkdirSync(libsDir);
|
|
145
|
+
fs.writeFileSync(
|
|
146
|
+
path.join(libsDir, 'lib.js'),
|
|
147
|
+
'module.exports = { fromLib: "yes" };'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const script = `
|
|
151
|
+
const lib = require('../libs/lib');
|
|
152
|
+
bru.setVar('result', lib.fromLib);
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const context = {
|
|
156
|
+
bru: { setVar: jest.fn() },
|
|
157
|
+
console: console
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const scriptingConfig = {
|
|
161
|
+
additionalContextRoots: ['../libs']
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
165
|
+
|
|
166
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'yes');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle nested additional context roots modules', async () => {
|
|
170
|
+
// Create an additional context root
|
|
171
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
172
|
+
fs.mkdirSync(additionalRoot);
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(additionalRoot, 'allowed.js'),
|
|
175
|
+
'module.exports = { allowed: true };'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Create a nested module that tries to require from additional root
|
|
179
|
+
fs.writeFileSync(
|
|
180
|
+
path.join(collectionPath, 'parent.js'),
|
|
181
|
+
`
|
|
182
|
+
const allowed = require('../shared/allowed');
|
|
183
|
+
module.exports = { nestedAccess: allowed.allowed };
|
|
184
|
+
`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const script = `
|
|
188
|
+
const parent = require('./parent');
|
|
189
|
+
bru.setVar('result', parent.nestedAccess);
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const context = {
|
|
193
|
+
bru: { setVar: jest.fn() },
|
|
194
|
+
console: console
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const scriptingConfig = {
|
|
198
|
+
additionalContextRoots: [additionalRoot]
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
202
|
+
|
|
203
|
+
// Nested module should successfully access the additional root
|
|
204
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('createCustomRequire - npm modules', () => {
|
|
209
|
+
it('should load npm module', async () => {
|
|
210
|
+
const script = `
|
|
211
|
+
const lodash = require('lodash');
|
|
212
|
+
bru.setVar('result', typeof lodash.get);
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
const context = {
|
|
216
|
+
bru: { setVar: jest.fn() },
|
|
217
|
+
console: console
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
221
|
+
|
|
222
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('createCustomRequire - module caching', () => {
|
|
227
|
+
it('should cache loaded modules', async () => {
|
|
228
|
+
let callCount = 0;
|
|
229
|
+
fs.writeFileSync(
|
|
230
|
+
path.join(collectionPath, 'cached.js'),
|
|
231
|
+
`
|
|
232
|
+
module.exports = { count: ${++callCount} };
|
|
233
|
+
`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const script = `
|
|
237
|
+
const mod1 = require('./cached');
|
|
238
|
+
const mod2 = require('./cached');
|
|
239
|
+
bru.setVar('same', mod1.count === mod2.count);
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
const context = {
|
|
243
|
+
bru: { setVar: jest.fn() },
|
|
244
|
+
console: console
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
248
|
+
|
|
249
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('same', true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -24,7 +24,7 @@ const toNumber = (value) => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const removeQuotes = (str) => {
|
|
27
|
-
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith(
|
|
27
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith('\'') && str.endsWith('\''))) {
|
|
28
28
|
return str.slice(1, -1);
|
|
29
29
|
}
|
|
30
30
|
return str;
|
|
@@ -36,7 +36,7 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
36
36
|
}
|
|
37
37
|
externalScript = externalScript?.trim();
|
|
38
38
|
|
|
39
|
-
if(scriptType === 'template-literal') {
|
|
39
|
+
if (scriptType === 'template-literal') {
|
|
40
40
|
if (!isNaN(Number(externalScript))) {
|
|
41
41
|
const number = Number(externalScript);
|
|
42
42
|
|
|
@@ -341,7 +341,7 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
341
341
|
const promise = vm.newPromise();
|
|
342
342
|
const dumpedUrl = vm.dump(url);
|
|
343
343
|
const dumpedNameOrObj = vm.dump(nameOrCookieObj);
|
|
344
|
-
|
|
344
|
+
|
|
345
345
|
// Check if the second argument is an object (cookie object case)
|
|
346
346
|
if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) {
|
|
347
347
|
// Cookie object case: setCookie(url, cookieObject, callback)
|
|
@@ -363,7 +363,7 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
363
363
|
}
|
|
364
364
|
});
|
|
365
365
|
}
|
|
366
|
-
|
|
366
|
+
|
|
367
367
|
promise.settled.then(vm.runtime.executePendingJobs);
|
|
368
368
|
return promise.handle;
|
|
369
369
|
});
|
|
@@ -371,7 +371,7 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
371
371
|
|
|
372
372
|
const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => {
|
|
373
373
|
const promise = vm.newPromise();
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => {
|
|
376
376
|
if (err) {
|
|
377
377
|
promise.reject(marshallToVm(cleanJson(err), vm));
|