Mercurial > eldonilo > blog
diff xmpp.js @ 13:161d4ea1c3f8
Migration of the client-side to XMPP.js instead of Strophe.js.
Drop BOSH support and add WebSockets support.
The server-side is untested, may be broken.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Thu, 03 Nov 2011 14:23:10 -0700 |
parents | |
children |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/xmpp.js @@ -0,0 +1,211 @@ +'use strict'; + +function XMPP (aURL) { + var parser = new DOMParser(); + var serializer = new XMLSerializer(); + this.handlers = {}; + this.iqid = 1024; + this.getNewId = function() { + this.iqid++; + return 'sendiq:'+this.iqid; + }; + this.parse = function(str) { + return parser.parseFromString(str, 'text/xml').documentElement; + }; + this.serialize = function(elm) { + return serializer.serializeToString(elm); + }; + this.connect = function(jid, password) { + this.domain = jid.split('@')[1]; + this.node = jid.split('@')[0]; + this.jid = jid; + this.password = password; + if(typeof WebSocket === 'undefined') + this.socket = new MozWebSocket(aURL); + else + this.socket = new WebSocket(aURL); + + var that = this; + this.socket.addEventListener('open', function() { + console.log('socket opened'); + that.send("<stream:stream to='"+that.domain+"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' />"); + }); + this.socket.addEventListener('error', function(err) { + console.log(err); + }); + this.socket.addEventListener('close', function(close) { + console.log(close); + }); + this.socket.addEventListener('message', function(e) { + that.emit('XMLInput', e.data); + var elm = that.parse(e.data); + that.emit('DOMInput', elm); + that.emit(elm.tagName, elm); + + var id = elm.getAttribute('id'); + if((elm.tagName === 'iq') && id) + that.emit(id, elm); + }); + }; + this.send = function(stanza, callback) { + //FIXME support for E4X + //~ if(typeof stanza === 'xml') { + //~ stanza = stanza.toXMLString(); + //~ } + if(stanza.cnode) + stanza = stanza.toString(); + if(typeof stanza === 'string') { + var str = stanza; + var elm = this.parse(stanza); + } + else { + var elm = stanza; + var str = this.serialize(stanza); + } + if(elm.tagName === 'iq') { + var id = elm.getAttribute('id'); + if(!id) { + id = this.getNewId(); + elm.setAttribute('id', id); + str = this.serialize(elm); + } + if(callback) + this.on(id, callback); + } + this.socket.send(str); + this.emit('XMLOutput', str); + this.emit('DOMOutput', elm); + }; + this.disconnect = function() { + this.send('</stream:stream>'); + this.emit('disconnected'); + this.socket.close(); + }; + //FIXME Callbacks sucks, better idea? + this.emit = function(name, data) { + var handlers = this.handlers[name]; + if(!handlers) + return; + + //FIXME Better idea than passing the scope as argument? + for(var i=0; i<handlers.length; i++) + handlers[i](data, this); + + if(name.match('sendiq:')) + delete this.handlers[name]; + }; + this.on = function(name, callback) { + if(!this.handlers[name]) + this.handlers[name] = []; + this.handlers[name].push(callback); + }; + //FIXME do this! + this.once = function(name, callback) { + if(!this.handlers[name]) + this.handlers[name] = []; + this.handlers[name].push(callback); + }; + //Internal + this.on('stream:features', function(stanza, that) { + var nodes = stanza.querySelectorAll('mechanism'); + //SASL/Auth features + if(nodes.length > 0) { + that.emit('mechanisms', stanza); + var mechanisms = {}; + for(var i=0; i<nodes.length; i++) + mechanisms[nodes[i].textContent] = true; + + + //FIXME support SCRAM-SHA1 && allow specify method preferences + if('DIGEST-MD5' in mechanisms) + that.send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>"); + else if('PLAIN' in mechanisms) { + var token = btoa(jid + "\u0000" + jid.split('@')[0] + "\u0000" + password); + that.send("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"+token+"</auth>"); + } + } + //XMPP features + else { + that.emit('features', stanza); + //Bind http://xmpp.org/rfcs/rfc3920.html#bind + that.send("<iq type='set' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>", function() { + //Session http://xmpp.org/rfcs/rfc3921.html#session + that.send("<iq type='set' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>", function() { + that.emit('connected'); + }); + }); + } + }); + //Internal + this.on('success', function(stanza, that) { + that.send("<stream:stream to='"+that.domain+"' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' />"); + }); + //Internal + this.on('challenge', function(stanza, that) { + //FIXME this is mostly Strophe code + + function _quote(str) { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + }; + + var challenge = atob(stanza.textContent); + + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var 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 = "xmpp/" + that.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(that.node + + ":" + realm + ":" + that.password) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username=' + _quote(that.node) + ','; + responseText += 'realm=' + _quote(realm) + ','; + responseText += 'nonce=' + _quote(nonce) + ','; + responseText += 'cnonce=' + _quote(cnonce) + ','; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri=' + _quote(digest_uri) + ','; + responseText += 'response=' + _quote( + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2))) + ','; + responseText += 'charset="utf-8"'; + + that.send("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"+btoa(responseText)+"</response>"); + }); +}; + +// vim: ts=2 et sw=2 sts=2