123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- /* eslint-disable */
-
- import { $build, Strophe } from 'strophe.js';
-
- /**
- * StropheJS - Stream Management XEP-0198
- *
- * This plugin implements stream management ACK capabilities of the specs XEP-0198.
- * Note: Resumption is not supported in this current implementation.
- *
- * Reference: http://xmpp.org/extensions/xep-0198.html
- *
- * @class streamManagement
- */
- Strophe.addConnectionPlugin('streamManagement', {
-
- /**
- * @property {Boolean} logging: Set to true to enable logging regarding out of sync stanzas.
- */
- logging: false,
-
- /**
- * @property {Boolean} autoSendCountOnEveryIncomingStanza: Set to true to send an 'a' response after every stanza.
- * @default false
- * @public
- */
- autoSendCountOnEveryIncomingStanza: false,
-
- /**
- * @property {Integer} requestResponseInterval: Set this value to send a request for counter on very interval
- * number of stanzas sent. Set to 0 to disable.
- * @default 5
- * @public
- */
- requestResponseInterval: 5,
-
- /**
- * @property {Pointer} _c: Strophe connection instance.
- * @private
- */
- _c: null,
-
- /**
- * @property {String} _NS XMPP Namespace.
- * @private
- */
- _NS: 'urn:xmpp:sm:3',
-
- /**
- * @property {Boolean} _isStreamManagementEnabled
- * @private
- */
- _isStreamManagementEnabled: false,
-
- /**
- * @property {Integer} _serverProcesssedStanzasCounter: Keeps count of stanzas confirmed processed by the server.
- * The server is the source of truth of this value. It is the 'h' attribute on the latest 'a' element received
- * from the server.
- * @private
- */
- _serverProcesssedStanzasCounter: null,
-
- /**
- * @property {Integer} _clientProcessedStanzasCounter: Counter of stanzas received by the client from the server.
- * Client is the source of truth of this value. It is the 'h' attribute in the 'a' sent from the client to
- * the server.
- * @private
- */
- _clientProcessedStanzasCounter: null,
-
- /**
- * @property {Integer} _clientSentStanzasCounter
- * @private
- */
- _clientSentStanzasCounter: null,
-
- /**
- * Stores a reference to Strophe connection xmlOutput function to wrap counting functionality.
- * @method _originalXMLOutput
- * @type {Handler}
- * @private
- */
- _originalXMLOutput: null,
-
- /**
- * @property {Handler} _requestHandler: Stores reference to handler that process count request from server.
- * @private
- */
- _requestHandler: null,
-
- /**
- * @property {Handler} _incomingHandler: Stores reference to handler that processes incoming stanzas count.
- * @private
- */
- _incomingHandler: null,
-
- /**
- * @property {Integer} _requestResponseIntervalCount: Counts sent stanzas since last response request.
- */
- _requestResponseIntervalCount: 0,
-
- /**
- * @property {boolean} _isSupported: indicates whether or not the server has advertised support for the stream
- * management namespace.
- */
- _isSupported: false,
-
- /**
- * @property {Queue} _unacknowledgedStanzas: Maintains a list of packet ids for stanzas which have yet to be acknowledged.
- */
- _unacknowledgedStanzas: [],
-
- /**
- * @property {Array} _acknowledgedStanzaListeners: Stores callbacks for each stanza acknowledged by the server.
- * Provides the packet id of the stanza as a parameter.
- * @private
- */
- _acknowledgedStanzaListeners: [],
-
- addAcknowledgedStanzaListener: function(listener) {
- this._acknowledgedStanzaListeners.push(listener);
- },
-
- enable: function(resume) {
- if (!this._isSupported) {
- throw new Error('The server doesn\'t support urn:xmpp:sm:3 namespace');
- } else if (this._connectionStatus !== Strophe.Status.CONNECTED) {
- throw new Error('enable() can only be called in the CONNECTED state');
- }
- this._c.send($build('enable', { xmlns: this._NS, resume }));
- this._c.flush();
- this._c.pause();
- },
-
- getResumeToken: function() {
- return this._resumeToken;
- },
-
- isSupported() {
- return this._isSupported;
- },
-
- resume: function() {
- if (!this.getResumeToken()) {
- throw new Error('No resume token');
- }
- if (this._connectionStatus !== Strophe.Status.DISCONNECTED) {
- throw new Error('resume() can only be called in the DISCONNECTED state');
- }
-
- this._c.options.explicitResourceBinding = true;
- this._resuming = true;
-
- this._originalConnect.apply(this._c, this._connectArgs);
- },
-
- requestAcknowledgement: function() {
- if (this._connectionStatus !== Strophe.Status.CONNECTED) {
- throw new Error('requestAcknowledgement() can only be called in the CONNECTED state');
- }
- this._requestResponseIntervalCount = 0;
- this._c.send($build('r', { xmlns: this._NS }));
- },
-
- getOutgoingCounter: function() {
- return this._clientSentStanzasCounter;
- },
-
- getIncomingCounter: function() {
- return this._clientProcessedStanzasCounter;
- },
-
- init: function(conn) {
- this._c = conn;
- Strophe.addNamespace('SM', this._NS);
-
- // Storing original xmlOutput function to use additional logic
- this._originalXMLOutput = this._c.xmlOutput;
- this._c.xmlOutput = this.xmlOutput.bind(this);
-
- this._originalConnect = this._c.connect;
- this._c.connect = this._interceptConnectArgs.bind(this);
-
- this._originalOnStreamFeaturesAfterSASL = this._c._onStreamFeaturesAfterSASL;
- this._c._onStreamFeaturesAfterSASL = this._onStreamFeaturesAfterSASL.bind(this);
-
- this._originalDoDisconnect = this._c._doDisconnect;
- this._c._doDisconnect = this._interceptDoDisconnect.bind(this);
-
- this._originalDisconnect = this._c.disconnect;
- this._c.disconnect = this._interceptDisconnect.bind(this);
- },
-
- _interceptDisconnect: function() {
- this._resumeToken = undefined;
- this._originalDisconnect.apply(this._c, arguments);
- },
-
- _interceptDoDisconnect: function() {
- if (this.getResumeToken()
- && !this._resuming
- && this._c.connected && !this._c.disconnecting) {
- this._resumeState = {
- handlers: this._c.handlers,
- timedHandlers: this._c.timedHandlers,
- removeTimeds: this._c.removeTimeds,
- removeHandlers: this._c.removeHandlers,
- addTimeds: this._c.addTimeds,
- addHandlers: this._c.addHandlers
- };
- this._storedJid = this._c.jid;
-
- this.logging && Strophe.debug('SM stored resume state, handler count: ' + this._resumeState.handlers.length);
- }
-
- // Remove any queued stanzas from the buffer that have failed to send while the socket was closed,
- // as they would interfere with the resume flow. They will be resent anyway.
- this._c._data = [];
-
- this._originalDoDisconnect.apply(this._c, arguments);
- },
-
- _interceptConnectArgs: function() {
- this._connectArgs = arguments;
-
- this._originalConnect.apply(this._c, arguments);
- },
-
- _onStreamFeaturesAfterSASL: function(elem) {
- this._isSupported = elem.getElementsByTagNameNS(this._NS, "sm").length > 0;
-
- return this._originalOnStreamFeaturesAfterSASL.apply(this._c, arguments);
- },
-
- statusChanged: function (status) {
- this._connectionStatus = status;
- if (!this.getResumeToken()
- && (status === Strophe.Status.CONNECTED || status === Strophe.Status.DISCONNECTED)) {
- this.logging && Strophe.debug('SM reset state');
-
- this._serverProcesssedStanzasCounter = 0;
- this._clientProcessedStanzasCounter = 0;
-
- this._clientSentStanzasCounter = 0;
-
- this._isStreamManagementEnabled = false;
- this._requestResponseIntervalCount = 0;
-
- // FIXME not described in JSDocs
- this._resuming = false;
-
- if (status === Strophe.Status.DISCONNECTED) {
- this._isSupported = false;
- }
-
- this._unacknowledgedStanzas = [];
-
- if (this._requestHandler) {
- this._c.deleteHandler(this._requestHandler);
- }
-
- if (this._incomingHandler) {
- this._c.deleteHandler(this._incomingHandler);
- }
-
- this._requestHandler = this._c.addHandler(this._handleServerRequestHandler.bind(this), this._NS, 'r');
- this._ackHandler = this._c.addHandler(this._handleServerAck.bind(this), this._NS, 'a');
- this._incomingHandler = this._c.addHandler(this._incomingStanzaHandler.bind(this));
-
- // FIXME handler instances stored, but never used
- this._enabledHandler = this._c._addSysHandler(this._handleEnabled.bind(this), this._NS, 'enabled');
- this._resumeFailedHandler = this._c._addSysHandler(this._handleResumeFailed.bind(this), this._NS, 'failed');
- this._resumedHandler = this._c._addSysHandler(this._handleResumed.bind(this), this._NS,'resumed');
-
- } else if (status === Strophe.Status.BINDREQUIRED) {
- this._c.jid = this._storedJid;
-
- // Restore Strophe handlers
- for (const h of (this._resumeState.handlers || [])
- .concat(this._resumeState.addHandlers || [])) {
- this._c._addSysHandler(h.handler, h.ns, h.name, h.type, h.id);
- }
- for (const h of (this._resumeState.timedHandlers || [])
- .concat(this._resumeState.addTimeds)) {
- this._c.addTimedHandler(h.period, h.handler);
- }
- for (const h of (this._resumeState.removeTimeds || [])
- .concat(this._resumeState.removeHandlers || [])) {
- this._c.deleteTimedHandler(h);
- }
-
- // FIXME check conditions if there's session ID and if enabled
- this._c.send($build('resume', {
- xmlns: this._NS,
- h: this._clientProcessedStanzasCounter,
- previd: this._resumeToken
- }));
- this._c.flush();
- } else if (status === Strophe.Status.ERROR) {
- this.logging && Strophe.debug('SM cleared resume token on error');
- this._resumeToken = undefined;
- }
- },
-
- /**
- * This method overrides the send method implemented by Strophe.Connection
- * to count outgoing stanzas
- *
- * @method Send
- * @public
- */
- xmlOutput: function(elem) {
- if (Strophe.isTagEqual(elem, 'iq') ||
- Strophe.isTagEqual(elem, 'presence') ||
- Strophe.isTagEqual(elem, 'message')) {
- this._increaseSentStanzasCounter(elem);
- }
-
- return this._originalXMLOutput.call(this._c, elem);
- },
-
- _handleEnabled: function(elem) {
- this._isStreamManagementEnabled = true;
- // FIXME fail if requested, but not enabled
- this._resumeToken = elem.getAttribute('resume') === 'true' && elem.getAttribute('id');
-
- this._c.resume();
-
- return true;
- },
-
- _handleResumeFailed: function(elem) {
- const error = elem && (
- (elem.firstElementChild && elem.firstElementChild.tagName)
- || (elem.firstChild && elem.firstChild.tagName));
-
- this._c._changeConnectStatus(Strophe.Status.ERROR, error, elem);
- this._c._doDisconnect();
-
- return true;
- },
-
- _handleResumed: function(elem) {
- // FIXME check if in the correct state
- var handledCount = parseInt(elem.getAttribute('h'));
- this._handleAcknowledgedStanzas(handledCount, this._serverProcesssedStanzasCounter);
-
- this._resuming = false;
- this._c.do_bind = false; // No need to bind our resource anymore
- this._c.authenticated = true;
- this._c.restored = true;
-
- if (this._unacknowledgedStanzas.length > 0) {
- this.logging && Strophe.debug('SM Sending unacknowledged stanzas', this._unacknowledgedStanzas);
- for(const stanza of this._unacknowledgedStanzas) {
- this._c.send(stanza);
- }
- } else {
- this.logging && Strophe.debug('SM No unacknowledged stanzas', this._unacknowledgedStanzas);
- }
-
- this._c._changeConnectStatus(Strophe.Status.CONNECTED, null);
-
- return true;
- },
-
- _incomingStanzaHandler: function(elem) {
- if (Strophe.isTagEqual(elem, 'iq') || Strophe.isTagEqual(elem, 'presence') || Strophe.isTagEqual(elem, 'message')) {
- this._increaseReceivedStanzasCounter();
-
- if (this.autoSendCountOnEveryIncomingStanza) {
- this._answerProcessedStanzas();
- }
- }
-
- return true;
- },
-
- _handleAcknowledgedStanzas: function(reportedHandledCount, lastKnownHandledCount) {
- var delta = reportedHandledCount - lastKnownHandledCount;
-
- if (delta < 0) {
- this._throwError('New reported stanza count lower than previous. New: ' + reportedHandledCount + ' - Previous: ' + lastKnownHandledCount);
- }
-
- if (delta > this._unacknowledgedStanzas.length) {
- this._throwError('Higher reported acknowledge count than unacknowledged stanzas. Reported Acknowledge Count: ' + delta + ' - Unacknowledge Stanza Count: ' + this._unacknowledgedStanzas.length + ' - New: ' + reportedHandledCount + ' - Previous: ' + lastKnownHandledCount);
- }
-
- for(var i = 0; i < delta; i++) {
- var stanza = this._unacknowledgedStanzas.shift();
- for (var j = 0; j < this._acknowledgedStanzaListeners.length; j++) {
- this._acknowledgedStanzaListeners[j](stanza);
- }
- }
-
- if (this.logging && this._unacknowledgedStanzas.length > 0) {
- Strophe.warn('SM Unacknowledged stanzas', this._unacknowledgedStanzas);
- }
-
- this._serverProcesssedStanzasCounter = reportedHandledCount;
-
- if (this.requestResponseInterval > 0) {
- this._requestResponseIntervalCount = 0;
- }
- },
-
- _handleServerRequestHandler: function() {
- this._answerProcessedStanzas();
-
- return true;
- },
-
- _handleServerAck: function(elem){
- var handledCount = parseInt(elem.getAttribute('h'));
- this._handleAcknowledgedStanzas(handledCount, this._serverProcesssedStanzasCounter);
-
- return true;
- },
-
- _answerProcessedStanzas: function() {
- if (this._isStreamManagementEnabled) {
- this._c.send($build('a', { xmlns: this._NS, h: this._clientProcessedStanzasCounter }));
- }
- },
-
- _increaseSentStanzasCounter: function(elem) {
- if (this._isStreamManagementEnabled) {
- if (this._unacknowledgedStanzas.indexOf(elem) !== -1) {
-
- return;
- }
-
- this._unacknowledgedStanzas.push(elem);
- this._clientSentStanzasCounter++;
-
- if (this.requestResponseInterval > 0) {
- this._requestResponseIntervalCount++;
-
- if (this._requestResponseIntervalCount === this.requestResponseInterval) {
- // FIXME Can not call send from onIdle.
- setTimeout(() => {
- if (this._connectionStatus === Strophe.Status.CONNECTED) {
- this.requestAcknowledgement();
- }
- }, 1);
- }
- }
- }
- },
-
- _increaseReceivedStanzasCounter: function() {
- if (this._isStreamManagementEnabled) {
- this._clientProcessedStanzasCounter++;
- }
- },
-
- _throwError: function(msg) {
- Strophe.error(msg);
- throw new Error(msg);
- }
-
- });
|