Skip to content

Instantly share code, notes, and snippets.

@defsprite
Forked from processone/strophe.websocket.js
Created March 7, 2011 19:05
Show Gist options
  • Save defsprite/858997 to your computer and use it in GitHub Desktop.
Save defsprite/858997 to your computer and use it in GitHub Desktop.
a (w)hacky strophe websockets connection implementation
/** Class: Strophe.WebSocket
* XMPP Connection manager.
*
* Thie 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.
*
* After creating a Strophe.Connection object, the user will typically
* call connect() with a user supplied callback to handle connection level
* events like authentication failure, disconnection, or connection
* complete.
*
* The user will also have several event handlers defined by using
* addHandler() and addTimedHandler(). These will allow the user code to
* respond to interesting stanzas or do something periodically with the
* connection. These handlers will be active once authentication is
* finished.
*
* To send data to the connection, use send().
*/
/** Constructor: Strophe.Connection
* Create and initialize a Strophe.Connection object.
*
* Parameters:
* (String) service - The WebSocket service URL.
*
* Returns:
* A new Strophe.WebSocket object.
*/
Strophe.WebSocket = function (service)
{
/* The websocket url. */
this.service = service;
this.ws = null;
this.connect_timeout = 300;
this.buffered_data = "";
/* The connected JID. */
this.jid = "";
/* The current stream ID. */
this.streamId = null;
// SASL
this.do_session = false;
this.do_bind = false;
// handler lists
this.timedHandlers = [];
this.handlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
this._idleTimeout = null;
this._disconnectTimeout = null;
this.authenticated = false;
this.disconnecting = false;
this.connected = false;
this._keep_alive_timer = 20000
this.errors = 0;
this._data = [];
this._requests = [];
this._uniqueId = Math.round(Math.random() * 10000);
this._sasl_success_handler = null;
this._sasl_failure_handler = null;
this._sasl_challenge_handler = null;
this._rebind_success_handler = null;
this._rebind_failure_handler = null;
// setup onIdle callback every 1/10th of a second
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
// initialize plugins
for (var k in Strophe._connectionPlugins) {
if (Strophe._connectionPlugins.hasOwnProperty(k)) {
var ptype = Strophe._connectionPlugins[k];
// jslint complaints about the below line, but this is fine
var F = function () {};
F.prototype = ptype;
this[k] = new F();
this[k].init(this);
}
}
};
Strophe.WebSocket.prototype = {
/** Function: reset
* Reset the connection.
*
* This function should be called after a connection is disconnected
* before that connection is reused.
*/
reset: function ()
{
this.sid = null;
this.streamId = null;
// SASL
this.do_session = false;
this.do_bind = false;
// handler lists
this.timedHandlers = [];
this.handlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
this.authenticated = false;
this.disconnecting = false;
this.connected = false;
this.errors = 0;
},
/** Function: pause
* UNUSED with websockets
*/
pause: function ()
{
return;
},
/** Function: resume
* UNUSED with websockets
*/
resume: function ()
{
return;
},
/** Function: getUniqueId
* Generate a unique ID for use in <iq/> elements.
*
* All <iq/> stanzas are required to have unique id attributes. This
* function makes creating these easy. Each connection instance has
* a counter which starts from zero, and the value of this counter
* plus a colon followed by the suffix becomes the unique id. If no
* suffix is supplied, the counter is used as the unique id.
*
* Suffixes are used to make debugging easier when reading the stream
* data, and their use is recommended. The counter resets to 0 for
* every new connection for the same reason. For connections to the
* same server that authenticate the same way, all the ids should be
* the same, which makes it easy to see changes. This is useful for
* automated testing as well.
*
* Parameters:
* (String) suffix - A optional suffix to append to the id.
*
* Returns:
* A unique string to be used for the id attribute.
*/
getUniqueId: function (suffix)
{
if (typeof(suffix) == "string" || typeof(suffix) == "number") {
return ++this._uniqueId + ":" + suffix;
} else {
return ++this._uniqueId + "";
}
},
/** Function: connect
* Starts the connection process.
*
* As the connection process proceeds, the user supplied callback will
* be triggered multiple times with status updates. The callback
* should take two arguments - the status code and the error condition.
*
* The status code will be one of the values in the Strophe.Status
* constants. The error condition will be one of the conditions
* defined in RFC 3920 or the condition 'strophe-parsererror'.
*
* Please see XEP 124 for a more detailed explanation of the optional
* parameters below.
*
* Parameters:
* (String) jid - The user's JID. This may be a bare JID,
* or a full JID. If a node is not supplied, SASL ANONYMOUS
* authentication will be attempted.
* (String) pass - The user's password.
* (Function) callback The connect callback function.
*/
connect: function (jid, pass, callback)
{
this.jid = jid;
this.pass = pass;
this.connect_callback = callback;
this.disconnecting = false;
this.connected = false;
this.authenticated = false;
this.errors = 0;
// parse jid for domain and resource
this.domain = Strophe.getDomainFromJid(this.jid);
if(window.WebSocket){
try{
this.ws = new WebSocket(this.service);
this.ws.onopen = this._send_initial_stream.bind(this);
this._addSysTimedHandler(this._keep_alive_timer, this._keep_alive_handler.bind(this));
this.ws.onmessage = this._get_stream_id.bind(this)
.prependArg(this._connect_cb.bind(this));
this.ws.onerror = function(e){console.log("error : " + e)};
this.ws.onclose = this._ws_on_close.bind(this);
} catch(e){
//console.log("exception "+e);
}
} else{
throw "no websocket support"
}
},
/** Function: attach
* UNUSED, use rebind
*/
attach: function(){return},
rebind: function (jid, sid, callback)
{
this.jid = jid;
this.connect_callback = callback;
this.domain = Strophe.getDomainFromJid(this.jid);
this._addSysTimedHandler(this._keep_alive_timer, this._keep_alive_handler.bind(this));
this.ws = new WebSocket(this.service);
this.ws.onopen = this._send_initial_stream.bind(this);
this.ws.onmessage = this._get_stream_id.bind(this)
.prependArg(this._rebind_cb.bind(this)
.prependArg(jid).prependArg(sid));
this.ws.onclose = this._ws_on_close.bind(this);
},
save: function(success, failure){
var push = $iq({type: "set"}).c("push", {xmlns: "p1:push"})
.c("keepalive", {max: "30"})
.up()
.c("session", {duration:"1"});
this.sendIQ(push, success,failure );
},
/** Function: xmlInput
* User overrideable function that receives XML data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.xmlInput = function (elem) {
* > (user code)
* > };
*
* Parameters:
* (XMLElement) elem - The XML data received by the connection.
*/
xmlInput: function (elem)
{
return;
},
/** Function: xmlOutput
* User overrideable function that receives XML data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.xmlOutput = function (elem) {
* > (user code)
* > };
*
* Parameters:
* (XMLElement) elem - The XMLdata sent by the connection.
*/
xmlOutput: function (elem)
{
return;
},
/** Function: rawInput
* User overrideable function that receives raw data coming into the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.rawInput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data received by the connection.
*/
rawInput: function (data)
{
return;
},
/** Function: rawOutput
* User overrideable function that receives raw data sent to the
* connection.
*
* The default function does nothing. User code can override this with
* > Strophe.Connection.rawOutput = function (data) {
* > (user code)
* > };
*
* Parameters:
* (String) data - The data sent by the connection.
*/
rawOutput: function (data)
{
return;
},
/** Function: send
* Send a stanza.
*
* This function is called to push data onto the send queue to
* go out over the wire. Whenever a request is sent to the BOSH
* server, all pending data is sent and the queue is flushed.
*
* Parameters:
* (XMLElement |
* [XMLElement] |
* Strophe.Builder) elem - The stanza to send.
*/
send: function (elem)
{
var toSend = "";
if (elem === null) {return ;}
if (typeof(elem.sort) === "function") {
for (var i = 0; i < elem.length; i++) {
toSend += Strophe.serialize(elem[i]);
this.xmlOutput(elem);
}
} else if (typeof(elem.tree) === "function") {
toSend = Strophe.serialize(elem.tree());
this.xmlOutput(elem.tree());
} else {
toSend = Strophe.serialize(elem);
this.xmlOutput(elem);
}
this.rawOutput(toSend);
this.ws.send(toSend);
},
/** Function: flush
* UNUSED
*/
flush: function ()
{
return
},
/** Function: sendIQ
* Helper function to send IQ stanzas.
*
* Parameters:
* (XMLElement) elem - The stanza to send.
* (Function) callback - The callback function for a successful request.
* (Function) errback - The callback function for a failed or timed
* out request. On timeout, the stanza will be null.
* (Integer) timeout - The time specified in milliseconds for a
* timeout to occur.
*
* Returns:
* The id used to send the IQ.
*/
sendIQ: function(elem, callback, errback, timeout) {
var timeoutHandler = null;
var that = this;
if (typeof(elem.tree) === "function") {
elem = elem.tree();
}
var id = elem.getAttribute('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
if (timeoutHandler) {
that.deleteTimedHandler(timeoutHandler);
}
var iqtype = stanza.getAttribute('type');
if (iqtype == 'result') {
if (callback) {callback(stanza);}
} else if (iqtype == 'error') {
if (errback) {errback(stanza);}
} else {
throw {
name: "StropheError",
message: "Got bad IQ type of " + iqtype
};
}
}, null, 'iq', null, id);
// 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
if (errback) {
errback(null);
}
return false;
});
}
this.send(elem);
return id;
},
/** Function: addTimedHandler
* Add a timed handler to the connection.
*
* This function adds a timed handler. The provided handler will
* be called every period milliseconds until it returns false,
* the connection is terminated, or the handler is removed. Handlers
* that wish to continue being invoked should return true.
*
* Because of method binding it is necessary to save the result of
* this function if you wish to remove a handler with
* deleteTimedHandler().
*
* Note that user handlers are not active until authentication is
* successful.
*
* Parameters:
* (Integer) period - The period of the handler.
* (Function) handler - The callback function.
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addTimedHandler: function (period, handler)
{
var thand = new Strophe.TimedHandler(period, handler);
this.addTimeds.push(thand);
return thand;
},
/** Function: deleteTimedHandler
* Delete a timed handler for a connection.
*
* This function removes a timed handler from the connection. The
* handRef parameter is *not* the function passed to addTimedHandler(),
* but is the reference returned from addTimedHandler().
*
* Parameters:
* (Strophe.TimedHandler) handRef - The handler reference.
*/
deleteTimedHandler: function (handRef)
{
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeTimeds.push(handRef);
},
/** Function: addHandler
* Add a stanza handler for the connection.
*
* This function adds a stanza handler to the connection. The
* handler callback will be called for any stanza that matches
* the parameters. Note that if multiple parameters are supplied,
* they must all match for the handler to be invoked.
*
* The handler will receive the stanza that triggered it as its argument.
* The handler should return true if it is to be invoked again;
* returning false will remove the handler after it returns.
*
* As a convenience, the ns parameters applies to the top level element
* and also any of its immediate children. This is primarily to make
* matching /iq/query elements easy.
*
* The options argument contains handler matching flags that affect how
* matches are determined. Currently the only flag is matchBare (a
* boolean). When matchBare is true, the from parameter and the from
* attribute on the stanza will be matched as bare JIDs instead of
* full JIDs. To use this, pass {matchBare: true} as the value of
* options. The default value for matchBare is false.
*
* The return value should be saved if you wish to remove the handler
* with deleteHandler().
*
* Parameters:
* (Function) handler - The user callback.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String) type - The stanza type attribute to match.
* (String) id - The stanza id attribute to match.
* (String) from - The stanza from attribute to match.
* (String) options - The handler options
*
* Returns:
* A reference to the handler that can be used to remove it.
*/
addHandler: function (handler, ns, name, type, id, from, options)
{
var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
this.addHandlers.push(hand);
return hand;
},
/** Function: deleteHandler
* Delete a stanza handler for a connection.
*
* This function removes a stanza handler from the connection. The
* handRef parameter is *not* the function passed to addHandler(),
* but is the reference returned from addHandler().
*
* Parameters:
* (Strophe.Handler) handRef - The handler reference.
*/
deleteHandler: function (handRef)
{
// this must be done in the Idle loop so that we don't change
// the handlers during iteration
this.removeHandlers.push(handRef);
},
/** Function: disconnect
* Start the graceful disconnection process.
*
* This function starts the disconnection process. This process starts
* by sending unavailable presence and sending BOSH body of type
* terminate. A timeout handler makes sure that disconnection happens
* even if the BOSH server does not respond.
*
* The user supplied connection callback will be notified of the
* progress as this process happens.
*
* Parameters:
* (String) reason - The reason the disconnect is occuring.
*/
disconnect: function (reason)
{
this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
Strophe.info("Disconnect was called because: " + reason);
if (this.connected) {
// setup timeout handler
this._disconnectTimeout = this._addSysTimedHandler(
3000, this._onDisconnectTimeout.bind(this));
this._sendTerminate();
}
},
/** PrivateFunction: _changeConnectStatus
* _Private_ helper function that makes sure plugins and the user's
* callback are notified of connection status changes.
*
* Parameters:
* (Integer) status - the new connection status, one of the values
* in Strophe.Status
* (String) condition - the error condition or null
*/
_changeConnectStatus: function (status, condition)
{
//window.console.log("_changeConnectStatus:" +status);
// notify all plugins listening for status changes
for (var k in Strophe._connectionPlugins) {
if (Strophe._connectionPlugins.hasOwnProperty(k)) {
var plugin = this[k];
if (plugin.statusChanged) {
try {
plugin.statusChanged(status, condition);
} catch (err) {
Strophe.error("" + k + " plugin caused an exception " +
"changing status: " + err);
}
}
}
}
// notify the user's callback
if (this.connect_callback) {
try {
this.connect_callback(status, condition);
} catch (e) {
Strophe.error("User connection callback caused an " +
"exception: " + e);
}
}
},
/** PrivateFunction: _doDisconnect
* _Private_ function to disconnect.
*
* This is the last piece of the disconnection logic. This resets the
* connection and alerts the user's connection callback.
*/
_doDisconnect: function ()
{
Strophe.info("_doDisconnect was called");
this.authenticated = false;
this.disconnecting = false;
this.sid = null;
this.streamId = null;
// tell the parent we disconnected
if (this.connected) {
this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
this.connected = false;
}
// delete handlers
this.handlers = [];
this.timedHandlers = [];
this.removeTimeds = [];
this.removeHandlers = [];
this.addTimeds = [];
this.addHandlers = [];
if(this.ws.readyState != this.ws.CLOSED)
{
this.ws.close();
}
},
_ws_on_close: function(ev){
Strophe.info("websocket closed");
this._doDisconnect();
},
_keep_alive_handler: function(){
this.ws.send("\n");
return true;
},
_send_initial_stream: function(){
//window.console.log("_send_initial_stream ");
this._changeConnectStatus(Strophe.Status.CONNECTING, null);
var stream = '<?xml version="1.0"?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" version="1.0" xmlns="jabber:client" to="'+ this.domain + '" xml:lang="en" xmlns:xml="http://www.w3.org/XML/1998/namespace" >'
this.rawOutput(stream);
this.ws.send(stream);
},
_get_stream_id: function(onmessage,event){
elem = event.data
this.rawInput(elem);
if (event.data.match(/id=[\'\"]([^\'\"]+)[\'\"]/))
this.streamId = RegExp.$1;
//window.console.log("_get_stream_id: "+this.streamId);
this.ws.onmessage = onmessage;
},
_parseTree: function(elem){
//window.console.log("_parseTree for '"+elem+"'.");
try {
if(this._parser == undefined){
this._parser = new DOMParser();
}
this._parser = new DOMParser();
// Because FF wants valid XML, with correct namespaces !
// node = this._parser.parseFromString("<body xmlns:stream='foo' >" + elem + "</body>", "text/xml").documentElement.firstChild;
//and chrome doesn't
node = this._parser.parseFromString(elem, "text/xml").documentElement;
//window.console.log("Document element is: "+ node.tagName);
if (node && node.tagName == "parsererror")
{
//window.console.log("_parseTree got parseerror when reading: '"+elem+"'.");
return null;
}
return node;
} catch (e) {
//window.console.log("Exception when parsing: "+e);
Strophe.error("Error : " + e)
}
return null;
},
/** PrivateFunction: _dataRecv
* _Private_ handler to processes incoming data from the the connection.
*
* Except for _connect_cb handling the initial connection request,
* this function handles the incoming data for all requests. This
* function also fires stanza handlers that match each incoming
* stanza.
*
* Parameters:
* (Strophe.Request) req - The request that has data ready.
*/
_dataRecv: function (event)
{
//window.console.log("_dataRecv");
if(!this.closingRegEx && event.data.match(/^<(\w+)/)) {
this.closingRegEx = new RegExp("\\</"+RegExp.$1+"\\>");
console.log("regex:" +this.closingRegEx);
}
this.buffered_data += event.data+"";
if (!event.data.match(this.closingRegEx) && !event.data.match(/\/>$/) && !event.data.match(/\<\?xml/) ) {
console.log("buffering: "+event.data);
return;
}
this.closingRegEx = null;
var elem, data = this.buffered_data;
// var elem, data = event.data;
console.log("performing data: "+data);
try {
elem = this._parseTree(data);
} catch (e) {
if (e != "parsererror") {throw e;}
window.console.log("_dataRecv strophe-parsererror");
this.disconnect("strophe-parsererror");
}
if (elem === null) {
this.buffered_data = "";
return;
}
this.xmlInput(elem);
this.rawInput(data);
// remove handlers scheduled for deletion
var i, hand;
while (this.removeHandlers.length > 0) {
hand = this.removeHandlers.pop();
i = this.handlers.indexOf(hand);
if (i >= 0) {
this.handlers.splice(i, 1);
}
}
// add handlers scheduled for addition
while (this.addHandlers.length > 0) {
this.handlers.push(this.addHandlers.pop());
}
// handle graceful disconnect
if (this.disconnecting) {
this.deleteTimedHandler(this._disconnectTimeout);
this._disconnectTimeout = null;
this._doDisconnect();
return;
}
var typ = elem.getAttribute("type");
var cond, conflict;
if (typ !== null && typ == "terminate") {
// an error occurred
cond = elem.getAttribute("condition");
conflict = elem.getElementsByTagName("conflict");
if (cond !== null) {
if (cond == "remote-stream-error" && conflict.length > 0) {
cond = "conflict";
}
this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
} else {
this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
}
this.disconnect();
return;
}
// // send each incoming stanza through the handler chain
// var i, newList;
// // process handlers
// newList = this.handlers;
// this.handlers = [];
// for (i = 0; i < newList.length; i++) {
// var hand = newList[i];
// window.console.log("_dataRecv checking handler: "+ hand.name + " (match: for '"+elem.tagName +"' :"+hand.isMatch(elem));
// if (hand.isMatch(elem) &&
// (this.authenticated || !hand.user)) {
// window.console.log("_dataRecv running handler: "+ hand.name);
// if (hand.run(elem)) {
// this.handlers.push(hand);
// }
// } else {
// this.handlers.push(hand);
// }
// }
//check if the document element needs to be handled
//window.console.log("_dataRev Checking root node for registered handlers");
newList = this.handlers;
this.handlers = [];
for (i = 0; i < newList.length; i++) {
hand = newList[i];
window.console.log("_dataRecv checking handler: "+ hand.name + " (match: for '"+elem.tagName +"' :"+hand.isMatch(elem));
if (hand.isMatch(elem) &&
(this.authenticated || !hand.user)) {
//window.console.log("_dataRecv running handler: "+ hand.name);
if (hand.run(elem)) {
this.handlers.push(hand);
}
} else {
this.handlers.push(hand);
}
}
// send each incoming stanza through the handler chain
var j, childNode;
if (elem.childNodes.length >0)
{
//window.console.log("_dataRev Checking child nodes...");
}
for (j = 0; j < elem.childNodes.length; j++) {
childNode = elem.childNodes[j];
if (childNode.nodeType == Strophe.ElementType.NORMAL) {
var newList;
// process handlers
newList = this.handlers;
this.handlers = [];
for (i = 0; i < newList.length; i++) {
var myHand = newList[i];
//window.console.log("_dataRecv checking handler: "+ myHand.name + " (match: for '"+childNode.tagName +"' :"+myHand.isMatch(childNode));
if (myHand.isMatch(childNode) &&
(this.authenticated || !myHand.user)) {
//window.console.log("_dataRecv running handler: "+ myHand.name);
if (myHand.run(childNode)) {
this.handlers.push(myHand);
}
} else {
this.handlers.push(myHand);
}
}
}
}
this.buffered_data = "";
},
/** 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");
window.console.log("_sendTerminate");
var stanza = {}
if (this.authenticated) {
stanza = $pres({
xmlns: Strophe.NS.CLIENT,
type: 'unavailable'
});
}
this.disconnecting = true;
this.send(stanza);
},
/** PrivateFunction: _connect_cb
* _Private_ handler for initial connection request.
*
* This handler is used to process the initial connection request
* response from the BOSH server. It is used to set up authentication
* handlers and start the authentication process.
*
* SASL authentication will be attempted if available, otherwise
* the code will fall back to legacy authentication.
*
* Parameters:
* (Strophe.Request) req - The current request.
*/
_connect_cb: function (event)
{
//window.console.log("_connect_cb was called");
Strophe.info("_connect_cb was called");
this.connected = true;
this.ws.onmessage=this._dataRecv.bind(this);
var strStanza = event.data;
if (!strStanza) {
//window.console.log("! strStanza");
return;
}
stanza = this._parseTree(strStanza)
this.xmlInput(stanza);
this.rawInput(Strophe.serialize(stanza));
var do_sasl_plain = false;
var do_sasl_digest_md5 = false;
var do_sasl_anonymous = false;
var mechanisms = stanza.getElementsByTagName("mechanism");
var i, mech, auth_str, hashed_auth_str;
if (mechanisms.length > 0) {
for (i = 0; i < mechanisms.length; i++) {
mech = Strophe.getText(mechanisms[i]);
if (mech == 'DIGEST-MD5') {
do_sasl_digest_md5 = true;
} else if (mech == 'PLAIN') {
do_sasl_plain = true;
} else if (mech == 'ANONYMOUS') {
do_sasl_anonymous = true;
}
}
}
if (Strophe.getNodeFromJid(this.jid) === null &&
do_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 (do_sasl_digest_md5) {
//window.console.log("_connect_cb: authenticating digest_md5");
this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
this._sasl_challenge_handler = this._addSysHandler(
this._sasl_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 (do_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());
}
},
_rebind_cb: function (jid, sid, event){
this.connected = true;
this.ws.onmessage=this._dataRecv.bind(this);
var strStanza = event.data;
if (!strStanza) {return;}
stanza = this._parseTree(strStanza)
this.xmlInput(stanza);
this.rawInput(Strophe.serialize(stanza));
var rebinds = stanza.getElementsByTagName("rebind");
if (rebinds.length > 0){
this._changeConnectStatus(Strophe.Status.ATTACHED, null);
this.send($build('rebind',{
xmlns:"p1:rebind"
}).c("jid", {}).t(jid)
.up()
.c("sid", {}).t(sid).tree());
this._rebind_success_handler = this._addSysHandler(
this._rebind_success_cb.bind(this), null,
"rebind", null, null);
this._rebind_failure_handler = this._addSysHandler(
this._rebind_failure_cb.bind(this), null,
"failure", null, null);
} else {
this._changeConnectStatus(Strophe.Status.CONNFAIL,
'x-strophe-rebind-not-supported');
}
},
_rebind_success_cb: function(elem){
Strophe.info("Rebinding succeeded.");
//window.console.log("_rebind_success_cb ");
// remove old handlers
this.authenticated=true;
this.connected=true;
this.deleteHandler(this._rebind_failure_handler);
this._rebind_failure_handler = null;
this._changeConnectStatus(Strophe.Status.ATTACHED, null);
return false;
},
_rebind_failure_cb: function(elem){
Strophe.info("Rebinding failed.");
//window.console.log("_rebind_failed_cb ");
// delete unneeded handlers
if (this._rebind_success_handler) {
this.deleteHandler(this._rebind_success_handler);
this._rebind_success_handler = null;
}
this._changeConnectStatus(Strophe.Status.CONNFAIL, null);
return false;
},
/** PrivateFunction: _sasl_challenge1_cb
* _Private_ handler for DIGEST-MD5 SASL authentication.
*
* Parameters:
* (XMLElement) elem - The challenge stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_challenge1_cb: function (elem)
{
//window.console.log("_sasl_challenge1_cb ");
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);
//window.console.log("_sasl_challenge1_cb "+elem);
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 = "xmpp/" + this.domain;
if (host !== null) {
digest_uri = digest_uri + "/" + host;
}
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_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_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_challenge2_cb: function (elem)
{
//window.console.log("_sasl_challenge2_cb ");
// remove unneeded handlers
this.deleteHandler(this._sasl_success_handler);
this.deleteHandler(this._sasl_failure_handler);
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}).tree());
return false;
},
/** PrivateFunction: _auth1_cb
* _Private_ handler for legacy authentication.
*
* This handler is called in response to the initial <iq type='get'/>
* for legacy authentication. It builds an authentication <iq/> and
* sends it, creating a handler (calling back to _auth2_cb()) to
* handle the result
*
* Parameters:
* (XMLElement) elem - The stanza that triggered the callback.
*
* Returns:
* false to remove the handler.
*/
_auth1_cb: function (elem)
{
// build plaintext auth iq
var iq = $iq({type: "set", id: "_auth_2"})
.c('query', {xmlns: Strophe.NS.AUTH})
.c('username', {}).t(Strophe.getNodeFromJid(this.jid))
.up()
.c('password').t(this.pass);
if (!Strophe.getResourceFromJid(this.jid)) {
// since the user has not supplied a resource, we pick
// a default one here. unlike other auth methods, the server
// cannot do this for us.
this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
}
iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));
this._addSysHandler(this._auth2_cb.bind(this), null,
null, null, "_auth_2");
this.send(iq.tree());
return false;
},
/** PrivateFunction: _sasl_success_cb
* _Private_ handler for succesful SASL authentication.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_success_cb: function (elem)
{
Strophe.info("SASL authentication succeeded.");
//window.console.log("_sasl_success_cb ");
// 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._addSysHandler(this._sasl_auth1_cb.bind(this), null,
"stream:features", null, null);
// We need the new stream_id
// this.ws.onmessage = this._get_stream_id.bind(this).prependArg(this._dataRecv.bind(this));
this._get_stream_id.bind(this).prependArg(this._dataRecv.bind(this));
// we must send an xmpp:restart now
this._send_initial_stream();
return false;
},
/** PrivateFunction: _sasl_auth1_cb
* _Private_ handler to start stream binding.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_auth1_cb: function (elem)
{
var i, child;
//window.console.log("_sasl_auth1_cb");
for (i = 0; i < elem.childNodes.length; i++) {
child = elem.childNodes[i];
if (child.nodeName == 'bind') {
//window.console.log("_sasl_auth1_cb bind");
this.do_bind = true;
}
if (child.nodeName == 'session') {
//window.console.log("_sasl_auth1_cb session");
this.do_session = true;
}
}
if (!this.do_bind) {
//window.console.log("_sasl_auth1_cb auth fail");
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
return false;
} else {
this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
null, "_bind_auth_2");
var resource = Strophe.getResourceFromJid(this.jid);
if (resource) {
this.send($iq({type: "set", id: "_bind_auth_2"})
.c('bind', {xmlns: Strophe.NS.BIND})
.c('resource', {}).t(resource).tree());
} else {
this.send($iq({type: "set", id: "_bind_auth_2"})
.c('bind', {xmlns: Strophe.NS.BIND})
.tree());
}
}
return false;
},
/** PrivateFunction: _sasl_bind_cb
* _Private_ handler for binding result and session start.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_bind_cb: function (elem)
{
//window.console.log("_sasl_bind_cb ");
if (elem.getAttribute("type") == "error") {
Strophe.info("SASL binding failed.");
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
return false;
}
// TODO - need to grab errors
var bind = elem.getElementsByTagName("bind");
var jidNode;
if (bind.length > 0) {
// Grab jid
jidNode = bind[0].getElementsByTagName("jid");
if (jidNode.length > 0) {
this.jid = Strophe.getText(jidNode[0]);
if (this.do_session) {
this._addSysHandler(this._sasl_session_cb.bind(this),
null, null, null, "_session_auth_2");
this.send($iq({type: "set", id: "_session_auth_2"})
.c('session', {xmlns: Strophe.NS.SESSION})
.tree());
} else {
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
}
}
} else {
Strophe.info("SASL binding failed.");
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
return false;
}
},
/** PrivateFunction: _sasl_session_cb
* _Private_ handler to finish successful SASL connection.
*
* This sets Connection.authenticated to true on success, which
* starts the processing of user handlers.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_session_cb: function (elem)
{
//window.console.log("_sasl_session_cb ");
if (elem.getAttribute("type") == "result") {
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
} else if (elem.getAttribute("type") == "error") {
Strophe.info("Session creation failed.");
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
return false;
}
return false;
},
/** PrivateFunction: _sasl_failure_cb
* _Private_ handler for SASL authentication failure.
*
* Parameters:
* (XMLElement) elem - The matching stanza.
*
* Returns:
* false to remove the handler.
*/
_sasl_failure_cb: function (elem)
{
//window.console.log("_sasl_failure_cb ");
// delete unneeded handlers
if (this._sasl_success_handler) {
this.deleteHandler(this._sasl_success_handler);
this._sasl_success_handler = null;
}
if (this._sasl_challenge_handler) {
this.deleteHandler(this._sasl_challenge_handler);
this._sasl_challenge_handler = null;
}
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
return false;
},
/** PrivateFunction: _auth2_cb
* _Private_ handler to finish legacy authentication.
*
* This handler is called when the result from the jabber:iq:auth
* <iq/> stanza is returned.
*
* Parameters:
* (XMLElement) elem - The stanza that triggered the callback.
*
* Returns:
* false to remove the handler.
*/
_auth2_cb: function (elem)
{
//window.console.log("_auth2_cb ");
if (elem.getAttribute("type") == "result") {
this.authenticated = true;
this._changeConnectStatus(Strophe.Status.CONNECTED, null);
} else if (elem.getAttribute("type") == "error") {
this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
this.disconnect();
}
return false;
},
/** PrivateFunction: _addSysTimedHandler
* _Private_ function to add a system level timed handler.
*
* This function is used to add a Strophe.TimedHandler for the
* library code. System timed handlers are allowed to run before
* authentication is complete.
*
* Parameters:
* (Integer) period - The period of the handler.
* (Function) handler - The callback function.
*/
_addSysTimedHandler: function (period, handler)
{
var thand = new Strophe.TimedHandler(period, handler);
thand.user = false;
this.addTimeds.push(thand);
return thand;
},
/** PrivateFunction: _addSysHandler
* _Private_ function to add a system level stanza handler.
*
* This function is used to add a Strophe.Handler for the
* library code. System stanza handlers are allowed to run before
* authentication is complete.
*
* Parameters:
* (Function) handler - The callback function.
* (String) ns - The namespace to match.
* (String) name - The stanza name to match.
* (String) type - The stanza type attribute to match.
* (String) id - The stanza id attribute to match.
*/
_addSysHandler: function (handler, ns, name, type, id)
{
var hand = new Strophe.Handler(handler, ns, name, type, id);
hand.user = false;
this.addHandlers.push(hand);
return hand;
},
/** PrivateFunction: _onDisconnectTimeout
* _Private_ timeout handler for handling non-graceful disconnection.
*
* If the graceful disconnect process does not complete within the
* time allotted, this handler finishes the disconnect anyway.
*
* Returns:
* false to remove the handler.
*/
_onDisconnectTimeout: function ()
{
Strophe.info("_onDisconnectTimeout was called");
// cancel all remaining requests and clear the queue
// actually disconnect
this._doDisconnect();
return false;
},
/** PrivateFunction: _onIdle
* _Private_ handler to process events during idle cycle.
*
* This handler is called every 100ms to fire timed handlers that
* are ready and keep poll requests going.
*/
_onIdle: function ()
{
var i, thand, since, newList;
// remove timed handlers that have been scheduled for deletion
while (this.removeTimeds.length > 0) {
thand = this.removeTimeds.pop();
i = this.timedHandlers.indexOf(thand);
if (i >= 0) {
this.timedHandlers.splice(i, 1);
}
}
// add timed handlers scheduled for addition
while (this.addTimeds.length > 0) {
this.timedHandlers.push(this.addTimeds.pop());
}
// call ready timed handlers
var now = new Date().getTime();
newList = [];
for (i = 0; i < this.timedHandlers.length; i++) {
thand = this.timedHandlers[i];
if (this.authenticated || !thand.user) {
since = thand.lastCalled + thand.period;
if (since - now <= 0) {
if (thand.run()) {
newList.push(thand);
}
} else {
newList.push(thand);
}
}
}
this.timedHandlers = newList;
// reactivate the timer
clearTimeout(this._idleTimeout);
this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment