1 /**
  2  * @fileOverview AWS Authentication.
  3  */
  4 
  5 (function () {
  6   // Requires.
  7   var http = require('http'),
  8     crypto = require('crypto'),
  9     parse = require('url').parse,
 10     util = require('util'),
 11     utils = require("../../utils"),
 12     BaseAuthentication = require("../../base/authentication").Authentication,
 13     AwsAuthentication;
 14 
 15   /**
 16    * AWS Authentication class.
 17    *
 18    * *See*: http://docs.amazonwebservices.com/AmazonS3/latest/API/
 19    *
 20    * @param  {Object}   options     Options object.
 21    * @config {string}   account     Account name.
 22    * @config {string}   secretKey   Secret key.
 23    * @config {string}   [ssl=false] Use SSL?
 24    * @config {string}   [authUrl]   Authentication URL.
 25    * @config {number}   [timeout]   HTTP timeout in seconds.
 26    * @exports AwsAuthentication as provider.aws.Authentication
 27    * @extends base.Authentication
 28    * @constructor
 29    */
 30   AwsAuthentication = function (options) {
 31     // Patch AWS-specific options.
 32     options.authUrl = options.authUrl || "s3.amazonaws.com";
 33 
 34     // Call superclass.
 35     BaseAuthentication.call(this, options);
 36 
 37     this._CUSTOM_HEADER_PREFIX = "x-amz";
 38     this._CUSTOM_HEADER_RE = /^x-amz-/i;
 39     this._SIGNATURE_ID = "AWS";
 40     this._CONN_CLS = require("./connection").Connection;
 41   };
 42 
 43   util.inherits(AwsAuthentication, BaseAuthentication);
 44 
 45   /** Test provider (AWS). */
 46   AwsAuthentication.prototype.isAws = function () {
 47     return true;
 48   };
 49 
 50   /**
 51    * Return canonical AMZ headers.
 52    *
 53    * > "x-amz headers are canonicalized by:
 54    * > Lower-case header name
 55    * > Headers sorted by header name
 56    * > The values of headers whose names occur more than once should be white
 57    * > space-trimmed and concatenated with comma separators to be compliant
 58    * > with section 4.2 of RFC 2616.
 59    * > remove any whitespace around the colon in the header
 60    * > remove any newlines ('\n') in continuation lines
 61    * > separate headers by newlines ('\n')"
 62    *
 63    * @see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html
 64    */
 65   AwsAuthentication.prototype._getCanonicalHeaders = function (headers) {
 66     var canonHeaders = [],
 67       header,
 68       value,
 69       customHeaders;
 70 
 71     // Extract all Custom headers.
 72     for (header in headers) {
 73       if (headers.hasOwnProperty(header)) {
 74         if (this._CUSTOM_HEADER_RE.test(header)) {
 75           // Extract value and flatten.
 76           value = headers[header];
 77           if (Array.isArray(value)) {
 78             value = value.join(',');
 79           }
 80 
 81           canonHeaders.push(header.toString().toLowerCase() + ":" + value);
 82         }
 83       }
 84     }
 85 
 86     // Sort and create string.
 87     return canonHeaders.sort().join("\n");
 88   };
 89 
 90   /**
 91    * Return canonical AMZ resource.
 92    *
 93    * > "The resource is the bucket and key (if applicable), separated by a '/'.
 94    * > If the request you are signing is for an ACL or a torrent file, you
 95    * > should include ?acl or ?torrent in the resource part of the canonical
 96    * > string. No other query string parameters should be included, however."
 97    *
 98    * @see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html
 99    */
100   AwsAuthentication.prototype._getCanonicalResource = function (
101     resource,
102     headers
103   ) {
104     // Strip off cname bucket, if present.
105     var canonResource = parse(resource, true).pathname,
106       bucketRe = new RegExp("." + this._authUrl + "$"),
107       bucketName;
108 
109     if (bucketRe.test(headers.host)) {
110       bucketName = headers.host.replace(bucketRe, '');
111       canonResource = parse("/" + bucketName + resource, true).pathname;
112     }
113 
114     return canonResource;
115   };
116 
117   /**
118    * Create string to sign.
119    *
120    * > "The string to be signed is formed by appending the REST verb,
121    * > content-md5 value, content-type value, date value, canonicalized x-amz
122    * > headers (see recipe below), and the resource; all separated by newlines.
123    * > (If you cannot set the Date header, use the x-amz-date header as
124    * > described below.)"
125    *
126    * Also:
127    *
128    * > "The content-type and content-md5 values are optional, but if you do
129    * > not include them you must still insert a newline at the point where these
130    * > values would normally be inserted."
131    *
132    * @see http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html
133    */
134   AwsAuthentication.prototype._getStringToSign = function (
135     method,
136     path,
137     headers
138   ) {
139     var customHeaders = this._getCanonicalHeaders(headers),
140       customResource = this._getCanonicalResource(path, headers),
141       parts;
142 
143     parts = [
144       method,
145       headers['content-md5'] || '',
146       headers['content-type'] || '',
147       headers['date'] ? headers['date'] : '',
148     ];
149 
150     if (customHeaders) {
151       parts.push(customHeaders);
152     }
153     if (customResource) {
154       parts.push(customResource);
155     }
156 
157     return parts.join("\n");
158   };
159 
160   /**
161    * Create signature.
162    * @private
163    */
164   function getSignature(secretKey, stringToSign) {
165     return crypto
166       .createHmac('sha1', secretKey)
167       .update(stringToSign, "utf8")
168       .digest('base64');
169   }
170 
171   /**
172    * @see base.Authentication#sign
173    */
174   AwsAuthentication.prototype.sign = function (method, path, headers) {
175     path = path || "/";
176     headers = utils.extend(this._getHeaders(headers));
177     headers['authorization'] = this._SIGNATURE_ID + " " + this._account + ":" +
178       getSignature(this._secretKey,
179                    this._getStringToSign(method, path, headers));
180 
181     return headers;
182   };
183 
184   module.exports.Authentication = AwsAuthentication;
185 }());
186