You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

430 lines
12 KiB

4 years ago
  1. var original = require('original')
  2. var parse = require('url').parse
  3. var events = require('events')
  4. var https = require('https')
  5. var http = require('http')
  6. var util = require('util')
  7. var httpsOptions = [
  8. 'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers',
  9. 'rejectUnauthorized', 'secureProtocol', 'servername', 'checkServerIdentity'
  10. ]
  11. var bom = [239, 187, 191]
  12. var colon = 58
  13. var space = 32
  14. var lineFeed = 10
  15. var carriageReturn = 13
  16. function hasBom (buf) {
  17. return bom.every(function (charCode, index) {
  18. return buf[index] === charCode
  19. })
  20. }
  21. /**
  22. * Creates a new EventSource object
  23. *
  24. * @param {String} url the URL to which to connect
  25. * @param {Object} [eventSourceInitDict] extra init params. See README for details.
  26. * @api public
  27. **/
  28. function EventSource (url, eventSourceInitDict) {
  29. var readyState = EventSource.CONNECTING
  30. Object.defineProperty(this, 'readyState', {
  31. get: function () {
  32. return readyState
  33. }
  34. })
  35. Object.defineProperty(this, 'url', {
  36. get: function () {
  37. return url
  38. }
  39. })
  40. var self = this
  41. self.reconnectInterval = 1000
  42. function onConnectionClosed (message) {
  43. if (readyState === EventSource.CLOSED) return
  44. readyState = EventSource.CONNECTING
  45. _emit('error', new Event('error', {message: message}))
  46. // The url may have been changed by a temporary
  47. // redirect. If that's the case, revert it now.
  48. if (reconnectUrl) {
  49. url = reconnectUrl
  50. reconnectUrl = null
  51. }
  52. setTimeout(function () {
  53. if (readyState !== EventSource.CONNECTING) {
  54. return
  55. }
  56. connect()
  57. }, self.reconnectInterval)
  58. }
  59. var req
  60. var lastEventId = ''
  61. if (eventSourceInitDict && eventSourceInitDict.headers && eventSourceInitDict.headers['Last-Event-ID']) {
  62. lastEventId = eventSourceInitDict.headers['Last-Event-ID']
  63. delete eventSourceInitDict.headers['Last-Event-ID']
  64. }
  65. var discardTrailingNewline = false
  66. var data = ''
  67. var eventName = ''
  68. var reconnectUrl = null
  69. function connect () {
  70. var options = parse(url)
  71. var isSecure = options.protocol === 'https:'
  72. options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' }
  73. if (lastEventId) options.headers['Last-Event-ID'] = lastEventId
  74. if (eventSourceInitDict && eventSourceInitDict.headers) {
  75. for (var i in eventSourceInitDict.headers) {
  76. var header = eventSourceInitDict.headers[i]
  77. if (header) {
  78. options.headers[i] = header
  79. }
  80. }
  81. }
  82. // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`,
  83. // but for now exists as a backwards-compatibility layer
  84. options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized)
  85. // If specify http proxy, make the request to sent to the proxy server,
  86. // and include the original url in path and Host headers
  87. var useProxy = eventSourceInitDict && eventSourceInitDict.proxy
  88. if (useProxy) {
  89. var proxy = parse(eventSourceInitDict.proxy)
  90. isSecure = proxy.protocol === 'https:'
  91. options.protocol = isSecure ? 'https:' : 'http:'
  92. options.path = url
  93. options.headers.Host = options.host
  94. options.hostname = proxy.hostname
  95. options.host = proxy.host
  96. options.port = proxy.port
  97. }
  98. // If https options are specified, merge them into the request options
  99. if (eventSourceInitDict && eventSourceInitDict.https) {
  100. for (var optName in eventSourceInitDict.https) {
  101. if (httpsOptions.indexOf(optName) === -1) {
  102. continue
  103. }
  104. var option = eventSourceInitDict.https[optName]
  105. if (option !== undefined) {
  106. options[optName] = option
  107. }
  108. }
  109. }
  110. // Pass this on to the XHR
  111. if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) {
  112. options.withCredentials = eventSourceInitDict.withCredentials
  113. }
  114. req = (isSecure ? https : http).request(options, function (res) {
  115. // Handle HTTP errors
  116. if (res.statusCode === 500 || res.statusCode === 502 || res.statusCode === 503 || res.statusCode === 504) {
  117. _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage}))
  118. onConnectionClosed()
  119. return
  120. }
  121. // Handle HTTP redirects
  122. if (res.statusCode === 301 || res.statusCode === 307) {
  123. if (!res.headers.location) {
  124. // Server sent redirect response without Location header.
  125. _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage}))
  126. return
  127. }
  128. if (res.statusCode === 307) reconnectUrl = url
  129. url = res.headers.location
  130. process.nextTick(connect)
  131. return
  132. }
  133. if (res.statusCode !== 200) {
  134. _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage}))
  135. return self.close()
  136. }
  137. readyState = EventSource.OPEN
  138. res.on('close', function () {
  139. res.removeAllListeners('close')
  140. res.removeAllListeners('end')
  141. onConnectionClosed()
  142. })
  143. res.on('end', function () {
  144. res.removeAllListeners('close')
  145. res.removeAllListeners('end')
  146. onConnectionClosed()
  147. })
  148. _emit('open', new Event('open'))
  149. // text/event-stream parser adapted from webkit's
  150. // Source/WebCore/page/EventSource.cpp
  151. var isFirst = true
  152. var buf
  153. res.on('data', function (chunk) {
  154. buf = buf ? Buffer.concat([buf, chunk]) : chunk
  155. if (isFirst && hasBom(buf)) {
  156. buf = buf.slice(bom.length)
  157. }
  158. isFirst = false
  159. var pos = 0
  160. var length = buf.length
  161. while (pos < length) {
  162. if (discardTrailingNewline) {
  163. if (buf[pos] === lineFeed) {
  164. ++pos
  165. }
  166. discardTrailingNewline = false
  167. }
  168. var lineLength = -1
  169. var fieldLength = -1
  170. var c
  171. for (var i = pos; lineLength < 0 && i < length; ++i) {
  172. c = buf[i]
  173. if (c === colon) {
  174. if (fieldLength < 0) {
  175. fieldLength = i - pos
  176. }
  177. } else if (c === carriageReturn) {
  178. discardTrailingNewline = true
  179. lineLength = i - pos
  180. } else if (c === lineFeed) {
  181. lineLength = i - pos
  182. }
  183. }
  184. if (lineLength < 0) {
  185. break
  186. }
  187. parseEventStreamLine(buf, pos, fieldLength, lineLength)
  188. pos += lineLength + 1
  189. }
  190. if (pos === length) {
  191. buf = void 0
  192. } else if (pos > 0) {
  193. buf = buf.slice(pos)
  194. }
  195. })
  196. })
  197. req.on('error', function (err) {
  198. onConnectionClosed(err.message)
  199. })
  200. if (req.setNoDelay) req.setNoDelay(true)
  201. req.end()
  202. }
  203. connect()
  204. function _emit () {
  205. if (self.listeners(arguments[0]).length > 0) {
  206. self.emit.apply(self, arguments)
  207. }
  208. }
  209. this._close = function () {
  210. if (readyState === EventSource.CLOSED) return
  211. readyState = EventSource.CLOSED
  212. if (req.abort) req.abort()
  213. if (req.xhr && req.xhr.abort) req.xhr.abort()
  214. }
  215. function parseEventStreamLine (buf, pos, fieldLength, lineLength) {
  216. if (lineLength === 0) {
  217. if (data.length > 0) {
  218. var type = eventName || 'message'
  219. _emit(type, new MessageEvent(type, {
  220. data: data.slice(0, -1), // remove trailing newline
  221. lastEventId: lastEventId,
  222. origin: original(url)
  223. }))
  224. data = ''
  225. }
  226. eventName = void 0
  227. } else if (fieldLength > 0) {
  228. var noValue = fieldLength < 0
  229. var step = 0
  230. var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)).toString()
  231. if (noValue) {
  232. step = lineLength
  233. } else if (buf[pos + fieldLength + 1] !== space) {
  234. step = fieldLength + 1
  235. } else {
  236. step = fieldLength + 2
  237. }
  238. pos += step
  239. var valueLength = lineLength - step
  240. var value = buf.slice(pos, pos + valueLength).toString()
  241. if (field === 'data') {
  242. data += value + '\n'
  243. } else if (field === 'event') {
  244. eventName = value
  245. } else if (field === 'id') {
  246. lastEventId = value
  247. } else if (field === 'retry') {
  248. var retry = parseInt(value, 10)
  249. if (!Number.isNaN(retry)) {
  250. self.reconnectInterval = retry
  251. }
  252. }
  253. }
  254. }
  255. }
  256. module.exports = EventSource
  257. util.inherits(EventSource, events.EventEmitter)
  258. EventSource.prototype.constructor = EventSource; // make stacktraces readable
  259. ['open', 'error', 'message'].forEach(function (method) {
  260. Object.defineProperty(EventSource.prototype, 'on' + method, {
  261. /**
  262. * Returns the current listener
  263. *
  264. * @return {Mixed} the set function or undefined
  265. * @api private
  266. */
  267. get: function get () {
  268. var listener = this.listeners(method)[0]
  269. return listener ? (listener._listener ? listener._listener : listener) : undefined
  270. },
  271. /**
  272. * Start listening for events
  273. *
  274. * @param {Function} listener the listener
  275. * @return {Mixed} the set function or undefined
  276. * @api private
  277. */
  278. set: function set (listener) {
  279. this.removeAllListeners(method)
  280. this.addEventListener(method, listener)
  281. }
  282. })
  283. })
  284. /**
  285. * Ready states
  286. */
  287. Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0})
  288. Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1})
  289. Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2})
  290. EventSource.prototype.CONNECTING = 0
  291. EventSource.prototype.OPEN = 1
  292. EventSource.prototype.CLOSED = 2
  293. /**
  294. * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed)
  295. *
  296. * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close
  297. * @api public
  298. */
  299. EventSource.prototype.close = function () {
  300. this._close()
  301. }
  302. /**
  303. * Emulates the W3C Browser based WebSocket interface using addEventListener.
  304. *
  305. * @param {String} type A string representing the event type to listen out for
  306. * @param {Function} listener callback
  307. * @see https://developer.mozilla.org/en/DOM/element.addEventListener
  308. * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
  309. * @api public
  310. */
  311. EventSource.prototype.addEventListener = function addEventListener (type, listener) {
  312. if (typeof listener === 'function') {
  313. // store a reference so we can return the original function again
  314. listener._listener = listener
  315. this.on(type, listener)
  316. }
  317. }
  318. /**
  319. * Emulates the W3C Browser based WebSocket interface using dispatchEvent.
  320. *
  321. * @param {Event} event An event to be dispatched
  322. * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
  323. * @api public
  324. */
  325. EventSource.prototype.dispatchEvent = function dispatchEvent (event) {
  326. if (!event.type) {
  327. throw new Error('UNSPECIFIED_EVENT_TYPE_ERR')
  328. }
  329. // if event is instance of an CustomEvent (or has 'details' property),
  330. // send the detail object as the payload for the event
  331. this.emit(event.type, event.detail)
  332. }
  333. /**
  334. * Emulates the W3C Browser based WebSocket interface using removeEventListener.
  335. *
  336. * @param {String} type A string representing the event type to remove
  337. * @param {Function} listener callback
  338. * @see https://developer.mozilla.org/en/DOM/element.removeEventListener
  339. * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
  340. * @api public
  341. */
  342. EventSource.prototype.removeEventListener = function removeEventListener (type, listener) {
  343. if (typeof listener === 'function') {
  344. listener._listener = undefined
  345. this.removeListener(type, listener)
  346. }
  347. }
  348. /**
  349. * W3C Event
  350. *
  351. * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event
  352. * @api private
  353. */
  354. function Event (type, optionalProperties) {
  355. Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
  356. if (optionalProperties) {
  357. for (var f in optionalProperties) {
  358. if (optionalProperties.hasOwnProperty(f)) {
  359. Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true })
  360. }
  361. }
  362. }
  363. }
  364. /**
  365. * W3C MessageEvent
  366. *
  367. * @see http://www.w3.org/TR/webmessaging/#event-definitions
  368. * @api private
  369. */
  370. function MessageEvent (type, eventInitDict) {
  371. Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true })
  372. for (var f in eventInitDict) {
  373. if (eventInitDict.hasOwnProperty(f)) {
  374. Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true })
  375. }
  376. }
  377. }