XMLHttpRequest.js 18 KB

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