Spaces:
Build error
Build error
| var parse = require('url').parse | |
| var events = require('events') | |
| var https = require('https') | |
| var http = require('http') | |
| var util = require('util') | |
| var httpsOptions = [ | |
| 'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers', | |
| 'rejectUnauthorized', 'secureProtocol', 'servername', 'checkServerIdentity' | |
| ] | |
| var bom = [239, 187, 191] | |
| var colon = 58 | |
| var space = 32 | |
| var lineFeed = 10 | |
| var carriageReturn = 13 | |
| // Beyond 256KB we could not observe any gain in performance | |
| var maxBufferAheadAllocation = 1024 * 256 | |
| // Headers matching the pattern should be removed when redirecting to different origin | |
| var reUnsafeHeader = /^(cookie|authorization)$/i | |
| function hasBom (buf) { | |
| return bom.every(function (charCode, index) { | |
| return buf[index] === charCode | |
| }) | |
| } | |
| /** | |
| * Creates a new EventSource object | |
| * | |
| * @param {String} url the URL to which to connect | |
| * @param {Object} [eventSourceInitDict] extra init params. See README for details. | |
| * @api public | |
| **/ | |
| function EventSource (url, eventSourceInitDict) { | |
| var readyState = EventSource.CONNECTING | |
| var headers = eventSourceInitDict && eventSourceInitDict.headers | |
| var hasNewOrigin = false | |
| Object.defineProperty(this, 'readyState', { | |
| get: function () { | |
| return readyState | |
| } | |
| }) | |
| Object.defineProperty(this, 'url', { | |
| get: function () { | |
| return url | |
| } | |
| }) | |
| var self = this | |
| self.reconnectInterval = 1000 | |
| self.connectionInProgress = false | |
| function onConnectionClosed (message) { | |
| if (readyState === EventSource.CLOSED) return | |
| readyState = EventSource.CONNECTING | |
| _emit('error', new Event('error', {message: message})) | |
| // The url may have been changed by a temporary redirect. If that's the case, | |
| // revert it now, and flag that we are no longer pointing to a new origin | |
| if (reconnectUrl) { | |
| url = reconnectUrl | |
| reconnectUrl = null | |
| hasNewOrigin = false | |
| } | |
| setTimeout(function () { | |
| if (readyState !== EventSource.CONNECTING || self.connectionInProgress) { | |
| return | |
| } | |
| self.connectionInProgress = true | |
| connect() | |
| }, self.reconnectInterval) | |
| } | |
| var req | |
| var lastEventId = '' | |
| if (headers && headers['Last-Event-ID']) { | |
| lastEventId = headers['Last-Event-ID'] | |
| delete headers['Last-Event-ID'] | |
| } | |
| var discardTrailingNewline = false | |
| var data = '' | |
| var eventName = '' | |
| var reconnectUrl = null | |
| function connect () { | |
| var options = parse(url) | |
| var isSecure = options.protocol === 'https:' | |
| options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' } | |
| if (lastEventId) options.headers['Last-Event-ID'] = lastEventId | |
| if (headers) { | |
| var reqHeaders = hasNewOrigin ? removeUnsafeHeaders(headers) : headers | |
| for (var i in reqHeaders) { | |
| var header = reqHeaders[i] | |
| if (header) { | |
| options.headers[i] = header | |
| } | |
| } | |
| } | |
| // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`, | |
| // but for now exists as a backwards-compatibility layer | |
| options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized) | |
| if (eventSourceInitDict && eventSourceInitDict.createConnection !== undefined) { | |
| options.createConnection = eventSourceInitDict.createConnection | |
| } | |
| // If specify http proxy, make the request to sent to the proxy server, | |
| // and include the original url in path and Host headers | |
| var useProxy = eventSourceInitDict && eventSourceInitDict.proxy | |
| if (useProxy) { | |
| var proxy = parse(eventSourceInitDict.proxy) | |
| isSecure = proxy.protocol === 'https:' | |
| options.protocol = isSecure ? 'https:' : 'http:' | |
| options.path = url | |
| options.headers.Host = options.host | |
| options.hostname = proxy.hostname | |
| options.host = proxy.host | |
| options.port = proxy.port | |
| } | |
| // If https options are specified, merge them into the request options | |
| if (eventSourceInitDict && eventSourceInitDict.https) { | |
| for (var optName in eventSourceInitDict.https) { | |
| if (httpsOptions.indexOf(optName) === -1) { | |
| continue | |
| } | |
| var option = eventSourceInitDict.https[optName] | |
| if (option !== undefined) { | |
| options[optName] = option | |
| } | |
| } | |
| } | |
| // Pass this on to the XHR | |
| if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) { | |
| options.withCredentials = eventSourceInitDict.withCredentials | |
| } | |
| req = (isSecure ? https : http).request(options, function (res) { | |
| self.connectionInProgress = false | |
| // Handle HTTP errors | |
| if (res.statusCode === 500 || res.statusCode === 502 || res.statusCode === 503 || res.statusCode === 504) { | |
| _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage})) | |
| onConnectionClosed() | |
| return | |
| } | |
| // Handle HTTP redirects | |
| if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) { | |
| var location = res.headers.location | |
| if (!location) { | |
| // Server sent redirect response without Location header. | |
| _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage})) | |
| return | |
| } | |
| var prevOrigin = new URL(url).origin | |
| var nextOrigin = new URL(location).origin | |
| hasNewOrigin = prevOrigin !== nextOrigin | |
| if (res.statusCode === 307) reconnectUrl = url | |
| url = location | |
| process.nextTick(connect) | |
| return | |
| } | |
| if (res.statusCode !== 200) { | |
| _emit('error', new Event('error', {status: res.statusCode, message: res.statusMessage})) | |
| return self.close() | |
| } | |
| readyState = EventSource.OPEN | |
| res.on('close', function () { | |
| res.removeAllListeners('close') | |
| res.removeAllListeners('end') | |
| onConnectionClosed() | |
| }) | |
| res.on('end', function () { | |
| res.removeAllListeners('close') | |
| res.removeAllListeners('end') | |
| onConnectionClosed() | |
| }) | |
| _emit('open', new Event('open')) | |
| // text/event-stream parser adapted from webkit's | |
| // Source/WebCore/page/EventSource.cpp | |
| var buf | |
| var newBuffer | |
| var startingPos = 0 | |
| var startingFieldLength = -1 | |
| var newBufferSize = 0 | |
| var bytesUsed = 0 | |
| res.on('data', function (chunk) { | |
| if (!buf) { | |
| buf = chunk | |
| if (hasBom(buf)) { | |
| buf = buf.slice(bom.length) | |
| } | |
| bytesUsed = buf.length | |
| } else { | |
| if (chunk.length > buf.length - bytesUsed) { | |
| newBufferSize = (buf.length * 2) + chunk.length | |
| if (newBufferSize > maxBufferAheadAllocation) { | |
| newBufferSize = buf.length + chunk.length + maxBufferAheadAllocation | |
| } | |
| newBuffer = Buffer.alloc(newBufferSize) | |
| buf.copy(newBuffer, 0, 0, bytesUsed) | |
| buf = newBuffer | |
| } | |
| chunk.copy(buf, bytesUsed) | |
| bytesUsed += chunk.length | |
| } | |
| var pos = 0 | |
| var length = bytesUsed | |
| while (pos < length) { | |
| if (discardTrailingNewline) { | |
| if (buf[pos] === lineFeed) { | |
| ++pos | |
| } | |
| discardTrailingNewline = false | |
| } | |
| var lineLength = -1 | |
| var fieldLength = startingFieldLength | |
| var c | |
| for (var i = startingPos; lineLength < 0 && i < length; ++i) { | |
| c = buf[i] | |
| if (c === colon) { | |
| if (fieldLength < 0) { | |
| fieldLength = i - pos | |
| } | |
| } else if (c === carriageReturn) { | |
| discardTrailingNewline = true | |
| lineLength = i - pos | |
| } else if (c === lineFeed) { | |
| lineLength = i - pos | |
| } | |
| } | |
| if (lineLength < 0) { | |
| startingPos = length - pos | |
| startingFieldLength = fieldLength | |
| break | |
| } else { | |
| startingPos = 0 | |
| startingFieldLength = -1 | |
| } | |
| parseEventStreamLine(buf, pos, fieldLength, lineLength) | |
| pos += lineLength + 1 | |
| } | |
| if (pos === length) { | |
| buf = void 0 | |
| bytesUsed = 0 | |
| } else if (pos > 0) { | |
| buf = buf.slice(pos, bytesUsed) | |
| bytesUsed = buf.length | |
| } | |
| }) | |
| }) | |
| req.on('error', function (err) { | |
| self.connectionInProgress = false | |
| onConnectionClosed(err.message) | |
| }) | |
| if (req.setNoDelay) req.setNoDelay(true) | |
| req.end() | |
| } | |
| connect() | |
| function _emit () { | |
| if (self.listeners(arguments[0]).length > 0) { | |
| self.emit.apply(self, arguments) | |
| } | |
| } | |
| this._close = function () { | |
| if (readyState === EventSource.CLOSED) return | |
| readyState = EventSource.CLOSED | |
| if (req.abort) req.abort() | |
| if (req.xhr && req.xhr.abort) req.xhr.abort() | |
| } | |
| function parseEventStreamLine (buf, pos, fieldLength, lineLength) { | |
| if (lineLength === 0) { | |
| if (data.length > 0) { | |
| var type = eventName || 'message' | |
| _emit(type, new MessageEvent(type, { | |
| data: data.slice(0, -1), // remove trailing newline | |
| lastEventId: lastEventId, | |
| origin: new URL(url).origin | |
| })) | |
| data = '' | |
| } | |
| eventName = void 0 | |
| } else if (fieldLength > 0) { | |
| var noValue = fieldLength < 0 | |
| var step = 0 | |
| var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)).toString() | |
| if (noValue) { | |
| step = lineLength | |
| } else if (buf[pos + fieldLength + 1] !== space) { | |
| step = fieldLength + 1 | |
| } else { | |
| step = fieldLength + 2 | |
| } | |
| pos += step | |
| var valueLength = lineLength - step | |
| var value = buf.slice(pos, pos + valueLength).toString() | |
| if (field === 'data') { | |
| data += value + '\n' | |
| } else if (field === 'event') { | |
| eventName = value | |
| } else if (field === 'id') { | |
| lastEventId = value | |
| } else if (field === 'retry') { | |
| var retry = parseInt(value, 10) | |
| if (!Number.isNaN(retry)) { | |
| self.reconnectInterval = retry | |
| } | |
| } | |
| } | |
| } | |
| } | |
| module.exports = EventSource | |
| util.inherits(EventSource, events.EventEmitter) | |
| EventSource.prototype.constructor = EventSource; // make stacktraces readable | |
| ['open', 'error', 'message'].forEach(function (method) { | |
| Object.defineProperty(EventSource.prototype, 'on' + method, { | |
| /** | |
| * Returns the current listener | |
| * | |
| * @return {Mixed} the set function or undefined | |
| * @api private | |
| */ | |
| get: function get () { | |
| var listener = this.listeners(method)[0] | |
| return listener ? (listener._listener ? listener._listener : listener) : undefined | |
| }, | |
| /** | |
| * Start listening for events | |
| * | |
| * @param {Function} listener the listener | |
| * @return {Mixed} the set function or undefined | |
| * @api private | |
| */ | |
| set: function set (listener) { | |
| this.removeAllListeners(method) | |
| this.addEventListener(method, listener) | |
| } | |
| }) | |
| }) | |
| /** | |
| * Ready states | |
| */ | |
| Object.defineProperty(EventSource, 'CONNECTING', {enumerable: true, value: 0}) | |
| Object.defineProperty(EventSource, 'OPEN', {enumerable: true, value: 1}) | |
| Object.defineProperty(EventSource, 'CLOSED', {enumerable: true, value: 2}) | |
| EventSource.prototype.CONNECTING = 0 | |
| EventSource.prototype.OPEN = 1 | |
| EventSource.prototype.CLOSED = 2 | |
| /** | |
| * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed) | |
| * | |
| * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close | |
| * @api public | |
| */ | |
| EventSource.prototype.close = function () { | |
| this._close() | |
| } | |
| /** | |
| * Emulates the W3C Browser based WebSocket interface using addEventListener. | |
| * | |
| * @param {String} type A string representing the event type to listen out for | |
| * @param {Function} listener callback | |
| * @see https://developer.mozilla.org/en/DOM/element.addEventListener | |
| * @see http://dev.w3.org/html5/websockets/#the-websocket-interface | |
| * @api public | |
| */ | |
| EventSource.prototype.addEventListener = function addEventListener (type, listener) { | |
| if (typeof listener === 'function') { | |
| // store a reference so we can return the original function again | |
| listener._listener = listener | |
| this.on(type, listener) | |
| } | |
| } | |
| /** | |
| * Emulates the W3C Browser based WebSocket interface using dispatchEvent. | |
| * | |
| * @param {Event} event An event to be dispatched | |
| * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent | |
| * @api public | |
| */ | |
| EventSource.prototype.dispatchEvent = function dispatchEvent (event) { | |
| if (!event.type) { | |
| throw new Error('UNSPECIFIED_EVENT_TYPE_ERR') | |
| } | |
| // if event is instance of an CustomEvent (or has 'details' property), | |
| // send the detail object as the payload for the event | |
| this.emit(event.type, event.detail) | |
| } | |
| /** | |
| * Emulates the W3C Browser based WebSocket interface using removeEventListener. | |
| * | |
| * @param {String} type A string representing the event type to remove | |
| * @param {Function} listener callback | |
| * @see https://developer.mozilla.org/en/DOM/element.removeEventListener | |
| * @see http://dev.w3.org/html5/websockets/#the-websocket-interface | |
| * @api public | |
| */ | |
| EventSource.prototype.removeEventListener = function removeEventListener (type, listener) { | |
| if (typeof listener === 'function') { | |
| listener._listener = undefined | |
| this.removeListener(type, listener) | |
| } | |
| } | |
| /** | |
| * W3C Event | |
| * | |
| * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event | |
| * @api private | |
| */ | |
| function Event (type, optionalProperties) { | |
| Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }) | |
| if (optionalProperties) { | |
| for (var f in optionalProperties) { | |
| if (optionalProperties.hasOwnProperty(f)) { | |
| Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true }) | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * W3C MessageEvent | |
| * | |
| * @see http://www.w3.org/TR/webmessaging/#event-definitions | |
| * @api private | |
| */ | |
| function MessageEvent (type, eventInitDict) { | |
| Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }) | |
| for (var f in eventInitDict) { | |
| if (eventInitDict.hasOwnProperty(f)) { | |
| Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true }) | |
| } | |
| } | |
| } | |
| /** | |
| * Returns a new object of headers that does not include any authorization and cookie headers | |
| * | |
| * @param {Object} headers An object of headers ({[headerName]: headerValue}) | |
| * @return {Object} a new object of headers | |
| * @api private | |
| */ | |
| function removeUnsafeHeaders (headers) { | |
| var safe = {} | |
| for (var key in headers) { | |
| if (reUnsafeHeader.test(key)) { | |
| continue | |
| } | |
| safe[key] = headers[key] | |
| } | |
| return safe | |
| } | |