node-bosh-xmpp-client
Version:
XMPP clients that connects via BOSH
554 lines (484 loc) • 13.9 kB
JavaScript
/*
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;