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