/*
This program is distributed under the terms of the MIT license.
Please see the LICENSE file for details.
Copyright 2006-2018, OGG, LLC
*/
/*global define, document, sessionStorage, setTimeout, clearTimeout, ActiveXObject, DOMParser, btoa, atob, module */
import MD5 from 'md5';
import SHA1 from 'sha1';
import utils from 'utils';
/** Function: $build
* Create a Strophe.Builder.
* This is an alias for 'new Strophe.Builder(name, attrs)'.
*
* Parameters:
* (String) name - The root element name.
* (Object) attrs - The attributes for the root element in object notation.
*
* Returns:
* A new Strophe.Builder object.
*/
function $build(name, attrs) {
return new Strophe.Builder(name, attrs);
}
/** Function: $msg
* Create a Strophe.Builder with a element as the root.
*
* Parameters:
* (Object) attrs - The element attributes in object notation.
*
* Returns:
* A new Strophe.Builder object.
*/
function $msg(attrs) {
return new Strophe.Builder("message", attrs);
}
/** Function: $iq
* Create a Strophe.Builder with an element as the root.
*
* Parameters:
* (Object) attrs - The element attributes in object notation.
*
* Returns:
* A new Strophe.Builder object.
*/
function $iq(attrs) {
return new Strophe.Builder("iq", attrs);
}
/** Function: $pres
* Create a Strophe.Builder with a element as the root.
*
* Parameters:
* (Object) attrs - The element attributes in object notation.
*
* Returns:
* A new Strophe.Builder object.
*/
function $pres(attrs) {
return new Strophe.Builder("presence", attrs);
}
/** Class: Strophe
* An object container for all Strophe library functions.
*
* This class is just a container for all the objects and constants
* used in the library. It is not meant to be instantiated, but to
* provide a namespace for library objects, constants, and functions.
*/
const Strophe = {
/** Constant: VERSION */
VERSION: "@VERSION@",
/** Constants: XMPP Namespace Constants
* Common namespace constants from the XMPP RFCs and XEPs.
*
* NS.HTTPBIND - HTTP BIND namespace from XEP 124.
* NS.BOSH - BOSH namespace from XEP 206.
* NS.CLIENT - Main XMPP client namespace.
* NS.AUTH - Legacy authentication namespace.
* NS.ROSTER - Roster operations namespace.
* NS.PROFILE - Profile namespace.
* NS.DISCO_INFO - Service discovery info namespace from XEP 30.
* NS.DISCO_ITEMS - Service discovery items namespace from XEP 30.
* NS.MUC - Multi-User Chat namespace from XEP 45.
* NS.SASL - XMPP SASL namespace from RFC 3920.
* NS.STREAM - XMPP Streams namespace from RFC 3920.
* NS.BIND - XMPP Binding namespace from RFC 3920.
* NS.SESSION - XMPP Session namespace from RFC 3920.
* NS.XHTML_IM - XHTML-IM namespace from XEP 71.
* NS.XHTML - XHTML body namespace from XEP 71.
*/
NS: {
HTTPBIND: "http://jabber.org/protocol/httpbind",
BOSH: "urn:xmpp:xbosh",
CLIENT: "jabber:client",
AUTH: "jabber:iq:auth",
ROSTER: "jabber:iq:roster",
PROFILE: "jabber:iq:profile",
DISCO_INFO: "http://jabber.org/protocol/disco#info",
DISCO_ITEMS: "http://jabber.org/protocol/disco#items",
MUC: "http://jabber.org/protocol/muc",
SASL: "urn:ietf:params:xml:ns:xmpp-sasl",
STREAM: "http://etherx.jabber.org/streams",
FRAMING: "urn:ietf:params:xml:ns:xmpp-framing",
BIND: "urn:ietf:params:xml:ns:xmpp-bind",
SESSION: "urn:ietf:params:xml:ns:xmpp-session",
VERSION: "jabber:iq:version",
STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas",
XHTML_IM: "http://jabber.org/protocol/xhtml-im",
XHTML: "http://www.w3.org/1999/xhtml"
},
/** Constants: XHTML_IM Namespace
* contains allowed tags, tag attributes, and css properties.
* Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset.
* See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended
* allowed tags and their attributes.
*/
XHTML: {
tags: ['a','blockquote','br','cite','em','img','li','ol','p','span','strong','ul','body'],
attributes: {
'a': ['href'],
'blockquote': ['style'],
'br': [],
'cite': ['style'],
'em': [],
'img': ['src', 'alt', 'style', 'height', 'width'],
'li': ['style'],
'ol': ['style'],
'p': ['style'],
'span': ['style'],
'strong': [],
'ul': ['style'],
'body': []
},
css: ['background-color','color','font-family','font-size','font-style','font-weight','margin-left','margin-right','text-align','text-decoration'],
/** Function: XHTML.validTag
*
* Utility method to determine whether a tag is allowed
* in the XHTML_IM namespace.
*
* XHTML tag names are case sensitive and must be lower case.
*/
validTag: function(tag) {
for (let i=0; i 0) {
for (let i=0; i/g, ">");
text = text.replace(/'/g, "'");
text = text.replace(/"/g, """);
return text;
},
/* Function: xmlunescape
* Unexcapes invalid xml characters.
*
* Parameters:
* (String) text - text to unescape.
*
* Returns:
* Unescaped text.
*/
xmlunescape: function(text) {
text = text.replace(/\&/g, "&");
text = text.replace(/</g, "<");
text = text.replace(/>/g, ">");
text = text.replace(/'/g, "'");
text = text.replace(/"/g, "\"");
return text;
},
/** Function: xmlTextNode
* Creates an XML DOM text node.
*
* Provides a cross implementation version of document.createTextNode.
*
* Parameters:
* (String) text - The content of the text node.
*
* Returns:
* A new XML DOM text node.
*/
xmlTextNode: function (text) {
return Strophe.xmlGenerator().createTextNode(text);
},
/** Function: xmlHtmlNode
* Creates an XML DOM html node.
*
* Parameters:
* (String) html - The content of the html node.
*
* Returns:
* A new XML DOM text node.
*/
xmlHtmlNode: function (html) {
let node;
//ensure text is escaped
if (DOMParser) {
const parser = new DOMParser();
node = parser.parseFromString(html, "text/xml");
} else {
node = new ActiveXObject("Microsoft.XMLDOM");
node.async="false";
node.loadXML(html);
}
return node;
},
/** Function: getText
* Get the concatenation of all text children of an element.
*
* Parameters:
* (XMLElement) elem - A DOM element.
*
* Returns:
* A String with the concatenated text of all text element children.
*/
getText: function (elem) {
if (!elem) { return null; }
let str = "";
if (elem.childNodes.length === 0 && elem.nodeType === Strophe.ElementType.TEXT) {
str += elem.nodeValue;
}
for (let i=0; i 0) {
value = css.join('; ');
el.setAttribute(attribute, value);
}
} else {
el.setAttribute(attribute, value);
}
}
for (let i=0; i < elem.childNodes.length; i++) {
el.appendChild(Strophe.createHtml(elem.childNodes[i]));
}
} catch(e) { // invalid elements
el = Strophe.xmlTextNode('');
}
} else {
el = Strophe.xmlGenerator().createDocumentFragment();
for (let i=0; i < elem.childNodes.length; i++) {
el.appendChild(Strophe.createHtml(elem.childNodes[i]));
}
}
} else if (elem.nodeType === Strophe.ElementType.FRAGMENT) {
el = Strophe.xmlGenerator().createDocumentFragment();
for (let i=0; i < elem.childNodes.length; i++) {
el.appendChild(Strophe.createHtml(elem.childNodes[i]));
}
} else if (elem.nodeType === Strophe.ElementType.TEXT) {
el = Strophe.xmlTextNode(elem.nodeValue);
}
return el;
},
/** Function: escapeNode
* Escape the node part (also called local part) of a JID.
*
* Parameters:
* (String) node - A node (or local part).
*
* Returns:
* An escaped node (or local part).
*/
escapeNode: function (node) {
if (typeof node !== "string") { return node; }
return node.replace(/^\s+|\s+$/g, '')
.replace(/\\/g, "\\5c")
.replace(/ /g, "\\20")
.replace(/\"/g, "\\22")
.replace(/\&/g, "\\26")
.replace(/\'/g, "\\27")
.replace(/\//g, "\\2f")
.replace(/:/g, "\\3a")
.replace(//g, "\\3e")
.replace(/@/g, "\\40");
},
/** Function: unescapeNode
* Unescape a node part (also called local part) of a JID.
*
* Parameters:
* (String) node - A node (or local part).
*
* Returns:
* An unescaped node (or local part).
*/
unescapeNode: function (node) {
if (typeof node !== "string") { return node; }
return node.replace(/\\20/g, " ")
.replace(/\\22/g, '"')
.replace(/\\26/g, "&")
.replace(/\\27/g, "'")
.replace(/\\2f/g, "/")
.replace(/\\3a/g, ":")
.replace(/\\3c/g, "<")
.replace(/\\3e/g, ">")
.replace(/\\40/g, "@")
.replace(/\\5c/g, "\\");
},
/** Function: getNodeFromJid
* Get the node portion of a JID String.
*
* Parameters:
* (String) jid - A JID.
*
* Returns:
* A String containing the node.
*/
getNodeFromJid: function (jid) {
if (jid.indexOf("@") < 0) { return null; }
return jid.split("@")[0];
},
/** Function: getDomainFromJid
* Get the domain portion of a JID String.
*
* Parameters:
* (String) jid - A JID.
*
* Returns:
* A String containing the domain.
*/
getDomainFromJid: function (jid) {
const bare = Strophe.getBareJidFromJid(jid);
if (bare.indexOf("@") < 0) {
return bare;
} else {
const parts = bare.split("@");
parts.splice(0, 1);
return parts.join('@');
}
},
/** Function: getResourceFromJid
* Get the resource portion of a JID String.
*
* Parameters:
* (String) jid - A JID.
*
* Returns:
* A String containing the resource.
*/
getResourceFromJid: function (jid) {
const s = jid.split("/");
if (s.length < 2) { return null; }
s.splice(0, 1);
return s.join('/');
},
/** Function: getBareJidFromJid
* Get the bare JID from a JID String.
*
* Parameters:
* (String) jid - A JID.
*
* Returns:
* A String containing the bare JID.
*/
getBareJidFromJid: function (jid) {
return jid ? jid.split("/")[0] : null;
},
/** PrivateFunction: _handleError
* _Private_ function that properly logs an error to the console
*/
_handleError: function (e) {
if (typeof e.stack !== "undefined") {
Strophe.fatal(e.stack);
}
if (e.sourceURL) {
Strophe.fatal("error: " + this.handler + " " + e.sourceURL + ":" +
e.line + " - " + e.name + ": " + e.message);
} else if (e.fileName) {
Strophe.fatal("error: " + this.handler + " " +
e.fileName + ":" + e.lineNumber + " - " +
e.name + ": " + e.message);
} else {
Strophe.fatal("error: " + e.message);
}
},
/** Function: log
* User overrideable logging function.
*
* This function is called whenever the Strophe library calls any
* of the logging functions. The default implementation of this
* function logs only fatal errors. If client code wishes to handle the logging
* messages, it should override this with
* > Strophe.log = function (level, msg) {
* > (user code here)
* > };
*
* Please note that data sent and received over the wire is logged
* via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
*
* The different levels and their meanings are
*
* DEBUG - Messages useful for debugging purposes.
* INFO - Informational messages. This is mostly information like
* 'disconnect was called' or 'SASL auth succeeded'.
* WARN - Warnings about potential problems. This is mostly used
* to report transient connection errors like request timeouts.
* ERROR - Some error occurred.
* FATAL - A non-recoverable fatal error occurred.
*
* Parameters:
* (Integer) level - The log level of the log message. This will
* be one of the values in Strophe.LogLevel.
* (String) msg - The log message.
*/
log: function (level, msg) {
if (level === this.LogLevel.FATAL &&
typeof window.console === 'object' &&
typeof window.console.error === 'function') {
window.console.error(msg);
}
},
/** Function: debug
* Log a message at the Strophe.LogLevel.DEBUG level.
*
* Parameters:
* (String) msg - The log message.
*/
debug: function(msg) {
this.log(this.LogLevel.DEBUG, msg);
},
/** Function: info
* Log a message at the Strophe.LogLevel.INFO level.
*
* Parameters:
* (String) msg - The log message.
*/
info: function (msg) {
this.log(this.LogLevel.INFO, msg);
},
/** Function: warn
* Log a message at the Strophe.LogLevel.WARN level.
*
* Parameters:
* (String) msg - The log message.
*/
warn: function (msg) {
this.log(this.LogLevel.WARN, msg);
},
/** Function: error
* Log a message at the Strophe.LogLevel.ERROR level.
*
* Parameters:
* (String) msg - The log message.
*/
error: function (msg) {
this.log(this.LogLevel.ERROR, msg);
},
/** Function: fatal
* Log a message at the Strophe.LogLevel.FATAL level.
*
* Parameters:
* (String) msg - The log message.
*/
fatal: function (msg) {
this.log(this.LogLevel.FATAL, msg);
},
/** Function: serialize
* Render a DOM element and all descendants to a String.
*
* Parameters:
* (XMLElement) elem - A DOM element.
*
* Returns:
* The serialized element tree as a String.
*/
serialize: function (elem) {
if (!elem) { return null; }
if (typeof(elem.tree) === "function") {
elem = elem.tree();
}
const names = [...Array(elem.attributes.length).keys()].map(i => elem.attributes[i].nodeName);
names.sort();
let result = names.reduce(
(a, n) => `${a} ${n}="${Strophe.xmlescape(elem.attributes.getNamedItem(n).value)}"`,
`<${elem.nodeName}`
);
if (elem.childNodes.length > 0) {
result += ">";
for (let i=0; i < elem.childNodes.length; i++) {
const child = elem.childNodes[i];
switch (child.nodeType) {
case Strophe.ElementType.NORMAL:
// normal element, so recurse
result += Strophe.serialize(child);
break;
case Strophe.ElementType.TEXT:
// text element to escape values
result += Strophe.xmlescape(child.nodeValue);
break;
case Strophe.ElementType.CDATA:
// cdata section so don't escape values
result += "";
}
}
result += "" + elem.nodeName + ">";
} else {
result += "/>";
}
return result;
},
/** PrivateVariable: _requestId
* _Private_ variable that keeps track of the request ids for
* connections.
*/
_requestId: 0,
/** PrivateVariable: Strophe.connectionPlugins
* _Private_ variable Used to store plugin names that need
* initialization on Strophe.Connection construction.
*/
_connectionPlugins: {},
/** Function: addConnectionPlugin
* Extends the Strophe.Connection object with the given plugin.
*
* Parameters:
* (String) name - The name of the extension.
* (Object) ptype - The plugin's prototype.
*/
addConnectionPlugin: function (name, ptype) {
Strophe._connectionPlugins[name] = ptype;
}
};
/** Class: Strophe.Builder
* XML DOM builder.
*
* This object provides an interface similar to JQuery but for building
* DOM elements easily and rapidly. All the functions except for toString()
* and tree() return the object, so calls can be chained. Here's an
* example using the $iq() builder helper.
* > $iq({to: 'you', from: 'me', type: 'get', id: '1'})
* > .c('query', {xmlns: 'strophe:example'})
* > .c('example')
* > .toString()
*
* The above generates this XML fragment
* >
* >
* >
* >
* >
* The corresponding DOM manipulations to get a similar fragment would be
* a lot more tedious and probably involve several helper variables.
*
* Since adding children makes new operations operate on the child, up()
* is provided to traverse up the tree. To add two children, do
* > builder.c('child1', ...).up().c('child2', ...)
* The next operation on the Builder will be relative to the second child.
*/
/** Constructor: Strophe.Builder
* Create a Strophe.Builder object.
*
* The attributes should be passed in object notation. For example
* > let b = new Builder('message', {to: 'you', from: 'me'});
* or
* > let b = new Builder('messsage', {'xml:lang': 'en'});
*
* Parameters:
* (String) name - The name of the root element.
* (Object) attrs - The attributes for the root element in object notation.
*
* Returns:
* A new Strophe.Builder.
*/
Strophe.Builder = function (name, attrs) {
// Set correct namespace for jabber:client elements
if (name === "presence" || name === "message" || name === "iq") {
if (attrs && !attrs.xmlns) {
attrs.xmlns = Strophe.NS.CLIENT;
} else if (!attrs) {
attrs = {xmlns: Strophe.NS.CLIENT};
}
}
// Holds the tree being built.
this.nodeTree = Strophe.xmlElement(name, attrs);
// Points to the current operation node.
this.node = this.nodeTree;
};
Strophe.Builder.prototype = {
/** Function: tree
* Return the DOM tree.
*
* This function returns the current DOM tree as an element object. This
* is suitable for passing to functions like Strophe.Connection.send().
*
* Returns:
* The DOM tree as a element object.
*/
tree: function () {
return this.nodeTree;
},
/** Function: toString
* Serialize the DOM tree to a String.
*
* This function returns a string serialization of the current DOM
* tree. It is often used internally to pass data to a
* Strophe.Request object.
*
* Returns:
* The serialized DOM tree in a String.
*/
toString: function () {
return Strophe.serialize(this.nodeTree);
},
/** Function: up
* Make the current parent element the new current element.
*
* This function is often used after c() to traverse back up the tree.
* For example, to add two children to the same element
* > builder.c('child1', {}).up().c('child2', {});
*
* Returns:
* The Stophe.Builder object.
*/
up: function () {
this.node = this.node.parentNode;
return this;
},
/** Function: root
* Make the root element the new current element.
*
* When at a deeply nested element in the tree, this function can be used
* to jump back to the root of the tree, instead of having to repeatedly
* call up().
*
* Returns:
* The Stophe.Builder object.
*/
root: function () {
this.node = this.nodeTree;
return this;
},
/** Function: attrs
* Add or modify attributes of the current element.
*
* The attributes should be passed in object notation. This function
* does not move the current element pointer.
*
* Parameters:
* (Object) moreattrs - The attributes to add/modify in object notation.
*
* Returns:
* The Strophe.Builder object.
*/
attrs: function (moreattrs) {
for (const k in moreattrs) {
if (Object.prototype.hasOwnProperty.call(moreattrs, k)) {
if (moreattrs[k] === undefined) {
this.node.removeAttribute(k);
} else {
this.node.setAttribute(k, moreattrs[k]);
}
}
}
return this;
},
/** Function: c
* Add a child to the current element and make it the new current
* element.
*
* This function moves the current element pointer to the child,
* unless text is provided. If you need to add another child, it
* is necessary to use up() to go back to the parent in the tree.
*
* Parameters:
* (String) name - The name of the child.
* (Object) attrs - The attributes of the child in object notation.
* (String) text - The text to add to the child.
*
* Returns:
* The Strophe.Builder object.
*/
c: function (name, attrs, text) {
const child = Strophe.xmlElement(name, attrs, text);
this.node.appendChild(child);
if (typeof text !== "string" && typeof text !=="number") {
this.node = child;
}
return this;
},
/** Function: cnode
* Add a child to the current element and make it the new current
* element.
*
* This function is the same as c() except that instead of using a
* name and an attributes object to create the child it uses an
* existing DOM element object.
*
* Parameters:
* (XMLElement) elem - A DOM element.
*
* Returns:
* The Strophe.Builder object.
*/
cnode: function (elem) {
let impNode;
const xmlGen = Strophe.xmlGenerator();
try {
impNode = (xmlGen.importNode !== undefined);
} catch (e) {
impNode = false;
}
const newElem = impNode ? xmlGen.importNode(elem, true) : Strophe.copyElement(elem);
this.node.appendChild(newElem);
this.node = newElem;
return this;
},
/** Function: t
* Add a child text element.
*
* This *does not* make the child the new current element since there
* are no children of text elements.
*
* Parameters:
* (String) text - The text data to append to the current element.
*
* Returns:
* The Strophe.Builder object.
*/
t: function (text) {
const child = Strophe.xmlTextNode(text);
this.node.appendChild(child);
return this;
},
/** Function: h
* Replace current element contents with the HTML passed in.
*
* This *does not* make the child the new current element
*
* Parameters:
* (String) html - The html to insert as contents of current element.
*
* Returns:
* The Strophe.Builder object.
*/
h: function (html) {
const fragment = document.createElement('body');
// force the browser to try and fix any invalid HTML tags
fragment.innerHTML = html;
// copy cleaned html into an xml dom
const xhtml = Strophe.createHtml(fragment);
while (xhtml.childNodes.length > 0) {
this.node.appendChild(xhtml.childNodes[0]);
}
return this;
}
};
/** PrivateClass: Strophe.Handler
* _Private_ helper class for managing stanza handlers.
*
* A Strophe.Handler encapsulates a user provided callback function to be
* executed when matching stanzas are received by the connection.
* Handlers can be either one-off or persistant depending on their
* return value. Returning true will cause a Handler to remain active, and
* returning false will remove the Handler.
*
* Users will not use Strophe.Handler objects directly, but instead they
* will use Strophe.Connection.addHandler() and
* Strophe.Connection.deleteHandler().
*/
/** PrivateConstructor: Strophe.Handler
* Create and initialize a new Strophe.Handler.
*
* Parameters:
* (Function) handler - A function to be executed when the handler is run.
* (String) ns - The namespace to match.
* (String) name - The element name to match.
* (String) type - The element type to match.
* (String) id - The element id attribute to match.
* (String) from - The element from attribute to match.
* (Object) options - Handler options
*
* Returns:
* A new Strophe.Handler object.
*/
Strophe.Handler = function (handler, ns, name, type, id, from, options) {
this.handler = handler;
this.ns = ns;
this.name = name;
this.type = type;
this.id = id;
this.options = options || {'matchBareFromJid': false, 'ignoreNamespaceFragment': false};
// BBB: Maintain backward compatibility with old `matchBare` option
if (this.options.matchBare) {
Strophe.warn('The "matchBare" option is deprecated, use "matchBareFromJid" instead.');
this.options.matchBareFromJid = this.options.matchBare;
delete this.options.matchBare;
}
if (this.options.matchBareFromJid) {
this.from = from ? Strophe.getBareJidFromJid(from) : null;
} else {
this.from = from;
}
// whether the handler is a user handler or a system handler
this.user = true;
};
Strophe.Handler.prototype = {
/** PrivateFunction: getNamespace
* Returns the XML namespace attribute on an element.
* If `ignoreNamespaceFragment` was passed in for this handler, then the
* URL fragment will be stripped.
*
* Parameters:
* (XMLElement) elem - The XML element with the namespace.
*
* Returns:
* The namespace, with optionally the fragment stripped.
*/
getNamespace: function (elem) {
let elNamespace = elem.getAttribute("xmlns");
if (elNamespace && this.options.ignoreNamespaceFragment) {
elNamespace = elNamespace.split('#')[0];
}
return elNamespace;
},
/** PrivateFunction: namespaceMatch
* Tests if a stanza matches the namespace set for this Strophe.Handler.
*
* Parameters:
* (XMLElement) elem - The XML element to test.
*
* Returns:
* true if the stanza matches and false otherwise.
*/
namespaceMatch: function (elem) {
let nsMatch = false;
if (!this.ns) {
return true;
} else {
Strophe.forEachChild(elem, null, (elem) => {
if (this.getNamespace(elem) === this.ns) {
nsMatch = true;
}
});
return nsMatch || this.getNamespace(elem) === this.ns;
}
},
/** PrivateFunction: isMatch
* Tests if a stanza matches the Strophe.Handler.
*
* Parameters:
* (XMLElement) elem - The XML element to test.
*
* Returns:
* true if the stanza matches and false otherwise.
*/
isMatch: function (elem) {
let from = elem.getAttribute('from');
if (this.options.matchBareFromJid) {
from = Strophe.getBareJidFromJid(from);
}
const elem_type = elem.getAttribute("type");
if (this.namespaceMatch(elem) &&
(!this.name || Strophe.isTagEqual(elem, this.name)) &&
(!this.type || (Array.isArray(this.type) ? this.type.indexOf(elem_type) !== -1 : elem_type === this.type)) &&
(!this.id || elem.getAttribute("id") === this.id) &&
(!this.from || from === this.from)) {
return true;
}
return false;
},
/** PrivateFunction: run
* Run the callback on a matching stanza.
*
* Parameters:
* (XMLElement) elem - The DOM element that triggered the
* Strophe.Handler.
*
* Returns:
* A boolean indicating if the handler should remain active.
*/
run: function (elem) {
let result = null;
try {
result = this.handler(elem);
} catch (e) {
Strophe._handleError(e);
throw e;
}
return result;
},
/** PrivateFunction: toString
* Get a String representation of the Strophe.Handler object.
*
* Returns:
* A String.
*/
toString: function () {
return "{Handler: " + this.handler + "(" + this.name + "," +
this.id + "," + this.ns + ")}";
}
};
/** PrivateClass: Strophe.TimedHandler
* _Private_ helper class for managing timed handlers.
*
* A Strophe.TimedHandler encapsulates a user provided callback that
* should be called after a certain period of time or at regular
* intervals. The return value of the callback determines whether the
* Strophe.TimedHandler will continue to fire.
*
* Users will not use Strophe.TimedHandler objects directly, but instead
* they will use Strophe.Connection.addTimedHandler() and
* Strophe.Connection.deleteTimedHandler().
*/
/** PrivateConstructor: Strophe.TimedHandler
* Create and initialize a new Strophe.TimedHandler object.
*
* Parameters:
* (Integer) period - The number of milliseconds to wait before the
* handler is called.
* (Function) handler - The callback to run when the handler fires. This
* function should take no arguments.
*
* Returns:
* A new Strophe.TimedHandler object.
*/
Strophe.TimedHandler = function (period, handler) {
this.period = period;
this.handler = handler;
this.lastCalled = new Date().getTime();
this.user = true;
};
Strophe.TimedHandler.prototype = {
/** PrivateFunction: run
* Run the callback for the Strophe.TimedHandler.
*
* Returns:
* true if the Strophe.TimedHandler should be called again, and false
* otherwise.
*/
run: function () {
this.lastCalled = new Date().getTime();
return this.handler();
},
/** PrivateFunction: reset
* Reset the last called time for the Strophe.TimedHandler.
*/
reset: function () {
this.lastCalled = new Date().getTime();
},
/** PrivateFunction: toString
* Get a string representation of the Strophe.TimedHandler object.
*
* Returns:
* The string representation.
*/
toString: function () {
return "{TimedHandler: " + this.handler + "(" + this.period +")}";
}
};
/** Class: Strophe.Connection
* 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 SASL PLAIN, SASL DIGEST-MD5, SASL SCRAM-SHA1
* and legacy authentication.
*
* After creating a Strophe.Connection object, the user will typically
* call 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
* addHandler() and 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 send().
*/
/** Constructor: Strophe.Connection
* Create and initialize a Strophe.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.
*
* To make Strophe connect to the current host you can leave out the protocol
* and host part and just pass the path, e.g.
*
* > let conn = new Strophe.Connection("/http-bind/");
*
* Options common to both Websocket and BOSH:
* ------------------------------------------
*
* cookies:
*
* The *cookies* option allows you to pass in cookies to be added to the
* document. These cookies will then be included in the BOSH XMLHttpRequest
* or in the websocket connection.
*
* The passed in value must be a map of cookie names and string values.
*
* > { "myCookie": {
* > "value": "1234",
* > "domain": ".example.org",
* > "path": "/",
* > "expires": expirationDate
* > }
* > }
*
* Note that cookies can't be set in this way for other domains (i.e. cross-domain).
* Those cookies need to be set under those domains, for example they can be
* set server-side by making a XHR call to that domain to ask it to set any
* necessary cookies.
*
* mechanisms:
*
* The *mechanisms* option allows you to specify the SASL mechanisms that this
* instance of Strophe.Connection (and therefore your XMPP client) will
* support.
*
* The value must be an array of objects with Strophe.SASLMechanism
* prototypes.
*
* If nothing is specified, then the following mechanisms (and their
* priorities) are registered:
*
* SCRAM-SHA1 - 70
* DIGEST-MD5 - 60
* PLAIN - 50
* OAUTH-BEARER - 40
* OAUTH-2 - 30
* ANONYMOUS - 20
* EXTERNAL - 10
*
* WebSocket options:
* ------------------
*
* If you want to connect to the current host with a WebSocket connection you
* can tell Strophe to use WebSockets through a "protocol" attribute in the
* optional options parameter. Valid values are "ws" for WebSocket and "wss"
* for Secure WebSocket.
* So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call
*
* > let conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"});
*
* Note that relative URLs _NOT_ starting with a "/" will also include the path
* of the current site.
*
* Also because downgrading security is not permitted by browsers, when using
* relative URLs both BOSH and WebSocket connections will use their secure
* variants if the current connection to the site is also secure (https).
*
* BOSH options:
* -------------
*
* By adding "sync" to the options, you can control if requests will
* be made synchronously or not. The default behaviour is asynchronous.
* If you want to make requests synchronous, make "sync" evaluate to true.
* > let conn = new Strophe.Connection("/http-bind/", {sync: true});
*
* You can also toggle this on an already established connection.
* > conn.options.sync = true;
*
* The *customHeaders* option can be used to provide custom HTTP headers to be
* included in the XMLHttpRequests made.
*
* The *keepalive* option can be used to instruct Strophe to maintain the
* current BOSH session across interruptions such as webpage reloads.
*
* It will do this by caching the sessions tokens in sessionStorage, and when
* "restore" is called it will check whether there are cached tokens with
* which it can resume an existing session.
*
* The *withCredentials* option should receive a Boolean value and is used to
* indicate wether cookies should be included in ajax requests (by default
* they're not).
* Set this value to true if you are connecting to a BOSH service
* and for some reason need to send cookies to it.
* In order for this to work cross-domain, the server must also enable
* credentials by setting the Access-Control-Allow-Credentials response header
* to "true". For most usecases however this setting should be false (which
* is the default).
* Additionally, when using Access-Control-Allow-Credentials, the
* Access-Control-Allow-Origin header can't be set to the wildcard "*", but
* instead must be restricted to actual domains.
*
* The *contentType* option can be set to change the default Content-Type
* of "text/xml; charset=utf-8", which can be useful to reduce the amount of
* CORS preflight requests that are sent to the server.
*
* Parameters:
* (String) service - The BOSH or WebSocket service URL.
* (Object) options - A hash of configuration options
*
* Returns:
* A new Strophe.Connection object.
*/
Strophe.Connection = function (service, options) {
// The service URL
this.service = service;
// Configuration options
this.options = options || {};
const proto = this.options.protocol || "";
// Select protocal based on service or options
if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 ||
proto.indexOf("ws") === 0) {
this._proto = new Strophe.Websocket(this);
} else {
this._proto = new Strophe.Bosh(this);
}
/* The connected JID. */
this.jid = "";
/* the JIDs domain */
this.domain = null;
/* stream:features */
this.features = null;
// SASL
this._sasl_data = {};
this.do_session = false;
this.do_bind = false;
// handler lists
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._idleTimeout = setTimeout(() => this._onIdle(), 100);
utils.addCookies(this.options.cookies);
this.registerSASLMechanisms(this.options.mechanisms);
// initialize plugins
for (const k in Strophe._connectionPlugins) {
if (Object.prototype.hasOwnProperty.call(Strophe._connectionPlugins, k)) {
const F = function () {};
F.prototype = Strophe._connectionPlugins[k];
this[k] = new F();
this[k].init(this);
}
}
};
Strophe.Connection.prototype = {
/** Function: reset
* Reset the connection.
*
* This function should be called after a connection is disconnected
* before that connection is reused.
*/
reset: function () {
this._proto._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;
},
/** Function: pause
* 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: function () {
this.paused = true;
},
/** Function: resume
* Resume the request manager.
*
* This resumes after pause() has been called.
*/
resume: function () {
this.paused = false;
},
/** Function: getUniqueId
* Generate a unique ID for use in elements.
*
* All 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.
*
* Parameters:
* (String) suffix - A optional suffix to append to the id.
*
* Returns:
* A unique string to be used for the id attribute.
*/
getUniqueId: function (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 + "";
}
},
/** Function: addProtocolErrorHandler
* 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.
*
* Parameters:
* (String) protocol - 'HTTP' or 'websocket'
* (Integer) status_code - Error status code (e.g 500, 400 or 404)
* (Function) callback - Function that will fire on Http error
*
* Example:
* function onError(err_code){
* //do stuff
* }
*
* let 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);
*/
addProtocolErrorHandler: function (protocol, status_code, callback){
this.protocolErrorHandlers[protocol][status_code] = callback;
},
/** Function: connect
* 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.
*
* Parameters:
* (String) 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) pass - The user's password.
* (Function) callback - The connect callback function.
* (Integer) 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.
* (Integer) 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).
* (String) route - The optional route value.
* (String) 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.
*/
connect: function (jid, pass, callback, wait, hold, route, authcid) {
this.jid = jid;
/** Variable: authzid
* Authorization identity.
*/
this.authzid = Strophe.getBareJidFromJid(this.jid);
/** Variable: authcid
* Authentication identity (User name).
*/
this.authcid = authcid || Strophe.getNodeFromJid(this.jid);
/** Variable: pass
* Authentication identity (User password).
*/
this.pass = pass;
/** Variable: servtype
* Digest MD5 compatibility.
*/
this.servtype = "xmpp";
this.connect_callback = callback;
this.disconnecting = false;
this.connected = false;
this.authenticated = false;
this.restored = false;
// parse jid for domain
this.domain = Strophe.getDomainFromJid(this.jid);
this._changeConnectStatus(Strophe.Status.CONNECTING, null);
this._proto._connect(wait, hold, route);
},
/** Function: attach
* 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.
*
* Parameters:
* (String) jid - The full JID that is bound by the session.
* (String) sid - The SID of the BOSH session.
* (String) rid - The current RID of the BOSH session. This RID
* will be used by the next request.
* (Function) callback The connect callback function.
* (Integer) 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.
* (Integer) 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).
* (Integer) wind - The optional HTTBIND window value. This is the
* allowed range of request ids that are valid. The default is 5.
*/
attach: function (jid, sid, rid, callback, wait, hold, wind) {
if (this._proto instanceof Strophe.Bosh) {
this._proto._attach(jid, sid, rid, callback, wait, hold, wind);
} else {
const error = new Error('The "attach" method can only be used with a BOSH connection.');
error.name = 'StropheSessionError';
throw error;
}
},
/** Function: restore
* Attempt to restore a cached BOSH session.
*
* This function is only useful in conjunction with providing the
* "keepalive":true option when instantiating a new Strophe.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
*
* Parameters:
* (String) jid - The user's JID. This may be a bare JID or a full JID.
* (Function) callback - The connect callback function.
* (Integer) 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.
* (Integer) 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).
* (Integer) wind - The optional HTTBIND window value. This is the
* allowed range of request ids that are valid. The default is 5.
*/
restore: function (jid, callback, wait, hold, wind) {
if (this._sessionCachingSupported()) {
this._proto._restore(jid, callback, wait, hold, wind);
} else {
const error = new Error('The "restore" method can only be used with a BOSH connection.');
error.name = 'StropheSessionError';
throw error;
}
},
/** PrivateFunction: _sessionCachingSupported
* Checks whether sessionStorage and JSON are supported and whether we're
* using BOSH.
*/
_sessionCachingSupported: function () {
if (this._proto instanceof Strophe.Bosh) {
if (!JSON) { return false; }
try {
sessionStorage.setItem('_strophe_', '_strophe_');
sessionStorage.removeItem('_strophe_');
} catch (e) {
return false;
}
return true;
}
return false;
},
/** Function: xmlInput
* User overrideable function that receives XML data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.xmlInput = function (elem) {
* > (user code)
* > };
*
* Due to limitations of current Browsers' XML-Parsers the opening and closing
* tag for WebSocket-Connoctions will be passed as selfclosing here.
*
* BOSH-Connections will have all stanzas wrapped in a tag. See
* if you want to strip this tag.
*
* Parameters:
* (XMLElement) elem - The XML data received by the connection.
*/
/* jshint unused:false */
xmlInput: function (elem) {
return;
},
/* jshint unused:true */
/** Function: xmlOutput
* User overrideable function that receives XML data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.xmlOutput = function (elem) {
* > (user code)
* > };
*
* Due to limitations of current Browsers' XML-Parsers the opening and closing
* tag for WebSocket-Connoctions will be passed as selfclosing here.
*
* BOSH-Connections will have all stanzas wrapped in a tag. See
* if you want to strip this tag.
*
* Parameters:
* (XMLElement) elem - The XMLdata sent by the connection.
*/
/* jshint unused:false */
xmlOutput: function (elem) {
return;
},
/* jshint unused:true */
/** Function: rawInput
* User overrideable function that receives raw data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.rawInput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data received by the connection.
*/
/* jshint unused:false */
rawInput: function (data) {
return;
},
/* jshint unused:true */
/** Function: rawOutput
* User overrideable function that receives raw data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.rawOutput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data sent by the connection.
*/
/* jshint unused:false */
rawOutput: function (data) {
return;
},
/* jshint unused:true */
/** Function: nextValidRid
* User overrideable function that receives the new valid rid.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.nextValidRid = function (rid) {
* > (user code)
* > };
*
* Parameters:
* (Number) rid - The next valid rid
*/
/* jshint unused:false */
nextValidRid: function (rid) {
return;
},
/* jshint unused:true */
/** Function: send
* 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.
*
* Parameters:
* (XMLElement |
* [XMLElement] |
* Strophe.Builder) elem - The stanza to send.
*/
send: function (elem) {
if (elem === null) { return ; }
if (typeof(elem.sort) === "function") {
for (let i=0; i < elem.length; i++) {
this._queueData(elem[i]);
}
} else if (typeof(elem.tree) === "function") {
this._queueData(elem.tree());
} else {
this._queueData(elem);
}
this._proto._send();
},
/** Function: flush
* 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: function () {
// cancel the pending idle period and run the idle function
// immediately
clearTimeout(this._idleTimeout);
this._onIdle();
},
/** Function: sendPresence
* 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).
*
* Parameters:
* (XMLElement) elem - The stanza to send.
* (Function) callback - The callback function for a successful request.
* (Function) errback - The callback function for a failed or timed
* out request. On timeout, the stanza will be null.
* (Integer) timeout - The time specified in milliseconds for a
* timeout to occur.
*
* Returns:
* The id used to send the presence.
*/
sendPresence: function(elem, callback, errback, timeout) {
let timeoutHandler = null;
if (typeof(elem.tree) === "function") {
elem = elem.tree();
}
let id = elem.getAttribute('id');
if (!id) { // inject id if not found
id = this.getUniqueId("sendPresence");
elem.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') {
if (errback) {
errback(stanza);
}
} else if (callback) {
callback(stanza);
}
}, 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
if (errback) {
errback(null);
}
return false;
});
}
}
this.send(elem);
return id;
},
/** Function: sendIQ
* Helper function to send IQ stanzas.
*
* Parameters:
* (XMLElement) elem - The stanza to send.
* (Function) callback - The callback function for a successful request.
* (Function) errback - The callback function for a failed or timed
* out request. On timeout, the stanza will be null.
* (Integer) timeout - The time specified in milliseconds for a
* timeout to occur.
*
* Returns:
* The id used to send the IQ.
*/
sendIQ: function(elem, callback, errback, timeout) {
let timeoutHandler = null;
if (typeof(elem.tree) === "function") {
elem = elem.tree();
}
let id = elem.getAttribute('id');
if (!id) { // inject id if not found
id = this.getUniqueId("sendIQ");
elem.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') {
if (callback) {
callback(stanza);
}
} else if (iqtype === 'error') {
if (errback) {
errback(stanza);
}
} else {
const error = new Error(`Got bad IQ type of ${iqtype}`);
error.name = "StropheError";
throw(error);
}
}, 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
if (errback) {
errback(null);
}
return false;
});
}
}
this.send(elem);
return id;
},
/** PrivateFunction: _queueData
* Queue outgoing data for later sending. Also ensures that the data
* is a DOMElement.
*/
_queueData: function (element) {
if (element === null ||
!element.tagName ||
!element.childNodes) {
const error = new Error("Cannot queue non-DOMElement.");
error.name = "StropheError";
throw(error);
}
this._data.push(element);
},
/** PrivateFunction: _sendRestart
* Send an xmpp:restart stanza.
*/
_sendRestart: function () {
this._data.push("restart");
this._proto._sendRestart();
this._idleTimeout = setTimeout(() => this._onIdle(), 100);
},
/** Function: addTimedHandler
* 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.
*
* Parameters:
* (Integer) period - The period of the handler.
* (Function) handler - The callback function.
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addTimedHandler: function (period, handler) {
const thand = new Strophe.TimedHandler(period, handler);
this.addTimeds.push(thand);
return thand;
},
/** Function: deleteTimedHandler
* 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().
*
* Parameters:
* (Strophe.TimedHandler) handRef - The handler reference.
*/
deleteTimedHandler: function (handRef) {
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeTimeds.push(handRef);
},
/** Function: addHandler
* 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().
*
* Parameters:
* (Function) handler - The user callback.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String|Array) type - The stanza type (or types if an array) to match.
* (String) id - The stanza id attribute to match.
* (String) from - The stanza from attribute to match.
* (String) options - The handler options
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addHandler: function (handler, ns, name, type, id, from, options) {
const hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
this.addHandlers.push(hand);
return hand;
},
/** Function: deleteHandler
* 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().
*
* Parameters:
* (Strophe.Handler) handRef - The handler reference.
*/
deleteHandler: function (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);
}
},
/** Function: registerSASLMechanisms
*
* Register the SASL mechanisms which will be supported by this instance of
* Strophe.Connection (i.e. which this XMPP client will support).
*
* Parameters:
* (Array) mechanisms - Array of objects with Strophe.SASLMechanism prototypes
*
*/
registerSASLMechanisms: function (mechanisms) {
this.mechanisms = {};
mechanisms = mechanisms || [
Strophe.SASLAnonymous,
Strophe.SASLExternal,
Strophe.SASLMD5,
Strophe.SASLOAuthBearer,
Strophe.SASLXOAuth2,
Strophe.SASLPlain,
Strophe.SASLSHA1
];
mechanisms.forEach(this.registerSASLMechanism.bind(this));
},
/** Function: registerSASLMechanism
*
* Register a single SASL mechanism, to be supported by this client.
*
* Parameters:
* (Object) mechanism - Object with a Strophe.SASLMechanism prototype
*
*/
registerSASLMechanism: function (mechanism) {
this.mechanisms[mechanism.prototype.name] = mechanism;
},
/** Function: disconnect
* 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.
*
* Parameters:
* (String) reason - The reason the disconnect is occuring.
*/
disconnect: function (reason) {
this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
Strophe.info("Disconnect was called because: " + reason);
if (this.connected) {
let pres = false;
this.disconnecting = true;
if (this.authenticated) {
pres = $pres({
'xmlns': Strophe.NS.CLIENT,
'type': 'unavailable'
});
}
// setup timeout handler
this._disconnectTimeout = this._addSysTimedHandler(
3000, this._onDisconnectTimeout.bind(this));
this._proto._disconnect(pres);
} else {
Strophe.info("Disconnect was called before Strophe connected to the server");
this._proto._abortAllRequests();
this._doDisconnect();
}
},
/** PrivateFunction: _changeConnectStatus
* _Private_ helper function that makes sure plugins and the user's
* callback are notified of connection status changes.
*
* Parameters:
* (Integer) status - the new connection status, one of the values
* in Strophe.Status
* (String) condition - the error condition or null
* (XMLElement) elem - The triggering stanza.
*/
_changeConnectStatus: function (status, condition, elem) {
// notify all plugins listening for status changes
for (const k in Strophe._connectionPlugins) {
if (Object.prototype.hasOwnProperty.call(Strophe._connectionPlugins, k)) {
const plugin = this[k];
if (plugin.statusChanged) {
try {
plugin.statusChanged(status, condition);
} catch (err) {
Strophe.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) {
Strophe._handleError(e);
Strophe.error(`User connection callback caused an exception: ${e}`);
}
}
},
/** PrivateFunction: _doDisconnect
* _Private_ function to disconnect.
*
* This is the last piece of the disconnection logic. This resets the
* connection and alerts the user's connection callback.
*/
_doDisconnect: function (condition) {
if (typeof this._idleTimeout === "number") {
clearTimeout(this._idleTimeout);
}
// Cancel Disconnect Timeout
if (this._disconnectTimeout !== null) {
this.deleteTimedHandler(this._disconnectTimeout);
this._disconnectTimeout = null;
}
Strophe.info("_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(Strophe.Status.DISCONNECTED, condition);
this.connected = false;
},
/** PrivateFunction: _dataRecv
* _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.
*
* Parameters:
* (Strophe.Request) req - The request that has data ready.
* (string) req - The stanza a raw string (optiona).
*/
_dataRecv: function (req, raw) {
Strophe.info("_dataRecv called");
const elem = this._proto._reqToData(req);
if (elem === null) { return; }
if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) {
if (elem.nodeName === this._proto.strip && elem.childNodes.length) {
this.xmlInput(elem.childNodes[0]);
} else {
this.xmlInput(elem);
}
}
if (this.rawInput !== Strophe.Connection.prototype.rawInput) {
if (raw) {
this.rawInput(raw);
} else {
this.rawInput(Strophe.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(Strophe.Status.CONNFAIL, cond);
} else {
this._changeConnectStatus(
Strophe.Status.CONNFAIL,
Strophe.ErrorCondition.UNKOWN_REASON
);
}
this._doDisconnect(cond);
return;
}
// send each incoming stanza through the handler chain
Strophe.forEachChild(elem, null, child => {
// process handlers
const newList = this.handlers;
this.handlers = [];
for (let i=0; i < newList.length; i++) {
const hand = newList[i];
// encapsulate 'handler.run' not to lose the whole handler list if
// one of the handlers throws an exception
try {
if (hand.isMatch(child) &&
(this.authenticated || !hand.user)) {
if (hand.run(child)) {
this.handlers.push(hand);
}
} else {
this.handlers.push(hand);
}
} catch(e) {
// if the handler throws an exception, we consider it as false
Strophe.warn('Removing Strophe handlers due to uncaught exception: '+e.message);
}
}
});
},
/** Attribute: mechanisms
* SASL Mechanisms available for Connection.
*/
mechanisms: {},
/** PrivateFunction: _connect_cb
* _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.
*
* Parameters:
* (Strophe.Request) req - The current request.
* (Function) _callback - low level (xmpp) connect callback function.
* Useful for plugins with their own xmpp connect callback (when they
* want to do something special).
*/
_connect_cb: function (req, _callback, raw) {
Strophe.info("_connect_cb was called");
this.connected = true;
let bodyWrap;
try {
bodyWrap = this._proto._reqToData(req);
} catch (e) {
if (e.name !== Strophe.ErrorCondition.BAD_FORMAT) { throw e; }
this._changeConnectStatus(
Strophe.Status.CONNFAIL,
Strophe.ErrorCondition.BAD_FORMAT
);
this._doDisconnect(Strophe.ErrorCondition.BAD_FORMAT);
}
if (!bodyWrap) { return; }
if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) {
if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) {
this.xmlInput(bodyWrap.childNodes[0]);
} else {
this.xmlInput(bodyWrap);
}
}
if (this.rawInput !== Strophe.Connection.prototype.rawInput) {
if (raw) {
this.rawInput(raw);
} else {
this.rawInput(Strophe.serialize(bodyWrap));
}
}
const conncheck = this._proto._connect_cb(bodyWrap);
if (conncheck === Strophe.Status.CONNFAIL) {
return;
}
// Check for the stream:features tag
let hasFeatures;
if (bodyWrap.getElementsByTagNameNS) {
hasFeatures = bodyWrap.getElementsByTagNameNS(Strophe.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 = [];
const mechanisms = bodyWrap.getElementsByTagName("mechanism");
if (mechanisms.length > 0) {
for (let i=0; i < mechanisms.length; i++) {
const mech = Strophe.getText(mechanisms[i]);
if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]);
}
}
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);
}
},
/** Function: sortMechanismsByPriority
*
* Sorts an array of objects with prototype SASLMechanism according to
* their priorities.
*
* Parameters:
* (Array) mechanisms - Array of SASL mechanisms.
*
*/
sortMechanismsByPriority: function (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].prototype.priority > mechanisms[higher].prototype.priority) {
higher = j;
}
}
if (higher !== i) {
const swap = mechanisms[i];
mechanisms[i] = mechanisms[higher];
mechanisms[higher] = swap;
}
}
return mechanisms;
},
/** PrivateFunction: _attemptSASLAuth
*
* Iterate through an array of SASL mechanisms and attempt authentication
* with the highest priority (enabled) mechanism.
*
* Parameters:
* (Array) mechanisms - Array of SASL mechanisms.
*
* Returns:
* (Boolean) mechanism_found - true or false, depending on whether a
* valid SASL mechanism was found with which authentication could be
* started.
*/
_attemptSASLAuth: function (mechanisms) {
mechanisms = this.sortMechanismsByPriority(mechanisms || []);
let mechanism_found = false;
for (let i=0; i < mechanisms.length; ++i) {
if (!mechanisms[i].prototype.test(this)) {
continue;
}
this._sasl_success_handler = this._addSysHandler(
this._sasl_success_cb.bind(this), null,
"success", null, null);
this._sasl_failure_handler = this._addSysHandler(
this._sasl_failure_cb.bind(this), null,
"failure", null, null);
this._sasl_challenge_handler = this._addSysHandler(
this._sasl_challenge_cb.bind(this), null,
"challenge", null, null);
this._sasl_mechanism = new mechanisms[i]();
this._sasl_mechanism.onStart(this);
const request_auth_exchange = $build("auth", {
'xmlns': Strophe.NS.SASL,
'mechanism': this._sasl_mechanism.name
});
if (this._sasl_mechanism.isClientFirst) {
const response = this._sasl_mechanism.onChallenge(this, null);
request_auth_exchange.t(btoa(response));
}
this.send(request_auth_exchange.tree());
mechanism_found = true;
break;
}
return mechanism_found;
},
/** PrivateFunction: _attemptLegacyAuth
*
* Attempt legacy (i.e. non-SASL) authentication.
*
*/
_attemptLegacyAuth: function () {
if (Strophe.getNodeFromJid(this.jid) === null) {
// we don't have a node, which is required for non-anonymous
// client connections
this._changeConnectStatus(
Strophe.Status.CONNFAIL,
Strophe.ErrorCondition.MISSING_JID_NODE
);
this.disconnect(Strophe.ErrorCondition.MISSING_JID_NODE);
} else {
// Fall back to legacy authentication
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
this._addSysHandler(
this._auth1_cb.bind(this),
null, null, null, "_auth_1"
);
this.send($iq({
'type': "get",
'to': this.domain,
'id': "_auth_1"
}).c("query", {xmlns: Strophe.NS.AUTH})
.c("username", {}).t(Strophe.getNodeFromJid(this.jid))
.tree());
}
},
/** Function: authenticate
* Set up authentication
*
* Continues the initial connection request by setting up authentication
* handlers and starting the authentication process.
*
* SASL authentication will be attempted if available, otherwise
* the code will fall back to legacy authentication.
*
* Parameters:
* (Array) matched - Array of SASL mechanisms supported.
*
*/
authenticate: function (matched) {
if (!this._attemptSASLAuth(matched)) {
this._attemptLegacyAuth();
}
},
/** PrivateFunction: _sasl_challenge_cb
* _Private_ handler for the SASL challenge
*
*/
_sasl_challenge_cb: function(elem) {
const challenge = atob(Strophe.getText(elem));
const response = this._sasl_mechanism.onChallenge(this, challenge);
const stanza = $build('response', {'xmlns': Strophe.NS.SASL});
if (response !== "") {
stanza.t(btoa(response));
}
this.send(stanza.tree());
return true;
},
/** PrivateFunction: _auth1_cb
* _Private_ handler for legacy authentication.
*
* This handler is called in response to the initial
* for legacy authentication. It builds an authentication and
* sends it, creating a handler (calling back to _auth2_cb()) to
* handle the result
*
* Parameters:
* (XMLElement) elem - The stanza that triggered the callback.
*
* Returns:
* false to remove the handler.
*/
/* jshint unused:false */
_auth1_cb: function (elem) {
// build plaintext auth iq
const iq = $iq({type: "set", id: "_auth_2"})
.c('query', {xmlns: Strophe.NS.AUTH})
.c('username', {}).t(Strophe.getNodeFromJid(this.jid))
.up()
.c('password').t(this.pass);
if (!Strophe.getResourceFromJid(this.jid)) {
// since the user has not supplied a resource, we pick
// a default one here. unlike other auth methods, the server
// cannot do this for us.
this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
}
iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));
this._addSysHandler(this._auth2_cb.bind(this), null, null, null, "_auth_2");
this.send(iq.tree());
return false;
},
/* jshint unused:true */
/** PrivateFunction: _sasl_success_cb
* _Private_ handler for succesful SASL authentication.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_success_cb: function (elem) {
if (this._sasl_data["server-signature"]) {
let serverSignature;
const success = atob(Strophe.getText(elem));
const attribMatch = /([a-z]+)=([^,]+)(,|$)/;
const matches = success.match(attribMatch);
if (matches[1] === "v") {
serverSignature = matches[2];
}
if (serverSignature !== this._sasl_data["server-signature"]) {
// remove old handlers
this.deleteHandler(this._sasl_failure_handler);
this._sasl_failure_handler = null;
if (this._sasl_challenge_handler) {
this.deleteHandler(this._sasl_challenge_handler);
this._sasl_challenge_handler = null;
}
this._sasl_data = {};
return this._sasl_failure_cb(null);
}
}
Strophe.info("SASL authentication succeeded.");
if (this._sasl_mechanism) {
this._sasl_mechanism.onSuccess();
}
// remove old handlers
this.deleteHandler(this._sasl_failure_handler);
this._sasl_failure_handler = null;
if (this._sasl_challenge_handler) {
this.deleteHandler(this._sasl_challenge_handler);
this._sasl_challenge_handler = null;
}
const streamfeature_handlers = [];
const wrapper = (handlers, elem) => {
while (handlers.length) {
this.deleteHandler(handlers.pop());
}
this._sasl_auth1_cb(elem);
return false;
};
streamfeature_handlers.push(
this._addSysHandler(elem => wrapper(streamfeature_handlers, elem),
null, "stream:features", null, null)
);
streamfeature_handlers.push(
this._addSysHandler(elem => wrapper(streamfeature_handlers, elem),
Strophe.NS.STREAM, "features", null, null)
);
// we must send an xmpp:restart now
this._sendRestart();
return false;
},
/** PrivateFunction: _sasl_auth1_cb
* _Private_ handler to start stream binding.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_auth1_cb: function (elem) {
// save stream:features for future usage
this.features = elem;
for (let i=0; i < elem.childNodes.length; i++) {
const child = elem.childNodes[i];
if (child.nodeName === 'bind') {
this.do_bind = true;
}
if (child.nodeName === 'session') {
this.do_session = true;
}
}
if (!this.do_bind) {
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
return false;
} else {
this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
null, "_bind_auth_2");
const resource = Strophe.getResourceFromJid(this.jid);
if (resource) {
this.send($iq({type: "set", id: "_bind_auth_2"})
.c('bind', {xmlns: Strophe.NS.BIND})
.c('resource', {}).t(resource).tree());
} else {
this.send($iq({type: "set", id: "_bind_auth_2"})
.c('bind', {xmlns: Strophe.NS.BIND})
.tree());
}
}
return false;
},
/** PrivateFunction: _sasl_bind_cb
* _Private_ handler for binding result and session start.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_bind_cb: function (elem) {
if (elem.getAttribute("type") === "error") {
Strophe.info("SASL binding failed.");
const conflict = elem.getElementsByTagName("conflict");
let condition;
if (conflict.length > 0) {
condition = Strophe.ErrorCondition.CONFLICT;
}
this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition, elem);
return false;
}
// TODO - need to grab errors
const bind = elem.getElementsByTagName("bind");
if (bind.length > 0) {
const jidNode = bind[0].getElementsByTagName("jid");
if (jidNode.length > 0) {
this.jid = Strophe.getText(jidNode[0]);
if (this.do_session) {
this._addSysHandler(this._sasl_session_cb.bind(this),
null, null, null, "_session_auth_2");
this.send($iq({type: "set", id: "_session_auth_2"})
.c('session', {xmlns: Strophe.NS.SESSION})
.tree());
} else {
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
}
}
} else {
Strophe.info("SASL binding failed.");
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
return false;
}
},
/** PrivateFunction: _sasl_session_cb
* _Private_ handler to finish successful SASL connection.
*
* This sets Connection.authenticated to true on success, which
* starts the processing of user handlers.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_session_cb: function (elem) {
if (elem.getAttribute("type") === "result") {
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
} else if (elem.getAttribute("type") === "error") {
Strophe.info("Session creation failed.");
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
return false;
}
return false;
},
/** PrivateFunction: _sasl_failure_cb
* _Private_ handler for SASL authentication failure.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
/* jshint unused:false */
_sasl_failure_cb: function (elem) {
// delete unneeded handlers
if (this._sasl_success_handler) {
this.deleteHandler(this._sasl_success_handler);
this._sasl_success_handler = null;
}
if (this._sasl_challenge_handler) {
this.deleteHandler(this._sasl_challenge_handler);
this._sasl_challenge_handler = null;
}
if(this._sasl_mechanism)
this._sasl_mechanism.onFailure();
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
return false;
},
/* jshint unused:true */
/** PrivateFunction: _auth2_cb
* _Private_ handler to finish legacy authentication.
*
* This handler is called when the result from the jabber:iq:auth
* stanza is returned.
*
* Parameters:
* (XMLElement) elem - The stanza that triggered the callback.
*
* Returns:
* false to remove the handler.
*/
_auth2_cb: function (elem) {
if (elem.getAttribute("type") === "result") {
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
} else if (elem.getAttribute("type") === "error") {
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem);
this.disconnect('authentication failed');
}
return false;
},
/** PrivateFunction: _addSysTimedHandler
* _Private_ function to add a system level timed handler.
*
* This function is used to add a Strophe.TimedHandler for the
* library code. System timed handlers are allowed to run before
* authentication is complete.
*
* Parameters:
* (Integer) period - The period of the handler.
* (Function) handler - The callback function.
*/
_addSysTimedHandler: function (period, handler) {
const thand = new Strophe.TimedHandler(period, handler);
thand.user = false;
this.addTimeds.push(thand);
return thand;
},
/** PrivateFunction: _addSysHandler
* _Private_ function to add a system level stanza handler.
*
* This function is used to add a Strophe.Handler for the
* library code. System stanza handlers are allowed to run before
* authentication is complete.
*
* Parameters:
* (Function) handler - The callback function.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String) type - The stanza type attribute to match.
* (String) id - The stanza id attribute to match.
*/
_addSysHandler: function (handler, ns, name, type, id) {
const hand = new Strophe.Handler(handler, ns, name, type, id);
hand.user = false;
this.addHandlers.push(hand);
return hand;
},
/** PrivateFunction: _onDisconnectTimeout
* _Private_ timeout handler for handling non-graceful disconnection.
*
* If the graceful disconnect process does not complete within the
* time allotted, this handler finishes the disconnect anyway.
*
* Returns:
* false to remove the handler.
*/
_onDisconnectTimeout: function () {
Strophe.info("_onDisconnectTimeout was called");
this._changeConnectStatus(Strophe.Status.CONNTIMEOUT, null);
this._proto._onDisconnectTimeout();
// actually disconnect
this._doDisconnect();
return false;
},
/** PrivateFunction: _onIdle
* _Private_ handler to process events during idle cycle.
*
* This handler is called every 100ms to fire timed handlers that
* are ready and keep poll requests going.
*/
_onIdle: function () {
// add timed handlers scheduled for addition
// NOTE: we add before remove in the case a timed handler is
// added and then deleted before the next _onIdle() call.
while (this.addTimeds.length > 0) {
this.timedHandlers.push(this.addTimeds.pop());
}
// remove timed handlers that have been scheduled for deletion
while (this.removeTimeds.length > 0) {
const thand = this.removeTimeds.pop();
const i = this.timedHandlers.indexOf(thand);
if (i >= 0) {
this.timedHandlers.splice(i, 1);
}
}
// call ready timed handlers
const now = new Date().getTime();
const newList = [];
for (let i=0; i < this.timedHandlers.length; i++) {
const thand = this.timedHandlers[i];
if (this.authenticated || !thand.user) {
const since = thand.lastCalled + thand.period;
if (since - now <= 0) {
if (thand.run()) {
newList.push(thand);
}
} else {
newList.push(thand);
}
}
}
this.timedHandlers = newList;
clearTimeout(this._idleTimeout);
this._proto._onIdle();
// reactivate the timer only if connected
if (this.connected) {
this._idleTimeout = setTimeout(() => this._onIdle(), 100);
}
}
};
/** Class: Strophe.SASLMechanism
*
* encapsulates SASL authentication mechanisms.
*
* User code may override the priority for each mechanism or disable it completely.
* See for information about changing priority and for informatian on
* how to disable a mechanism.
*
* By default, all mechanisms are enabled and the priorities are
*
* OAUTHBEARER - 60
* SCRAM-SHA1 - 50
* DIGEST-MD5 - 40
* PLAIN - 30
* ANONYMOUS - 20
* EXTERNAL - 10
*
* See: Strophe.Connection.addSupportedSASLMechanisms
*/
/**
* PrivateConstructor: Strophe.SASLMechanism
* SASL auth mechanism abstraction.
*
* Parameters:
* (String) name - SASL Mechanism name.
* (Boolean) isClientFirst - If client should send response first without challenge.
* (Number) priority - Priority.
*
* Returns:
* A new Strophe.SASLMechanism object.
*/
Strophe.SASLMechanism = function(name, isClientFirst, priority) {
/** PrivateVariable: name
* Mechanism name.
*/
this.name = name;
/** PrivateVariable: isClientFirst
* If client sends response without initial server challenge.
*/
this.isClientFirst = isClientFirst;
/** Variable: priority
* Determines which is chosen for authentication (Higher is better).
* Users may override this to prioritize mechanisms differently.
*
* In the default configuration the priorities are
*
* SCRAM-SHA1 - 40
* DIGEST-MD5 - 30
* Plain - 20
*
* Example: (This will cause Strophe to choose the mechanism that the server sent first)
*
* > Strophe.SASLMD5.priority = Strophe.SASLSHA1.priority;
*
* See for a list of available mechanisms.
*
*/
this.priority = priority;
};
Strophe.SASLMechanism.prototype = {
/**
* Function: test
* Checks if mechanism able to run.
* To disable a mechanism, make this return false;
*
* To disable plain authentication run
* > Strophe.SASLPlain.test = function() {
* > return false;
* > }
*
* See for a list of available mechanisms.
*
* Parameters:
* (Strophe.Connection) connection - Target Connection.
*
* Returns:
* (Boolean) If mechanism was able to run.
*/
/* jshint unused:false */
test: function(connection) {
return true;
},
/* jshint unused:true */
/** PrivateFunction: onStart
* Called before starting mechanism on some connection.
*
* Parameters:
* (Strophe.Connection) connection - Target Connection.
*/
onStart: function(connection) {
this._connection = connection;
},
/** PrivateFunction: onChallenge
* Called by protocol implementation on incoming challenge. If client is
* first (isClientFirst === true) challenge will be null on the first call.
*
* Parameters:
* (Strophe.Connection) connection - Target Connection.
* (String) challenge - current challenge to handle.
*
* Returns:
* (String) Mechanism response.
*/
/* jshint unused:false */
onChallenge: function (connection, challenge) {
throw new Error("You should implement challenge handling!");
},
/* jshint unused:true */
/** PrivateFunction: onFailure
* Protocol informs mechanism implementation about SASL failure.
*/
onFailure: function() {
this._connection = null;
},
/** PrivateFunction: onSuccess
* Protocol informs mechanism implementation about SASL success.
*/
onSuccess: function() {
this._connection = null;
}
};
/** Constants: SASL mechanisms
* Available authentication mechanisms
*
* Strophe.SASLAnonymous - SASL ANONYMOUS authentication.
* Strophe.SASLPlain - SASL PLAIN authentication.
* Strophe.SASLMD5 - SASL DIGEST-MD5 authentication
* Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication
* Strophe.SASLOAuthBearer - SASL OAuth Bearer authentication
* Strophe.SASLExternal - SASL EXTERNAL authentication
* Strophe.SASLXOAuth2 - SASL X-OAuth2 authentication
*/
// Building SASL callbacks
/** PrivateConstructor: SASLAnonymous
* SASL ANONYMOUS authentication.
*/
Strophe.SASLAnonymous = function() {};
Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 20);
Strophe.SASLAnonymous.prototype.test = function(connection) {
return connection.authcid === null;
};
/** PrivateConstructor: SASLPlain
* SASL PLAIN authentication.
*/
Strophe.SASLPlain = function() {};
Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 50);
Strophe.SASLPlain.prototype.test = function(connection) {
return connection.authcid !== null;
};
Strophe.SASLPlain.prototype.onChallenge = function(connection) {
let auth_str = connection.authzid;
auth_str = auth_str + "\u0000";
auth_str = auth_str + connection.authcid;
auth_str = auth_str + "\u0000";
auth_str = auth_str + connection.pass;
return utils.utf16to8(auth_str);
};
/** PrivateConstructor: SASLSHA1
* SASL SCRAM SHA 1 authentication.
*/
Strophe.SASLSHA1 = function() {};
Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 70);
Strophe.SASLSHA1.prototype.test = function(connection) {
return connection.authcid !== null;
};
Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cnonce) {
const cnonce = test_cnonce || MD5.hexdigest(Math.random() * 1234567890);
let auth_str = "n=" + utils.utf16to8(connection.authcid);
auth_str += ",r=";
auth_str += cnonce;
connection._sasl_data.cnonce = cnonce;
connection._sasl_data["client-first-message-bare"] = auth_str;
auth_str = "n,," + auth_str;
this.onChallenge = (connection, challenge) => {
let nonce, salt, iter, Hi, U, U_old, i, k;
let responseText = "c=biws,";
let authMessage = `${connection._sasl_data["client-first-message-bare"]},${challenge},`;
const cnonce = connection._sasl_data.cnonce;
const attribMatch = /([a-z]+)=([^,]+)(,|$)/;
while (challenge.match(attribMatch)) {
const matches = challenge.match(attribMatch);
challenge = challenge.replace(matches[0], "");
switch (matches[1]) {
case "r":
nonce = matches[2];
break;
case "s":
salt = matches[2];
break;
case "i":
iter = matches[2];
break;
}
}
if (nonce.substr(0, cnonce.length) !== cnonce) {
connection._sasl_data = {};
return connection._sasl_failure_cb();
}
responseText += "r=" + nonce;
authMessage += responseText;
salt = atob(salt);
salt += "\x00\x00\x00\x01";
const pass = utils.utf16to8(connection.pass);
Hi = U_old = SHA1.core_hmac_sha1(pass, salt);
for (i=1; i "";
return responseText;
};
/** PrivateConstructor: SASLOAuthBearer
* SASL OAuth Bearer authentication.
*/
Strophe.SASLOAuthBearer = function() {};
Strophe.SASLOAuthBearer.prototype = new Strophe.SASLMechanism("OAUTHBEARER", true, 40);
Strophe.SASLOAuthBearer.prototype.test = function(connection) {
return connection.pass !== null;
};
Strophe.SASLOAuthBearer.prototype.onChallenge = function(connection) {
let auth_str = 'n,';
if (connection.authcid !== null) {
auth_str = auth_str + 'a=' + connection.authzid;
}
auth_str = auth_str + ',';
auth_str = auth_str + "\u0001";
auth_str = auth_str + 'auth=Bearer ';
auth_str = auth_str + connection.pass;
auth_str = auth_str + "\u0001";
auth_str = auth_str + "\u0001";
return utils.utf16to8(auth_str);
};
/** PrivateConstructor: SASLExternal
* SASL EXTERNAL authentication.
*
* The EXTERNAL mechanism allows a client to request the server to use
* credentials established by means external to the mechanism to
* authenticate the client. The external means may be, for instance,
* TLS services.
*/
Strophe.SASLExternal = function() {};
Strophe.SASLExternal.prototype = new Strophe.SASLMechanism("EXTERNAL", true, 10);
Strophe.SASLExternal.prototype.onChallenge = function(connection) {
/** According to XEP-178, an authzid SHOULD NOT be presented when the
* authcid contained or implied in the client certificate is the JID (i.e.
* authzid) with which the user wants to log in as.
*
* To NOT send the authzid, the user should therefore set the authcid equal
* to the JID when instantiating a new Strophe.Connection object.
*/
return connection.authcid === connection.authzid ? '' : connection.authzid;
};
/** PrivateConstructor: SASLXOAuth2
* SASL X-OAuth2 authentication.
*/
Strophe.SASLXOAuth2 = function () { };
Strophe.SASLXOAuth2.prototype = new Strophe.SASLMechanism("X-OAUTH2", true, 30);
Strophe.SASLXOAuth2.prototype.test = function (connection) {
return connection.pass !== null;
};
Strophe.SASLXOAuth2.prototype.onChallenge = function (connection) {
let auth_str = '\u0000';
if (connection.authcid !== null) {
auth_str = auth_str + connection.authzid;
}
auth_str = auth_str + "\u0000";
auth_str = auth_str + connection.pass;
return utils.utf16to8(auth_str);
};
export { Strophe };
export { $build };
export { $iq };
export { $msg };
export { $pres };
export { SHA1 };
export { MD5 };
export default {
'Strophe': Strophe,
'$build': $build,
'$iq': $iq,
'$msg': $msg,
'$pres': $pres,
'SHA1': SHA1,
'MD5': MD5,
'b64_hmac_sha1': SHA1.b64_hmac_sha1,
'b64_sha1': SHA1.b64_sha1,
'str_hmac_sha1': SHA1.str_hmac_sha1,
'str_sha1': SHA1.str_sha1
};