diff options
| author | Guillaume Horel <guillaume@gmail.com> | 2012-01-18 22:51:23 +0100 |
|---|---|---|
| committer | Guillaume Horel <guillaume@gmail.com> | 2012-01-18 22:51:23 +0100 |
| commit | 3f3018f542830b68acaa95714897843624434087 (patch) | |
| tree | 8326611d59d3c375f2bddbbcee1cf01b849c3da8 /webclient/lib/strophe.js | |
| parent | 01e7057e94c6f502b6d283453315e5a718786d04 (diff) | |
| download | alias-3f3018f542830b68acaa95714897843624434087.tar.gz | |
Updated sjcl and strop to the latest release
Now the bug fixing starts!
Diffstat (limited to 'webclient/lib/strophe.js')
| -rw-r--r-- | webclient/lib/strophe.js | 781 |
1 files changed, 704 insertions, 77 deletions
diff --git a/webclient/lib/strophe.js b/webclient/lib/strophe.js index 4151135..b30ca50 100644 --- a/webclient/lib/strophe.js +++ b/webclient/lib/strophe.js @@ -79,6 +79,213 @@ var Base64 = (function () { return obj; })(); /* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS PUB 180-1 + * Version 2.1a Copyright Paul Johnston 2000 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} +function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} +function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} +function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} +function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} +function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha1_vm_test() +{ + return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; +} + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function core_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = new Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + var i, j, t, olda, oldb, oldc, oldd, olde; + for (i = 0; i < x.length; i += 16) + { + olda = a; + oldb = b; + oldc = c; + oldd = d; + olde = e; + + for (j = 0; j < 80; j++) + { + if (j < 16) { w[j] = x[i + j]; } + else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); } + t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return [a, b, c, d, e]; +} + +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if (t < 20) { return (b & c) | ((~b) & d); } + if (t < 40) { return b ^ c ^ d; } + if (t < 60) { return (b & c) | (b & d) | (c & d); } + return b ^ c ^ d; +} + +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} + +/* + * Calculate the HMAC-SHA1 of a key and some data + */ +function core_hmac_sha1(key, data) +{ + var bkey = str2binb(key); + if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * chrsz); } + + var ipad = new Array(16), opad = new Array(16); + for (var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); + return core_sha1(opad.concat(hash), 512 + 160); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ +function str2binb(str) +{ + var bin = []; + var mask = (1 << chrsz) - 1; + for (var i = 0; i < str.length * chrsz; i += chrsz) + { + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); + } + return bin; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for (var i = 0; i < bin.length * 32; i += chrsz) + { + str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); + } + return str; +} + +/* + * Convert an array of big-endian words to a hex string. + */ +function binb2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for (var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of big-endian words to a base-64 string + */ +function binb2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + for (var i = 0; i < binarray.length * 4; i += 3) + { + triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) | + (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) | + ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for (j = 0; j < 4; j++) + { + if (i * 8 + j * 6 > binarray.length * 32) { str += b64pad; } + else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } + } + } + return str; +} +/* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. @@ -405,7 +612,7 @@ if (!Function.prototype.bind) { var _slice = Array.prototype.slice; var _concat = Array.prototype.concat; var _args = _slice.call(arguments, 1); - + return function () { return func.apply(obj ? obj : this, _concat.call(_args, @@ -514,7 +721,7 @@ Strophe = { * The version of the Strophe library. Unreleased builds will have * a version of head-HASH where HASH is a partial revision. */ - VERSION: "5e6ee02", + VERSION: "f013c94", /** Constants: XMPP Namespace Constants * Common namespace constants from the XMPP RFCs and XEPs. @@ -532,6 +739,8 @@ Strophe = { * 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", @@ -548,10 +757,68 @@ Strophe = { 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" + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas", + XHTML_IM: "http://jabber.org/protocol/xhtml-im", + XHTML: "http://www.w3.org/1999/xhtml" }, - /** Function: addNamespace + + /** 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'], + validTag: function(tag) + { + for(var i = 0; i < Strophe.XHTML.tags.length; i++) { + if(tag == Strophe.XHTML.tags[i]) { + return true; + } + } + return false; + }, + validAttribute: function(tag, attribute) + { + if(typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) { + for(var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + if(attribute == Strophe.XHTML.attributes[tag][i]) { + return true; + } + } + } + return false; + }, + validCSS: function(style) + { + for(var i = 0; i < Strophe.XHTML.css.length; i++) { + if(style == Strophe.XHTML.css[i]) { + return true; + } + } + return false; + } + }, + + /** Function: addNamespace * This function is used to extend the current namespaces in * Strophe.NS. It takes a key and a value with the key being the * name of the new namespace, with its actual value. @@ -565,7 +832,7 @@ Strophe = { */ addNamespace: function (name, value) { - Strophe.NS[name] = value; + Strophe.NS[name] = value; }, /** Constants: Connection Status Constants @@ -616,10 +883,13 @@ Strophe = { * * ElementType.NORMAL - Normal element. * ElementType.TEXT - Text data element. + * ElementType.FRAGMENT - XHTML fragment element. */ ElementType: { NORMAL: 1, - TEXT: 3 + TEXT: 3, + CDATA: 4, + FRAGMENT: 11 }, /** PrivateConstants: Timeout Values @@ -697,7 +967,7 @@ Strophe = { _makeGenerator: function () { var doc; - if (window.ActiveXObject) { + if (document.implementation.createDocument === undefined) { doc = this._getIEXmlDom(); doc.appendChild(doc.createElement('strophe')); } else { @@ -820,9 +1090,11 @@ Strophe = { */ xmlescape: function(text) { - text = text.replace(/\&/g, "&"); + text = text.replace(/\&/g, "&"); text = text.replace(/</g, "<"); text = text.replace(/>/g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, """); return text; }, @@ -839,12 +1111,32 @@ Strophe = { */ xmlTextNode: function (text) { - //ensure text is escaped - text = Strophe.xmlescape(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) + { + //ensure text is escaped + if (window.DOMParser) { + 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. * @@ -870,7 +1162,7 @@ Strophe = { } } - return str; + return Strophe.xmlescape(str); }, /** Function: copyElement @@ -900,6 +1192,83 @@ Strophe = { el.appendChild(Strophe.copyElement(elem.childNodes[i])); } } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlGenerator().createTextNode(elem.nodeValue); + } + + return el; + }, + + + /** Function: createHtml + * Copy an HTML DOM element into an XML DOM. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (HTMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + createHtml: function (elem) + { + var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue, children, child; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + tag = elem.nodeName.toLowerCase(); + if(Strophe.XHTML.validTag(tag)) { + try { + el = Strophe.xmlElement(tag); + for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + attribute = Strophe.XHTML.attributes[tag][i]; + value = elem.getAttribute(attribute); + if(typeof value == 'undefined' || value === null || value === '' || value === false || value === 0) { + continue; + } + if(attribute == 'style' && typeof value == 'object') { + if(typeof value.cssText != 'undefined') { + value = value.cssText; // we're dealing with IE, need to get CSS out + } + } + // filter out invalid css styles + if(attribute == 'style') { + css = []; + cssAttrs = value.split(';'); + for(j = 0; j < cssAttrs.length; j++) { + attr = cssAttrs[j].split(':'); + cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase(); + if(Strophe.XHTML.validCSS(cssName)) { + cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, ""); + css.push(cssName + ': ' + cssValue); + } + } + if(css.length > 0) { + value = css.join('; '); + el.setAttribute(attribute, value); + } + } else { + el.setAttribute(attribute, value); + } + } + + for (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 (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 (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); } @@ -1142,6 +1511,7 @@ Strophe = { "='" + elem.attributes[i].value .replace(/&/g, "&") .replace(/\'/g, "'") + .replace(/>/g, ">") .replace(/</g, "<") + "'"; } } @@ -1150,12 +1520,18 @@ Strophe = { result += ">"; for (i = 0; i < elem.childNodes.length; i++) { child = elem.childNodes[i]; - if (child.nodeType == Strophe.ElementType.NORMAL) { + switch( child.nodeType ){ + case Strophe.ElementType.NORMAL: // normal element, so recurse result += Strophe.serialize(child); - } else if (child.nodeType == Strophe.ElementType.TEXT) { - // text element - result += child.nodeValue; + 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 += "<![CDATA["+child.nodeValue+"]]>"; } } result += "</" + nodeName + ">"; @@ -1181,7 +1557,7 @@ Strophe = { /** Function: addConnectionPlugin * Extends the Strophe.Connection object with the given plugin. * - * Paramaters: + * Parameters: * (String) name - The name of the extension. * (Object) ptype - The plugin's prototype. */ @@ -1322,22 +1698,25 @@ Strophe.Builder.prototype = { * Add a child to the current element and make it the new current * element. * - * This function moves the current element pointer to the child. If you - * need to add another child, it is necessary to use up() to go back - * to the parent in the tree. + * 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) + c: function (name, attrs, text) { - var child = Strophe.xmlElement(name, attrs); + var child = Strophe.xmlElement(name, attrs, text); this.node.appendChild(child); - this.node = child; + if (!text) { + this.node = child; + } return this; }, @@ -1358,7 +1737,15 @@ Strophe.Builder.prototype = { cnode: function (elem) { var xmlGen = Strophe.xmlGenerator(); - var newElem = xmlGen.importNode ? xmlGen.importNode(elem, true) : Strophe.copyElement(elem); + try { + var impNode = (xmlGen.importNode !== undefined); + } + catch (e) { + var impNode = false; + } + var newElem = impNode ? + xmlGen.importNode(elem, true) : + Strophe.copyElement(elem); this.node.appendChild(newElem); this.node = newElem; return this; @@ -1381,10 +1768,36 @@ Strophe.Builder.prototype = { var 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) + { + var 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 + var 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. * @@ -1422,7 +1835,7 @@ Strophe.Handler = function (handler, ns, name, type, id, from, options) this.type = type; this.id = id; this.options = options || {matchbare: false}; - + // default matchBare to false if undefined if (!this.options.matchBare) { this.options.matchBare = false; @@ -1452,7 +1865,7 @@ Strophe.Handler.prototype = { { var nsMatch; var from = null; - + if (this.options.matchBare) { from = Strophe.getBareJidFromJid(elem.getAttribute('from')); } else { @@ -1513,7 +1926,7 @@ Strophe.Handler.prototype = { e.fileName + ":" + e.lineNumber + " - " + e.name + ": " + e.message); } else { - Strophe.fatal("error: " + this.handler); + Strophe.fatal("error: " + e.message + "\n" + e.stack); } throw e; @@ -1714,7 +2127,7 @@ Strophe.Request.prototype = { /** Class: Strophe.Connection * XMPP Connection manager. * - * Thie class is the main part of Strophe. It manages a BOSH connection + * This class is the main part of Strophe. It manages a BOSH connection * to an XMPP server and dispatches events to the user callbacks as * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy * authentication. @@ -1748,6 +2161,8 @@ Strophe.Connection = function (service) this.service = service; /* The connected JID. */ this.jid = ""; + /* the JIDs domain */ + this.domain = null; /* request id for body tags */ this.rid = Math.floor(Math.random() * 4294967295); /* The current session ID. */ @@ -1757,6 +2172,7 @@ Strophe.Connection = function (service) this.features = null; // SASL + this._sasl_data = []; this.do_session = false; this.do_bind = false; @@ -1768,9 +2184,11 @@ Strophe.Connection = function (service) this.addTimeds = []; this.addHandlers = []; + this._authentication = {}; this._idleTimeout = null; this._disconnectTimeout = null; + this.do_authentication = true; this.authenticated = false; this.disconnecting = false; this.connected = false; @@ -1792,6 +2210,9 @@ Strophe.Connection = function (service) this._sasl_failure_handler = null; this._sasl_challenge_handler = null; + // Max retries before disconnecting + this.maxRetries = 5; + // setup onIdle callback every 1/10th of a second this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); @@ -1833,6 +2254,7 @@ Strophe.Connection.prototype = { this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; + this._authentication = {}; this.authenticated = false; this.disconnecting = false; @@ -1917,7 +2339,7 @@ Strophe.Connection.prototype = { * or a full JID. If a node is not supplied, SASL ANONYMOUS * authentication will be attempted. * (String) pass - The user's password. - * (Function) callback The connect callback function. + * (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. @@ -1925,8 +2347,9 @@ Strophe.Connection.prototype = { * (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 */ - connect: function (jid, pass, callback, wait, hold) + connect: function (jid, pass, callback, wait, hold, route) { this.jid = jid; this.pass = pass; @@ -1940,7 +2363,7 @@ Strophe.Connection.prototype = { this.hold = hold || this.hold; // parse jid for domain and resource - this.domain = Strophe.getDomainFromJid(this.jid); + this.domain = this.domain || Strophe.getDomainFromJid(this.jid); // build the body tag var body = this._buildBody().attrs({ @@ -1954,12 +2377,21 @@ Strophe.Connection.prototype = { "xmlns:xmpp": Strophe.NS.BOSH }); + if(route){ + body.attrs({ + route: route + }); + } + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + var _connect_cb = this._connect_callback || this._connect_cb; + this._connect_callback = null; + this._requests.push( new Strophe.Request(body.tree(), this._onRequestStateChange.bind( - this, this._connect_cb.bind(this)), + this, _connect_cb.bind(this)), body.tree().getAttribute("rid"))); this._throttledRequestHandler(); }, @@ -2205,7 +2637,7 @@ Strophe.Connection.prototype = { message: "Cannot queue non-DOMElement." }; } - + this._data.push(element); }, @@ -2480,7 +2912,7 @@ Strophe.Connection.prototype = { } // make sure we limit the number of retries - if (req.sends > 5) { + if (req.sends > this.maxRetries) { this._onDisconnectTimeout(); return; } @@ -2515,7 +2947,6 @@ Strophe.Connection.prototype = { Strophe.debug("request id " + req.id + "." + req.sends + " posting"); - req.date = new Date(); try { req.xhr.open("POST", this.service, true); } catch (e2) { @@ -2531,15 +2962,17 @@ Strophe.Connection.prototype = { // Fires the XHR request -- may be invoked immediately // or on a gradually expanding retry window for reconnects var sendFunc = function () { + req.date = new Date(); req.xhr.send(req.data); }; // Implement progressive backoff for reconnects -- // First retry (send == 1) should also be instantaneous if (req.sends > 1) { - // Using a cube of the retry number creats a nicely + // Using a cube of the retry number creates a nicely // expanding retry window - var backoff = Math.pow(req.sends, 3) * 1000; + var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), + Math.pow(req.sends, 3)) * 1000; setTimeout(sendFunc, backoff); } else { sendFunc(); @@ -2547,8 +2980,12 @@ Strophe.Connection.prototype = { req.sends++; - this.xmlOutput(req.xmlData); - this.rawOutput(req.data); + if (this.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { + this.xmlOutput(req.xmlData); + } + if (this.rawOutput !== Strophe.Connection.prototype.rawOutput) { + this.rawOutput(req.data); + } } else { Strophe.debug("_processRequest: " + (i === 0 ? "first" : "second") + @@ -2757,8 +3194,12 @@ Strophe.Connection.prototype = { } if (elem === null) { return; } - this.xmlInput(elem); - this.rawInput(Strophe.serialize(elem)); + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + this.xmlInput(elem); + } + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + this.rawInput(Strophe.serialize(elem)); + } // remove handlers scheduled for deletion var i, hand; @@ -2815,13 +3256,19 @@ Strophe.Connection.prototype = { that.handlers = []; for (i = 0; i < newList.length; i++) { var hand = newList[i]; - if (hand.isMatch(child) && - (that.authenticated || !hand.user)) { - if (hand.run(child)) { + // encapsulate 'handler.run' not to lose the whole handler list if + // one of the handlers throws an exception + try { + if (hand.isMatch(child) && + (that.authenticated || !hand.user)) { + if (hand.run(child)) { + that.handlers.push(hand); + } + } else { that.handlers.push(hand); } - } else { - that.handlers.push(hand); + } catch(e) { + //if the handler throws an exception, we consider it as false } } }); @@ -2869,8 +3316,11 @@ Strophe.Connection.prototype = { * * 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 their) + * want to do something special). */ - _connect_cb: function (req) + _connect_cb: function (req, _callback) { Strophe.info("_connect_cb was called"); @@ -2878,8 +3328,12 @@ Strophe.Connection.prototype = { var bodyWrap = req.getResponse(); if (!bodyWrap) { return; } - this.xmlInput(bodyWrap); - this.rawInput(Strophe.serialize(bodyWrap)); + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + this.xmlInput(bodyWrap); + } + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + this.rawInput(Strophe.serialize(bodyWrap)); + } var typ = bodyWrap.getAttribute("type"); var cond, conflict; @@ -2913,39 +3367,74 @@ Strophe.Connection.prototype = { var wait = bodyWrap.getAttribute('wait'); if (wait) { this.wait = parseInt(wait, 10); } + this._authentication.sasl_scram_sha1 = false; + this._authentication.sasl_plain = false; + this._authentication.sasl_digest_md5 = false; + this._authentication.sasl_anonymous = false; + this._authentication.legacy_auth = false; - var do_sasl_plain = false; - var do_sasl_digest_md5 = false; - var do_sasl_anonymous = false; + // Check for the stream:features tag + var hasFeatures = bodyWrap.getElementsByTagName("stream:features").length > 0; + if (!hasFeatures) { + hasFeatures = bodyWrap.getElementsByTagName("features").length > 0; + } var mechanisms = bodyWrap.getElementsByTagName("mechanism"); - var i, mech, auth_str, hashed_auth_str; - if (mechanisms.length > 0) { + var i, mech, auth_str, hashed_auth_str, + found_authentication = false; + if (hasFeatures && mechanisms.length > 0) { + var missmatchedmechs = 0; for (i = 0; i < mechanisms.length; i++) { mech = Strophe.getText(mechanisms[i]); - if (mech == 'DIGEST-MD5') { - do_sasl_digest_md5 = true; + if (mech == 'SCRAM-SHA-1') { + this._authentication.sasl_scram_sha1 = true; + } else if (mech == 'DIGEST-MD5') { + this._authentication.sasl_digest_md5 = true; } else if (mech == 'PLAIN') { - do_sasl_plain = true; + this._authentication.sasl_plain = true; } else if (mech == 'ANONYMOUS') { - do_sasl_anonymous = true; - } + this._authentication.sasl_anonymous = true; + } else missmatchedmechs++; } - } else { + + this._authentication.legacy_auth = + bodyWrap.getElementsByTagName("auth").length > 0; + + found_authentication = + this._authentication.legacy_auth || + missmatchedmechs < mechanisms.length; + } + if (!found_authentication) { + _callback = _callback || this._connect_cb; // we didn't get stream:features yet, so we need wait for it // by sending a blank poll request var body = this._buildBody(); this._requests.push( new Strophe.Request(body.tree(), this._onRequestStateChange.bind( - this, this._connect_cb.bind(this)), + this, _callback.bind(this)), body.tree().getAttribute("rid"))); this._throttledRequestHandler(); return; } + if (this.do_authentication !== false) + this.authenticate(); + }, + /** Function: authenticate + * Set up authentication + * + * Contiunues the initial connection request by setting up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + */ + authenticate: function () + { if (Strophe.getNodeFromJid(this.jid) === null && - do_sasl_anonymous) { + this._authentication.sasl_anonymous) { this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); this._sasl_success_handler = this._addSysHandler( this._sasl_success_cb.bind(this), null, @@ -2964,10 +3453,34 @@ Strophe.Connection.prototype = { this._changeConnectStatus(Strophe.Status.CONNFAIL, 'x-strophe-bad-non-anon-jid'); this.disconnect(); - } else if (do_sasl_digest_md5) { + } else if (this._authentication.sasl_scram_sha1) { + var cnonce = MD5.hexdigest(Math.random() * 1234567890); + + var auth_str = "n=" + Strophe.getNodeFromJid(this.jid); + auth_str += ",r="; + auth_str += cnonce; + + this._sasl_data["cnonce"] = cnonce; + this._sasl_data["client-first-message-bare"] = auth_str; + + auth_str = "n,," + auth_str; + + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_scram_challenge_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "SCRAM-SHA-1" + }).t(Base64.encode(auth_str)).tree()); + } else if (this._authentication.sasl_digest_md5) { this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); this._sasl_challenge_handler = this._addSysHandler( - this._sasl_challenge1_cb.bind(this), null, + this._sasl_digest_challenge1_cb.bind(this), null, "challenge", null, null); this._sasl_failure_handler = this._addSysHandler( this._sasl_failure_cb.bind(this), null, @@ -2977,7 +3490,7 @@ Strophe.Connection.prototype = { xmlns: Strophe.NS.SASL, mechanism: "DIGEST-MD5" }).tree()); - } else if (do_sasl_plain) { + } else if (this._authentication.sasl_plain) { // Build the plain auth string (barejid null // username null password) and base 64 encoded. auth_str = Strophe.getBareJidFromJid(this.jid); @@ -3014,7 +3527,7 @@ Strophe.Connection.prototype = { } }, - /** PrivateFunction: _sasl_challenge1_cb + /** PrivateFunction: _sasl_digest_challenge1_cb * _Private_ handler for DIGEST-MD5 SASL authentication. * * Parameters: @@ -3023,12 +3536,12 @@ Strophe.Connection.prototype = { * Returns: * false to remove the handler. */ - _sasl_challenge1_cb: function (elem) + _sasl_digest_challenge1_cb: function (elem) { var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; var challenge = Base64.decode(Strophe.getText(elem)); - var cnonce = MD5.hexdigest(Math.random() * 1234567890); + var cnonce = MD5.hexdigest("" + (Math.random() * 1234567890)); var realm = ""; var host = null; var nonce = ""; @@ -3085,7 +3598,7 @@ Strophe.Connection.prototype = { responseText += 'charset="utf-8"'; this._sasl_challenge_handler = this._addSysHandler( - this._sasl_challenge2_cb.bind(this), null, + this._sasl_digest_challenge2_cb.bind(this), null, "challenge", null, null); this._sasl_success_handler = this._addSysHandler( this._sasl_success_cb.bind(this), null, @@ -3117,7 +3630,7 @@ Strophe.Connection.prototype = { }, - /** PrivateFunction: _sasl_challenge2_cb + /** PrivateFunction: _sasl_digest_challenge2_cb * _Private_ handler for second step of DIGEST-MD5 SASL authentication. * * Parameters: @@ -3126,7 +3639,7 @@ Strophe.Connection.prototype = { * Returns: * false to remove the handler. */ - _sasl_challenge2_cb: function (elem) + _sasl_digest_challenge2_cb: function (elem) { // remove unneeded handlers this.deleteHandler(this._sasl_success_handler); @@ -3142,6 +3655,91 @@ Strophe.Connection.prototype = { return false; }, + /** PrivateFunction: _sasl_scram_challenge_cb + * _Private_ handler for SCRAM-SHA-1 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_scram_challenge_cb: function (elem) + { + var nonce, salt, iter, Hi, U, U_old; + var clientKey, serverKey, clientSignature; + var responseText = "c=biws,"; + var challenge = Base64.decode(Strophe.getText(elem)); + var authMessage = this._sasl_data["client-first-message-bare"] + "," + + challenge + ","; + var cnonce = this._sasl_data["cnonce"] + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + 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)) { + this._sasl_data = []; + return this._sasl_failure_cb(null); + } + + responseText += "r=" + nonce; + authMessage += responseText; + + salt = Base64.decode(salt); + salt += "\0\0\0\1"; + + Hi = U_old = core_hmac_sha1(this.pass, salt); + for (i = 1; i < iter; i++) { + U = core_hmac_sha1(this.pass, binb2str(U_old)); + for (k = 0; k < 5; k++) { + Hi[k] ^= U[k]; + } + U_old = U; + } + Hi = binb2str(Hi); + + clientKey = core_hmac_sha1(Hi, "Client Key"); + serverKey = str_hmac_sha1(Hi, "Server Key"); + clientSignature = core_hmac_sha1(str_sha1(binb2str(clientKey)), authMessage); + this._sasl_data["server-signature"] = b64_hmac_sha1(serverKey, authMessage); + + for (k = 0; k < 5; k++) { + clientKey[k] ^= clientSignature[k]; + } + + responseText += ",p=" + Base64.encode(binb2str(clientKey)); + + 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.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(Base64.encode(responseText)).tree()); + + return false; + }, + /** PrivateFunction: _auth1_cb * _Private_ handler for legacy authentication. * @@ -3192,7 +3790,29 @@ Strophe.Connection.prototype = { */ _sasl_success_cb: function (elem) { - Strophe.info("SASL authentication succeeded."); + if (this._sasl_data["server-signature"]) { + var serverSignature; + var success = Base64.decode(Strophe.getText(elem)); + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + 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."); // remove old handlers this.deleteHandler(this._sasl_failure_handler); @@ -3223,7 +3843,7 @@ Strophe.Connection.prototype = { _sasl_auth1_cb: function (elem) { // save stream:features for future usage - this.features = elem + this.features = elem; var i, child; @@ -3273,7 +3893,11 @@ Strophe.Connection.prototype = { { if (elem.getAttribute("type") == "error") { Strophe.info("SASL binding failed."); - this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + var conflict = elem.getElementsByTagName("conflict"), condition; + if (conflict.length > 0) { + condition = 'conflict'; + } + this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition); return false; } @@ -3551,9 +4175,12 @@ Strophe.Connection.prototype = { } } - // reactivate the timer clearTimeout(this._idleTimeout); - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // reactivate the timer only if connected + if (this.connected) { + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } } }; |
