384 lines
17 KiB
JavaScript
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();
|
|
});
|
|
}
|
|
];
|