XMLHttpRequest.js 16 KB

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