diff options
author | sualko <github@spam.herberth.eu> | 2014-01-28 17:57:12 +0400 |
---|---|---|
committer | sualko <github@spam.herberth.eu> | 2014-01-28 17:57:12 +0400 |
commit | 7735202a0a1e4fb321e3fe01de9db16b5af6ce7e (patch) | |
tree | 1998a7949151832dbaa10202005ef6db9e12b13f | |
parent | f071f149675253ce6a1286cd01a781a7108a097b (diff) |
bump version
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | app.json | 2 | ||||
-rwxr-xr-x | appinfo/info.xml | 2 | ||||
-rwxr-xr-x | appinfo/version | 2 | ||||
-rw-r--r-- | build/appinfo/info.xml | 2 | ||||
-rw-r--r-- | build/appinfo/version | 2 | ||||
-rw-r--r-- | build/css/jsxc.oc.css | 8 | ||||
-rw-r--r-- | build/js/admin.js | 4 | ||||
-rw-r--r-- | build/js/eof.js | 4 | ||||
-rw-r--r-- | build/js/jsxc/jsxc.lib.js | 162 | ||||
-rw-r--r-- | build/js/jsxc/jsxc.lib.webrtc.js | 4 | ||||
-rw-r--r-- | build/js/jsxc/lib/strophe.jingle/strophe.jingle.adapter.js | 12 | ||||
-rw-r--r-- | build/js/jsxc/lib/strophe.jingle/strophe.jingle.js | 2 | ||||
-rw-r--r-- | build/js/jsxc/lib/strophe.jingle/strophe.jingle.sdp.js | 6 | ||||
-rw-r--r-- | build/js/jsxc/lib/strophe.jingle/strophe.jingle.session.js | 6 | ||||
-rw-r--r-- | build/js/jsxc/lib/strophe.js | 3377 | ||||
-rw-r--r-- | build/js/ojsxc.js | 13 | ||||
-rw-r--r-- | css/jsxc.oc.css | 4 | ||||
m--------- | js/jsxc | 0 | ||||
-rw-r--r-- | js/ojsxc.js | 9 |
20 files changed, 2324 insertions, 1301 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fadaf2..2dcbc5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.5.2 / 2014-01-28 +=== +- upgrade jsxc to v0.5.2 + v0.5.1 / 2014-01-27 === - downgrade required oc version @@ -1,6 +1,6 @@ { "name": "ojsxc", - "version": "0.5.1", + "version": "0.5.2", "description": "Real-time chat app for owncloud", "homepage": "http://jsxc.org/", "license": "MIT", diff --git a/appinfo/info.xml b/appinfo/info.xml index 8cd672e..5d88b87 100755 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -3,7 +3,7 @@ <id>ojsxc</id> <name>JavaScript XMPP Chat</name> <description>XMPP Chat with OTR</description> - <version>0.5.1</version> + <version>0.5.2</version> <licence>MIT</licence> <author>Klaus Herberth</author> <require>5</require> diff --git a/appinfo/version b/appinfo/version index 5d4294b..2411653 100755 --- a/appinfo/version +++ b/appinfo/version @@ -1 +1 @@ -0.5.1
\ No newline at end of file +0.5.2
\ No newline at end of file diff --git a/build/appinfo/info.xml b/build/appinfo/info.xml index 8cd672e..5d88b87 100644 --- a/build/appinfo/info.xml +++ b/build/appinfo/info.xml @@ -3,7 +3,7 @@ <id>ojsxc</id> <name>JavaScript XMPP Chat</name> <description>XMPP Chat with OTR</description> - <version>0.5.1</version> + <version>0.5.2</version> <licence>MIT</licence> <author>Klaus Herberth</author> <require>5</require> diff --git a/build/appinfo/version b/build/appinfo/version index 5d4294b..2411653 100644 --- a/build/appinfo/version +++ b/build/appinfo/version @@ -1 +1 @@ -0.5.1
\ No newline at end of file +0.5.2
\ No newline at end of file diff --git a/build/css/jsxc.oc.css b/build/css/jsxc.oc.css index 9913917..609a5ab 100644 --- a/build/css/jsxc.oc.css +++ b/build/css/jsxc.oc.css @@ -1,5 +1,5 @@ /** - * ojsxc v0.5.1 - 2014-01-27 + * ojsxc v0.5.2 - 2014-01-28 * * Copyright (c) 2014 Klaus Herberth <klaus@jsxc.org> <br> * Released under the MIT license @@ -7,7 +7,7 @@ * Please see http://jsxc.org/ * * @author Klaus Herberth <klaus@jsxc.org> - * @version 0.5.1 + * @version 0.5.2 */ .jsxc_online { @@ -595,4 +595,8 @@ div.jsxc_transfer.jsxc_enc.jsxc_trust { background-image: url('%appswebroot%/ojsxc/img/fail-icon.png'); color: #800000; border-color: #800000; +} + +.jsxc_log{ + width: 500px; }
\ No newline at end of file diff --git a/build/js/admin.js b/build/js/admin.js index b159a20..6ec423d 100644 --- a/build/js/admin.js +++ b/build/js/admin.js @@ -1,5 +1,5 @@ /** - * ojsxc v0.5.1 - 2014-01-27 + * ojsxc v0.5.2 - 2014-01-28 * * Copyright (c) 2014 Klaus Herberth <klaus@jsxc.org> <br> * Released under the MIT license @@ -7,7 +7,7 @@ * Please see http://jsxc.org/ * * @author Klaus Herberth <klaus@jsxc.org> - * @version 0.5.1 + * @version 0.5.2 */ $(document).ready(function() { diff --git a/build/js/eof.js b/build/js/eof.js index d833831..9a6cee6 100644 --- a/build/js/eof.js +++ b/build/js/eof.js @@ -1,5 +1,5 @@ /** - * ojsxc v0.5.1 - 2014-01-27 + * ojsxc v0.5.2 - 2014-01-28 * * Copyright (c) 2014 Klaus Herberth <klaus@jsxc.org> <br> * Released under the MIT license @@ -7,7 +7,7 @@ * Please see http://jsxc.org/ * * @author Klaus Herberth <klaus@jsxc.org> - * @version 0.5.1 + * @version 0.5.2 */ /** diff --git a/build/js/jsxc/jsxc.lib.js b/build/js/jsxc/jsxc.lib.js index 0b8185a..ad9b6af 100644 --- a/build/js/jsxc/jsxc.lib.js +++ b/build/js/jsxc/jsxc.lib.js @@ -1,5 +1,5 @@ /** - * jsxc v0.5.1 - 2014-01-27 + * jsxc v0.5.2 - 2014-01-28 * * Copyright (c) 2014 Klaus Herberth <klaus@jsxc.org> <br> * Released under the MIT license @@ -7,7 +7,7 @@ * Please see http://jsxc.org/ * * @author Klaus Herberth <klaus@jsxc.org> - * @version 0.5.1 + * @version 0.5.2 */ var jsxc; @@ -22,7 +22,7 @@ var jsxc; */ jsxc = { /** Version of jsxc */ - version: '0.5.1', + version: '0.5.2', /** True if i'm the chief */ chief: false, @@ -81,9 +81,6 @@ var jsxc; /** My css id */ cid: null, - /** Shortcut for jsxc.options.debug */ - debug: null, - /** Some constants */ CONST: { NOTIFICATION_DEFAULT: 'default', @@ -93,6 +90,53 @@ var jsxc; }, /** + * Write debug message to console and to log. + * + * @memberOf jsxc + * @param {String} msg Debug message + * @param {Object} data + * @param {String} Could be warn|error|null + */ + debug: function(msg, data, level) { + if (level) { + msg = '[' + level + '] ' + msg; + } + + if (data) { + console.log(msg, data); + jsxc.log = jsxc.log + msg + ': ' + $("<span>").prepend($(data).clone()).html() + '\n'; + } else { + console.log(msg); + jsxc.log = jsxc.log + msg + '\n'; + } + }, + + /** + * Write warn message. + * + * @memberOf jsxc + * @param {String} msg Warn message + * @param {Object} data + */ + warn: function(msg, data) { + jsxc.debug(msg, data, 'WARN'); + }, + + /** + * Write error message. + * + * @memberOf jsxc + * @param {String} msg Error message + * @param {Object} data + */ + error: function(msg, data) { + jsxc.debug(msg, data, 'ERROR'); + }, + + /** debug log */ + log: '', + + /** * Starts the action * * @memberOf jsxc @@ -114,9 +158,6 @@ var jsxc; jsxc.storage.updateUserItem('options', key, value); }; - // Shortcut - jsxc.debug = jsxc.options.debug; - jsxc.storageNotConform = jsxc.storage.getItem('storageNotConform') || 2; // detect language @@ -174,7 +215,9 @@ var jsxc; // create jquery object var form = jsxc.options.loginForm.form = $(jsxc.options.loginForm.form); - var events = form.data('events') || {submit: []}; + var events = form.data('events') || { + submit: [] + }; var submits = []; // save attached submit events and remove them. Will be reattached @@ -182,7 +225,7 @@ var jsxc; $.each(events.submit, function(index, val) { submits.push(val.handler); }); - + form.data('submits', submits); form.off('submit'); @@ -455,12 +498,12 @@ var jsxc; submitLoginForm: function() { var form = jsxc.options.loginForm.form.off('submit'); - //Attach original events + // Attach original events var submits = form.data('submits') || []; $.each(submits, function(index, val) { form.submit(val); - }); - + }); + if (form.find('#submit')) { form.find('#submit').click(); } else { @@ -526,7 +569,7 @@ var jsxc; var k = key.replace(/ /gi, '_'); if (!jsxc.l[k]) { - jsxc.debug('[WARN] No translation for: ' + k); + jsxc.warn('No translation for: ' + k); } return jsxc.l[k] || key.replace(/_/gi, ' '); @@ -672,6 +715,7 @@ var jsxc; var ri = $('#' + cid); // roster item from user var we = jsxc.gui.getWindow(cid); // window element from user var ue = $('#' + cid + ', #jsxc_window_' + cid + ', .jsxc_buddy_' + cid); // both + var bullet = $('.jsxc_buddy_' + cid); // Attach data to corresponding roster item ri.data(data); @@ -681,6 +725,7 @@ var jsxc; // Change name and add title ue.find('.jsxc_name').text(data.name).attr('title', 'is ' + jsxc.CONST.STATUS[data.status]); + bullet.attr('title', 'is ' + jsxc.CONST.STATUS[data.status]); // Update gui according to encryption state switch (data.msgstate) { @@ -738,7 +783,7 @@ var jsxc; jsxc.storage.setUserItem('avatar_' + data.avatar, src); setAvatar(src); }, Strophe.getBareJidFromJid(data.jid), function(msg) { - jsxc.debug('Error', msg); + jsxc.error('Could not load vcard.', msg); }); } } @@ -1083,6 +1128,37 @@ var jsxc; */ showAboutDialog: function() { jsxc.gui.dialog.open(jsxc.gui.template.get('aboutDialog')); + + $('#jsxc_dialog .jsxc_debuglog').click(function() { + jsxc.gui.showDebugLog(); + }); + }, + + /** + * Show debug log. + * + * @memberOf jsxc.gui + */ + showDebugLog: function() { + var userInfo = '<h3>User information</h3>'; + + if (navigator) { + var key; + for (key in navigator) { + if (navigator.hasOwnProperty(key) && typeof navigator[key] === 'string') { + userInfo += '<b>' + key + ':</b> ' + navigator[key] + '<br />'; + } + } + } + + if (window.screen) { + userInfo += '<b>Height:</b> ' + window.screen.height + '<br />'; + userInfo += '<b>Width:</b> ' + window.screen.width + '<br />'; + } + + userInfo += '<b>jsxc version:</b> ' + jsxc.version + '<br />'; + + jsxc.gui.dialog.open('<div class="jsxc_log">'+userInfo+'<h3>Log</h3><pre>' + jsxc.escapeHTML(jsxc.log) + '</pre></div>'); } }; @@ -1156,7 +1232,7 @@ var jsxc; * * @param {String} cid CSS compatible jid */ - add: function(cid) { + add: function(cid) { var data = jsxc.storage.getUserItem('buddy_' + cid); var bud = jsxc.gui.buddyTemplate.clone().attr('id', cid).attr('data-type', data.type || 'chat'); @@ -1577,9 +1653,9 @@ var jsxc; * @param {String} cid CSS compatible jid */ close: function(cid) { - + if (!jsxc.el_exists('#jsxc_window_' + cid)) { - jsxc.debug('[Warning] Want to close a window, that is not open.'); + jsxc.warn('Want to close a window, that is not open.'); return; } @@ -1653,7 +1729,7 @@ var jsxc; * * @param {String} cid */ - hide: function(cid) { + hide: function(cid) { jsxc.storage.updateUserItem('window_' + cid, 'minimize', true); jsxc.gui.window._hide(cid); @@ -1664,7 +1740,7 @@ var jsxc; * * @param {String} cid */ - _hide: function(cid) { + _hide: function(cid) { $('#jsxc_window_' + cid + ' .jsxc_window').slideUp(); jsxc.gui.getWindow(cid).trigger('hidden.window.jsxc'); }, @@ -2019,7 +2095,8 @@ var jsxc; <br />\ Real-time chat app for OwnCloud. This app requires external<br /> XMPP server (openfire, ejabberd etc.).<br />\ <br />\ - <i>Released under the MIT license</i></p>' + <i>Released under the MIT license</i></p>\ + <p class="jsxc_right"><a class="button jsxc_debuglog" href="#">Show debug log</a></p>' }; /** @@ -2051,16 +2128,16 @@ var jsxc; // Create new connection (no login) jsxc.xmpp.conn = new Strophe.Connection(url); - // jsxc.xmpp.conn.xmlInput = function(data) { - // jsxc.debug('<', data); - // }; - // jsxc.xmpp.conn.xmlOutput = function(data) { - // jsxc.debug('>', data); - // }; - // - // Strophe.log = function (level, msg) { - // jsxc.debug(level + " " + msg); - // }; +// jsxc.xmpp.conn.xmlInput = function(data) { +// console.log('<', data); +// }; +// jsxc.xmpp.conn.xmlOutput = function(data) { +// console.log('>', data); +// }; + +// Strophe.log = function (level, msg) { +// console.log(level + " " + msg); +// }; var callback = function(status, condition) { @@ -2071,7 +2148,7 @@ var jsxc; jsxc.cid = jsxc.jidToCid(jsxc.xmpp.conn.jid.toLowerCase()); $(document).trigger('connected.jsxc'); break; - case Strophe.Status.ATTACHED: + case Strophe.Status.ATTACHED: $(document).trigger('attached.jsxc'); break; case Strophe.Status.DISCONNECTED: @@ -2142,7 +2219,7 @@ var jsxc; jsxc.xmpp.conn.pause(); // Save sid and jid - jsxc.storage.setItem('sid', jsxc.xmpp.conn.sid); + jsxc.storage.setItem('sid', jsxc.xmpp.conn._proto.sid); jsxc.storage.setItem('jid', jsxc.xmpp.conn.jid.toLowerCase()); jsxc.storage.setItem('lastActivity', (new Date()).getTime()); @@ -2191,7 +2268,7 @@ var jsxc; }).c('query', { xmlns: 'jabber:iq:roster' }); - + jsxc.xmpp.conn.sendIQ(iq, jsxc.xmpp.onRoster); } else { jsxc.xmpp.sendPres(); @@ -2226,6 +2303,7 @@ var jsxc; pres.c('c', jsxc.xmpp.conn.caps.generateCapsAttrs()); } + jsxc.debug('Send presence', pres.toString()); jsxc.xmpp.conn.send(pres); }, @@ -2392,7 +2470,7 @@ var jsxc; * @param {dom} presence * @private */ - onPresence: function(presence) { + onPresence: function(presence) { /* * <presence xmlns='jabber:client' type='unavailable' from='' to=''/> * @@ -2407,6 +2485,8 @@ var jsxc; * node='http://psi-im.org/caps' ver='caps-b75d8d2b25' ext='ca cs * ep-notify-2 html'/> </presence> */ + jsxc.debug('onPresence', presence); + var ptype = $(presence).attr('type'); var from = $(presence).attr('from'); var jid = Strophe.getBareJidFromJid(from).toLowerCase(); @@ -2418,14 +2498,12 @@ var jsxc; var status = null; var xVCard = $(presence).find('x[xmlns="vcard-temp:x:update"]'); - jsxc.debug('onPresence', presence); - if (jid === to) { return true; } if (ptype === 'error') { - jsxc.debug('[XMPP ERROR] ' + $(presence).attr('code')); + jsxc.error('[XMPP] ' + $(presence).attr('code')); return true; } @@ -2456,7 +2534,7 @@ var jsxc; } var maxVal = []; - var max = 0, prop; + var max = 0, prop = null; for (prop in res) { if (res.hasOwnProperty(prop)) { if (max <= res[prop]) { @@ -2515,7 +2593,7 @@ var jsxc; * <body>...</body> <active * xmlns='http://jabber.org/protocol/chatstates'/> </message> */ - + jsxc.debug('Incoming message', message); var type = $(message).attr('type'); @@ -3205,7 +3283,7 @@ var jsxc; }); jsxc.buddyList[cid].on('error', function(err) { - jsxc.debug('[OTR] ' + err); + jsxc.error('[OTR] ', err); jsxc.gui.window.postMessage(cid, 'sys', '[OTR] ' + err); }); diff --git a/build/js/jsxc/jsxc.lib.webrtc.js b/build/js/jsxc/jsxc.lib.webrtc.js index d0bbe28..97bb2ef 100644 --- a/build/js/jsxc/jsxc.lib.webrtc.js +++ b/build/js/jsxc/jsxc.lib.webrtc.js @@ -1,5 +1,5 @@ /** - * jsxc v0.5.1 - 2014-01-27 + * jsxc v0.5.2 - 2014-01-28 * * Copyright (c) 2014 Klaus Herberth <klaus@jsxc.org> <br> * Released under the MIT license @@ -7,7 +7,7 @@ * Please see http://jsxc.org/ * * @author Klaus Herberth <klaus@jsxc.org> - * @version 0.5.1 + * @version 0.5.2 */ /* jsxc, Strophe, SDPUtil, getUserMediaWithConstraints, setupRTC, jQuery */ diff --git a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.adapter.js b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.adapter.js index 23984de..f10fcdf 100644 --- a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.adapter.js +++ b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.adapter.js @@ -1,5 +1,8 @@ /* jshint -W117 */ -function TraceablePeerConnection(ice_config, constraints) { +var setupRTC, getUserMediaWithConstraints, TraceablePeerConnection; + +(function($){ +TraceablePeerConnection = function(ice_config, constraints) { var self = this; var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection; this.peerconnection = new RTCPeerconnection(ice_config, constraints); @@ -176,7 +179,7 @@ TraceablePeerConnection.prototype.getStats = function(callback) { // mozilla chrome compat layer -- very similar to adapter.js -function setupRTC() { +setupRTC = function (){ var RTC = null; if (navigator.mozGetUserMedia) { console.log('This appears to be Firefox'); @@ -229,9 +232,9 @@ function setupRTC() { try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { } } return RTC; -} +}; -function getUserMediaWithConstraints(um, resolution, bandwidth, fps) { +getUserMediaWithConstraints = function(um, resolution, bandwidth, fps) { var constraints = {audio: false, video: false}; if (um.indexOf('video') >= 0) { @@ -324,3 +327,4 @@ function getUserMediaWithConstraints(um, resolution, bandwidth, fps) { $(document).trigger('mediafailure.jingle'); } } +}(jQuery));
\ No newline at end of file diff --git a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.js b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.js index 4417f10..f1fff5a 100644 --- a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.js +++ b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.js @@ -1,4 +1,5 @@ /* jshint -W117 */ +(function($){ Strophe.addConnectionPlugin('jingle', { connection: null, sessions: {}, @@ -257,3 +258,4 @@ Strophe.addConnectionPlugin('jingle', { // implement push? } }); +}(jQuery)); diff --git a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.sdp.js b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.sdp.js index 3ec230c..7cdcd97 100644 --- a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.sdp.js +++ b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.sdp.js @@ -1,6 +1,9 @@ /* jshint -W117 */ +var SDP; + +(function($){ // SDP STUFF -function SDP(sdp) { +SDP = function(sdp) { this.media = sdp.split('\r\nm='); for (var i = 1; i < this.media.length; i++) { this.media[i] = 'm=' + this.media[i]; @@ -807,3 +810,4 @@ SDPUtil = { return line + '\r\n'; } }; +}(jQuery));
\ No newline at end of file diff --git a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.session.js b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.session.js index d5be0b0..e1cfa0b 100644 --- a/build/js/jsxc/lib/strophe.jingle/strophe.jingle.session.js +++ b/build/js/jsxc/lib/strophe.jingle/strophe.jingle.session.js @@ -1,6 +1,9 @@ /* jshint -W117 */ // Jingle stuff -function JingleSession(me, sid, connection) { +var JingleSession; + +(function($){ +JingleSession = function(me, sid, connection) { this.me = me; this.sid = sid; this.connection = connection; @@ -853,3 +856,4 @@ JingleSession.prototype.getStats = function (interval) { return this.statsinterval; }; +}(jQuery)); diff --git a/build/js/jsxc/lib/strophe.js b/build/js/jsxc/lib/strophe.js index 2cdfb91..564465b 100644 --- a/build/js/jsxc/lib/strophe.js +++ b/build/js/jsxc/lib/strophe.js @@ -1,8 +1,6 @@ - /** * Modified by - * Klaus Herberth, 2013 - * https://github.com/sualko/strophejs/tree/master/ + * Klaus Herberth, 2014 */ // This code was written by Tyler Akins and has been placed in the @@ -85,6 +83,7 @@ var Base64 = (function () { return obj; })(); + /* * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined * in FIPS PUB 180-1 @@ -94,34 +93,18 @@ var Base64 = (function () { * 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 */ +/* Some functions and variables have been stripped for use with Strophe */ /* * 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_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * 8));} +function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * 8));} 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) @@ -195,7 +178,7 @@ function sha1_kt(t) function core_hmac_sha1(key, data) { var bkey = str2binb(key); - if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * chrsz); } + if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * 8); } var ipad = new Array(16), opad = new Array(16); for (var i = 0; i < 16; i++) @@ -204,7 +187,7 @@ function core_hmac_sha1(key, data) opad[i] = bkey[i] ^ 0x5C5C5C5C; } - var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * 8); return core_sha1(opad.concat(hash), 512 + 160); } @@ -234,10 +217,10 @@ function rol(num, cnt) function str2binb(str) { var bin = []; - var mask = (1 << chrsz) - 1; - for (var i = 0; i < str.length * chrsz; i += chrsz) + var mask = 255; + for (var i = 0; i < str.length * 8; i += 8) { - bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); + bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (24 - i%32); } return bin; } @@ -248,25 +231,10 @@ function str2binb(str) 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++) + var mask = 255; + for (var i = 0; i < bin.length * 32; i += 8) { - str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + - hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask); } return str; } @@ -286,12 +254,13 @@ function binb2b64(binarray) ((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; } + if (i * 8 + j * 6 > binarray.length * 32) { str += "="; } 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. @@ -301,15 +270,11 @@ function binb2b64(binarray) * See http://pajhome.org.uk/crypt/md5 for more info. */ -var MD5 = (function () { - /* - * 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 */ +/* + * Everything that isn't used by Strophe has been stripped here! + */ +var MD5 = (function () { /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. @@ -329,14 +294,12 @@ var MD5 = (function () { /* * Convert a string to an array of little-endian words - * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. */ var str2binl = function (str) { var bin = []; - var mask = (1 << chrsz) - 1; - for(var i = 0; i < str.length * chrsz; i += chrsz) + for(var i = 0; i < str.length * 8; i += 8) { - bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + bin[i>>5] |= (str.charCodeAt(i / 8) & 255) << (i%32); } return bin; }; @@ -346,10 +309,9 @@ var MD5 = (function () { */ var binl2str = function (bin) { var str = ""; - var mask = (1 << chrsz) - 1; - for(var i = 0; i < bin.length * 32; i += chrsz) + for(var i = 0; i < bin.length * 32; i += 8) { - str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & 255); } return str; }; @@ -358,7 +320,7 @@ var MD5 = (function () { * Convert an array of little-endian words to a hex string. */ var binl2hex = function (binarray) { - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var hex_tab = "0123456789abcdef"; var str = ""; for(var i = 0; i < binarray.length * 4; i++) { @@ -369,27 +331,6 @@ var MD5 = (function () { }; /* - * Convert an array of little-endian words to a base-64 string - */ - var binl2b64 = function (binarray) { - var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var str = ""; - var triplet, j; - for(var i = 0; i < binarray.length * 4; i += 3) - { - triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) | - (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) | - ((binarray[i+2 >> 2] >> 8 * ((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; - }; - - /* * These functions implement the four basic operations the algorithm uses. */ var md5_cmn = function (q, a, b, x, s, t) { @@ -510,24 +451,6 @@ var MD5 = (function () { }; - /* - * Calculate the HMAC-MD5, of a key and some data - */ - var core_hmac_md5 = function (key, data) { - var bkey = str2binl(key); - if(bkey.length > 16) { bkey = core_md5(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_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); - return core_md5(opad.concat(hash), 512 + 128); - }; - var obj = { /* * These are the functions you'll usually want to call. @@ -535,39 +458,17 @@ var MD5 = (function () { * strings. */ hexdigest: function (s) { - return binl2hex(core_md5(str2binl(s), s.length * chrsz)); - }, - - b64digest: function (s) { - return binl2b64(core_md5(str2binl(s), s.length * chrsz)); + return binl2hex(core_md5(str2binl(s), s.length * 8)); }, hash: function (s) { - return binl2str(core_md5(str2binl(s), s.length * chrsz)); - }, - - hmac_hexdigest: function (key, data) { - return binl2hex(core_hmac_md5(key, data)); - }, - - hmac_b64digest: function (key, data) { - return binl2b64(core_hmac_md5(key, data)); - }, - - hmac_hash: function (key, data) { - return binl2str(core_hmac_md5(key, data)); - }, - - /* - * Perform a simple self-test to see if the VM is working - */ - test: function () { - return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72"; + return binl2str(core_md5(str2binl(s), s.length * 8)); } }; return obj; })(); + /* This program is distributed under the terms of the MIT license. Please see the LICENSE file for details. @@ -575,20 +476,24 @@ var MD5 = (function () { Copyright 2006-2008, OGG, LLC */ -/* jslint configuration: */ +/* jshint undef: true, unused: true:, noarg: true, latedef: true */ /*global document, window, setTimeout, clearTimeout, console, - XMLHttpRequest, ActiveXObject, - Base64, MD5, - Strophe, $build, $msg, $iq, $pres */ + ActiveXObject, Base64, MD5, DOMParser */ +// from sha1.js +/*global core_hmac_sha1, binb2str, str_hmac_sha1, str_sha1, b64_hmac_sha1*/ /** File: strophe.js - * A JavaScript library for XMPP BOSH. + * A JavaScript library for XMPP BOSH/XMPP over Websocket. * * This is the JavaScript version of the Strophe library. Since JavaScript - * has no facilities for persistent TCP connections, this library uses + * had no facilities for persistent TCP connections, this library uses * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate * a persistent, stateful, two-way connection to an XMPP server. More * information on BOSH can be found in XEP 124. + * + * This version of Strophe also works with WebSockets. + * For more information on XMPP-over WebSocket see this RFC draft: + * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 */ /** PrivateFunction: Function.prototype.bind @@ -597,7 +502,7 @@ var MD5 = (function () { * This Function object extension method creates a bound method similar * to those in Python. This means that the 'this' object will point * to the instance you want. See - * <a href='https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind'>MDC's bind() documentation</a> and + * <a href='https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind'>MDC's bind() documentation</a> and * <a href='http://benjamin.smedbergs.us/blog/2007-01-03/bound-functions-and-function-imports-in-javascript/'>Bound Functions and Function Imports in JavaScript</a> * for a complete explanation. * @@ -606,7 +511,7 @@ var MD5 = (function () { * * Parameters: * (Object) obj - The object that will become 'this' in the bound function. - * (Object) argN - An option argument that will be prepended to the + * (Object) argN - An option argument that will be prepended to the * arguments given for the function call * * Returns: @@ -724,12 +629,11 @@ function $pres(attrs) { return new Strophe.Builder("presence", attrs); } * provide a namespace for library objects, constants, and functions. */ Strophe = { - ASYNC: true, /** Constant: VERSION * The version of the Strophe library. Unreleased builds will have * a version of head-HASH where HASH is a partial revision. */ - VERSION: "8e14efd", + VERSION: "1.1.3", /** Constants: XMPP Namespace Constants * Common namespace constants from the XMPP RFCs and XEPs. @@ -771,76 +675,59 @@ Strophe = { }, - /** Constants: XHTML_IM Namespace - * contains allowed tags, tag attributes, and css properties. + /** 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. - * For example: - * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); - * - * Parameters: - * (String) name - The name under which the namespace will be - * referenced under Strophe.NS - * (String) value - The actual namespace. - */ - addNamespace: function (name, value) - { - Strophe.NS[name] = value; + 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; + } }, /** Constants: Connection Status Constants @@ -917,6 +804,23 @@ Strophe = { TIMEOUT: 1.1, SECONDARY_TIMEOUT: 0.1, + /** 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. + * For example: + * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); + * + * Parameters: + * (String) name - The name under which the namespace will be + * referenced under Strophe.NS + * (String) value - The actual namespace. + */ + addNamespace: function (name, value) + { + Strophe.NS[name] = value; + }, + /** Function: forEachChild * Map a function over some or all child elements of a given element. * @@ -976,10 +880,10 @@ Strophe = { var doc; // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload. - // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be - // less than 10 in the case of IE9 and below. - if (document.implementation.createDocument === undefined || - document.implementation.createDocument && document.documentMode && document.documentMode < 10) { + // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be + // less than 10 in the case of IE9 and below. + if (document.implementation.createDocument === undefined || + document.implementation.createDocument && document.documentMode && document.documentMode < 10) { doc = this._getIEXmlDom(); doc.appendChild(doc.createElement('strophe')); } else { @@ -1097,7 +1001,7 @@ Strophe = { * Parameters: * (String) text - text to escape. * - * Returns: + * Returns: * Escaped text. */ xmlescape: function(text) @@ -1137,9 +1041,10 @@ Strophe = { */ xmlHtmlNode: function (html) { + var node; //ensure text is escaped if (window.DOMParser) { - parser = new DOMParser(); + var parser = new DOMParser(); node = parser.parseFromString(html, "text/xml"); } else { node = new ActiveXObject("Microsoft.XMLDOM"); @@ -1225,7 +1130,7 @@ Strophe = { */ createHtml: function (elem) { - var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue, children, child; + var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue; if (elem.nodeType == Strophe.ElementType.NORMAL) { tag = elem.nodeName.toLowerCase(); if(Strophe.XHTML.validTag(tag)) { @@ -1430,10 +1335,12 @@ Strophe = { * be one of the values in Strophe.LogLevel. * (String) msg - The log message. */ + /* jshint ignore:start */ log: function (level, msg) { return; }, + /* jshint ignore:end */ /** Function: debug * Log a message at the Strophe.LogLevel.DEBUG level. @@ -1748,12 +1655,13 @@ Strophe.Builder.prototype = { */ cnode: function (elem) { + var impNode; var xmlGen = Strophe.xmlGenerator(); try { - var impNode = (xmlGen.importNode !== undefined); + impNode = (xmlGen.importNode !== undefined); } catch (e) { - var impNode = false; + impNode = false; } var newElem = impNode ? xmlGen.importNode(elem, true) : @@ -1847,7 +1755,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; @@ -1877,7 +1785,7 @@ Strophe.Handler.prototype = { { var nsMatch; var from = null; - + if (this.options.matchBare) { from = Strophe.getBareJidFromJid(elem.getAttribute('from')); } else { @@ -2028,121 +1936,13 @@ Strophe.TimedHandler.prototype = { } }; -/** PrivateClass: Strophe.Request - * _Private_ helper class that provides a cross implementation abstraction - * for a BOSH related XMLHttpRequest. - * - * The Strophe.Request class is used internally to encapsulate BOSH request - * information. It is not meant to be used from user's code. - */ - -/** PrivateConstructor: Strophe.Request - * Create and initialize a new Strophe.Request object. - * - * Parameters: - * (XMLElement) elem - The XML data to be sent in the request. - * (Function) func - The function that will be called when the - * XMLHttpRequest readyState changes. - * (Integer) rid - The BOSH rid attribute associated with this request. - * (Integer) sends - The number of times this same request has been - * sent. - */ -Strophe.Request = function (elem, func, rid, sends) -{ - this.id = ++Strophe._requestId; - this.xmlData = elem; - this.data = Strophe.serialize(elem); - // save original function in case we need to make a new request - // from this one. - this.origFunc = func; - this.func = func; - this.rid = rid; - this.date = NaN; - this.sends = sends || 0; - this.abort = false; - this.dead = null; - this.age = function () { - if (!this.date) { return 0; } - var now = new Date(); - return (now - this.date) / 1000; - }; - this.timeDead = function () { - if (!this.dead) { return 0; } - var now = new Date(); - return (now - this.dead) / 1000; - }; - this.xhr = this._newXHR(); -}; - -Strophe.Request.prototype = { - /** PrivateFunction: getResponse - * Get a response from the underlying XMLHttpRequest. - * - * This function attempts to get a response from the request and checks - * for errors. - * - * Throws: - * "parsererror" - A parser error occured. - * - * Returns: - * The DOM element tree of the response. - */ - getResponse: function () - { - var node = null; - if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { - node = this.xhr.responseXML.documentElement; - if (node.tagName == "parsererror") { - Strophe.error("invalid response received"); - Strophe.error("responseText: " + this.xhr.responseText); - Strophe.error("responseXML: " + - Strophe.serialize(this.xhr.responseXML)); - throw "parsererror"; - } - } else if (this.xhr.responseText) { - Strophe.error("invalid response received"); - Strophe.error("responseText: " + this.xhr.responseText); - Strophe.error("responseXML: " + - Strophe.serialize(this.xhr.responseXML)); - } - - return node; - }, - - /** PrivateFunction: _newXHR - * _Private_ helper function to create XMLHttpRequests. - * - * This function creates XMLHttpRequests across all implementations. - * - * Returns: - * A new XMLHttpRequest. - */ - _newXHR: function () - { - var xhr = null; - if (window.XMLHttpRequest) { - xhr = new XMLHttpRequest(); - if (xhr.overrideMimeType) { - xhr.overrideMimeType("text/xml"); - } - } else if (window.ActiveXObject) { - xhr = new ActiveXObject("Microsoft.XMLHTTP"); - } - - // use Function.bind() to prepend ourselves as an argument - xhr.onreadystatechange = this.func.bind(null, this); - - return xhr; - } -}; - /** Class: Strophe.Connection * XMPP Connection manager. * * 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. + * 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 @@ -2161,31 +1961,75 @@ Strophe.Request.prototype = { /** 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. + * + * > var conn = new Strophe.Connection("/http-bind/"); + * + * 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 + * + * > var 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: + * > var conn = new Strophe.Connection("/http-bind/", {sync: true}); + * You can also toggle this on an already established connection: + * > conn.options.sync = true; + * + * * Parameters: - * (String) service - The BOSH service URL. + * (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) +Strophe.Connection = function (service, options) { - /* The path to the httpbind service. */ + // The service URL this.service = service; + + // Configuration options + this.options = options || {}; + var 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; - /* request id for body tags */ - this.rid = Math.floor(Math.random() * 4294967295); - - /* The current session ID. */ - this.sid = null; - this.streamId = null; /* stream:features */ this.features = null; // SASL - this._sasl_data = []; + this._sasl_data = {}; this.do_session = false; this.do_bind = false; @@ -2210,14 +2054,8 @@ Strophe.Connection = function (service) this.paused = false; - // default BOSH values - this.hold = 1; - this.wait = 60; - this.window = 5; - this._data = []; - this._requests = []; - this._uniqueId = Math.round(Math.random() * 10000); + this._uniqueId = 0; this._sasl_success_handler = null; this._sasl_failure_handler = null; @@ -2232,12 +2070,12 @@ Strophe.Connection = function (service) // initialize plugins for (var k in Strophe._connectionPlugins) { if (Strophe._connectionPlugins.hasOwnProperty(k)) { - var ptype = Strophe._connectionPlugins[k]; + var ptype = Strophe._connectionPlugins[k]; // jslint complaints about the below line, but this is fine - var F = function () {}; + var F = function () {}; // jshint ignore:line F.prototype = ptype; this[k] = new F(); - this[k].init(this); + this[k].init(this); } } }; @@ -2251,11 +2089,7 @@ Strophe.Connection.prototype = { */ reset: function () { - this.rid = Math.floor(Math.random() * 4294967295); - jQuery(document).trigger('ridChange', {rid: this.rid}); - - this.sid = null; - this.streamId = null; + this._proto._reset(); // SASL this.do_session = false; @@ -2277,16 +2111,17 @@ Strophe.Connection.prototype = { this.errors = 0; this._requests = []; - this._uniqueId = Math.round(Math.random()*10000); + 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 while a lot - * of send() calls are happening quickly. This causes Strophe to - * send the data in a single request, saving many request trips. + * 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 () { @@ -2345,8 +2180,9 @@ Strophe.Connection.prototype = { * constants. The error condition will be one of the conditions * defined in RFC 3920 or the condition 'strophe-parsererror'. * - * Please see XEP 124 for a more detailed explanation of the optional - * parameters below. + * 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, @@ -2360,53 +2196,39 @@ 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 + * (String) route - The optional route value. */ connect: function (jid, pass, callback, wait, hold, route) { this.jid = jid; + /** Variable: authzid + * Authorization identity. + */ + this.authzid = Strophe.getBareJidFromJid(this.jid); + /** Variable: authcid + * Authentication identity (User name). + */ + this.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.errors = 0; - this.wait = wait || this.wait; - this.hold = hold || this.hold; - - // parse jid for domain and resource - this.domain = this.domain || Strophe.getDomainFromJid(this.jid); - - // build the body tag - var body = this._buildBody().attrs({ - to: this.domain, - "xml:lang": "en", - wait: this.wait, - hold: this.hold, - content: "text/xml; charset=utf-8", - ver: "1.6", - "xmpp:version": "1.0", - "xmlns:xmpp": Strophe.NS.BOSH - }); - - if(route){ - body.attrs({ - route: route - }); - } + // parse jid for domain + this.domain = Strophe.getDomainFromJid(this.jid); 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, _connect_cb.bind(this)), - body.tree().getAttribute("rid"))); - this._throttledRequestHandler(); + this._proto._connect(wait, hold, route); }, /** Function: attach @@ -2435,22 +2257,7 @@ Strophe.Connection.prototype = { */ attach: function (jid, sid, rid, callback, wait, hold, wind) { - this.jid = jid; - this.sid = sid; - this.rid = rid; - - this.connect_callback = callback; - - this.domain = Strophe.getDomainFromJid(this.jid); - - this.authenticated = true; - this.connected = true; - - this.wait = wait || this.wait; - this.hold = hold || this.hold; - this.window = wind || this.window; - - this._changeConnectStatus(Strophe.Status.ATTACHED, null); + this._proto._attach(jid, sid, rid, callback, wait, hold, wind); }, /** Function: xmlInput @@ -2462,13 +2269,21 @@ Strophe.Connection.prototype = { * > (user code) * > }; * + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * <stream> tag for WebSocket-Connoctions will be passed as selfclosing here. + * + * BOSH-Connections will have all stanzas wrapped in a <body> tag. See + * <Strophe.Bosh.strip> 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 @@ -2479,13 +2294,21 @@ Strophe.Connection.prototype = { * > (user code) * > }; * + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * <stream> tag for WebSocket-Connoctions will be passed as selfclosing here. + * + * BOSH-Connections will have all stanzas wrapped in a <body> tag. See + * <Strophe.Bosh.strip> 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 @@ -2499,10 +2322,12 @@ Strophe.Connection.prototype = { * 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 @@ -2516,10 +2341,12 @@ Strophe.Connection.prototype = { * Parameters: * (String) data - The data sent by the connection. */ + /* jshint unused:false */ rawOutput: function (data) { return; }, + /* jshint unused:true */ /** Function: send * Send a stanza. @@ -2546,9 +2373,7 @@ Strophe.Connection.prototype = { this._queueData(elem); } - this._throttledRequestHandler(); - clearTimeout(this._idleTimeout); - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + this._proto._send(); }, /** Function: flush @@ -2588,54 +2413,54 @@ Strophe.Connection.prototype = { if (typeof(elem.tree) === "function") { elem = elem.tree(); } - var id = elem.getAttribute('id'); + var id = elem.getAttribute('id'); - // inject id if not found - if (!id) { - id = this.getUniqueId("sendIQ"); - elem.setAttribute("id", id); - } + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } - var handler = this.addHandler(function (stanza) { - // remove timeout handler if there is one + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one if (timeoutHandler) { that.deleteTimedHandler(timeoutHandler); } var iqtype = stanza.getAttribute('type'); - if (iqtype == 'result') { - if (callback) { + if (iqtype == 'result') { + if (callback) { callback(stanza); } - } else if (iqtype == 'error') { - if (errback) { + } else if (iqtype == 'error') { + if (errback) { errback(stanza); } - } else { + } else { throw { name: "StropheError", - message: "Got bad IQ type of " + iqtype + message: "Got bad IQ type of " + iqtype }; } - }, null, 'iq', null, id); + }, null, 'iq', null, id); - // if timeout specified, setup timeout handler. - if (timeout) { - timeoutHandler = this.addTimedHandler(timeout, function () { + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { // get rid of normal handler that.deleteHandler(handler); - // call errback on timeout with null stanza + // call errback on timeout with null stanza if (errback) { - errback(null); + errback(null); } - return false; - }); - } + return false; + }); + } - this.send(elem); + this.send(elem); - return id; + return id; }, /** PrivateFunction: _queueData @@ -2651,7 +2476,7 @@ Strophe.Connection.prototype = { message: "Cannot queue non-DOMElement." }; } - + this._data.push(element); }, @@ -2662,8 +2487,8 @@ Strophe.Connection.prototype = { { this._data.push("restart"); - this._throttledRequestHandler(); - clearTimeout(this._idleTimeout); + this._proto._sendRestart(); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); }, @@ -2795,10 +2620,18 @@ Strophe.Connection.prototype = { Strophe.info("Disconnect was called because: " + reason); if (this.connected) { + var 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._sendTerminate(); + this._proto._disconnect(pres); } }, @@ -2839,326 +2672,6 @@ Strophe.Connection.prototype = { } }, - /** PrivateFunction: _buildBody - * _Private_ helper function to generate the <body/> wrapper for BOSH. - * - * Returns: - * A Strophe.Builder with a <body/> element. - */ - _buildBody: function () - { - var bodyWrap = $build('body', { - rid: this.rid++, - xmlns: Strophe.NS.HTTPBIND - }); - - if (this.sid !== null) { - bodyWrap.attrs({sid: this.sid}); - } - - return bodyWrap; - }, - - /** PrivateFunction: _removeRequest - * _Private_ function to remove a request from the queue. - * - * Parameters: - * (Strophe.Request) req - The request to remove. - */ - _removeRequest: function (req) - { - Strophe.debug("removing request"); - - var i; - for (i = this._requests.length - 1; i >= 0; i--) { - if (req == this._requests[i]) { - this._requests.splice(i, 1); - } - } - - // IE6 fails on setting to null, so set to empty function - req.xhr.onreadystatechange = function () {}; - - this._throttledRequestHandler(); - }, - - /** PrivateFunction: _restartRequest - * _Private_ function to restart a request that is presumed dead. - * - * Parameters: - * (Integer) i - The index of the request in the queue. - */ - _restartRequest: function (i) - { - var req = this._requests[i]; - if (req.dead === null) { - req.dead = new Date(); - } - - this._processRequest(i); - }, - - /** PrivateFunction: _processRequest - * _Private_ function to process a request in the queue. - * - * This function takes requests off the queue and sends them and - * restarts dead requests. - * - * Parameters: - * (Integer) i - The index of the request in the queue. - */ - _processRequest: function (i) - { - var req = this._requests[i]; - var reqStatus = -1; - - try { - if (req.xhr.readyState == 4) { - reqStatus = req.xhr.status; - } - } catch (e) { - Strophe.error("caught an error in _requests[" + i + - "], reqStatus: " + reqStatus); - } - - if (typeof(reqStatus) == "undefined") { - reqStatus = -1; - } - - // make sure we limit the number of retries - if (req.sends > this.maxRetries) { - this._onDisconnectTimeout(); - return; - } - - var time_elapsed = req.age(); - var primaryTimeout = (!isNaN(time_elapsed) && - time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); - var secondaryTimeout = (req.dead !== null && - req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); - var requestCompletedWithServerError = (req.xhr.readyState == 4 && - (reqStatus < 1 || - reqStatus >= 500)); - if (primaryTimeout || secondaryTimeout || - requestCompletedWithServerError) { - if (secondaryTimeout) { - Strophe.error("Request " + - this._requests[i].id + - " timed out (secondary), restarting"); - } - req.abort = true; - req.xhr.abort(); - // setting to null fails on IE6, so set to empty function - req.xhr.onreadystatechange = function () {}; - this._requests[i] = new Strophe.Request(req.xmlData, - req.origFunc, - req.rid, - req.sends); - req = this._requests[i]; - } - - if (req.xhr.readyState === 0) { - Strophe.debug("request id " + req.id + - "." + req.sends + " posting"); - - try { - req.xhr.open("POST", this.service, Strophe.ASYNC); - } catch (e2) { - Strophe.error("XHR open failed."); - if (!this.connected) { - this._changeConnectStatus(Strophe.Status.CONNFAIL, - "bad-service"); - } - this.disconnect(); - return; - } - - // 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 creates a nicely - // expanding retry window - var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), - Math.pow(req.sends, 3)) * 1000; - setTimeout(sendFunc, backoff); - } else { - sendFunc(); - } - - req.sends++; - - 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") + - " request has readyState of " + - req.xhr.readyState); - } - }, - - /** PrivateFunction: _throttledRequestHandler - * _Private_ function to throttle requests to the connection window. - * - * This function makes sure we don't send requests so fast that the - * request ids overflow the connection window in the case that one - * request died. - */ - _throttledRequestHandler: function () - { - if (!this._requests) { - Strophe.debug("_throttledRequestHandler called with " + - "undefined requests"); - } else { - Strophe.debug("_throttledRequestHandler called with " + - this._requests.length + " requests"); - } - - if (!this._requests || this._requests.length === 0) { - return; - } - - if (this._requests.length > 0) { - this._processRequest(0); - } - - if (this._requests.length > 1 && - Math.abs(this._requests[0].rid - - this._requests[1].rid) < this.window) { - this._processRequest(1); - } - }, - - /** PrivateFunction: _onRequestStateChange - * _Private_ handler for Strophe.Request state changes. - * - * This function is called when the XMLHttpRequest readyState changes. - * It contains a lot of error handling logic for the many ways that - * requests can fail, and calls the request callback when requests - * succeed. - * - * Parameters: - * (Function) func - The handler for the request. - * (Strophe.Request) req - The request that is changing readyState. - */ - _onRequestStateChange: function (func, req) - { - Strophe.debug("request id " + req.id + - "." + req.sends + " state changed to " + - req.xhr.readyState); - - if (req.abort) { - req.abort = false; - return; - } - - if(req.xhr.readyState == 2){ - jQuery(document).trigger('ridChange', {rid: Number(req.rid)+1}); - } - - // request complete - var reqStatus; - if (req.xhr.readyState == 4) { - reqStatus = 0; - try { - reqStatus = req.xhr.status; - } catch (e) { - // ignore errors from undefined status attribute. works - // around a browser bug - } - - if (typeof(reqStatus) == "undefined") { - reqStatus = 0; - } - - if (this.disconnecting) { - if (reqStatus >= 400) { - this._hitError(reqStatus); - return; - } - } - - var reqIs0 = (this._requests[0] == req); - var reqIs1 = (this._requests[1] == req); - - if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { - // remove from internal queue - this._removeRequest(req); - Strophe.debug("request id " + - req.id + - " should now be removed"); - } - - // request succeeded - if (reqStatus == 200) { - // if request 1 finished, or request 0 finished and request - // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to - // restart the other - both will be in the first spot, as the - // completed request has been removed from the queue already - if (reqIs1 || - (reqIs0 && this._requests.length > 0 && - this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { - this._restartRequest(0); - } - - // call handler - func(req); - this.errors = 0; - } else { - Strophe.error("request id " + - req.id + "." + - req.sends + " error " + reqStatus + - " happened"); - if (reqStatus === 0 || - (reqStatus >= 400 && reqStatus < 600) || - reqStatus >= 12000) { - this._hitError(reqStatus); - if (reqStatus >= 400 && reqStatus < 500) { - this._changeConnectStatus(Strophe.Status.DISCONNECTING, - null); - this._doDisconnect(); - } - } - } - - if (!((reqStatus > 0 && reqStatus < 500) || - req.sends > 5)) { - this._throttledRequestHandler(); - } - } - }, - - /** PrivateFunction: _hitError - * _Private_ function to handle the error count. - * - * Requests are resent automatically until their error count reaches - * 5. Each time an error is encountered, this function is called to - * increment the count and disconnect if the count is too high. - * - * Parameters: - * (Integer) reqStatus - The request status. - */ - _hitError: function (reqStatus) - { - this.errors++; - Strophe.warn("request errored, status: " + reqStatus + - ", number of errors: " + this.errors); - if (this.errors > 4) { - this._onDisconnectTimeout(); - } - }, - /** PrivateFunction: _doDisconnect * _Private_ function to disconnect. * @@ -3167,19 +2680,17 @@ Strophe.Connection.prototype = { */ _doDisconnect: function () { + // 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.sid = null; - this.streamId = null; - this.rid = Math.floor(Math.random() * 4294967295); - jQuery(document).trigger('ridChange', {rid: this.rid}); - - // tell the parent we disconnected - if (this.connected) { - this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); - this.connected = false; - } // delete handlers this.handlers = []; @@ -3188,6 +2699,10 @@ Strophe.Connection.prototype = { this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; + + // tell the parent we disconnected + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; }, /** PrivateFunction: _dataRecv @@ -3200,22 +2715,27 @@ Strophe.Connection.prototype = { * * Parameters: * (Strophe.Request) req - The request that has data ready. + * (string) req - The stanza a raw string (optiona). */ - _dataRecv: function (req) + _dataRecv: function (req, raw) { - try { - var elem = req.getResponse(); - } catch (e) { - if (e != "parsererror") { throw e; } - this.disconnect("strophe-parsererror"); - } + Strophe.info("_dataRecv called"); + var elem = this._proto._reqToData(req); if (elem === null) { return; } if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { - this.xmlInput(elem); + 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) { - this.rawInput(Strophe.serialize(elem)); + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(elem)); + } } // remove handlers scheduled for deletion @@ -3234,9 +2754,7 @@ Strophe.Connection.prototype = { } // handle graceful disconnect - if (this.disconnecting && this._requests.length === 0) { - this.deleteTimedHandler(this._disconnectTimeout); - this._disconnectTimeout = null; + if (this.disconnecting && this._proto._emptyQueue()) { this._doDisconnect(); return; } @@ -3260,7 +2778,7 @@ Strophe.Connection.prototype = { } else { this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); } - this.disconnect(); + this.disconnect('unknown stream-error'); return; } @@ -3285,41 +2803,18 @@ Strophe.Connection.prototype = { that.handlers.push(hand); } } catch(e) { - //if the handler throws an exception, we consider it as false + // if the handler throws an exception, we consider it as false + Strophe.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); } } }); }, - /** PrivateFunction: _sendTerminate - * _Private_ function to send initial disconnect sequence. - * - * This is the first step in a graceful disconnect. It sends - * the BOSH server a terminate body and includes an unavailable - * presence if authentication has completed. - */ - _sendTerminate: function () - { - Strophe.info("_sendTerminate was called"); - var body = this._buildBody().attrs({type: "terminate"}); - - if (this.authenticated) { - body.c('presence', { - xmlns: Strophe.NS.CLIENT, - type: 'unavailable' - }); - } - - this.disconnecting = true; - var req = new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, this._dataRecv.bind(this)), - body.tree().getAttribute("rid")); - - this._requests.push(req); - this._throttledRequestHandler(); - }, + /** Attribute: mechanisms + * SASL Mechanisms available for Conncection. + */ + mechanisms: {}, /** PrivateFunction: _connect_cb * _Private_ handler for initial connection request. @@ -3337,59 +2832,41 @@ Strophe.Connection.prototype = { * Useful for plugins with their own xmpp connect callback (when their) * want to do something special). */ - _connect_cb: function (req, _callback) + _connect_cb: function (req, _callback, raw) { Strophe.info("_connect_cb was called"); this.connected = true; - var bodyWrap = req.getResponse(); + + var bodyWrap = this._proto._reqToData(req); if (!bodyWrap) { return; } if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { - this.xmlInput(bodyWrap); + 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) { - this.rawInput(Strophe.serialize(bodyWrap)); - } - - var typ = bodyWrap.getAttribute("type"); - var cond, conflict; - if (typ !== null && typ == "terminate") { - // an error occurred - cond = bodyWrap.getAttribute("condition"); - conflict = bodyWrap.getElementsByTagName("conflict"); - if (cond !== null) { - if (cond == "remote-stream-error" && conflict.length > 0) { - cond = "conflict"; - } - this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + if (raw) { + this.rawInput(raw); } else { - this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + this.rawInput(Strophe.serialize(bodyWrap)); } - return; } - // check to make sure we don't overwrite these if _connect_cb is - // called multiple times in the case of missing stream:features - if (!this.sid) { - this.sid = bodyWrap.getAttribute("sid"); - } - if (!this.stream_id) { - this.stream_id = bodyWrap.getAttribute("authid"); + var conncheck = this._proto._connect_cb(bodyWrap); + if (conncheck === Strophe.Status.CONNFAIL) { + return; } - var wind = bodyWrap.getAttribute('requests'); - if (wind) { this.window = parseInt(wind, 10); } - var hold = bodyWrap.getAttribute('hold'); - if (hold) { this.hold = parseInt(hold, 10); } - 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; + this._authentication.legacy_auth = false; // Check for the stream:features tag var hasFeatures = bodyWrap.getElementsByTagName("stream:features").length > 0; @@ -3397,45 +2874,28 @@ Strophe.Connection.prototype = { hasFeatures = bodyWrap.getElementsByTagName("features").length > 0; } var mechanisms = bodyWrap.getElementsByTagName("mechanism"); - var i, mech, auth_str, hashed_auth_str, - found_authentication = false; - if (hasFeatures && mechanisms.length > 0) { - var missmatchedmechs = 0; + var matched = []; + var i, mech, found_authentication = false; + if (!hasFeatures) { + this._proto._no_auth_received(_callback); + return; + } + if (mechanisms.length > 0) { for (i = 0; i < mechanisms.length; i++) { mech = Strophe.getText(mechanisms[i]); - 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') { - this._authentication.sasl_plain = true; - } else if (mech == 'ANONYMOUS') { - this._authentication.sasl_anonymous = true; - } else missmatchedmechs++; + if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]); } - - this._authentication.legacy_auth = - bodyWrap.getElementsByTagName("auth").length > 0; - - found_authentication = - this._authentication.legacy_auth || - missmatchedmechs < mechanisms.length; } + this._authentication.legacy_auth = + bodyWrap.getElementsByTagName("auth").length > 0; + found_authentication = this._authentication.legacy_auth || + matched.length > 0; 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, _callback.bind(this)), - body.tree().getAttribute("rid"))); - this._throttledRequestHandler(); + this._proto._no_auth_received(_callback); return; } if (this.do_authentication !== false) - this.authenticate(); + this.authenticate(matched); }, /** Function: authenticate @@ -3448,313 +2908,97 @@ Strophe.Connection.prototype = { * the code will fall back to legacy authentication. * */ - authenticate: function () - { - if (Strophe.getNodeFromJid(this.jid) === null && - this._authentication.sasl_anonymous) { - this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); - 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("auth", { - xmlns: Strophe.NS.SASL, - mechanism: "ANONYMOUS" - }).tree()); - } else 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, - 'x-strophe-bad-non-anon-jid'); - this.disconnect(); - } 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_digest_challenge1_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: "DIGEST-MD5" - }).tree()); - } 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); - auth_str = auth_str + "\u0000"; - auth_str = auth_str + Strophe.getNodeFromJid(this.jid); - auth_str = auth_str + "\u0000"; - auth_str = auth_str + this.pass; - - this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); - 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); - - hashed_auth_str = Base64.encode(auth_str); - this.send($build("auth", { - xmlns: Strophe.NS.SASL, - mechanism: "PLAIN" - }).t(hashed_auth_str).tree()); - } else { - 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()); - } - }, - - /** PrivateFunction: _sasl_digest_challenge1_cb - * _Private_ handler for DIGEST-MD5 SASL authentication. - * - * Parameters: - * (XMLElement) elem - The challenge stanza. - * - * Returns: - * false to remove the handler. - */ - _sasl_digest_challenge1_cb: function (elem) + authenticate: function (matched) { - var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; - - var challenge = Base64.decode(Strophe.getText(elem)); - var cnonce = MD5.hexdigest("" + (Math.random() * 1234567890)); - var realm = ""; - var host = null; - var nonce = ""; - var qop = ""; - var matches; - - // remove unneeded handlers - this.deleteHandler(this._sasl_failure_handler); - - while (challenge.match(attribMatch)) { - matches = challenge.match(attribMatch); - challenge = challenge.replace(matches[0], ""); - matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); - switch (matches[1]) { - case "realm": - realm = matches[2]; - break; - case "nonce": - nonce = matches[2]; - break; - case "qop": - qop = matches[2]; - break; - case "host": - host = matches[2]; - break; - } + var i; + // Sorting matched mechanisms according to priority. + for (i = 0; i < matched.length - 1; ++i) { + var higher = i; + for (var j = i + 1; j < matched.length; ++j) { + if (matched[j].prototype.priority > matched[higher].prototype.priority) { + higher = j; + } } - - var digest_uri = "xmpp/" + this.domain; - if (host !== null) { - digest_uri = digest_uri + "/" + host; + if (higher != i) { + var swap = matched[i]; + matched[i] = matched[higher]; + matched[higher] = swap; } + } - var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) + - ":" + realm + ":" + this.pass) + - ":" + nonce + ":" + cnonce; - var A2 = 'AUTHENTICATE:' + digest_uri; - - var responseText = ""; - responseText += 'username=' + - this._quote(Strophe.getNodeFromJid(this.jid)) + ','; - responseText += 'realm=' + this._quote(realm) + ','; - responseText += 'nonce=' + this._quote(nonce) + ','; - responseText += 'cnonce=' + this._quote(cnonce) + ','; - responseText += 'nc="00000001",'; - responseText += 'qop="auth",'; - responseText += 'digest-uri=' + this._quote(digest_uri) + ','; - responseText += 'response=' + this._quote( - MD5.hexdigest(MD5.hexdigest(A1) + ":" + - nonce + ":00000001:" + - cnonce + ":auth:" + - MD5.hexdigest(A2))) + ','; - responseText += 'charset="utf-8"'; - - this._sasl_challenge_handler = this._addSysHandler( - this._sasl_digest_challenge2_cb.bind(this), null, - "challenge", null, null); - 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: _quote - * _Private_ utility function to backslash escape and quote strings. - * - * Parameters: - * (String) str - The string to be quoted. - * - * Returns: - * quoted string - */ - _quote: function (str) - { - return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; - //" end string workaround for emacs - }, - - - /** PrivateFunction: _sasl_digest_challenge2_cb - * _Private_ handler for second step of DIGEST-MD5 SASL authentication. - * - * Parameters: - * (XMLElement) elem - The challenge stanza. - * - * Returns: - * false to remove the handler. - */ - _sasl_digest_challenge2_cb: function (elem) - { - // remove unneeded handlers - this.deleteHandler(this._sasl_success_handler); - this.deleteHandler(this._sasl_failure_handler); + // run each mechanism + var mechanism_found = false; + for (i = 0; i < matched.length; ++i) { + if (!matched[i].test(this)) continue; this._sasl_success_handler = this._addSysHandler( - this._sasl_success_cb.bind(this), null, - "success", null, null); + 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}).tree()); - return false; - }, + 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); - /** 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); + this._sasl_mechanism = new matched[i](); + this._sasl_mechanism.onStart(this); - 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; - } - } + var request_auth_exchange = $build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: this._sasl_mechanism.name + }); - if (!(nonce.substr(0, cnonce.length) === cnonce)) { - this._sasl_data = []; - return this._sasl_failure_cb(null); + if (this._sasl_mechanism.isClientFirst) { + var response = this._sasl_mechanism.onChallenge(this, null); + request_auth_exchange.t(Base64.encode(response)); } - responseText += "r=" + nonce; - authMessage += responseText; - - salt = Base64.decode(salt); - salt += "\0\0\0\1"; + this.send(request_auth_exchange.tree()); - 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); + mechanism_found = true; + break; + } - 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); + if (!mechanism_found) { + // if none of the mechanism worked + 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, + 'x-strophe-bad-non-anon-jid'); + this.disconnect('x-strophe-bad-non-anon-jid'); + } else { + // fall back to legacy authentication + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._addSysHandler(this._auth1_cb.bind(this), null, null, + null, "_auth_1"); - for (k = 0; k < 5; k++) { - clientKey[k] ^= clientSignature[k]; + 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()); } + } - 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); + _sasl_challenge_cb: function(elem) { + var challenge = Base64.decode(Strophe.getText(elem)); + var response = this._sasl_mechanism.onChallenge(this, challenge); - this.send($build('response', { - xmlns: Strophe.NS.SASL - }).t(Base64.encode(responseText)).tree()); + var stanza = $build('response', { + xmlns: Strophe.NS.SASL + }); + if (response !== "") { + stanza.t(Base64.encode(response)); + } + this.send(stanza.tree()); - return false; + return true; }, /** PrivateFunction: _auth1_cb @@ -3771,6 +3015,7 @@ Strophe.Connection.prototype = { * Returns: * false to remove the handler. */ + /* jshint unused:false */ _auth1_cb: function (elem) { // build plaintext auth iq @@ -3795,6 +3040,7 @@ Strophe.Connection.prototype = { return false; }, + /* jshint unused:true */ /** PrivateFunction: _sasl_success_cb * _Private_ handler for succesful SASL authentication. @@ -3811,25 +3057,29 @@ Strophe.Connection.prototype = { var serverSignature; var success = Base64.decode(Strophe.getText(elem)); var attribMatch = /([a-z]+)=([^,]+)(,|$)/; - matches = success.match(attribMatch); + var 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 (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); @@ -3910,10 +3160,10 @@ Strophe.Connection.prototype = { { if (elem.getAttribute("type") == "error") { Strophe.info("SASL binding failed."); - var conflict = elem.getElementsByTagName("conflict"), condition; - if (conflict.length > 0) { - condition = 'conflict'; - } + var conflict = elem.getElementsByTagName("conflict"), condition; + if (conflict.length > 0) { + condition = 'conflict'; + } this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition); return false; } @@ -3981,6 +3231,7 @@ Strophe.Connection.prototype = { * Returns: * false to remove the handler. */ + /* jshint unused:false */ _sasl_failure_cb: function (elem) { // delete unneeded handlers @@ -3993,9 +3244,12 @@ Strophe.Connection.prototype = { this._sasl_challenge_handler = null; } + if(this._sasl_mechanism) + this._sasl_mechanism.onFailure(); this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); return false; }, + /* jshint unused:true */ /** PrivateFunction: _auth2_cb * _Private_ handler to finish legacy authentication. @@ -4016,7 +3270,7 @@ Strophe.Connection.prototype = { this._changeConnectStatus(Strophe.Status.CONNECTED, null); } else if (elem.getAttribute("type") == "error") { this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); - this.disconnect(); + this.disconnect('authentication failed'); } return false; @@ -4076,16 +3330,7 @@ Strophe.Connection.prototype = { { Strophe.info("_onDisconnectTimeout was called"); - // cancel all remaining requests and clear the queue - var req; - while (this._requests.length > 0) { - req = this._requests.pop(); - req.abort = true; - req.xhr.abort(); - // jslint complains, but this is fine. setting to empty func - // is necessary for IE6 - req.xhr.onreadystatechange = function () {}; - } + this._proto._onDisconnectTimeout(); // actually disconnect this._doDisconnect(); @@ -4137,46 +3382,863 @@ Strophe.Connection.prototype = { } this.timedHandlers = newList; - var body, time_elapsed; -//console.log('Authenticated: '+this.authenticated+', req length: '+this._requests.length+', data length: '+ -// this._data.length+', dis: '+!this.disconnecting); + clearTimeout(this._idleTimeout); + + this._proto._onIdle(); + + // reactivate the timer only if connected + if (this.connected) { + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } + } +}; + +if (callback) { + callback(Strophe, $build, $msg, $iq, $pres); +} + +/** Class: Strophe.SASLMechanism + * + * encapsulates SASL authentication mechanisms. + * + * User code may override the priority for each mechanism or disable it completely. + * See <priority> for information about changing priority and <test> for informatian on + * how to disable a mechanism. + * + * By default, all mechanisms are enabled and the priorities are + * + * SCRAM-SHA1 - 40 + * DIGEST-MD5 - 30 + * Plain - 20 + */ + +/** + * 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 <SASLMechanism> 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 <SASL mechanisms> 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 <SASL mechanisms> 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 + */ + +// Building SASL callbacks + +/** PrivateConstructor: SASLAnonymous + * SASL Anonymous authentication. + */ +Strophe.SASLAnonymous = function() {}; + +Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 10); + +Strophe.SASLAnonymous.test = function(connection) { + return connection.authcid === null; +}; + +Strophe.Connection.prototype.mechanisms[Strophe.SASLAnonymous.prototype.name] = Strophe.SASLAnonymous; + +/** PrivateConstructor: SASLPlain + * SASL Plain authentication. + */ +Strophe.SASLPlain = function() {}; + +Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 20); + +Strophe.SASLPlain.test = function(connection) { + return connection.authcid !== null; +}; + +Strophe.SASLPlain.prototype.onChallenge = function(connection) { + var 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 auth_str; +}; + +Strophe.Connection.prototype.mechanisms[Strophe.SASLPlain.prototype.name] = Strophe.SASLPlain; + +/** PrivateConstructor: SASLSHA1 + * SASL SCRAM SHA 1 authentication. + */ +Strophe.SASLSHA1 = function() {}; + +/* TEST: + * This is a simple example of a SCRAM-SHA-1 authentication exchange + * when the client doesn't support channel bindings (username 'user' and + * password 'pencil' are used): + * + * C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + * S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92, + * i=4096 + * C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j, + * p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + * S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + * + */ + +Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 40); + +Strophe.SASLSHA1.test = function(connection) { + return connection.authcid !== null; +}; + +Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cnonce) { + var cnonce = test_cnonce || MD5.hexdigest(Math.random() * 1234567890); + + var auth_str = "n=" + 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 = function (connection, challenge) + { + var nonce, salt, iter, Hi, U, U_old, i, k; + var clientKey, serverKey, clientSignature; + var responseText = "c=biws,"; + var authMessage = connection._sasl_data["client-first-message-bare"] + "," + + challenge + ","; + var cnonce = connection._sasl_data.cnonce; + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + + while (challenge.match(attribMatch)) { + var 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 = Base64.decode(salt); + salt += "\x00\x00\x00\x01"; + + Hi = U_old = core_hmac_sha1(connection.pass, salt); + for (i = 1; i < iter; i++) { + U = core_hmac_sha1(connection.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); + connection._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)); + + return responseText; + }.bind(this); + + return auth_str; +}; + +Strophe.Connection.prototype.mechanisms[Strophe.SASLSHA1.prototype.name] = Strophe.SASLSHA1; + +/** PrivateConstructor: SASLMD5 + * SASL DIGEST MD5 authentication. + */ +Strophe.SASLMD5 = function() {}; + +Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 30); + +Strophe.SASLMD5.test = function(connection) { + return connection.authcid !== null; +}; + +/** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ +Strophe.SASLMD5.prototype._quote = function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }; + + +Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cnonce) { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + var cnonce = test_cnonce || MD5.hexdigest("" + (Math.random() * 1234567890)); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = connection.servtype + "/" + connection.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(connection.authcid + + ":" + realm + ":" + this._connection.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'charset=utf-8,'; + responseText += 'username=' + + this._quote(connection.authcid) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'nc=00000001,'; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2)) + ","; + responseText += 'qop=auth'; + + this.onChallenge = function () + { + return ""; + }.bind(this); + + return responseText; +}; + +Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; + +})(function () { + window.Strophe = arguments[0]; + window.$build = arguments[1]; + window.$msg = arguments[2]; + window.$iq = arguments[3]; + window.$pres = arguments[4]; +}); + +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/* jshint undef: true, unused: true:, noarg: true, latedef: true */ +/*global window, setTimeout, clearTimeout, + XMLHttpRequest, ActiveXObject, + Strophe, $build */ + + +/** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. + * + * The Strophe.Request class is used internally to encapsulate BOSH request + * information. It is not meant to be used from user's code. + */ + +/** PrivateConstructor: Strophe.Request + * Create and initialize a new Strophe.Request object. + * + * Parameters: + * (XMLElement) elem - The XML data to be sent in the request. + * (Function) func - The function that will be called when the + * XMLHttpRequest readyState changes. + * (Integer) rid - The BOSH rid attribute associated with this request. + * (Integer) sends - The number of times this same request has been + * sent. + */ +Strophe.Request = function (elem, func, rid, sends) +{ + this.id = ++Strophe._requestId; + this.xmlData = elem; + this.data = Strophe.serialize(elem); + // save original function in case we need to make a new request + // from this one. + this.origFunc = func; + this.func = func; + this.rid = rid; + this.date = NaN; + this.sends = sends || 0; + this.abort = false; + this.dead = null; + + this.age = function () { + if (!this.date) { return 0; } + var now = new Date(); + return (now - this.date) / 1000; + }; + this.timeDead = function () { + if (!this.dead) { return 0; } + var now = new Date(); + return (now - this.dead) / 1000; + }; + this.xhr = this._newXHR(); +}; + +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. + * + * This function attempts to get a response from the request and checks + * for errors. + * + * Throws: + * "parsererror" - A parser error occured. + * + * Returns: + * The DOM element tree of the response. + */ + getResponse: function () + { + var node = null; + if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { + node = this.xhr.responseXML.documentElement; + if (node.tagName == "parsererror") { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + throw "parsererror"; + } + } else if (this.xhr.responseText) { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + } + + return node; + }, + + /** PrivateFunction: _newXHR + * _Private_ helper function to create XMLHttpRequests. + * + * This function creates XMLHttpRequests across all implementations. + * + * Returns: + * A new XMLHttpRequest. + */ + _newXHR: function () + { + var xhr = null; + if (window.XMLHttpRequest) { + xhr = new XMLHttpRequest(); + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/xml"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // use Function.bind() to prepend ourselves as an argument + xhr.onreadystatechange = this.func.bind(null, this); + + return xhr; + } +}; + +/** Class: Strophe.Bosh + * _Private_ helper class that handles BOSH Connections + * + * The Strophe.Bosh class is used internally by Strophe.Connection + * to encapsulate BOSH sessions. It is not meant to be used from user's code. + */ + +/** File: bosh.js + * A JavaScript library to enable BOSH in Strophejs. + * + * this library uses Bidirectional-streams Over Synchronous HTTP (BOSH) + * to emulate a persistent, stateful, two-way connection to an XMPP server. + * More information on BOSH can be found in XEP 124. + */ + +/** PrivateConstructor: Strophe.Bosh + * Create and initialize a Strophe.Bosh object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use BOSH. + * + * Returns: + * A new Strophe.Bosh object. + */ +Strophe.Bosh = function(connection) { + this._conn = connection; + /* request id for body tags */ + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + this.sid = null; + + // default BOSH values + this.hold = 1; + this.wait = 60; + this.window = 5; + + this._requests = []; +}; + +Strophe.Bosh.prototype = { + /** Variable: strip + * + * BOSH-Connections will have all stanzas wrapped in a <body> tag when + * passed to <Strophe.Connection.xmlInput> or <Strophe.Connection.xmlOutput>. + * To strip this tag, User code can set <Strophe.Bosh.strip> to "body": + * + * > Strophe.Bosh.prototype.strip = "body"; + * + * This will enable stripping of the body tag in both + * <Strophe.Connection.xmlInput> and <Strophe.Connection.xmlOutput>. + */ + strip: null, + + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the <body/> wrapper for BOSH. + * + * Returns: + * A Strophe.Builder with a <body/> element. + */ + _buildBody: function () + { + var bodyWrap = $build('body', { + rid: this.rid++, + xmlns: Strophe.NS.HTTPBIND + }); + + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); + } + + return bodyWrap; + }, + + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection + */ + _reset: function () + { + this.rid = Math.floor(Math.random() * 4294967295); + this.sid = null; + + jQuery(document).trigger('ridChange', {rid: this.rid}); + }, + + /** PrivateFunction: _connect + * _Private_ function that initializes the BOSH connection. + * + * Creates and sends the Request that initializes the BOSH connection. + */ + _connect: function (wait, hold, route) + { + this.wait = wait || this.wait; + this.hold = hold || this.hold; + + // build the body tag + var body = this._buildBody().attrs({ + to: this._conn.domain, + "xml:lang": "en", + wait: this.wait, + hold: this.hold, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); + + if(route){ + body.attrs({ + route: route + }); + } + + var _connect_cb = this._conn._connect_cb; + + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, _connect_cb.bind(this._conn)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _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) + { + this._conn.jid = jid; + this.sid = sid; + this.rid = rid; + + this._conn.connect_callback = callback; + + this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); + + this._conn.authenticated = true; + this._conn.connected = true; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._conn._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the Bosh-part of the initial request. + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function (bodyWrap) + { + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + Strophe.error("BOSH-Connection failed: " + cond); + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + this._conn._doDisconnect(); + return Strophe.Status.CONNFAIL; + } + + // check to make sure we don't overwrite these if _connect_cb is + // called multiple times in the case of missing stream:features + if (!this.sid) { + this.sid = bodyWrap.getAttribute("sid"); + } + var wind = bodyWrap.getAttribute('requests'); + if (wind) { this.window = parseInt(wind, 10); } + var hold = bodyWrap.getAttribute('hold'); + if (hold) { this.hold = parseInt(hold, 10); } + var wait = bodyWrap.getAttribute('wait'); + if (wait) { this.wait = parseInt(wait, 10); } + }, + + /** PrivateFunction: _disconnect + * _Private_ part of Connection.disconnect for Bosh + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + this._sendTerminate(pres); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Resets the SID and RID. + */ + _doDisconnect: function () + { + this.sid = null; + this.rid = Math.floor(Math.random() * 4294967295); + + jQuery(document).trigger('ridChange', {rid: this.rid}); + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the Request queue is empty. + * + * Returns: + * True, if there are no Requests queued, False otherwise. + */ + _emptyQueue: function () + { + return this._requests.length === 0; + }, + + /** PrivateFunction: _hitError + * _Private_ function to handle the error count. + * + * Requests are resent automatically until their error count reaches + * 5. Each time an error is encountered, this function is called to + * increment the count and disconnect if the count is too high. + * + * Parameters: + * (Integer) reqStatus - The request status. + */ + _hitError: function (reqStatus) + { + this.errors++; + Strophe.warn("request errored, status: " + reqStatus + + ", number of errors: " + this.errors); + if (this.errors > 4) { + this._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received and sends a blank poll request. + */ + _no_auth_received: function (_callback) + { + if (_callback) { + _callback = _callback.bind(this._conn); + } else { + _callback = this._conn._connect_cb.bind(this._conn); + } + var body = this._buildBody(); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, _callback.bind(this._conn)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * Cancels all remaining Requests and clears the queue. + */ + _onDisconnectTimeout: function () + { + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.abort = true; + req.xhr.abort(); + // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + req.xhr.onreadystatechange = function () {}; // jshint ignore:line + } + }, + + /** PrivateFunction: _onIdle + * _Private_ handler called by Strophe.Connection._onIdle + * + * Sends all queued Requests or polls with empty Request if there are none. + */ + _onIdle: function () { + var data = this._conn._data; + // if no requests are in progress, poll - if (this.authenticated && this._requests.length === 0 && - this._data.length === 0 && !this.disconnecting) { + if (this._conn.authenticated && this._requests.length === 0 && + data.length === 0 && !this._conn.disconnecting) { Strophe.info("no requests during idle cycle, sending " + "blank request"); - this._data.push(null); + data.push(null); } - if (this._requests.length < 2 && this._data.length > 0 && - !this.paused) { - body = this._buildBody(); - for (i = 0; i < this._data.length; i++) { - if (this._data[i] !== null) { - if (this._data[i] === "restart") { + if (this._requests.length < 2 && data.length > 0 && + !this._conn.paused) { + var body = this._buildBody(); + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + if (data[i] === "restart") { body.attrs({ - to: this.domain, + to: this._conn.domain, "xml:lang": "en", "xmpp:restart": "true", "xmlns:xmpp": Strophe.NS.BOSH }); } else { - body.cnode(this._data[i]).up(); + body.cnode(data[i]).up(); } } } - delete this._data; - this._data = []; + delete this._conn._data; + this._conn._data = []; this._requests.push( new Strophe.Request(body.tree(), this._onRequestStateChange.bind( - this, this._dataRecv.bind(this)), + this, this._conn._dataRecv.bind(this._conn)), body.tree().getAttribute("rid"))); this._processRequest(this._requests.length - 1); } if (this._requests.length > 0) { - time_elapsed = this._requests[0].age(); + var time_elapsed = this._requests[0].age(); if (this._requests[0].dead !== null) { if (this._requests[0].timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { @@ -4192,24 +4254,899 @@ Strophe.Connection.prototype = { this._throttledRequestHandler(); } } + }, - clearTimeout(this._idleTimeout); + /** PrivateFunction: _onRequestStateChange + * _Private_ handler for Strophe.Request state changes. + * + * This function is called when the XMLHttpRequest readyState changes. + * It contains a lot of error handling logic for the many ways that + * requests can fail, and calls the request callback when requests + * succeed. + * + * Parameters: + * (Function) func - The handler for the request. + * (Strophe.Request) req - The request that is changing readyState. + */ + _onRequestStateChange: function (func, req) + { + Strophe.debug("request id " + req.id + + "." + req.sends + " state changed to " + + req.xhr.readyState); - // reactivate the timer only if connected - if (this.connected) { - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + if (req.abort) { + req.abort = false; + return; + } + + if(req.xhr.readyState == 2){ + jQuery(document).trigger('ridChange', {rid: Number(req.rid)+1}); + } + + // request complete + var reqStatus; + if (req.xhr.readyState == 4) { + reqStatus = 0; + try { + reqStatus = req.xhr.status; + } catch (e) { + // ignore errors from undefined status attribute. works + // around a browser bug + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = 0; + } + + if (this.disconnecting) { + if (reqStatus >= 400) { + this._hitError(reqStatus); + return; + } + } + + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); + + if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { + // remove from internal queue + this._removeRequest(req); + Strophe.debug("request id " + + req.id + + " should now be removed"); + } + + // request succeeded + if (reqStatus == 200) { + // if request 1 finished, or request 0 finished and request + // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to + // restart the other - both will be in the first spot, as the + // completed request has been removed from the queue already + if (reqIs1 || + (reqIs0 && this._requests.length > 0 && + this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { + this._restartRequest(0); + } + // call handler + Strophe.debug("request id " + + req.id + "." + + req.sends + " got 200"); + func(req); + this.errors = 0; + } else { + Strophe.error("request id " + + req.id + "." + + req.sends + " error " + reqStatus + + " happened"); + if (reqStatus === 0 || + (reqStatus >= 400 && reqStatus < 600) || + reqStatus >= 12000) { + this._hitError(reqStatus); + if (reqStatus >= 400 && reqStatus < 500) { + this._conn._changeConnectStatus(Strophe.Status.DISCONNECTING, + null); + this._conn._doDisconnect(); + } + } + } + + if (!((reqStatus > 0 && reqStatus < 500) || + req.sends > 5)) { + this._throttledRequestHandler(); + } + } + }, + + /** PrivateFunction: _processRequest + * _Private_ function to process a request in the queue. + * + * This function takes requests off the queue and sends them and + * restarts dead requests. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _processRequest: function (i) + { + var self = this; + var req = this._requests[i]; + var reqStatus = -1; + + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; + } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; + } + + // make sure we limit the number of retries + if (req.sends > this.maxRetries) { + this._onDisconnectTimeout(); + return; + } + + var time_elapsed = req.age(); + var primaryTimeout = (!isNaN(time_elapsed) && + time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); + var secondaryTimeout = (req.dead !== null && + req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); + var requestCompletedWithServerError = (req.xhr.readyState == 4 && + (reqStatus < 1 || + reqStatus >= 500)); + if (primaryTimeout || secondaryTimeout || + requestCompletedWithServerError) { + if (secondaryTimeout) { + Strophe.error("Request " + + this._requests[i].id + + " timed out (secondary), restarting"); + } + req.abort = true; + req.xhr.abort(); + // setting to null fails on IE6, so set to empty function + req.xhr.onreadystatechange = function () {}; + this._requests[i] = new Strophe.Request(req.xmlData, + req.origFunc, + req.rid, + req.sends); + req = this._requests[i]; + } + + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + + "." + req.sends + " posting"); + + try { + req.xhr.open("POST", this._conn.service, this._conn.options.sync ? false : true); + } catch (e2) { + Strophe.error("XHR open failed."); + if (!this._conn.connected) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, + "bad-service"); + } + this._conn.disconnect(); + return; + } + + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.date = new Date(); + if (self._conn.options.customHeaders){ + var headers = self._conn.options.customHeaders; + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + req.xhr.setRequestHeader(header, headers[header]); + } + } + } + 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 creates a nicely + // expanding retry window + var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), + Math.pow(req.sends, 3)) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); + } + + req.sends++; + + if (this._conn.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { + if (req.xmlData.nodeName === this.strip && req.xmlData.childNodes.length) { + this._conn.xmlOutput(req.xmlData.childNodes[0]); + } else { + this._conn.xmlOutput(req.xmlData); + } + } + if (this._conn.rawOutput !== Strophe.Connection.prototype.rawOutput) { + this._conn.rawOutput(req.data); + } + } else { + Strophe.debug("_processRequest: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); + } + }, + + /** PrivateFunction: _removeRequest + * _Private_ function to remove a request from the queue. + * + * Parameters: + * (Strophe.Request) req - The request to remove. + */ + _removeRequest: function (req) + { + Strophe.debug("removing request"); + + var i; + for (i = this._requests.length - 1; i >= 0; i--) { + if (req == this._requests[i]) { + this._requests.splice(i, 1); + } + } + + // IE6 fails on setting to null, so set to empty function + req.xhr.onreadystatechange = function () {}; + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _restartRequest + * _Private_ function to restart a request that is presumed dead. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _restartRequest: function (i) + { + var req = this._requests[i]; + if (req.dead === null) { + req.dead = new Date(); + } + + this._processRequest(i); + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * Tries to extract a stanza out of a Request Object. + * When this fails the current connection will be disconnected. + * + * Parameters: + * (Object) req - The Request. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (req) + { + try { + return req.getResponse(); + } catch (e) { + if (e != "parsererror") { throw e; } + this._conn.disconnect("strophe-parsererror"); + } + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function (pres) + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + if (pres) { + body.cnode(pres.tree()); + } + + var req = new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._conn._dataRecv.bind(this._conn)), + body.tree().getAttribute("rid")); + + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for BOSH + * + * Just triggers the RequestHandler to send the messages that are in the queue + */ + _send: function () { + clearTimeout(this._conn._idleTimeout); + this._throttledRequestHandler(); + this._conn._idleTimeout = setTimeout(this._conn._onIdle.bind(this._conn), 100); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._throttledRequestHandler(); + clearTimeout(this._conn._idleTimeout); + }, + + /** PrivateFunction: _throttledRequestHandler + * _Private_ function to throttle requests to the connection window. + * + * This function makes sure we don't send requests so fast that the + * request ids overflow the connection window in the case that one + * request died. + */ + _throttledRequestHandler: function () + { + if (!this._requests) { + Strophe.debug("_throttledRequestHandler called with " + + "undefined requests"); + } else { + Strophe.debug("_throttledRequestHandler called with " + + this._requests.length + " requests"); + } + + if (!this._requests || this._requests.length === 0) { + return; + } + + if (this._requests.length > 0) { + this._processRequest(0); + } + + if (this._requests.length > 1 && + Math.abs(this._requests[0].rid - + this._requests[1].rid) < this.window) { + this._processRequest(1); } } }; -if (callback) { - callback(Strophe, $build, $msg, $iq, $pres); -} +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. -})(function () { - window.Strophe = arguments[0]; - window.$build = arguments[1]; - window.$msg = arguments[2]; - window.$iq = arguments[3]; - window.$pres = arguments[4]; -});
\ No newline at end of file + Copyright 2006-2008, OGG, LLC +*/ + +/* jshint undef: true, unused: true:, noarg: true, latedef: true */ +/*global document, window, clearTimeout, WebSocket, + DOMParser, Strophe, $build */ + +/** Class: Strophe.WebSocket + * _Private_ helper class that handles WebSocket Connections + * + * The Strophe.WebSocket class is used internally by Strophe.Connection + * to encapsulate WebSocket sessions. It is not meant to be used from user's code. + */ + +/** File: websocket.js + * A JavaScript library to enable XMPP over Websocket in Strophejs. + * + * This file implements XMPP over WebSockets for Strophejs. + * If a Connection is established with a Websocket url (ws://...) + * Strophe will use WebSockets. + * For more information on XMPP-over WebSocket see this RFC draft: + * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 + * + * WebSocket support implemented by Andreas Guth (andreas.guth@rwth-aachen.de) + */ + +/** PrivateConstructor: Strophe.Websocket + * Create and initialize a Strophe.WebSocket object. + * Currently only sets the connection Object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets. + * + * Returns: + * A new Strophe.WebSocket object. + */ +Strophe.Websocket = function(connection) { + this._conn = connection; + this.strip = "stream:stream"; + + var service = connection.service; + if (service.indexOf("ws:") !== 0 && service.indexOf("wss:") !== 0) { + // If the service is not an absolute URL, assume it is a path and put the absolute + // URL together from options, current URL and the path. + var new_service = ""; + + if (connection.options.protocol === "ws" && window.location.protocol !== "https:") { + new_service += "ws"; + } else { + new_service += "wss"; + } + + new_service += "://" + window.location.host; + + if (service.indexOf("/") !== 0) { + new_service += window.location.pathname + service; + } else { + new_service += service; + } + + connection.service = new_service; + } +}; + +Strophe.Websocket.prototype = { + /** PrivateFunction: _buildStream + * _Private_ helper function to generate the <stream> start tag for WebSockets + * + * Returns: + * A Strophe.Builder with a <stream> element. + */ + _buildStream: function () + { + return $build("stream:stream", { + "to": this._conn.domain, + "xmlns": Strophe.NS.CLIENT, + "xmlns:stream": Strophe.NS.STREAM, + "version": '1.0' + }); + }, + + /** PrivateFunction: _check_streamerror + * _Private_ checks a message for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + * connectstatus - The ConnectStatus that will be set on error. + * Returns: + * true if there was a streamerror, false otherwise. + */ + _check_streamerror: function (bodyWrap, connectstatus) { + var errors = bodyWrap.getElementsByTagName("stream:error"); + if (errors.length === 0) { + return false; + } + var error = errors[0]; + + var condition = ""; + var text = ""; + + var ns = "urn:ietf:params:xml:ns:xmpp-streams"; + for (var i = 0; i < error.childNodes.length; i++) { + var e = error.childNodes[i]; + if (e.getAttribute("xmlns") !== ns) { + break; + } if (e.nodeName === "text") { + text = e.textContent; + } else { + condition = e.nodeName; + } + } + + var errorString = "WebSocket stream error: "; + + if (condition) { + errorString += condition; + } else { + errorString += "unknown"; + } + + if (text) { + errorString += " - " + condition; + } + + Strophe.error(errorString); + + // close the connection on stream_error + this._conn._changeConnectStatus(connectstatus, condition); + this._conn._doDisconnect(); + return true; + }, + + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection. + * Is not needed by WebSockets. + */ + _reset: function () + { + return; + }, + + /** PrivateFunction: _connect + * _Private_ function called by Strophe.Connection.connect + * + * Creates a WebSocket for a connection and assigns Callbacks to it. + * Does nothing if there already is a WebSocket. + */ + _connect: function () { + // Ensure that there is no open WebSocket from a previous Connection. + this._closeSocket(); + + // Create the new WobSocket + this.socket = new WebSocket(this._conn.service, "xmpp"); + this.socket.onopen = this._onOpen.bind(this); + this.socket.onerror = this._onError.bind(this); + this.socket.onclose = this._onClose.bind(this); + this.socket.onmessage = this._connect_cb_wrapper.bind(this); + }, + + /** PrivateFunction: _connect_cb + * _Private_ function called by Strophe.Connection._connect_cb + * + * checks for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function(bodyWrap) { + var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); + if (error) { + return Strophe.Status.CONNFAIL; + } + }, + + /** PrivateFunction: _handleStreamStart + * _Private_ function that checks the opening stream:stream tag for errors. + * + * Disconnects if there is an error and returns false, true otherwise. + * + * Parameters: + * (Node) message - Stanza containing the stream:stream. + */ + _handleStreamStart: function(message) { + var error = false; + // Check for errors in the stream:stream tag + var ns = message.getAttribute("xmlns"); + if (typeof ns !== "string") { + error = "Missing xmlns in stream:stream"; + } else if (ns !== Strophe.NS.CLIENT) { + error = "Wrong xmlns in stream:stream: " + ns; + } + + var ns_stream = message.namespaceURI; + if (typeof ns_stream !== "string") { + error = "Missing xmlns:stream in stream:stream"; + } else if (ns_stream !== Strophe.NS.STREAM) { + error = "Wrong xmlns:stream in stream:stream: " + ns_stream; + } + + var ver = message.getAttribute("version"); + if (typeof ver !== "string") { + error = "Missing version in stream:stream"; + } else if (ver !== "1.0") { + error = "Wrong version in stream:stream: " + ver; + } + + if (error) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, error); + this._conn._doDisconnect(); + return false; + } + + return true; + }, + + /** PrivateFunction: _connect_cb_wrapper + * _Private_ function that handles the first connection messages. + * + * On receiving an opening stream tag this callback replaces itself with the real + * message handler. On receiving a stream error the connection is terminated. + */ + _connect_cb_wrapper: function(message) { + if (message.data.indexOf("<stream:stream ") === 0 || message.data.indexOf("<?xml") === 0) { + // Strip the XML Declaration, if there is one + var data = message.data.replace(/^(<\?.*?\?>\s*)*/, ""); + if (data === '') return; + + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + data = message.data.replace(/<stream:stream (.*[^\/])>/, "<stream:stream $1/>"); + + var streamStart = new DOMParser().parseFromString(data, "text/xml").documentElement; + this._conn.xmlInput(streamStart); + this._conn.rawInput(message.data); + + //_handleStreamSteart will check for XML errors and disconnect on error + if (this._handleStreamStart(streamStart)) { + + //_connect_cb will check for stream:error and disconnect on error + this._connect_cb(streamStart); + + // ensure received stream:stream is NOT selfclosing and save it for following messages + this.streamStart = message.data.replace(/^<stream:(.*)\/>$/, "<stream:$1>"); + } + } else if (message.data === "</stream:stream>") { + this._conn.rawInput(message.data); + this._conn.xmlInput(document.createElement("stream:stream")); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Received closing stream"); + this._conn._doDisconnect(); + return; + } else { + var string = this._streamWrap(message.data); + var elem = new DOMParser().parseFromString(string, "text/xml").documentElement; + this.socket.onmessage = this._onMessage.bind(this); + this._conn._connect_cb(elem, null, message.data); + } + }, + + /** PrivateFunction: _disconnect + * _Private_ function called by Strophe.Connection.disconnect + * + * Disconnects and sends a last stanza if one is given + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + if (this.socket.readyState !== WebSocket.CLOSED) { + if (pres) { + this._conn.send(pres); + } + var close = '</stream:stream>'; + this._conn.xmlOutput(document.createElement("stream:stream")); + this._conn.rawOutput(close); + try { + this.socket.send(close); + } catch (e) { + Strophe.info("Couldn't send closing stream tag."); + } + } + + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Just closes the Socket for WebSockets + */ + _doDisconnect: function () + { + Strophe.info("WebSockets _doDisconnect was called"); + this._closeSocket(); + }, + + /** PrivateFunction _streamWrap + * _Private_ helper function to wrap a stanza in a <stream> tag. + * This is used so Strophe can process stanzas from WebSockets like BOSH + */ + _streamWrap: function (stanza) + { + return this.streamStart + stanza + '</stream:stream>'; + }, + + + /** PrivateFunction: _closeSocket + * _Private_ function to close the WebSocket. + * + * Closes the socket if it is still open and deletes it + */ + _closeSocket: function () + { + if (this.socket) { try { + this.socket.close(); + } catch (e) {} } + this.socket = null; + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the message queue is empty. + * + * Returns: + * True, because WebSocket messages are send immediately after queueing. + */ + _emptyQueue: function () + { + return true; + }, + + /** PrivateFunction: _onClose + * _Private_ function to handle websockets closing. + * + * Nothing to do here for WebSockets + */ + _onClose: function() { + if(this._conn.connected && !this._conn.disconnecting) { + Strophe.error("Websocket closed unexcectedly"); + this._conn._doDisconnect(); + } else { + Strophe.info("Websocket closed"); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received. + */ + _no_auth_received: function (_callback) + { + Strophe.error("Server did not send any auth methods"); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods"); + if (_callback) { + _callback = _callback.bind(this._conn); + _callback(); + } + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * This does nothing for WebSockets + */ + _onDisconnectTimeout: function () {}, + + /** PrivateFunction: _onError + * _Private_ function to handle websockets errors. + * + * Parameters: + * (Object) error - The websocket error. + */ + _onError: function(error) { + Strophe.error("Websocket error " + error); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established was disconnected."); + this._disconnect(); + }, + + /** PrivateFunction: _onIdle + * _Private_ function called by Strophe.Connection._onIdle + * + * sends all queued stanzas + */ + _onIdle: function () { + var data = this._conn._data; + if (data.length > 0 && !this._conn.paused) { + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + var stanza, rawStanza; + if (data[i] === "restart") { + stanza = this._buildStream(); + rawStanza = this._removeClosingTag(stanza); + stanza = stanza.tree(); + } else { + stanza = data[i]; + rawStanza = Strophe.serialize(stanza); + } + this._conn.xmlOutput(stanza); + this._conn.rawOutput(rawStanza); + this.socket.send(rawStanza); + } + } + this._conn._data = []; + } + }, + + /** PrivateFunction: _onMessage + * _Private_ function to handle websockets messages. + * + * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. + * + * Since all XMPP traffic starts with "<stream:stream version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='3697395463' from='SERVER'>" + * The first stanza will always fail to be parsed... + * Addtionnaly, the seconds stanza will always be a <stream:features> with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! + * + * Parameters: + * (string) message - The websocket message. + */ + _onMessage: function(message) { + var elem, data; + // check for closing stream + if (message.data === "</stream:stream>") { + var close = "</stream:stream>"; + this._conn.rawInput(close); + this._conn.xmlInput(document.createElement("stream:stream")); + if (!this._conn.disconnecting) { + this._conn._doDisconnect(); + } + return; + } else if (message.data.search("<stream:stream ") === 0) { + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + data = message.data.replace(/<stream:stream (.*[^\/])>/, "<stream:stream $1/>"); + elem = new DOMParser().parseFromString(data, "text/xml").documentElement; + + if (!this._handleStreamStart(elem)) { + return; + } + } else { + data = this._streamWrap(message.data); + elem = new DOMParser().parseFromString(data, "text/xml").documentElement; + } + + if (this._check_streamerror(elem, Strophe.Status.ERROR)) { + return; + } + + //handle unavailable presence stanza before disconnecting + if (this._conn.disconnecting && + elem.firstChild.nodeName === "presence" && + elem.firstChild.getAttribute("type") === "unavailable") { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + // if we are already disconnecting we will ignore the unavailable stanza and + // wait for the </stream:stream> tag before we close the connection + return; + } + this._conn._dataRecv(elem, message.data); + }, + + /** PrivateFunction: _onOpen + * _Private_ function to handle websockets connection setup. + * + * The opening stream tag is sent here. + */ + _onOpen: function() { + Strophe.info("Websocket open"); + var start = this._buildStream(); + this._conn.xmlOutput(start.tree()); + + var startString = this._removeClosingTag(start); + this._conn.rawOutput(startString); + this.socket.send(startString); + }, + + /** PrivateFunction: _removeClosingTag + * _Private_ function to Make the first <stream:stream> non-selfclosing + * + * Parameters: + * (Object) elem - The <stream:stream> tag. + * + * Returns: + * The stream:stream tag as String + */ + _removeClosingTag: function(elem) { + var string = Strophe.serialize(elem); + string = string.replace(/<(stream:stream .*[^\/])\/>$/, "<$1>"); + return string; + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * WebSockets don't use requests, so the passed argument is just returned. + * + * Parameters: + * (Object) stanza - The stanza. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (stanza) + { + return stanza; + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for WebSocket + * + * Just flushes the messages that are in the queue + */ + _send: function () { + this._conn.flush(); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + clearTimeout(this._conn._idleTimeout); + this._conn._onIdle.bind(this._conn)(); + } +}; diff --git a/build/js/ojsxc.js b/build/js/ojsxc.js index 63d6b47..90f68ef 100644 --- a/build/js/ojsxc.js +++ b/build/js/ojsxc.js @@ -1,5 +1,5 @@ /** - * ojsxc v0.5.1 - 2014-01-27 + * ojsxc v0.5.2 - 2014-01-28 * * Copyright (c) 2014 Klaus Herberth <klaus@jsxc.org> <br> * Released under the MIT license @@ -7,7 +7,7 @@ * Please see http://jsxc.org/ * * @author Klaus Herberth <klaus@jsxc.org> - * @version 0.5.1 + * @version 0.5.2 */ /* global jsxc, oc_appswebroots, OC, $ */ @@ -89,15 +89,6 @@ $(function() { }, logoutElement: $('#logout'), checkFlash: false, - debug: function(msg, data) { - if (data) { - console.log(msg, data); - jsxc.log = jsxc.log + msg + ' >> ' + $("<span>").prepend(data).html() + '\n'; - } else { - console.log(msg); - jsxc.log = jsxc.log + msg + '\n'; - } - }, rosterAppend: 'body', root: oc_appswebroots.ojsxc, // @TODO: don't include get turn credentials routine into jsxc diff --git a/css/jsxc.oc.css b/css/jsxc.oc.css index 620bea3..4461ad1 100644 --- a/css/jsxc.oc.css +++ b/css/jsxc.oc.css @@ -583,4 +583,8 @@ div.jsxc_transfer.jsxc_enc.jsxc_trust { background-image: url('%appswebroot%/ojsxc/img/fail-icon.png'); color: #800000; border-color: #800000; +} + +.jsxc_log{ + width: 500px; }
\ No newline at end of file diff --git a/js/jsxc b/js/jsxc -Subproject bc3ec65a50d03bbe4f86c295712bd414c3901d0 +Subproject 32514ea25d919d5c1ea0022d635d91defc5bf40 diff --git a/js/ojsxc.js b/js/ojsxc.js index 125078f..7fff95d 100644 --- a/js/ojsxc.js +++ b/js/ojsxc.js @@ -77,15 +77,6 @@ $(function() { }, logoutElement: $('#logout'), checkFlash: false, - debug: function(msg, data) { - if (data) { - console.log(msg, data); - jsxc.log = jsxc.log + msg + ' >> ' + $("<span>").prepend(data).html() + '\n'; - } else { - console.log(msg); - jsxc.log = jsxc.log + msg + '\n'; - } - }, rosterAppend: 'body', root: oc_appswebroots.ojsxc, // @TODO: don't include get turn credentials routine into jsxc |