# HG changeset patch # User Sonny Piers # Date 1339523093 -7200 # Node ID f14558915187e0f88db7c4a6ccd364846db87a8c # Parent 6ec16b3e9cfcd22d954b0558d5ce5e0fad999581 bosh support diff --git a/bosh.js b/bosh.js new file mode 100644 --- /dev/null +++ b/bosh.js @@ -0,0 +1,215 @@ +'use strict'; + +(function() { + Lightstring.BOSHConnection = function(aService) { + this.service = aService; + this.rid = 1337; + this.currentRequests = 0; + this.maxHTTPRetries = 5; + this.maxRequests = 2; + this.queue = []; + }; + Lightstring.BOSHConnection.prototype = new EventEmitter(); + Lightstring.BOSHConnection.prototype.open = function() { + var that = this; + + var attrs = { + wait: '60', + hold: '1', + to: 'yuilop', + content: 'text/xml; charset=utf-8', + ver: '1.6', + 'xmpp:version': '1.0', + 'xmlns:xmpp': 'urn:xmpp:xbosh', + }; + + this.request(attrs, null, function(data) { + that.emit('open'); + that.sid = data.getAttribute('sid'); + that.maxRequests = data.getAttribute('maxRequests') || that.maxRequests; + }); + + + this.on('in', function(stanza) { + if (stanza.localName === 'success') { + that.request({ + 'xmpp:restart': 'true', + 'xmlns:xmpp': 'urn:xmpp:xbosh' + }) + } + }) + }; + Lightstring.BOSHConnection.prototype.request = function(attrs, children, aOnSuccess, aOnError, aRetry) { + // if (children && children[0] && children[0].name === 'body') { + // var body = children[0]; + // } + // else { + // var body = new ltx.Element('body'); + // if (children) { + // if(util.isArray(children)) + // for (var k in children) + // body.cnode(children[k]); + // else + // body.cnode(children); + // } + // } + + var body = ''; + var body = Lightstring.XML2DOM(body); + + //sid + if (this.sid) + body.setAttribute('sid', this.sid); + + //attributes on body + for (var i in attrs) + body.setAttribute(i, attrs[i]); + + //children + for (var i in children) + body.appendChild(children[i]); + + + + var retry = aRetry || 0; + + var req = new XMLHttpRequest(); + req.open('POST', this.service); + + + // req.upload.addEventListener("progress", updateProgress, false); + // req.upload.addEventListener("load", transferComplete, false); + // req.upload.addEventListener("error", transferFailed, false); + // req.upload.addEventListener("abort", transferCanceled, false); + + // req.addEventListener("progress", updateProgress, false); + // req.addEventListener("load", transferComplete, false); + // req.addEventListener("error", transferFailed, false); + // req.addEventListener("abort", transferCanceled, false); + + var that = this; + // req.responseType = 'document'; + req.addEventListener("load", function() { + if (req.status < 200 || req.status >= 400) { + that.emit('error', "HTTP status " + req.status); + that.emit('close'); + return; + } + that.currentRequests--; + + var body = this.response; + that.emit('rawin', body); + var bodyEl = Lightstring.XML2DOM(body); + that.processResponse(bodyEl) + if (aOnSuccess) + aOnSuccess(bodyEl); + + }, false); + // req.on('error', function(error) { + // if (retry < that.maxHTTPRetries) { + // that.request(attrs, children, aOnSuccess, aOnError, ++retry); + // } + // else { + // that.emit('close'); + // that.emit('error', error); + // if (aOnError) + // aOnError(error); + // } + // }); + // this.emit('rawout', body.toString()); + + for(var i = 0; i < body.children.length; i++) { + var child = body.children[i]; + that.emit('out', child); + } + this.emit('rawout', Lightstring.DOM2XML(body)) + + req.send(Lightstring.DOM2XML(body)); + this.currentRequests++; + }; + Lightstring.BOSHConnection.prototype.send = function(aData) { + if (!aData) { + var el = ''; + } + + else if(typeof aData == 'string') { + try { + var el = Lightstring.XML2DOM(aData); + } + catch(e) { + console.log(e); + console.log(aData); + } + } + else { + var el = aData.root(); + } + + var that = this; + + this.queue.push(el); + + setTimeout(this.mayRequest.bind(this), 0) + + }; + Lightstring.BOSHConnection.prototype.end = function(stanzas) { + var that = this; + + stanzas = stanzas || []; + if (typeof stanzas !== 'array') + stanzas = [stanzas]; + + stanzas = this.queue.concat(stanzas); + this.queue = []; + this.request({type: 'terminate'}, stanzas, + function(err, bodyEl) { + that.emit('end'); + that.emit('close'); + delete that.sid; + }); + }; + Lightstring.BOSHConnection.prototype.processResponse = function(bodyEl) { + if (bodyEl && bodyEl.children) { + for(var i = 0; i < bodyEl.children.length; i++) { + var child = bodyEl.children[i]; + this.emit('in', child); + } + } + if (bodyEl && bodyEl.getAttribute('type') === 'terminate') { + var condition = bodyEl.getAttribute('condition'); + this.emit('error', + new Error(condition || "Session terminated")); + this.emit('close'); + } + }; + Lightstring.BOSHConnection.prototype.mayRequest = function() { + var canRequest = + this.sid && (this.currentRequests === 0 || ((this.queue.length > 0 && this.currentRequests < this.maxRequests)) + ); + + if (!canRequest) + return; + + var stanzas = this.queue; + this.queue = []; + //~ this.rid++; + + var that = this; + this.request({}, stanzas, + //success + function(data) { + //if (data) + //that.processResponse(data); + + setTimeout(that.mayRequest.bind(that), 0); + + }, + //error + function(error) { + that.emit('error', error); + that.emit('close'); + delete that.sid; + } + ); + }; +})(); \ No newline at end of file diff --git a/lightstring.js b/lightstring.js --- a/lightstring.js +++ b/lightstring.js @@ -118,7 +118,7 @@ var Lightstring = { /** * @constructor Creates a new Lightstring connection - * @param {String} [aService] The Websocket service URL. + * @param {String} [aService] The connection manager URL. * @memberOf Lightstring */ Lightstring.Connection = function(aService) { @@ -133,248 +133,188 @@ Lightstring.Connection = function(aServi */ this.callbacks = {}; }; -Lightstring.Connection.prototype = { - /** - * @function Create and open a websocket then go though the XMPP authentification process. - * @param {String} [aJid] The JID (Jabber id) to use. - * @param {String} [aPassword] The associated password. - */ - connect: function(aJid, aPassword) { - this.emit('connecting'); - this.jid = new Lightstring.JID(aJid); - if (aPassword) - this.password = aPassword; - - if (!this.jid.bare) - return; //TODO: error - if (!this.service) - return; //TODO: error - - function getProtocol(aURL) { - var a = document.createElement('a'); - a.href = aURL; - return a.protocol.replace(':', ''); - } - var protocol = getProtocol(this.service); +Lightstring.Connection.prototype = new EventEmitter(); +/** + * @function Create and open a websocket then go though the XMPP authentification process. + * @param {String} [aJid] The JID (Jabber id) to use. + * @param {String} [aPassword] The associated password. + */ +Lightstring.Connection.prototype.connect = function(aJid, aPassword) { + this.emit('connecting'); + this.jid = new Lightstring.JID(aJid); + if (aPassword) + this.password = aPassword; - if (protocol.match('http')) - this.connection = new Lightstring.BOSHConnection(this.service); - else if (protocol.match('ws')) - this.connection = new Lightstring.WebSocketConnection(this.service); - - this.connection.connect(); - - var that = this; - - this.connection.once('open', function() { - var stream = Lightstring.stanzas.stream.open(that.jid.domain); - that.connection.send(stream); - var stanza = { - XML: stream - }; - that.emit('output', stanza); - }); - this.connection.on('stanza', function(stanza) { - var stanza = new Lightstring.Stanza(stanza); - - //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong! - that.emit('input', stanza); - - if (!stanza.DOM) - return; - - var name = stanza.DOM.localName; + if (!this.jid.bare) + return; //TODO: error + if (!this.service) + return; //TODO: error - //Authentication - //FIXME SASL mechanisms and XMPP features can be both in a stream:features - if (name === 'features') { - //SASL mechanisms - if (stanza.DOM.firstChild.localName === 'mechanisms') { - stanza.mechanisms = []; - var nodes = stanza.DOM.getElementsByTagName('mechanism'); - for (var i = 0; i < nodes.length; i++) - stanza.mechanisms.push(nodes[i].textContent); - that.emit('mechanisms', stanza); - } - //XMPP features - else { - //TODO: stanza.features - that.emit('features', stanza); - } - } - else if (name === 'challenge') { - that.emit('challenge', stanza); - } - else if (name === 'failure') { - that.emit('failure', stanza); - } - else if (name === 'success') { - that.emit('success', stanza); - } + function getProtocol(aURL) { + var a = document.createElement('a'); + a.href = aURL; + return a.protocol.replace(':', ''); + } + var protocol = getProtocol(this.service); - //Iq callbacks - else if (name === 'iq') { - var payload = stanza.DOM.firstChild; - if (payload) - that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza); + if (protocol.match('http')) + this.connection = new Lightstring.BOSHConnection(this.service); + else if (protocol.match('ws')) + this.connection = new Lightstring.WebSocketConnection(this.service); + + this.connection.open(); - var id = stanza.DOM.getAttribute('id'); - if (!(id && id in that.callbacks)) - return; - - var type = stanza.DOM.getAttribute('type'); - if (type !== 'result' && type !== 'error') - return; //TODO: warning - - var callback = that.callbacks[id]; - if (type === 'result' && callback.success) - callback.success.call(that, stanza); - else if (type === 'error' && callback.error) - callback.error.call(that, stanza); - - delete that.callbacks[id]; - } + var that = this; - else if (name === 'presence' || name === 'message') { - that.emit(name, stanza); - } - }); - }, - /** - * @function Send a message. - * @param {String|Object} aStanza The message to send. - * @param {Function} [aCallback] Executed on answer. (stanza must be iq) - */ - send: function(aStanza, aSuccess, aError) { - if (!(aStanza instanceof Lightstring.Stanza)) - var stanza = new Lightstring.Stanza(aStanza); - else - var stanza = aStanza; + this.connection.once('open', function() { + that.emit('open'); + }); + this.connection.on('out', function(stanza) { + that.emit('out', stanza); + }); + this.connection.on('in', function(stanza) { + var stanza = new Lightstring.Stanza(stanza); - if (!stanza) + //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong! + that.emit('stanza', stanza); + + if (!stanza.DOM) return; - if (stanza.DOM.tagName === 'iq') { - var type = stanza.DOM.getAttribute('type'); - if (type !== 'get' || type !== 'set') - ; //TODO: error + var name = stanza.DOM.localName; - var callback = {success: aSuccess, error: aError}; + //Authentication + //FIXME SASL mechanisms and XMPP features can be both in a stream:features + if (name === 'features') { + //SASL mechanisms + if (stanza.DOM.firstChild.localName === 'mechanisms') { + stanza.mechanisms = []; + var nodes = stanza.DOM.getElementsByTagName('mechanism'); + for (var i = 0; i < nodes.length; i++) + stanza.mechanisms.push(nodes[i].textContent); + that.emit('mechanisms', stanza); + } + //XMPP features + else { + //TODO: stanza.features + that.emit('features', stanza); + } + } + else if (name === 'challenge') { + that.emit('challenge', stanza); + } + else if (name === 'failure') { + that.emit('failure', stanza); + } + else if (name === 'success') { + that.emit('success', stanza); + } + + //Iq callbacks + else if (name === 'iq') { + var payload = stanza.DOM.firstChild; + if (payload) + that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza); var id = stanza.DOM.getAttribute('id'); - if (!id) { - var id = Lightstring.newId('sendiq:'); - stanza.DOM.setAttribute('id', id); - } + if (!(id && id in that.callbacks)) + return; + + var type = stanza.DOM.getAttribute('type'); + if (type !== 'result' && type !== 'error') + return; //TODO: warning + + var callback = that.callbacks[id]; + if (type === 'result' && callback.success) + callback.success.call(that, stanza); + else if (type === 'error' && callback.error) + callback.error.call(that, stanza); + + delete that.callbacks[id]; + } - this.callbacks[id] = callback; - + else if (name === 'presence' || name === 'message') { + that.emit(name, stanza); } - else if (aSuccess || aError) - ; //TODO: warning (no callback without iq) + }); +}; +/** + * @function Send a message. + * @param {String|Object} aStanza The message to send. + * @param {Function} [aCallback] Executed on answer. (stanza must be iq) + */ +Lightstring.Connection.prototype.send = function(aStanza, aSuccess, aError) { + if (!(aStanza instanceof Lightstring.Stanza)) + var stanza = new Lightstring.Stanza(aStanza); + else + var stanza = aStanza; + + if (!stanza) + return; + + if (stanza.DOM.tagName === 'iq') { + var type = stanza.DOM.getAttribute('type'); + if (type !== 'get' || type !== 'set') + ; //TODO: error + + var callback = {success: aSuccess, error: aError}; + + var id = stanza.DOM.getAttribute('id'); + if (!id) { + var id = Lightstring.newId('sendiq:'); + stanza.DOM.setAttribute('id', id); + } + + this.callbacks[id] = callback; + + } + else if (aSuccess || aError) + ; //TODO: warning (no callback without iq) - //FIXME this.socket.send(stanza.XML); (need some work on Lightstring.Stanza) - var fixme = Lightstring.DOM2XML(stanza.DOM); - stanza.XML = fixme; - this.connection.send(fixme); - this.emit('output', stanza); - }, - /** - * @function Closes the XMPP stream and the socket. - */ - disconnect: function() { - this.emit('disconnecting'); - var stream = Lightstring.stanzas.stream.close(); - this.socket.send(stream); - this.emit('XMLOutput', stream); - this.socket.close(); - }, - load: function() { - for (var i = 0; i < arguments.length; i++) { - var name = arguments[i]; - if (!(name in Lightstring.plugins)) - continue; //TODO: error - - var plugin = Lightstring.plugins[name]; - - //Namespaces - for (var ns in plugin.namespaces) - Lightstring.ns[ns] = plugin.namespaces[ns]; - - //Stanzas - Lightstring.stanzas[name] = {}; - for (var stanza in plugin.stanzas) - Lightstring.stanzas[name][stanza] = plugin.stanzas[stanza]; - - //Handlers - for (var handler in plugin.handlers) - this.on(handler, plugin.handlers[handler]); - - //Methods - this[name] = {}; - for (var method in plugin.methods) - this[name][method] = plugin.methods[method].bind(this); + //FIXME this.socket.send(stanza.XML); (need some work on Lightstring.Stanza) + var fixme = Lightstring.DOM2XML(stanza.DOM); + stanza.XML = fixme; + this.connection.send(fixme); + this.emit('output', stanza); +}; +/** + * @function Closes the XMPP stream and the socket. + */ +Lightstring.Connection.prototype.disconnect = function() { + this.emit('disconnecting'); + var stream = Lightstring.stanzas.stream.close(); + this.socket.send(stream); + this.emit('XMLOutput', stream); + this.socket.close(); +}; +Lightstring.Connection.prototype.load = function() { + for (var i = 0; i < arguments.length; i++) { + var name = arguments[i]; + if (!(name in Lightstring.plugins)) + continue; //TODO: error - if (plugin.init) - plugin.init.apply(this); - } - }, - /** - * @function Emits an event. - * @param {String} aName The event name. - * @param {Function|Array|Object} [aData] Data about the event. - */ - emit: function(aName, aData) { - var handlers = this.handlers[aName]; - if (!handlers) - return; + var plugin = Lightstring.plugins[name]; - //Non-data events - if(!aData) { - for (var i = 0; i < handlers.length; i++) - handlers[i].call(this, aData); - - return; - } + //Namespaces + for (var ns in plugin.namespaces) + Lightstring.ns[ns] = plugin.namespaces[ns]; - //Non-iq events - if (aData && aData.DOM && aData.DOM.localName !== 'iq') { - for (var i = 0; i < handlers.length; i++) - handlers[i].call(this, aData); - - return; - } + //Stanzas + Lightstring.stanzas[name] = {}; + for (var stanza in plugin.stanzas) + Lightstring.stanzas[name][stanza] = plugin.stanzas[stanza]; - //Iq events - var ret; - for (var i = 0; i < handlers.length; i++) { - ret = handlers[i].call(this, aData); - if (typeof ret !== 'boolean') - return; //TODO: error - - if (ret) - return; - } - - if (aData && aData.DOM) { - var type = aData.DOM.getAttribute('type'); - if (type !== 'get' && type !== 'set') - return; + //Handlers + for (var handler in plugin.handlers) + this.on(handler, plugin.handlers[handler]); - var from = aData.DOM.getAttribute('from'); - var id = aData.DOM.getAttribute('id'); - this.send(Lightstring.stanzas.errors.iq(from, id, 'cancel', 'service-unavailable')); - } - }, - /** - * @function Register an event handler. - * @param {String} aName The event name. - * @param {Function} aCallback The callback to call when the event is emitted. - */ - on: function(aName, callback) { - if (!this.handlers[aName]) - this.handlers[aName] = []; - this.handlers[aName].push(callback); + //Methods + this[name] = {}; + for (var method in plugin.methods) + this[name][method] = plugin.methods[method].bind(this); + + if (plugin.init) + plugin.init.apply(this); } -}; +}; \ No newline at end of file