v4.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. var AWS = require('../core');
  2. var v4Credentials = require('./v4_credentials');
  3. var inherit = AWS.util.inherit;
  4. /**
  5. * @api private
  6. */
  7. var expiresHeader = 'presigned-expires';
  8. /**
  9. * @api private
  10. */
  11. AWS.Signers.V4 = inherit(AWS.Signers.RequestSigner, {
  12. constructor: function V4(request, serviceName, options) {
  13. AWS.Signers.RequestSigner.call(this, request);
  14. this.serviceName = serviceName;
  15. options = options || {};
  16. this.signatureCache = typeof options.signatureCache === 'boolean' ? options.signatureCache : true;
  17. this.operation = options.operation;
  18. this.signatureVersion = options.signatureVersion;
  19. },
  20. algorithm: 'AWS4-HMAC-SHA256',
  21. addAuthorization: function addAuthorization(credentials, date) {
  22. var datetime = AWS.util.date.iso8601(date).replace(/[:\-]|\.\d{3}/g, '');
  23. if (this.isPresigned()) {
  24. this.updateForPresigned(credentials, datetime);
  25. } else {
  26. this.addHeaders(credentials, datetime);
  27. }
  28. this.request.headers['Authorization'] =
  29. this.authorization(credentials, datetime);
  30. },
  31. addHeaders: function addHeaders(credentials, datetime) {
  32. this.request.headers['X-Amz-Date'] = datetime;
  33. if (credentials.sessionToken) {
  34. this.request.headers['x-amz-security-token'] = credentials.sessionToken;
  35. }
  36. },
  37. updateForPresigned: function updateForPresigned(credentials, datetime) {
  38. var credString = this.credentialString(datetime);
  39. var qs = {
  40. 'X-Amz-Date': datetime,
  41. 'X-Amz-Algorithm': this.algorithm,
  42. 'X-Amz-Credential': credentials.accessKeyId + '/' + credString,
  43. 'X-Amz-Expires': this.request.headers[expiresHeader],
  44. 'X-Amz-SignedHeaders': this.signedHeaders()
  45. };
  46. if (credentials.sessionToken) {
  47. qs['X-Amz-Security-Token'] = credentials.sessionToken;
  48. }
  49. if (this.request.headers['Content-Type']) {
  50. qs['Content-Type'] = this.request.headers['Content-Type'];
  51. }
  52. if (this.request.headers['Content-MD5']) {
  53. qs['Content-MD5'] = this.request.headers['Content-MD5'];
  54. }
  55. if (this.request.headers['Cache-Control']) {
  56. qs['Cache-Control'] = this.request.headers['Cache-Control'];
  57. }
  58. // need to pull in any other X-Amz-* headers
  59. AWS.util.each.call(this, this.request.headers, function(key, value) {
  60. if (key === expiresHeader) return;
  61. if (this.isSignableHeader(key)) {
  62. var lowerKey = key.toLowerCase();
  63. // Metadata should be normalized
  64. if (lowerKey.indexOf('x-amz-meta-') === 0) {
  65. qs[lowerKey] = value;
  66. } else if (lowerKey.indexOf('x-amz-') === 0) {
  67. qs[key] = value;
  68. }
  69. }
  70. });
  71. var sep = this.request.path.indexOf('?') >= 0 ? '&' : '?';
  72. this.request.path += sep + AWS.util.queryParamsToString(qs);
  73. },
  74. authorization: function authorization(credentials, datetime) {
  75. var parts = [];
  76. var credString = this.credentialString(datetime);
  77. parts.push(this.algorithm + ' Credential=' +
  78. credentials.accessKeyId + '/' + credString);
  79. parts.push('SignedHeaders=' + this.signedHeaders());
  80. parts.push('Signature=' + this.signature(credentials, datetime));
  81. return parts.join(', ');
  82. },
  83. signature: function signature(credentials, datetime) {
  84. var signingKey = v4Credentials.getSigningKey(
  85. credentials,
  86. datetime.substr(0, 8),
  87. this.request.region,
  88. this.serviceName,
  89. this.signatureCache
  90. );
  91. return AWS.util.crypto.hmac(signingKey, this.stringToSign(datetime), 'hex');
  92. },
  93. stringToSign: function stringToSign(datetime) {
  94. var parts = [];
  95. parts.push('AWS4-HMAC-SHA256');
  96. parts.push(datetime);
  97. parts.push(this.credentialString(datetime));
  98. parts.push(this.hexEncodedHash(this.canonicalString()));
  99. return parts.join('\n');
  100. },
  101. canonicalString: function canonicalString() {
  102. var parts = [], pathname = this.request.pathname();
  103. if (this.serviceName !== 's3' && this.signatureVersion !== 's3v4') pathname = AWS.util.uriEscapePath(pathname);
  104. parts.push(this.request.method);
  105. parts.push(pathname);
  106. parts.push(this.request.search());
  107. parts.push(this.canonicalHeaders() + '\n');
  108. parts.push(this.signedHeaders());
  109. parts.push(this.hexEncodedBodyHash());
  110. return parts.join('\n');
  111. },
  112. canonicalHeaders: function canonicalHeaders() {
  113. var headers = [];
  114. AWS.util.each.call(this, this.request.headers, function (key, item) {
  115. headers.push([key, item]);
  116. });
  117. headers.sort(function (a, b) {
  118. return a[0].toLowerCase() < b[0].toLowerCase() ? -1 : 1;
  119. });
  120. var parts = [];
  121. AWS.util.arrayEach.call(this, headers, function (item) {
  122. var key = item[0].toLowerCase();
  123. if (this.isSignableHeader(key)) {
  124. var value = item[1];
  125. if (typeof value === 'undefined' || value === null || typeof value.toString !== 'function') {
  126. throw AWS.util.error(new Error('Header ' + key + ' contains invalid value'), {
  127. code: 'InvalidHeader'
  128. });
  129. }
  130. parts.push(key + ':' +
  131. this.canonicalHeaderValues(value.toString()));
  132. }
  133. });
  134. return parts.join('\n');
  135. },
  136. canonicalHeaderValues: function canonicalHeaderValues(values) {
  137. return values.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '');
  138. },
  139. signedHeaders: function signedHeaders() {
  140. var keys = [];
  141. AWS.util.each.call(this, this.request.headers, function (key) {
  142. key = key.toLowerCase();
  143. if (this.isSignableHeader(key)) keys.push(key);
  144. });
  145. return keys.sort().join(';');
  146. },
  147. credentialString: function credentialString(datetime) {
  148. return v4Credentials.createScope(
  149. datetime.substr(0, 8),
  150. this.request.region,
  151. this.serviceName
  152. );
  153. },
  154. hexEncodedHash: function hash(string) {
  155. return AWS.util.crypto.sha256(string, 'hex');
  156. },
  157. hexEncodedBodyHash: function hexEncodedBodyHash() {
  158. var request = this.request;
  159. if (this.isPresigned() && (['s3', 's3-object-lambda'].indexOf(this.serviceName) > -1) && !request.body) {
  160. return 'UNSIGNED-PAYLOAD';
  161. } else if (request.headers['X-Amz-Content-Sha256']) {
  162. return request.headers['X-Amz-Content-Sha256'];
  163. } else {
  164. return this.hexEncodedHash(this.request.body || '');
  165. }
  166. },
  167. unsignableHeaders: [
  168. 'authorization',
  169. 'content-type',
  170. 'content-length',
  171. 'user-agent',
  172. expiresHeader,
  173. 'expect',
  174. 'x-amzn-trace-id'
  175. ],
  176. isSignableHeader: function isSignableHeader(key) {
  177. if (key.toLowerCase().indexOf('x-amz-') === 0) return true;
  178. return this.unsignableHeaders.indexOf(key) < 0;
  179. },
  180. isPresigned: function isPresigned() {
  181. return this.request.headers[expiresHeader] ? true : false;
  182. }
  183. });
  184. /**
  185. * @api private
  186. */
  187. module.exports = AWS.Signers.V4;