XMLHttpRequest.js 15 KB

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