postman-runtime
Version:
Underlying library of executing Postman Collections
327 lines (278 loc) • 12.6 kB
JavaScript
var { Url, UrlMatchPatternList, VariableList } = require('postman-collection'),
sdk = require('postman-collection'),
_ = require('lodash'),
/**
* @const
* @type {string}
*/
FUNCTION = 'function',
/**
* @const
* @type {string}
*/
STRING = 'string',
createReadStream; // function
/**
* Create readable stream for given file as well as detect possible file
* read issues.
*
* @param {Object} resolver - External file resolver module
* @param {String} fileSrc - File path
* @param {Function} callback - Final callback
*
* @note This function is defined in the file's root because there is a need to
* trap it within closure in order to append the stream clone functionalities.
* This ensures lesser footprint in case we have a memory leak.
*/
createReadStream = function (resolver, fileSrc, callback) {
var readStream;
// check for the existence of the file before creating read stream.
// eslint-disable-next-line security/detect-non-literal-fs-filename
resolver.stat(fileSrc, function (err, stats) {
if (err) {
// overwrite `ENOENT: no such file or directory` error message. Most likely the case.
err.code === 'ENOENT' && (err.message = `"${fileSrc}", no such file`);
return callback(err);
}
// check for a valid file.
if (stats && typeof stats.isFile === FUNCTION && !stats.isFile()) {
return callback(new Error(`"${fileSrc}", is not a file`));
}
// check read permissions for user.
// octal `400` signifies 'user permissions'. [4 0 0] -> [u g o]
// `4` signifies 'read permission'. [4] -> [1 0 0] -> [r w x]
if (stats && !(stats.mode & 0o400)) {
return callback(new Error(`"${fileSrc}", read permission denied`));
}
// @note Handle all the errors before `createReadStream` to avoid listening on stream error event.
// listening on error requires listening on end event as well. which will make this sync.
// @note In form-data mode stream error will be handled in postman-request but bails out ongoing request.
// eslint-disable-next-line security/detect-non-literal-fs-filename
readStream = resolver.createReadStream(fileSrc);
// We might have to read the file before making the actual request
// e.g, while calculating body hash during AWS auth or redirecting form-data params
// So, this method wraps the `createReadStream` function with fixed arguments.
// This makes sure that we don't have to pass `fileResolver` to
// internal modules (like auth plugins) for security reasons.
readStream.cloneReadStream = function (callback) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
return createReadStream(resolver, fileSrc, callback);
};
callback(null, readStream);
});
};
/**
* Utility functions that are required to be re-used throughout the runner
*
* @module Runner~util
* @private
*
* @note Do not put module logic or business logic related functions here.
* The functions here are purely decoupled and low-level functions.
*/
module.exports = {
/**
* This function allows one to call another function by wrapping it within a try-catch block.
* The first parameter is the function itself, followed by the scope in which this function is to be executed.
* The third parameter onwards are blindly forwarded to the function being called
*
* @param {Function} fn -
* @param {*} ctx -
*
* @returns {Error} If there was an error executing the function, the error is returned.
* Note that if the function called here is asynchronous, it's errors will not be returned (for obvious reasons!)
*/
safeCall (fn, ctx) {
// extract the arguments that are to be forwarded to the function to be called
var args = Array.prototype.slice.call(arguments, 2);
try {
(typeof fn === FUNCTION) && fn.apply(ctx || global, args);
}
catch (err) {
return err;
}
},
/**
* Copies attributes from source object to destination object.
*
* @param {Object} dest -
* @param {Object} src -
*
* @return {Object}
*/
syncObject (dest, src) {
var prop;
// update or add values from src
for (prop in src) {
if (Object.hasOwn(src, prop)) {
dest[prop] = src[prop];
}
}
// remove values that no longer exist
for (prop in dest) {
if (Object.hasOwn(dest, prop) && !Object.hasOwn(src, prop)) {
delete dest[prop];
}
}
return dest;
},
/**
* Create readable stream for given file as well as detect possible file
* read issues. The resolver also attaches a clone function to the stream
* so that the stream can be restarted any time.
*
* @param {Object} resolver - External file resolver module
* @param {Function} resolver.stat - Resolver method to check for existence and permissions of file
* @param {Function} resolver.createReadStream - Resolver method for creating read stream
* @param {String} fileSrc - File path
* @param {Function} callback -
*
*/
createReadStream (resolver, fileSrc, callback) {
// bail out if resolver not found.
if (!resolver) {
return callback(new Error('file resolver not supported'));
}
// bail out if resolver is not supported.
if (typeof resolver.stat !== FUNCTION || typeof resolver.createReadStream !== FUNCTION) {
return callback(new Error('file resolver interface mismatch'));
}
// bail out if file source is invalid or empty string.
if (!fileSrc || typeof fileSrc !== STRING) {
return callback(new Error('invalid or missing file source'));
}
// now that things are sanitized and validated, we transfer it to the
// stream reading utility function that does the heavy lifting of
// calling there resolver to return the stream
return createReadStream(resolver, fileSrc, callback);
},
/**
* Mutates the given variable scope to be a vault variable scope by
* converting the domains to UrlMatchPattern and adding a helper function
* to get the matching variables for a given URL string.
*
* @note vault variables have a meta property called `domains` which is an
* array of URL match pattern strings.
*
* @private
* @param {PostmanVariableScope} scope - Postman variable scope instance
*/
prepareVaultVariableScope (scope) {
// bail out if the given scope is already a vault variable scope
if (scope.__vaultVariableScope) {
return scope;
}
// traverse all the variables and convert the domains to UrlMatchPattern
scope.values.each((variable) => {
const domains = variable && variable._ && variable._.domains;
if (!(Array.isArray(domains) && domains.length)) {
return;
}
// mark the scope as having domain patterns
scope.__hasDomainPatterns = true;
// convert the domains to UrlMatchPattern
variable._.domainPatterns = new UrlMatchPatternList(null, domains.map((domain) => {
const url = new Url(domain);
// @note URL path is ignored
return `${url.protocol || 'https'}://${url.getRemote()}:*/*`;
}));
});
/**
* Returns a list of variables that match the given URL string against
* the domain patterns.
*
* @private
* @param {String} urlString - URL string to match against
* @returns {PostmanVariableList}
*/
scope.__getMatchingVariables = function (urlString) {
// return all the variables if there are no domain patterns
if (!scope.__hasDomainPatterns) {
return scope.values;
}
const variables = scope.values.filter((variable) => {
const domainPatterns = variable && variable._ && variable._.domainPatterns;
if (!domainPatterns) {
return true;
}
return domainPatterns.test(urlString);
});
return new VariableList(null, variables.map((variable) => {
return variable.toJSON(); // clone the variable
}));
};
// mark the scope as a vault variable scope
scope.__vaultVariableScope = true;
},
/**
* Resolve variables in item and auth in context.
*
* @param {ItemContext} context -
* @param {Item} [context.item] -
* @param {RequestAuth} [context.auth] -
* @param {Object} payload -
* @param {VariableScope} payload._variables -
* @param {Object} payload.data -
* @param {VariableScope} payload.environment -
* @param {VariableScope} payload.collectionVariables -
* @param {VariableScope} payload.globals -
* @param {VariableScope} payload.vaultSecrets -
*/
resolveVariables (context, payload) {
if (!(context.item && context.item.request)) { return; }
// @todo - resolve variables in a more graceful way
var variableDefinitions = [
// extract the variable list from variable scopes
// @note: this is the order of precedence for variable resolution - don't change it
_.get(payload, '_variables.values', []),
_.get(payload, 'data', []),
_.get(payload, 'environment.values', []),
_.get(payload, 'collectionVariables.values', []),
_.get(payload, 'globals.values', [])
// @note vault variables are added later
],
vaultValues = _.get(payload, 'vaultSecrets.values'),
hasVaultSecrets = vaultValues ? vaultValues.count() > 0 : false,
itemParent = context.item.parent(),
urlObj = context.item.request.url,
// @note URL string is used to resolve nested variables as URL parser doesn't support them well.
urlString = urlObj.toString(),
unresolvedUrlString = urlString,
vaultVariables,
vaultUrl,
item,
auth;
if (hasVaultSecrets) {
// get the vault variables that match the unresolved URL string
vaultUrl = urlObj.protocol ? urlString : `http://${urlString}`; // force protocol
vaultVariables = payload.vaultSecrets.__getMatchingVariables(vaultUrl);
// resolve variables in URL string with initial vault variables
urlString = sdk.Property.replaceSubstitutions(urlString, [...variableDefinitions, vaultVariables]);
if (urlString !== unresolvedUrlString) {
// get the final list of vault variables that match the resolved URL string
vaultUrl = new sdk.Url(urlString).toString(true);
vaultVariables = payload.vaultSecrets.__getMatchingVariables(vaultUrl);
// resolve vault variables in URL string
// @note other variable scopes are skipped as they are already resolved
urlString = sdk.Property.replaceSubstitutions(urlString, [vaultVariables]);
}
// add vault variables to the list of variable definitions
variableDefinitions.push(vaultVariables);
}
else if (urlString) {
urlString = sdk.Property.replaceSubstitutions(urlString, variableDefinitions);
}
// @todo - no need to sync variables when SDK starts supporting resolution from scope directly
// @todo - avoid resolving the entire item as this unnecessarily resolves URL
item = context.item = new sdk.Item(context.item.toObjectResolved(null,
variableDefinitions, { ignoreOwnVariables: true }));
// restore the parent reference
item.setParent(itemParent);
// re-parse and update the URL from the resolved string
urlString && (item.request.url = new sdk.Url(urlString));
auth = context.auth;
// resolve variables in auth
auth && (context.auth = new sdk.RequestAuth(auth.toObjectResolved(null,
variableDefinitions, { ignoreOwnVariables: true })));
}
};