annotate node_modules/xmlhttprequest/lib/XMLHttpRequest.js @ 101:52e44ee1c791 tip master

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