1 /**
  2  * @fileOverview Cloud requests.
  3  *
  4  * For most of these classes, we wrap up events for both the request and
  5  * response into a single "request" class.
  6  */
  7 
  8 /**
  9  * @name request
 10  */
 11 (function () {
 12   var http = require('http'),
 13     https = require('https'),
 14     url = require('url'),
 15     util = require('util'),
 16     EventEmitter = require('events').EventEmitter,
 17     xml2js = require('xml2js'),
 18     CloudError = require("./errors").CloudError,
 19     utils = require("./utils"),
 20     Request,
 21     DummyRequest,
 22     AuthenticatedRawRequest,
 23     AuthenticatedRequest,
 24     AuthenticatedXmlRequest;
 25 
 26   function startsWith(value, prefix) {
 27     return value.slice(0, prefix.length) === prefix;
 28   }
 29 
 30   // From: http://stackoverflow.com/questions/5185326
 31   function isAscii(value) {
 32     return (/^[\000-\177]*$/).test(value);
 33   }
 34 
 35   /**
 36    * Completion event ('``end``').
 37    *
 38    * @name request.Request#event:end
 39    * @event
 40    * @param  {Object} data            Results data.
 41    * @param  {Object} meta            Headers, meta object.
 42    * @config {Object} [headers]       HTTP headers.
 43    * @config {Object} [cloudHeaders]  Cloud provider headers.
 44    * @config {Object} [metadata]      Cloud metadata.
 45    */
 46   /**
 47    * Error event ('``error``').
 48    *
 49    * @name request.Request#event:error
 50    * @event
 51    * @param {Error|errors.CloudError} err Error object.
 52    */
 53   /**
 54    * Abstract base request class.
 55    *
 56    * @param  {Object}   options       Options object.
 57    * @exports Request as request.Request
 58    * @constructor
 59    */
 60   Request = function (options) {
 61     this._ended = false;
 62   };
 63 
 64   Request.prototype = new EventEmitter();
 65 
 66   /**
 67    * End the request.
 68    *
 69    * Typically starts the async code execution.
 70    *
 71    * *Note*: This function can be called multiple times without bad effect.
 72    * Calling code has the option to call ``end()`` once the request is set
 73    * up, or leave it to the end user.
 74    */
 75   Request.prototype.end = function () {
 76     if (!this._ended) {
 77       this._end.apply(this, arguments);
 78       this._ended = true;
 79     }
 80   };
 81 
 82   /**
 83    * End implementation.
 84    */
 85   Request.prototype._end = function () {
 86     throw new Error("Not implemented.");
 87   };
 88 
 89   module.exports.Request = Request;
 90 
 91   /**
 92    * Nop dummy request wrapper class.
 93    *
 94    * @param  {Object}   options       Options object.
 95    * @config {Function} [endFn]       Function to invoke 'end' event.
 96    * @config {Function} [resultsFn]   'end' event results callback.
 97    * @config {Function} [metaFn]      'end' event meta callback.
 98    * @extends request.Request
 99    * @exports DummyRequest as request.DummyRequest
100    * @constructor
101    */
102   DummyRequest = function (options) {
103     var self = this;
104 
105     self._resultsFn = options.resultsFn || function () {};
106     self._metaFn = options.metaFn || function () {
107       return utils.extractMeta();
108     };
109 
110     self._endFn = options.endFn || function () {
111       self.emit('end', self._resultsFn(), self._metaFn());
112     };
113   };
114 
115   util.inherits(DummyRequest, Request);
116 
117   /** @see request.Request#_end */
118   DummyRequest.prototype._end = function () {
119     this._endFn();
120   };
121 
122   module.exports.DummyRequest = DummyRequest;
123 
124   /**
125    * Authenticated raw request wrapper class.
126    *
127    * @param  {base.Authentication} auth  Authentication object.
128    * @param  {Object}   options     Options object.
129    * @config {string}   [method]    HTTP method (verb).
130    * @config {string}   [path]      HTTP path.
131    * @config {Object}   [params]    HTTP path parameters.
132    * @config {Object}   [headers]   HTTP headers.
133    * @config {Object}   [cloudHeaders] Cloud provider headers to add.
134    * @config {Object}   [metadata]  Cloud metadata to add.
135    * @config {string}   [encoding]  Response encoding.
136    * @extends request.Request
137    * @exports AuthenticatedRawRequest as request.AuthenticatedRawRequest
138    * @constructor
139    */
140   AuthenticatedRawRequest = function (auth, options) {
141     options = options || {};
142     var self = this,
143       method = options.method || "GET",
144       path = options.path || "/",
145       params = options.params || {},
146       urlObj,
147       reqOpts;
148 
149     // Parse object path.
150     urlObj = url.parse(path, true);
151 
152     // Unicode: Need `encodeURIComponent` to handle utf8 characters, which are
153     // valid for AWS, etc. key names.
154     if (auth.isGoogle()) {
155       // Unfortunately, Google doesn't work with this.
156       // Throw error if non-ascii characters.
157       if (!isAscii(urlObj.pathname)) {
158         self._err = new Error(
159           "Google requires ascii path: " + urlObj.pathname);
160       }
161     } else {
162       // Encode for UTF8 support.
163       urlObj.pathname = encodeURIComponent(urlObj.pathname);
164     }
165 
166     // Patch path to add in query params.
167     if (Object.keys(params).length > 0) {
168       urlObj.query = utils.extend(urlObj.query, params);
169     }
170 
171     // Set finished, normalized path.
172     path = url.format(urlObj);
173 
174     // Sign the headers, create the request.
175     self._auth = auth;
176     self._method = method;
177     self._encoding = options.encoding || null;
178 
179     // Set headers last (so other object members are created).
180     self._headers = auth.sign(method, path, self._getHeaders(options));
181     self._protocol = auth.ssl ? https : http;
182     self._request = self._protocol.request({
183       host: auth.authUrl(),
184       port: auth.port,
185       path: path,
186       method: method,
187       headers: self._headers
188     });
189   };
190 
191   util.inherits(AuthenticatedRawRequest, Request);
192 
193   Object.defineProperties(AuthenticatedRawRequest.prototype, {
194     /**
195      * Authentication object.
196      *
197      * @name AuthenticatedRawRequest#auth
198      * @type Authentication
199      */
200     auth: {
201       get: function () {
202         return this._auth;
203       }
204     },
205 
206     /**
207      * HTTP method verb.
208      *
209      * @name AuthenticatedRawRequest#method
210      * @type string
211      */
212     method: {
213       get: function () {
214         return this._method;
215       }
216     },
217 
218     /**
219      * Real HTTP request object.
220      *
221      * @name AuthenticatedRawRequest#realRequest
222      * @type http.ClientRequest
223      */
224     realRequest: {
225       get: function () {
226         return this._request;
227       }
228     }
229   });
230 
231   /** Set request encoding. */
232   AuthenticatedRawRequest.prototype.setEncoding = function (encoding) {
233     this._encoding = encoding;
234     this._request.setEncoding(encoding);
235   };
236 
237   /** Set header. */
238   AuthenticatedRawRequest.prototype.setHeader = function (name, value) {
239     name = name ? name.toLowerCase() : null;
240     this._request.setHeader(name, value);
241     this._headers[name] = value;
242   };
243 
244   /**
245    * Return full headers from cloud headers and metadata.
246    *
247    * @param  {Object}   options     Options object.
248    * @config {Object}   [headers]   HTTP headers.
249    * @config {Object}   [cloudHeaders] Cloud provider headers to add.
250    * @config {Object}   [metadata]  Cloud metadata to add.
251    * @returns {Object}              HTTP headers.
252    * @private
253    */
254   AuthenticatedRawRequest.prototype._getHeaders = function (options) {
255     options = options || {};
256     var conn = this._auth.connection,
257       headerPrefix = conn.headerPrefix,
258       metaPrefix = conn.metadataPrefix,
259       rawHeaders = {},
260       headers = options.headers || {},
261       cloudHeaders = options.cloudHeaders || {},
262       metadata = options.metadata || {};
263 
264     // Order is metadata, cloud headers, headers.
265     Object.keys(metadata).forEach(function (header) {
266       rawHeaders[metaPrefix + header.toLowerCase()] = metadata[header];
267     });
268     Object.keys(cloudHeaders).forEach(function (header) {
269       rawHeaders[headerPrefix + header.toLowerCase()] = cloudHeaders[header];
270     });
271     Object.keys(headers).forEach(function (header) {
272       rawHeaders[header.toLowerCase()] = headers[header];
273     });
274 
275     return rawHeaders;
276   };
277 
278   /**
279    * Return separate headers, cloud headers and metadata from headers.
280    *
281    * @param  {HttpResponse} response Response object.
282    * @returns {Object} Object of headers, cloudHeaders, metadata.
283    */
284   AuthenticatedRawRequest.prototype.getMeta = function (response) {
285     var conn = this._auth.connection,
286       headerPrefix = conn.headerPrefix,
287       metaPrefix = conn.metadataPrefix,
288       rawHeaders = response.headers,
289       headers = {},
290       cloudHeaders = {},
291       metadata = {};
292 
293     // First try to get metadata, then cloud headers, then headers.
294     Object.keys(rawHeaders).forEach(function (header) {
295       var key = header.toLowerCase();
296       if (startsWith(key, metaPrefix)) {
297         key = key.substring(metaPrefix.length);
298         metadata[key] = rawHeaders[header];
299       } else if (startsWith(key, headerPrefix)) {
300         key = key.substring(headerPrefix.length);
301         cloudHeaders[key] = rawHeaders[header];
302       } else {
303         headers[key] = rawHeaders[header];
304       }
305     });
306 
307     return {
308       headers: headers,
309       cloudHeaders: cloudHeaders,
310       metadata: metadata
311     };
312   };
313 
314   /**
315    * @see request.Request#_end
316    * @private
317    */
318   AuthenticatedRawRequest.prototype._end = function () {
319     var req = this._request,
320       err = this._err;
321 
322     // Apply any stashed errors.
323     if (err) {
324       return req.emit("error", err);
325     }
326 
327     req.end.apply(req, arguments);
328   };
329 
330   module.exports.AuthenticatedRawRequest = AuthenticatedRawRequest;
331 
332   /**
333    * Authenticated request wrapper class.
334    *
335    * **Note**: Accumulates data for final 'end' event instead of passing
336    * through via typical 'data' events.
337    *
338    * @param  {base.Authentication} auth  Authentication object.
339    * @param  {Object}   options     Options object.
340    * @config {string}   [method]    HTTP method (verb).
341    * @config {string}   [path]      HTTP path.
342    * @config {Object}   [params]    HTTP path parameters.
343    * @config {Object}   [headers]   HTTP headers.
344    * @config {Object}   [cloudHeaders] Cloud provider headers to add.
345    * @config {Object}   [metadata]  Cloud metadata to add.
346    * @config {string}   [encoding]  Response encoding.
347    * @config {Function} [errorFn]   errorFn(err, request, [response])
348    *                                Error handler (stops further emission).
349    * @config {Function} [resultsFn] resultsFn(results, request, [response])
350    *                                Successful results data transform.
351    * @extends request.Request
352    * @exports AuthenticatedRequest as request.AuthenticatedRequest
353    * @constructor
354    */
355   AuthenticatedRequest = function (auth, options) {
356     var self = this;
357 
358     AuthenticatedRawRequest.apply(self, arguments);
359 
360     // Additional members.
361     self._buf = [];
362     self._resultsFn = options.resultsFn || null;
363     self._errorFn = options.errorFn || null;
364     self._handleErr = function (err, response) {
365       if (self._errorFn) {
366         self._errorFn(err, self, response);
367       } else {
368         self.emit('error', err, response);
369       }
370     };
371 
372     // Set up bindings.
373     self._request.on('error', function (err) {
374       self._handleErr(err);
375     });
376     self._request.on('response', function (response) {
377       if (self._encoding) {
378         response.setEncoding(self._encoding);
379       }
380 
381       // Shortcut: If no-data response, just return here.
382       //
383       // **Note**: For some reason, when using HTTPS for both AWS and GSFD,
384       // DELETE blob responses would not have an 'end' event. This avoids
385       // the problem by not even bothering with listening.
386       switch (response.statusCode) {
387       case 200: // Check 200 OK for no bytes.
388         if (response.headers && response.headers['content-length'] === "0") {
389           self.processResults(null, response);
390           return;
391         }
392         break;
393       case 204: // Response has no content.
394         self.processResults(null, response);
395         return;
396       default:
397         // Do nothing - continue processing.
398         break;
399       }
400 
401       // Need more processing, listen to response.
402       response.on('data', function (chunk) {
403         self._buf.push(chunk);
404       });
405       response.on('end', function () {
406         var data = null,
407           getData,
408           msg,
409           err;
410 
411         // Handle the buffer, if we have any.
412         if (self._buf.length > 0) {
413           if (self._encoding) {
414             // If encoding, then join as strings.
415             data = self._buf
416               .map(function (buf) { return buf.toString(self._encoding); })
417               .join("");
418           } else {
419             // Else, return array of buffers.
420             data = self._buf;
421           }
422         }
423 
424         switch (response.statusCode) {
425         case 200:
426           // processResults emits 'end'.
427           self.processResults(data, response);
428           break;
429         default:
430           // Everything unknown is an error.
431           msg = utils.bufToStr(self._buf, self._encoding, 'utf8');
432           err = new CloudError(msg, { response: response });
433           self._handleErr(err, response);
434         }
435       });
436     });
437   };
438 
439   util.inherits(AuthenticatedRequest, AuthenticatedRawRequest);
440 
441   /**
442    * Process data.
443    *
444    * Also emits '``end``' event on processed data.
445    */
446   AuthenticatedRequest.prototype.processResults = function (data, response) {
447     var self = this,
448       meta = self.getMeta(response),
449       results;
450 
451     results = self._resultsFn ? self._resultsFn(data, self, response) : data;
452 
453     self.emit('end', results, meta);
454   };
455 
456   module.exports.AuthenticatedRequest = AuthenticatedRequest;
457 
458   /**
459    * Authenticated request wrapper class with JSON results (from XML).
460    *
461    * **Note**: Accumulates data for final 'end' event instead of passing
462    * through via typical 'data' events.
463    *
464    * @param  {base.Authentication} auth  Authentication object.
465    * @param  {Object}   options     Options object.
466    * @config {string}   [method]    HTTP method (verb).
467    * @config {string}   [path]      HTTP path.
468    * @config {Object}   [headers]   HTTP headers.
469    * @config {Object}   [cloudHeaders] Cloud provider headers to add.
470    * @config {Object}   [metadata]  Cloud metadata to add.
471    * @config {Function} [errorFn]   errorFn(err, request, [response])
472    *                                Error handler (if return True, no further
473    *                                error handling takes place).
474    * @config {Function} [resultsFn] resultsFn(results, request, [response])
475    *                                Successful results data transform.
476    * @extends request.AuthenticatedRequest
477    * @exports AuthenticatedXmlRequest as request.AuthenticatedXmlRequest
478    * @constructor
479    */
480   AuthenticatedXmlRequest = function (auth, options) {
481     AuthenticatedRequest.apply(this, arguments);
482   };
483 
484   util.inherits(AuthenticatedXmlRequest, AuthenticatedRequest);
485 
486   /** @see request.AuthenticatedXmlRequest#processResults */
487   AuthenticatedXmlRequest.prototype.processResults = function (data, response) {
488     var self = this,
489       meta = self.getMeta(response),
490       parser = new xml2js.Parser();
491 
492     // Parse the XML response to JSON.
493     parser.on('end', function (data) {
494       var results = self._resultsFn
495         ? self._resultsFn(data, self, response)
496         : data;
497       self.emit('end', results, meta);
498     });
499     parser.on('error', function (err) {
500       self.emit('error', err, response);
501     });
502 
503     parser.parseString(data);
504   };
505 
506   module.exports.AuthenticatedXmlRequest = AuthenticatedXmlRequest;
507 }());
508