UNPKG

postman-runtime

Version:

Underlying library of executing Postman Collections

470 lines (391 loc) 20.3 kB
const _ = require('lodash'), async = require('async'), util = require('./util'), sdk = require('postman-collection'), stripJSONComments = require('strip-json-comments'), createAuthInterface = require('../authorizer/auth-interface'), AuthLoader = require('../authorizer/index').AuthLoader, ReplayController = require('./replay-controller'), { getRequestBody } = require('../requester/core'), // eslint-disable-next-line security/detect-unsafe-regex JSON_CONTENT_TYPE_RE = /^application\/(\S+\+)?json/, REQUEST_BODY_MODE_RAW = 'raw', CONTENT_LANGUAGE_JSON = 'json', STRING = 'string', DOT_AUTH = '.auth'; module.exports = [ // Raw request body update function (context, run, done) { if (!context.item) { return done(new Error('Nothing to update body.')); } const request = _.get(context.item, 'request'), requestBody = _.get(request, 'body'), language = _.get(requestBody, 'options.raw.language'); let contentType = _.get(request, 'headers.reference.content-type'), rawContent; // bail out if request body mode is not raw if (!(requestBody && requestBody.mode === REQUEST_BODY_MODE_RAW)) { return done(); } // bail out if there's no way to determine if the body is JSON if (!(language || contentType)) { return done(); } // bail out when language is present and is not JSON if (language && language !== CONTENT_LANGUAGE_JSON) { return done(); } // bail out when content-type is present and is not JSON if (!language && contentType) { if (Array.isArray(contentType)) { contentType = contentType.find((type) => { return !(type && type.disabled); }); } if (contentType && typeof contentType.value === STRING && !JSON_CONTENT_TYPE_RE.test(contentType.value)) { return done(); } } // get raw body from core `getRequestBody` rawContent = getRequestBody(request, context.protocolProfileBehavior); // bail out if raw body is empty if (!(rawContent && rawContent.body && typeof rawContent.body === STRING)) { return done(); } // bail out if no comments present in raw body if (!(rawContent.body.includes('//') || rawContent.body.includes('/*'))) { return done(); } // NOTE: mutates context.item request body requestBody.raw = stripJSONComments(rawContent.body, { whitespace: false }); done(); }, // File loading function (context, run, done) { if (!context.item) { return done(new Error('Nothing to resolve files for.')); } var triggers = run.triggers, cursor = context.coords, resolver = run.options.fileResolver, request = context.item && context.item.request, mode, data; if (!request) { return done(new Error('No request to send.')); } // if body is disabled than skip loading files. // @todo this may cause problem if body is enabled/disabled programmatically from pre-request script. if (request.body && request.body.disabled) { return done(); } // todo: add helper functions in the sdk to do this cleanly for us mode = _.get(request, 'body.mode'); data = _.get(request, ['body', mode]); // if there is no mode specified, or no data for the specified mode we cannot resolve anything! // @note that if source is not readable, there is no point reading anything, yet we need to warn that file // upload was not done. hence we will have to proceed even without an unreadable source if (!data) { // we do not need to check `mode` here since false mode returns no `data` return done(); } // in this block, we simply use async.waterfall to ensure that all form of file reading is async. essentially, // we first determine the data mode and based on it pass the waterfall functions. async.waterfall([async.constant(data), { // form data parsing simply "enriches" all form parameters having file data type by replacing / setting the // value as a read stream formdata (formdata, next) { // ensure that we only process the file type async.eachSeries(_.filter(formdata.all(), { type: 'file' }), function (formparam, callback) { if (!formparam || formparam.disabled) { return callback(); // disabled params will be filtered in body-builder. } var paramIsComposite = Array.isArray(formparam.src), onLoadError = function (err, disableParam) { // triggering a warning message for the user triggers.console(cursor, 'warn', `Form param \`${formparam.key}\`, file load error: ${err.message || err}`); // set disabled, it will be filtered in body-builder disableParam && (formparam.disabled = true); }; // handle base64 encoded file if (!formparam.src && formparam.value && typeof formparam.value === STRING) { formparam.value = Buffer.from(formparam.value, 'base64'); return callback(); } // handle missing file src if (!formparam.src || (paramIsComposite && !formparam.src.length)) { onLoadError(new Error('missing file source'), false); return callback(); } // handle form param with a single file // @note we are handling single file first so that we do not need to hit additional complexity of // handling multiple files while the majority use-case would be to handle single file. if (!paramIsComposite) { // eslint-disable-next-line security/detect-non-literal-fs-filename util.createReadStream(resolver, formparam.src, function (err, stream) { if (err) { onLoadError(err, true); } else { formparam.value = stream; } callback(); }); return; } // handle form param with multiple files // @note we use map-limit here instead of free-form map in order to avoid choking the file system // with many parallel descriptor access. async.mapLimit(formparam.src, 10, function (src, next) { // eslint-disable-next-line security/detect-non-literal-fs-filename util.createReadStream(resolver, src, function (err, stream) { if (err) { // @note don't throw error or disable param if one of the src fails to load onLoadError(err); return next(); // swallow the error } next(null, { src: src, value: stream }); }); }, function (err, results) { if (err) { onLoadError(err, true); return done(); } _.forEach(results, function (result) { // Insert individual param above the current formparam result && formdata.insert(new sdk.FormParam(_.assign(formparam.toJSON(), result)), formparam); }); // remove the current formparam after exploding src formdata.remove(formparam); done(); }); }, next); }, // file data file (filedata, next) { // handle base64 encoded file if (!filedata.src && filedata.content && typeof filedata.content === STRING) { filedata.content = Buffer.from(filedata.content, 'base64'); return next(); } // eslint-disable-next-line security/detect-non-literal-fs-filename util.createReadStream(resolver, filedata.src, function (err, stream) { if (err) { triggers.console(cursor, 'warn', 'Binary file load error: ' + err.message || err); filedata.value = null; // ensure this does not mess with requester delete filedata.content; // @todo - why content? } else { filedata.content = stream; } next(); }); } }[mode] || async.constant()], function (err) { // just as a precaution, show the error in console. each resolver anyway should handle their own console // warnings. // @todo - get cursor here. err && triggers.console(cursor, 'warn', 'file data resolution error: ' + (err.message || err)); done(null); // absorb the error since a console has been trigerred }); }, // Authorization function (context, run, done) { // validate all stuff. dont ask. if (!context.item) { return done(new Error('runtime: nothing to authorize.')); } // bail out if there is no auth if (!(context.auth && context.auth.type)) { return done(null); } // get auth handler var auth = context.auth, authType = auth.type, originalAuth = context.originalItem.getAuth(), originalAuthParams = originalAuth && originalAuth.parameters(), authHandler = AuthLoader.getHandler(authType), authPreHook, authInterface, logError = function (err) { if (err) { run.triggers.console(context.coords, 'warn', 'runtime~' + authType + '.auth: could not sign the request: ' + (err.message || err)); } }, authSignHook = function () { try { authHandler.sign(authInterface, context.item.request, function (err) { logError(err); done(); }); } catch (err) { // handles synchronous errors in auth.sign logError(err); // swallow the error, we've warned the user done(); } }; // bail out if there is no matching auth handler for the type if (!authHandler) { run.triggers.console(context.coords, 'warn', 'runtime: could not find a handler for auth: ' + auth.type); return done(); } authInterface = createAuthInterface(auth, context.protocolProfileBehavior, run.options.requester && run.options.requester.authorizer); /** * We go through the `pre` request send validation for the auth. In this step one of the three things can happen * * If the Auth `pre` hook * 1. gives a go, we sign the request and proceed to send the request. * 2. gives a no go, we don't sign the request, but proceed to send the request. * 3. gives a no go, with a intermediate request, * a. we suspend current request, send the intermediate request * b. invoke Auth `init` hook with the response of the intermediate request * c. invoke Auth `pre` hook, and repeat from 1 */ authPreHook = function () { authHandler.pre(authInterface, function (err, success, request) { // there was an error in pre hook of auth if (err) { // warn the user run.triggers.console(context.coords, 'warn', 'runtime~' + authType + '.auth: could not validate the request: ' + (err.message || err), err); // swallow the error, we've warned the user return done(); } // sync all auth system parameters to the original auth originalAuthParams && auth.parameters().each(function (param) { param && param.system && originalAuthParams.upsert({ key: param.key, value: param.value, system: true }); }); // authHandler gave a go, sign the request // FIXME: `authSignHook` is probably async if (success) { return authSignHook(); } // auth gave a no go, but no intermediate request if (!request) { return done(); } // prepare for sending intermediate request var replayController = new ReplayController(context.replayState, run), item = new sdk.Item({ request }), originalItem = context.originalItem; item.setParent(originalItem.parent()); // auth handler gave a no go, and an intermediate request. // make the intermediate request the response is passed to `init` hook // we are overriding the originalItem because in pre we have a entire new request replayController.requestReplay(_.assign(context, { originalItem: item }), item, // marks the auth as source for intermediate request { source: auth.type + DOT_AUTH }, function (err, response) { context.originalItem = originalItem; // reset the original item // errors for intermediate requests are passed to request callback // passing it here will add it to original request as well, so don't do it if (err) { return done(); } // pass the response to Auth `init` hook authHandler.init(authInterface, response, function (error) { if (error) { // warn about the err run.triggers.console(context.coords, 'warn', 'runtime~' + authType + '.auth: ' + 'could not initialize auth: ' + (error.message || error), error); // swallow the error, we've warned the user return done(); } // schedule back to pre hook authPreHook(); }); }, function (err) { context.originalItem = originalItem; // reset the original item // warn users that maximum retries have exceeded if (err) { run.triggers.console(context.coords, 'warn', 'runtime~' + authType + '.auth: ' + (err.message || err)); } // but don't bubble up the error with the request done(); }); }); }; // start the by calling the pre hook of the auth authPreHook(); }, // Proxy lookup function (context, run, done) { var proxies = run.options.proxies, request = context.item.request, url; if (!request) { return done(new Error('No request to resolve proxy for.')); } url = request.url && request.url.toString(); async.waterfall([ // try resolving custom proxies before falling-back to system proxy function (cb) { if (_.isFunction(_.get(proxies, 'resolve'))) { return cb(null, proxies.resolve(url)); } return cb(null, undefined); }, // fallback to system proxy function (config, cb) { if (config) { return cb(null, config); } return _.isFunction(run.options.systemProxy) ? run.options.systemProxy(url, cb) : cb(null, undefined); } ], function (err, config) { if (err) { run.triggers.console(context.coords, 'warn', 'proxy lookup error: ' + (err.message || err)); } config && (request.proxy = sdk.ProxyConfig.isProxyConfig(config) ? config : new sdk.ProxyConfig(config)); return done(); }); }, // Certificate lookup + reading from whichever file resolver is provided function (context, run, done) { var request, pfxPath, keyPath, certPath, fileResolver, certificate; // A. Check if we have the file resolver fileResolver = run.options.fileResolver; if (!fileResolver) { return done(); } // No point going ahead // B. Ensure we have the request request = _.get(context.item, 'request'); if (!request) { return done(new Error('No request to resolve certificates for.')); } // C. See if any cert should be sent, by performing a URL matching certificate = run.options.certificates && run.options.certificates.resolveOne(request.url); if (!certificate) { return done(); } // D. Fetch the paths // @todo: check why aren't we reading ca file (why are we not supporting ca file) pfxPath = _.get(certificate, 'pfx.src'); keyPath = _.get(certificate, 'key.src'); certPath = _.get(certificate, 'cert.src'); // E. Read from the path, and add the values to the certificate, also associate // the certificate with the current request. async.mapValues({ pfx: pfxPath, key: keyPath, cert: certPath }, function (value, key, next) { // bail out if value is not defined // @todo add test with server which only accepts cert file if (!value) { return next(); } // eslint-disable-next-line security/detect-non-literal-fs-filename fileResolver.readFile(value, function (err, data) { // Swallow the error after triggering a warning message for the user. err && run.triggers.console(context.coords, 'warn', `certificate "${key}" load error: ${(err.message || err)}`); next(null, data); }); }, function (err, fileContents) { if (err) { // Swallow the error after triggering a warning message for the user. run.triggers.console(context.coords, 'warn', 'certificate load error: ' + (err.message || err)); return done(); } if (fileContents) { !_.isNil(fileContents.pfx) && _.set(certificate, 'pfx.value', fileContents.pfx); !_.isNil(fileContents.key) && _.set(certificate, 'key.value', fileContents.key); !_.isNil(fileContents.cert) && _.set(certificate, 'cert.value', fileContents.cert); (fileContents.cert || fileContents.key || fileContents.pfx) && (request.certificate = certificate); } done(); }); } ];