├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── autotest.watchr ├── example └── demo.js ├── lib ├── XMLHttpRequest.js └── browser.js ├── package.json └── tests ├── test-constants.js ├── test-events.js ├── test-exceptions.js ├── test-headers.js ├── test-redirect-301.js ├── test-redirect-302.js ├── test-redirect-303.js ├── test-redirect-307.js ├── test-request-methods.js ├── test-request-protocols.js ├── test-streaming.js └── testdata.txt /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "esnext": true, 5 | "bitwise": false, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "eqnull": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "laxbreak": true, 14 | "newcap": true, 15 | "noarg": true, 16 | "quotmark": "double", 17 | "regexp": true, 18 | "undef": true, 19 | "unused": true, 20 | "strict": true, 21 | "trailing": true, 22 | "smarttabs": true, 23 | "globals": { 24 | "define": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | tests 3 | 4 | autotest.watchr 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 passive.ly LLC 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-XMLHttpRequest # 2 | 3 | node-XMLHttpRequest is a wrapper for the built-in http client to emulate the 4 | browser XMLHttpRequest object. 5 | 6 | This can be used with JS designed for browsers to improve reuse of code and 7 | allow the use of existing libraries. 8 | 9 | Note: This library currently conforms to [XMLHttpRequest 1](http://www.w3.org/TR/XMLHttpRequest/). Version 2.0 will target [XMLHttpRequest Level 2](http://www.w3.org/TR/XMLHttpRequest2/). 10 | 11 | ## Usage ## 12 | 13 | Here's how to include the module in your project and use as the browser-based 14 | XHR object. 15 | 16 | var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; 17 | var xhr = new XMLHttpRequest(); 18 | 19 | Note: use the lowercase string "xmlhttprequest" in your require(). On 20 | case-sensitive systems (eg Linux) using uppercase letters won't work. 21 | 22 | ## Versions ## 23 | 24 | Prior to 1.4.0 version numbers were arbitrary. From 1.4.0 on they conform to 25 | the standard major.minor.bugfix. 1.x shouldn't necessarily be considered 26 | stable just because it's above 0.x. 27 | 28 | Since the XMLHttpRequest API is stable this library's API is stable as 29 | well. Major version numbers indicate significant core code changes. 30 | Minor versions indicate minor core code changes or better conformity to 31 | the W3C spec. 32 | 33 | ## License ## 34 | 35 | MIT license. See LICENSE for full details. 36 | 37 | ## Supports ## 38 | 39 | * Async and synchronous requests 40 | * GET, POST, PUT, and DELETE requests 41 | * All spec methods (open, send, abort, getRequestHeader, 42 | getAllRequestHeaders, event methods) 43 | * Requests to all domains 44 | 45 | ## Known Issues / Missing Features ## 46 | 47 | For a list of open issues or to report your own visit the [github issues 48 | page](https://github.com/driverdan/node-XMLHttpRequest/issues). 49 | 50 | * Local file access may have unexpected results for non-UTF8 files 51 | * Synchronous requests don't set headers properly 52 | * Synchronous requests freeze node while waiting for response (But that's what you want, right? Stick with async!). 53 | * Some events are missing, such as abort 54 | * Cookies aren't persisted between requests 55 | * Missing XML support 56 | -------------------------------------------------------------------------------- /autotest.watchr: -------------------------------------------------------------------------------- 1 | def run_all_tests 2 | puts `clear` 3 | puts `node tests/test-constants.js` 4 | puts `node tests/test-headers.js` 5 | puts `node tests/test-request.js` 6 | end 7 | watch('.*.js') { run_all_tests } 8 | run_all_tests 9 | -------------------------------------------------------------------------------- /example/demo.js: -------------------------------------------------------------------------------- 1 | var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; 2 | 3 | var xhr = new XMLHttpRequest(); 4 | 5 | xhr.onreadystatechange = function() { 6 | console.log("State: " + this.readyState); 7 | 8 | if (this.readyState === 4) { 9 | console.log("Complete.\nBody length: " + this.responseText.length); 10 | console.log("Body:\n" + this.responseText); 11 | } 12 | }; 13 | 14 | xhr.open("GET", "http://driverdan.com"); 15 | xhr.send(); 16 | -------------------------------------------------------------------------------- /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 10 | * @contributor David Ellis 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 | // IPv6 addresses must be escaped with brackets 347 | if (url.host[0] === "[") { 348 | headers.Host = "[" + headers.Host + "]"; 349 | } 350 | if (!((ssl && port === 443) || port === 80)) { 351 | headers.Host += ":" + url.port; 352 | } 353 | 354 | // Set Basic Auth if necessary 355 | if (settings.user) { 356 | if (typeof settings.password === "undefined") { 357 | settings.password = ""; 358 | } 359 | var authBuf = new Buffer(settings.user + ":" + settings.password); 360 | headers.Authorization = "Basic " + authBuf.toString("base64"); 361 | } 362 | 363 | // Set content length header 364 | if (settings.method === "GET" || settings.method === "HEAD") { 365 | data = null; 366 | } else if (data) { 367 | headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data); 368 | 369 | if (!this.getRequestHeader("Content-Type")) { 370 | headers["Content-Type"] = "text/plain;charset=UTF-8"; 371 | } 372 | } else if (settings.method === "POST") { 373 | // For a post with no data set Content-Length: 0. 374 | // This is required by buggy servers that don't meet the specs. 375 | headers["Content-Length"] = 0; 376 | } 377 | 378 | var options = { 379 | host: host, 380 | port: port, 381 | path: uri, 382 | method: settings.method, 383 | headers: headers, 384 | agent: false, 385 | withCredentials: self.withCredentials 386 | }; 387 | 388 | // Reset error flag 389 | errorFlag = false; 390 | 391 | // Handle async requests 392 | if (settings.async) { 393 | // Use the proper protocol 394 | var doRequest = ssl ? https.request : http.request; 395 | 396 | // Request is being sent, set send flag 397 | sendFlag = true; 398 | 399 | // As per spec, this is called here for historical reasons. 400 | self.dispatchEvent("readystatechange"); 401 | 402 | // Handler for the response 403 | var responseHandler = function responseHandler(resp) { 404 | // Set response var to the response we got back 405 | // This is so it remains accessable outside this scope 406 | response = resp; 407 | // Check for redirect 408 | // @TODO Prevent looped redirects 409 | if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { 410 | // Change URL to the redirect location 411 | settings.url = response.headers.location; 412 | var url = Url.parse(settings.url); 413 | // Set host var in case it's used later 414 | host = url.hostname; 415 | // Options for the new request 416 | var newOptions = { 417 | hostname: url.hostname, 418 | port: url.port, 419 | path: url.path, 420 | method: response.statusCode === 303 ? "GET" : settings.method, 421 | headers: headers, 422 | withCredentials: self.withCredentials 423 | }; 424 | 425 | // Issue the new request 426 | request = doRequest(newOptions, responseHandler).on("error", errorHandler); 427 | request.end(); 428 | // @TODO Check if an XHR event needs to be fired here 429 | return; 430 | } 431 | 432 | response.setEncoding("utf8"); 433 | 434 | setState(self.HEADERS_RECEIVED); 435 | self.status = response.statusCode; 436 | 437 | response.on("data", function(chunk) { 438 | // Make sure there's some data 439 | if (chunk) { 440 | self.responseText += chunk; 441 | } 442 | // Don't emit state changes if the connection has been aborted. 443 | if (sendFlag) { 444 | setState(self.LOADING); 445 | } 446 | }); 447 | 448 | response.on("end", function() { 449 | if (sendFlag) { 450 | // Discard the end event if the connection has been aborted 451 | setState(self.DONE); 452 | sendFlag = false; 453 | } 454 | }); 455 | 456 | response.on("error", function(error) { 457 | self.handleError(error); 458 | }); 459 | }; 460 | 461 | // Error handler for the request 462 | var errorHandler = function errorHandler(error) { 463 | self.handleError(error); 464 | }; 465 | 466 | // Create the request 467 | request = doRequest(options, responseHandler).on("error", errorHandler); 468 | 469 | // Node 0.4 and later won't accept empty data. Make sure it's needed. 470 | if (data) { 471 | request.write(data); 472 | } 473 | 474 | request.end(); 475 | 476 | self.dispatchEvent("loadstart"); 477 | } else { // Synchronous 478 | // Create a temporary file for communication with the other Node process 479 | var contentFile = ".node-xmlhttprequest-content-" + process.pid; 480 | var syncFile = ".node-xmlhttprequest-sync-" + process.pid; 481 | fs.writeFileSync(syncFile, "", "utf8"); 482 | // The async request the other Node process executes 483 | var execString = "var http = require('http'), https = require('https'), fs = require('fs');" 484 | + "var doRequest = http" + (ssl ? "s" : "") + ".request;" 485 | + "var options = " + JSON.stringify(options) + ";" 486 | + "var responseText = '';" 487 | + "var req = doRequest(options, function(response) {" 488 | + "response.setEncoding('utf8');" 489 | + "response.on('data', function(chunk) {" 490 | + " responseText += chunk;" 491 | + "});" 492 | + "response.on('end', function() {" 493 | + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');" 494 | + "fs.unlinkSync('" + syncFile + "');" 495 | + "});" 496 | + "response.on('error', function(error) {" 497 | + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');" 498 | + "fs.unlinkSync('" + syncFile + "');" 499 | + "});" 500 | + "}).on('error', function(error) {" 501 | + "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');" 502 | + "fs.unlinkSync('" + syncFile + "');" 503 | + "});" 504 | + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"") 505 | + "req.end();"; 506 | // Start the other Node Process, executing this string 507 | var syncProc = spawn(process.argv[0], ["-e", execString]); 508 | while(fs.existsSync(syncFile)) { 509 | // Wait while the sync file is empty 510 | } 511 | var resp = JSON.parse(fs.readFileSync(contentFile, 'utf8')); 512 | // Kill the child process once the file has data 513 | syncProc.stdin.end(); 514 | // Remove the temporary file 515 | fs.unlinkSync(contentFile); 516 | 517 | if (resp.err) { 518 | self.handleError(resp.err); 519 | } else { 520 | response = resp.data; 521 | self.status = resp.data.statusCode; 522 | self.responseText = resp.data.text; 523 | setState(self.DONE); 524 | } 525 | } 526 | }; 527 | 528 | /** 529 | * Called when an error is encountered to deal with it. 530 | */ 531 | this.handleError = function(error) { 532 | this.status = 0; 533 | this.statusText = error; 534 | this.responseText = error.stack; 535 | errorFlag = true; 536 | setState(this.DONE); 537 | this.dispatchEvent('error'); 538 | }; 539 | 540 | /** 541 | * Aborts a request. 542 | */ 543 | this.abort = function() { 544 | if (request) { 545 | request.abort(); 546 | request = null; 547 | } 548 | 549 | headers = defaultHeaders; 550 | this.status = 0; 551 | this.responseText = ""; 552 | this.responseXML = ""; 553 | 554 | errorFlag = true; 555 | 556 | if (this.readyState !== this.UNSENT 557 | && (this.readyState !== this.OPENED || sendFlag) 558 | && this.readyState !== this.DONE) { 559 | sendFlag = false; 560 | setState(this.DONE); 561 | } 562 | this.readyState = this.UNSENT; 563 | this.dispatchEvent('abort'); 564 | }; 565 | 566 | /** 567 | * Adds an event listener. Preferred method of binding to events. 568 | */ 569 | this.addEventListener = function(event, callback) { 570 | if (!(event in listeners)) { 571 | listeners[event] = []; 572 | } 573 | // Currently allows duplicate callbacks. Should it? 574 | listeners[event].push(callback); 575 | }; 576 | 577 | /** 578 | * Remove an event callback that has already been bound. 579 | * Only works on the matching funciton, cannot be a copy. 580 | */ 581 | this.removeEventListener = function(event, callback) { 582 | if (event in listeners) { 583 | // Filter will return a new array with the callback removed 584 | listeners[event] = listeners[event].filter(function(ev) { 585 | return ev !== callback; 586 | }); 587 | } 588 | }; 589 | 590 | /** 591 | * Dispatch any events, including both "on" methods and events attached using addEventListener. 592 | */ 593 | this.dispatchEvent = function(event) { 594 | if (typeof self["on" + event] === "function") { 595 | self["on" + event](); 596 | } 597 | if (event in listeners) { 598 | for (var i = 0, len = listeners[event].length; i < len; i++) { 599 | listeners[event][i].call(self); 600 | } 601 | } 602 | }; 603 | 604 | /** 605 | * Changes readyState and calls onreadystatechange. 606 | * 607 | * @param int state New state 608 | */ 609 | var setState = function(state) { 610 | if (state == self.LOADING || self.readyState !== state) { 611 | self.readyState = state; 612 | 613 | if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { 614 | self.dispatchEvent("readystatechange"); 615 | } 616 | 617 | if (self.readyState === self.DONE && !errorFlag) { 618 | self.dispatchEvent("load"); 619 | // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie) 620 | self.dispatchEvent("loadend"); 621 | } 622 | } 623 | }; 624 | }; 625 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | exports.XMLHttpRequest = XMLHttpRequest; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmlhttprequest", 3 | "description": "XMLHttpRequest for Node", 4 | "version": "1.8.0", 5 | "author": { 6 | "name": "Dan DeFelippi", 7 | "url": "http://driverdan.com" 8 | }, 9 | "keywords": ["xhr", "ajax"], 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/driverdan/node-XMLHttpRequest.git" 14 | }, 15 | "browser": "./lib/browser.js", 16 | "bugs": "http://github.com/driverdan/node-XMLHttpRequest/issues", 17 | "engines": { 18 | "node": ">=0.4.0" 19 | }, 20 | "directories": { 21 | "lib": "./lib", 22 | "example": "./example" 23 | }, 24 | "main": "./lib/XMLHttpRequest.js", 25 | "browser": { 26 | "fs": false, 27 | "child_process": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/test-constants.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest(); 5 | 6 | // Test constant values 7 | assert.equal(0, xhr.UNSENT); 8 | assert.equal(1, xhr.OPENED); 9 | assert.equal(2, xhr.HEADERS_RECEIVED); 10 | assert.equal(3, xhr.LOADING); 11 | assert.equal(4, xhr.DONE); 12 | 13 | sys.puts("done"); 14 | -------------------------------------------------------------------------------- /tests/test-events.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , http = require("http") 4 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 5 | , xhr; 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | var body = (req.method != "HEAD" ? "Hello World" : ""); 10 | 11 | res.writeHead(200, { 12 | "Content-Type": "text/plain", 13 | "Content-Length": Buffer.byteLength(body) 14 | }); 15 | // HEAD has no body 16 | if (req.method != "HEAD") { 17 | res.write(body); 18 | } 19 | res.end(); 20 | assert.equal(onreadystatechange, true); 21 | assert.equal(readystatechange, true); 22 | assert.equal(removed, true); 23 | sys.puts("done"); 24 | this.close(); 25 | }).listen(8000); 26 | 27 | xhr = new XMLHttpRequest(); 28 | 29 | // Track event calls 30 | var onreadystatechange = false; 31 | var readystatechange = false; 32 | var removed = true; 33 | var removedEvent = function() { 34 | removed = false; 35 | }; 36 | 37 | xhr.onreadystatechange = function() { 38 | onreadystatechange = true; 39 | }; 40 | 41 | xhr.addEventListener("readystatechange", function() { 42 | readystatechange = true; 43 | }); 44 | 45 | // This isn't perfect, won't guarantee it was added in the first place 46 | xhr.addEventListener("readystatechange", removedEvent); 47 | xhr.removeEventListener("readystatechange", removedEvent); 48 | 49 | xhr.open("GET", "http://localhost:8000"); 50 | xhr.send(); 51 | -------------------------------------------------------------------------------- /tests/test-exceptions.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest(); 5 | 6 | // Test request methods that aren't allowed 7 | try { 8 | xhr.open("TRACK", "http://localhost:8000/"); 9 | console.log("ERROR: TRACK should have thrown exception"); 10 | } catch(e) {} 11 | try { 12 | xhr.open("TRACE", "http://localhost:8000/"); 13 | console.log("ERROR: TRACE should have thrown exception"); 14 | } catch(e) {} 15 | try { 16 | xhr.open("CONNECT", "http://localhost:8000/"); 17 | console.log("ERROR: CONNECT should have thrown exception"); 18 | } catch(e) {} 19 | // Test valid request method 20 | try { 21 | xhr.open("GET", "http://localhost:8000/"); 22 | } catch(e) { 23 | console.log("ERROR: Invalid exception for GET", e); 24 | } 25 | 26 | // Test forbidden headers 27 | var forbiddenRequestHeaders = [ 28 | "accept-charset", 29 | "accept-encoding", 30 | "access-control-request-headers", 31 | "access-control-request-method", 32 | "connection", 33 | "content-length", 34 | "content-transfer-encoding", 35 | "cookie", 36 | "cookie2", 37 | "date", 38 | "expect", 39 | "host", 40 | "keep-alive", 41 | "origin", 42 | "referer", 43 | "te", 44 | "trailer", 45 | "transfer-encoding", 46 | "upgrade", 47 | "user-agent", 48 | "via" 49 | ]; 50 | 51 | for (var i in forbiddenRequestHeaders) { 52 | try { 53 | xhr.setRequestHeader(forbiddenRequestHeaders[i], "Test"); 54 | console.log("ERROR: " + forbiddenRequestHeaders[i] + " should have thrown exception"); 55 | } catch(e) { 56 | } 57 | } 58 | 59 | // Try valid header 60 | xhr.setRequestHeader("X-Foobar", "Test"); 61 | 62 | console.log("Done"); 63 | -------------------------------------------------------------------------------- /tests/test-headers.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest() 5 | , http = require("http"); 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | // Test setRequestHeader 10 | assert.equal("Foobar", req.headers["x-test"]); 11 | // Test non-conforming allowed header 12 | assert.equal("node-XMLHttpRequest-test", req.headers["user-agent"]); 13 | // Test header set with blacklist disabled 14 | assert.equal("http://github.com", req.headers["referer"]); 15 | 16 | var body = "Hello World"; 17 | res.writeHead(200, { 18 | "Content-Type": "text/plain", 19 | "Content-Length": Buffer.byteLength(body), 20 | // Set cookie headers to see if they're correctly suppressed 21 | // Actual values don't matter 22 | "Set-Cookie": "foo=bar", 23 | "Set-Cookie2": "bar=baz", 24 | "Date": "Thu, 30 Aug 2012 18:17:53 GMT", 25 | "Connection": "close" 26 | }); 27 | res.write("Hello World"); 28 | res.end(); 29 | 30 | this.close(); 31 | }).listen(8000); 32 | 33 | xhr.onreadystatechange = function() { 34 | if (this.readyState == 4) { 35 | // Test getAllResponseHeaders() 36 | var headers = "content-type: text/plain\r\ncontent-length: 11\r\ndate: Thu, 30 Aug 2012 18:17:53 GMT\r\nconnection: close"; 37 | assert.equal(headers, this.getAllResponseHeaders()); 38 | 39 | // Test case insensitivity 40 | assert.equal('text/plain', this.getResponseHeader('Content-Type')); 41 | assert.equal('text/plain', this.getResponseHeader('Content-type')); 42 | assert.equal('text/plain', this.getResponseHeader('content-Type')); 43 | assert.equal('text/plain', this.getResponseHeader('content-type')); 44 | 45 | // Test aborted getAllResponseHeaders 46 | this.abort(); 47 | assert.equal("", this.getAllResponseHeaders()); 48 | assert.equal(null, this.getResponseHeader("Connection")); 49 | 50 | sys.puts("done"); 51 | } 52 | }; 53 | 54 | assert.equal(null, xhr.getResponseHeader("Content-Type")); 55 | try { 56 | xhr.open("GET", "http://localhost:8000/"); 57 | // Valid header 58 | xhr.setRequestHeader("X-Test", "Foobar"); 59 | xhr.setRequestHeader("X-Test2", "Foobar1"); 60 | xhr.setRequestHeader("X-Test2", "Foobar2"); 61 | // Invalid header 62 | xhr.setRequestHeader("Content-Length", 0); 63 | // Allowed header outside of specs 64 | xhr.setRequestHeader("user-agent", "node-XMLHttpRequest-test"); 65 | // Test getRequestHeader 66 | assert.equal("Foobar", xhr.getRequestHeader("X-Test")); 67 | assert.equal("Foobar", xhr.getRequestHeader("x-tEST")); 68 | assert.equal("Foobar1, Foobar2", xhr.getRequestHeader("x-test2")); 69 | // Test invalid header 70 | assert.equal("", xhr.getRequestHeader("Content-Length")); 71 | 72 | // Test allowing all headers 73 | xhr.setDisableHeaderCheck(true); 74 | xhr.setRequestHeader("Referer", "http://github.com"); 75 | assert.equal("http://github.com", xhr.getRequestHeader("Referer")); 76 | 77 | xhr.send(); 78 | } catch(e) { 79 | console.log("ERROR: Exception raised", e); 80 | } 81 | -------------------------------------------------------------------------------- /tests/test-redirect-301.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest() 5 | , http = require("http"); 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | if (req.url === '/redirectingResource') { 10 | res.writeHead(301, {'Location': 'http://localhost:8000/'}); 11 | res.end(); 12 | return; 13 | } 14 | 15 | var body = "Hello World"; 16 | res.writeHead(200, { 17 | "Content-Type": "text/plain", 18 | "Content-Length": Buffer.byteLength(body), 19 | "Date": "Thu, 30 Aug 2012 18:17:53 GMT", 20 | "Connection": "close" 21 | }); 22 | res.write("Hello World"); 23 | res.end(); 24 | 25 | this.close(); 26 | }).listen(8000); 27 | 28 | xhr.onreadystatechange = function() { 29 | if (this.readyState == 4) { 30 | assert.equal(xhr.status, 200); 31 | assert.equal(xhr.getRequestHeader('Location'), ''); 32 | assert.equal(xhr.responseText, "Hello World"); 33 | sys.puts("done"); 34 | } 35 | }; 36 | 37 | try { 38 | xhr.open("GET", "http://localhost:8000/redirectingResource"); 39 | xhr.send(); 40 | } catch(e) { 41 | console.log("ERROR: Exception raised", e); 42 | } 43 | -------------------------------------------------------------------------------- /tests/test-redirect-302.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest() 5 | , http = require("http"); 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | if (req.url === '/redirectingResource') { 10 | res.writeHead(302, {'Location': 'http://localhost:8000/'}); 11 | res.end(); 12 | return; 13 | } 14 | 15 | var body = "Hello World"; 16 | res.writeHead(200, { 17 | "Content-Type": "text/plain", 18 | "Content-Length": Buffer.byteLength(body), 19 | "Date": "Thu, 30 Aug 2012 18:17:53 GMT", 20 | "Connection": "close" 21 | }); 22 | res.write("Hello World"); 23 | res.end(); 24 | 25 | this.close(); 26 | }).listen(8000); 27 | 28 | xhr.onreadystatechange = function() { 29 | if (this.readyState == 4) { 30 | assert.equal(xhr.getRequestHeader('Location'), ''); 31 | assert.equal(xhr.responseText, "Hello World"); 32 | sys.puts("done"); 33 | } 34 | }; 35 | 36 | try { 37 | xhr.open("GET", "http://localhost:8000/redirectingResource"); 38 | xhr.send(); 39 | } catch(e) { 40 | console.log("ERROR: Exception raised", e); 41 | } 42 | -------------------------------------------------------------------------------- /tests/test-redirect-303.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest() 5 | , http = require("http"); 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | if (req.url === '/redirectingResource') { 10 | res.writeHead(303, {'Location': 'http://localhost:8000/'}); 11 | res.end(); 12 | return; 13 | } 14 | 15 | var body = "Hello World"; 16 | res.writeHead(200, { 17 | "Content-Type": "text/plain", 18 | "Content-Length": Buffer.byteLength(body), 19 | "Date": "Thu, 30 Aug 2012 18:17:53 GMT", 20 | "Connection": "close" 21 | }); 22 | res.write("Hello World"); 23 | res.end(); 24 | 25 | this.close(); 26 | }).listen(8000); 27 | 28 | xhr.onreadystatechange = function() { 29 | if (this.readyState == 4) { 30 | assert.equal(xhr.getRequestHeader('Location'), ''); 31 | assert.equal(xhr.responseText, "Hello World"); 32 | sys.puts("done"); 33 | } 34 | }; 35 | 36 | try { 37 | xhr.open("POST", "http://localhost:8000/redirectingResource"); 38 | xhr.send(); 39 | } catch(e) { 40 | console.log("ERROR: Exception raised", e); 41 | } 42 | -------------------------------------------------------------------------------- /tests/test-redirect-307.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr = new XMLHttpRequest() 5 | , http = require("http"); 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | if (req.url === '/redirectingResource') { 10 | res.writeHead(307, {'Location': 'http://localhost:8000/'}); 11 | res.end(); 12 | return; 13 | } 14 | 15 | assert.equal(req.method, 'POST'); 16 | 17 | var body = "Hello World"; 18 | res.writeHead(200, { 19 | "Content-Type": "text/plain", 20 | "Content-Length": Buffer.byteLength(body), 21 | "Date": "Thu, 30 Aug 2012 18:17:53 GMT", 22 | "Connection": "close" 23 | }); 24 | res.write("Hello World"); 25 | res.end(); 26 | 27 | this.close(); 28 | }).listen(8000); 29 | 30 | xhr.onreadystatechange = function() { 31 | if (this.readyState == 4) { 32 | assert.equal(xhr.getRequestHeader('Location'), ''); 33 | assert.equal(xhr.responseText, "Hello World"); 34 | sys.puts("done"); 35 | } 36 | }; 37 | 38 | try { 39 | xhr.open("POST", "http://localhost:8000/redirectingResource"); 40 | xhr.send(); 41 | } catch(e) { 42 | console.log("ERROR: Exception raised", e); 43 | } 44 | -------------------------------------------------------------------------------- /tests/test-request-methods.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , http = require("http") 5 | , xhr; 6 | 7 | // Test server 8 | var server = http.createServer(function (req, res) { 9 | // Check request method and URL 10 | assert.equal(methods[curMethod], req.method); 11 | assert.equal("/" + methods[curMethod], req.url); 12 | 13 | var body = (req.method != "HEAD" ? "Hello World" : ""); 14 | 15 | res.writeHead(200, { 16 | "Content-Type": "text/plain", 17 | "Content-Length": Buffer.byteLength(body) 18 | }); 19 | // HEAD has no body 20 | if (req.method != "HEAD") { 21 | res.write(body); 22 | } 23 | res.end(); 24 | 25 | if (curMethod == methods.length - 1) { 26 | this.close(); 27 | sys.puts("done"); 28 | } 29 | }).listen(8000); 30 | 31 | // Test standard methods 32 | var methods = ["GET", "POST", "HEAD", "PUT", "DELETE"]; 33 | var curMethod = 0; 34 | 35 | function start(method) { 36 | // Reset each time 37 | xhr = new XMLHttpRequest(); 38 | 39 | xhr.onreadystatechange = function() { 40 | if (this.readyState == 4) { 41 | if (method == "HEAD") { 42 | assert.equal("", this.responseText); 43 | } else { 44 | assert.equal("Hello World", this.responseText); 45 | } 46 | 47 | curMethod++; 48 | 49 | if (curMethod < methods.length) { 50 | sys.puts("Testing " + methods[curMethod]); 51 | start(methods[curMethod]); 52 | } 53 | } 54 | }; 55 | 56 | var url = "http://localhost:8000/" + method; 57 | xhr.open(method, url); 58 | xhr.send(); 59 | } 60 | 61 | sys.puts("Testing " + methods[curMethod]); 62 | start(methods[curMethod]); 63 | -------------------------------------------------------------------------------- /tests/test-request-protocols.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 4 | , xhr; 5 | 6 | xhr = new XMLHttpRequest(); 7 | 8 | xhr.onreadystatechange = function() { 9 | if (this.readyState == 4) { 10 | assert.equal("Hello World", this.responseText); 11 | runSync(); 12 | } 13 | }; 14 | 15 | // Async 16 | var url = "file://" + __dirname + "/testdata.txt"; 17 | xhr.open("GET", url); 18 | xhr.send(); 19 | 20 | // Sync 21 | var runSync = function() { 22 | xhr = new XMLHttpRequest(); 23 | 24 | xhr.onreadystatechange = function() { 25 | if (this.readyState == 4) { 26 | assert.equal("Hello World", this.responseText); 27 | sys.puts("done"); 28 | } 29 | }; 30 | xhr.open("GET", url, false); 31 | xhr.send(); 32 | } 33 | -------------------------------------------------------------------------------- /tests/test-streaming.js: -------------------------------------------------------------------------------- 1 | var sys = require("util") 2 | , assert = require("assert") 3 | , http = require("http") 4 | , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest 5 | , xhr; 6 | 7 | // Test server 8 | 9 | function completeResponse(res,server,body) { 10 | res.end(); 11 | assert.equal(onreadystatechange, true); 12 | assert.equal(readystatechange, true); 13 | assert.equal(removed, true); 14 | assert.equal(loadCount, body.length); 15 | sys.puts("done"); 16 | server.close(); 17 | } 18 | function push(res,piece) { 19 | res.write(piece); 20 | } 21 | 22 | var server = http.createServer(function (req, res) { 23 | var body = (req.method != "HEAD" ? ["Hello","World","Stream"] : []); 24 | 25 | res.writeHead(200, { 26 | "Content-Type": "text/plain", 27 | "Content-Length": Buffer.byteLength(body.join("")) 28 | }); 29 | 30 | var nextPiece = 0; 31 | var self = this; 32 | var interval = setInterval(function() { 33 | if (nextPiece < body.length) { 34 | res.write(body[nextPiece]); 35 | nextPiece++; 36 | } else { 37 | completeResponse(res,self,body); 38 | clearInterval(interval); 39 | } 40 | },100); //nagle may put writes together, if it happens rise the interval time 41 | 42 | }).listen(8000); 43 | 44 | xhr = new XMLHttpRequest(); 45 | 46 | // Track event calls 47 | var onreadystatechange = false; 48 | var readystatechange = false; 49 | var removed = true; 50 | var loadCount = 0; 51 | var removedEvent = function() { 52 | removed = false; 53 | }; 54 | 55 | xhr.onreadystatechange = function() { 56 | onreadystatechange = true; 57 | }; 58 | 59 | xhr.addEventListener("readystatechange", function() { 60 | readystatechange = true; 61 | if (xhr.readyState == xhr.LOADING) { 62 | loadCount++; 63 | } 64 | }); 65 | 66 | // This isn't perfect, won't guarantee it was added in the first place 67 | xhr.addEventListener("readystatechange", removedEvent); 68 | xhr.removeEventListener("readystatechange", removedEvent); 69 | 70 | xhr.open("GET", "http://localhost:8000"); 71 | xhr.send(); 72 | -------------------------------------------------------------------------------- /tests/testdata.txt: -------------------------------------------------------------------------------- 1 | Hello World --------------------------------------------------------------------------------