[SignalingServer] Optimize dependent modules
[platform/framework/web/wrtjs.git] / device_home / node_modules / xmlhttprequest-ssl / lib / XMLHttpRequest.js
1 /**
2  * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
3  *
4  * This can be used with JS designed for browsers to improve reuse of code and
5  * allow the use of existing libraries.
6  *
7  * Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.
8  *
9  * @author Dan DeFelippi <dan@driverdan.com>
10  * @contributor David Ellis <d.f.ellis@ieee.org>
11  * @license MIT
12  */
13
14 var fs = require('fs');
15 var Url = require('url');
16 var spawn = require('child_process').spawn;
17
18 /**
19  * Module exports.
20  */
21
22 module.exports = XMLHttpRequest;
23
24 // backwards-compat
25 XMLHttpRequest.XMLHttpRequest = XMLHttpRequest;
26
27 /**
28  * `XMLHttpRequest` constructor.
29  *
30  * Supported options for the `opts` object are:
31  *
32  *  - `agent`: An http.Agent instance; http.globalAgent may be used; if 'undefined', agent usage is disabled
33  *
34  * @param {Object} opts optional "options" object
35  */
36
37 function XMLHttpRequest(opts) {
38   "use strict";
39
40   opts = opts || {};
41
42   /**
43    * Private variables
44    */
45   var self = this;
46   var http = require('http');
47   var https = require('https');
48
49   // Holds http.js objects
50   var request;
51   var response;
52
53   // Request settings
54   var settings = {};
55
56   // Disable header blacklist.
57   // Not part of XHR specs.
58   var disableHeaderCheck = false;
59
60   // Set some default headers
61   var defaultHeaders = {
62     "User-Agent": "node-XMLHttpRequest",
63     "Accept": "*/*"
64   };
65
66   var headers = Object.assign({}, defaultHeaders);
67
68   // These headers are not user setable.
69   // The following are allowed but banned in the spec:
70   // * user-agent
71   var forbiddenRequestHeaders = [
72     "accept-charset",
73     "accept-encoding",
74     "access-control-request-headers",
75     "access-control-request-method",
76     "connection",
77     "content-length",
78     "content-transfer-encoding",
79     "cookie",
80     "cookie2",
81     "date",
82     "expect",
83     "host",
84     "keep-alive",
85     "origin",
86     "referer",
87     "te",
88     "trailer",
89     "transfer-encoding",
90     "upgrade",
91     "via"
92   ];
93
94   // These request methods are not allowed
95   var forbiddenRequestMethods = [
96     "TRACE",
97     "TRACK",
98     "CONNECT"
99   ];
100
101   // Send flag
102   var sendFlag = false;
103   // Error flag, used when errors occur or abort is called
104   var errorFlag = false;
105
106   // Event listeners
107   var listeners = {};
108
109   /**
110    * Constants
111    */
112
113   this.UNSENT = 0;
114   this.OPENED = 1;
115   this.HEADERS_RECEIVED = 2;
116   this.LOADING = 3;
117   this.DONE = 4;
118
119   /**
120    * Public vars
121    */
122
123   // Current state
124   this.readyState = this.UNSENT;
125
126   // default ready state change handler in case one is not set or is set late
127   this.onreadystatechange = null;
128
129   // Result & response
130   this.responseText = "";
131   this.responseXML = "";
132   this.status = null;
133   this.statusText = null;
134
135   /**
136    * Private methods
137    */
138
139   /**
140    * Check if the specified header is allowed.
141    *
142    * @param string header Header to validate
143    * @return boolean False if not allowed, otherwise true
144    */
145   var isAllowedHttpHeader = function(header) {
146     return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1);
147   };
148
149   /**
150    * Check if the specified method is allowed.
151    *
152    * @param string method Request method to validate
153    * @return boolean False if not allowed, otherwise true
154    */
155   var isAllowedHttpMethod = function(method) {
156     return (method && forbiddenRequestMethods.indexOf(method) === -1);
157   };
158
159   /**
160    * Public methods
161    */
162
163   /**
164    * Open the connection. Currently supports local server requests.
165    *
166    * @param string method Connection method (eg GET, POST)
167    * @param string url URL for the connection.
168    * @param boolean async Asynchronous connection. Default is true.
169    * @param string user Username for basic authentication (optional)
170    * @param string password Password for basic authentication (optional)
171    */
172   this.open = function(method, url, async, user, password) {
173     this.abort();
174     errorFlag = false;
175
176     // Check for valid request method
177     if (!isAllowedHttpMethod(method)) {
178       throw "SecurityError: Request method not allowed";
179     }
180
181     settings = {
182       "method": method,
183       "url": url.toString(),
184       "async": (typeof async !== "boolean" ? true : async),
185       "user": user || null,
186       "password": password || null
187     };
188
189     setState(this.OPENED);
190   };
191
192   /**
193    * Disables or enables isAllowedHttpHeader() check the request. Enabled by default.
194    * This does not conform to the W3C spec.
195    *
196    * @param boolean state Enable or disable header checking.
197    */
198   this.setDisableHeaderCheck = function(state) {
199     disableHeaderCheck = state;
200   };
201
202   /**
203    * Sets a header for the request.
204    *
205    * @param string header Header name
206    * @param string value Header value
207    * @return boolean Header added
208    */
209   this.setRequestHeader = function(header, value) {
210     if (this.readyState != this.OPENED) {
211       throw "INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN";
212       return false;
213     }
214     if (!isAllowedHttpHeader(header)) {
215       console.warn('Refused to set unsafe header "' + header + '"');
216       return false;
217     }
218     if (sendFlag) {
219       throw "INVALID_STATE_ERR: send flag is true";
220       return false;
221     }
222     headers[header] = value;
223     return true;
224   };
225
226   /**
227    * Gets a header from the server response.
228    *
229    * @param string header Name of header to get.
230    * @return string Text of the header or null if it doesn't exist.
231    */
232   this.getResponseHeader = function(header) {
233     if (typeof header === "string"
234       && this.readyState > this.OPENED
235       && response.headers[header.toLowerCase()]
236       && !errorFlag
237     ) {
238       return response.headers[header.toLowerCase()];
239     }
240
241     return null;
242   };
243
244   /**
245    * Gets all the response headers.
246    *
247    * @return string A string with all response headers separated by CR+LF
248    */
249   this.getAllResponseHeaders = function() {
250     if (this.readyState < this.HEADERS_RECEIVED || errorFlag) {
251       return "";
252     }
253     var result = "";
254
255     for (var i in response.headers) {
256       // Cookie headers are excluded
257       if (i !== "set-cookie" && i !== "set-cookie2") {
258         result += i + ": " + response.headers[i] + "\r\n";
259       }
260     }
261     return result.substr(0, result.length - 2);
262   };
263
264   /**
265    * Gets a request header
266    *
267    * @param string name Name of header to get
268    * @return string Returns the request header or empty string if not set
269    */
270   this.getRequestHeader = function(name) {
271     // @TODO Make this case insensitive
272     if (typeof name === "string" && headers[name]) {
273       return headers[name];
274     }
275
276     return "";
277   };
278
279   /**
280    * Sends the request to the server.
281    *
282    * @param string data Optional data to send as request body.
283    */
284   this.send = function(data) {
285     if (this.readyState != this.OPENED) {
286       throw "INVALID_STATE_ERR: connection must be opened before send() is called";
287     }
288
289     if (sendFlag) {
290       throw "INVALID_STATE_ERR: send has already been called";
291     }
292
293     var ssl = false, local = false;
294     var url = Url.parse(settings.url);
295     var host;
296     // Determine the server
297     switch (url.protocol) {
298       case 'https:':
299         ssl = true;
300         // SSL & non-SSL both need host, no break here.
301       case 'http:':
302         host = url.hostname;
303         break;
304
305       case 'file:':
306         local = true;
307         break;
308
309       case undefined:
310       case '':
311         host = "localhost";
312         break;
313
314       default:
315         throw "Protocol not supported.";
316     }
317
318     // Load files off the local filesystem (file://)
319     if (local) {
320       if (settings.method !== "GET") {
321         throw "XMLHttpRequest: Only GET method is supported";
322       }
323
324       if (settings.async) {
325         fs.readFile(url.pathname, 'utf8', function(error, data) {
326           if (error) {
327             self.handleError(error);
328           } else {
329             self.status = 200;
330             self.responseText = data;
331             setState(self.DONE);
332           }
333         });
334       } else {
335         try {
336           this.responseText = fs.readFileSync(url.pathname, 'utf8');
337           this.status = 200;
338           setState(self.DONE);
339         } catch(e) {
340           this.handleError(e);
341         }
342       }
343
344       return;
345     }
346
347     // Default to port 80. If accessing localhost on another port be sure
348     // to use http://localhost:port/path
349     var port = url.port || (ssl ? 443 : 80);
350     // Add query string if one is used
351     var uri = url.pathname + (url.search ? url.search : '');
352
353     // Set the Host header or the server may reject the request
354     headers["Host"] = host;
355     if (!((ssl && port === 443) || port === 80)) {
356       headers["Host"] += ':' + url.port;
357     }
358
359     // Set Basic Auth if necessary
360     if (settings.user) {
361       if (typeof settings.password == "undefined") {
362         settings.password = "";
363       }
364       var authBuf = new Buffer(settings.user + ":" + settings.password);
365       headers["Authorization"] = "Basic " + authBuf.toString("base64");
366     }
367
368     // Set content length header
369     if (settings.method === "GET" || settings.method === "HEAD") {
370       data = null;
371     } else if (data) {
372       headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
373
374       if (!headers["Content-Type"]) {
375         headers["Content-Type"] = "text/plain;charset=UTF-8";
376       }
377     } else if (settings.method === "POST") {
378       // For a post with no data set Content-Length: 0.
379       // This is required by buggy servers that don't meet the specs.
380       headers["Content-Length"] = 0;
381     }
382
383     var agent = opts.agent || false;
384     var options = {
385       host: host,
386       port: port,
387       path: uri,
388       method: settings.method,
389       headers: headers,
390       agent: agent
391     };
392
393     if (ssl) {
394       options.pfx = opts.pfx;
395       options.key = opts.key;
396       options.passphrase = opts.passphrase;
397       options.cert = opts.cert;
398       options.ca = opts.ca;
399       options.ciphers = opts.ciphers;
400       options.rejectUnauthorized = opts.rejectUnauthorized;
401     }
402
403     // Reset error flag
404     errorFlag = false;
405
406     // Handle async requests
407     if (settings.async) {
408       // Use the proper protocol
409       var doRequest = ssl ? https.request : http.request;
410
411       // Request is being sent, set send flag
412       sendFlag = true;
413
414       // As per spec, this is called here for historical reasons.
415       self.dispatchEvent("readystatechange");
416
417       // Handler for the response
418       var responseHandler = function(resp) {
419         // Set response var to the response we got back
420         // This is so it remains accessable outside this scope
421         response = resp;
422         // Check for redirect
423         // @TODO Prevent looped redirects
424         if (response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
425           // Change URL to the redirect location
426           settings.url = response.headers.location;
427           var url = Url.parse(settings.url);
428           // Set host var in case it's used later
429           host = url.hostname;
430           // Options for the new request
431           var newOptions = {
432             hostname: url.hostname,
433             port: url.port,
434             path: url.path,
435             method: response.statusCode === 303 ? 'GET' : settings.method,
436             headers: headers
437           };
438
439           if (ssl) {
440             newOptions.pfx = opts.pfx;
441             newOptions.key = opts.key;
442             newOptions.passphrase = opts.passphrase;
443             newOptions.cert = opts.cert;
444             newOptions.ca = opts.ca;
445             newOptions.ciphers = opts.ciphers;
446             newOptions.rejectUnauthorized = opts.rejectUnauthorized;
447           }
448
449           // Issue the new request
450           request = doRequest(newOptions, responseHandler).on('error', errorHandler);
451           request.end();
452           // @TODO Check if an XHR event needs to be fired here
453           return;
454         }
455
456         if (response && response.setEncoding) {
457           response.setEncoding("utf8");
458         }
459
460         setState(self.HEADERS_RECEIVED);
461         self.status = response.statusCode;
462
463         response.on('data', function(chunk) {
464           // Make sure there's some data
465           if (chunk) {
466             self.responseText += chunk;
467           }
468           // Don't emit state changes if the connection has been aborted.
469           if (sendFlag) {
470             setState(self.LOADING);
471           }
472         });
473
474         response.on('end', function() {
475           if (sendFlag) {
476             // The sendFlag needs to be set before setState is called.  Otherwise if we are chaining callbacks
477             // there can be a timing issue (the callback is called and a new call is made before the flag is reset).
478             sendFlag = false;
479             // Discard the 'end' event if the connection has been aborted
480             setState(self.DONE);
481           }
482         });
483
484         response.on('error', function(error) {
485           self.handleError(error);
486         });
487       }
488
489       // Error handler for the request
490       var errorHandler = function(error) {
491         self.handleError(error);
492       }
493
494       // Create the request
495       request = doRequest(options, responseHandler).on('error', errorHandler);
496
497       // Node 0.4 and later won't accept empty data. Make sure it's needed.
498       if (data) {
499         request.write(data);
500       }
501
502       request.end();
503
504       self.dispatchEvent("loadstart");
505     } else { // Synchronous
506       // Create a temporary file for communication with the other Node process
507       var contentFile = ".node-xmlhttprequest-content-" + process.pid;
508       var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
509       fs.writeFileSync(syncFile, "", "utf8");
510       // The async request the other Node process executes
511       var execString = "var http = require('http'), https = require('https'), fs = require('fs');"
512         + "var doRequest = http" + (ssl ? "s" : "") + ".request;"
513         + "var options = " + JSON.stringify(options) + ";"
514         + "var responseText = '';"
515         + "var req = doRequest(options, function(response) {"
516         + "response.setEncoding('utf8');"
517         + "response.on('data', function(chunk) {"
518         + "  responseText += chunk;"
519         + "});"
520         + "response.on('end', function() {"
521         + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');"
522         + "fs.unlinkSync('" + syncFile + "');"
523         + "});"
524         + "response.on('error', function(error) {"
525         + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
526         + "fs.unlinkSync('" + syncFile + "');"
527         + "});"
528         + "}).on('error', function(error) {"
529         + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
530         + "fs.unlinkSync('" + syncFile + "');"
531         + "});"
532         + (data ? "req.write('" + data.replace(/'/g, "\\'") + "');":"")
533         + "req.end();";
534       // Start the other Node Process, executing this string
535       var syncProc = spawn(process.argv[0], ["-e", execString]);
536       var statusText;
537       while(fs.existsSync(syncFile)) {
538         // Wait while the sync file is empty
539       }
540       self.responseText = fs.readFileSync(contentFile, 'utf8');
541       // Kill the child process once the file has data
542       syncProc.stdin.end();
543       // Remove the temporary file
544       fs.unlinkSync(contentFile);
545       if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
546         // If the file returned an error, handle it
547         var errorObj = self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "");
548         self.handleError(errorObj);
549       } else {
550         // If the file returned okay, parse its data and move to the DONE state
551         self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1");
552         self.responseText = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1");
553         setState(self.DONE);
554       }
555     }
556   };
557
558   /**
559    * Called when an error is encountered to deal with it.
560    */
561   this.handleError = function(error) {
562     this.status = 503;
563     this.statusText = error;
564     this.responseText = error.stack;
565     errorFlag = true;
566     setState(this.DONE);
567   };
568
569   /**
570    * Aborts a request.
571    */
572   this.abort = function() {
573     if (request) {
574       request.abort();
575       request = null;
576     }
577
578     headers = Object.assign({}, defaultHeaders);
579     this.responseText = "";
580     this.responseXML = "";
581
582     errorFlag = true;
583
584     if (this.readyState !== this.UNSENT
585         && (this.readyState !== this.OPENED || sendFlag)
586         && this.readyState !== this.DONE) {
587       sendFlag = false;
588       setState(this.DONE);
589     }
590     this.readyState = this.UNSENT;
591   };
592
593   /**
594    * Adds an event listener. Preferred method of binding to events.
595    */
596   this.addEventListener = function(event, callback) {
597     if (!(event in listeners)) {
598       listeners[event] = [];
599     }
600     // Currently allows duplicate callbacks. Should it?
601     listeners[event].push(callback);
602   };
603
604   /**
605    * Remove an event callback that has already been bound.
606    * Only works on the matching funciton, cannot be a copy.
607    */
608   this.removeEventListener = function(event, callback) {
609     if (event in listeners) {
610       // Filter will return a new array with the callback removed
611       listeners[event] = listeners[event].filter(function(ev) {
612         return ev !== callback;
613       });
614     }
615   };
616
617   /**
618    * Dispatch any events, including both "on" methods and events attached using addEventListener.
619    */
620   this.dispatchEvent = function(event) {
621     if (typeof self["on" + event] === "function") {
622       self["on" + event]();
623     }
624     if (event in listeners) {
625       for (var i = 0, len = listeners[event].length; i < len; i++) {
626         listeners[event][i].call(self);
627       }
628     }
629   };
630
631   /**
632    * Changes readyState and calls onreadystatechange.
633    *
634    * @param int state New state
635    */
636   var setState = function(state) {
637     if (self.readyState !== state) {
638       self.readyState = state;
639
640       if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) {
641         self.dispatchEvent("readystatechange");
642       }
643
644       if (self.readyState === self.DONE && !errorFlag) {
645         self.dispatchEvent("load");
646         // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie)
647         self.dispatchEvent("loadend");
648       }
649     }
650   };
651 };