| 1 | // xmpp.js - Server-side XMPP in Javascript |
| 2 | // (C) 2010 Matthew Wild |
| 3 | // This project is released under the MIT/X11 |
| 4 | // license. For more info see the COPYING file. |
| 5 | |
| 6 | // Node libs |
| 7 | var tcp = require("tcp"); |
| 8 | |
| 9 | // External libs |
| 10 | var xml = require("./node-xml/lib/node-xml"); |
| 11 | var sha1 = require("./sha1"); |
| 12 | |
| 13 | // This lib |
| 14 | var xmpp = exports; |
| 15 | |
| 16 | // Wraps a function so that its 'this' is always 'context' when called |
| 17 | var recontext = function (context, f) { return function () { return f.apply(context, arguments); }; }; |
| 18 | |
| 19 | xmpp.xmlns = { |
| 20 | streams: "http://etherx.jabber.org/streams", |
| 21 | component_accept: "jabber:component:accept", |
| 22 | chatstates: "http://jabber.org/protocol/chatstates" |
| 23 | }; |
| 24 | |
| 25 | xmpp.Status = { |
| 26 | ERROR: 0, |
| 27 | CONNECTING: 1, |
| 28 | CONNFAIL: 2, |
| 29 | AUTHENTICATING: 3, |
| 30 | AUTHFAIL: 4, |
| 31 | CONNECTED: 5, |
| 32 | DISCONNECTED: 6, |
| 33 | DISCONNECTING: 7, |
| 34 | }; |
| 35 | |
| 36 | xmpp.LogLevel = { |
| 37 | DEBUG: 0, |
| 38 | INFO: 1, |
| 39 | WARN: 2, |
| 40 | ERROR: 3, |
| 41 | FATAL: 4 |
| 42 | }; |
| 43 | /** XMPPStream: Takes a parser, eats bytes, fires callbacks on stream events **/ |
| 44 | xmpp.Stream = function (callbacks) |
| 45 | { |
| 46 | this.callbacks = callbacks; |
| 47 | var stream = this; |
| 48 | var stanza; |
| 49 | this.parser = new xml.SaxParser(function (cb) |
| 50 | { |
| 51 | cb.onStartElementNS(function (tagname, attr_arr, prefix, uri, namespaces) |
| 52 | { |
| 53 | var attr = {}; |
| 54 | if(uri != xmpp.xmlns.component_accept) |
| 55 | attr.xmlns = uri; |
| 56 | for(var i=0;i<attr_arr.length;i++) |
| 57 | attr[attr_arr[i][0]] = attr_arr[i][1]; |
| 58 | for(var i=0;i<namespaces.length;i++) |
| 59 | if(namespaces[i][0].length > 0) |
| 60 | attr["xmlns:"+namespaces[i][0]] = namespaces[i][1]; |
| 61 | if(!stanza) |
| 62 | { |
| 63 | if(stream.opened) |
| 64 | stanza = xmpp.stanza(tagname, attr); |
| 65 | else if(tagname == "stream" && uri == xmpp.xmlns.streams) |
| 66 | { |
| 67 | stream.opened = true; |
| 68 | callbacks.opened(attr); |
| 69 | } |
| 70 | else |
| 71 | { |
| 72 | callbacks.error("no-stream"); |
| 73 | } |
| 74 | } |
| 75 | else |
| 76 | { |
| 77 | stanza.c(tagname, attr); |
| 78 | } |
| 79 | |
| 80 | }); |
| 81 | |
| 82 | cb.onEndElementNS(function(tagname) { |
| 83 | if(stanza) |
| 84 | if(stanza.last_node.length == 1) |
| 85 | { |
| 86 | callbacks.stanza(stanza); |
| 87 | stanza = null; |
| 88 | } |
| 89 | else |
| 90 | stanza.up(); |
| 91 | else |
| 92 | { |
| 93 | stream.opened = false; |
| 94 | callbacks.closed(); |
| 95 | } |
| 96 | }); |
| 97 | |
| 98 | cb.onCharacters(function(chars) { |
| 99 | if(stanza) |
| 100 | stanza.t(chars); |
| 101 | }); |
| 102 | }); |
| 103 | |
| 104 | this.data = function (data) |
| 105 | { |
| 106 | return this.parser.parseString(data); |
| 107 | } |
| 108 | |
| 109 | return this; |
| 110 | }; |
| 111 | |
| 112 | |
| 113 | /** Connection: Takes host/port, manages stream **/ |
| 114 | xmpp.Connection = function (host, port) |
| 115 | { |
| 116 | this.host = host || "localhost"; |
| 117 | this.port = port || 5347; |
| 118 | |
| 119 | this.stream = new xmpp.Stream({ |
| 120 | opened: recontext(this, this._stream_opened), |
| 121 | stanza: recontext(this, this._handle_stanza), |
| 122 | closed: recontext(this, this._stream_closed) |
| 123 | }); |
| 124 | |
| 125 | this._uniqueId = 0; |
| 126 | |
| 127 | return this; |
| 128 | }; |
| 129 | |
| 130 | exports.Connection.prototype = { |
| 131 | connect: function (jid, pass, callback) |
| 132 | { |
| 133 | this.jid = jid; |
| 134 | this.password = pass; |
| 135 | this.connect_callback = callback; |
| 136 | |
| 137 | var conn = this; |
| 138 | |
| 139 | // Note that tcp.createConnection also initiates the connection. |
| 140 | // This doesn't appear to create problems with adding listeners |
| 141 | // afterward, but should be kept in mind should any arise. |
| 142 | this.socket = tcp.createConnection(this.port, this.host) |
| 143 | |
| 144 | this.socket.setTimeout(0); // Because Node's default timeout is 60s |
| 145 | |
| 146 | this.socket.addListener("connect", recontext(this, conn._socket_connected)); |
| 147 | this.socket.addListener("disconnect", recontext(this, conn._socket_disconnected)); |
| 148 | this.socket.addListener("data", recontext(this, conn._socket_received)); |
| 149 | |
| 150 | this.handlers = []; |
| 151 | |
| 152 | this._setStatus(xmpp.Status.CONNECTING); |
| 153 | }, |
| 154 | |
| 155 | send: function (data) |
| 156 | { |
| 157 | this.debug("SND: "+data); |
| 158 | this.socket.write(data.toString()); |
| 159 | }, |
| 160 | |
| 161 | sendIQ: function (iq, on_result, on_error) |
| 162 | { |
| 163 | if(!iq.attr.id) |
| 164 | iq.attr.id = this.getUniqueId(); |
| 165 | this.addHandler(function (reply) { |
| 166 | if(reply.attr.type == "result") |
| 167 | return on_result(reply); |
| 168 | elseif(on_error) |
| 169 | return on_error(reply); |
| 170 | return false; |
| 171 | |
| 172 | }, null, "iq", null, iq.attr.id); |
| 173 | this.send(iq); |
| 174 | }, |
| 175 | |
| 176 | addHandler: function (handler, ns, name, type, id, from, options) |
| 177 | { |
| 178 | return this.handlers.push({ |
| 179 | callback: handler, |
| 180 | xmlns: ns, |
| 181 | name: name, |
| 182 | type: type, |
| 183 | id: id, |
| 184 | from: from, |
| 185 | matchBare: options && options.matchBare}); |
| 186 | }, |
| 187 | |
| 188 | getUniqueId: function (suffix) |
| 189 | { |
| 190 | return ++this._uniqueId + (suffix?(":"+suffix):""); |
| 191 | }, |
| 192 | |
| 193 | // Update the status of the connection, call connect_callback |
| 194 | _setStatus: function (status, condition) |
| 195 | { |
| 196 | this.status = status; |
| 197 | this.connect_callback(status, condition); |
| 198 | }, |
| 199 | |
| 200 | // Socket listeners, called on TCP-level events |
| 201 | _socket_connected: function () |
| 202 | { |
| 203 | this.info("CONNECTED."); |
| 204 | this.send("<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='"+this.jid+"'>"); |
| 205 | }, |
| 206 | |
| 207 | _socket_disconnected: function (had_error) |
| 208 | { |
| 209 | if(this.status == xmpp.Status.CONNECTING) |
| 210 | this._setStatus(xmpp.Status.CONNFAIL); |
| 211 | elseif(this.status == xmpp.Status.CONNECTED) |
| 212 | this._setStatus(xmpp.Status.DISCONNECTED); |
| 213 | this.info("DISCONNECTED."); |
| 214 | }, |
| 215 | |
| 216 | _socket_received: function (data) |
| 217 | { |
| 218 | this.debug("RCV: "+data); |
| 219 | // Push to parser |
| 220 | this.stream.data(data); |
| 221 | }, |
| 222 | |
| 223 | // Stream listeners, called on XMPP-level events |
| 224 | _stream_opened: function (attr) |
| 225 | { |
| 226 | this.debug("STREAM: opened."); |
| 227 | this._setStatus(xmpp.Status.AUTHENTICATING); |
| 228 | var handshake = sha1.hex(attr.id + this.password); |
| 229 | this.debug("Calculated authentication token " + handshake |
| 230 | + " from stream id '" + attr.id |
| 231 | + "' and password '" + this.password + "'"); |
| 232 | this.debug("Sending authentication token..."); |
| 233 | this.send("<handshake>"+handshake+"</handshake>"); |
| 234 | }, |
| 235 | |
| 236 | _handle_stanza: function (stanza) |
| 237 | { |
| 238 | if(!stanza.attr.xmlns) // Default namespace |
| 239 | { |
| 240 | if(stanza.name == "handshake") |
| 241 | { |
| 242 | this._setStatus(xmpp.Status.CONNECTED); |
| 243 | } |
| 244 | } |
| 245 | this.debug("STANZA: "+stanza.toString()); |
| 246 | |
| 247 | // Match and call handlers |
| 248 | var removeHandlers = []; |
| 249 | for(var i=0;i<this.handlers.length;i++) |
| 250 | { |
| 251 | var handler = this.handlers[i]; |
| 252 | if( |
| 253 | (!handler.name || handler.name == stanza.name) && |
| 254 | (!handler.xmlns || (handler.xmlns == stanza.attr.xmlns |
| 255 | || (stanza.tags[0] && handler.xmlns == stanza.tags[0].attr.xmlns))) && |
| 256 | (!handler.type || handler.type == stanza.attr.type) && |
| 257 | (!handler.id || handler.id == stanza.attr.id) && |
| 258 | (!handler.from || (handler.from == (handler.matchBare?xmpp.getBareJID(stanza.attr.from):stanza.attr.from))) && |
| 259 | (!handler.to || (handler.to == (handler.matchBare?xmpp.getBareJID(stanza.attr.to):stanza.attr.to))) |
| 260 | ) |
| 261 | { |
| 262 | var ret = handler.callback(stanza); |
| 263 | if(ret == false) |
| 264 | removeHandlers.push(i); |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | var adjust = 0; |
| 269 | for(var i=0;i<removeHandlers.length;i++) |
| 270 | this.handlers.splice(removeHandlers[i]-(adjust++), 1); |
| 271 | }, |
| 272 | |
| 273 | _stream_closed: function () |
| 274 | { |
| 275 | this.debug("STREAM: closed."); |
| 276 | this.socket.close(); |
| 277 | if(this.status == xmpp.Status.CONNECTING) |
| 278 | this._setStatus(xmpp.status.CONNFAIL); |
| 279 | else |
| 280 | this._setStatus(xmpp.Status.DISCONNECTED); |
| 281 | }, |
| 282 | |
| 283 | _stream_error: function (condition) |
| 284 | { |
| 285 | this._setStatus(xmpp.Status.ERROR, condition); |
| 286 | }, |
| 287 | |
| 288 | // Logging |
| 289 | log: function (level, message) {}, |
| 290 | debug: function (message) { return this.log(xmpp.LogLevel.DEBUG, message); }, |
| 291 | info: function (message) { return this.log(xmpp.LogLevel.INFO , message); }, |
| 292 | warn: function (message) { return this.log(xmpp.LogLevel.WARN , message); }, |
| 293 | error: function (message) { return this.log(xmpp.LogLevel.ERROR, message); }, |
| 294 | fatal: function (message) { return this.log(xmpp.LogLevel.FATAL, message); } |
| 295 | |
| 296 | }; |
| 297 | |
| 298 | function xmlescape(s) |
| 299 | { |
| 300 | return s.replace(/&/g, "&") |
| 301 | .replace(/</g, "<") |
| 302 | .replace(/>/g, ">") |
| 303 | .replace(/\"/g, """) |
| 304 | .replace(/\'/g, "'"); |
| 305 | } |
| 306 | |
| 307 | /** StanzaBuilder: Helps create and manipulate XML snippets **/ |
| 308 | xmpp.StanzaBuilder = function (name, attr) |
| 309 | { |
| 310 | this.name = name; |
| 311 | this.attr = attr || {}; |
| 312 | this.tags = []; |
| 313 | this.children = []; |
| 314 | this.last_node = [this]; |
| 315 | return this; |
| 316 | }; |
| 317 | |
| 318 | xmpp.StanzaBuilder.prototype = { |
| 319 | s: function (name, attr) |
| 320 | { |
| 321 | // This function was created because c() doesn't seem to work for adding |
| 322 | // multiple children on the same level with each other. This is just a |
| 323 | // quick fix mostly to get chatstates working. |
| 324 | var s = new xmpp.StanzaBuilder(name, attr); |
| 325 | var parent = this; |
| 326 | parent.tags.push(s); |
| 327 | parent.children.push(s); |
| 328 | this.last_node.push(s); |
| 329 | return this |
| 330 | }, |
| 331 | |
| 332 | c: function (name, attr) |
| 333 | { |
| 334 | var s = new xmpp.StanzaBuilder(name, attr); |
| 335 | var parent = this.last_node[this.last_node.length-1]; |
| 336 | parent.tags.push(s); |
| 337 | parent.children.push(s); |
| 338 | this.last_node.push(s); |
| 339 | return this; |
| 340 | }, |
| 341 | |
| 342 | t: function (text) |
| 343 | { |
| 344 | var parent = this.last_node[this.last_node.length-1]; |
| 345 | parent.children.push(text); |
| 346 | return this; |
| 347 | }, |
| 348 | |
| 349 | up: function () |
| 350 | { |
| 351 | this.last_node.pop(); |
| 352 | return this; |
| 353 | }, |
| 354 | |
| 355 | toString: function (top_tag_only) |
| 356 | { |
| 357 | var buf = []; |
| 358 | buf.push("<" + this.name); |
| 359 | for(var attr in this.attr) |
| 360 | { |
| 361 | buf.push(" " + attr + "='" + xmlescape(this.attr[attr]) + "'"); |
| 362 | } |
| 363 | |
| 364 | // Now add children if wanted |
| 365 | if(top_tag_only) |
| 366 | { |
| 367 | buf.push(">"); |
| 368 | } |
| 369 | else if(this.children.length == 0) |
| 370 | { |
| 371 | buf.push("/>"); |
| 372 | } |
| 373 | else |
| 374 | { |
| 375 | buf.push(">"); |
| 376 | for(var i = 0; i<this.children.length; i++) |
| 377 | { |
| 378 | var child = this.children[i]; |
| 379 | if(typeof(child) == "string") |
| 380 | buf.push(xmlescape(child)); |
| 381 | else |
| 382 | buf.push(child.toString()); |
| 383 | } |
| 384 | buf.push("</" + this.name + ">"); |
| 385 | } |
| 386 | return buf.join(""); |
| 387 | }, |
| 388 | |
| 389 | getChild: function (name, xmlns) { |
| 390 | for(var i=0;i<this.tags.length;i++) |
| 391 | { |
| 392 | var child = this.tags[i]; |
| 393 | if((!name || child.name == name) && (!xmlns || child.attr.xmlns == xmlns)) |
| 394 | return child; |
| 395 | } |
| 396 | return null; |
| 397 | }, |
| 398 | |
| 399 | getText: function () { |
| 400 | var buf = []; |
| 401 | for(var i=0;i<this.children.length;i++) |
| 402 | if(typeof(this.children[i]) == "string") |
| 403 | buf.push(this.children[i]); |
| 404 | return buf.join(""); |
| 405 | }, |
| 406 | |
| 407 | getAttribute: function (name) { |
| 408 | return this.attr[name] || null; |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | xmpp.stanza = function (name, attr) |
| 413 | { |
| 414 | return new xmpp.StanzaBuilder(name, attr); |
| 415 | } |
| 416 | |
| 417 | xmpp.message = function (attr) |
| 418 | { |
| 419 | return xmpp.stanza("message", attr); |
| 420 | } |
| 421 | |
| 422 | xmpp.presence = function (attr) |
| 423 | { |
| 424 | return xmpp.stanza("presence", attr); |
| 425 | } |
| 426 | |
| 427 | xmpp.iq = function (attr) |
| 428 | { |
| 429 | return xmpp.stanza("iq", attr); |
| 430 | } |