signer.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. var AWS = require('../core'),
  2. url = AWS.util.url,
  3. crypto = AWS.util.crypto.lib,
  4. base64Encode = AWS.util.base64.encode,
  5. inherit = AWS.util.inherit;
  6. var queryEncode = function (string) {
  7. var replacements = {
  8. '+': '-',
  9. '=': '_',
  10. '/': '~'
  11. };
  12. return string.replace(/[\+=\/]/g, function (match) {
  13. return replacements[match];
  14. });
  15. };
  16. var signPolicy = function (policy, privateKey) {
  17. var sign = crypto.createSign('RSA-SHA1');
  18. sign.write(policy);
  19. return queryEncode(sign.sign(privateKey, 'base64'));
  20. };
  21. var signWithCannedPolicy = function (url, expires, keyPairId, privateKey) {
  22. var policy = JSON.stringify({
  23. Statement: [
  24. {
  25. Resource: url,
  26. Condition: { DateLessThan: { 'AWS:EpochTime': expires } }
  27. }
  28. ]
  29. });
  30. return {
  31. Expires: expires,
  32. 'Key-Pair-Id': keyPairId,
  33. Signature: signPolicy(policy.toString(), privateKey)
  34. };
  35. };
  36. var signWithCustomPolicy = function (policy, keyPairId, privateKey) {
  37. policy = policy.replace(/\s/mg, '');
  38. return {
  39. Policy: queryEncode(base64Encode(policy)),
  40. 'Key-Pair-Id': keyPairId,
  41. Signature: signPolicy(policy, privateKey)
  42. };
  43. };
  44. var determineScheme = function (url) {
  45. var parts = url.split('://');
  46. if (parts.length < 2) {
  47. throw new Error('Invalid URL.');
  48. }
  49. return parts[0].replace('*', '');
  50. };
  51. var getRtmpUrl = function (rtmpUrl) {
  52. var parsed = url.parse(rtmpUrl);
  53. return parsed.path.replace(/^\//, '') + (parsed.hash || '');
  54. };
  55. var getResource = function (url) {
  56. switch (determineScheme(url)) {
  57. case 'http':
  58. case 'https':
  59. return url;
  60. case 'rtmp':
  61. return getRtmpUrl(url);
  62. default:
  63. throw new Error('Invalid URI scheme. Scheme must be one of'
  64. + ' http, https, or rtmp');
  65. }
  66. };
  67. var handleError = function (err, callback) {
  68. if (!callback || typeof callback !== 'function') {
  69. throw err;
  70. }
  71. callback(err);
  72. };
  73. var handleSuccess = function (result, callback) {
  74. if (!callback || typeof callback !== 'function') {
  75. return result;
  76. }
  77. callback(null, result);
  78. };
  79. AWS.CloudFront.Signer = inherit({
  80. /**
  81. * A signer object can be used to generate signed URLs and cookies for granting
  82. * access to content on restricted CloudFront distributions.
  83. *
  84. * @see http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html
  85. *
  86. * @param keyPairId [String] (Required) The ID of the CloudFront key pair
  87. * being used.
  88. * @param privateKey [String] (Required) A private key in RSA format.
  89. */
  90. constructor: function Signer(keyPairId, privateKey) {
  91. if (keyPairId === void 0 || privateKey === void 0) {
  92. throw new Error('A key pair ID and private key are required');
  93. }
  94. this.keyPairId = keyPairId;
  95. this.privateKey = privateKey;
  96. },
  97. /**
  98. * Create a signed Amazon CloudFront Cookie.
  99. *
  100. * @param options [Object] The options to create a signed cookie.
  101. * @option options url [String] The URL to which the signature will grant
  102. * access. Required unless you pass in a full
  103. * policy.
  104. * @option options expires [Number] A Unix UTC timestamp indicating when the
  105. * signature should expire. Required unless you
  106. * pass in a full policy.
  107. * @option options policy [String] A CloudFront JSON policy. Required unless
  108. * you pass in a url and an expiry time.
  109. *
  110. * @param cb [Function] if a callback is provided, this function will
  111. * pass the hash as the second parameter (after the error parameter) to
  112. * the callback function.
  113. *
  114. * @return [Object] if called synchronously (with no callback), returns the
  115. * signed cookie parameters.
  116. * @return [null] nothing is returned if a callback is provided.
  117. */
  118. getSignedCookie: function (options, cb) {
  119. var signatureHash = 'policy' in options
  120. ? signWithCustomPolicy(options.policy, this.keyPairId, this.privateKey)
  121. : signWithCannedPolicy(options.url, options.expires, this.keyPairId, this.privateKey);
  122. var cookieHash = {};
  123. for (var key in signatureHash) {
  124. if (Object.prototype.hasOwnProperty.call(signatureHash, key)) {
  125. cookieHash['CloudFront-' + key] = signatureHash[key];
  126. }
  127. }
  128. return handleSuccess(cookieHash, cb);
  129. },
  130. /**
  131. * Create a signed Amazon CloudFront URL.
  132. *
  133. * Keep in mind that URLs meant for use in media/flash players may have
  134. * different requirements for URL formats (e.g. some require that the
  135. * extension be removed, some require the file name to be prefixed
  136. * - mp4:<path>, some require you to add "/cfx/st" into your URL).
  137. *
  138. * @param options [Object] The options to create a signed URL.
  139. * @option options url [String] The URL to which the signature will grant
  140. * access. Any query params included with
  141. * the URL should be encoded. Required.
  142. * @option options expires [Number] A Unix UTC timestamp indicating when the
  143. * signature should expire. Required unless you
  144. * pass in a full policy.
  145. * @option options policy [String] A CloudFront JSON policy. Required unless
  146. * you pass in a url and an expiry time.
  147. *
  148. * @param cb [Function] if a callback is provided, this function will
  149. * pass the URL as the second parameter (after the error parameter) to
  150. * the callback function.
  151. *
  152. * @return [String] if called synchronously (with no callback), returns the
  153. * signed URL.
  154. * @return [null] nothing is returned if a callback is provided.
  155. */
  156. getSignedUrl: function (options, cb) {
  157. try {
  158. var resource = getResource(options.url);
  159. } catch (err) {
  160. return handleError(err, cb);
  161. }
  162. var parsedUrl = url.parse(options.url, true),
  163. signatureHash = Object.prototype.hasOwnProperty.call(options, 'policy')
  164. ? signWithCustomPolicy(options.policy, this.keyPairId, this.privateKey)
  165. : signWithCannedPolicy(resource, options.expires, this.keyPairId, this.privateKey);
  166. parsedUrl.search = null;
  167. for (var key in signatureHash) {
  168. if (Object.prototype.hasOwnProperty.call(signatureHash, key)) {
  169. parsedUrl.query[key] = signatureHash[key];
  170. }
  171. }
  172. try {
  173. var signedUrl = determineScheme(options.url) === 'rtmp'
  174. ? getRtmpUrl(url.format(parsedUrl))
  175. : url.format(parsedUrl);
  176. } catch (err) {
  177. return handleError(err, cb);
  178. }
  179. return handleSuccess(signedUrl, cb);
  180. }
  181. });
  182. /**
  183. * @api private
  184. */
  185. module.exports = AWS.CloudFront.Signer;