/* istanbul ignore file */ // @todo // 1. Return with annotations like (overridden headers, auth headers etc.) // 2. Utilize requester (core.js) methods for dryRun // 3. Add tests const _ = require('lodash'), async = require('async'), mime = require('mime-types'), urlEncoder = require('postman-url-encoder'), Request = require('postman-collection').Request, authorizeRequest = require('../authorizer').authorizeRequest, authHandlers = require('../authorizer').AuthLoader.handlers, version = require('../../package.json').version, CALCULATED_AT_RUNTIME = '', COOKIE = 'Cookie', FUNCTION = 'function', CONTENT_TYPE = 'Content-Type', DEFAULT_MIME_TYPE = 'application/octet-stream', CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded', CONTENT_TYPE_FORMDATA = 'multipart/form-data; boundary=' + CALCULATED_AT_RUNTIME, CONTENT_TYPE_LANGUAGE = { 'html': 'text/html', 'text': 'text/plain', 'json': 'application/json', 'javascript': 'application/javascript', 'xml': 'application/xml' }, BODY_MODE = { raw: 'raw', file: 'file', graphql: 'graphql', formdata: 'formdata', urlencoded: 'urlencoded' }; /** * Check if request body is empty and also handles disabled params for urlencoded * and formdata bodies. * * @todo Update Collection SDK isEmpty method to account for this. * * @private * @param {RequestBody} body * @returns {Boolean} */ function bodyIsEmpty (body) { if (!body || body.disabled || body.isEmpty()) { return true; } var i, param, mode = body.mode; if (!(mode === BODY_MODE.formdata || mode === BODY_MODE.urlencoded)) { return false; } for (i = body[mode].members.length - 1; i >= 0; i--) { param = body[mode].members[i]; // bail out if a single enabled param is present if (param && !param.disabled) { return false; } } return true; } /** * Add new System header. * * @param {object} headers * @param {String} key * @param {String} value */ function addSystemHeader (headers, key, value) { headers.add({ key: key, value: value, system: true }); } /** * Authorize the given request. * * @private * @param {Request} request * @param {Function} callback */ function setAuthorization (request, callback) { authorizeRequest(request, function (err, clonedRequest) { // @note authorizeRequest returns a cloned request. !clonedRequest && (clonedRequest = new Request(request.toJSON())); if (err) { return callback(null, clonedRequest); } var auth = request.auth, authType = auth && auth.type, manifest = _.get(authHandlers, [authType, 'manifest']), headers = _.get(clonedRequest, 'headers.reference') || {}, queryParams = _.get(clonedRequest, 'url.query.reference') || {}, bodyParams = _.get(clonedRequest, 'body.urlencoded.reference') || {}, propertyList, propertyKey, property; if (authType === 'apikey' && (auth = auth.apikey)) { propertyKey = String(auth.get('key')).toLowerCase(); propertyList = auth.get('in') === 'query' ? queryParams : headers; if ((property = propertyList[propertyKey])) { Array.isArray(property) && (property = property[property.length - 1]); property.auth = true; } return callback(null, clonedRequest); } if (!(manifest && manifest.updates)) { return callback(null, clonedRequest); } manifest.updates.forEach(function (update) { propertyKey = update.property; switch (update.type) { case 'header': propertyKey = propertyKey.toLowerCase(); propertyList = headers; break; case 'url.param': propertyList = queryParams; break; case 'body.urlencoded': propertyList = bodyParams; break; default: return; } if ((property = propertyList[propertyKey])) { Array.isArray(property) && (property = property[property.length - 1]); property.auth = true; } }); callback(null, clonedRequest); }); } /** * Adds Content-Type header based on selected request body. * * @private * @param {Request} request * @param {Function} callback */ function setContentType (request, callback) { // bail out if body is empty if (bodyIsEmpty(request.body)) { return callback(null, request); } var headers = request.headers, contentLanguage; switch (request.body.mode) { case BODY_MODE.raw: contentLanguage = _.get(request, 'body.options.raw.language', 'text'); addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_LANGUAGE[contentLanguage] || CONTENT_TYPE_LANGUAGE.text); break; case BODY_MODE.urlencoded: addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_URLENCODED); break; case BODY_MODE.formdata: addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_FORMDATA); break; case BODY_MODE.graphql: addSystemHeader(headers, CONTENT_TYPE, CONTENT_TYPE_LANGUAGE.json); break; case BODY_MODE.file: addSystemHeader(headers, CONTENT_TYPE, mime.lookup(request.body.file && request.body.file.src) || DEFAULT_MIME_TYPE); break; default: break; } addSystemHeader(headers, 'Content-Length', CALCULATED_AT_RUNTIME); callback(null, request); } /** * Adds Cookie header for the given request url. * * @private * @param {Request} request * @param {Object} cookieJar * @param {Function} callback */ function setCookie (request, cookieJar, callback) { // bail out if not a valid instance of CookieJar if (!(cookieJar && cookieJar.getCookieString)) { return callback(null, request); } // @note don't pass request.url instance to force re-parsing of the URL cookieJar.getCookieString(urlEncoder.toNodeUrl(request.url.toString()), function (err, cookies) { if (err) { return callback(null, request); } if (cookies && cookies.length) { addSystemHeader(request.headers, COOKIE, cookies); } callback(null, request); }); } /** * A helper method to dry run the given request instance. * It returns the cloned request instance with the system added properties. * * @param {Request} request * @param {Object} options * @param {Object} options.cookieJar * @param {Object} options.protocolProfileBehavior * @param {Function} done */ function dryRun (request, options, done) { if (!done && typeof options === FUNCTION) { done = options; options = {}; } if (!Request.isRequest(request)) { return done(new Error('Invalid Request instance')); } !options && (options = {}); var cookieJar = options.cookieJar, implicitCacheControl = options.implicitCacheControl, implicitTraceHeader = options.implicitTraceHeader, disabledSystemHeaders = _.get(options.protocolProfileBehavior, 'disabledSystemHeaders') || {}, disableCookies = _.get(options.protocolProfileBehavior, 'disableCookies'); async.waterfall([ function setAuthorizationHeaders (next) { setAuthorization(request, next); }, function setContentTypeHeader (request, next) { setContentType(request, next); }, function setContentLength (request, next) { var headers = request.headers, header = headers.one('content-length'); // bail out if header added by body helper if (header && header.system) { return next(null, request); } switch (String(request.method).toUpperCase()) { case 'GET': case 'HEAD': case 'TRACE': case 'DELETE': case 'CONNECT': case 'OPTIONS': break; default: addSystemHeader(headers, 'Content-Length', '0'); break; } next(null, request); }, function setCookieHeader (request, next) { if (disableCookies || !cookieJar) { return next(null, request); } setCookie(request, cookieJar, next); }, function setStaticHeaders (request, next) { var headers = request.headers; // remove header added by auth helpers headers.remove(function (header) { return header.system && header.key.toLowerCase() === 'host'; }); addSystemHeader(headers, 'User-Agent', 'PostmanRuntime/' + version); addSystemHeader(headers, 'Accept', '*/*'); addSystemHeader(headers, 'Accept-Encoding', 'gzip, deflate, br'); addSystemHeader(headers, 'Host', CALCULATED_AT_RUNTIME); addSystemHeader(headers, 'Connection', 'keep-alive'); implicitCacheControl && addSystemHeader(headers, 'Cache-Control', 'no-cache'); implicitTraceHeader && addSystemHeader(headers, 'Postman-Token', CALCULATED_AT_RUNTIME); next(null, request); }, function disableSystemHeaders (request, next) { var headersReference = request.headers.reference, header; _.forEach(disabledSystemHeaders, function (disabled, headerKey) { if (!disabled) { return; } if ((header = headersReference[headerKey.toLowerCase()])) { Array.isArray(header) && (header = header[header.length - 1]); header.system && (header.disabled = true); } }); next(null, request); } ], function (err, request) { if (err) { return done(err); } done(null, request); }); } module.exports = dryRun;