Skip to content

Commit

Permalink
Added support for top-level await (#1000)
Browse files Browse the repository at this point in the history
  • Loading branch information
appurva21 committed Jun 12, 2024
1 parent f90fd9f commit 0f22aa5
Show file tree
Hide file tree
Showing 14 changed files with 363 additions and 158 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
unreleased:
new features:
- GH-999 Improved isolation for executing scripts
breaking changes:
- GH-1004 Dropped support for Node < v16
new features:
- GH-999 Improved isolation for executing scripts
- GH-1000 Added support for top-level await in scripts
chores:
- GH-999 Bumped `uvm` dependency
- GH-1004 Updated ESLint rules
Expand Down
229 changes: 133 additions & 96 deletions lib/sandbox/cookie-jar.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,34 @@ const CookieJar = require('@postman/tough-cookie').CookieJar,
callback(null, result);
},

dualModeFunction = function (fn, callback, isCallbackRequired) {
const handler = () => {
return new Promise((resolve, reject) => {
fn(function (err, result) {
if (err) {
return reject(err instanceof Error ? err : new Error(err));
}

resolve(result);
});
});
};

if (callback === undefined) {
return handler();
}

if (typeof callback !== FUNCTION && isCallbackRequired) {
throw new TypeError('callback needs to be a function');
}

handler().then((result) => {
callbackHandler(callback, null, result);
}).catch((err) => {
callbackHandler(callback, err);
});
},

/**
* Helper function to fetch a cookie with given name.
*
Expand Down Expand Up @@ -188,32 +216,31 @@ class PostmanCookieJar {
*
* @param {String} url - URL string
* @param {String} name - Cookie name
* @param {Function} callback - Callback function
* @param {Function} [callback] - Callback function
* @returns {Promise|undefined} - Returns a promise if callback is not provided
*/
get (url, name, callback) {
url = sanitizeURL(url);
return dualModeFunction((cb) => {
url = sanitizeURL(url);

if (!url) {
throw new TypeError('CookieJar.get() requires a valid url');
}

if (typeof callback !== FUNCTION) {
throw new TypeError('CookieJar.get() requires a callback function');
}

if (typeof name !== STRING) {
throw new TypeError('CookieJar.get() requires cookie name to be a string');
}
if (!url) {
throw new TypeError('CookieJar.get() requires a valid url');
}

getCookie(this.jar, url.toString(true), name, function (err, cookie) {
if (err || !cookie) {
return callbackHandler(callback, err, null);
if (typeof name !== STRING) {
throw new TypeError('CookieJar.get() requires cookie name to be a string');
}

cookie = deserialize(cookie);
getCookie(this.jar, url.toString(true), name, function (err, cookie) {
if (err || !cookie) {
return cb(err, null);
}

return callbackHandler(callback, null, cookie.valueOf());
});
cookie = deserialize(cookie);

return cb(null, cookie.valueOf());
});
}, callback, true);
}

/**
Expand All @@ -223,42 +250,45 @@ class PostmanCookieJar {
* @param {Object} [options] - Options object
* @param {Boolean} [options.http] - Include only HttpOnly cookies
* @param {Boolean} [options.secure] - Include Secure cookies
* @param {Function} callback - Callback function
* @param {Function} [callback] - Callback function
* @returns {Promise|undefined} - Returns a promise if callback is not provided
*/
getAll (url, options, callback) {
url = sanitizeURL(url);

if (!url) {
throw new TypeError('CookieJar.getAll() requires a valid url');
}

if (typeof options === FUNCTION && !callback) {
callback = options;
options = {};
}

if (typeof callback !== FUNCTION) {
throw new TypeError('CookieJar.getAll() requires a callback function');
if (options === undefined && !callback) {
options = {};
}

if (typeof options !== OBJECT) {
throw new TypeError('CookieJar.getAll() requires options to be an object');
}
return dualModeFunction((cb) => {
url = sanitizeURL(url);

options = {
// return HttpOnly cookies by default
http: Object.hasOwn(options, 'http') ? Boolean(options.http) : true,
// if undefined, auto-detect from url
secure: Object.hasOwn(options, 'secure') ? Boolean(options.secure) : undefined
};
if (!url) {
throw new TypeError('CookieJar.getAll() requires a valid url');
}

this.jar.getCookies(url.toString(true), options, function (err, cookies) {
if (err) {
return callbackHandler(callback, err);
if (typeof options !== OBJECT) {
throw new TypeError('CookieJar.getAll() requires options to be an object');
}

callbackHandler(callback, null, new PostmanCookieList(null, cookies && cookies.map(deserialize)));
});
options = {
// return HttpOnly cookies by default
http: Object.hasOwn(options, 'http') ? Boolean(options.http) : true,
// if undefined, auto-detect from url
secure: Object.hasOwn(options, 'secure') ? Boolean(options.secure) : undefined
};

this.jar.getCookies(url.toString(true), options, function (err, cookies) {
if (err) {
return cb(err);
}

cb(null, new PostmanCookieList(null, cookies && cookies.map(deserialize)));
});
}, callback, true);
}

/**
Expand All @@ -268,42 +298,45 @@ class PostmanCookieJar {
* @param {String|Object} name - Cookie name
* @param {String|Function} [value] - Cookie value
* @param {Function} [callback] - Callback function
* @returns {Promise|undefined} - Returns a promise if callback is not provided
*/
set (url, name, value, callback) {
url = sanitizeURL(url);

if (!url) {
throw new TypeError('CookieJar.set() requires a valid url');
}

if (typeof value === FUNCTION && !callback) {
callback = value;
value = null;
}

var cookie;
return dualModeFunction((cb) => {
url = sanitizeURL(url);

// @todo avoid else-if to reduce cyclomatic complexity
if (name && value) {
cookie = serialize({ name, value });
}
else if (typeof name === OBJECT) {
cookie = serialize(name);
}
else if (typeof name === STRING) {
cookie = name;
}
else {
throw new TypeError('CookieJar.set() requires a valid set cookie arguments');
}
if (!url) {
throw new TypeError('CookieJar.set() requires a valid url');
}

this.jar.setCookie(cookie, url.toString(true), function (err, cookie) {
if (err) {
return callbackHandler(callback, err);
var cookie;

// @todo avoid else-if to reduce cyclomatic complexity
if (name && value) {
cookie = serialize({ name, value });
}
else if (typeof name === OBJECT) {
cookie = serialize(name);
}
else if (typeof name === STRING) {
cookie = name;
}
else {
throw new TypeError('CookieJar.set() requires a valid set cookie arguments');
}

callbackHandler(callback, null, deserialize(cookie));
});
this.jar.setCookie(cookie, url.toString(true), function (err, cookie) {
if (err) {
return cb(err);
}

cb(null, deserialize(cookie));
});
}, callback);
}

/**
Expand All @@ -312,57 +345,61 @@ class PostmanCookieJar {
* @param {String} url - URL string
* @param {String} name - Cookie name
* @param {Function} [callback] - Callback function
* @returns {Promise|undefined} - Returns a promise if callback is not provided
*/
unset (url, name, callback) {
url = sanitizeURL(url);
return dualModeFunction((cb) => {
url = sanitizeURL(url);

if (!url) {
throw new TypeError('CookieJar.unset() requires a valid url');
}
if (!url) {
throw new TypeError('CookieJar.unset() requires a valid url');
}

if (typeof name !== STRING) {
throw new TypeError('CookieJar.unset() requires cookie name to be a string');
}
if (typeof name !== STRING) {
throw new TypeError('CookieJar.unset() requires cookie name to be a string');
}

var store = this.store;
var store = this.store;

getCookie(this.jar, url.toString(true), name, function (err, cookie) {
if (err || !cookie) {
return callbackHandler(callback, err);
}
getCookie(this.jar, url.toString(true), name, function (err, cookie) {
if (err || !cookie) {
return cb(err);
}

store.removeCookie(cookie.domain, cookie.path, cookie.key, function (err) {
callbackHandler(callback, err);
store.removeCookie(cookie.domain, cookie.path, cookie.key, function (err) {
cb(err);
});
});
});
}, callback);
}

/**
* Remove all the cookies for the given URL.
*
* @param {String} url - URL string
* @param {Function} [callback] - Callback function
* @returns {Promise|undefined} - Returns a promise if callback is not provided
*/
clear (url, callback) {
url = sanitizeURL(url);
return dualModeFunction((cb) => {
url = sanitizeURL(url);

if (!url) {
throw new TypeError('CookieJar.clear() requires a valid url');
}
if (!url) {
throw new TypeError('CookieJar.clear() requires a valid url');
}

var store = this.store;
var store = this.store;

this.jar.getCookies(url.toString(true), function (err, cookies) {
if (err || !cookies) {
return callbackHandler(callback, err);
}
this.jar.getCookies(url.toString(true), function (err, cookies) {
if (err || !cookies) {
return cb(err);
}

forEachWithCallback(cookies, function (cookie, next) {
store.removeCookie(cookie.domain, cookie.path, cookie.key, next);
}, function (err) {
callbackHandler(callback, err);
forEachWithCallback(cookies, function (cookie, next) {
store.removeCookie(cookie.domain, cookie.path, cookie.key, next);
}, cb);
});
});
}, callback);
}
}

Expand Down
12 changes: 4 additions & 8 deletions lib/sandbox/execute-context.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
const { isNonLegacySandbox } = require('./non-legacy-codemarkers');
const _ = require('lodash'),
legacy = require('./postman-legacy-interface'),

NONLEGACY_SANDBOX_MARKERS = {
'"use sandbox2";': true,
'\'use sandbox2\';': true
};
legacy = require('./postman-legacy-interface');

module.exports = function (scope, code, execution, console, timers, pmapi, onAssertion, options) {
// if there is no code, then no point bubbling anything up
Expand All @@ -15,7 +11,7 @@ module.exports = function (scope, code, execution, console, timers, pmapi, onAss
// start by resetting the scope
scope.reset();

if (NONLEGACY_SANDBOX_MARKERS[code.substr(0, 15)] || options.disableLegacyAPIs) {
if (isNonLegacySandbox(code) || options.disableLegacyAPIs) {
// ensure any previously added global variables from legacy are torn down. side-effect is that if user
// explicitly created global variables with same name as legacy ones, they will be torn down too!
// for that reason, the setup function tags the scope and avoids tearing down an scope that was never setup
Expand Down Expand Up @@ -49,7 +45,7 @@ module.exports = function (scope, code, execution, console, timers, pmapi, onAss
clearImmediate: timers.clearImmediate
});

scope.exec(code, function (err) {
scope.exec(code, { async: true }, function (err) {
// we check if the execution went async by determining the timer queue length at this time
execution.return.async = (timers.queueLength() > 0);

Expand Down
17 changes: 12 additions & 5 deletions lib/sandbox/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const _ = require('lodash'),
PostmanAPI = require('./pmapi'),
PostmanCookieStore = require('./cookie-store'),
createPostmanRequire = require('./pm-require'),
{ isNonLegacySandbox, getNonLegacyCodeMarker } = require('./non-legacy-codemarkers'),

EXECUTION_RESULT_EVENT_BASE = 'execution.result.',
EXECUTION_REQUEST_EVENT_BASE = 'execution.request.',
Expand Down Expand Up @@ -115,9 +116,16 @@ module.exports = function (bridge, glob) {
assertionEventName = EXECUTION_ASSERTION_EVENT_BASE + id,
skipRequestEventName = EXECUTION_SKIP_REQUEST_EVENT_BASE + id,

// extract the code from event. The event can be the code itself and we know that if the event is of type
// string.
code = _.isFunction(event.script && event.script.toSource) && event.script.toSource(),
// extract the code from event
code = _.isFunction(event.script && event.script.toSource) && ((code) => {
// wrap it in an async function to support top-level await
const asyncCode = `;(async()=>{;
${code}
;})().then(__exitscope).catch(__exitscope);`;

return isNonLegacySandbox(code) ? `${getNonLegacyCodeMarker()}${asyncCode}` : asyncCode;
})(event.script.toSource()),

// create the execution object
execution = new Execution(id, event, context, { ...options, initializeExecution }),

Expand Down Expand Up @@ -161,8 +169,7 @@ module.exports = function (bridge, glob) {
// create the controlled timers
timers = new PostmanTimers(null, function (err) {
if (err) { // propagate the error out of sandbox
bridge.dispatch(errorEventName, options.cursor, err);
bridge.dispatch(EXECUTION_ERROR_EVENT, options.cursor, err);
onError(err);
}
}, function () {
execution.return.async = true;
Expand Down
Loading

0 comments on commit 0f22aa5

Please sign in to comment.