view 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 source

'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