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