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.
This commit is contained in:
498
node_modules/postman-runtime/lib/authorizer/digest.js
generated
vendored
Normal file
498
node_modules/postman-runtime/lib/authorizer/digest.js
generated
vendored
Normal file
@@ -0,0 +1,498 @@
|
||||
var _ = require('lodash'),
|
||||
crypto = require('crypto'),
|
||||
urlEncoder = require('postman-url-encoder'),
|
||||
RequestBody = require('postman-collection').RequestBody,
|
||||
bodyBuilder = require('../requester/core-body-builder'),
|
||||
|
||||
EMPTY = '',
|
||||
ONE = '00000001',
|
||||
DISABLE_RETRY_REQUEST = 'disableRetryRequest',
|
||||
WWW_AUTHENTICATE = 'www-authenticate',
|
||||
DIGEST_PREFIX = 'Digest ',
|
||||
QOP = 'qop',
|
||||
AUTH = 'auth',
|
||||
COLON = ':',
|
||||
QUOTE = '"',
|
||||
SESS = '-sess',
|
||||
AUTH_INT = 'auth-int',
|
||||
AUTHORIZATION = 'Authorization',
|
||||
MD5_SESS = 'MD5-sess',
|
||||
ASCII_SOURCE = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
||||
ASCII_SOURCE_LENGTH = ASCII_SOURCE.length,
|
||||
USERNAME_EQUALS_QUOTE = 'username="',
|
||||
REALM_EQUALS_QUOTE = 'realm="',
|
||||
NONCE_EQUALS_QUOTE = 'nonce="',
|
||||
URI_EQUALS_QUOTE = 'uri="',
|
||||
ALGORITHM_EQUALS_QUOTE = 'algorithm="',
|
||||
CNONCE_EQUALS_QUOTE = 'cnonce="',
|
||||
RESPONSE_EQUALS_QUOTE = 'response="',
|
||||
OPAQUE_EQUALS_QUOTE = 'opaque="',
|
||||
QOP_EQUALS = 'qop=',
|
||||
NC_EQUALS = 'nc=',
|
||||
ALGO = {
|
||||
MD5: 'MD5',
|
||||
MD5_SESS: 'MD5-sess',
|
||||
SHA_256: 'SHA-256',
|
||||
SHA_256_SESS: 'SHA-256-sess',
|
||||
SHA_512_256: 'SHA-512-256',
|
||||
SHA_512_256_SESS: 'SHA-512-256-sess'
|
||||
},
|
||||
AUTH_PARAMETERS = [
|
||||
'algorithm',
|
||||
'username',
|
||||
'realm',
|
||||
'password',
|
||||
'method',
|
||||
'nonce',
|
||||
'nonceCount',
|
||||
'clientNonce',
|
||||
'opaque',
|
||||
'qop',
|
||||
'uri'
|
||||
],
|
||||
|
||||
nonceRegex = /nonce="([^"]*)"/,
|
||||
realmRegex = /realm="([^"]*)"/,
|
||||
qopRegex = /qop="([^"]*)"/,
|
||||
opaqueRegex = /opaque="([^"]*)"/,
|
||||
_extractField,
|
||||
SHA512_256,
|
||||
nodeCrypto;
|
||||
|
||||
// Current Electron version(7.2.3) in Postman app uses OpenSSL 1.1.0
|
||||
// which don't support `SHA-512-256`. Use external `js-sha512` module
|
||||
// to handle this case.
|
||||
if (!_.includes(crypto.getHashes(), 'sha512-256')) {
|
||||
SHA512_256 = require('js-sha512').sha512_256;
|
||||
nodeCrypto = crypto;
|
||||
|
||||
// create a wrapper class with similar interface to Node's crypto and use jsSHA
|
||||
// to support SHA512-256 algorithm
|
||||
crypto = function () {
|
||||
this._hash = SHA512_256.create();
|
||||
};
|
||||
|
||||
_.assign(crypto.prototype, {
|
||||
update: function (data) {
|
||||
this._hash.update(data);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
digest: function () {
|
||||
// we only need 'hex' digest for this auth
|
||||
return this._hash.hex();
|
||||
}
|
||||
});
|
||||
|
||||
_.assign(crypto, {
|
||||
createHash: function (hashAlgo) {
|
||||
// return hash from js-sha for SHA512-256
|
||||
if (hashAlgo === 'sha512-256') {
|
||||
return new crypto();
|
||||
}
|
||||
|
||||
// return Node's hash otherwise
|
||||
return nodeCrypto.createHash(hashAlgo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random string of given length
|
||||
*
|
||||
* @todo Move this to util.js. After moving use that for hawk auth too
|
||||
* @param {Number} length
|
||||
*/
|
||||
function randomString (length) {
|
||||
length = length || 6;
|
||||
|
||||
var result = [],
|
||||
i;
|
||||
|
||||
for (i = 0; i < length; i++) {
|
||||
result[i] = ASCII_SOURCE[(Math.random() * ASCII_SOURCE_LENGTH) | 0];
|
||||
}
|
||||
|
||||
return result.join(EMPTY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts a Digest Auth field from a WWW-Authenticate header value using a given regexp.
|
||||
*
|
||||
* @param {String} string
|
||||
* @param {RegExp} regexp
|
||||
* @private
|
||||
*/
|
||||
_extractField = function (string, regexp) {
|
||||
var match = string.match(regexp);
|
||||
|
||||
return match ? match[1] : EMPTY;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the 'www-authenticate' header for Digest auth. Since a server can suport more than more auth-scheme,
|
||||
* there can be more than one header with the same key. So need to loop over and check each one.
|
||||
*
|
||||
* @param {VariableList} headers
|
||||
* @private
|
||||
*/
|
||||
function _getDigestAuthHeader (headers) {
|
||||
return headers.find(function (property) {
|
||||
return (property.key.toLowerCase() === WWW_AUTHENTICATE) && (_.startsWith(property.value, DIGEST_PREFIX));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns hex encoded hash of given data using given algorithm.
|
||||
*
|
||||
* @param {String} data string to calculate hash
|
||||
* @param {String} algorithm hash algorithm
|
||||
* @returns {String} hex encoded hash of given data
|
||||
*/
|
||||
function getHash (data, algorithm) {
|
||||
return crypto.createHash(algorithm).update(data || EMPTY).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates body hash with given algorithm and digestEncoding.
|
||||
*
|
||||
* @param {RequestBody} body Request body
|
||||
* @param {String} algorithm Hash algorithm to use
|
||||
* @param {String} digestEncoding Encoding of the hash
|
||||
* @param {Function} callback Callback function that will be called with body hash
|
||||
*/
|
||||
function computeBodyHash (body, algorithm, digestEncoding, callback) {
|
||||
if (!(algorithm && digestEncoding)) { return callback(); }
|
||||
|
||||
var hash = crypto.createHash(algorithm),
|
||||
originalReadStream,
|
||||
rawBody,
|
||||
graphqlBody,
|
||||
urlencodedBody;
|
||||
|
||||
// if body is not available, return hash of empty string
|
||||
if (!body || body.isEmpty()) {
|
||||
return callback(hash.digest(digestEncoding));
|
||||
}
|
||||
|
||||
if (body.mode === RequestBody.MODES.raw) {
|
||||
rawBody = bodyBuilder.raw(body.raw).body;
|
||||
hash.update(rawBody);
|
||||
|
||||
return callback(hash.digest(digestEncoding));
|
||||
}
|
||||
|
||||
if (body.mode === RequestBody.MODES.urlencoded) {
|
||||
urlencodedBody = bodyBuilder.urlencoded(body.urlencoded).form;
|
||||
urlencodedBody = urlEncoder.encodeQueryString(urlencodedBody);
|
||||
hash.update(urlencodedBody);
|
||||
|
||||
return callback(hash.digest(digestEncoding));
|
||||
}
|
||||
|
||||
if (body.mode === RequestBody.MODES.file) {
|
||||
originalReadStream = _.get(body, 'file.content');
|
||||
|
||||
if (!originalReadStream) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
return originalReadStream.cloneReadStream(function (err, clonedStream) {
|
||||
if (err) { return callback(); }
|
||||
|
||||
clonedStream.on('data', function (chunk) {
|
||||
hash.update(chunk);
|
||||
});
|
||||
|
||||
clonedStream.on('end', function () {
|
||||
callback(hash.digest(digestEncoding));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (body.mode === RequestBody.MODES.graphql) {
|
||||
graphqlBody = bodyBuilder.graphql(body.graphql).body;
|
||||
hash.update(graphqlBody);
|
||||
|
||||
return callback(hash.digest(digestEncoding));
|
||||
}
|
||||
|
||||
// @todo: Figure out a way to calculate hash for formdata body type.
|
||||
|
||||
// ensure that callback is called if body.mode doesn't match with any of the above modes
|
||||
return callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* All the auth definition parameters excluding username and password should be stored and resued.
|
||||
* @todo The current implementation would fail for the case when two requests to two different hosts inherits the same
|
||||
* auth. In that case a retry would not be attempted for the second request (since all the parameters would be present
|
||||
* in the auth definition though invalid).
|
||||
*
|
||||
* @implements {AuthHandlerInterface}
|
||||
*/
|
||||
module.exports = {
|
||||
/**
|
||||
* @property {AuthHandlerInterface~AuthManifest}
|
||||
*/
|
||||
manifest: {
|
||||
info: {
|
||||
name: 'digest',
|
||||
version: '1.0.0'
|
||||
},
|
||||
updates: [
|
||||
{
|
||||
property: 'Authorization',
|
||||
type: 'header'
|
||||
},
|
||||
{
|
||||
property: 'nonce',
|
||||
type: 'auth'
|
||||
},
|
||||
{
|
||||
property: 'realm',
|
||||
type: 'auth'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks whether the given item has all the required parameters in its request.
|
||||
* Sanitizes the auth parameters if needed.
|
||||
*
|
||||
* @param {AuthInterface} auth
|
||||
* @param {AuthHandlerInterface~authPreHookCallback} done
|
||||
*/
|
||||
pre: function (auth, done) {
|
||||
// ensure that all dynamic parameter values are present in the parameters
|
||||
// if even one is absent, we return false.
|
||||
done(null, Boolean(auth.get('nonce') && auth.get('realm')));
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies whether the request was successfully authorized after being sent.
|
||||
*
|
||||
* @param {AuthInterface} auth
|
||||
* @param {Response} response
|
||||
* @param {AuthHandlerInterface~authPostHookCallback} done
|
||||
*/
|
||||
post: function (auth, response, done) {
|
||||
if (auth.get(DISABLE_RETRY_REQUEST) || !response) {
|
||||
return done(null, true);
|
||||
}
|
||||
|
||||
var code,
|
||||
realm,
|
||||
nonce,
|
||||
qop,
|
||||
opaque,
|
||||
authHeader,
|
||||
authParams = {};
|
||||
|
||||
code = response.code;
|
||||
authHeader = _getDigestAuthHeader(response.headers);
|
||||
|
||||
// If code is forbidden or unauthorized, and an auth header exists,
|
||||
// we can extract the realm & the nonce, and replay the request.
|
||||
// todo: add response.is4XX, response.is5XX, etc in the SDK.
|
||||
if ((code === 401 || code === 403) && authHeader) {
|
||||
nonce = _extractField(authHeader.value, nonceRegex);
|
||||
realm = _extractField(authHeader.value, realmRegex);
|
||||
qop = _extractField(authHeader.value, qopRegex);
|
||||
opaque = _extractField(authHeader.value, opaqueRegex);
|
||||
|
||||
authParams.nonce = nonce;
|
||||
authParams.realm = realm;
|
||||
opaque && (authParams.opaque = opaque);
|
||||
qop && (authParams.qop = qop);
|
||||
|
||||
if (authParams.qop || auth.get(QOP)) {
|
||||
authParams.clientNonce = randomString(8);
|
||||
authParams.nonceCount = ONE;
|
||||
}
|
||||
|
||||
// if all the auth parameters sent by server were already present in auth definition then we do not retry
|
||||
if (_.every(authParams, function (value, key) { return auth.get(key); })) {
|
||||
return done(null, true);
|
||||
}
|
||||
|
||||
auth.set(authParams);
|
||||
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
done(null, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Computes the Digest Authentication header from the given parameters.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {String} params.algorithm
|
||||
* @param {String} params.username
|
||||
* @param {String} params.realm
|
||||
* @param {String} params.password
|
||||
* @param {String} params.method
|
||||
* @param {String} params.nonce
|
||||
* @param {String} params.nonceCount
|
||||
* @param {String} params.clientNonce
|
||||
* @param {String} params.opaque
|
||||
* @param {String} params.qop
|
||||
* @param {String} params.uri
|
||||
* @returns {String}
|
||||
*/
|
||||
computeHeader: function (params) {
|
||||
var algorithm = params.algorithm,
|
||||
hashAlgo = params.hashAlgo,
|
||||
username = params.username,
|
||||
realm = params.realm,
|
||||
password = params.password,
|
||||
method = params.method,
|
||||
nonce = params.nonce,
|
||||
nonceCount = params.nonceCount,
|
||||
clientNonce = params.clientNonce,
|
||||
opaque = params.opaque,
|
||||
qop = params.qop,
|
||||
uri = params.uri,
|
||||
|
||||
// RFC defined terms, http://tools.ietf.org/html/rfc2617#section-3
|
||||
A0,
|
||||
A1,
|
||||
A2,
|
||||
hashA1,
|
||||
hashA2,
|
||||
|
||||
reqDigest,
|
||||
headerParams;
|
||||
|
||||
if (_.endsWith(algorithm, SESS)) {
|
||||
A0 = getHash(username + COLON + realm + COLON + password, hashAlgo);
|
||||
A1 = A0 + COLON + nonce + COLON + clientNonce;
|
||||
}
|
||||
else {
|
||||
A1 = username + COLON + realm + COLON + password;
|
||||
}
|
||||
|
||||
if (qop === AUTH_INT) {
|
||||
A2 = method + COLON + uri + COLON + params.bodyhash;
|
||||
}
|
||||
else {
|
||||
A2 = method + COLON + uri;
|
||||
}
|
||||
hashA1 = getHash(A1, hashAlgo);
|
||||
hashA2 = getHash(A2, hashAlgo);
|
||||
|
||||
if (qop === AUTH || qop === AUTH_INT) {
|
||||
reqDigest = getHash([hashA1, nonce, nonceCount, clientNonce, qop, hashA2].join(COLON), hashAlgo);
|
||||
}
|
||||
else {
|
||||
reqDigest = getHash([hashA1, nonce, hashA2].join(COLON), hashAlgo);
|
||||
}
|
||||
|
||||
headerParams = [USERNAME_EQUALS_QUOTE + username + QUOTE,
|
||||
REALM_EQUALS_QUOTE + realm + QUOTE,
|
||||
NONCE_EQUALS_QUOTE + nonce + QUOTE,
|
||||
URI_EQUALS_QUOTE + uri + QUOTE
|
||||
];
|
||||
|
||||
algorithm && headerParams.push(ALGORITHM_EQUALS_QUOTE + algorithm + QUOTE);
|
||||
|
||||
if (qop === AUTH || qop === AUTH_INT) {
|
||||
headerParams.push(QOP_EQUALS + qop);
|
||||
}
|
||||
|
||||
if (qop === AUTH || qop === AUTH_INT || algorithm === MD5_SESS) {
|
||||
nonceCount && headerParams.push(NC_EQUALS + nonceCount);
|
||||
headerParams.push(CNONCE_EQUALS_QUOTE + clientNonce + QUOTE);
|
||||
}
|
||||
|
||||
headerParams.push(RESPONSE_EQUALS_QUOTE + reqDigest + QUOTE);
|
||||
opaque && headerParams.push(OPAQUE_EQUALS_QUOTE + opaque + QUOTE);
|
||||
|
||||
return DIGEST_PREFIX + headerParams.join(', ');
|
||||
},
|
||||
|
||||
/**
|
||||
* Signs a request.
|
||||
*
|
||||
* @param {AuthInterface} auth
|
||||
* @param {Request} request
|
||||
* @param {AuthHandlerInterface~authSignHookCallback} done
|
||||
*/
|
||||
sign: function (auth, request, done) {
|
||||
var self = this,
|
||||
params = auth.get(AUTH_PARAMETERS),
|
||||
url = urlEncoder.toNodeUrl(request.url),
|
||||
header;
|
||||
|
||||
if (!params.username || !params.realm) {
|
||||
return done(); // Nothing to do if required parameters are not present.
|
||||
}
|
||||
|
||||
request.removeHeader(AUTHORIZATION, {ignoreCase: true});
|
||||
|
||||
params.method = request.method;
|
||||
params.uri = url.path;
|
||||
|
||||
switch (params.algorithm) {
|
||||
case ALGO.SHA_256:
|
||||
case ALGO.SHA_256_SESS:
|
||||
params.hashAlgo = 'sha256';
|
||||
break;
|
||||
case ALGO.MD5:
|
||||
case ALGO.MD5_SESS:
|
||||
case EMPTY:
|
||||
case undefined:
|
||||
case null:
|
||||
params.algorithm = params.algorithm || ALGO.MD5;
|
||||
params.hashAlgo = 'md5';
|
||||
break;
|
||||
case ALGO.SHA_512_256:
|
||||
case ALGO.SHA_512_256_SESS:
|
||||
params.hashAlgo = 'sha512-256';
|
||||
break;
|
||||
default:
|
||||
return done(new Error(`Unsupported digest algorithm: ${params.algorithm}`));
|
||||
}
|
||||
|
||||
// calculate body hash for qop='auth-int'
|
||||
if (params.qop === AUTH_INT) {
|
||||
return computeBodyHash(request.body, params.hashAlgo, 'hex', function (bodyhash) {
|
||||
params.bodyhash = bodyhash;
|
||||
header = self.computeHeader(params);
|
||||
|
||||
request.addHeader({
|
||||
key: AUTHORIZATION,
|
||||
value: header,
|
||||
system: true
|
||||
});
|
||||
|
||||
return done();
|
||||
});
|
||||
}
|
||||
|
||||
header = self.computeHeader(params);
|
||||
|
||||
request.addHeader({
|
||||
key: AUTHORIZATION,
|
||||
value: header,
|
||||
system: true
|
||||
});
|
||||
|
||||
return done();
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user