UNPKG

strophe.js

Version:

Strophe.js is an XMPP library for JavaScript

1,226 lines (1,225 loc) 73.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import Handler from './handler'; import TimedHandler from './timed-handler'; import Builder, { $build, $iq, $pres } from './builder'; import log from './log'; import { ErrorCondition, NS, Status } from './constants'; import SASLAnonymous from './sasl-anon'; import SASLExternal from './sasl-external'; import SASLOAuthBearer from './sasl-oauthbearer'; import SASLPlain from './sasl-plain'; import SASLSHA1 from './sasl-sha1'; import SASLSHA256 from './sasl-sha256'; import SASLSHA384 from './sasl-sha384'; import SASLSHA512 from './sasl-sha512'; import SASLXOAuth2 from './sasl-xoauth2'; import { addCookies, forEachChild, getBareJidFromJid, getDomainFromJid, getNodeFromJid, getResourceFromJid, getText, handleError, toElement, } from './utils'; import { SessionError } from './errors'; import Bosh from './transports/bosh'; import WorkerWebsocket from './transports/worker-websocket'; import Websocket from './transports/websocket'; import StreamManagement, { SessionStorageBackend, StreamManagementMirror, isCountableStanza, toStanzaView, } from './stream-management'; /** * _Private_ variable Used to store plugin names that need * initialization during Connection construction. */ const connectionPlugins = {}; /** * _Private_ registry of optional, environment-specific transports keyed by the * `protocol` option that selects them. The Node-only XEP-0114 component * transport registers itself here (see the Node entry point) so that its * Node-only dependencies never reach the browser bundle. */ const transportProtocols = {}; /** * **XMPP Connection manager** * * This class is the main part of Strophe. It manages a BOSH or websocket * connection to an XMPP server and dispatches events to the user callbacks * as data arrives. * * It supports various authentication mechanisms (e.g. SASL PLAIN, SASL SCRAM), * and more can be added via * {@link Connection#registerSASLMechanisms|registerSASLMechanisms()}. * * After creating a Connection object, the user will typically * call {@link Connection#connect|connect()} with a user supplied callback * to handle connection level events like authentication failure, * disconnection, or connection complete. * * The user will also have several event handlers defined by using * {@link Connection#addHandler|addHandler()} and * {@link Connection#addTimedHandler|addTimedHandler()}. * These will allow the user code to respond to interesting stanzas or do * something periodically with the connection. These handlers will be active * once authentication is finished. * * To send data to the connection, use {@link Connection#send|send()}. * * @memberof Strophe */ class Connection { /** * Create and initialize a {@link Connection} object. * * The transport-protocol for this connection will be chosen automatically * based on the given service parameter. URLs starting with "ws://" or * "wss://" will use WebSockets, URLs starting with "http://", "https://" * or without a protocol will use [BOSH](https://xmpp.org/extensions/xep-0124.html). * * To make Strophe connect to the current host you can leave out the protocol * and host part and just pass the path: * * const conn = new Strophe.Connection("/http-bind/"); * * @param service - The BOSH or WebSocket service URL. * @param options - A object containing configuration options */ constructor(service, options = {}) { // The service URL this.service = service; // Configuration options this.options = options; this.setProtocol(); this._smHandlers = []; if (options.enableStreamManagement) { if (options.worker) { // Under a shared worker the page hosts no SM engine since // the worker owns all counting and queueing. The mirror // only reflects the session state so that hasResumed() // and friends keep working in every tab. this.sm = new StreamManagementMirror(); } else { // The engine is constructed regardless of the current // transport since embedders may swap `_proto` after construction. const smOptions = Object.assign({}, (options.streamManagement || {})); if (!smOptions.storage && typeof sessionStorage !== 'undefined') { smOptions.storage = new SessionStorageBackend(); } // The SM engine emits nonzas (and re-sends queued stanzas) as // strings. They are pushed directly onto the send queue and // they ride the same FIFO, so an <r/> goes out after the // stanzas it covers. this.sm = new StreamManagement((data) => { this._data.push(toElement(data)); this._proto._send(); }, smOptions); } } /* The connected JID. */ this.jid = ''; /* the JIDs domain */ this.domain = null; /* stream:features */ this.features = null; // SASL this._sasl_data = {}; this.do_bind = false; this.do_session = false; this.mechanisms = {}; this.timedHandlers = []; this.handlers = []; this.removeTimeds = []; this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; this.protocolErrorHandlers = { 'HTTP': {}, 'websocket': {}, }; this._idleTimeout = null; this._disconnectTimeout = null; this.authenticated = false; this.connected = false; this.disconnecting = false; this.do_authentication = true; this.paused = false; this.restored = false; this._data = []; this._uniqueId = 0; this._sasl_success_handler = null; this._sasl_failure_handler = null; this._sasl_challenge_handler = null; // Max retries before disconnecting this.maxRetries = 5; // Call onIdle callback every 1/10th of a second this._scheduleIdle(); addCookies(this.options.cookies); this.registerSASLMechanisms(this.options.mechanisms); // A client must always respond to incoming IQ "set" and "get" stanzas. // See https://datatracker.ietf.org/doc/html/rfc6120#section-8.2.3 // // This is a fallback handler which gets called when no other handler // was called for a received IQ "set" or "get". this.iqFallbackHandler = new Handler((iq) => { this.send($iq({ type: 'error', id: iq.getAttribute('id') }) .c('error', { 'type': 'cancel' }) .c('service-unavailable', { 'xmlns': NS.STANZAS })); return false; }, null, null, ['get', 'set'], null, null); // initialize plugins for (const k in connectionPlugins) { if (Object.prototype.hasOwnProperty.call(connectionPlugins, k)) { const F = function () { }; F.prototype = connectionPlugins[k]; // @ts-ignore this[k] = new F(); // @ts-ignore this[k].init(this); } } } /** * Extends the Connection object with the given plugin. * @param name - The name of the extension. * @param ptype - The plugin's prototype. */ static addConnectionPlugin(name, ptype) { connectionPlugins[name] = ptype; } /** * Register an optional transport, selectable via the `protocol` connection * option. Used to plug in environment-specific transports (such as the * Node-only XEP-0114 component transport) without the core browser build * depending on them. * @param name - The `protocol` option value that selects this transport. * @param manager - The transport (protocol-manager) constructor. */ static addProtocol(name, manager) { transportProtocols[name] = manager; } /** * Select protocal based on this.options or this.service */ setProtocol() { const proto = this.options.protocol || ''; const RegisteredTransport = transportProtocols[proto]; if (RegisteredTransport) { this._proto = new RegisteredTransport(this); } else if (this.options.worker) { this._proto = new WorkerWebsocket(this); } else if (this.service.indexOf('ws:') === 0 || this.service.indexOf('wss:') === 0 || proto.indexOf('ws') === 0) { this._proto = new Websocket(this); } else if (proto) { throw new Error(`Strophe: unknown connection protocol "${proto}". Valid values are "ws" and "wss"; ` + `other transports must first be registered with Connection.addProtocol().`); } else { this._proto = new Bosh(this); } } /** * Reset the connection. * * This function should be called after a connection is disconnected * before that connection is reused. */ reset() { var _a; this._proto._reset(); // In-memory SM state only. Persisted (resumable) state is kept and // reloaded when the next stream advertises SM support. (_a = this.sm) === null || _a === void 0 ? void 0 : _a.reset(); // SASL this.do_session = false; this.do_bind = false; // handler lists this.timedHandlers = []; this.handlers = []; this.removeTimeds = []; this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; this.authenticated = false; this.connected = false; this.disconnecting = false; this.restored = false; this._data = []; this._requests = []; this._uniqueId = 0; } /** * @returns true if the current session was established by resuming a * previous one via XEP-0198 Stream Management (in which case the * previously bound resource is still valid and roster/presence * state was retained by the server). */ hasResumed() { var _a; return !!((_a = this.sm) === null || _a === void 0 ? void 0 : _a.resumed); } /** * @returns true if a XEP-0198 Stream Management session is currently * active (i.e. <enabled/> was received or a session was resumed). */ isStreamManagementEnabled() { var _a; return !!((_a = this.sm) === null || _a === void 0 ? void 0 : _a.enabled); } /** * Pause the request manager. * * This will prevent Strophe from sending any more requests to the * server. This is very useful for temporarily pausing * BOSH-Connections while a lot of send() calls are happening quickly. * This causes Strophe to send the data in a single request, saving * many request trips. */ pause() { this.paused = true; } /** * Resume the request manager. * * This resumes after pause() has been called. */ resume() { this.paused = false; } /** * Generate a unique ID for use in <iq/> elements. * * All <iq/> stanzas are required to have unique id attributes. This * function makes creating these easy. Each connection instance has * a counter which starts from zero, and the value of this counter * plus a colon followed by the suffix becomes the unique id. If no * suffix is supplied, the counter is used as the unique id. * * Suffixes are used to make debugging easier when reading the stream * data, and their use is recommended. The counter resets to 0 for * every new connection for the same reason. For connections to the * same server that authenticate the same way, all the ids should be * the same, which makes it easy to see changes. This is useful for * automated testing as well. * * @param suffix - A optional suffix to append to the id. * @returns A unique string to be used for the id attribute. */ getUniqueId(suffix) { const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); if (typeof suffix === 'string' || typeof suffix === 'number') { return uuid + ':' + suffix; } else { return uuid + ''; } } /** * Register a handler function for when a protocol (websocker or HTTP) * error occurs. * * NOTE: Currently only HTTP errors for BOSH requests are handled. * Patches that handle websocket errors would be very welcome. * * @example * function onError(err_code){ * //do stuff * } * * const conn = Strophe.connect('http://example.com/http-bind'); * conn.addProtocolErrorHandler('HTTP', 500, onError); * // Triggers HTTP 500 error and onError handler will be called * conn.connect('user_jid@incorrect_jabber_host', 'secret', onConnect); * * @param protocol - 'HTTP' or 'websocket' * @param status_code - Error status code (e.g 500, 400 or 404) * @param callback - Function that will fire on Http error */ addProtocolErrorHandler(protocol, status_code, callback) { this.protocolErrorHandlers[protocol][status_code] = callback; } /** * Starts the connection process. * * As the connection process proceeds, the user supplied callback will * be triggered multiple times with status updates. The callback * should take two arguments - the status code and the error condition. * * The status code will be one of the values in the Strophe.Status * constants. The error condition will be one of the conditions * defined in RFC 3920 or the condition 'strophe-parsererror'. * * The Parameters _wait_, _hold_ and _route_ are optional and only relevant * for BOSH connections. Please see XEP 124 for a more detailed explanation * of the optional parameters. * * @param jid - The user's JID. This may be a bare JID, * or a full JID. If a node is not supplied, SASL OAUTHBEARER or * SASL ANONYMOUS authentication will be attempted (OAUTHBEARER will * process the provided password value as an access token). * (String or Object) pass - The user's password, or an object containing * the users SCRAM client and server keys, in a fashion described as follows: * * { name: String, representing the hash used (eg. SHA-1), * salt: String, base64 encoded salt used to derive the client key, * iter: Int, the iteration count used to derive the client key, * ck: String, the base64 encoding of the SCRAM client key * sk: String, the base64 encoding of the SCRAM server key * } * @param pass - The user password * @param callback - The connect callback function. * @param wait - The optional HTTPBIND wait value. This is the * time the server will wait before returning an empty result for * a request. The default setting of 60 seconds is recommended. * @param hold - The optional HTTPBIND hold value. This is the * number of connections the server will hold at one time. This * should almost always be set to 1 (the default). * @param route - The optional route value. * @param authcid - The optional alternative authentication identity * (username) if intending to impersonate another user. * When using the SASL-EXTERNAL authentication mechanism, for example * with client certificates, then the authcid value is used to * determine whether an authorization JID (authzid) should be sent to * the server. The authzid should NOT be sent to the server if the * authzid and authcid are the same. So to prevent it from being sent * (for example when the JID is already contained in the client * certificate), set authcid to that same JID. See XEP-178 for more * details. * @param disconnection_timeout - The optional disconnection timeout * in milliseconds before _doDisconnect will be called. */ connect(jid, pass, callback, wait, hold, route, authcid, disconnection_timeout = 3000) { var _a; this.jid = jid; /** Authorization identity */ this.authzid = getBareJidFromJid(this.jid); /** Authentication identity (User name) */ this.authcid = authcid || getNodeFromJid(this.jid); /** Authentication identity (User password) */ this.pass = pass; /** * The SASL SCRAM client and server keys. This variable will be populated with a non-null * object of the above described form after a successful SCRAM connection */ this.scram_keys = null; this.connect_callback = callback; this.disconnecting = false; this.connected = false; this.authenticated = false; this.restored = false; this.disconnection_timeout = disconnection_timeout; // Per-stream SM state starts fresh; resumable state is reloaded from // storage once the server advertises SM (_onStreamFeaturesAfterSASL). (_a = this.sm) === null || _a === void 0 ? void 0 : _a.reset(); // parse jid for domain this.domain = getDomainFromJid(this.jid); this._changeConnectStatus(Status.CONNECTING, null); this._proto._connect(wait, hold, route); } /** * Attach to an already created and authenticated BOSH session. * * This function is provided to allow Strophe to attach to BOSH * sessions which have been created externally, perhaps by a Web * application. This is often used to support auto-login type features * without putting user credentials into the page. * * @param jid - The full JID that is bound by the session. * @param sid - The SID of the BOSH session. * @param rid - The current RID of the BOSH session. This RID * will be used by the next request. * @param callback - The connect callback function. * @param wait - The optional HTTPBIND wait value. This is the * time the server will wait before returning an empty result for * a request. The default setting of 60 seconds is recommended. * Other settings will require tweaks to the Strophe.TIMEOUT value. * @param hold - The optional HTTPBIND hold value. This is the * number of connections the server will hold at one time. This * should almost always be set to 1 (the default). * @param wind - The optional HTTBIND window value. This is the * allowed range of request ids that are valid. The default is 5. */ attach(jid, sid, rid, callback, wait, hold, wind) { if (this._proto instanceof Bosh && typeof jid === 'string') { return this._proto._attach(jid, sid, rid, callback, wait, hold, wind); } else if (this._proto instanceof WorkerWebsocket && typeof jid === 'function') { return this._proto._attach(jid); } else { throw new SessionError('The "attach" method is not available for your connection protocol'); } } /** * Attempt to restore a cached BOSH session. * * This function is only useful in conjunction with providing the * "keepalive":true option when instantiating a new {@link Connection}. * * When "keepalive" is set to true, Strophe will cache the BOSH tokens * RID (Request ID) and SID (Session ID) and then when this function is * called, it will attempt to restore the session from those cached * tokens. * * This function must therefore be called instead of connect or attach. * * For an example on how to use it, please see examples/restore.js * * @param jid - The user's JID. This may be a bare JID or a full JID. * @param callback - The connect callback function. * @param wait - The optional HTTPBIND wait value. This is the * time the server will wait before returning an empty result for * a request. The default setting of 60 seconds is recommended. * @param hold - The optional HTTPBIND hold value. This is the * number of connections the server will hold at one time. This * should almost always be set to 1 (the default). * @param wind - The optional HTTBIND window value. This is the * allowed range of request ids that are valid. The default is 5. */ restore(jid, callback, wait, hold, wind) { if (!(this._proto instanceof Bosh) || !this._sessionCachingSupported()) { throw new SessionError('The "restore" method can only be used with a BOSH connection.'); } if (this._sessionCachingSupported()) { this._proto._restore(jid, callback, wait, hold, wind); } } /** * Checks whether sessionStorage and JSON are supported and whether we're * using BOSH. */ _sessionCachingSupported() { if (this._proto instanceof Bosh) { if (!JSON) { return false; } try { sessionStorage.setItem('_strophe_', '_strophe_'); sessionStorage.removeItem('_strophe_'); } catch (_e) { return false; } return true; } return false; } /** * User overrideable function that receives XML data coming into the * connection. * * Due to limitations of current Browsers' XML-Parsers the opening and closing * <stream> tag for WebSocket-Connoctions will be passed as selfclosing here. * * BOSH-Connections will have all stanzas wrapped in a <body> tag. See * <Bosh.strip> if you want to strip this tag. * * @param _elem - The XML data received by the connection. */ xmlInput(_elem) { return; } /** * User overrideable function that receives XML data sent to the * connection. * * Due to limitations of current Browsers' XML-Parsers the opening and closing * <stream> tag for WebSocket-Connoctions will be passed as selfclosing here. * * BOSH-Connections will have all stanzas wrapped in a <body> tag. See * <Bosh.strip> if you want to strip this tag. * * @param _elem - The XMLdata sent by the connection. */ xmlOutput(_elem) { return; } /** * User overrideable function that receives raw data coming into the * connection. * * @param _data - The data received by the connection. */ rawInput(_data) { return; } /** * User overrideable function that receives raw data sent to the * connection. * * @param _data - The data sent by the connection. */ rawOutput(_data) { return; } /** * User overrideable function that receives the new valid rid. * * @param _rid - The next valid rid */ nextValidRid(_rid) { return; } /** * User overrideable function that receives the new role of this * connection in a shared-worker setup. * * Called when the shared worker assigns or changes this tab's role, * for example when this tab is promoted to 'primary' after the previous * primary tab went away. * * @param _role - The new role ('primary' or 'secondary') */ onRoleChanged(_role) { return; } /** * User overrideable function that receives message and presence stanzas * sent by *another* tab sharing this connection (via the `worker` * option), so every tab can render what any tab sent. * * Deliberately separate from the inbound handler pipeline: these stanzas * were sent, not received, so they must not trigger stanza handlers. * IQs are not reflected — they are request/response traffic private to * the sending tab. * * @param _elem - The sent stanza. */ onForeignStanzaSent(_elem) { return; } /** * Send a stanza. * * This function is called to push data onto the send queue to * go out over the wire. Whenever a request is sent to the BOSH * server, all pending data is sent and the queue is flushed. * * @param stanza - The stanza to send */ send(stanza) { if (stanza === null) return; if (Array.isArray(stanza)) { stanza.forEach((s) => this._queueData(s instanceof Builder ? s.tree() : s)); } else { const el = stanza instanceof Builder ? stanza.tree() : stanza; this._queueData(el); } this._proto._send(); } /** * Immediately send any pending outgoing data. * * Normally send() queues outgoing data until the next idle period * (100ms), which optimizes network use in the common cases when * several send()s are called in succession. flush() can be used to * immediately send all pending data. */ flush() { // cancel the pending idle period and run the idle function // immediately clearTimeout(this._idleTimeout); this._onIdle(); } /** * Helper function to send presence stanzas. The main benefit is for * sending presence stanzas for which you expect a responding presence * stanza with the same id (for example when leaving a chat room). * * @param stanza - The stanza to send. * @param callback - The callback function for a successful request. * @param errback - The callback function for a failed or timed * out request. On timeout, the stanza will be null. * @param timeout - The time specified in milliseconds for a * timeout to occur. * @return The id used to send the presence. */ sendPresence(stanza, callback, errback, timeout) { let timeoutHandler = null; const el = stanza instanceof Builder ? stanza.tree() : stanza; let id = el.getAttribute('id'); if (!id) { // inject id if not found id = this.getUniqueId('sendPresence'); el.setAttribute('id', id); } if (typeof callback === 'function' || typeof errback === 'function') { const handler = this.addHandler((stanza) => { // remove timeout handler if there is one if (timeoutHandler) this.deleteTimedHandler(timeoutHandler); if (stanza.getAttribute('type') === 'error') { errback === null || errback === void 0 ? void 0 : errback(stanza); } else if (callback) { callback(stanza); } return false; }, null, 'presence', null, id); // if timeout specified, set up a timeout handler. if (timeout) { timeoutHandler = this.addTimedHandler(timeout, () => { // get rid of normal handler this.deleteHandler(handler); // call errback on timeout with null stanza errback === null || errback === void 0 ? void 0 : errback(null); return false; }); } } this.send(el); return id; } /** * Helper function to send IQ stanzas. * * @param stanza - The stanza to send. * @param callback - The callback function for a successful request. * @param errback - The callback function for a failed or timed * out request. On timeout, the stanza will be null. * @param timeout - The time specified in milliseconds for a * timeout to occur. * @return The id used to send the IQ. */ sendIQ(stanza, callback, errback, timeout) { let timeoutHandler = null; const el = stanza instanceof Builder ? stanza.tree() : stanza; let id = el.getAttribute('id'); if (!id) { // inject id if not found id = this.getUniqueId('sendIQ'); el.setAttribute('id', id); } if (typeof callback === 'function' || typeof errback === 'function') { const handler = this.addHandler((stanza) => { // remove timeout handler if there is one if (timeoutHandler) this.deleteTimedHandler(timeoutHandler); const iqtype = stanza.getAttribute('type'); if (iqtype === 'result') { callback === null || callback === void 0 ? void 0 : callback(stanza); } else if (iqtype === 'error') { errback === null || errback === void 0 ? void 0 : errback(stanza); } else { const error = new Error(`Got bad IQ type of ${iqtype}`); error.name = 'StropheError'; throw error; } return false; }, null, 'iq', ['error', 'result'], id); // if timeout specified, set up a timeout handler. if (timeout) { timeoutHandler = this.addTimedHandler(timeout, () => { // get rid of normal handler this.deleteHandler(handler); // call errback on timeout with null stanza errback === null || errback === void 0 ? void 0 : errback(null); return false; }); } } this.send(el); return id; } /** * Queue outgoing data for later sending. Also ensures that the data * is a DOMElement. * @private * @param element */ _queueData(element) { var _a; if (element === null || !element.tagName || !element.childNodes) { const error = new Error('Cannot queue non-DOMElement.'); error.name = 'StropheError'; throw error; } this._data.push(element); // XEP-0198: every countable outbound stanza is queued here, after the // push, so that an <r/> emitted by the engine lands behind it in the // send FIFO. Hooking _queueData (rather than send/sendIQ/sendPresence) // means raw send() calls can't escape the counting. if (((_a = this.sm) === null || _a === void 0 ? void 0 : _a.isTracking()) && isCountableStanza(element.tagName)) { this.sm.trackOutbound(toStanzaView(element)); } } /** * Send an xmpp:restart stanza. * @private */ _sendRestart() { this._data.push('restart'); this._proto._sendRestart(); this._scheduleIdle(); } /** * Add a timed handler to the connection. * * This function adds a timed handler. The provided handler will * be called every period milliseconds until it returns false, * the connection is terminated, or the handler is removed. Handlers * that wish to continue being invoked should return true. * * Because of method binding it is necessary to save the result of * this function if you wish to remove a handler with * deleteTimedHandler(). * * Note that user handlers are not active until authentication is * successful. * * @param period - The period of the handler. * @param handler - The callback function. * @return A reference to the handler that can be used to remove it. */ addTimedHandler(period, handler) { const thand = new TimedHandler(period, handler); this.addTimeds.push(thand); return thand; } /** * Delete a timed handler for a connection. * * This function removes a timed handler from the connection. The * handRef parameter is *not* the function passed to addTimedHandler(), * but is the reference returned from addTimedHandler(). * @param handRef - The handler reference. */ deleteTimedHandler(handRef) { // this must be done in the Idle loop so that we don't change // the handlers during iteration this.removeTimeds.push(handRef); } /** * Add a stanza handler for the connection. * * This function adds a stanza handler to the connection. The * handler callback will be called for any stanza that matches * the parameters. Note that if multiple parameters are supplied, * they must all match for the handler to be invoked. * * The handler will receive the stanza that triggered it as its argument. * *The handler should return true if it is to be invoked again; * returning false will remove the handler after it returns.* * * As a convenience, the ns parameters applies to the top level element * and also any of its immediate children. This is primarily to make * matching /iq/query elements easy. * * ### Options * * With the options argument, you can specify boolean flags that affect how * matches are being done. * * Currently two flags exist: * * * *matchBareFromJid*: * When set to true, the from parameter and the * from attribute on the stanza will be matched as bare JIDs instead * of full JIDs. To use this, pass {matchBareFromJid: true} as the * value of options. The default value for matchBareFromJid is false. * * * *ignoreNamespaceFragment*: * When set to true, a fragment specified on the stanza's namespace * URL will be ignored when it's matched with the one configured for * the handler. * * This means that if you register like this: * * > connection.addHandler( * > handler, * > 'http://jabber.org/protocol/muc', * > null, null, null, null, * > {'ignoreNamespaceFragment': true} * > ); * * Then a stanza with XML namespace of * 'http://jabber.org/protocol/muc#user' will also be matched. If * 'ignoreNamespaceFragment' is false, then only stanzas with * 'http://jabber.org/protocol/muc' will be matched. * * ### Deleting the handler * * The return value should be saved if you wish to remove the handler * with `deleteHandler()`. * * @param handler - The user callback. * @param ns - The namespace to match. * @param name - The stanza name to match. * @param type - The stanza type (or types if an array) to match. * @param id - The stanza id attribute to match. * @param from - The stanza from attribute to match. * @param options - The handler options * @return A reference to the handler that can be used to remove it. */ addHandler(handler, ns, name, type, id, from, options) { const hand = new Handler(handler, ns, name, type, id, from, options); this.addHandlers.push(hand); return hand; } /** * Delete a stanza handler for a connection. * * This function removes a stanza handler from the connection. The * handRef parameter is *not* the function passed to addHandler(), * but is the reference returned from addHandler(). * * @param handRef - The handler reference. */ deleteHandler(handRef) { // this must be done in the Idle loop so that we don't change // the handlers during iteration this.removeHandlers.push(handRef); // If a handler is being deleted while it is being added, // prevent it from getting added const i = this.addHandlers.indexOf(handRef); if (i >= 0) { this.addHandlers.splice(i, 1); } } /** * Register the SASL mechanisms which will be supported by this instance of * Connection (i.e. which this XMPP client will support). * @param mechanisms - Array of objects with SASLMechanism prototypes */ registerSASLMechanisms(mechanisms) { this.mechanisms = {}; (mechanisms || [ SASLAnonymous, SASLExternal, SASLOAuthBearer, SASLXOAuth2, SASLPlain, SASLSHA1, SASLSHA256, SASLSHA384, SASLSHA512, ]).forEach((m) => this.registerSASLMechanism(m)); } /** * Register a single SASL mechanism, to be supported by this client. * @param Mechanism - Object with a Strophe.SASLMechanism prototype */ registerSASLMechanism(Mechanism) { const mechanism = new Mechanism(); this.mechanisms[mechanism.mechname] = mechanism; } /** * Start the graceful disconnection process. * * This function starts the disconnection process. This process starts * by sending unavailable presence and sending BOSH body of type * terminate. A timeout handler makes sure that disconnection happens * even if the BOSH server does not respond. * If the Connection object isn't connected, at least tries to abort all pending requests * so the connection object won't generate successful requests (which were already opened). * * The user supplied connection callback will be notified of the * progress as this process happens. * * @param reason - The reason the disconnect is occuring. */ disconnect(reason) { var _a; this._changeConnectStatus(Status.DISCONNECTING, reason); if (reason) { log.info('Disconnect was called because: ' + reason); } else { log.debug('Disconnect was called'); } if (this.connected) { let pres = null; this.disconnecting = true; if (this.authenticated) { pres = $pres({ 'xmlns': NS.CLIENT, 'type': 'unavailable', }).tree(); } // A cleanly closed stream is not resumable: send a final <a/> so // the server doesn't redeliver stanzas we already received, and // drop the persisted XEP-0198 state. (_a = this.sm) === null || _a === void 0 ? void 0 : _a.onGracefulClose(); // setup timeout handler this._disconnectTimeout = this._addSysTimedHandler(this.disconnection_timeout, this._onDisconnectTimeout.bind(this)); this._proto._disconnect(pres); } else { log.debug('Disconnect was called before Strophe connected to the server'); this._proto._abortAllRequests(); this._doDisconnect(); } } /** * _Private_ helper function that makes sure plugins and the user's * callback are notified of connection status changes. * @param status - the new connection status, one of the values * in Strophe.Status * @param condition - the error condition * @param elem - The triggering stanza. */ _changeConnectStatus(status, condition, elem) { // notify all plugins listening for status changes for (const k in connectionPlugins) { if (Object.prototype.hasOwnProperty.call(connectionPlugins, k)) { // @ts-ignore const plugin = this[k]; if (plugin.statusChanged) { try { plugin.statusChanged(status, condition); } catch (err) { log.error(`${k} plugin caused an exception changing status: ${err}`); } } } } // notify the user's callback if (this.connect_callback) { try { this.connect_callback(status, condition, elem); } catch (e) { handleError(e); log.error(`User connection callback caused an exception: ${e}`); } } } /** * _Private_ function to disconnect. * * This is the last piece of the disconnection logic. This resets the * connection and alerts the user's connection callback. * @param condition - the error condition */ _doDisconnect(condition) { clearTimeout(this._idleTimeout); // Cancel Disconnect Timeout if (this._disconnectTimeout !== null) { this.deleteTimedHandler(this._disconnectTimeout); this._disconnectTimeout = null; } log.debug('_doDisconnect was called'); this._proto._doDisconnect(); this.authenticated = false; this.disconnecting = false; this.restored = false; // delete handlers this.handlers = []; this.timedHandlers = []; this.removeTimeds = []; this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; // tell the parent we disconnected this._changeConnectStatus(Status.DISCONNECTED, condition); this.connected = false; } /** * _Private_ handler to processes incoming data from the the connection. * * Except for _connect_cb handling the initial connection request, * this function handles the incoming data for all requests. This * function also fires stanza handlers that match each incoming * stanza. * @param req - The request that has data ready. * @param raw - The stanza as raw string. */ _dataRecv(req, raw) { const elem = (this._proto instanceof Bosh ? this._proto._reqToData(req) : req); if (elem === null) { return; } if (this.xmlInput !== Connection.prototype.xmlInput) { if (elem.nodeName === this._proto.strip && elem.childNodes.length) { this.xmlInput(elem.childNodes[0]); } else { this.xmlInput(elem); } } if (this.rawInput !== Connection.prototype.rawInput) { if (raw) { this.rawInput(raw); } else { this.rawInput(Builder.serialize(elem)); } } // remove handlers scheduled for deletion while (this.removeHandlers.length > 0) { const hand = this.removeHandlers.pop(); const i = this.handlers.indexOf(hand); if (i >= 0) { this.handlers.splice(i, 1); } } // add handlers scheduled for addition while (this.addHandlers.length > 0) { this.handlers.push(this.addHandlers.pop()); } // handle graceful disconnect if (this.disconnecting && this._proto._emptyQueue()) { this._doDisconnect(); return; } const type = elem.getAttribute('type'); if (type !== null && type === 'terminate') { // Don't process stanzas that come in after disconnect if (this.disconnecting) { return; } // an error occurred let cond = elem.getAttribute('condition'); const conflict = elem.getElementsByTagName('conflict'); if (cond !== null) { if (cond === 'remote-stream-error' && conflict.length > 0) { cond = 'conflict'; } this._changeConnectStatus(Status.CONNFAIL, cond); } else { this._changeConnectStatus(Status.CONNFAIL, ErrorCondition.UNKNOWN_REASON); } this._doDisconnect(cond); return; } // send each incoming stanza through the handler chain forEachChild(elem, null, (child) => { var _a; // XEP-0198: count inbound stanzas here in the dispatch loop — // exactly-once, in order, and immune to handler churn. (_a = this.sm) === null || _a === void 0 ? void 0 : _a.onInboundStanza(child.nodeName); const matches = []; this.handlers = this.handlers.reduce((handlers, handler) => { try { if (handler.isMatch(child) && (this.authenticated || !handler.user)) { if (handler.run(child)) { handlers.push(handler); } matches.push(handler); } else { handlers.push(handler); } } catch (e) { // if the handler throws an exception, we consider it as false log.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); } return handlers; }, []); // If no handler was fired for an incoming IQ with type="set", // then we return an IQ error stanza with service-unavailable. if (!matches.length && this.iqFallbackHandler.isMatch(child)) { this.iqFallbackHandler.run(child); } }); } /** * _Private_ handler for initial connection request. * * This handler is used to process the initial connection request * response from the BOSH server. It is used to set up authentication * handlers and start the authentication process. * * SASL authentication will be attempted if available, otherwise * the code will fall back to legacy authentication. * * @param req - The current request. * @param _callback - low level (xmpp) connect callback function. * Useful for plugins with their own xmpp connect callback (when they * want to do something special). * @param raw - The stanza as raw string. */ _connect_cb(req, _callback, raw) { log.debug('_connect_cb was called'); this.connected = true; let bodyWrap; try { bodyWrap = (this._proto instanceof Bosh ? this._proto._reqToData(req) : req); } catch (e) { if (e.name !== ErrorCondition.BAD_FORMAT) { throw e; } this._changeConnectStatus(Status.CONNFAIL, ErrorCondition.BAD_FORMAT); this._doDisconnect(ErrorCondition.BAD_FORMAT); } if (!bodyWrap) { return; } if (this.xmlInput !== Connection.prototype.xmlInput) { if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) { this.xmlInput(bodyWrap.childNodes[0]); } else { this.xmlInput(bodyWrap); } } if (this.rawInput !== Connection.prototype.rawInput) { if (raw) { this.rawInput(raw); } else { this.rawInput(Builder.serialize(bodyWrap)); } } const conncheck = this._proto._connect_cb(bodyWrap); if (conncheck === Status.CONNFAIL) { return; } // Check for the stream:features tag let hasFeatures; if (bodyWrap.getElementsByTagNameNS) { hasFeatures = bodyWrap.getElementsByTagNameNS(NS.STREAM, 'features').length > 0; } else { hasFeatures = bodyWrap.getElementsByTagName('stream:features').length > 0 || bodyWrap.getElementsByTagName('features').length > 0; } if (!hasFeatures) { this._proto._no_auth_received(_callback); return; } const matched = Array.from(bodyWrap.getElementsByTagName('mechanism')) .map((m) => this.mechanisms[m.textContent]) .filter((m) => m); if (matched.length === 0) { if (bodyWrap.getElementsByTagName('auth').length === 0) { // There are no matching SASL mechanisms and also no legacy // auth available. this._proto._no_auth_received(_callback); return; } } if (this.do_authentication !== false) { this.authenticate(matched); } } /** * Sorts an array of objects with prototype SASLMechanism according to * their priorities. * @param mechanisms - Array of SASL mechanisms. */ sortMechanismsByPriority(mechanisms) { // Sorting mechanisms according to priority. for (let i = 0; i < mechanisms.length - 1; ++i) { let higher = i; for (let j = i + 1; j < mechanisms.length; ++j) { if (mechanisms[j].priority > mechanisms[higher].priority) { higher = j; } }