304 lines
10 KiB
JavaScript
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();
|
|
});
|
|
}
|
|
};
|