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

304 lines
10 KiB
JavaScript

var _ = require('lodash'),
aws4 = require('aws4'),
crypto = require('crypto'),
sdk = require('postman-collection'),
urlEncoder = require('postman-url-encoder'),
bodyBuilder = require('../requester/core-body-builder'),
RequestBody = sdk.RequestBody,
X_AMZ_PREFIX = 'X-Amz-',
BODY_HASH_HEADER = 'X-Amz-Content-Sha256',
/**
* Calculates body hash with given algorithm and digestEncoding.
*
* @todo This function can also be used in Digest auth so that it works correctly for urlencoded and file body types
*
* @param {RequestBody} body
* @param {String} algorithm
* @param {String} digestEncoding
* @param {Function} callback
*/
computeBodyHash = function (body, algorithm, digestEncoding, callback) {
if (!(body && algorithm && digestEncoding) || body.isEmpty()) { return callback(); }
var hash = crypto.createHash(algorithm),
originalReadStream,
rawBody,
urlencodedBody,
graphqlBody;
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: formdata body type requires adding new data to form instead of setting headers for AWS auth.
// Figure out how to do that. See below link:
// AWS auth with formdata: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
// ensure that callback is called if body.mode doesn't match with any of the above modes
return callback();
};
/**
* @implements {AuthHandlerInterface}
*/
module.exports = {
/**
* @property {AuthHandlerInterface~AuthManifest}
*/
manifest: {
info: {
name: 'awsv4',
version: '1.0.0'
},
updates: [
{
property: 'Host',
type: 'header'
},
{
property: 'Authorization',
type: 'header'
},
{
property: 'X-Amz-Date',
type: 'header'
},
{
property: 'X-Amz-Security-Token',
type: 'header'
},
{
property: 'X-Amz-Content-Sha256',
type: 'header'
},
{
property: 'X-Amz-Security-Token',
type: 'url.param'
},
{
property: 'X-Amz-Expires',
type: 'url.param'
},
{
property: 'X-Amz-Date',
type: 'url.param'
},
{
property: 'X-Amz-Algorithm',
type: 'url.param'
},
{
property: 'X-Amz-Credential',
type: 'url.param'
},
{
property: 'X-Amz-SignedHeaders',
type: 'url.param'
},
{
property: 'X-Amz-Signature',
type: 'url.param'
}
]
},
/**
* Initializes a item (fetches all required parameters, etc) before the actual authorization step.
*
* @param {AuthInterface} auth
* @param {Response} response
* @param {AuthHandlerInterface~authInitHookCallback} done
*/
init: function (auth, response, done) {
done(null);
},
/**
* Checks the item, and fetches any parameters that are not already provided.
*
* @param {AuthInterface} auth
* @param {AuthHandlerInterface~authPreHookCallback} done
*/
pre: function (auth, done) {
done(null, true);
},
/**
* Verifies whether the request was successful after being sent.
*
* @param {AuthInterface} auth
* @param {Requester} response
* @param {AuthHandlerInterface~authPostHookCallback} done
*/
post: function (auth, response, done) {
done(null, true);
},
/**
* Generates the signature and adds auth data to the request as additional headers/query params.
* AWS v4 auth mandates that a content type header be present in each request.
*
* @param {Request} request request to add auth data
* @param {Object} params data required for auth
* @param {Object} params.credentials Should contain the AWS credentials, "accessKeyId" and "secretAccessKey"
* @param {String} params.host Contains the host name for the request
* @param {String} params.path Contains the complete path, with query string as well, e.g: /something/kane?hi=ho
* @param {String} params.service The name of the AWS service
* @param {String} params.region AWS region
* @param {String} params.method Request method
* @param {String} params.body Stringified request body
* @param {Object} params.headers Each key should be a header key, and the value should be a header value
* @param {Boolean} params.signQuery Add auth data to query params if true, otherwise add it to headers
*/
addAuthDataToRequest: function (request, params) {
var signedData = aws4.sign(params, params.credentials);
if (params.signQuery) {
_.forEach(sdk.Url.parse(signedData.path).query, function (param) {
// only add additional AWS specific params to request
if (_.startsWith(param.key, X_AMZ_PREFIX) && !request.url.query.has(param.key)) {
param.system = true;
request.url.query.add(param);
}
});
}
_.forEach(signedData.headers, function (value, key) {
request.upsertHeader({
key: key,
value: value,
system: true
});
});
},
/**
* Signs a request.
*
* @param {AuthInterface} auth
* @param {Request} request
* @param {AuthHandlerInterface~authSignHookCallback} done
*/
sign: function (auth, request, done) {
var self = this,
params = auth.get([
'accessKey',
'secretKey',
'sessionToken',
'service',
'region',
'addAuthDataToQuery'
]),
url = urlEncoder.toNodeUrl(request.url),
dataToSign;
// Clean up the request (if needed)
request.removeHeader('Authorization', {ignoreCase: true});
request.removeHeader('X-Amz-Date', {ignoreCase: true});
request.removeHeader('X-Amz-Security-Token', {ignoreCase: true});
request.removeHeader('X-Amz-Content-Sha256', {ignoreCase: true});
// Not removing `X-Amz-Expires` from params here allowing user to override
// default value
request.removeQueryParams([
'X-Amz-Security-Token',
'X-Amz-Date',
'X-Amz-Algorithm',
'X-Amz-Credential',
'X-Amz-SignedHeaders',
'X-Amz-Signature'
]);
dataToSign = {
credentials: {
accessKeyId: params.accessKey,
secretAccessKey: params.secretKey,
sessionToken: params.sessionToken || undefined
},
host: url.host,
path: url.path, // path = pathname + query
service: params.service || 'execute-api', // AWS API Gateway is the default service.
region: params.region || 'us-east-1',
method: request.method,
body: undefined, // no need to give body since we are setting 'X-Amz-Content-Sha256' header
headers: _.transform(request.getHeaders({enabled: true}), function (accumulator, value, key) {
accumulator[key] = value;
}, {}),
signQuery: params.addAuthDataToQuery
};
// Removed the code which was adding content-type header if it is not there in the request. Because
// aws4 does not require content-type header. It is only mandatory to include content-type header in signature
// calculation if it is there in the request.
// Refer: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request
// body hash is not required when adding auth data to qury params
// @see: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
if (params.addAuthDataToQuery) {
self.addAuthDataToRequest(request, dataToSign);
return done();
}
// aws4 module can't calculate body hash for body with ReadStream.
// So we calculate it our self and set 'X-Amz-Content-Sha256' header which will be used by aws4 module
// to calculate the signature.
computeBodyHash(request.body, 'sha256', 'hex', function (bodyHash) {
if (bodyHash) {
request.upsertHeader({
key: BODY_HASH_HEADER,
value: bodyHash,
system: true
});
dataToSign.headers[BODY_HASH_HEADER] = bodyHash;
}
self.addAuthDataToRequest(request, dataToSign);
return done();
});
}
};