postman-runtime
Version:
Underlying library of executing Postman Collections (used by Newman)
324 lines (297 loc) • 10.9 kB
JavaScript
var _ = require('lodash'),
oAuth1 = require('node-oauth1'),
urlEncoder = require('postman-url-encoder'),
RequestBody = require('postman-collection').RequestBody,
EMPTY = '',
PROTOCOL_HTTP = 'http',
PROTOCOL_SEPARATOR = '://',
HTTPS_PORT = '443',
HTTP_PORT = '80',
OAUTH1_PARAMS = {
oauthConsumerKey: 'oauth_consumer_key',
oauthToken: 'oauth_token',
oauthSignatureMethod: 'oauth_signature_method',
oauthTimestamp: 'oauth_timestamp',
oauthNonce: 'oauth_nonce',
oauthVersion: 'oauth_version',
oauthSignature: 'oauth_signature'
};
/**
* Returns a OAuth1.0-a compatible representation of the request URL, also called "Base URL".
* For details, http://oauth.net/core/1.0a/#anchor13
*
* todo: should we ignore the auth parameters of the URL or not? (the standard does not mention them)
* we currently are.
*
* @private
* @param {Url} url - Node's URL object
* @returns {String}
*/
function getOAuth1BaseUrl (url) {
var port = url.port ? url.port : undefined,
host = ((port === HTTP_PORT ||
port === HTTPS_PORT ||
port === undefined) && url.hostname) || url.host,
path = url.pathname,
// trim to convert 'http:' from Node's URL object to 'http'
protocol = _.trimEnd(url.protocol || PROTOCOL_HTTP, PROTOCOL_SEPARATOR);
protocol = (_.endsWith(protocol, PROTOCOL_SEPARATOR) ? protocol : protocol + PROTOCOL_SEPARATOR);
return protocol.toLowerCase() + host.toLowerCase() + path;
}
/**
* @implements {AuthHandlerInterface}
*/
module.exports = {
/**
* @property {AuthHandlerInterface~AuthManifest}
*/
manifest: {
info: {
name: 'oauth1',
version: '1.0.0'
},
updates: [
{
property: 'Authorization',
type: 'header'
},
{
property: OAUTH1_PARAMS.oauthConsumerKey,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthToken,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthSignatureMethod,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthTimestamp,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthNonce,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthVersion,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthSignature,
type: 'url.param'
},
{
property: OAUTH1_PARAMS.oauthConsumerKey,
type: 'body.urlencoded'
},
{
property: OAUTH1_PARAMS.oauthToken,
type: 'body.urlencoded'
},
{
property: OAUTH1_PARAMS.oauthSignatureMethod,
type: 'body.urlencoded'
},
{
property: OAUTH1_PARAMS.oauthTimestamp,
type: 'body.urlencoded'
},
{
property: OAUTH1_PARAMS.oauthNonce,
type: 'body.urlencoded'
},
{
property: OAUTH1_PARAMS.oauthVersion,
type: 'body.urlencoded'
},
{
property: OAUTH1_PARAMS.oauthSignature,
type: 'body.urlencoded'
}
]
},
/**
* Initializes an item (extracts parameters from intermediate requests if any, etc)
* before the actual authorization step.
*
* @param {AuthInterface} auth
* @param {Response} response
* @param {AuthHandlerInterface~authInitHookCallback} done
*/
init: function (auth, response, done) {
done(null);
},
/**
* Verifies whether the request has valid basic auth credentials (which is always).
* Sanitizes the auth parameters if needed.
*
* @param {AuthInterface} auth
* @param {AuthHandlerInterface~authPreHookCallback} done
*/
pre: function (auth, done) {
done(null, true);
},
/**
* Verifies whether the basic auth succeeded.
*
* @param {AuthInterface} auth
* @param {Response} response
* @param {AuthHandlerInterface~authPostHookCallback} done
*/
post: function (auth, response, done) {
done(null, true);
},
/**
* Generates a oAuth1.
*
* @param {Object} params
* @param {String} params.url OAuth 1.0 Base URL
* @param {String} params.method Request method
* @param {Object[]} params.helperParams The auth parameters stored with the `Request`
* @param {Object[]} params.queryParams An array of query parameters
* @param {Object[]} params.bodyParams An array of request body parameters (in case of url-encoded bodies)
*
* @returns {*}
*/
computeHeader: function (params) {
var url = params.url,
method = params.method,
helperParams = params.helperParams,
queryParams = params.queryParams,
bodyParams = params.bodyParams,
signatureParams,
message,
accessor = {},
allParams,
signature;
signatureParams = [
{system: true, key: OAUTH1_PARAMS.oauthConsumerKey, value: helperParams.consumerKey},
{system: true, key: OAUTH1_PARAMS.oauthToken, value: helperParams.token},
{system: true, key: OAUTH1_PARAMS.oauthSignatureMethod, value: helperParams.signatureMethod},
{system: true, key: OAUTH1_PARAMS.oauthTimestamp, value: helperParams.timestamp},
{system: true, key: OAUTH1_PARAMS.oauthNonce, value: helperParams.nonce},
{system: true, key: OAUTH1_PARAMS.oauthVersion, value: helperParams.version}
];
signatureParams = _.filter(signatureParams, function (param) {
return helperParams.addEmptyParamsToSign || param.value;
});
allParams = [].concat(signatureParams, queryParams, bodyParams);
message = {
action: url,
method: method,
parameters: _.map(allParams, function (param) {
return [param.key, param.value];
})
};
if (helperParams.consumerSecret) {
accessor.consumerSecret = helperParams.consumerSecret;
}
if (helperParams.tokenSecret) {
accessor.tokenSecret = helperParams.tokenSecret;
}
signature = oAuth1.SignatureMethod.sign(message, accessor);
signatureParams.push({system: true, key: OAUTH1_PARAMS.oauthSignature, value: signature});
return signatureParams;
},
/**
* Signs a request.
*
* @param {AuthInterface} auth
* @param {Request} request
* @param {AuthHandlerInterface~authSignHookCallback} done
*/
sign: function (auth, request, done) {
var params = auth.get([
'consumerKey',
'consumerSecret',
'token',
'tokenSecret',
'signatureMethod',
'timestamp',
'nonce',
'version',
'realm',
'addParamsToHeader',
'addEmptyParamsToSign'
]),
url = urlEncoder.toNodeUrl(request.url.toString(true)),
signatureParams,
header;
if (!params.consumerKey || !params.consumerSecret) {
return done(); // Nothing to do if required parameters are not present.
}
// Remove existing headers and params (if any)
request.removeHeader('Authorization');
request.removeQueryParams(_.values(OAUTH1_PARAMS));
request.body && request.body.urlencoded && request.body.urlencoded.remove(function (param) {
return _.includes(_.values(OAUTH1_PARAMS), param.key);
});
// Generate a new nonce and timestamp
params.nonce = params.nonce || oAuth1.nonce(11).toString();
params.timestamp = params.timestamp || oAuth1.timestamp().toString();
// Ensure that empty parameters are not added to the signature
if (!params.addEmptyParamsToSign) {
params = _.reduce(params, function (accumulator, value, key) {
if (_.isString(value) && (value.trim() === EMPTY)) {
return accumulator;
}
accumulator[key] = value;
return accumulator;
}, {});
}
// Generate a list of parameters associated with the signature.
signatureParams = this.computeHeader({
url: getOAuth1BaseUrl(url),
method: request.method,
queryParams: request.url.query && request.url.query.count() ? request.url.query.map(function (qParam) {
return {
key: qParam.key,
value: qParam.value
};
}) : [],
// todo: WTF! figure out a better way
// Body params only need to be included if they are URL encoded.
// http://oauth.net/core/1.0a/#anchor13
bodyParams: (request.body &&
request.body.mode === RequestBody.MODES.urlencoded &&
request.body.urlencoded &&
request.body.urlencoded.count &&
request.body.urlencoded.count()) ? request.body.urlencoded.map(function (formParam) {
return {
key: formParam.key,
value: formParam.value
};
}) : [],
helperParams: params
});
// The OAuth specification says that we should add parameters in the following order of preference:
// 1. Auth Header
// 2. Body parameters
// 3. Query parameters
//
// http://oauth.net/core/1.0/#consumer_req_param
if (params.addParamsToHeader) {
header = oAuth1.getAuthorizationHeader(params.realm, _.map(signatureParams, function (param) {
return [param.key, param.value];
}));
request.addHeader({
key: 'Authorization',
value: header,
system: true
});
}
else if ((/PUT|POST/).test(request.method) &&
(request.body && request.body.mode === RequestBody.MODES.urlencoded)) {
_.forEach(signatureParams, function (param) {
request.body.urlencoded.add(param);
});
}
else {
request.addQueryParams(signatureParams);
}
return done();
}
};