XMLHttpRequest.js 20 KB

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