From d29b8a6efb6ac0820ad88b01977e6998300f07e3 Mon Sep 17 00:00:00 2001 From: Gordin <9ordin@gmail.com> Date: Fri, 26 Sep 2014 10:19:07 +0200 Subject: [PATCH] added webrtc patch --- Gruntfile.js | 2 +- src/core.js | 4 + src/webrtc.js | 795 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 800 insertions(+), 1 deletion(-) create mode 100644 src/webrtc.js diff --git a/Gruntfile.js b/Gruntfile.js index 6c095d7a..de292e22 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -24,7 +24,7 @@ module.exports = function(grunt){ concat: { dist: { - src: ['src/wrap_header.js', 'src/base64.js', 'src/sha1.js', 'src/md5.js', 'src/polyfills.js', 'src/core.js', 'src/bosh.js', 'src/websocket.js', 'src/wrap_footer.js'], + src: ['src/wrap_header.js', 'src/base64.js', 'src/sha1.js', 'src/md5.js', 'src/polyfills.js', 'src/core.js', 'src/bosh.js', 'src/websocket.js', 'src/webrtc.js', 'src/wrap_footer.js'], dest: '<%= pkg.name %>' }, options: { diff --git a/src/core.js b/src/core.js index ce1e65dc..a70211a5 100644 --- a/src/core.js +++ b/src/core.js @@ -1477,6 +1477,10 @@ Strophe.Connection = function (service, options) if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 || proto.indexOf("ws") === 0) { this._proto = new Strophe.Websocket(this); + } else if (service.indexOf("p2p") === 0) { + this._proto = new Strophe.WebRTC(this, options); + } else if (service.indexOf("lo") === 0) { + this._proto = new Strophe.Loopback(this); } else { this._proto = new Strophe.Bosh(this); } diff --git a/src/webrtc.js b/src/webrtc.js new file mode 100644 index 00000000..32fcc4cf --- /dev/null +++ b/src/webrtc.js @@ -0,0 +1,795 @@ + +/** File: strophe.js + * A JavaScript library to enable XMPP over WebRTC in Strophejs. + * + * This file implements XMPP over WebRTC for Strophejs. + * If a Connection is established with a Websocket url (p2p://...) + * Strophe will use WebRTC instead of BOSH. + * WebRTC support implemented by Jens Bavendiek (bavendiek@dbis.rwth-aachen.de) + * For more information on XMPP-over WebRTC see this RFC draft: + * TBA + */ + +/** PrivateConstructor: Strophe.WebRTC + * Create and initialize a Strophe.WebRTC object. + * Currently only sets the connection Object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use WebRTC. + * + * Returns: + * A new Strophe.WebRTC object. + */ +Strophe.WebRTC = function(connection, options) { + this._conn = connection; + /** PrivateFunction _bodyWrap + * _Private_ helper function to wrap a stanza in a tag. + * This is used so Strophe can process stanzas from WebSockets like BOSH + */ + this._conn._bodyWrap = function (stanza) + { + return $build('body', { + xmlns: Strophe.NS.HTTPBIND + }).cnode(stanza); + }; + + this.options = options; +}; + +Strophe.WebRTC.prototype = { + /** PrivateFunction: _buildStream + * _Private_ helper function to generate the start tag for WebRTC + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildStream: function () + { + return $build("stream:stream", { + "to": this._conn.jid, + "xmlns": Strophe.NS.CLIENT, + "xmlns:stream": Strophe.NS.STREAM, + "version": '1.0' + }); + }, + + /** PrivateFunction: _check_streamerror + * _Private_ checks a message for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + * connectstatus - The ConnectStatus that will be set on error. + * Returns: + * true if there was a streamerror, false otherwise. + */ + _check_streamerror: function (bodyWrap, connectstatus) { + var errors = bodyWrap.getElementsByTagName("stream:error"); + if (errors.length === 0) { + return false; + } + var error = errors[0]; + var condition = error.childNodes[0].tagName; + var text = error.getElementsByTagName("text")[0].textContent; + Strophe.error("WebRTC stream error: " + condition + " - " + text); + + // close the connection on stream_error + this._conn._changeConnectStatus(connectstatus, condition); + this._conn._doDisconnect(); + return true; + }, + + /** PrivateFunction: _connect + * _Private_ function called by Strophe.Connection.connect + * + * Creates a WebRTC for a connection and assigns Callbacks to it. + * Does nothing if there already is a WebRTC. + */ + //TODO + _connect: function (connection, route) { + if(this.options.channel === undefined) { + Console.log("JAB: channel = undefined"); + return false; + } + // Ensure that there is no open WebSocket from a previous Connection. + //this._closeSocket(); + this._onConnection(this.options.channel); + }, + + /** PrivateFunction: _connect_cb + * _Private_ function called by Strophe.Connection._connect_cb + * + * checks for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function(bodyWrap) { + var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); + if (error) { + return Strophe.Status.CONNFAIL; + } + }, + + /** PrivateFunction: _connect_cb_wrapper + * _Private_ function that handles the first connection messages. + * + * On receiving an opening stream tag this callback replaces itself with the real + * message handler. On receiving a stream error the connection is terminated. + */ + _connect_cb_wrapper: function(evt) { + console.log("Wrapper called"); + //Inject namespaces into stream tags. has to be done because no SAX parser is used. + var string = evt.data.replace(//, ""); + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + string = string.replace(//, ""); + + var parser = new DOMParser(); + var elem = parser.parseFromString(string, "text/xml").documentElement; + + if (elem.nodeName == "stream:stream") { + this.channel.onmessage = this._onMessage.bind(this); + this._conn._connect_cb(this._conn._bodyWrap(elem).tree()); + this._conn._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + //_connect_cb will check for stream:error and disconnect on error + this._connect_cb(elem); + } + }, + + /** PrivateFunction: _disconnect + * _Private_ function called by Strophe.Connection.disconnect + * + * Disconnects and sends a last stanza if one is given + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + console.log("Disconnect"); + //TODO + if (this.channel.peerConnection.readyState !== "closed") { + if (pres) { + this._conn.send(pres); + } + var close = ''; + this._conn.xmlOutput(this._conn._bodyWrap(document.createElement("stream:stream"))); + this._conn.rawOutput(close); + try { + this.channel.send(close); + } catch (e) { + Strophe.info("Couldn't send closing stream tag."); + } + + this._conn._doDisconnect(); + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Just closes the Channel for WebRTC + */ + //TODO + _doDisconnect: function () + { + Strophe.info("WebRTC _doDisconnect was called"); + this._closeSocket(); + }, + + /** PrivateFunction: _closeSocket + * _Private_ function to close the DataChannel. + * + * Closes the DataChannel if it is still open and deletes it + */ + //TODO + _closeSocket: function () + { + if (this.channel) { + this.channel.close(); + } + delete this.channel; + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the message queue is empty. + * + * Returns: + * True, because WebRTC messages are send immediately after queueing. + */ + _emptyQueue: function () + { + return true; + }, + + /** PrivateFunction: _onClose + * _Private_ function to handle websockets closing. + * + * Nothing to do here for WebRTCs + */ + _onClose: function(event) { + if(this._conn.connected && !this._conn.disconnecting) { + Strophe.error("DataChannel closed unexcectedly"); + this._conn._doDisconnect(); + } else { + Strophe.info("Websocket closed"); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received. + */ + _no_auth_received: function (_callback) + { + this._conn.authenticated=true; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * This does nothing for WebRTCs + */ + _onDisconnectTimeout: function () {}, + + /** PrivateFunction: _onError + * _Private_ function to handle websockets errors. + * + * Parameters: + * (Object) error - The websocket error. + */ + _onError: function(error) { + Strophe.error("DataChannel error " + error); + }, + + /** PrivateFunction: _onIdle + * _Private_ function called by Strophe.Connection._onIdle + * + * sends all queued stanzas + */ + _onIdle: function () { + var data = this._conn._data; + if (data.length > 0 && !this._conn.paused) { + for (i = 0; i < data.length; i++) { + if (data[i] !== null) { + var stanza, rawStanza; + if (data[i] === "restart") { + stanza = this._buildStream(); + rawStanza = this._removeClosingTag(stanza); + } else { + stanza = data[i]; + rawStanza = Strophe.serialize(stanza); + } + this._conn.xmlOutput(stanza); + this._conn.rawOutput(rawStanza); + this.channel.send(rawStanza); + } + } + this._conn._data = []; + } + }, + + /** PrivateFunction: _onMessage + * _Private_ function to handle websockets messages. + * + * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. + * + * Since all XMPP traffic starts with "" + * The first stanza will always fail to be parsed... + * Addtionnaly, the seconds stanza will always be a with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! + * + * Parameters: + * (string) message - The websocket message. + */ + _onMessage: function(evt) { + // check for closing stream + if (evt.data === "") { + var close = ""; + this._conn.rawInput(close); + this._conn.xmlInput(this._conn._bodyWrap(document.createElement("stream:stream"))); + if (!this._conn.disconnecting) { + this._conn._doDisconnect(); + } + return; + } + var string = evt.data; + if (string.search("xmlns:stream") == -1) { + //Inject namespaces into stream tags if they are missing. Has to be done because no SAX parser is used. + string = string.replace(/]*)>/, ""); + } + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + string = string.replace(//, ""); + + parser = new DOMParser(); + var elem = parser.parseFromString(string, "text/xml").documentElement; + + elem = this._conn._bodyWrap(elem).tree(); + + if (this._check_streamerror(elem, Strophe.Status.ERROR)) { + return; + } + + //handle unavailable presence stanza before disconnecting + if (this._conn.disconnecting && + elem.firstChild.nodeName === "presence" && + elem.firstChild.getAttribute("type") === "unavailable") { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + // if we are already disconnecting we will ignore the unavailable stanza and + // wait for the tag before we close the connection + return; + } else { + this._conn._dataRecv(elem); + } + }, + + /** PrivateFunction: _onConnection + * _Private_ function to handle DataChannel connection setup. + * + * The opening stream tag is sent here. + */ + _onConnection: function(connection) { + this.channel = connection; + Strophe.info("WebRTC datachannel open"); + + this.channel.onerror = this._onError.bind(this); + this.channel.onclose = this._onClose.bind(this); + this.channel.onmessage = this._connect_cb_wrapper.bind(this); + + var start = this._buildStream(); + this._conn.xmlOutput(start); + + var startString = this._removeClosingTag(start); + this._conn.rawOutput(startString); + + this.channel.send( startString); + }, + + /** PrivateFunction: _removeClosingTag + * _Private_ function to Make the first non-selfclosing + * + * Parameters: + * (Object) elem - The tag. + * + * Returns: + * The stream:stream tag as String + */ + _removeClosingTag: function(elem) { + var string = Strophe.serialize(elem); + string = string.replace(/<(stream:stream .*[^/])\/>$/, "<$1>"); + return string; + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * WebRTCs don't use requests, so the passed argument is just returned. + * + * Parameters: + * (Object) stanza - The stanza. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (stanza) + { + return stanza; + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for WebRTC + * + * Just flushes the messages that are in the queue + */ + _send: function () { + this._conn.flush(); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + clearTimeout(this._conn._idleTimeout); + this._conn._onIdle.bind(this._conn)(); + } +}; +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ +PubSubNode = function(name){ + this.name = name; + this.subscribers = {}; + this.callback = onQuery; + + function onQuery(iq){ + console.log("onQuery", iq); + return []; + } +}; + +PubSubNode.prototype = { + subscribe: function(connection){ + if(this.subscribers.hasOwnProperty(connection.jid)) { + return this.subscribers[connection]; + } else{ + var subid = Math.random().toString(36).substr(2, 12); + this.subscribers[connection.jid] = connection; + return subid; + } + }, + publish: function(message){ + for(var conn in this.subscribers){ + if(this.subscribers[conn].connected) { + this.subscribers[conn].send(message); + }else { + delete this.subscribers[conn]; + } + } + //this.subscribers[conn].send(message); + // this.subscribers[conn]._dataRecv($msg().t(message)); TODO Notify how the relay itself gets messages, + } +}; + +Strophe.PubSub = function(){ + this.nodes = {}; //Array holding the nodes from which information may be published. + +}; + +Strophe.PubSub.prototype = { + + createNode: function(nodeName){ + console.log("Attempt to create node "+nodeName); + if(this.nodes.hasOwnProperty(nodeName)){ + return false; + }else { + this.nodes[nodeName] = new PubSubNode(nodeName); + return true; + } + }, + deleteNode: function(nodeName){ + delete this.nodes[nodeName]; + }, +}; +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/** PrivateConstructor: Strophe.Loopback + * Create and initialize a Strophe.Loopback object. + * Currently only sets the connection Object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use Loopback. + * + * Returns: + * A new Strophe.Loopback object. + */ +Strophe.Loopback = function(connection) { + this._conn = connection; +}; + +Strophe.Loopback.prototype = { + /** PrivateFunction: _buildStream + * _Private_ helper function to generate the start tag for Loopback + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildStream: function () + { + return $build("stream:stream", { + "to": this._conn.jid, + "xmlns": Strophe.NS.CLIENT, + "xmlns:stream": Strophe.NS.STREAM, + "version": '1.0' + }); + }, + + /** PrivateFunction: _check_streamerror + * _Private_ checks a message for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + * connectstatus - The ConnectStatus that will be set on error. + * Returns: + * true if there was a streamerror, false otherwise. + */ + _check_streamerror: function (bodyWrap, connectstatus) { + var errors = bodyWrap.getElementsByTagName("stream:error"); + if (errors.length === 0) { + return false; + } + var error = errors[0]; + var condition = error.childNodes[0].tagName; + var text = error.getElementsByTagName("text")[0].textContent; + Strophe.error("Loopback stream error: " + condition + " - " + text); + + // close the connection on stream_error + this._conn._changeConnectStatus(connectstatus, condition); + this._conn._doDisconnect(); + return true; + }, + + /** PrivateFunction: _connect + * _Private_ function called by Strophe.Connection.connect + * + * Creates a Loopback for a connection and assigns Callbacks to it. + * Does nothing if there already is a Loopback. + */ + //TODO + _connect: function (connection, route) { + var start = this._buildStream(); + this._conn.xmlOutput(start); + + var startString = this._removeClosingTag(start); + this._conn.rawOutput(startString); + this.onMessage = this._connect_cb_wrapper.bind(this); + this.send(startString); + }, + + /** PrivateFunction: _connect_cb + * _Private_ function called by Strophe.Connection._connect_cb + * + * checks for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function(bodyWrap) { + var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); + if (error) { + return Strophe.Status.CONNFAIL; + } + }, + + /** PrivateFunction: _connect_cb_wrapper + * _Private_ function that handles the first connection messages. + * + * On receiving an opening stream tag this callback replaces itself with the real + * message handler. On receiving a stream error the connection is terminated. + */ + _connect_cb_wrapper: function(data) { + console.log("Wrapper called"); + //Inject namespaces into stream tags. has to be done because no SAX parser is used. + var string = data.replace(//, ""); + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + string = string.replace(//, ""); + + var parser = new DOMParser(); + var elem = parser.parseFromString(string, "text/xml").documentElement; + + if (elem.nodeName == "stream:stream") { + this.onMessage = this._onMessage.bind(this); + this._conn._connect_cb(this._conn._bodyWrap(elem).tree()); + this._conn._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + //_connect_cb will check for stream:error and disconnect on error + this._connect_cb(elem); + } + }, + + + + /** PrivateFunction: _disconnect + * _Private_ function called by Strophe.Connection.disconnect + * + * Disconnects and sends a last stanza if one is given + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + console.log("Disconnect"); + var close = ''; + this._conn.xmlOutput(this._conn._bodyWrap(document.createElement("stream:stream"))); + this._conn.rawOutput(close); + try { + this.send(close); + } catch (e) { + Strophe.info("Couldn't send closing stream tag."); + } + this._conn._doDisconnect(); + + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the message queue is empty. + * + * Returns: + * True, because Loopback messages are send immediately after queueing. + */ + _emptyQueue: function () + { + return true; + }, + + /** PrivateFunction: _onClose + * _Private_ function to handle websockets closing. + * + * Nothing to do here for Loopbacks + */ + _onClose: function(event) { + if(this._conn.connected && !this._conn.disconnecting) { + Strophe.error("Loopback closed unexcectedly"); + this._conn._doDisconnect(); + } else { + Strophe.info("Loopback closed"); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received. + */ + _no_auth_received: function (_callback) + { + this._conn.authenticated=true; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * This does nothing for Loopbacks + */ + _onDisconnectTimeout: function () {}, + + /** PrivateFunction: _onError + * _Private_ function to handle websockets errors. + * + * Parameters: + * (Object) error - The websocket error. + */ + _onError: function(error) { + Strophe.error("Loopback error " + error); + }, + + /** PrivateFunction: _onIdle + * _Private_ function called by Strophe.Connection._onIdle + * + * sends all queued stanzas + */ + _onIdle: function () { + var data = this._conn._data; + if (data.length > 0 && !this._conn.paused) { + for (i = 0; i < data.length; i++) { + if (data[i] !== null) { + var stanza, rawStanza; + if (data[i] === "restart") { + stanza = this._buildStream(); + rawStanza = this._removeClosingTag(stanza); + } else { + stanza = data[i]; + rawStanza = Strophe.serialize(stanza); + } + this._conn.xmlOutput(stanza); + this._conn.rawOutput(rawStanza); + this.send(rawStanza); + } + } + this._conn._data = []; + } + }, + + /** PrivateFunction: _onMessage + * _Private_ function to handle websockets messages. + * + * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. + * + * Since all XMPP traffic starts with "" + * The first stanza will always fail to be parsed... + * Addtionnaly, the seconds stanza will always be a with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! + * + * Parameters: + * (string) message - The websocket message. + */ + _onMessage: function(data) { + // check for closing stream + if (data === "") { + var close = ""; + this._conn.rawInput(close); + this._conn.xmlInput(this._conn._bodyWrap(document.createElement("stream:stream"))); + if (!this._conn.disconnecting) { + this._conn._doDisconnect(); + } + return; + } + var string = data; + if (string.search("xmlns:stream") == -1) { + //Inject namespaces into stream tags if they are missing. Has to be done because no SAX parser is used. + string = string.replace(/]*)>/, ""); + } + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + string = string.replace(//, ""); + + parser = new DOMParser(); + var elem = parser.parseFromString(string, "text/xml").documentElement; + + elem = this._conn._bodyWrap(elem).tree(); + + if (this._check_streamerror(elem, Strophe.Status.ERROR)) { + return; + } + + //handle unavailable presence stanza before disconnecting + if (this._conn.disconnecting && + elem.firstChild.nodeName === "presence" && + elem.firstChild.getAttribute("type") === "unavailable") { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + // if we are already disconnecting we will ignore the unavailable stanza and + // wait for the tag before we close the connection + return; + } else { + this._conn._dataRecv(elem); + } + }, + + /** PrivateFunction: _removeClosingTag + * _Private_ function to Make the first non-selfclosing + * + * Parameters: + * (Object) elem - The tag. + * + * Returns: + * The stream:stream tag as String + */ + _removeClosingTag: function(elem) { + var string = Strophe.serialize(elem); + string = string.replace(/<(stream:stream .*[^/])\/>$/, "<$1>"); + return string; + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * Loopbacks don't use requests, so the passed argument is just returned. + * + * Parameters: + * (Object) stanza - The stanza. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (stanza) + { + return stanza; + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for Loopback + * + * Just flushes the messages that are in the queue + */ + _send: function () { + this._conn.flush(); + }, + + send:function(message){ + this.onMessage(message); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + clearTimeout(this._conn._idleTimeout); + this._conn._onIdle.bind(this._conn)(); + } +};