XMLHttpRequest.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  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.status = null;
  111. this.statusText = null;
  112. /**
  113. * Private methods
  114. */
  115. /**
  116. * Check if the specified header is allowed.
  117. *
  118. * @param string header Header to validate
  119. * @return boolean False if not allowed, otherwise true
  120. */
  121. var isAllowedHttpHeader = function(header) {
  122. return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1);
  123. };
  124. /**
  125. * Check if the specified method is allowed.
  126. *
  127. * @param string method Request method to validate
  128. * @return boolean False if not allowed, otherwise true
  129. */
  130. var isAllowedHttpMethod = function(method) {
  131. return (method && forbiddenRequestMethods.indexOf(method) === -1);
  132. };
  133. /**
  134. * Public methods
  135. */
  136. /**
  137. * Open the connection. Currently supports local server requests.
  138. *
  139. * @param string method Connection method (eg GET, POST)
  140. * @param string url URL for the connection.
  141. * @param boolean async Asynchronous connection. Default is true.
  142. * @param string user Username for basic authentication (optional)
  143. * @param string password Password for basic authentication (optional)
  144. */
  145. this.open = function(method, url, async, user, password) {
  146. this.abort();
  147. errorFlag = false;
  148. abortedFlag = false;
  149. // Check for valid request method
  150. if (!isAllowedHttpMethod(method)) {
  151. throw new Error("SecurityError: Request method not allowed");
  152. }
  153. settings = {
  154. "method": method,
  155. "url": url.toString(),
  156. "async": (typeof async !== "boolean" ? true : async),
  157. "user": user || null,
  158. "password": password || null
  159. };
  160. setState(this.OPENED);
  161. };
  162. /**
  163. * Disables or enables isAllowedHttpHeader() check the request. Enabled by default.
  164. * This does not conform to the W3C spec.
  165. *
  166. * @param boolean state Enable or disable header checking.
  167. */
  168. this.setDisableHeaderCheck = function(state) {
  169. disableHeaderCheck = state;
  170. };
  171. /**
  172. * Sets a header for the request.
  173. *
  174. * @param string header Header name
  175. * @param string value Header value
  176. * @return boolean Header added
  177. */
  178. this.setRequestHeader = function(header, value) {
  179. if (this.readyState != this.OPENED) {
  180. throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN");
  181. }
  182. if (!isAllowedHttpHeader(header)) {
  183. console.warn('Refused to set unsafe header "' + header + '"');
  184. return false;
  185. }
  186. if (sendFlag) {
  187. throw new Error("INVALID_STATE_ERR: send flag is true");
  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 new Error("INVALID_STATE_ERR: connection must be opened before send() is called");
  247. }
  248. if (sendFlag) {
  249. throw new Error("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 new Error("Protocol not supported.");
  271. }
  272. // Load files off the local filesystem (file://)
  273. if (local) {
  274. if (settings.method !== "GET") {
  275. throw new Error("XMLHttpRequest: Only GET method is supported");
  276. }
  277. if (settings.async) {
  278. fs.readFile(unescape(url.pathname), 'utf8', function(error, data) {
  279. if (error) {
  280. self.handleError(error, error.errno || -1);
  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(unescape(url.pathname), 'utf8');
  290. this.status = 200;
  291. setState(self.DONE);
  292. } catch(e) {
  293. this.handleError(e, e.errno || -1);
  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 === false ? false : true;
  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 === false ? false : true;
  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. if (opts.autoUnref) {
  428. request.on('socket', (socket) => {
  429. socket.unref();
  430. });
  431. }
  432. // Node 0.4 and later won't accept empty data. Make sure it's needed.
  433. if (data) {
  434. request.write(data);
  435. }
  436. request.end();
  437. self.dispatchEvent("loadstart");
  438. } else { // Synchronous
  439. // Create a temporary file for communication with the other Node process
  440. var contentFile = ".node-xmlhttprequest-content-" + process.pid;
  441. var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
  442. fs.writeFileSync(syncFile, "", "utf8");
  443. // The async request the other Node process executes
  444. var execString = "var http = require('http'), https = require('https'), fs = require('fs');"
  445. + "var doRequest = http" + (ssl ? "s" : "") + ".request;"
  446. + "var options = " + JSON.stringify(options) + ";"
  447. + "var responseText = '';"
  448. + "var req = doRequest(options, function(response) {"
  449. + "response.setEncoding('utf8');"
  450. + "response.on('data', function(chunk) {"
  451. + " responseText += chunk;"
  452. + "});"
  453. + "response.on('end', function() {"
  454. + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-STATUS:' + response.statusCode + ',' + responseText, 'utf8');"
  455. + "fs.unlinkSync('" + syncFile + "');"
  456. + "});"
  457. + "response.on('error', function(error) {"
  458. + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
  459. + "fs.unlinkSync('" + syncFile + "');"
  460. + "});"
  461. + "}).on('error', function(error) {"
  462. + "fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
  463. + "fs.unlinkSync('" + syncFile + "');"
  464. + "});"
  465. + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"")
  466. + "req.end();";
  467. // Start the other Node Process, executing this string
  468. var syncProc = spawn(process.argv[0], ["-e", execString]);
  469. var statusText;
  470. while(fs.existsSync(syncFile)) {
  471. // Wait while the sync file is empty
  472. }
  473. self.responseText = fs.readFileSync(contentFile, 'utf8');
  474. // Kill the child process once the file has data
  475. syncProc.stdin.end();
  476. // Remove the temporary file
  477. fs.unlinkSync(contentFile);
  478. if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
  479. // If the file returned an error, handle it
  480. var errorObj = self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, "");
  481. self.handleError(errorObj, 503);
  482. } else {
  483. // If the file returned okay, parse its data and move to the DONE state
  484. self.status = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:([0-9]*),.*/, "$1");
  485. self.responseText = self.responseText.replace(/^NODE-XMLHTTPREQUEST-STATUS:[0-9]*,(.*)/, "$1");
  486. setState(self.DONE);
  487. }
  488. }
  489. };
  490. /**
  491. * Called when an error is encountered to deal with it.
  492. * @param status {number} HTTP status code to use rather than the default (0) for XHR errors.
  493. */
  494. this.handleError = function(error, status) {
  495. this.status = status || 0;
  496. this.statusText = error;
  497. this.responseText = error.stack;
  498. errorFlag = true;
  499. setState(this.DONE);
  500. };
  501. /**
  502. * Aborts a request.
  503. */
  504. this.abort = function() {
  505. if (request) {
  506. request.abort();
  507. request = null;
  508. }
  509. headers = Object.assign({}, defaultHeaders);
  510. this.responseText = "";
  511. this.responseXML = "";
  512. errorFlag = abortedFlag = true
  513. if (this.readyState !== this.UNSENT
  514. && (this.readyState !== this.OPENED || sendFlag)
  515. && this.readyState !== this.DONE) {
  516. sendFlag = false;
  517. setState(this.DONE);
  518. }
  519. this.readyState = this.UNSENT;
  520. };
  521. /**
  522. * Adds an event listener. Preferred method of binding to events.
  523. */
  524. this.addEventListener = function(event, callback) {
  525. if (!(event in listeners)) {
  526. listeners[event] = [];
  527. }
  528. // Currently allows duplicate callbacks. Should it?
  529. listeners[event].push(callback);
  530. };
  531. /**
  532. * Remove an event callback that has already been bound.
  533. * Only works on the matching funciton, cannot be a copy.
  534. */
  535. this.removeEventListener = function(event, callback) {
  536. if (event in listeners) {
  537. // Filter will return a new array with the callback removed
  538. listeners[event] = listeners[event].filter(function(ev) {
  539. return ev !== callback;
  540. });
  541. }
  542. };
  543. /**
  544. * Dispatch any events, including both "on" methods and events attached using addEventListener.
  545. */
  546. this.dispatchEvent = function(event) {
  547. if (typeof self["on" + event] === "function") {
  548. if (this.readyState === this.DONE)
  549. setImmediate(function() { self["on" + event]() })
  550. else
  551. self["on" + event]()
  552. }
  553. if (event in listeners) {
  554. for (let i = 0, len = listeners[event].length; i < len; i++) {
  555. if (this.readyState === this.DONE)
  556. setImmediate(function() { listeners[event][i].call(self) })
  557. else
  558. listeners[event][i].call(self)
  559. }
  560. }
  561. };
  562. /**
  563. * Changes readyState and calls onreadystatechange.
  564. *
  565. * @param int state New state
  566. */
  567. var setState = function(state) {
  568. if ((self.readyState === state) || (self.readyState === self.UNSENT && abortedFlag))
  569. return
  570. self.readyState = state;
  571. if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) {
  572. self.dispatchEvent("readystatechange");
  573. }
  574. if (self.readyState === self.DONE) {
  575. let fire
  576. if (abortedFlag)
  577. fire = "abort"
  578. else if (errorFlag)
  579. fire = "error"
  580. else
  581. fire = "load"
  582. self.dispatchEvent(fire)
  583. // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie)
  584. self.dispatchEvent("loadend");
  585. }
  586. };
  587. };