Simon Priet e69a613a37 feat: Created a mini nodeJS server with NewMan for testing without PostMan GUI.
This will mimic a run in a CD/CI environment or docker container.
2021-09-08 14:01:19 +02:00

384 lines
17 KiB
JavaScript

var _ = require('lodash'),
async = require('async'),
util = require('./util'),
sdk = require('postman-collection'),
createAuthInterface = require('../authorizer/auth-interface'),
AuthLoader = require('../authorizer/index').AuthLoader,
ReplayController = require('./replay-controller'),
DOT_AUTH = '.auth';
module.exports = [
// 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: function (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 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: function (filedata, 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,
authSignHook = function () {
try {
authHandler.sign(authInterface, context.item.request, function (err) {
// handle all types of errors in one place, see catch block
if (err) { throw err; }
done();
});
}
catch (err) {
// handles synchronous and asynchronous errors in auth.sign
run.triggers.console(context.coords,
'warn',
'runtime~' + authType + '.auth: could not sign the request: ' + (err.message || err),
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);
/**
* 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
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: request});
// auth handler gave a no go, and an intermediate request.
// make the intermediate request the response is passed to `init` hook
replayController.requestReplay(context,
item,
// marks the auth as source for intermediate request
{source: auth.type + DOT_AUTH},
function (err, response) {
// 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) {
// 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();
});
}
];