UNPKG

node-bosh-xmpp-client

Version:

XMPP clients that connects via BOSH

554 lines (484 loc) 13.9 kB
/* node-xmpp-bosh-client [A]: Client() 1. Event-emitter for the following events a: "online" b: "error" c: "offline" d: "stanza" [just for now, may be later split it into seperate presence, iq, message stanza events] 2. send(ltxe) to send stanzas 3. sendMessage(to,body,type="chat") to send messages 4. disconnect() to disconnect [B]: Element() alias to ltx.Element [C]: $build(xname,attrs) returns an instance of corresponding xml object [D]: $msg(attrs) returns an instance of message xml object [E]: $iq(attrs) returns an instance of iq xml object [F]: $pres(attrs) returns an instance of presence xml object [G]: setLogLevel(logLevel) sets the loglevel[use only when extremely necessary] */ var http = require("http"); var url = require("url"); var ltx = require("ltx"); var util = require("util"); var events = require("events"); var autil = require("./autil.js"); //my utilities var url = require("url"); var NS_CLIENT = "jabber:client"; var NS_XMPP_SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; var NS_XMPP_BIND = "urn:ietf:params:xml:ns:xmpp-bind"; var NS_XMPP_SESSION = "urn:ietf:params:xml:ns:xmpp-session"; var NS_DEF = "http://jabber.org/protocol/httpbind"; var NS_STREAM = "http://etherx.jabber.org/streams"; var STATE_FIRST = 0, STATE_PREAUTH = 1, STATE_AUTH = 2, STATE_AUTHED = 3, STATE_BIND = 4, STATE_SESSION = 5, STATE_ONLINE = 6, STATE_TERM = 7, STATE_OVER = 8; /* jid : [String] jabber id of user (e.g. 'user@example.com/office') password : [String] password bosh : [String] url of the bosh-server (e.g. 'http://localhost:5280/http-bind/') route : [String] (optional) route attribute [if used] for connecting to xmpp server */ function nxbClient(jid, password, bosh, route) { events.EventEmitter.call(this); this.sess_attr = { rid : Math.round(Math.random() * 10000), jid : autil.jid_parse(jid), password : password }; //no. of requests held by bosh-server this.chold = 0; //bool to check whether sendPending is scheduled on nextTick to send pending stenzas this.hasNextTick = false; this.state = STATE_FIRST; var u = url.parse(bosh); this.options = { host : u.hostname, port : u.port, path : u.pathname, method : "POST", agent : false, protocol: u.protocol }; // an array of pending xml stanzas to be sent this.pending = []; //sends an http request this.sendHttp = function(body) { var that = this; this.chold++; autil.xmlHttpRequest(this.options, function(err, response) {that.handle(err, response);}, body); }; //logs error and terminates session this.pError = function(ss) { autil.logIt("ERROR", ss); //emit "error" event this.emit("error", ss); if (this.state != STATE_TERM) { this.terminate(); } return; }; //consructor definition var attr = { content : "text/xml; charset=utf-8", to : this.sess_attr.jid.domain, rid : this.sess_attr.rid++, hold : 1, wait : 60, ver : "1.6", "xml:lang" : "en", "xmpp:version" : "1.0", xmlns : NS_DEF, "xmlns:xmpp" : "urn:xmpp:xbosh" }; if(route) { attr.route = route; } var body = new ltx.Element("body", attr); this.sendHttp(body.toString()); //***********************Other Member Functions****************************** //handles all the http responses came this.handle = function(err, response) { var iq, error, body, features, success, failure; this.chold--; // some error in sending or receiving http request if(err) { autil.logIt("ERROR", this.sess_attr.jid + " no response " + response); //emit offline event with condition this.emit("error", response); return; } // ltx.parse() throws exceptions if unable to parse try { body = ltx.parse(response); } catch(e) { this.pError("xml parsing ERROR: " + response); return; } // check for stream error serror = body.getChild("error", NS_STREAM); if(error) { autil.logIt("ERROR", "stream Error : " + serror); /* No need to terminate as stream already closed by xmppserver and hence bosh-server but to inform other asynch methods not to send messages any more change the state */ state = STATE_TERM; this.emit("offline", "stream-error " + body.toString()); return; } // session termination by bosh server if(body.attrs.type && body.attrs.type == "terminate") { if(this.state != STATE_TERM) { autil.logIt("INFO", "Session terminated By the Server " + body); this.state = STATE_TERM; this.emit("offline", "Session termination by server " + body.toString()); return; } } if(this.state == STATE_FIRST) { this.state = STATE_PREAUTH; for(var i in body.attrs) { this.sess_attr[i] = body.attrs[i]; } } if(this.state == STATE_PREAUTH) { features = body.getChild("features", NS_STREAM); if(features) { this.startSasl(features); this.state = STATE_AUTH; } else { this.sendXml(); } return; } if(this.state == STATE_AUTH) { autil.logIt("DEBUG", "STATE_AUTH with body: " + body.getChild("success", "urn:ietf:params:xml:ns:xmpp-sasl") + " and NS_CLIENT: " + NS_CLIENT); success = body.getChild("success", "urn:ietf:params:xml:ns:xmpp-sasl"); failure = body.getChild("failure", NS_CLIENT); if (success) { autil.logIt("DEBUG", "Authentication Success: " + this.sess_attr.jid); this.state = STATE_AUTHED; this.restartStream(); //restart stream } else if (failure) { this.pError("Authentication Failure: " + this.sess_attr.jid + body); } else { this.sendXml(); //sending empty request } return; } if(this.state == STATE_AUTHED) { //stream already restarted features = body.getChild("features", NS_STREAM); if(features) { //checking for session support from xmpp if(features.getChild("session", NS_XMPP_SESSION)) { this.sessionSupport = true; } else { this.sessionSupport = false; } //resource binding if(features.getChild("bind", NS_XMPP_BIND)) { this.state = STATE_BIND; this.bindResource(this.sess_attr.jid.resource); //bind resource } else { this.pError("Resource binding not supported"); } } else { this.sendXml(); } return; } if(this.state == STATE_BIND) { iq = body.getChild("iq", NS_CLIENT); if(iq) { if(iq.attrs.id == "bind_1" && iq.attrs.type == "result") { var cjid = iq.getChild("bind", NS_XMPP_BIND).getChild("jid", NS_XMPP_BIND).getText(); this.sess_attr.jid.resource = cjid.substr(cjid.indexOf("/") + 1); if(this.sessionSupport) { iq = new ltx.Element("iq", {to : this.sess_attr.jid.domain, type : "set", id : "sess_1"}); iq.c("session", {xmlns : NS_XMPP_SESSION}); this.sendXml(iq); this.state = STATE_SESSION; } else { this.getOnline(); } } else { // stanza error to be handled properly this.pError("iq stanza error resource binding : " + iq); } } else { this.sendXml(); } return; } if(this.state == STATE_SESSION) { iq = body.getChild("iq"); if(iq) { if(iq.attrs.id == "sess_1" && iq.attrs.type == "result") { this.getOnline(); } else { this.pError("iq stanza error session establishment : " + iq); } } else { this.sendXml(); } return; } if(this.state == STATE_ONLINE) { this.handleOnline(body); return; } if(this.state == STATE_TERM) { autil.logIt("INFO", "client terminating : " + this.sess_attr.jid); this.state = STATE_OVER; return; } if(this.state == STATE_OVER) { //extra held responses objects coming back do nothing :P return; } }; //gets u online this.getOnline = function() { autil.logIt("INFO", "Session Created : " + this.sess_attr.jid); //now u r online this.state = STATE_ONLINE; this.emit("online"); //send any pending stanza's this.sendPending(); return; }; this.setNoMoreResponse = function() { // Artificially set the OVER state in order to no longer answer or send on this connection. // This is useful if you are taking the rid/jid/sid to another connection and need this one // to stop being 'active' at that time. this.state = STATE_OVER; }; //what to do on response arrival after online this.handleOnline = function(body) { var that=this; //process body and emit "stanza" event body.children.forEach(function(ltxe) { that.emit("stanza", ltxe); }); //send any pending stanzas this.sendPending(); return; }; //start plain sasl authentication this.startSasl = function(features) { var mechanisms = features.getChild("mechanisms", NS_XMPP_SASL); if(!mechanisms) { this.pError("No features-startSasl"); return; } for(i = 0;i<mechanisms.children.length;i++) { if(mechanisms.children[i].getText() == "PLAIN") { var e = new ltx.Element("auth", {xmlns : NS_XMPP_SASL, mechanism : "PLAIN"}); e.t(this.getPlain()); this.sendXml(e); return; } } this.pError("Plain SASL authentication unavailable!!!"); }; // get plain auth data this.getPlain = function() { authzid = this.sess_attr.jid.username + "@" + this.sess_attr.jid.domain; authcid = this.sess_attr.jid.username; password = this.sess_attr.password; return autil.encode64(authzid + "\u0000" + authcid + "\u0000" + password); }; //send terminate packet this.terminate = function() { var body = new ltx.Element("body", {sid : this.sess_attr.sid, rid : this.sess_attr.rid++, type : "terminate", xmlns : NS_DEF}); body.c("presence", {type : "unavailable", xmlns : NS_CLIENT}); this.sendHttp(body.toString()); this.state = STATE_TERM; }; //sends restart stream packet this.restartStream = function() { var attr = { rid : this.sess_attr.rid++, sid : this.sess_attr.sid, "xmpp:restart" : "true", to : this.sess_attr.from, "xml:lang" : "en", xmlns : NS_DEF, "xmlns:xmpp" : "urn:xmpp:xbosh" }; var body = new ltx.Element("body",attr); this.sendHttp(body.toString()); }; //sends resource bind packet this.bindResource = function(res_name) { var resource = new ltx.Element("resource"); resource.t(res_name); var bind = new ltx.Element("bind", {xmlns : NS_XMPP_BIND}); bind.cnode(resource); var iq = new ltx.Element("iq", {id : "bind_1", type : "set", xmlns : NS_CLIENT}); iq.cnode(bind); this.sendXml(iq); }; //sends an ltx-xml element by wrapping it into body element[change it to array thing] this.sendXml = function(ltxe) { var body = new ltx.Element("body", {sid : this.sess_attr.sid, rid : this.sess_attr.rid++, xmlns : NS_DEF, stream : this.sess_attr.stream}); if(ltxe) { body.cnode(ltxe); } this.sendHttp(body.toString()); }; //sends a single message packet this.sendMessage = function(to, mbody, type) { var message = new ltx.Element("message", {to : to, from : this.sess_attr.jid.toString(), type : type || "chat", "xml:lang" : "en"}); var body = new ltx.Element("body").t(mbody); message.cnode(body); this.send(message); }; //puts ltx-element into pending[] to be sent later this.send = function(ltxe) { ltxe = ltxe.tree(); if(this.state != STATE_ONLINE) { this.emit("error", "can send something only when u are ONLINE!!!"); return; } if(ltxe) { this.pending.push(ltxe); } if(!this.hasNextTick) { this.hasNextTick = true; var that = this; process.nextTick(function() { if(that.hasNextTick && that.state == STATE_ONLINE) { that.sendPending(); } }); } }; //sends all the pending messages (in pending []) to server this.sendPending = function() { //send only if u have something to send or u need to poll the bosh-server if(this.pending.length > 0 || this.chold < 1) { var body = new ltx.Element("body", {sid : this.sess_attr.sid, rid : this.sess_attr.rid++, xmlns : NS_DEF, stream : this.sess_attr.stream}); while(this.pending.length > 0) { body.cnode(this.pending.shift()); } this.sendHttp(body.toString()); this.hasNextTick = false; } }; //disconnect the connection this.disconnect = function() { //before terminating, send any pending stanzas this.sendPending(); this.terminate(); //should emit 'offline' event or not?? //[yes, because if i am calling disconnect() and i dont care whatever comes to me afterwards ] this.emit("offline", "session termination by user"); return; }; } util.inherits(nxbClient, events.EventEmitter); exports.Client = nxbClient; //stanza builders //ltx Element object to create stanzas exports.Element = ltx.Element; //generic packet building helper function exports.$build = function(xname, attrib){ return new ltx.element(xname, attrib); }; //packet builder helper function for message stanza exports.$msg = function(attrib){ return new ltx.Element("message", attrib); }; //packet builder helper function for iq stanza exports.$iq = function(attrib){ return new ltx.Element("iq", attrib); }; //packet builder helper function for iq stanza exports.$pres = function(attrib){ return new ltx.Element("presence", attrib); }; exports.setLogLevel = autil.setLogLevel;