UNPKG

taro-sockjs-client

Version:
436 lines (386 loc) 12.5 kB
import './shims' import URL from 'url-parse' import inherits from './utils/inherits' import JSON3 from 'json3' import random from './utils/random' import escape from './utils/escape' import urlUtils from './utils/url' import eventUtils from './utils/event' import transport from './utils/transport' import objectUtils from './utils/object' import browser from './utils/browser' import log from './utils/log' import Event from './event/event' import EventTarget from './event/eventtarget' import loc from './location' import CloseEvent from './event/close' import TransportMessageEvent from './event/trans-message' import InfoReceiver from './info-receiver' import version from './version' import iframeBootstrap from './iframe-bootstrap' import debug from './utils/debug' let transports // follow constructor steps defined at http://dev.w3.org/html5/websockets/#the-websocket-interface function SockJS(url, protocols, options) { if (!(this instanceof SockJS)) { return new SockJS(url, protocols, options) } if (arguments.length < 1) { throw new TypeError( "Failed to construct 'SockJS: 1 argument from , but only 0 present", ) } EventTarget.call(this) this.readyState = SockJS.CONNECTING this.extensions = '' this.protocol = '' // non-standard extension options = options || {} if (options.protocols_whitelist) { log.warn("'protocols_whitelist' is DEPRECATED. Use 'transports' instead.") } this._transportsWhitelist = options.transports this._transportOptions = options.transportOptions || {} this._timeout = options.timeout || 0 let sessionId = options.sessionId || 8 if (typeof sessionId === 'function') { this._generateSessionId = sessionId } else if (typeof sessionId === 'number') { this._generateSessionId = function () { return random.string(sessionId) } } else { throw new TypeError( 'If sessionId is used in the options, it needs to be a number or a function.', ) } this._server = options.server || random.numberString(1000) // Step 1 of WS spec - parse and validate the url. Issue #8 let parsedUrl = new URL(url) if (!parsedUrl.host || !parsedUrl.protocol) { throw new SyntaxError("The URL '" + url + "' is invalid") } else if (parsedUrl.hash) { throw new SyntaxError('The URL must not contain a fragment') } else if ( parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:' ) { throw new SyntaxError( "The URL's scheme must be either 'http:' or 'https:'. '" + parsedUrl.protocol + "' is not allowed.", ) } let secure = parsedUrl.protocol === 'https:' // Step 2 - don't allow secure origin with an insecure protocol if (loc.protocol === 'https:' && !secure) { // exception is 127.0.0.0/8 and ::1 urls if (!urlUtils.isLoopbackAddr(parsedUrl.hostname)) { throw new Error( 'SecurityError: An insecure SockJS connection may not be initiated from a page loaded over HTTPS', ) } } // Step 3 - check port access - no need here // Step 4 - parse protocols argument if (!protocols) { protocols = [] } else if (!Array.isArray(protocols)) { protocols = [protocols] } // Step 5 - check protocols argument let sortedProtocols = protocols.sort() sortedProtocols.forEach(function (proto, i) { if (!proto) { throw new SyntaxError("The protocols entry '" + proto + "' is invalid.") } if (i < sortedProtocols.length - 1 && proto === sortedProtocols[i + 1]) { throw new SyntaxError( "The protocols entry '" + proto + "' is duplicated.", ) } }) // Step 6 - convert origin let o = urlUtils.getOrigin(loc.href) this._origin = o ? o.toLowerCase() : null // remove the trailing slash parsedUrl.set('pathname', parsedUrl.pathname.replace(/\/+$/, '')) // store the sanitized url this.url = parsedUrl.href debug('using url', this.url) // Step 7 - start connection in background // obtain server info // http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-26 this._urlInfo = { nullOrigin: !browser.hasDomain(), sameOrigin: urlUtils.isOriginEqual(this.url, loc.href), sameScheme: urlUtils.isSchemeEqual(this.url, loc.href), } this._ir = new InfoReceiver(this.url, this._urlInfo) this._ir.once('finish', this._receiveInfo.bind(this)) } inherits(SockJS, EventTarget) function userSetCode(code) { return code === 1000 || (code >= 3000 && code <= 4999) } SockJS.prototype.close = function (code, reason) { // Step 1 if (code && !userSetCode(code)) { throw new Error('InvalidAccessError: Invalid code') } // Step 2.4 states the max is 123 bytes, but we are just checking length if (reason && reason.length > 123) { throw new SyntaxError('reason argument has an invalid length') } // Step 3.1 if (this.readyState === SockJS.CLOSING || this.readyState === SockJS.CLOSED) { return } // TODO look at docs to determine how to set this let wasClean = true this._close(code || 1000, reason || 'Normal closure', wasClean) } SockJS.prototype.send = function (data) { // #13 - convert anything non-string to string // TODO this currently turns objects into [object Object] if (typeof data !== 'string') { data = '' + data } if (this.readyState === SockJS.CONNECTING) { throw new Error( 'InvalidStateError: The connection has not been established yet', ) } if (this.readyState !== SockJS.OPEN) { return } this._transport.send(escape.quote(data)) } SockJS.version = version SockJS.CONNECTING = 0 SockJS.OPEN = 1 SockJS.CLOSING = 2 SockJS.CLOSED = 3 SockJS.prototype._receiveInfo = function (info, rtt) { debug('_receiveInfo', rtt) this._ir = null if (!info) { this._close(1002, 'Cannot connect to server') return } // establish a round-trip timeout (RTO) based on the // round-trip time (RTT) this._rto = this.countRTO(rtt) // allow server to override url used for the actual transport this._transUrl = info.base_url ? info.base_url : this.url info = objectUtils.extend(info, this._urlInfo) debug('info', info) // determine list of desired and supported transports let enabledTransports = transports.filterToEnabled( this._transportsWhitelist, info, ) this._transports = enabledTransports.main debug(this._transports.length + ' enabled transports') this._connect() } SockJS.prototype._connect = async function () { for ( let Transport = this._transports.shift(); Transport; Transport = this._transports.shift() ) { debug('attempt', Transport.transportName) if (Transport.needBody) { if ( !global.document.body || (typeof global.document.readyState !== 'undefined' && global.document.readyState !== 'complete' && global.document.readyState !== 'interactive') ) { debug('waiting for body') this._transports.unshift(Transport) eventUtils.attachEvent('load', this._connect.bind(this)) return } } // calculate timeout based on RTO and round trips. Default to 5s let timeoutMs = Math.max( this._timeout, this._rto * Transport.roundTrips || 5000, ) // this._transportTimeoutId = setTimeout( // this._transportTimeout.bind(this), // timeoutMs, // ) // debug('using timeout', timeoutMs) let transportUrl = urlUtils.addPath( this._transUrl, '/' + this._server + '/' + this._generateSessionId(), ) let options = this._transportOptions[Transport.transportName] debug('transport url', transportUrl) let transportObj = new Transport(transportUrl, this._transUrl, options) if (transportObj.then) { transportObj.then((transportObj) => { transportObj.on('message', this._transportMessage.bind(this)) transportObj.once('close', this._transportClose.bind(this)) this._transport = transportObj }) } else { transportObj.on('message', this._transportMessage.bind(this)) transportObj.once('close', this._transportClose.bind(this)) this._transport = transportObj } transportObj.transportName = Transport.transportName return } this._close(2000, 'All transports failed', false) } SockJS.prototype._transportTimeout = function () { debug('_transportTimeout') if (this.readyState === SockJS.CONNECTING) { if (this._transport) { this._transport.close() } this._transportClose(2007, 'Transport timed out') } } SockJS.prototype._transportMessage = function (msg) { debug('_transportMessage', msg) let self = this, type = msg.slice(0, 1), content = msg.slice(1), payload // first check for messages that don't need a payload switch (type) { case 'o': this._open() return case 'h': this.dispatchEvent(new Event('heartbeat')) debug('heartbeat', this.transport) return } if (content) { try { payload = JSON3.parse(content) } catch (e) { debug('bad json', content) } } if (typeof payload === 'undefined') { debug('empty payload', content) return } switch (type) { case 'a': if (Array.isArray(payload)) { payload.forEach(function (p) { debug('message', self.transport, p) self.dispatchEvent(new TransportMessageEvent(p)) }) } break case 'm': debug('message', this.transport, payload) this.dispatchEvent(new TransportMessageEvent(payload)) break case 'c': if (Array.isArray(payload) && payload.length === 2) { this._close(payload[0], payload[1], true) } break } } SockJS.prototype._transportClose = function (code, reason) { debug('_transportClose', this.transport, code, reason) if (this._transport) { this._transport.removeAllListeners() this._transport = null this.transport = null } if ( !userSetCode(code) && code !== 2000 && this.readyState === SockJS.CONNECTING ) { this._connect() return } this._close(code, reason) } SockJS.prototype._open = function () { debug( '_open', this._transport && this._transport.transportName, this.readyState, ) if (this.readyState === SockJS.CONNECTING) { if (this._transportTimeoutId) { clearTimeout(this._transportTimeoutId) this._transportTimeoutId = null } this.readyState = SockJS.OPEN this.transport = this._transport.transportName this.dispatchEvent(new Event('open')) debug('connected', this.transport) } else { // The server might have been restarted, and lost track of our // connection. this._close(1006, 'Server lost session') } } SockJS.prototype._close = function (code, reason, wasClean) { debug('_close', this.transport, code, reason, wasClean, this.readyState) let forceFail = false if (this._ir) { forceFail = true this._ir.close() this._ir = null } if (this._transport) { this._transport.close() this._transport = null this.transport = null } if (this.readyState === SockJS.CLOSED) { throw new Error('InvalidStateError: SockJS has already been closed') } this.readyState = SockJS.CLOSING setTimeout( function () { this.readyState = SockJS.CLOSED if (forceFail) { this.dispatchEvent(new Event('error')) } let e = new CloseEvent('close') e.wasClean = wasClean || false e.code = code || 1000 e.reason = reason this.dispatchEvent(e) this.onmessage = this.onclose = this.onerror = null debug('disconnected') }.bind(this), 0, ) } // See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/ // and RFC 2988. SockJS.prototype.countRTO = function (rtt) { // In a local environment, when using IE8/9 and the `jsonp-polling` // transport the time needed to establish a connection (the time that pass // from the opening of the transport to the call of `_dispatchOpen`) is // around 200msec (the lower bound used in the article above) and this // causes spurious timeouts. For this reason we calculate a value slightly // larger than that used in the article. if (rtt > 100) { return 4 * rtt // rto > 400msec } return 300 + rtt // 300msec < rto <= 400msec } export default function wrapSockJs(availableTransports) { transports = transport(availableTransports) iframeBootstrap(SockJS, availableTransports) return SockJS }