From f9846a27b6d415426de95f428a414c58464a8ef1 Mon Sep 17 00:00:00 2001
From: Tjardick van der Kraan
Date: Tue, 2 Dec 2014 11:22:00 +0100
Subject: [PATCH 01/18] Added the needed libssl-dev package
---
README.rst | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.rst b/README.rst
index 5e020081a8..0b2b00412d 100644
--- a/README.rst
+++ b/README.rst
@@ -94,7 +94,8 @@ header files for python C extensions.
Installing prerequisites on Ubuntu or Debian::
$ sudo apt-get install build-essential python2.7-dev libffi-dev \
- python-pip python-setuptools sqlite3
+ python-pip python-setuptools sqlite3 \
+ libssl-dev
Installing prerequisites on Mac OS X::
From 758052d7f841c6ac8331d6dd0b419bb249fe770b Mon Sep 17 00:00:00 2001
From: David Baker
Date: Tue, 2 Dec 2014 12:01:30 +0000
Subject: [PATCH 02/18] Add non-working jitsi meet bridge
---
contrib/jitsimeetbridge/jitsimeetbridge.py | 260 ++
.../syweb-jitsi-conference.patch | 188 +
contrib/jitsimeetbridge/unjingle/.swp | Bin 0 -> 12288 bytes
.../unjingle/strophe.jingle.sdp.js | 712 ++++
.../unjingle/strophe.jingle.sdp.util.js | 408 +++
.../unjingle/strophe/XMLHttpRequest.js | 254 ++
.../unjingle/strophe/base64.js | 83 +
.../jitsimeetbridge/unjingle/strophe/md5.js | 279 ++
.../unjingle/strophe/strophe.js | 3256 +++++++++++++++++
contrib/jitsimeetbridge/unjingle/unjingle.js | 48 +
10 files changed, 5488 insertions(+)
create mode 100644 contrib/jitsimeetbridge/jitsimeetbridge.py
create mode 100644 contrib/jitsimeetbridge/syweb-jitsi-conference.patch
create mode 100644 contrib/jitsimeetbridge/unjingle/.swp
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/base64.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/md5.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/strophe.js
create mode 100644 contrib/jitsimeetbridge/unjingle/unjingle.js
diff --git a/contrib/jitsimeetbridge/jitsimeetbridge.py b/contrib/jitsimeetbridge/jitsimeetbridge.py
new file mode 100644
index 0000000000..dbc6f6ffa5
--- /dev/null
+++ b/contrib/jitsimeetbridge/jitsimeetbridge.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+"""
+This is an attempt at bridging matrix clients into a Jitis meet room via Matrix
+video call. It uses hard-coded xml strings overg XMPP BOSH. It can display one
+of the streams from the Jitsi bridge until the second lot of SDP comes down and
+we set the remote SDP at which point the stream ends. Our video never gets to
+the bridge.
+
+Requires:
+npm install jquery jsdom
+"""
+
+import gevent
+import grequests
+from BeautifulSoup import BeautifulSoup
+import json
+import urllib
+import subprocess
+import time
+
+#ACCESS_TOKEN="" #
+
+MATRIXBASE = 'https://matrix.org/_matrix/client/api/v1/'
+MYUSERNAME = '@davetest:matrix.org'
+
+HTTPBIND = 'https://meet.jit.si/http-bind'
+#HTTPBIND = 'https://jitsi.vuc.me/http-bind'
+#ROOMNAME = "matrix"
+ROOMNAME = "pibble"
+
+HOST="guest.jit.si"
+#HOST="jitsi.vuc.me"
+
+TURNSERVER="turn.guest.jit.si"
+#TURNSERVER="turn.jitsi.vuc.me"
+
+ROOMDOMAIN="meet.jit.si"
+#ROOMDOMAIN="conference.jitsi.vuc.me"
+
+class TrivialMatrixClient:
+ def __init__(self, access_token):
+ self.token = None
+ self.access_token = access_token
+
+ def getEvent(self):
+ while True:
+ url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
+ if self.token:
+ url += "&from="+self.token
+ req = grequests.get(url)
+ resps = grequests.map([req])
+ obj = json.loads(resps[0].content)
+ print "incoming from matrix",obj
+ if 'end' not in obj:
+ continue
+ self.token = obj['end']
+ if len(obj['chunk']):
+ return obj['chunk'][0]
+
+ def joinRoom(self, roomId):
+ url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
+ print url
+ headers={ 'Content-Type': 'application/json' }
+ req = grequests.post(url, headers=headers, data='{}')
+ resps = grequests.map([req])
+ obj = json.loads(resps[0].content)
+ print "response: ",obj
+
+ def sendEvent(self, roomId, evType, event):
+ url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
+ print url
+ print json.dumps(event)
+ headers={ 'Content-Type': 'application/json' }
+ req = grequests.post(url, headers=headers, data=json.dumps(event))
+ resps = grequests.map([req])
+ obj = json.loads(resps[0].content)
+ print "response: ",obj
+
+
+
+xmppClients = {}
+
+
+def matrixLoop():
+ while True:
+ ev = matrixCli.getEvent()
+ print ev
+ if ev['type'] == 'm.room.member':
+ print 'membership event'
+ if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
+ roomId = ev['room_id']
+ print "joining room %s" % (roomId)
+ matrixCli.joinRoom(roomId)
+ elif ev['type'] == 'm.room.message':
+ if ev['room_id'] in xmppClients:
+ print "already have a bridge for that user, ignoring"
+ continue
+ print "got message, connecting"
+ xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
+ gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
+ elif ev['type'] == 'm.call.invite':
+ print "Incoming call"
+ #sdp = ev['content']['offer']['sdp']
+ #print "sdp: %s" % (sdp)
+ #xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
+ #gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
+ elif ev['type'] == 'm.call.answer':
+ print "Call answered"
+ sdp = ev['content']['answer']['sdp']
+ if ev['room_id'] not in xmppClients:
+ print "We didn't have a call for that room"
+ continue
+ # should probably check call ID too
+ xmppCli = xmppClients[ev['room_id']]
+ xmppCli.sendAnswer(sdp)
+ elif ev['type'] == 'm.call.hangup':
+ if ev['room_id'] in xmppClients:
+ xmppClients[ev['room_id']].stop()
+ del xmppClients[ev['room_id']]
+
+class TrivialXmppClient:
+ def __init__(self, matrixRoom, userId):
+ self.rid = 0
+ self.matrixRoom = matrixRoom
+ self.userId = userId
+ self.running = True
+
+ def stop(self):
+ self.running = False
+
+ def nextRid(self):
+ self.rid += 1
+ return '%d' % (self.rid)
+
+ def sendIq(self, xml):
+ fullXml = "%s" % (self.nextRid(), self.sid, xml)
+ #print "\t>>>%s" % (fullXml)
+ return self.xmppPoke(fullXml)
+
+ def xmppPoke(self, xml):
+ headers = {'Content-Type': 'application/xml'}
+ req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
+ resps = grequests.map([req])
+ obj = BeautifulSoup(resps[0].content)
+ return obj
+
+ def sendAnswer(self, answer):
+ print "sdp from matrix client",answer
+ p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ jingle, out_err = p.communicate(answer)
+ jingle = jingle % {
+ 'tojid': self.callfrom,
+ 'action': 'session-accept',
+ 'initiator': self.callfrom,
+ 'responder': self.jid,
+ 'sid': self.callsid
+ }
+ print "answer jingle from sdp",jingle
+ res = self.sendIq(jingle)
+ print "reply from answer: ",res
+
+ self.ssrcs = {}
+ jingleSoup = BeautifulSoup(jingle)
+ for cont in jingleSoup.iq.jingle.findAll('content'):
+ if cont.description:
+ self.ssrcs[cont['name']] = cont.description['ssrc']
+ print "my ssrcs:",self.ssrcs
+
+ gevent.joinall([
+ gevent.spawn(self.advertiseSsrcs)
+ ])
+
+ def advertiseSsrcs(self):
+ time.sleep(7)
+ print "SSRC spammer started"
+ while self.running:
+ ssrcMsg = "%(nick)s" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
+ res = self.sendIq(ssrcMsg)
+ print "reply from ssrc announce: ",res
+ time.sleep(10)
+
+
+
+ def xmppLoop(self):
+ self.matrixCallId = time.time()
+ res = self.xmppPoke("" % (self.nextRid(), HOST))
+
+ print res
+ self.sid = res.body['sid']
+ print "sid %s" % (self.sid)
+
+ res = self.sendIq("")
+
+ res = self.xmppPoke("" % (self.nextRid(), self.sid, HOST))
+
+ res = self.sendIq("")
+ print res
+
+ self.jid = res.body.iq.bind.jid.string
+ print "jid: %s" % (self.jid)
+ self.shortJid = self.jid.split('-')[0]
+
+ res = self.sendIq("")
+
+ #randomthing = res.body.iq['to']
+ #whatsitpart = randomthing.split('-')[0]
+
+ #print "other random bind thing: %s" % (randomthing)
+
+ # advertise preence to the jitsi room, with our nick
+ res = self.sendIq("%s" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
+ self.muc = {'users': []}
+ for p in res.body.findAll('presence'):
+ u = {}
+ u['shortJid'] = p['from'].split('/')[1]
+ if p.c and p.c.nick:
+ u['nick'] = p.c.nick.string
+ self.muc['users'].append(u)
+ print "muc: ",self.muc
+
+ # wait for stuff
+ while True:
+ print "waiting..."
+ res = self.sendIq("")
+ print "got from stream: ",res
+ if res.body.iq:
+ jingles = res.body.iq.findAll('jingle')
+ if len(jingles):
+ self.callfrom = res.body.iq['from']
+ self.handleInvite(jingles[0])
+ elif 'type' in res.body and res.body['type'] == 'terminate':
+ self.running = False
+ del xmppClients[self.matrixRoom]
+ return
+
+ def handleInvite(self, jingle):
+ self.initiator = jingle['initiator']
+ self.callsid = jingle['sid']
+ p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ print "raw jingle invite",str(jingle)
+ sdp, out_err = p.communicate(str(jingle))
+ print "transformed remote offer sdp",sdp
+ inviteEvent = {
+ 'offer': {
+ 'type': 'offer',
+ 'sdp': sdp
+ },
+ 'call_id': self.matrixCallId,
+ 'version': 0,
+ 'lifetime': 30000
+ }
+ matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
+
+matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
+
+gevent.joinall([
+ gevent.spawn(matrixLoop)
+])
+
diff --git a/contrib/jitsimeetbridge/syweb-jitsi-conference.patch b/contrib/jitsimeetbridge/syweb-jitsi-conference.patch
new file mode 100644
index 0000000000..aed23c78aa
--- /dev/null
+++ b/contrib/jitsimeetbridge/syweb-jitsi-conference.patch
@@ -0,0 +1,188 @@
+diff --git a/syweb/webclient/app/components/matrix/matrix-call.js b/syweb/webclient/app/components/matrix/matrix-call.js
+index 9fbfff0..dc68077 100644
+--- a/syweb/webclient/app/components/matrix/matrix-call.js
++++ b/syweb/webclient/app/components/matrix/matrix-call.js
+@@ -16,6 +16,45 @@ limitations under the License.
+
+ 'use strict';
+
++
++function sendKeyframe(pc) {
++ console.log('sendkeyframe', pc.iceConnectionState);
++ if (pc.iceConnectionState !== 'connected') return; // safe...
++ pc.setRemoteDescription(
++ pc.remoteDescription,
++ function () {
++ pc.createAnswer(
++ function (modifiedAnswer) {
++ pc.setLocalDescription(
++ modifiedAnswer,
++ function () {
++ // noop
++ },
++ function (error) {
++ console.log('triggerKeyframe setLocalDescription failed', error);
++ messageHandler.showError();
++ }
++ );
++ },
++ function (error) {
++ console.log('triggerKeyframe createAnswer failed', error);
++ messageHandler.showError();
++ }
++ );
++ },
++ function (error) {
++ console.log('triggerKeyframe setRemoteDescription failed', error);
++ messageHandler.showError();
++ }
++ );
++}
++
++
++
++
++
++
++
+ var forAllVideoTracksOnStream = function(s, f) {
+ var tracks = s.getVideoTracks();
+ for (var i = 0; i < tracks.length; i++) {
+@@ -83,7 +122,7 @@ angular.module('MatrixCall', [])
+ }
+
+ // FIXME: we should prevent any calls from being placed or accepted before this has finished
+- MatrixCall.getTurnServer();
++ //MatrixCall.getTurnServer();
+
+ MatrixCall.CALL_TIMEOUT = 60000;
+ MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
+@@ -132,6 +171,22 @@ angular.module('MatrixCall', [])
+ pc.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
+ pc.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
+ pc.onaddstream = function(s) { self.onAddStream(s); };
++
++ var datachan = pc.createDataChannel('RTCDataChannel', {
++ reliable: false
++ });
++ console.log("data chan: "+datachan);
++ datachan.onopen = function() {
++ console.log("data channel open");
++ };
++ datachan.onmessage = function() {
++ console.log("data channel message");
++ };
++ pc.ondatachannel = function(event) {
++ console.log("have data channel");
++ event.channel.binaryType = 'blob';
++ };
++
+ return pc;
+ }
+
+@@ -200,6 +255,12 @@ angular.module('MatrixCall', [])
+ }, this.msg.lifetime - event.age);
+ };
+
++ MatrixCall.prototype.receivedInvite = function(event) {
++ console.log("Got second invite for call "+this.call_id);
++ this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
++ };
++
++
+ // perverse as it may seem, sometimes we want to instantiate a call with a hangup message
+ // (because when getting the state of the room on load, events come in reverse order and
+ // we want to remember that a call has been hung up)
+@@ -349,7 +410,7 @@ angular.module('MatrixCall', [])
+ 'mandatory': {
+ 'OfferToReceiveAudio': true,
+ 'OfferToReceiveVideo': this.type == 'video'
+- },
++ }
+ };
+ this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
+ // This can't be in an apply() because it's called by a predecessor call under glare conditions :(
+@@ -359,8 +420,20 @@ angular.module('MatrixCall', [])
+ MatrixCall.prototype.gotLocalIceCandidate = function(event) {
+ if (event.candidate) {
+ console.log("Got local ICE "+event.candidate.sdpMid+" candidate: "+event.candidate.candidate);
+- this.sendCandidate(event.candidate);
+- }
++ //this.sendCandidate(event.candidate);
++ } else {
++ console.log("have all candidates, sending answer");
++ var content = {
++ version: 0,
++ call_id: this.call_id,
++ answer: this.peerConn.localDescription
++ };
++ this.sendEventWithRetry('m.call.answer', content);
++ var self = this;
++ $rootScope.$apply(function() {
++ self.state = 'connecting';
++ });
++ }
+ }
+
+ MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
+@@ -418,15 +491,6 @@ angular.module('MatrixCall', [])
+ console.log("Created answer: "+description);
+ var self = this;
+ this.peerConn.setLocalDescription(description, function() {
+- var content = {
+- version: 0,
+- call_id: self.call_id,
+- answer: self.peerConn.localDescription
+- };
+- self.sendEventWithRetry('m.call.answer', content);
+- $rootScope.$apply(function() {
+- self.state = 'connecting';
+- });
+ }, function() { console.log("Error setting local description!"); } );
+ };
+
+@@ -448,6 +512,9 @@ angular.module('MatrixCall', [])
+ $rootScope.$apply(function() {
+ self.state = 'connected';
+ self.didConnect = true;
++ /*$timeout(function() {
++ sendKeyframe(self.peerConn);
++ }, 1000);*/
+ });
+ } else if (this.peerConn.iceConnectionState == 'failed') {
+ this.hangup('ice_failed');
+@@ -518,6 +585,7 @@ angular.module('MatrixCall', [])
+
+ MatrixCall.prototype.onRemoteStreamEnded = function(event) {
+ console.log("Remote stream ended");
++ return;
+ var self = this;
+ $rootScope.$apply(function() {
+ self.state = 'ended';
+diff --git a/syweb/webclient/app/components/matrix/matrix-phone-service.js b/syweb/webclient/app/components/matrix/matrix-phone-service.js
+index 55dbbf5..272fa27 100644
+--- a/syweb/webclient/app/components/matrix/matrix-phone-service.js
++++ b/syweb/webclient/app/components/matrix/matrix-phone-service.js
+@@ -48,6 +48,13 @@ angular.module('matrixPhoneService', [])
+ return;
+ }
+
++ // do we already have an entry for this call ID?
++ var existingEntry = matrixPhoneService.allCalls[msg.call_id];
++ if (existingEntry) {
++ existingEntry.receivedInvite(msg);
++ return;
++ }
++
+ var call = undefined;
+ if (!isLive) {
+ // if this event wasn't live then this call may already be over
+@@ -108,7 +115,7 @@ angular.module('matrixPhoneService', [])
+ call.hangup();
+ }
+ } else {
+- $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
++ $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
+ }
+ } else if (event.type == 'm.call.answer') {
+ var call = matrixPhoneService.allCalls[msg.call_id];
diff --git a/contrib/jitsimeetbridge/unjingle/.swp b/contrib/jitsimeetbridge/unjingle/.swp
new file mode 100644
index 0000000000000000000000000000000000000000..449c08da561e6bb3b4429ae8e8817bf8c07463d1
GIT binary patch
literal 12288
zcmeHNOK;;g5O#X&t!V#%+M-bRpqP4D5@8!ioGhAl69g!-MSG(t%2uKWSEBMb^+)!{
zw8su5+iN*;)<}wM4>STxqBt|(d>now!3oj(vvbWmvm;A2OuNt0yqf5BndAG6bKDMNQ`ZO;Ns&+VAAW4l-soDACn6!DDCOw-
z0&bM7;zb^@=EnL0Zd6Gm=fdF)#GIE!&3A<)oL9J+0mv`{Tb7YrBoyEsUtOHMI|UE4
z$OYina+0x$PU2qSKxAqFBP_%M
zK!h;yBXtWQI35mJC5q}WV#B-|wi9Hmh*h*qxMcFlyi4!_oM$Xb$SSYtn3IRJAdw*+
zIMXDPrU{eU+zVb;x<=E2-jqZD8IF$-utpao&shp+Er7~N2}}uAF0Ha82QV4awOf+b
zU}d6)lFdkcagn`>d-m(mtP|H9ZN)XE$4!SVbmZ)GVOx8-fb81bFK6dh9Sqy4_e1bE
z?2n#dZ=%u4!|bACzh?p_a(6h`m&EllYysGh0MlA0AXv)yqR5%SZlxe>R?=fVcXf_&1tC*smUAHHrH1rpcp
z&%Zf$%s&6+kHG4)$Tsl>u@#1%Z5pv-Muvm@zz9*~86ow224^GRw=@^UBhPi2F$&y~
z;aJo+h;P{jVwUN9mQP&D`XhC7*(X?^pRSjE(F`|t@N`@X%8jhPm-@LOYSXbONe<#)
z3aySyGN0%y5`kj|UdnGO+tW3E2g2p1MHVgQ+`JWtq3eo?ArL$#ioO2CwS)JV;`FVK6bDj;1dTQpW>s_@o)nPkg|pg=gTg&Pm5`L
z)$6yIHg2y@uVwcAB`D@bhY3KXI~_)@M^hMa_$2+ZyDjI*Rgmp$qEnhdkFUe_Do#pS
zyU+HP6MFL^2lJws8zsv^>1C_Wt?
z;HCneEx}J&cLk@B?Y$-V;@$85a8#YKku|&g@Ke$!XdMYQ&45KC;u{Pejts|zFJhs<
z@IpTzw&@|td<{9SN05z-*kZPnU}}&kCPr)rPBd~yJ|W9ThHm>>!CpTt?GdaGahH*O
X(QH+rlHlh^@UVTOfjymkm=690r{`%W
literal 0
HcmV?d00001
diff --git a/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js
new file mode 100644
index 0000000000..e99dd7bf96
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js
@@ -0,0 +1,712 @@
+/* jshint -W117 */
+// SDP STUFF
+function SDP(sdp) {
+ this.media = sdp.split('\r\nm=');
+ for (var i = 1; i < this.media.length; i++) {
+ this.media[i] = 'm=' + this.media[i];
+ if (i != this.media.length - 1) {
+ this.media[i] += '\r\n';
+ }
+ }
+ this.session = this.media.shift() + '\r\n';
+ this.raw = this.session + this.media.join('');
+}
+
+exports.SDP = SDP;
+
+var jsdom = require("jsdom");
+var window = jsdom.jsdom().parentWindow;
+var $ = require('jquery')(window);
+
+var SDPUtil = require('./strophe.jingle.sdp.util.js').SDPUtil;
+
+/**
+ * Returns map of MediaChannel mapped per channel idx.
+ */
+SDP.prototype.getMediaSsrcMap = function() {
+ var self = this;
+ var media_ssrcs = {};
+ for (channelNum = 0; channelNum < self.media.length; channelNum++) {
+ modified = true;
+ tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc:');
+ var type = SDPUtil.parse_mid(SDPUtil.find_line(self.media[channelNum], 'a=mid:'));
+ var channel = new MediaChannel(channelNum, type);
+ media_ssrcs[channelNum] = channel;
+ tmp.forEach(function (line) {
+ var linessrc = line.substring(7).split(' ')[0];
+ // allocate new ChannelSsrc
+ if(!channel.ssrcs[linessrc]) {
+ channel.ssrcs[linessrc] = new ChannelSsrc(linessrc, type);
+ }
+ channel.ssrcs[linessrc].lines.push(line);
+ });
+ tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc-group:');
+ tmp.forEach(function(line){
+ var semantics = line.substr(0, idx).substr(13);
+ var ssrcs = line.substr(14 + semantics.length).split(' ');
+ if (ssrcs.length != 0) {
+ var ssrcGroup = new ChannelSsrcGroup(semantics, ssrcs);
+ channel.ssrcGroups.push(ssrcGroup);
+ }
+ });
+ }
+ return media_ssrcs;
+};
+/**
+ * Returns true if this SDP contains given SSRC.
+ * @param ssrc the ssrc to check.
+ * @returns {boolean} true if this SDP contains given SSRC.
+ */
+SDP.prototype.containsSSRC = function(ssrc) {
+ var channels = this.getMediaSsrcMap();
+ var contains = false;
+ Object.keys(channels).forEach(function(chNumber){
+ var channel = channels[chNumber];
+ //console.log("Check", channel, ssrc);
+ if(Object.keys(channel.ssrcs).indexOf(ssrc) != -1){
+ contains = true;
+ }
+ });
+ return contains;
+};
+
+/**
+ * Returns map of MediaChannel that contains only media not contained in otherSdp. Mapped by channel idx.
+ * @param otherSdp the other SDP to check ssrc with.
+ */
+SDP.prototype.getNewMedia = function(otherSdp) {
+
+ // this could be useful in Array.prototype.
+ function arrayEquals(array) {
+ // if the other array is a falsy value, return
+ if (!array)
+ return false;
+
+ // compare lengths - can save a lot of time
+ if (this.length != array.length)
+ return false;
+
+ for (var i = 0, l=this.length; i < l; i++) {
+ // Check if we have nested arrays
+ if (this[i] instanceof Array && array[i] instanceof Array) {
+ // recurse into the nested arrays
+ if (!this[i].equals(array[i]))
+ return false;
+ }
+ else if (this[i] != array[i]) {
+ // Warning - two different object instances will never be equal: {x:20} != {x:20}
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var myMedia = this.getMediaSsrcMap();
+ var othersMedia = otherSdp.getMediaSsrcMap();
+ var newMedia = {};
+ Object.keys(othersMedia).forEach(function(channelNum) {
+ var myChannel = myMedia[channelNum];
+ var othersChannel = othersMedia[channelNum];
+ if(!myChannel && othersChannel) {
+ // Add whole channel
+ newMedia[channelNum] = othersChannel;
+ return;
+ }
+ // Look for new ssrcs accross the channel
+ Object.keys(othersChannel.ssrcs).forEach(function(ssrc) {
+ if(Object.keys(myChannel.ssrcs).indexOf(ssrc) === -1) {
+ // Allocate channel if we've found ssrc that doesn't exist in our channel
+ if(!newMedia[channelNum]){
+ newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
+ }
+ newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
+ }
+ });
+
+ // Look for new ssrc groups across the channels
+ othersChannel.ssrcGroups.forEach(function(otherSsrcGroup){
+
+ // try to match the other ssrc-group with an ssrc-group of ours
+ var matched = false;
+ for (var i = 0; i < myChannel.ssrcGroups.length; i++) {
+ var mySsrcGroup = myChannel.ssrcGroups[i];
+ if (otherSsrcGroup.semantics == mySsrcGroup.semantics
+ && arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
+
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ // Allocate channel if we've found an ssrc-group that doesn't
+ // exist in our channel
+
+ if(!newMedia[channelNum]){
+ newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
+ }
+ newMedia[channelNum].ssrcGroups.push(otherSsrcGroup);
+ }
+ });
+ });
+ return newMedia;
+};
+
+// remove iSAC and CN from SDP
+SDP.prototype.mangle = function () {
+ var i, j, mline, lines, rtpmap, newdesc;
+ for (i = 0; i < this.media.length; i++) {
+ lines = this.media[i].split('\r\n');
+ lines.pop(); // remove empty last element
+ mline = SDPUtil.parse_mline(lines.shift());
+ if (mline.media != 'audio')
+ continue;
+ newdesc = '';
+ mline.fmt.length = 0;
+ for (j = 0; j < lines.length; j++) {
+ if (lines[j].substr(0, 9) == 'a=rtpmap:') {
+ rtpmap = SDPUtil.parse_rtpmap(lines[j]);
+ if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
+ continue;
+ mline.fmt.push(rtpmap.id);
+ newdesc += lines[j] + '\r\n';
+ } else {
+ newdesc += lines[j] + '\r\n';
+ }
+ }
+ this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
+ this.media[i] += newdesc;
+ }
+ this.raw = this.session + this.media.join('');
+};
+
+// remove lines matching prefix from session section
+SDP.prototype.removeSessionLines = function(prefix) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.session, prefix);
+ lines.forEach(function(line) {
+ self.session = self.session.replace(line + '\r\n', '');
+ });
+ this.raw = this.session + this.media.join('');
+ return lines;
+}
+// remove lines matching prefix from a media section specified by mediaindex
+// TODO: non-numeric mediaindex could match mid
+SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
+ lines.forEach(function(line) {
+ self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
+ });
+ this.raw = this.session + this.media.join('');
+ return lines;
+}
+
+// add content's to a jingle element
+SDP.prototype.toJingle = function (elem, thecreator) {
+ var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
+ var self = this;
+ // new bundle plan
+ if (SDPUtil.find_line(this.session, 'a=group:')) {
+ lines = SDPUtil.find_lines(this.session, 'a=group:');
+ for (i = 0; i < lines.length; i++) {
+ tmp = lines[i].split(' ');
+ var semantics = tmp.shift().substr(8);
+ elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
+ for (j = 0; j < tmp.length; j++) {
+ elem.c('content', {name: tmp[j]}).up();
+ }
+ elem.up();
+ }
+ }
+ // old bundle plan, to be removed
+ var bundle = [];
+ if (SDPUtil.find_line(this.session, 'a=group:BUNDLE')) {
+ bundle = SDPUtil.find_line(this.session, 'a=group:BUNDLE ').split(' ');
+ bundle.shift();
+ }
+ for (i = 0; i < this.media.length; i++) {
+ mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
+ if (!(mline.media === 'audio' ||
+ mline.media === 'video' ||
+ mline.media === 'application'))
+ {
+ continue;
+ }
+ if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
+ ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
+ } else {
+ ssrc = false;
+ }
+
+ elem.c('content', {creator: thecreator, name: mline.media});
+ if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
+ // prefer identifier from a=mid if present
+ var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
+ elem.attrs({ name: mid });
+
+ // old BUNDLE plan, to be removed
+ if (bundle.indexOf(mid) !== -1) {
+ elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
+ bundle.splice(bundle.indexOf(mid), 1);
+ }
+ }
+
+ if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
+ {
+ elem.c('description',
+ {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
+ media: mline.media });
+ if (ssrc) {
+ elem.attrs({ssrc: ssrc});
+ }
+ for (j = 0; j < mline.fmt.length; j++) {
+ rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
+ elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
+ // put any 'a=fmtp:' + mline.fmt[j] lines into
+ if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
+ tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
+ for (k = 0; k < tmp.length; k++) {
+ elem.c('parameter', tmp[k]).up();
+ }
+ }
+ this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
+
+ elem.up();
+ }
+ if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
+ elem.c('encryption', {required: 1});
+ var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
+ crypto.forEach(function(line) {
+ elem.c('crypto', SDPUtil.parse_crypto(line)).up();
+ });
+ elem.up(); // end of encryption
+ }
+
+ if (ssrc) {
+ // new style mapping
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ // FIXME: group by ssrc and support multiple different ssrcs
+ var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
+ ssrclines.forEach(function(line) {
+ idx = line.indexOf(' ');
+ var linessrc = line.substr(0, idx).substr(7);
+ if (linessrc != ssrc) {
+ elem.up();
+ ssrc = linessrc;
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ }
+ var kv = line.substr(idx + 1);
+ elem.c('parameter');
+ if (kv.indexOf(':') == -1) {
+ elem.attrs({ name: kv });
+ } else {
+ elem.attrs({ name: kv.split(':', 2)[0] });
+ elem.attrs({ value: kv.split(':', 2)[1] });
+ }
+ elem.up();
+ });
+ elem.up();
+
+ // old proprietary mapping, to be removed at some point
+ tmp = SDPUtil.parse_ssrc(this.media[i]);
+ tmp.xmlns = 'http://estos.de/ns/ssrc';
+ tmp.ssrc = ssrc;
+ elem.c('ssrc', tmp).up(); // ssrc is part of description
+
+ // XEP-0339 handle ssrc-group attributes
+ var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
+ ssrc_group_lines.forEach(function(line) {
+ idx = line.indexOf(' ');
+ var semantics = line.substr(0, idx).substr(13);
+ var ssrcs = line.substr(14 + semantics.length).split(' ');
+ if (ssrcs.length != 0) {
+ elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ ssrcs.forEach(function(ssrc) {
+ elem.c('source', { ssrc: ssrc })
+ .up();
+ });
+ elem.up();
+ }
+ });
+ }
+
+ if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
+ elem.c('rtcp-mux').up();
+ }
+
+ // XEP-0293 -- map a=rtcp-fb:*
+ this.RtcpFbToJingle(i, elem, '*');
+
+ // XEP-0294
+ if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
+ lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
+ for (j = 0; j < lines.length; j++) {
+ tmp = SDPUtil.parse_extmap(lines[j]);
+ elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
+ uri: tmp.uri,
+ id: tmp.value });
+ if (tmp.hasOwnProperty('direction')) {
+ switch (tmp.direction) {
+ case 'sendonly':
+ elem.attrs({senders: 'responder'});
+ break;
+ case 'recvonly':
+ elem.attrs({senders: 'initiator'});
+ break;
+ case 'sendrecv':
+ elem.attrs({senders: 'both'});
+ break;
+ case 'inactive':
+ elem.attrs({senders: 'none'});
+ break;
+ }
+ }
+ // TODO: handle params
+ elem.up();
+ }
+ }
+ elem.up(); // end of description
+ }
+
+ // map ice-ufrag/pwd, dtls fingerprint, candidates
+ this.TransportToJingle(i, elem);
+
+ if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
+ elem.attrs({senders: 'both'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
+ elem.attrs({senders: 'initiator'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
+ elem.attrs({senders: 'responder'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
+ elem.attrs({senders: 'none'});
+ }
+ if (mline.port == '0') {
+ // estos hack to reject an m-line
+ elem.attrs({senders: 'rejected'});
+ }
+ elem.up(); // end of content
+ }
+ elem.up();
+ return elem;
+};
+
+SDP.prototype.TransportToJingle = function (mediaindex, elem) {
+ var i = mediaindex;
+ var tmp;
+ var self = this;
+ elem.c('transport');
+
+ // XEP-0343 DTLS/SCTP
+ if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
+ {
+ var sctpmap = SDPUtil.find_line(
+ this.media[i], 'a=sctpmap:', self.session);
+ if (sctpmap)
+ {
+ var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
+ elem.c('sctpmap',
+ {
+ xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
+ number: sctpAttrs[0], /* SCTP port */
+ protocol: sctpAttrs[1], /* protocol */
+ });
+ // Optional stream count attribute
+ if (sctpAttrs.length > 2)
+ elem.attrs({ streams: sctpAttrs[2]});
+ elem.up();
+ }
+ }
+ // XEP-0320
+ var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
+ fingerprints.forEach(function(line) {
+ tmp = SDPUtil.parse_fingerprint(line);
+ tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
+ elem.c('fingerprint').t(tmp.fingerprint);
+ delete tmp.fingerprint;
+ line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
+ if (line) {
+ tmp.setup = line.substr(8);
+ }
+ elem.attrs(tmp);
+ elem.up(); // end of fingerprint
+ });
+ tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
+ if (tmp) {
+ tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ elem.attrs(tmp);
+ // XEP-0176
+ if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
+ var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
+ lines.forEach(function (line) {
+ elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
+ });
+ }
+ }
+ elem.up(); // end of transport
+}
+
+SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
+ var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
+ lines.forEach(function (line) {
+ var tmp = SDPUtil.parse_rtcpfb(line);
+ if (tmp.type == 'trr-int') {
+ elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
+ elem.up();
+ } else {
+ elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
+ if (tmp.params.length > 0) {
+ elem.attrs({'subtype': tmp.params[0]});
+ }
+ elem.up();
+ }
+ });
+};
+
+SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
+ var media = '';
+ var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ if (tmp.length) {
+ media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
+ if (tmp.attr('value')) {
+ media += tmp.attr('value');
+ } else {
+ media += '0';
+ }
+ media += '\r\n';
+ }
+ tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ tmp.each(function () {
+ media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
+ if ($(this).attr('subtype')) {
+ media += ' ' + $(this).attr('subtype');
+ }
+ media += '\r\n';
+ });
+ return media;
+};
+
+// construct an SDP from a jingle stanza
+SDP.prototype.fromJingle = function (jingle) {
+ var self = this;
+ this.raw = 'v=0\r\n' +
+ 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
+ 's=-\r\n' +
+ 't=0 0\r\n';
+ // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
+ if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
+ $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
+ var contents = $(group).find('>content').map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (contents.length > 0) {
+ self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ } else if ($(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').length) {
+ // temporary namespace, not to be used. to be removed soon.
+ $(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').each(function (idx, group) {
+ var contents = $(group).find('>content').map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (group.getAttribute('type') !== null && contents.length > 0) {
+ self.raw += 'a=group:' + group.getAttribute('type') + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ } else {
+ // for backward compability, to be removed soon
+ // assume all contents are in the same bundle group, can be improved upon later
+ var bundle = $(jingle).find('>content').filter(function (idx, content) {
+ //elem.c('bundle', {xmlns:'http://estos.de/ns/bundle'});
+ return $(content).find('>bundle').length > 0;
+ }).map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (bundle.length) {
+ this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
+ }
+ }
+
+ this.session = this.raw;
+ jingle.find('>content').each(function () {
+ var m = self.jingle2media($(this));
+ self.media.push(m);
+ });
+
+ // reconstruct msid-semantic -- apparently not necessary
+ /*
+ var msid = SDPUtil.parse_ssrc(this.raw);
+ if (msid.hasOwnProperty('mslabel')) {
+ this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
+ }
+ */
+
+ this.raw = this.session + this.media.join('');
+};
+
+// translate a jingle content element into an an SDP media part
+SDP.prototype.jingle2media = function (content) {
+ var media = '',
+ desc = content.find('description'),
+ ssrc = desc.attr('ssrc'),
+ self = this,
+ tmp;
+ var sctp = content.find(
+ '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
+
+ tmp = { media: desc.attr('media') };
+ tmp.port = '1';
+ if (content.attr('senders') == 'rejected') {
+ // estos hack to reject an m-line.
+ tmp.port = '0';
+ }
+ if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
+ if (sctp.length)
+ tmp.proto = 'DTLS/SCTP';
+ else
+ tmp.proto = 'RTP/SAVPF';
+ } else {
+ tmp.proto = 'RTP/AVPF';
+ }
+ if (!sctp.length)
+ {
+ tmp.fmt = desc.find('payload-type').map(
+ function () { return this.getAttribute('id'); }).get();
+ media += SDPUtil.build_mline(tmp) + '\r\n';
+ }
+ else
+ {
+ media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
+ media += 'a=sctpmap:' + sctp.attr('number') +
+ ' ' + sctp.attr('protocol');
+
+ var streamCount = sctp.attr('streams');
+ if (streamCount)
+ media += ' ' + streamCount + '\r\n';
+ else
+ media += '\r\n';
+ }
+
+ media += 'c=IN IP4 0.0.0.0\r\n';
+ if (!sctp.length)
+ media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
+ //tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
+ tmp = content.find('>bundle>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
+ //console.log('transports: '+content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]').length);
+ //console.log('bundle.transports: '+content.find('>bundle>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]').length);
+ //console.log("tmp fingerprint: "+tmp.find('>fingerprint').innerHTML);
+ if (tmp.length) {
+ if (tmp.attr('ufrag')) {
+ media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
+ }
+ if (tmp.attr('pwd')) {
+ media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
+ }
+ tmp.find('>fingerprint').each(function () {
+ // FIXME: check namespace at some point
+ media += 'a=fingerprint:' + this.getAttribute('hash');
+ media += ' ' + $(this).text();
+ media += '\r\n';
+ //console.log("mline "+media);
+ if (this.getAttribute('setup')) {
+ media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
+ }
+ });
+ }
+ switch (content.attr('senders')) {
+ case 'initiator':
+ media += 'a=sendonly\r\n';
+ break;
+ case 'responder':
+ media += 'a=recvonly\r\n';
+ break;
+ case 'none':
+ media += 'a=inactive\r\n';
+ break;
+ case 'both':
+ media += 'a=sendrecv\r\n';
+ break;
+ }
+ media += 'a=mid:' + content.attr('name') + '\r\n';
+ /*if (content.attr('name') == 'video') {
+ media += 'a=x-google-flag:conference' + '\r\n';
+ }*/
+
+ //
+ // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
+ // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
+ if (desc.find('rtcp-mux').length) {
+ media += 'a=rtcp-mux\r\n';
+ }
+
+ if (desc.find('encryption').length) {
+ desc.find('encryption>crypto').each(function () {
+ media += 'a=crypto:' + this.getAttribute('tag');
+ media += ' ' + this.getAttribute('crypto-suite');
+ media += ' ' + this.getAttribute('key-params');
+ if (this.getAttribute('session-params')) {
+ media += ' ' + this.getAttribute('session-params');
+ }
+ media += '\r\n';
+ });
+ }
+ desc.find('payload-type').each(function () {
+ media += SDPUtil.build_rtpmap(this) + '\r\n';
+ if ($(this).find('>parameter').length) {
+ media += 'a=fmtp:' + this.getAttribute('id') + ' ';
+ media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join('; ');
+ media += '\r\n';
+ }
+ // xep-0293
+ media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
+ });
+
+ // xep-0293
+ media += self.RtcpFbFromJingle(desc, '*');
+
+ // xep-0294
+ tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
+ tmp.each(function () {
+ media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
+ });
+
+ content.find('>bundle>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
+ media += SDPUtil.candidateFromJingle(this);
+ });
+
+ // XEP-0339 handle ssrc-group attributes
+ tmp = content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
+ var semantics = this.getAttribute('semantics');
+ var ssrcs = $(this).find('>source').map(function() {
+ return this.getAttribute('ssrc');
+ }).get();
+
+ if (ssrcs.length != 0) {
+ media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
+ }
+ });
+
+ tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
+ tmp.each(function () {
+ var ssrc = this.getAttribute('ssrc');
+ $(this).find('>parameter').each(function () {
+ media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
+ if (this.getAttribute('value') && this.getAttribute('value').length)
+ media += ':' + this.getAttribute('value');
+ media += '\r\n';
+ });
+ });
+
+ if (tmp.length === 0) {
+ // fallback to proprietary mapping of a=ssrc lines
+ tmp = content.find('description>ssrc[xmlns="http://estos.de/ns/ssrc"]');
+ if (tmp.length) {
+ media += 'a=ssrc:' + ssrc + ' cname:' + tmp.attr('cname') + '\r\n';
+ media += 'a=ssrc:' + ssrc + ' msid:' + tmp.attr('msid') + '\r\n';
+ media += 'a=ssrc:' + ssrc + ' mslabel:' + tmp.attr('mslabel') + '\r\n';
+ media += 'a=ssrc:' + ssrc + ' label:' + tmp.attr('label') + '\r\n';
+ }
+ }
+ return media;
+};
+
diff --git a/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js
new file mode 100644
index 0000000000..042a123c32
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js
@@ -0,0 +1,408 @@
+/**
+ * Contains utility classes used in SDP class.
+ *
+ */
+
+/**
+ * Class holds a=ssrc lines and media type a=mid
+ * @param ssrc synchronization source identifier number(a=ssrc lines from SDP)
+ * @param type media type eg. "audio" or "video"(a=mid frm SDP)
+ * @constructor
+ */
+function ChannelSsrc(ssrc, type) {
+ this.ssrc = ssrc;
+ this.type = type;
+ this.lines = [];
+}
+
+/**
+ * Class holds a=ssrc-group: lines
+ * @param semantics
+ * @param ssrcs
+ * @constructor
+ */
+function ChannelSsrcGroup(semantics, ssrcs, line) {
+ this.semantics = semantics;
+ this.ssrcs = ssrcs;
+}
+
+/**
+ * Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type.
+ * @param channelNumber channel idx in SDP media array.
+ * @param mediaType media type(a=mid)
+ * @constructor
+ */
+function MediaChannel(channelNumber, mediaType) {
+ /**
+ * SDP channel number
+ * @type {*}
+ */
+ this.chNumber = channelNumber;
+ /**
+ * Channel media type(a=mid)
+ * @type {*}
+ */
+ this.mediaType = mediaType;
+ /**
+ * The maps of ssrc numbers to ChannelSsrc objects.
+ */
+ this.ssrcs = {};
+
+ /**
+ * The array of ChannelSsrcGroup objects.
+ * @type {Array}
+ */
+ this.ssrcGroups = [];
+}
+
+SDPUtil = {
+ iceparams: function (mediadesc, sessiondesc) {
+ var data = null;
+ if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
+ SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
+ data = {
+ ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
+ pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
+ };
+ }
+ return data;
+ },
+ parse_iceufrag: function (line) {
+ return line.substring(12);
+ },
+ build_iceufrag: function (frag) {
+ return 'a=ice-ufrag:' + frag;
+ },
+ parse_icepwd: function (line) {
+ return line.substring(10);
+ },
+ build_icepwd: function (pwd) {
+ return 'a=ice-pwd:' + pwd;
+ },
+ parse_mid: function (line) {
+ return line.substring(6);
+ },
+ parse_mline: function (line) {
+ var parts = line.substring(2).split(' '),
+ data = {};
+ data.media = parts.shift();
+ data.port = parts.shift();
+ data.proto = parts.shift();
+ if (parts[parts.length - 1] === '') { // trailing whitespace
+ parts.pop();
+ }
+ data.fmt = parts;
+ return data;
+ },
+ build_mline: function (mline) {
+ return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
+ },
+ parse_rtpmap: function (line) {
+ var parts = line.substring(9).split(' '),
+ data = {};
+ data.id = parts.shift();
+ parts = parts[0].split('/');
+ data.name = parts.shift();
+ data.clockrate = parts.shift();
+ data.channels = parts.length ? parts.shift() : '1';
+ return data;
+ },
+ /**
+ * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
+ * @param line eg. "a=sctpmap:5000 webrtc-datachannel"
+ * @returns [SCTP port number, protocol, streams]
+ */
+ parse_sctpmap: function (line)
+ {
+ var parts = line.substring(10).split(' ');
+ var sctpPort = parts[0];
+ var protocol = parts[1];
+ // Stream count is optional
+ var streamCount = parts.length > 2 ? parts[2] : null;
+ return [sctpPort, protocol, streamCount];// SCTP port
+ },
+ build_rtpmap: function (el) {
+ var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
+ if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
+ line += '/' + el.getAttribute('channels');
+ }
+ return line;
+ },
+ parse_crypto: function (line) {
+ var parts = line.substring(9).split(' '),
+ data = {};
+ data.tag = parts.shift();
+ data['crypto-suite'] = parts.shift();
+ data['key-params'] = parts.shift();
+ if (parts.length) {
+ data['session-params'] = parts.join(' ');
+ }
+ return data;
+ },
+ parse_fingerprint: function (line) { // RFC 4572
+ var parts = line.substring(14).split(' '),
+ data = {};
+ data.hash = parts.shift();
+ data.fingerprint = parts.shift();
+ // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
+ return data;
+ },
+ parse_fmtp: function (line) {
+ var parts = line.split(' '),
+ i, key, value,
+ data = [];
+ parts.shift();
+ parts = parts.join(' ').split(';');
+ for (i = 0; i < parts.length; i++) {
+ key = parts[i].split('=')[0];
+ while (key.length && key[0] == ' ') {
+ key = key.substring(1);
+ }
+ value = parts[i].split('=')[1];
+ if (key && value) {
+ data.push({name: key, value: value});
+ } else if (key) {
+ // rfc 4733 (DTMF) style stuff
+ data.push({name: '', value: key});
+ }
+ }
+ return data;
+ },
+ parse_icecandidate: function (line) {
+ var candidate = {},
+ elems = line.split(' ');
+ candidate.foundation = elems[0].substring(12);
+ candidate.component = elems[1];
+ candidate.protocol = elems[2].toLowerCase();
+ candidate.priority = elems[3];
+ candidate.ip = elems[4];
+ candidate.port = elems[5];
+ // elems[6] => "typ"
+ candidate.type = elems[7];
+ candidate.generation = 0; // default value, may be overwritten below
+ for (var i = 8; i < elems.length; i += 2) {
+ switch (elems[i]) {
+ case 'raddr':
+ candidate['rel-addr'] = elems[i + 1];
+ break;
+ case 'rport':
+ candidate['rel-port'] = elems[i + 1];
+ break;
+ case 'generation':
+ candidate.generation = elems[i + 1];
+ break;
+ case 'tcptype':
+ candidate.tcptype = elems[i + 1];
+ break;
+ default: // TODO
+ console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
+ }
+ }
+ candidate.network = '1';
+ candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
+ return candidate;
+ },
+ build_icecandidate: function (cand) {
+ var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
+ line += ' ';
+ switch (cand.type) {
+ case 'srflx':
+ case 'prflx':
+ case 'relay':
+ if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
+ line += 'raddr';
+ line += ' ';
+ line += cand['rel-addr'];
+ line += ' ';
+ line += 'rport';
+ line += ' ';
+ line += cand['rel-port'];
+ line += ' ';
+ }
+ break;
+ }
+ if (cand.hasOwnAttribute('tcptype')) {
+ line += 'tcptype';
+ line += ' ';
+ line += cand.tcptype;
+ line += ' ';
+ }
+ line += 'generation';
+ line += ' ';
+ line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
+ return line;
+ },
+ parse_ssrc: function (desc) {
+ // proprietary mapping of a=ssrc lines
+ // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
+ // and parse according to that
+ var lines = desc.split('\r\n'),
+ data = {};
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, 7) == 'a=ssrc:') {
+ var idx = lines[i].indexOf(' ');
+ data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
+ }
+ }
+ return data;
+ },
+ parse_rtcpfb: function (line) {
+ var parts = line.substr(10).split(' ');
+ var data = {};
+ data.pt = parts.shift();
+ data.type = parts.shift();
+ data.params = parts;
+ return data;
+ },
+ parse_extmap: function (line) {
+ var parts = line.substr(9).split(' ');
+ var data = {};
+ data.value = parts.shift();
+ if (data.value.indexOf('/') != -1) {
+ data.direction = data.value.substr(data.value.indexOf('/') + 1);
+ data.value = data.value.substr(0, data.value.indexOf('/'));
+ } else {
+ data.direction = 'both';
+ }
+ data.uri = parts.shift();
+ data.params = parts;
+ return data;
+ },
+ find_line: function (haystack, needle, sessionpart) {
+ var lines = haystack.split('\r\n');
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, needle.length) == needle) {
+ return lines[i];
+ }
+ }
+ if (!sessionpart) {
+ return false;
+ }
+ // search session part
+ lines = sessionpart.split('\r\n');
+ for (var j = 0; j < lines.length; j++) {
+ if (lines[j].substring(0, needle.length) == needle) {
+ return lines[j];
+ }
+ }
+ return false;
+ },
+ find_lines: function (haystack, needle, sessionpart) {
+ var lines = haystack.split('\r\n'),
+ needles = [];
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, needle.length) == needle)
+ needles.push(lines[i]);
+ }
+ if (needles.length || !sessionpart) {
+ return needles;
+ }
+ // search session part
+ lines = sessionpart.split('\r\n');
+ for (var j = 0; j < lines.length; j++) {
+ if (lines[j].substring(0, needle.length) == needle) {
+ needles.push(lines[j]);
+ }
+ }
+ return needles;
+ },
+ candidateToJingle: function (line) {
+ // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
+ //
+ if (line.indexOf('candidate:') === 0) {
+ line = 'a=' + line;
+ } else if (line.substring(0, 12) != 'a=candidate:') {
+ console.log('parseCandidate called with a line that is not a candidate line');
+ console.log(line);
+ return null;
+ }
+ if (line.substring(line.length - 2) == '\r\n') // chomp it
+ line = line.substring(0, line.length - 2);
+ var candidate = {},
+ elems = line.split(' '),
+ i;
+ if (elems[6] != 'typ') {
+ console.log('did not find typ in the right place');
+ console.log(line);
+ return null;
+ }
+ candidate.foundation = elems[0].substring(12);
+ candidate.component = elems[1];
+ candidate.protocol = elems[2].toLowerCase();
+ candidate.priority = elems[3];
+ candidate.ip = elems[4];
+ candidate.port = elems[5];
+ // elems[6] => "typ"
+ candidate.type = elems[7];
+
+ candidate.generation = '0'; // default, may be overwritten below
+ for (i = 8; i < elems.length; i += 2) {
+ switch (elems[i]) {
+ case 'raddr':
+ candidate['rel-addr'] = elems[i + 1];
+ break;
+ case 'rport':
+ candidate['rel-port'] = elems[i + 1];
+ break;
+ case 'generation':
+ candidate.generation = elems[i + 1];
+ break;
+ case 'tcptype':
+ candidate.tcptype = elems[i + 1];
+ break;
+ default: // TODO
+ console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
+ }
+ }
+ candidate.network = '1';
+ candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
+ return candidate;
+ },
+ candidateFromJingle: function (cand) {
+ var line = 'a=candidate:';
+ line += cand.getAttribute('foundation');
+ line += ' ';
+ line += cand.getAttribute('component');
+ line += ' ';
+ line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
+ line += ' ';
+ line += cand.getAttribute('priority');
+ line += ' ';
+ line += cand.getAttribute('ip');
+ line += ' ';
+ line += cand.getAttribute('port');
+ line += ' ';
+ line += 'typ';
+ line += ' ' + cand.getAttribute('type');
+ line += ' ';
+ switch (cand.getAttribute('type')) {
+ case 'srflx':
+ case 'prflx':
+ case 'relay':
+ if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
+ line += 'raddr';
+ line += ' ';
+ line += cand.getAttribute('rel-addr');
+ line += ' ';
+ line += 'rport';
+ line += ' ';
+ line += cand.getAttribute('rel-port');
+ line += ' ';
+ }
+ break;
+ }
+ if (cand.getAttribute('protocol').toLowerCase() == 'tcp') {
+ line += 'tcptype';
+ line += ' ';
+ line += cand.getAttribute('tcptype');
+ line += ' ';
+ }
+ line += 'generation';
+ line += ' ';
+ line += cand.getAttribute('generation') || '0';
+ return line + '\r\n';
+ }
+};
+
+exports.SDPUtil = SDPUtil;
+
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js b/contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js
new file mode 100644
index 0000000000..9c45c2df18
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js
@@ -0,0 +1,254 @@
+/**
+ * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
+ *
+ * This can be used with JS designed for browsers to improve reuse of code and
+ * allow the use of existing libraries.
+ *
+ * Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.
+ *
+ * @todo SSL Support
+ * @author Dan DeFelippi
+ * @license MIT
+ */
+
+var Url = require("url")
+ ,sys = require("util");
+
+exports.XMLHttpRequest = function() {
+ /**
+ * Private variables
+ */
+ var self = this;
+ var http = require('http');
+ var https = require('https');
+
+ // Holds http.js objects
+ var client;
+ var request;
+ var response;
+
+ // Request settings
+ var settings = {};
+
+ // Set some default headers
+ var defaultHeaders = {
+ "User-Agent": "node.js",
+ "Accept": "*/*",
+ };
+
+ var headers = defaultHeaders;
+
+ /**
+ * Constants
+ */
+ this.UNSENT = 0;
+ this.OPENED = 1;
+ this.HEADERS_RECEIVED = 2;
+ this.LOADING = 3;
+ this.DONE = 4;
+
+ /**
+ * Public vars
+ */
+ // Current state
+ this.readyState = this.UNSENT;
+
+ // default ready state change handler in case one is not set or is set late
+ this.onreadystatechange = function() {};
+
+ // Result & response
+ this.responseText = "";
+ this.responseXML = "";
+ this.status = null;
+ this.statusText = null;
+
+ /**
+ * Open the connection. Currently supports local server requests.
+ *
+ * @param string method Connection method (eg GET, POST)
+ * @param string url URL for the connection.
+ * @param boolean async Asynchronous connection. Default is true.
+ * @param string user Username for basic authentication (optional)
+ * @param string password Password for basic authentication (optional)
+ */
+ this.open = function(method, url, async, user, password) {
+ settings = {
+ "method": method,
+ "url": url,
+ "async": async || null,
+ "user": user || null,
+ "password": password || null
+ };
+
+ this.abort();
+
+ setState(this.OPENED);
+ };
+
+ /**
+ * Sets a header for the request.
+ *
+ * @param string header Header name
+ * @param string value Header value
+ */
+ this.setRequestHeader = function(header, value) {
+ headers[header] = value;
+ };
+
+ /**
+ * Gets a header from the server response.
+ *
+ * @param string header Name of header to get.
+ * @return string Text of the header or null if it doesn't exist.
+ */
+ this.getResponseHeader = function(header) {
+ if (this.readyState > this.OPENED && response.headers[header]) {
+ return header + ": " + response.headers[header];
+ }
+
+ return null;
+ };
+
+ /**
+ * Gets all the response headers.
+ *
+ * @return string
+ */
+ this.getAllResponseHeaders = function() {
+ if (this.readyState < this.HEADERS_RECEIVED) {
+ throw "INVALID_STATE_ERR: Headers have not been received.";
+ }
+ var result = "";
+
+ for (var i in response.headers) {
+ result += i + ": " + response.headers[i] + "\r\n";
+ }
+ return result.substr(0, result.length - 2);
+ };
+
+ /**
+ * Sends the request to the server.
+ *
+ * @param string data Optional data to send as request body.
+ */
+ this.send = function(data) {
+ if (this.readyState != this.OPENED) {
+ throw "INVALID_STATE_ERR: connection must be opened before send() is called";
+ }
+
+ var ssl = false;
+ var url = Url.parse(settings.url);
+
+ // Determine the server
+ switch (url.protocol) {
+ case 'https:':
+ ssl = true;
+ // SSL & non-SSL both need host, no break here.
+ case 'http:':
+ var host = url.hostname;
+ break;
+
+ case undefined:
+ case '':
+ var host = "localhost";
+ break;
+
+ default:
+ throw "Protocol not supported.";
+ }
+
+ // Default to port 80. If accessing localhost on another port be sure
+ // to use http://localhost:port/path
+ var port = url.port || (ssl ? 443 : 80);
+ // Add query string if one is used
+ var uri = url.pathname + (url.search ? url.search : '');
+
+ // Set the Host header or the server may reject the request
+ this.setRequestHeader("Host", host);
+
+ // Set content length header
+ if (settings.method == "GET" || settings.method == "HEAD") {
+ data = null;
+ } else if (data) {
+ this.setRequestHeader("Content-Length", Buffer.byteLength(data));
+
+ if (!headers["Content-Type"]) {
+ this.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
+ }
+ }
+
+ // Use the proper protocol
+ var doRequest = ssl ? https.request : http.request;
+
+ var options = {
+ host: host,
+ port: port,
+ path: uri,
+ method: settings.method,
+ headers: headers,
+ agent: false
+ };
+
+ var req = doRequest(options, function(res) {
+ response = res;
+ response.setEncoding("utf8");
+
+ setState(self.HEADERS_RECEIVED);
+ self.status = response.statusCode;
+
+ response.on('data', function(chunk) {
+ // Make sure there's some data
+ if (chunk) {
+ self.responseText += chunk;
+ }
+ setState(self.LOADING);
+ });
+
+ response.on('end', function() {
+ setState(self.DONE);
+ });
+
+ response.on('error', function() {
+ self.handleError(error);
+ });
+ }).on('error', function(error) {
+ self.handleError(error);
+ });
+
+ req.setHeader("Connection", "Close");
+
+ // Node 0.4 and later won't accept empty data. Make sure it's needed.
+ if (data) {
+ req.write(data);
+ }
+
+ req.end();
+ };
+
+ this.handleError = function(error) {
+ this.status = 503;
+ this.statusText = error;
+ this.responseText = error.stack;
+ setState(this.DONE);
+ };
+
+ /**
+ * Aborts a request.
+ */
+ this.abort = function() {
+ headers = defaultHeaders;
+ this.readyState = this.UNSENT;
+ this.responseText = "";
+ this.responseXML = "";
+ };
+
+ /**
+ * Changes readyState and calls onreadystatechange.
+ *
+ * @param int state New state
+ */
+ var setState = function(state) {
+ self.readyState = state;
+ self.onreadystatechange();
+ }
+};
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/base64.js b/contrib/jitsimeetbridge/unjingle/strophe/base64.js
new file mode 100644
index 0000000000..418caac050
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/base64.js
@@ -0,0 +1,83 @@
+// This code was written by Tyler Akins and has been placed in the
+// public domain. It would be nice if you left this header intact.
+// Base64 code from Tyler Akins -- http://rumkin.com
+
+var Base64 = (function () {
+ var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+ var obj = {
+ /**
+ * Encodes a string in base64
+ * @param {String} input The string to encode in base64.
+ */
+ encode: function (input) {
+ var output = "";
+ var chr1, chr2, chr3;
+ var enc1, enc2, enc3, enc4;
+ var i = 0;
+
+ do {
+ chr1 = input.charCodeAt(i++);
+ chr2 = input.charCodeAt(i++);
+ chr3 = input.charCodeAt(i++);
+
+ enc1 = chr1 >> 2;
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+ enc4 = chr3 & 63;
+
+ if (isNaN(chr2)) {
+ enc3 = enc4 = 64;
+ } else if (isNaN(chr3)) {
+ enc4 = 64;
+ }
+
+ output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
+ keyStr.charAt(enc3) + keyStr.charAt(enc4);
+ } while (i < input.length);
+
+ return output;
+ },
+
+ /**
+ * Decodes a base64 string.
+ * @param {String} input The string to decode.
+ */
+ decode: function (input) {
+ var output = "";
+ var chr1, chr2, chr3;
+ var enc1, enc2, enc3, enc4;
+ var i = 0;
+
+ // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
+
+ do {
+ enc1 = keyStr.indexOf(input.charAt(i++));
+ enc2 = keyStr.indexOf(input.charAt(i++));
+ enc3 = keyStr.indexOf(input.charAt(i++));
+ enc4 = keyStr.indexOf(input.charAt(i++));
+
+ chr1 = (enc1 << 2) | (enc2 >> 4);
+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+ chr3 = ((enc3 & 3) << 6) | enc4;
+
+ output = output + String.fromCharCode(chr1);
+
+ if (enc3 != 64) {
+ output = output + String.fromCharCode(chr2);
+ }
+ if (enc4 != 64) {
+ output = output + String.fromCharCode(chr3);
+ }
+ } while (i < input.length);
+
+ return output;
+ }
+ };
+
+ return obj;
+})();
+
+// Nodify
+exports.Base64 = Base64;
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/md5.js b/contrib/jitsimeetbridge/unjingle/strophe/md5.js
new file mode 100644
index 0000000000..5334325e2f
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/md5.js
@@ -0,0 +1,279 @@
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+var MD5 = (function () {
+ /*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+ var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+ var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
+ var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
+
+ /*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+ var safe_add = function (x, y) {
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+ };
+
+ /*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+ var bit_rol = function (num, cnt) {
+ return (num << cnt) | (num >>> (32 - cnt));
+ };
+
+ /*
+ * Convert a string to an array of little-endian words
+ * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
+ */
+ var str2binl = function (str) {
+ var bin = [];
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < str.length * chrsz; i += chrsz)
+ {
+ bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
+ }
+ return bin;
+ };
+
+ /*
+ * Convert an array of little-endian words to a string
+ */
+ var binl2str = function (bin) {
+ var str = "";
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < bin.length * 32; i += chrsz)
+ {
+ str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
+ }
+ return str;
+ };
+
+ /*
+ * Convert an array of little-endian words to a hex string.
+ */
+ var binl2hex = function (binarray) {
+ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var str = "";
+ for(var i = 0; i < binarray.length * 4; i++)
+ {
+ str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
+ hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
+ }
+ return str;
+ };
+
+ /*
+ * Convert an array of little-endian words to a base-64 string
+ */
+ var binl2b64 = function (binarray) {
+ var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var str = "";
+ var triplet, j;
+ for(var i = 0; i < binarray.length * 4; i += 3)
+ {
+ triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) |
+ (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
+ ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
+ for(j = 0; j < 4; j++)
+ {
+ if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
+ else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
+ }
+ }
+ return str;
+ };
+
+ /*
+ * These functions implement the four basic operations the algorithm uses.
+ */
+ var md5_cmn = function (q, a, b, x, s, t) {
+ return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b);
+ };
+
+ var md5_ff = function (a, b, c, d, x, s, t) {
+ return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+ };
+
+ var md5_gg = function (a, b, c, d, x, s, t) {
+ return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+ };
+
+ var md5_hh = function (a, b, c, d, x, s, t) {
+ return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+ };
+
+ var md5_ii = function (a, b, c, d, x, s, t) {
+ return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+ };
+
+ /*
+ * Calculate the MD5 of an array of little-endian words, and a bit length
+ */
+ var core_md5 = function (x, len) {
+ /* append padding */
+ x[len >> 5] |= 0x80 << ((len) % 32);
+ x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+
+ var olda, oldb, oldc, oldd;
+ for (var i = 0; i < x.length; i += 16)
+ {
+ olda = a;
+ oldb = b;
+ oldc = c;
+ oldd = d;
+
+ a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+ d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
+ c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
+ b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+ a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+ d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
+ c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+ b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
+ a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
+ d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+ c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
+ b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
+ a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
+ d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
+ c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
+ b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
+
+ a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+ d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+ c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
+ b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
+ a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+ d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
+ c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
+ b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
+ a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
+ d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
+ c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
+ b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
+ a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
+ d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+ c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
+ b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+ a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
+ d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+ c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
+ b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
+ a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+ d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
+ c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
+ b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
+ a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
+ d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
+ c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
+ b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
+ a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+ d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
+ c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
+ b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+ a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+ d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
+ c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
+ b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
+ a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
+ d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+ c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
+ b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+ a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
+ d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
+ c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+ b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
+ a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+ d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
+ c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
+ b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ }
+ return [a, b, c, d];
+ };
+
+
+ /*
+ * Calculate the HMAC-MD5, of a key and some data
+ */
+ var core_hmac_md5 = function (key, data) {
+ var bkey = str2binl(key);
+ if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }
+
+ var ipad = new Array(16), opad = new Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5C5C5C5C;
+ }
+
+ var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
+ return core_md5(opad.concat(hash), 512 + 128);
+ };
+
+ var obj = {
+ /*
+ * These are the functions you'll usually want to call.
+ * They take string arguments and return either hex or base-64 encoded
+ * strings.
+ */
+ hexdigest: function (s) {
+ return binl2hex(core_md5(str2binl(s), s.length * chrsz));
+ },
+
+ b64digest: function (s) {
+ return binl2b64(core_md5(str2binl(s), s.length * chrsz));
+ },
+
+ hash: function (s) {
+ return binl2str(core_md5(str2binl(s), s.length * chrsz));
+ },
+
+ hmac_hexdigest: function (key, data) {
+ return binl2hex(core_hmac_md5(key, data));
+ },
+
+ hmac_b64digest: function (key, data) {
+ return binl2b64(core_hmac_md5(key, data));
+ },
+
+ hmac_hash: function (key, data) {
+ return binl2str(core_hmac_md5(key, data));
+ },
+
+ /*
+ * Perform a simple self-test to see if the VM is working
+ */
+ test: function () {
+ return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72";
+ }
+ };
+
+ return obj;
+})();
+
+// Nodify
+exports.MD5 = MD5;
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/strophe.js b/contrib/jitsimeetbridge/unjingle/strophe/strophe.js
new file mode 100644
index 0000000000..06d426cdec
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/strophe.js
@@ -0,0 +1,3256 @@
+/*
+ This program is distributed under the terms of the MIT license.
+ Please see the LICENSE file for details.
+
+ Copyright 2006-2008, OGG, LLC
+*/
+
+/* jslint configuration: */
+/*global document, window, setTimeout, clearTimeout, console,
+ XMLHttpRequest, ActiveXObject,
+ Base64, MD5,
+ Strophe, $build, $msg, $iq, $pres */
+
+/** File: strophe.js
+ * A JavaScript library for XMPP BOSH.
+ *
+ * This is the JavaScript version of the Strophe library. Since JavaScript
+ * has no facilities for persistent TCP connections, this library uses
+ * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate
+ * a persistent, stateful, two-way connection to an XMPP server. More
+ * information on BOSH can be found in XEP 124.
+ */
+
+/** PrivateFunction: Function.prototype.bind
+ * Bind a function to an instance.
+ *
+ * This Function object extension method creates a bound method similar
+ * to those in Python. This means that the 'this' object will point
+ * to the instance you want. See
+ * MDC's bind() documentation and
+ * Bound Functions and Function Imports in JavaScript
+ * for a complete explanation.
+ *
+ * This extension already exists in some browsers (namely, Firefox 3), but
+ * we provide it to support those that don't.
+ *
+ * Parameters:
+ * (Object) obj - The object that will become 'this' in the bound function.
+ * (Object) argN - An option argument that will be prepended to the
+ * arguments given for the function call
+ *
+ * Returns:
+ * The bound function.
+ */
+
+/* Make it work on node.js: Nodify
+ *
+ * Steps:
+ * 1. Create the global objects: window, document, Base64, MD5 and XMLHttpRequest
+ * 2. Use the node-XMLHttpRequest module.
+ * 3. Use jsdom for the document object - since it supports DOM functions.
+ * 4. Replace all calls to childNodes with _childNodes (since the former doesn't
+ * seem to work on jsdom).
+ * 5. While getting the response from XMLHttpRequest, manually convert the text
+ * data to XML.
+ * 6. All calls to nodeName should replaced by nodeName.toLowerCase() since jsdom
+ * seems to always convert node names to upper case.
+ *
+ */
+var XMLHttpRequest = require('./XMLHttpRequest.js').XMLHttpRequest;
+var Base64 = require('./base64.js').Base64;
+var MD5 = require('./md5.js').MD5;
+var jsdom = require("jsdom").jsdom;
+
+document = jsdom(""),
+
+window = {
+ XMLHttpRequest: XMLHttpRequest,
+ Base64: Base64,
+ MD5: MD5
+};
+
+exports.Strophe = window;
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function (obj /*, arg1, arg2, ... */)
+ {
+ var func = this;
+ var _slice = Array.prototype.slice;
+ var _concat = Array.prototype.concat;
+ var _args = _slice.call(arguments, 1);
+
+ return function () {
+ return func.apply(obj ? obj : this,
+ _concat.call(_args,
+ _slice.call(arguments, 0)));
+ };
+ };
+}
+
+/** PrivateFunction: Array.prototype.indexOf
+ * Return the index of an object in an array.
+ *
+ * This function is not supplied by some JavaScript implementations, so
+ * we provide it if it is missing. This code is from:
+ * http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
+ *
+ * Parameters:
+ * (Object) elt - The object to look for.
+ * (Integer) from - The index from which to start looking. (optional).
+ *
+ * Returns:
+ * The index of elt in the array or -1 if not found.
+ */
+if (!Array.prototype.indexOf)
+{
+ Array.prototype.indexOf = function(elt /*, from*/)
+ {
+ var len = this.length;
+
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+ if (from < 0) {
+ from += len;
+ }
+
+ for (; from < len; from++) {
+ if (from in this && this[from] === elt) {
+ return from;
+ }
+ }
+
+ return -1;
+ };
+}
+
+/* All of the Strophe globals are defined in this special function below so
+ * that references to the globals become closures. This will ensure that
+ * on page reload, these references will still be available to callbacks
+ * that are still executing.
+ */
+
+(function (callback) {
+var Strophe;
+
+/** Function: $build
+ * Create a Strophe.Builder.
+ * This is an alias for 'new Strophe.Builder(name, attrs)'.
+ *
+ * Parameters:
+ * (String) name - The root element name.
+ * (Object) attrs - The attributes for the root element in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $build(name, attrs) { return new Strophe.Builder(name, attrs); }
+/** Function: $msg
+ * Create a Strophe.Builder with a element as the root.
+ *
+ * Parmaeters:
+ * (Object) attrs - The element attributes in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $msg(attrs) { return new Strophe.Builder("message", attrs); }
+/** Function: $iq
+ * Create a Strophe.Builder with an element as the root.
+ *
+ * Parameters:
+ * (Object) attrs - The element attributes in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $iq(attrs) { return new Strophe.Builder("iq", attrs); }
+/** Function: $pres
+ * Create a Strophe.Builder with a element as the root.
+ *
+ * Parameters:
+ * (Object) attrs - The element attributes in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $pres(attrs) { return new Strophe.Builder("presence", attrs); }
+
+/** Class: Strophe
+ * An object container for all Strophe library functions.
+ *
+ * This class is just a container for all the objects and constants
+ * used in the library. It is not meant to be instantiated, but to
+ * provide a namespace for library objects, constants, and functions.
+ */
+Strophe = {
+ /** Constant: VERSION
+ * The version of the Strophe library. Unreleased builds will have
+ * a version of head-HASH where HASH is a partial revision.
+ */
+ VERSION: "@VERSION@",
+
+ /** Constants: XMPP Namespace Constants
+ * Common namespace constants from the XMPP RFCs and XEPs.
+ *
+ * NS.HTTPBIND - HTTP BIND namespace from XEP 124.
+ * NS.BOSH - BOSH namespace from XEP 206.
+ * NS.CLIENT - Main XMPP client namespace.
+ * NS.AUTH - Legacy authentication namespace.
+ * NS.ROSTER - Roster operations namespace.
+ * NS.PROFILE - Profile namespace.
+ * NS.DISCO_INFO - Service discovery info namespace from XEP 30.
+ * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30.
+ * NS.MUC - Multi-User Chat namespace from XEP 45.
+ * NS.SASL - XMPP SASL namespace from RFC 3920.
+ * NS.STREAM - XMPP Streams namespace from RFC 3920.
+ * NS.BIND - XMPP Binding namespace from RFC 3920.
+ * NS.SESSION - XMPP Session namespace from RFC 3920.
+ */
+ NS: {
+ HTTPBIND: "http://jabber.org/protocol/httpbind",
+ BOSH: "urn:xmpp:xbosh",
+ CLIENT: "jabber:client",
+ AUTH: "jabber:iq:auth",
+ ROSTER: "jabber:iq:roster",
+ PROFILE: "jabber:iq:profile",
+ DISCO_INFO: "http://jabber.org/protocol/disco#info",
+ DISCO_ITEMS: "http://jabber.org/protocol/disco#items",
+ MUC: "http://jabber.org/protocol/muc",
+ SASL: "urn:ietf:params:xml:ns:xmpp-sasl",
+ STREAM: "http://etherx.jabber.org/streams",
+ BIND: "urn:ietf:params:xml:ns:xmpp-bind",
+ SESSION: "urn:ietf:params:xml:ns:xmpp-session",
+ VERSION: "jabber:iq:version",
+ STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas"
+ },
+
+ /** Function: addNamespace
+ * This function is used to extend the current namespaces in
+ * Strophe.NS. It takes a key and a value with the key being the
+ * name of the new namespace, with its actual value.
+ * For example:
+ * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub");
+ *
+ * Parameters:
+ * (String) name - The name under which the namespace will be
+ * referenced under Strophe.NS
+ * (String) value - The actual namespace.
+ */
+ addNamespace: function (name, value)
+ {
+ Strophe.NS[name] = value;
+ },
+
+ /** Constants: Connection Status Constants
+ * Connection status constants for use by the connection handler
+ * callback.
+ *
+ * Status.ERROR - An error has occurred
+ * Status.CONNECTING - The connection is currently being made
+ * Status.CONNFAIL - The connection attempt failed
+ * Status.AUTHENTICATING - The connection is authenticating
+ * Status.AUTHFAIL - The authentication attempt failed
+ * Status.CONNECTED - The connection has succeeded
+ * Status.DISCONNECTED - The connection has been terminated
+ * Status.DISCONNECTING - The connection is currently being terminated
+ * Status.ATTACHED - The connection has been attached
+ */
+ Status: {
+ ERROR: 0,
+ CONNECTING: 1,
+ CONNFAIL: 2,
+ AUTHENTICATING: 3,
+ AUTHFAIL: 4,
+ CONNECTED: 5,
+ DISCONNECTED: 6,
+ DISCONNECTING: 7,
+ ATTACHED: 8
+ },
+
+ /** Constants: Log Level Constants
+ * Logging level indicators.
+ *
+ * LogLevel.DEBUG - Debug output
+ * LogLevel.INFO - Informational output
+ * LogLevel.WARN - Warnings
+ * LogLevel.ERROR - Errors
+ * LogLevel.FATAL - Fatal errors
+ */
+ LogLevel: {
+ DEBUG: 0,
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3,
+ FATAL: 4
+ },
+
+ /** PrivateConstants: DOM Element Type Constants
+ * DOM element types.
+ *
+ * ElementType.NORMAL - Normal element.
+ * ElementType.TEXT - Text data element.
+ */
+ ElementType: {
+ NORMAL: 1,
+ TEXT: 3
+ },
+
+ /** PrivateConstants: Timeout Values
+ * Timeout values for error states. These values are in seconds.
+ * These should not be changed unless you know exactly what you are
+ * doing.
+ *
+ * TIMEOUT - Timeout multiplier. A waiting request will be considered
+ * failed after Math.floor(TIMEOUT * wait) seconds have elapsed.
+ * This defaults to 1.1, and with default wait, 66 seconds.
+ * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where
+ * Strophe can detect early failure, it will consider the request
+ * failed if it doesn't return after
+ * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed.
+ * This defaults to 0.1, and with default wait, 6 seconds.
+ */
+ TIMEOUT: 1.1,
+ SECONDARY_TIMEOUT: 0.1,
+
+ /** Function: forEachChild
+ * Map a function over some or all child elements of a given element.
+ *
+ * This is a small convenience function for mapping a function over
+ * some or all of the children of an element. If elemName is null, all
+ * children will be passed to the function, otherwise only children
+ * whose tag names match elemName will be passed.
+ *
+ * Parameters:
+ * (XMLElement) elem - The element to operate on.
+ * (String) elemName - The child element tag name filter.
+ * (Function) func - The function to apply to each child. This
+ * function should take a single argument, a DOM element.
+ */
+ forEachChild: function (elem, elemName, func)
+ {
+ var i, childNode;
+
+ for (i = 0; i < elem._childNodes.length; i++) {
+ childNode = elem._childNodes[i];
+ if (childNode.nodeType == Strophe.ElementType.NORMAL &&
+ (!elemName || this.isTagEqual(childNode, elemName))) {
+ func(childNode);
+ }
+ }
+ },
+
+ /** Function: isTagEqual
+ * Compare an element's tag name with a string.
+ *
+ * This function is case insensitive.
+ *
+ * Parameters:
+ * (XMLElement) el - A DOM element.
+ * (String) name - The element name.
+ *
+ * Returns:
+ * true if the element's tag name matches _el_, and false
+ * otherwise.
+ */
+ isTagEqual: function (el, name)
+ {
+ return el.tagName.toLowerCase() == name.toLowerCase();
+ },
+
+ /** PrivateVariable: _xmlGenerator
+ * _Private_ variable that caches a DOM document to
+ * generate elements.
+ */
+ _xmlGenerator: null,
+
+ /** PrivateFunction: _makeGenerator
+ * _Private_ function that creates a dummy XML DOM document to serve as
+ * an element and text node generator.
+ */
+ _makeGenerator: function () {
+ var doc;
+
+ if (window.ActiveXObject) {
+ doc = this._getIEXmlDom();
+ doc.appendChild(doc.createElement('strophe'));
+ } else {
+ doc = document.implementation
+ .createDocument('jabber:client', 'strophe', null);
+ }
+
+ return doc;
+ },
+
+ /** Function: xmlGenerator
+ * Get the DOM document to generate elements.
+ *
+ * Returns:
+ * The currently used DOM document.
+ */
+ xmlGenerator: function () {
+ if (!Strophe._xmlGenerator) {
+ Strophe._xmlGenerator = Strophe._makeGenerator();
+ }
+ return Strophe._xmlGenerator;
+ },
+
+ /** PrivateFunction: _getIEXmlDom
+ * Gets IE xml doc object
+ *
+ * Returns:
+ * A Microsoft XML DOM Object
+ * See Also:
+ * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx
+ */
+ _getIEXmlDom : function() {
+ var doc = null;
+ var docStrings = [
+ "Msxml2.DOMDocument.6.0",
+ "Msxml2.DOMDocument.5.0",
+ "Msxml2.DOMDocument.4.0",
+ "MSXML2.DOMDocument.3.0",
+ "MSXML2.DOMDocument",
+ "MSXML.DOMDocument",
+ "Microsoft.XMLDOM"
+ ];
+
+ for (var d = 0; d < docStrings.length; d++) {
+ if (doc === null) {
+ try {
+ doc = new ActiveXObject(docStrings[d]);
+ } catch (e) {
+ doc = null;
+ }
+ } else {
+ break;
+ }
+ }
+
+ return doc;
+ },
+
+ /** Function: xmlElement
+ * Create an XML DOM element.
+ *
+ * This function creates an XML DOM element correctly across all
+ * implementations. Note that these are not HTML DOM elements, which
+ * aren't appropriate for XMPP stanzas.
+ *
+ * Parameters:
+ * (String) name - The name for the element.
+ * (Array|Object) attrs - An optional array or object containing
+ * key/value pairs to use as element attributes. The object should
+ * be in the format {'key': 'value'} or {key: 'value'}. The array
+ * should have the format [['key1', 'value1'], ['key2', 'value2']].
+ * (String) text - The text child data for the element.
+ *
+ * Returns:
+ * A new XML DOM element.
+ */
+ xmlElement: function (name)
+ {
+ if (!name) { return null; }
+
+ var node = Strophe.xmlGenerator().createElement(name);
+
+ // FIXME: this should throw errors if args are the wrong type or
+ // there are more than two optional args
+ var a, i, k;
+ for (a = 1; a < arguments.length; a++) {
+ if (!arguments[a]) { continue; }
+ if (typeof(arguments[a]) == "string" ||
+ typeof(arguments[a]) == "number") {
+ node.appendChild(Strophe.xmlTextNode(arguments[a]));
+ } else if (typeof(arguments[a]) == "object" &&
+ typeof(arguments[a].sort) == "function") {
+ for (i = 0; i < arguments[a].length; i++) {
+ if (typeof(arguments[a][i]) == "object" &&
+ typeof(arguments[a][i].sort) == "function") {
+ node.setAttribute(arguments[a][i][0],
+ arguments[a][i][1]);
+ }
+ }
+ } else if (typeof(arguments[a]) == "object") {
+ for (k in arguments[a]) {
+ if (arguments[a].hasOwnProperty(k)) {
+ node.setAttribute(k, arguments[a][k]);
+ }
+ }
+ }
+ }
+
+ return node;
+ },
+
+ /* Function: xmlescape
+ * Excapes invalid xml characters.
+ *
+ * Parameters:
+ * (String) text - text to escape.
+ *
+ * Returns:
+ * Escaped text.
+ */
+ xmlescape: function(text)
+ {
+ text = text.replace(/\&/g, "&");
+ text = text.replace(//g, ">");
+ return text;
+ },
+
+ /** Function: xmlTextNode
+ * Creates an XML DOM text node.
+ *
+ * Provides a cross implementation version of document.createTextNode.
+ *
+ * Parameters:
+ * (String) text - The content of the text node.
+ *
+ * Returns:
+ * A new XML DOM text node.
+ */
+ xmlTextNode: function (text)
+ {
+ //ensure text is escaped
+ text = Strophe.xmlescape(text);
+
+ return Strophe.xmlGenerator().createTextNode(text);
+ },
+
+ /** Function: getText
+ * Get the concatenation of all text children of an element.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * A String with the concatenated text of all text element children.
+ */
+ getText: function (elem)
+ {
+ if (!elem) { return null; }
+
+ var str = "";
+ if (elem._childNodes.length === 0 && elem.nodeType ==
+ Strophe.ElementType.TEXT) {
+ str += elem.nodeValue;
+ }
+
+ for (var i = 0; i < elem._childNodes.length; i++) {
+ if (elem._childNodes[i].nodeType == Strophe.ElementType.TEXT) {
+ str += elem._childNodes[i].nodeValue;
+ }
+ }
+
+ return str;
+ },
+
+ /** Function: copyElement
+ * Copy an XML DOM element.
+ *
+ * This function copies a DOM element and all its descendants and returns
+ * the new copy.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * A new, copied DOM element tree.
+ */
+ copyElement: function (elem)
+ {
+ var i, el;
+ if (elem.nodeType == Strophe.ElementType.NORMAL) {
+ el = Strophe.xmlElement(elem.tagName);
+
+ for (i = 0; i < elem.attributes.length; i++) {
+ el.setAttribute(elem.attributes[i].nodeName.toLowerCase(),
+ elem.attributes[i].value);
+ }
+
+ for (i = 0; i < elem._childNodes.length; i++) {
+ el.appendChild(Strophe.copyElement(elem._childNodes[i]));
+ }
+ } else if (elem.nodeType == Strophe.ElementType.TEXT) {
+ el = Strophe.xmlTextNode(elem.nodeValue);
+ }
+
+ return el;
+ },
+
+ /** Function: escapeNode
+ * Escape the node part (also called local part) of a JID.
+ *
+ * Parameters:
+ * (String) node - A node (or local part).
+ *
+ * Returns:
+ * An escaped node (or local part).
+ */
+ escapeNode: function (node)
+ {
+ return node.replace(/^\s+|\s+$/g, '')
+ .replace(/\\/g, "\\5c")
+ .replace(/ /g, "\\20")
+ .replace(/\"/g, "\\22")
+ .replace(/\&/g, "\\26")
+ .replace(/\'/g, "\\27")
+ .replace(/\//g, "\\2f")
+ .replace(/:/g, "\\3a")
+ .replace(//g, "\\3e")
+ .replace(/@/g, "\\40");
+ },
+
+ /** Function: unescapeNode
+ * Unescape a node part (also called local part) of a JID.
+ *
+ * Parameters:
+ * (String) node - A node (or local part).
+ *
+ * Returns:
+ * An unescaped node (or local part).
+ */
+ unescapeNode: function (node)
+ {
+ return node.replace(/\\20/g, " ")
+ .replace(/\\22/g, '"')
+ .replace(/\\26/g, "&")
+ .replace(/\\27/g, "'")
+ .replace(/\\2f/g, "/")
+ .replace(/\\3a/g, ":")
+ .replace(/\\3c/g, "<")
+ .replace(/\\3e/g, ">")
+ .replace(/\\40/g, "@")
+ .replace(/\\5c/g, "\\");
+ },
+
+ /** Function: getNodeFromJid
+ * Get the node portion of a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the node.
+ */
+ getNodeFromJid: function (jid)
+ {
+ if (jid.indexOf("@") < 0) { return null; }
+ return jid.split("@")[0];
+ },
+
+ /** Function: getDomainFromJid
+ * Get the domain portion of a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the domain.
+ */
+ getDomainFromJid: function (jid)
+ {
+ var bare = Strophe.getBareJidFromJid(jid);
+ if (bare.indexOf("@") < 0) {
+ return bare;
+ } else {
+ var parts = bare.split("@");
+ parts.splice(0, 1);
+ return parts.join('@');
+ }
+ },
+
+ /** Function: getResourceFromJid
+ * Get the resource portion of a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the resource.
+ */
+ getResourceFromJid: function (jid)
+ {
+ var s = jid.split("/");
+ if (s.length < 2) { return null; }
+ s.splice(0, 1);
+ return s.join('/');
+ },
+
+ /** Function: getBareJidFromJid
+ * Get the bare JID from a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the bare JID.
+ */
+ getBareJidFromJid: function (jid)
+ {
+ return jid ? jid.split("/")[0] : null;
+ },
+
+ /** Function: log
+ * User overrideable logging function.
+ *
+ * This function is called whenever the Strophe library calls any
+ * of the logging functions. The default implementation of this
+ * function does nothing. If client code wishes to handle the logging
+ * messages, it should override this with
+ * > Strophe.log = function (level, msg) {
+ * > (user code here)
+ * > };
+ *
+ * Please note that data sent and received over the wire is logged
+ * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
+ *
+ * The different levels and their meanings are
+ *
+ * DEBUG - Messages useful for debugging purposes.
+ * INFO - Informational messages. This is mostly information like
+ * 'disconnect was called' or 'SASL auth succeeded'.
+ * WARN - Warnings about potential problems. This is mostly used
+ * to report transient connection errors like request timeouts.
+ * ERROR - Some error occurred.
+ * FATAL - A non-recoverable fatal error occurred.
+ *
+ * Parameters:
+ * (Integer) level - The log level of the log message. This will
+ * be one of the values in Strophe.LogLevel.
+ * (String) msg - The log message.
+ */
+ log: function (level, msg)
+ {
+ return;
+ },
+
+ /** Function: debug
+ * Log a message at the Strophe.LogLevel.DEBUG level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ debug: function(msg)
+ {
+ this.log(this.LogLevel.DEBUG, msg);
+ },
+
+ /** Function: info
+ * Log a message at the Strophe.LogLevel.INFO level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ info: function (msg)
+ {
+ this.log(this.LogLevel.INFO, msg);
+ },
+
+ /** Function: warn
+ * Log a message at the Strophe.LogLevel.WARN level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ warn: function (msg)
+ {
+ this.log(this.LogLevel.WARN, msg);
+ },
+
+ /** Function: error
+ * Log a message at the Strophe.LogLevel.ERROR level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ error: function (msg)
+ {
+ this.log(this.LogLevel.ERROR, msg);
+ },
+
+ /** Function: fatal
+ * Log a message at the Strophe.LogLevel.FATAL level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ fatal: function (msg)
+ {
+ this.log(this.LogLevel.FATAL, msg);
+ },
+
+ /** Function: serialize
+ * Render a DOM element and all descendants to a String.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * The serialized element tree as a String.
+ */
+ serialize: function (elem)
+ {
+ var result;
+
+ if (!elem) { return null; }
+
+ if (typeof(elem.tree) === "function") {
+ elem = elem.tree();
+ }
+
+ var nodeName = elem.nodeName.toLowerCase();
+ var i, child;
+
+ if (elem.getAttribute("_realname")) {
+ nodeName = elem.getAttribute("_realname").toLowerCase();
+ }
+
+ result = "<" + nodeName.toLowerCase();
+ for (i = 0; i < elem.attributes.length; i++) {
+ if(elem.attributes[i].nodeName.toLowerCase() != "_realname") {
+ result += " " + elem.attributes[i].nodeName.toLowerCase() +
+ "='" + elem.attributes[i].value
+ .replace(/&/g, "&")
+ .replace(/\'/g, "'")
+ .replace(/ 0) {
+ result += ">";
+ for (i = 0; i < elem._childNodes.length; i++) {
+ child = elem._childNodes[i];
+ if (child.nodeType == Strophe.ElementType.NORMAL) {
+ // normal element, so recurse
+ result += Strophe.serialize(child);
+ } else if (child.nodeType == Strophe.ElementType.TEXT) {
+ // text element
+ result += child.nodeValue;
+ }
+ }
+ result += "" + nodeName.toLowerCase() + ">";
+ } else {
+ result += "/>";
+ }
+
+ return result;
+ },
+
+ /** PrivateVariable: _requestId
+ * _Private_ variable that keeps track of the request ids for
+ * connections.
+ */
+ _requestId: 0,
+
+ /** PrivateVariable: Strophe.connectionPlugins
+ * _Private_ variable Used to store plugin names that need
+ * initialization on Strophe.Connection construction.
+ */
+ _connectionPlugins: {},
+
+ /** Function: addConnectionPlugin
+ * Extends the Strophe.Connection object with the given plugin.
+ *
+ * Paramaters:
+ * (String) name - The name of the extension.
+ * (Object) ptype - The plugin's prototype.
+ */
+ addConnectionPlugin: function (name, ptype)
+ {
+ Strophe._connectionPlugins[name] = ptype;
+ }
+};
+
+/** Class: Strophe.Builder
+ * XML DOM builder.
+ *
+ * This object provides an interface similar to JQuery but for building
+ * DOM element easily and rapidly. All the functions except for toString()
+ * and tree() return the object, so calls can be chained. Here's an
+ * example using the $iq() builder helper.
+ * > $iq({to: 'you', from: 'me', type: 'get', id: '1'})
+ * > .c('query', {xmlns: 'strophe:example'})
+ * > .c('example')
+ * > .toString()
+ * The above generates this XML fragment
+ * >
+ * >
+ * >
+ * >
+ * >
+ * The corresponding DOM manipulations to get a similar fragment would be
+ * a lot more tedious and probably involve several helper variables.
+ *
+ * Since adding children makes new operations operate on the child, up()
+ * is provided to traverse up the tree. To add two children, do
+ * > builder.c('child1', ...).up().c('child2', ...)
+ * The next operation on the Builder will be relative to the second child.
+ */
+
+/** Constructor: Strophe.Builder
+ * Create a Strophe.Builder object.
+ *
+ * The attributes should be passed in object notation. For example
+ * > var b = new Builder('message', {to: 'you', from: 'me'});
+ * or
+ * > var b = new Builder('messsage', {'xml:lang': 'en'});
+ *
+ * Parameters:
+ * (String) name - The name of the root element.
+ * (Object) attrs - The attributes for the root element in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder.
+ */
+Strophe.Builder = function (name, attrs)
+{
+ // Set correct namespace for jabber:client elements
+ if (name == "presence" || name == "message" || name == "iq") {
+ if (attrs && !attrs.xmlns) {
+ attrs.xmlns = Strophe.NS.CLIENT;
+ } else if (!attrs) {
+ attrs = {xmlns: Strophe.NS.CLIENT};
+ }
+ }
+
+ // Holds the tree being built.
+ this.nodeTree = Strophe.xmlElement(name, attrs);
+
+ // Points to the current operation node.
+ this.node = this.nodeTree;
+};
+
+Strophe.Builder.prototype = {
+ /** Function: tree
+ * Return the DOM tree.
+ *
+ * This function returns the current DOM tree as an element object. This
+ * is suitable for passing to functions like Strophe.Connection.send().
+ *
+ * Returns:
+ * The DOM tree as a element object.
+ */
+ tree: function ()
+ {
+ return this.nodeTree;
+ },
+
+ /** Function: toString
+ * Serialize the DOM tree to a String.
+ *
+ * This function returns a string serialization of the current DOM
+ * tree. It is often used internally to pass data to a
+ * Strophe.Request object.
+ *
+ * Returns:
+ * The serialized DOM tree in a String.
+ */
+ toString: function ()
+ {
+ return Strophe.serialize(this.nodeTree);
+ },
+
+ /** Function: up
+ * Make the current parent element the new current element.
+ *
+ * This function is often used after c() to traverse back up the tree.
+ * For example, to add two children to the same element
+ * > builder.c('child1', {}).up().c('child2', {});
+ *
+ * Returns:
+ * The Stophe.Builder object.
+ */
+ up: function ()
+ {
+ this.node = this.node.parentNode;
+ return this;
+ },
+
+ /** Function: attrs
+ * Add or modify attributes of the current element.
+ *
+ * The attributes should be passed in object notation. This function
+ * does not move the current element pointer.
+ *
+ * Parameters:
+ * (Object) moreattrs - The attributes to add/modify in object notation.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ attrs: function (moreattrs)
+ {
+ for (var k in moreattrs) {
+ if (moreattrs.hasOwnProperty(k)) {
+ this.node.setAttribute(k, moreattrs[k]);
+ }
+ }
+ return this;
+ },
+
+ /** Function: c
+ * Add a child to the current element and make it the new current
+ * element.
+ *
+ * This function moves the current element pointer to the child. If you
+ * need to add another child, it is necessary to use up() to go back
+ * to the parent in the tree.
+ *
+ * Parameters:
+ * (String) name - The name of the child.
+ * (Object) attrs - The attributes of the child in object notation.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ c: function (name, attrs)
+ {
+ var child = Strophe.xmlElement(name, attrs);
+ this.node.appendChild(child);
+ this.node = child;
+ return this;
+ },
+
+ /** Function: cnode
+ * Add a child to the current element and make it the new current
+ * element.
+ *
+ * This function is the same as c() except that instead of using a
+ * name and an attributes object to create the child it uses an
+ * existing DOM element object.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ cnode: function (elem)
+ {
+ var xmlGen = Strophe.xmlGenerator();
+ var newElem = xmlGen.importNode ? xmlGen.importNode(elem, true) : Strophe.copyElement(elem);
+ this.node.appendChild(newElem);
+ this.node = newElem;
+ return this;
+ },
+
+ /** Function: t
+ * Add a child text element.
+ *
+ * This *does not* make the child the new current element since there
+ * are no children of text elements.
+ *
+ * Parameters:
+ * (String) text - The text data to append to the current element.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ t: function (text)
+ {
+ var child = Strophe.xmlTextNode(text);
+ this.node.appendChild(child);
+ return this;
+ }
+};
+
+
+/** PrivateClass: Strophe.Handler
+ * _Private_ helper class for managing stanza handlers.
+ *
+ * A Strophe.Handler encapsulates a user provided callback function to be
+ * executed when matching stanzas are received by the connection.
+ * Handlers can be either one-off or persistant depending on their
+ * return value. Returning true will cause a Handler to remain active, and
+ * returning false will remove the Handler.
+ *
+ * Users will not use Strophe.Handler objects directly, but instead they
+ * will use Strophe.Connection.addHandler() and
+ * Strophe.Connection.deleteHandler().
+ */
+
+/** PrivateConstructor: Strophe.Handler
+ * Create and initialize a new Strophe.Handler.
+ *
+ * Parameters:
+ * (Function) handler - A function to be executed when the handler is run.
+ * (String) ns - The namespace to match.
+ * (String) name - The element name to match.
+ * (String) type - The element type to match.
+ * (String) id - The element id attribute to match.
+ * (String) from - The element from attribute to match.
+ * (Object) options - Handler options
+ *
+ * Returns:
+ * A new Strophe.Handler object.
+ */
+Strophe.Handler = function (handler, ns, name, type, id, from, options)
+{
+ this.handler = handler;
+ this.ns = ns;
+ this.name = name;
+ this.type = type;
+ this.id = id;
+ this.options = options || {matchbare: false};
+
+ // default matchBare to false if undefined
+ if (!this.options.matchBare) {
+ this.options.matchBare = false;
+ }
+
+ if (this.options.matchBare) {
+ this.from = from ? Strophe.getBareJidFromJid(from) : null;
+ } else {
+ this.from = from;
+ }
+
+ // whether the handler is a user handler or a system handler
+ this.user = true;
+};
+
+Strophe.Handler.prototype = {
+ /** PrivateFunction: isMatch
+ * Tests if a stanza matches the Strophe.Handler.
+ *
+ * Parameters:
+ * (XMLElement) elem - The XML element to test.
+ *
+ * Returns:
+ * true if the stanza matches and false otherwise.
+ */
+ isMatch: function (elem)
+ {
+ var nsMatch;
+ var from = null;
+
+ if (this.options.matchBare) {
+ from = Strophe.getBareJidFromJid(elem.getAttribute('from'));
+ } else {
+ from = elem.getAttribute('from');
+ }
+
+ nsMatch = false;
+ if (!this.ns) {
+ nsMatch = true;
+ } else {
+ var that = this;
+ Strophe.forEachChild(elem, null, function (elem) {
+ if (elem.getAttribute("xmlns") == that.ns) {
+ nsMatch = true;
+ }
+ });
+
+ nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns;
+ }
+
+ if (nsMatch &&
+ (!this.name || Strophe.isTagEqual(elem, this.name)) &&
+ (!this.type || elem.getAttribute("type") == this.type) &&
+ (!this.id || elem.getAttribute("id") == this.id) &&
+ (!this.from || from == this.from)) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: run
+ * Run the callback on a matching stanza.
+ *
+ * Parameters:
+ * (XMLElement) elem - The DOM element that triggered the
+ * Strophe.Handler.
+ *
+ * Returns:
+ * A boolean indicating if the handler should remain active.
+ */
+ run: function (elem)
+ {
+ var result = null;
+ try {
+ result = this.handler(elem);
+ } catch (e) {
+ if (e.sourceURL) {
+ Strophe.fatal("error: " + this.handler +
+ " " + e.sourceURL + ":" +
+ e.line + " - " + e.name + ": " + e.message);
+ } else if (e.fileName) {
+ if (typeof(console) != "undefined") {
+ console.trace();
+ console.error(this.handler, " - error - ", e, e.message);
+ }
+ Strophe.fatal("error: " + this.handler + " " +
+ e.fileName + ":" + e.lineNumber + " - " +
+ e.name + ": " + e.message);
+ } else {
+ Strophe.fatal("error: " + this.handler);
+ }
+
+ throw e;
+ }
+
+ return result;
+ },
+
+ /** PrivateFunction: toString
+ * Get a String representation of the Strophe.Handler object.
+ *
+ * Returns:
+ * A String.
+ */
+ toString: function ()
+ {
+ return "{Handler: " + this.handler + "(" + this.name + "," +
+ this.id + "," + this.ns + ")}";
+ }
+};
+
+/** PrivateClass: Strophe.TimedHandler
+ * _Private_ helper class for managing timed handlers.
+ *
+ * A Strophe.TimedHandler encapsulates a user provided callback that
+ * should be called after a certain period of time or at regular
+ * intervals. The return value of the callback determines whether the
+ * Strophe.TimedHandler will continue to fire.
+ *
+ * Users will not use Strophe.TimedHandler objects directly, but instead
+ * they will use Strophe.Connection.addTimedHandler() and
+ * Strophe.Connection.deleteTimedHandler().
+ */
+
+/** PrivateConstructor: Strophe.TimedHandler
+ * Create and initialize a new Strophe.TimedHandler object.
+ *
+ * Parameters:
+ * (Integer) period - The number of milliseconds to wait before the
+ * handler is called.
+ * (Function) handler - The callback to run when the handler fires. This
+ * function should take no arguments.
+ *
+ * Returns:
+ * A new Strophe.TimedHandler object.
+ */
+Strophe.TimedHandler = function (period, handler)
+{
+ this.period = period;
+ this.handler = handler;
+
+ this.lastCalled = new Date().getTime();
+ this.user = true;
+};
+
+Strophe.TimedHandler.prototype = {
+ /** PrivateFunction: run
+ * Run the callback for the Strophe.TimedHandler.
+ *
+ * Returns:
+ * true if the Strophe.TimedHandler should be called again, and false
+ * otherwise.
+ */
+ run: function ()
+ {
+ this.lastCalled = new Date().getTime();
+ return this.handler();
+ },
+
+ /** PrivateFunction: reset
+ * Reset the last called time for the Strophe.TimedHandler.
+ */
+ reset: function ()
+ {
+ this.lastCalled = new Date().getTime();
+ },
+
+ /** PrivateFunction: toString
+ * Get a string representation of the Strophe.TimedHandler object.
+ *
+ * Returns:
+ * The string representation.
+ */
+ toString: function ()
+ {
+ return "{TimedHandler: " + this.handler + "(" + this.period +")}";
+ }
+};
+
+/** PrivateClass: Strophe.Request
+ * _Private_ helper class that provides a cross implementation abstraction
+ * for a BOSH related XMLHttpRequest.
+ *
+ * The Strophe.Request class is used internally to encapsulate BOSH request
+ * information. It is not meant to be used from user's code.
+ */
+
+/** PrivateConstructor: Strophe.Request
+ * Create and initialize a new Strophe.Request object.
+ *
+ * Parameters:
+ * (XMLElement) elem - The XML data to be sent in the request.
+ * (Function) func - The function that will be called when the
+ * XMLHttpRequest readyState changes.
+ * (Integer) rid - The BOSH rid attribute associated with this request.
+ * (Integer) sends - The number of times this same request has been
+ * sent.
+ */
+Strophe.Request = function (elem, func, rid, sends)
+{
+ this.id = ++Strophe._requestId;
+ this.xmlData = elem;
+ this.data = Strophe.serialize(elem);
+ // save original function in case we need to make a new request
+ // from this one.
+ this.origFunc = func;
+ this.func = func;
+ this.rid = rid;
+ this.date = NaN;
+ this.sends = sends || 0;
+ this.abort = false;
+ this.dead = null;
+ this.age = function () {
+ if (!this.date) { return 0; }
+ var now = new Date();
+ return (now - this.date) / 1000;
+ };
+ this.timeDead = function () {
+ if (!this.dead) { return 0; }
+ var now = new Date();
+ return (now - this.dead) / 1000;
+ };
+ this.xhr = this._newXHR();
+};
+
+Strophe.Request.prototype = {
+ /** PrivateFunction: getResponse
+ * Get a response from the underlying XMLHttpRequest.
+ *
+ * This function attempts to get a response from the request and checks
+ * for errors.
+ *
+ * Throws:
+ * "parsererror" - A parser error occured.
+ *
+ * Returns:
+ * The DOM element tree of the response.
+ */
+ getResponse: function ()
+ {
+ // console.log("getResponse:", this.xhr.responseXML, ":", this.xhr.responseText);
+
+ var node = null;
+ if (this.xhr.responseXML && this.xhr.responseXML.documentElement) {
+ node = this.xhr.responseXML.documentElement;
+ if (node.tagName == "parsererror") {
+ Strophe.error("invalid response received");
+ Strophe.error("responseText: " + this.xhr.responseText);
+ Strophe.error("responseXML: " +
+ Strophe.serialize(this.xhr.responseXML));
+ throw "parsererror";
+ }
+ } else if (this.xhr.responseText) {
+ // Hack for node.
+ var _div = document.createElement("div");
+ _div.innerHTML = this.xhr.responseText;
+ node = _div._childNodes[0];
+
+ Strophe.error("invalid response received");
+ Strophe.error("responseText: " + this.xhr.responseText);
+ Strophe.error("responseXML: " +
+ Strophe.serialize(this.xhr.responseXML));
+ }
+
+ return node;
+ },
+
+ /** PrivateFunction: _newXHR
+ * _Private_ helper function to create XMLHttpRequests.
+ *
+ * This function creates XMLHttpRequests across all implementations.
+ *
+ * Returns:
+ * A new XMLHttpRequest.
+ */
+ _newXHR: function ()
+ {
+ var xhr = null;
+ if (window.XMLHttpRequest) {
+ xhr = new XMLHttpRequest();
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType("text/xml");
+ }
+ } else if (window.ActiveXObject) {
+ xhr = new ActiveXObject("Microsoft.XMLHTTP");
+ }
+
+ // use Function.bind() to prepend ourselves as an argument
+ xhr.onreadystatechange = this.func.bind(null, this);
+
+ return xhr;
+ }
+};
+
+/** Class: Strophe.Connection
+ * XMPP Connection manager.
+ *
+ * Thie class is the main part of Strophe. It manages a BOSH connection
+ * to an XMPP server and dispatches events to the user callbacks as
+ * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy
+ * authentication.
+ *
+ * After creating a Strophe.Connection object, the user will typically
+ * call connect() with a user supplied callback to handle connection level
+ * events like authentication failure, disconnection, or connection
+ * complete.
+ *
+ * The user will also have several event handlers defined by using
+ * addHandler() and addTimedHandler(). These will allow the user code to
+ * respond to interesting stanzas or do something periodically with the
+ * connection. These handlers will be active once authentication is
+ * finished.
+ *
+ * To send data to the connection, use send().
+ */
+
+/** Constructor: Strophe.Connection
+ * Create and initialize a Strophe.Connection object.
+ *
+ * Parameters:
+ * (String) service - The BOSH service URL.
+ *
+ * Returns:
+ * A new Strophe.Connection object.
+ */
+Strophe.Connection = function (service)
+{
+ /* The path to the httpbind service. */
+ this.service = service;
+ /* The connected JID. */
+ this.jid = "";
+ /* request id for body tags */
+ this.rid = Math.floor(Math.random() * 4294967295);
+ /* The current session ID. */
+ this.sid = null;
+ this.streamId = null;
+ /* stream:features */
+ this.features = null;
+
+ // SASL
+ this.do_session = false;
+ this.do_bind = false;
+
+ // handler lists
+ this.timedHandlers = [];
+ this.handlers = [];
+ this.removeTimeds = [];
+ this.removeHandlers = [];
+ this.addTimeds = [];
+ this.addHandlers = [];
+
+ this._idleTimeout = null;
+ this._disconnectTimeout = null;
+
+ this.authenticated = false;
+ this.disconnecting = false;
+ this.connected = false;
+
+ this.errors = 0;
+
+ this.paused = false;
+
+ // default BOSH values
+ this.hold = 1;
+ this.wait = 60;
+ this.window = 5;
+
+ this._data = [];
+ this._requests = [];
+ this._uniqueId = Math.round(Math.random() * 10000);
+
+ this._sasl_success_handler = null;
+ this._sasl_failure_handler = null;
+ this._sasl_challenge_handler = null;
+
+ // setup onIdle callback every 1/10th of a second
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+
+ // initialize plugins
+ for (var k in Strophe._connectionPlugins) {
+ if (Strophe._connectionPlugins.hasOwnProperty(k)) {
+ var ptype = Strophe._connectionPlugins[k];
+ // jslint complaints about the below line, but this is fine
+ var F = function () {};
+ F.prototype = ptype;
+ this[k] = new F();
+ this[k].init(this);
+ }
+ }
+};
+
+Strophe.Connection.prototype = {
+ /** Function: reset
+ * Reset the connection.
+ *
+ * This function should be called after a connection is disconnected
+ * before that connection is reused.
+ */
+ reset: function ()
+ {
+ this.rid = Math.floor(Math.random() * 4294967295);
+
+ this.sid = null;
+ this.streamId = null;
+
+ // SASL
+ this.do_session = false;
+ this.do_bind = false;
+
+ // handler lists
+ this.timedHandlers = [];
+ this.handlers = [];
+ this.removeTimeds = [];
+ this.removeHandlers = [];
+ this.addTimeds = [];
+ this.addHandlers = [];
+
+ this.authenticated = false;
+ this.disconnecting = false;
+ this.connected = false;
+
+ this.errors = 0;
+
+ this._requests = [];
+ this._uniqueId = Math.round(Math.random()*10000);
+ },
+
+ /** Function: pause
+ * Pause the request manager.
+ *
+ * This will prevent Strophe from sending any more requests to the
+ * server. This is very useful for temporarily pausing while a lot
+ * of send() calls are happening quickly. This causes Strophe to
+ * send the data in a single request, saving many request trips.
+ */
+ pause: function ()
+ {
+ this.paused = true;
+ },
+
+ /** Function: resume
+ * Resume the request manager.
+ *
+ * This resumes after pause() has been called.
+ */
+ resume: function ()
+ {
+ this.paused = false;
+ },
+
+ /** Function: getUniqueId
+ * Generate a unique ID for use in elements.
+ *
+ * All stanzas are required to have unique id attributes. This
+ * function makes creating these easy. Each connection instance has
+ * a counter which starts from zero, and the value of this counter
+ * plus a colon followed by the suffix becomes the unique id. If no
+ * suffix is supplied, the counter is used as the unique id.
+ *
+ * Suffixes are used to make debugging easier when reading the stream
+ * data, and their use is recommended. The counter resets to 0 for
+ * every new connection for the same reason. For connections to the
+ * same server that authenticate the same way, all the ids should be
+ * the same, which makes it easy to see changes. This is useful for
+ * automated testing as well.
+ *
+ * Parameters:
+ * (String) suffix - A optional suffix to append to the id.
+ *
+ * Returns:
+ * A unique string to be used for the id attribute.
+ */
+ getUniqueId: function (suffix)
+ {
+ if (typeof(suffix) == "string" || typeof(suffix) == "number") {
+ return ++this._uniqueId + ":" + suffix;
+ } else {
+ return ++this._uniqueId + "";
+ }
+ },
+
+ /** Function: connect
+ * Starts the connection process.
+ *
+ * As the connection process proceeds, the user supplied callback will
+ * be triggered multiple times with status updates. The callback
+ * should take two arguments - the status code and the error condition.
+ *
+ * The status code will be one of the values in the Strophe.Status
+ * constants. The error condition will be one of the conditions
+ * defined in RFC 3920 or the condition 'strophe-parsererror'.
+ *
+ * Please see XEP 124 for a more detailed explanation of the optional
+ * parameters below.
+ *
+ * Parameters:
+ * (String) jid - The user's JID. This may be a bare JID,
+ * or a full JID. If a node is not supplied, SASL ANONYMOUS
+ * authentication will be attempted.
+ * (String) pass - The user's password.
+ * (Function) callback The connect callback function.
+ * (Integer) wait - The optional HTTPBIND wait value. This is the
+ * time the server will wait before returning an empty result for
+ * a request. The default setting of 60 seconds is recommended.
+ * Other settings will require tweaks to the Strophe.TIMEOUT value.
+ * (Integer) hold - The optional HTTPBIND hold value. This is the
+ * number of connections the server will hold at one time. This
+ * should almost always be set to 1 (the default).
+ */
+ connect: function (jid, pass, callback, wait, hold, route)
+ {
+ this.jid = jid;
+ this.pass = pass;
+ this.connect_callback = callback;
+ this.disconnecting = false;
+ this.connected = false;
+ this.authenticated = false;
+ this.errors = 0;
+
+ this.wait = wait || this.wait;
+ this.hold = hold || this.hold;
+
+ // parse jid for domain and resource
+ this.domain = Strophe.getDomainFromJid(this.jid);
+
+ // build the body tag
+ var body_attrs = {
+ to: this.domain,
+ "xml:lang": "en",
+ wait: this.wait,
+ hold: this.hold,
+ content: "text/xml; charset=utf-8",
+ ver: "1.6",
+ "xmpp:version": "1.0",
+ "xmlns:xmpp": Strophe.NS.BOSH
+ };
+ if (route) {
+ body_attrs.route = route;
+ }
+
+ var body = this._buildBody().attrs(body_attrs);
+
+ this._changeConnectStatus(Strophe.Status.CONNECTING, null);
+
+ this._requests.push(
+ new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._connect_cb.bind(this)),
+ body.tree().getAttribute("rid")));
+ this._throttledRequestHandler();
+ },
+
+ /** Function: attach
+ * Attach to an already created and authenticated BOSH session.
+ *
+ * This function is provided to allow Strophe to attach to BOSH
+ * sessions which have been created externally, perhaps by a Web
+ * application. This is often used to support auto-login type features
+ * without putting user credentials into the page.
+ *
+ * Parameters:
+ * (String) jid - The full JID that is bound by the session.
+ * (String) sid - The SID of the BOSH session.
+ * (String) rid - The current RID of the BOSH session. This RID
+ * will be used by the next request.
+ * (Function) callback The connect callback function.
+ * (Integer) wait - The optional HTTPBIND wait value. This is the
+ * time the server will wait before returning an empty result for
+ * a request. The default setting of 60 seconds is recommended.
+ * Other settings will require tweaks to the Strophe.TIMEOUT value.
+ * (Integer) hold - The optional HTTPBIND hold value. This is the
+ * number of connections the server will hold at one time. This
+ * should almost always be set to 1 (the default).
+ * (Integer) wind - The optional HTTBIND window value. This is the
+ * allowed range of request ids that are valid. The default is 5.
+ */
+ attach: function (jid, sid, rid, callback, wait, hold, wind)
+ {
+ this.jid = jid;
+ this.sid = sid;
+ this.rid = rid;
+ this.connect_callback = callback;
+
+ this.domain = Strophe.getDomainFromJid(this.jid);
+
+ this.authenticated = true;
+ this.connected = true;
+
+ this.wait = wait || this.wait;
+ this.hold = hold || this.hold;
+ this.window = wind || this.window;
+
+ this._changeConnectStatus(Strophe.Status.ATTACHED, null);
+ },
+
+ /** Function: xmlInput
+ * User overrideable function that receives XML data coming into the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.xmlInput = function (elem) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (XMLElement) elem - The XML data received by the connection.
+ */
+ xmlInput: function (elem)
+ {
+ return;
+ },
+
+ /** Function: xmlOutput
+ * User overrideable function that receives XML data sent to the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.xmlOutput = function (elem) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (XMLElement) elem - The XMLdata sent by the connection.
+ */
+ xmlOutput: function (elem)
+ {
+ return;
+ },
+
+ /** Function: rawInput
+ * User overrideable function that receives raw data coming into the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.rawInput = function (data) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (String) data - The data received by the connection.
+ */
+ rawInput: function (data)
+ {
+ return;
+ },
+
+ /** Function: rawOutput
+ * User overrideable function that receives raw data sent to the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.rawOutput = function (data) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (String) data - The data sent by the connection.
+ */
+ rawOutput: function (data)
+ {
+ return;
+ },
+
+ /** Function: send
+ * Send a stanza.
+ *
+ * This function is called to push data onto the send queue to
+ * go out over the wire. Whenever a request is sent to the BOSH
+ * server, all pending data is sent and the queue is flushed.
+ *
+ * Parameters:
+ * (XMLElement |
+ * [XMLElement] |
+ * Strophe.Builder) elem - The stanza to send.
+ */
+ send: function (elem)
+ {
+ if (elem === null) { return ; }
+ if (typeof(elem.sort) === "function") {
+ for (var i = 0; i < elem.length; i++) {
+ this._queueData(elem[i]);
+ }
+ } else if (typeof(elem.tree) === "function") {
+ this._queueData(elem.tree());
+ } else {
+ this._queueData(elem);
+ }
+
+ this._throttledRequestHandler();
+ clearTimeout(this._idleTimeout);
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+ },
+
+ /** Function: flush
+ * Immediately send any pending outgoing data.
+ *
+ * Normally send() queues outgoing data until the next idle period
+ * (100ms), which optimizes network use in the common cases when
+ * several send()s are called in succession. flush() can be used to
+ * immediately send all pending data.
+ */
+ flush: function ()
+ {
+ // cancel the pending idle period and run the idle function
+ // immediately
+ clearTimeout(this._idleTimeout);
+ this._onIdle();
+ },
+
+ /** Function: sendIQ
+ * Helper function to send IQ stanzas.
+ *
+ * Parameters:
+ * (XMLElement) elem - The stanza to send.
+ * (Function) callback - The callback function for a successful request.
+ * (Function) errback - The callback function for a failed or timed
+ * out request. On timeout, the stanza will be null.
+ * (Integer) timeout - The time specified in milliseconds for a
+ * timeout to occur.
+ *
+ * Returns:
+ * The id used to send the IQ.
+ */
+ sendIQ: function(elem, callback, errback, timeout) {
+ var timeoutHandler = null;
+ var that = this;
+
+ if (typeof(elem.tree) === "function") {
+ elem = elem.tree();
+ }
+ var id = elem.getAttribute('id');
+
+ // inject id if not found
+ if (!id) {
+ id = this.getUniqueId("sendIQ");
+ elem.setAttribute("id", id);
+ }
+
+ var handler = this.addHandler(function (stanza) {
+ // remove timeout handler if there is one
+ if (timeoutHandler) {
+ that.deleteTimedHandler(timeoutHandler);
+ }
+
+ var iqtype = stanza.getAttribute('type');
+ if (iqtype == 'result') {
+ if (callback) {
+ callback(stanza);
+ }
+ } else if (iqtype == 'error') {
+ if (errback) {
+ errback(stanza);
+ }
+ } else {
+ throw {
+ name: "StropheError",
+ message: "Got bad IQ type of " + iqtype
+ };
+ }
+ }, null, 'iq', null, id);
+
+ // if timeout specified, setup timeout handler.
+ if (timeout) {
+ timeoutHandler = this.addTimedHandler(timeout, function () {
+ // get rid of normal handler
+ that.deleteHandler(handler);
+
+ // call errback on timeout with null stanza
+ if (errback) {
+ errback(null);
+ }
+ return false;
+ });
+ }
+
+ this.send(elem);
+
+ return id;
+ },
+
+ /** PrivateFunction: _queueData
+ * Queue outgoing data for later sending. Also ensures that the data
+ * is a DOMElement.
+ */
+ _queueData: function (element) {
+ if (element === null ||
+ !element.tagName ||
+ !element._childNodes) {
+ throw {
+ name: "StropheError",
+ message: "Cannot queue non-DOMElement."
+ };
+ }
+
+ this._data.push(element);
+ },
+
+ /** PrivateFunction: _sendRestart
+ * Send an xmpp:restart stanza.
+ */
+ _sendRestart: function ()
+ {
+ this._data.push("restart");
+
+ this._throttledRequestHandler();
+ clearTimeout(this._idleTimeout);
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+ },
+
+ /** Function: addTimedHandler
+ * Add a timed handler to the connection.
+ *
+ * This function adds a timed handler. The provided handler will
+ * be called every period milliseconds until it returns false,
+ * the connection is terminated, or the handler is removed. Handlers
+ * that wish to continue being invoked should return true.
+ *
+ * Because of method binding it is necessary to save the result of
+ * this function if you wish to remove a handler with
+ * deleteTimedHandler().
+ *
+ * Note that user handlers are not active until authentication is
+ * successful.
+ *
+ * Parameters:
+ * (Integer) period - The period of the handler.
+ * (Function) handler - The callback function.
+ *
+ * Returns:
+ * A reference to the handler that can be used to remove it.
+ */
+ addTimedHandler: function (period, handler)
+ {
+ var thand = new Strophe.TimedHandler(period, handler);
+ this.addTimeds.push(thand);
+ return thand;
+ },
+
+ /** Function: deleteTimedHandler
+ * Delete a timed handler for a connection.
+ *
+ * This function removes a timed handler from the connection. The
+ * handRef parameter is *not* the function passed to addTimedHandler(),
+ * but is the reference returned from addTimedHandler().
+ *
+ * Parameters:
+ * (Strophe.TimedHandler) handRef - The handler reference.
+ */
+ deleteTimedHandler: function (handRef)
+ {
+ // this must be done in the Idle loop so that we don't change
+ // the handlers during iteration
+ this.removeTimeds.push(handRef);
+ },
+
+ /** Function: addHandler
+ * Add a stanza handler for the connection.
+ *
+ * This function adds a stanza handler to the connection. The
+ * handler callback will be called for any stanza that matches
+ * the parameters. Note that if multiple parameters are supplied,
+ * they must all match for the handler to be invoked.
+ *
+ * The handler will receive the stanza that triggered it as its argument.
+ * The handler should return true if it is to be invoked again;
+ * returning false will remove the handler after it returns.
+ *
+ * As a convenience, the ns parameters applies to the top level element
+ * and also any of its immediate children. This is primarily to make
+ * matching /iq/query elements easy.
+ *
+ * The options argument contains handler matching flags that affect how
+ * matches are determined. Currently the only flag is matchBare (a
+ * boolean). When matchBare is true, the from parameter and the from
+ * attribute on the stanza will be matched as bare JIDs instead of
+ * full JIDs. To use this, pass {matchBare: true} as the value of
+ * options. The default value for matchBare is false.
+ *
+ * The return value should be saved if you wish to remove the handler
+ * with deleteHandler().
+ *
+ * Parameters:
+ * (Function) handler - The user callback.
+ * (String) ns - The namespace to match.
+ * (String) name - The stanza name to match.
+ * (String) type - The stanza type attribute to match.
+ * (String) id - The stanza id attribute to match.
+ * (String) from - The stanza from attribute to match.
+ * (String) options - The handler options
+ *
+ * Returns:
+ * A reference to the handler that can be used to remove it.
+ */
+ addHandler: function (handler, ns, name, type, id, from, options)
+ {
+ var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
+ this.addHandlers.push(hand);
+ return hand;
+ },
+
+ /** Function: deleteHandler
+ * Delete a stanza handler for a connection.
+ *
+ * This function removes a stanza handler from the connection. The
+ * handRef parameter is *not* the function passed to addHandler(),
+ * but is the reference returned from addHandler().
+ *
+ * Parameters:
+ * (Strophe.Handler) handRef - The handler reference.
+ */
+ deleteHandler: function (handRef)
+ {
+ // this must be done in the Idle loop so that we don't change
+ // the handlers during iteration
+ this.removeHandlers.push(handRef);
+ },
+
+ /** Function: disconnect
+ * Start the graceful disconnection process.
+ *
+ * This function starts the disconnection process. This process starts
+ * by sending unavailable presence and sending BOSH body of type
+ * terminate. A timeout handler makes sure that disconnection happens
+ * even if the BOSH server does not respond.
+ *
+ * The user supplied connection callback will be notified of the
+ * progress as this process happens.
+ *
+ * Parameters:
+ * (String) reason - The reason the disconnect is occuring.
+ */
+ disconnect: function (reason)
+ {
+ this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
+
+ Strophe.info("Disconnect was called because: " + reason);
+ if (this.connected) {
+ // setup timeout handler
+ this._disconnectTimeout = this._addSysTimedHandler(
+ 3000, this._onDisconnectTimeout.bind(this));
+ this._sendTerminate();
+ }
+ },
+
+ /** PrivateFunction: _changeConnectStatus
+ * _Private_ helper function that makes sure plugins and the user's
+ * callback are notified of connection status changes.
+ *
+ * Parameters:
+ * (Integer) status - the new connection status, one of the values
+ * in Strophe.Status
+ * (String) condition - the error condition or null
+ */
+ _changeConnectStatus: function (status, condition)
+ {
+ // notify all plugins listening for status changes
+ for (var k in Strophe._connectionPlugins) {
+ if (Strophe._connectionPlugins.hasOwnProperty(k)) {
+ var plugin = this[k];
+ if (plugin.statusChanged) {
+ try {
+ plugin.statusChanged(status, condition);
+ } catch (err) {
+ Strophe.error("" + k + " plugin caused an exception " +
+ "changing status: " + err);
+ }
+ }
+ }
+ }
+
+ // notify the user's callback
+ if (this.connect_callback) {
+ try {
+ this.connect_callback(status, condition);
+ } catch (e) {
+ Strophe.error("User connection callback caused an " +
+ "exception: " + e);
+ }
+ }
+ },
+
+ /** PrivateFunction: _buildBody
+ * _Private_ helper function to generate the wrapper for BOSH.
+ *
+ * Returns:
+ * A Strophe.Builder with a element.
+ */
+ _buildBody: function ()
+ {
+ var bodyWrap = $build('body', {
+ rid: this.rid++,
+ xmlns: Strophe.NS.HTTPBIND
+ });
+
+ if (this.sid !== null) {
+ bodyWrap.attrs({sid: this.sid});
+ }
+
+ return bodyWrap;
+ },
+
+ /** PrivateFunction: _removeRequest
+ * _Private_ function to remove a request from the queue.
+ *
+ * Parameters:
+ * (Strophe.Request) req - The request to remove.
+ */
+ _removeRequest: function (req)
+ {
+ Strophe.debug("removing request");
+
+ var i;
+ for (i = this._requests.length - 1; i >= 0; i--) {
+ if (req == this._requests[i]) {
+ this._requests.splice(i, 1);
+ }
+ }
+
+ // IE6 fails on setting to null, so set to empty function
+ req.xhr.onreadystatechange = function () {};
+
+ this._throttledRequestHandler();
+ },
+
+ /** PrivateFunction: _restartRequest
+ * _Private_ function to restart a request that is presumed dead.
+ *
+ * Parameters:
+ * (Integer) i - The index of the request in the queue.
+ */
+ _restartRequest: function (i)
+ {
+ var req = this._requests[i];
+ if (req.dead === null) {
+ req.dead = new Date();
+ }
+
+ this._processRequest(i);
+ },
+
+ /** PrivateFunction: _processRequest
+ * _Private_ function to process a request in the queue.
+ *
+ * This function takes requests off the queue and sends them and
+ * restarts dead requests.
+ *
+ * Parameters:
+ * (Integer) i - The index of the request in the queue.
+ */
+ _processRequest: function (i)
+ {
+ var req = this._requests[i];
+ var reqStatus = -1;
+
+ try {
+ if (req.xhr.readyState == 4) {
+ reqStatus = req.xhr.status;
+ }
+ } catch (e) {
+ Strophe.error("caught an error in _requests[" + i +
+ "], reqStatus: " + reqStatus);
+ }
+
+ if (typeof(reqStatus) == "undefined") {
+ reqStatus = -1;
+ }
+
+ // make sure we limit the number of retries
+ if (req.sends > 5) {
+ this._onDisconnectTimeout();
+ return;
+ }
+
+ var time_elapsed = req.age();
+ var primaryTimeout = (!isNaN(time_elapsed) &&
+ time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait));
+ var secondaryTimeout = (req.dead !== null &&
+ req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait));
+ var requestCompletedWithServerError = (req.xhr.readyState == 4 &&
+ (reqStatus < 1 ||
+ reqStatus >= 500));
+ if (primaryTimeout || secondaryTimeout ||
+ requestCompletedWithServerError) {
+ if (secondaryTimeout) {
+ Strophe.error("Request " +
+ this._requests[i].id +
+ " timed out (secondary), restarting");
+ }
+ req.abort = true;
+ req.xhr.abort();
+ // setting to null fails on IE6, so set to empty function
+ req.xhr.onreadystatechange = function () {};
+ this._requests[i] = new Strophe.Request(req.xmlData,
+ req.origFunc,
+ req.rid,
+ req.sends);
+ req = this._requests[i];
+ }
+
+ if (req.xhr.readyState === 0) {
+ Strophe.debug("request id " + req.id +
+ "." + req.sends + " posting");
+
+ req.date = new Date();
+ try {
+ req.xhr.open("POST", this.service, true);
+ } catch (e2) {
+ Strophe.error("XHR open failed.");
+ if (!this.connected) {
+ this._changeConnectStatus(Strophe.Status.CONNFAIL,
+ "bad-service");
+ }
+ this.disconnect();
+ return;
+ }
+
+ // Fires the XHR request -- may be invoked immediately
+ // or on a gradually expanding retry window for reconnects
+ var sendFunc = function () {
+ req.xhr.send(req.data);
+ };
+
+ // Implement progressive backoff for reconnects --
+ // First retry (send == 1) should also be instantaneous
+ if (req.sends > 1) {
+ // Using a cube of the retry number creats a nicely
+ // expanding retry window
+ var backoff = Math.pow(req.sends, 3) * 1000;
+ setTimeout(sendFunc, backoff);
+ } else {
+ sendFunc();
+ }
+
+ req.sends++;
+
+ this.xmlOutput(req.xmlData);
+ this.rawOutput(req.data);
+ } else {
+ Strophe.debug("_processRequest: " +
+ (i === 0 ? "first" : "second") +
+ " request has readyState of " +
+ req.xhr.readyState);
+ }
+ },
+
+ /** PrivateFunction: _throttledRequestHandler
+ * _Private_ function to throttle requests to the connection window.
+ *
+ * This function makes sure we don't send requests so fast that the
+ * request ids overflow the connection window in the case that one
+ * request died.
+ */
+ _throttledRequestHandler: function ()
+ {
+ if (!this._requests) {
+ Strophe.debug("_throttledRequestHandler called with " +
+ "undefined requests");
+ } else {
+ Strophe.debug("_throttledRequestHandler called with " +
+ this._requests.length + " requests");
+ }
+
+ if (!this._requests || this._requests.length === 0) {
+ return;
+ }
+
+ if (this._requests.length > 0) {
+ this._processRequest(0);
+ }
+
+ if (this._requests.length > 1 &&
+ Math.abs(this._requests[0].rid -
+ this._requests[1].rid) < this.window) {
+ this._processRequest(1);
+ }
+ },
+
+ /** PrivateFunction: _onRequestStateChange
+ * _Private_ handler for Strophe.Request state changes.
+ *
+ * This function is called when the XMLHttpRequest readyState changes.
+ * It contains a lot of error handling logic for the many ways that
+ * requests can fail, and calls the request callback when requests
+ * succeed.
+ *
+ * Parameters:
+ * (Function) func - The handler for the request.
+ * (Strophe.Request) req - The request that is changing readyState.
+ */
+ _onRequestStateChange: function (func, req)
+ {
+ Strophe.debug("request id " + req.id +
+ "." + req.sends + " state changed to " +
+ req.xhr.readyState);
+
+ if (req.abort) {
+ req.abort = false;
+ return;
+ }
+
+ // request complete
+ var reqStatus;
+ if (req.xhr.readyState == 4) {
+ reqStatus = 0;
+ try {
+ reqStatus = req.xhr.status;
+ } catch (e) {
+ // ignore errors from undefined status attribute. works
+ // around a browser bug
+ }
+
+ if (typeof(reqStatus) == "undefined") {
+ reqStatus = 0;
+ }
+
+ if (this.disconnecting) {
+ if (reqStatus >= 400) {
+ this._hitError(reqStatus);
+ return;
+ }
+ }
+
+ var reqIs0 = (this._requests[0] == req);
+ var reqIs1 = (this._requests[1] == req);
+
+ if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) {
+ // remove from internal queue
+ this._removeRequest(req);
+ Strophe.debug("request id " +
+ req.id +
+ " should now be removed");
+ }
+
+ // request succeeded
+ if (reqStatus == 200) {
+ // if request 1 finished, or request 0 finished and request
+ // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to
+ // restart the other - both will be in the first spot, as the
+ // completed request has been removed from the queue already
+ if (reqIs1 ||
+ (reqIs0 && this._requests.length > 0 &&
+ this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) {
+ this._restartRequest(0);
+ }
+ // call handler
+ Strophe.debug("request id " +
+ req.id + "." +
+ req.sends + " got 200");
+ func(req);
+ this.errors = 0;
+ } else {
+ Strophe.error("request id " +
+ req.id + "." +
+ req.sends + " error " + reqStatus +
+ " happened");
+ if (reqStatus === 0 ||
+ (reqStatus >= 400 && reqStatus < 600) ||
+ reqStatus >= 12000) {
+ this._hitError(reqStatus);
+ if (reqStatus >= 400 && reqStatus < 500) {
+ this._changeConnectStatus(Strophe.Status.DISCONNECTING,
+ null);
+ this._doDisconnect();
+ }
+ }
+ }
+
+ if (!((reqStatus > 0 && reqStatus < 500) ||
+ req.sends > 5)) {
+ this._throttledRequestHandler();
+ }
+ }
+ },
+
+ /** PrivateFunction: _hitError
+ * _Private_ function to handle the error count.
+ *
+ * Requests are resent automatically until their error count reaches
+ * 5. Each time an error is encountered, this function is called to
+ * increment the count and disconnect if the count is too high.
+ *
+ * Parameters:
+ * (Integer) reqStatus - The request status.
+ */
+ _hitError: function (reqStatus)
+ {
+ this.errors++;
+ Strophe.warn("request errored, status: " + reqStatus +
+ ", number of errors: " + this.errors);
+ if (this.errors > 4) {
+ this._onDisconnectTimeout();
+ }
+ },
+
+ /** PrivateFunction: _doDisconnect
+ * _Private_ function to disconnect.
+ *
+ * This is the last piece of the disconnection logic. This resets the
+ * connection and alerts the user's connection callback.
+ */
+ _doDisconnect: function ()
+ {
+ Strophe.info("_doDisconnect was called");
+ this.authenticated = false;
+ this.disconnecting = false;
+ this.sid = null;
+ this.streamId = null;
+ this.rid = Math.floor(Math.random() * 4294967295);
+
+ // tell the parent we disconnected
+ if (this.connected) {
+ this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
+ this.connected = false;
+ }
+
+ // delete handlers
+ this.handlers = [];
+ this.timedHandlers = [];
+ this.removeTimeds = [];
+ this.removeHandlers = [];
+ this.addTimeds = [];
+ this.addHandlers = [];
+ },
+
+ /** PrivateFunction: _dataRecv
+ * _Private_ handler to processes incoming data from the the connection.
+ *
+ * Except for _connect_cb handling the initial connection request,
+ * this function handles the incoming data for all requests. This
+ * function also fires stanza handlers that match each incoming
+ * stanza.
+ *
+ * Parameters:
+ * (Strophe.Request) req - The request that has data ready.
+ */
+ _dataRecv: function (req)
+ {
+ try {
+ var elem = req.getResponse();
+ } catch (e) {
+ if (e != "parsererror") { throw e; }
+ this.disconnect("strophe-parsererror");
+ }
+ if (elem === null) { return; }
+
+ this.xmlInput(elem);
+ this.rawInput(Strophe.serialize(elem));
+
+ // remove handlers scheduled for deletion
+ var i, hand;
+ while (this.removeHandlers.length > 0) {
+ hand = this.removeHandlers.pop();
+ i = this.handlers.indexOf(hand);
+ if (i >= 0) {
+ this.handlers.splice(i, 1);
+ }
+ }
+
+ // add handlers scheduled for addition
+ while (this.addHandlers.length > 0) {
+ this.handlers.push(this.addHandlers.pop());
+ }
+
+ // handle graceful disconnect
+ if (this.disconnecting && this._requests.length === 0) {
+ this.deleteTimedHandler(this._disconnectTimeout);
+ this._disconnectTimeout = null;
+ this._doDisconnect();
+ return;
+ }
+
+ var typ = elem.getAttribute("type");
+ var cond, conflict;
+ if (typ !== null && typ == "terminate") {
+ // Don't process stanzas that come in after disconnect
+ if (this.disconnecting) {
+ return;
+ }
+
+ // an error occurred
+ cond = elem.getAttribute("condition");
+ conflict = elem.getElementsByTagName("conflict");
+ if (cond !== null) {
+ if (cond == "remote-stream-error" && conflict.length > 0) {
+ cond = "conflict";
+ }
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
+ } else {
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+ }
+ this.disconnect();
+ return;
+ }
+
+ // send each incoming stanza through the handler chain
+ var that = this;
+ Strophe.forEachChild(elem, null, function (child) {
+ var i, newList;
+ // process handlers
+ newList = that.handlers;
+ that.handlers = [];
+ for (i = 0; i < newList.length; i++) {
+ var hand = newList[i];
+ if (hand.isMatch(child) &&
+ (that.authenticated || !hand.user)) {
+ if (hand.run(child)) {
+ that.handlers.push(hand);
+ }
+ } else {
+ that.handlers.push(hand);
+ }
+ }
+ });
+ },
+
+ /** PrivateFunction: _sendTerminate
+ * _Private_ function to send initial disconnect sequence.
+ *
+ * This is the first step in a graceful disconnect. It sends
+ * the BOSH server a terminate body and includes an unavailable
+ * presence if authentication has completed.
+ */
+ _sendTerminate: function ()
+ {
+ Strophe.info("_sendTerminate was called");
+ var body = this._buildBody().attrs({type: "terminate"});
+
+ if (this.authenticated) {
+ body.c('presence', {
+ xmlns: Strophe.NS.CLIENT,
+ type: 'unavailable'
+ });
+ }
+
+ this.disconnecting = true;
+
+ var req = new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._dataRecv.bind(this)),
+ body.tree().getAttribute("rid"));
+
+ this._requests.push(req);
+ this._throttledRequestHandler();
+ },
+
+ /** PrivateFunction: _connect_cb
+ * _Private_ handler for initial connection request.
+ *
+ * This handler is used to process the initial connection request
+ * response from the BOSH server. It is used to set up authentication
+ * handlers and start the authentication process.
+ *
+ * SASL authentication will be attempted if available, otherwise
+ * the code will fall back to legacy authentication.
+ *
+ * Parameters:
+ * (Strophe.Request) req - The current request.
+ */
+ _connect_cb: function (req)
+ {
+ Strophe.info("_connect_cb was called");
+
+ this.connected = true;
+ var bodyWrap = req.getResponse();
+ if (!bodyWrap) { return; }
+
+ this.xmlInput(bodyWrap);
+ this.rawInput(Strophe.serialize(bodyWrap));
+
+ var typ = bodyWrap.getAttribute("type");
+ var cond, conflict;
+ if (typ !== null && typ == "terminate") {
+ // an error occurred
+ cond = bodyWrap.getAttribute("condition");
+ conflict = bodyWrap.getElementsByTagName("conflict");
+ if (cond !== null) {
+ if (cond == "remote-stream-error" && conflict.length > 0) {
+ cond = "conflict";
+ }
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
+ } else {
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+ }
+ return;
+ }
+
+ // check to make sure we don't overwrite these if _connect_cb is
+ // called multiple times in the case of missing stream:features
+ if (!this.sid) {
+ this.sid = bodyWrap.getAttribute("sid");
+ }
+ if (!this.stream_id) {
+ this.stream_id = bodyWrap.getAttribute("authid");
+ }
+ var wind = bodyWrap.getAttribute('requests');
+ if (wind) { this.window = parseInt(wind, 10); }
+ var hold = bodyWrap.getAttribute('hold');
+ if (hold) { this.hold = parseInt(hold, 10); }
+ var wait = bodyWrap.getAttribute('wait');
+ if (wait) { this.wait = parseInt(wait, 10); }
+
+
+ var do_sasl_plain = false;
+ var do_sasl_digest_md5 = false;
+ var do_sasl_anonymous = false;
+
+ var mechanisms = bodyWrap.getElementsByTagName("mechanism");
+ var i, mech, auth_str, hashed_auth_str;
+ if (mechanisms.length > 0) {
+ for (i = 0; i < mechanisms.length; i++) {
+ mech = Strophe.getText(mechanisms[i]);
+ if (mech == 'DIGEST-MD5') {
+ do_sasl_digest_md5 = true;
+ } else if (mech == 'PLAIN') {
+ do_sasl_plain = true;
+ } else if (mech == 'ANONYMOUS') {
+ do_sasl_anonymous = true;
+ }
+ }
+ } else {
+ // we didn't get stream:features yet, so we need wait for it
+ // by sending a blank poll request
+ var body = this._buildBody();
+ this._requests.push(
+ new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._connect_cb.bind(this)),
+ body.tree().getAttribute("rid")));
+ this._throttledRequestHandler();
+ return;
+ }
+
+ if (Strophe.getNodeFromJid(this.jid) === null &&
+ do_sasl_anonymous) {
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ this.send($build("auth", {
+ xmlns: Strophe.NS.SASL,
+ mechanism: "ANONYMOUS"
+ }).tree());
+ } else if (Strophe.getNodeFromJid(this.jid) === null) {
+ // we don't have a node, which is required for non-anonymous
+ // client connections
+ this._changeConnectStatus(Strophe.Status.CONNFAIL,
+ 'x-strophe-bad-non-anon-jid');
+ this.disconnect();
+ } else if (do_sasl_digest_md5) {
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._sasl_challenge_handler = this._addSysHandler(
+ this._sasl_challenge1_cb.bind(this), null,
+ "challenge", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ this.send($build("auth", {
+ xmlns: Strophe.NS.SASL,
+ mechanism: "DIGEST-MD5"
+ }).tree());
+ } else if (do_sasl_plain) {
+ // Build the plain auth string (barejid null
+ // username null password) and base 64 encoded.
+ auth_str = Strophe.getBareJidFromJid(this.jid);
+ auth_str = auth_str + "\u0000";
+ auth_str = auth_str + Strophe.getNodeFromJid(this.jid);
+ auth_str = auth_str + "\u0000";
+ auth_str = auth_str + this.pass;
+
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ hashed_auth_str = Base64.encode(auth_str);
+ this.send($build("auth", {
+ xmlns: Strophe.NS.SASL,
+ mechanism: "PLAIN"
+ }).t(hashed_auth_str).tree());
+ } else {
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._addSysHandler(this._auth1_cb.bind(this), null, null,
+ null, "_auth_1");
+
+ this.send($iq({
+ type: "get",
+ to: this.domain,
+ id: "_auth_1"
+ }).c("query", {
+ xmlns: Strophe.NS.AUTH
+ }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree());
+ }
+ },
+
+ /** PrivateFunction: _sasl_challenge1_cb
+ * _Private_ handler for DIGEST-MD5 SASL authentication.
+ *
+ * Parameters:
+ * (XMLElement) elem - The challenge stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_challenge1_cb: function (elem)
+ {
+ var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;
+
+ var challenge = Base64.decode(Strophe.getText(elem));
+ var cnonce = MD5.hexdigest(Math.random() * 1234567890);
+ var realm = "";
+ var host = null;
+ var nonce = "";
+ var qop = "";
+ var matches;
+
+ // remove unneeded handlers
+ this.deleteHandler(this._sasl_failure_handler);
+
+ while (challenge.match(attribMatch)) {
+ matches = challenge.match(attribMatch);
+ challenge = challenge.replace(matches[0], "");
+ matches[2] = matches[2].replace(/^"(.+)"$/, "$1");
+ switch (matches[1]) {
+ case "realm":
+ realm = matches[2];
+ break;
+ case "nonce":
+ nonce = matches[2];
+ break;
+ case "qop":
+ qop = matches[2];
+ break;
+ case "host":
+ host = matches[2];
+ break;
+ }
+ }
+
+ var digest_uri = "xmpp/" + this.domain;
+ if (host !== null) {
+ digest_uri = digest_uri + "/" + host;
+ }
+
+ var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) +
+ ":" + realm + ":" + this.pass) +
+ ":" + nonce + ":" + cnonce;
+ var A2 = 'AUTHENTICATE:' + digest_uri;
+
+ var responseText = "";
+ responseText += 'username=' +
+ this._quote(Strophe.getNodeFromJid(this.jid)) + ',';
+ responseText += 'realm=' + this._quote(realm) + ',';
+ responseText += 'nonce=' + this._quote(nonce) + ',';
+ responseText += 'cnonce=' + this._quote(cnonce) + ',';
+ responseText += 'nc="00000001",';
+ responseText += 'qop="auth",';
+ responseText += 'digest-uri=' + this._quote(digest_uri) + ',';
+ responseText += 'response=' + this._quote(
+ MD5.hexdigest(MD5.hexdigest(A1) + ":" +
+ nonce + ":00000001:" +
+ cnonce + ":auth:" +
+ MD5.hexdigest(A2))) + ',';
+ responseText += 'charset="utf-8"';
+
+ this._sasl_challenge_handler = this._addSysHandler(
+ this._sasl_challenge2_cb.bind(this), null,
+ "challenge", null, null);
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ this.send($build('response', {
+ xmlns: Strophe.NS.SASL
+ }).t(Base64.encode(responseText)).tree());
+
+ return false;
+ },
+
+ /** PrivateFunction: _quote
+ * _Private_ utility function to backslash escape and quote strings.
+ *
+ * Parameters:
+ * (String) str - The string to be quoted.
+ *
+ * Returns:
+ * quoted string
+ */
+ _quote: function (str)
+ {
+ return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
+ //" end string workaround for emacs
+ },
+
+
+ /** PrivateFunction: _sasl_challenge2_cb
+ * _Private_ handler for second step of DIGEST-MD5 SASL authentication.
+ *
+ * Parameters:
+ * (XMLElement) elem - The challenge stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_challenge2_cb: function (elem)
+ {
+ // remove unneeded handlers
+ this.deleteHandler(this._sasl_success_handler);
+ this.deleteHandler(this._sasl_failure_handler);
+
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+ this.send($build('response', {xmlns: Strophe.NS.SASL}).tree());
+ return false;
+ },
+
+ /** PrivateFunction: _auth1_cb
+ * _Private_ handler for legacy authentication.
+ *
+ * This handler is called in response to the initial
+ * for legacy authentication. It builds an authentication and
+ * sends it, creating a handler (calling back to _auth2_cb()) to
+ * handle the result
+ *
+ * Parameters:
+ * (XMLElement) elem - The stanza that triggered the callback.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _auth1_cb: function (elem)
+ {
+ // build plaintext auth iq
+ var iq = $iq({type: "set", id: "_auth_2"})
+ .c('query', {xmlns: Strophe.NS.AUTH})
+ .c('username', {}).t(Strophe.getNodeFromJid(this.jid))
+ .up()
+ .c('password').t(this.pass);
+
+ if (!Strophe.getResourceFromJid(this.jid)) {
+ // since the user has not supplied a resource, we pick
+ // a default one here. unlike other auth methods, the server
+ // cannot do this for us.
+ this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
+ }
+ iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));
+
+ this._addSysHandler(this._auth2_cb.bind(this), null,
+ null, null, "_auth_2");
+
+ this.send(iq.tree());
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_success_cb
+ * _Private_ handler for succesful SASL authentication.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_success_cb: function (elem)
+ {
+ Strophe.info("SASL authentication succeeded.");
+
+ // remove old handlers
+ this.deleteHandler(this._sasl_failure_handler);
+ this._sasl_failure_handler = null;
+ if (this._sasl_challenge_handler) {
+ this.deleteHandler(this._sasl_challenge_handler);
+ this._sasl_challenge_handler = null;
+ }
+
+ this._addSysHandler(this._sasl_auth1_cb.bind(this), null,
+ "stream:features", null, null);
+
+ // we must send an xmpp:restart now
+ this._sendRestart();
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_auth1_cb
+ * _Private_ handler to start stream binding.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_auth1_cb: function (elem)
+ {
+ // save stream:features for future usage
+ this.features = elem;
+
+ var i, child;
+
+ for (i = 0; i < elem._childNodes.length; i++) {
+ child = elem._childNodes[i];
+ if (child.nodeName.toLowerCase() == 'bind') {
+ this.do_bind = true;
+ }
+
+ if (child.nodeName.toLowerCase() == 'session') {
+ this.do_session = true;
+ }
+ }
+
+ if (!this.do_bind) {
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ } else {
+ this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
+ null, "_bind_auth_2");
+
+ var resource = Strophe.getResourceFromJid(this.jid);
+ if (resource) {
+ this.send($iq({type: "set", id: "_bind_auth_2"})
+ .c('bind', {xmlns: Strophe.NS.BIND})
+ .c('resource', {}).t(resource).tree());
+ } else {
+ this.send($iq({type: "set", id: "_bind_auth_2"})
+ .c('bind', {xmlns: Strophe.NS.BIND})
+ .tree());
+ }
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_bind_cb
+ * _Private_ handler for binding result and session start.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_bind_cb: function (elem)
+ {
+ if (elem.getAttribute("type") == "error") {
+ Strophe.info("SASL binding failed.");
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ }
+
+ // TODO - need to grab errors
+ var bind = elem.getElementsByTagName("bind");
+ var jidNode;
+ if (bind.length > 0) {
+ // Grab jid
+ jidNode = bind[0].getElementsByTagName("jid");
+ if (jidNode.length > 0) {
+ this.jid = Strophe.getText(jidNode[0]);
+
+ if (this.do_session) {
+ this._addSysHandler(this._sasl_session_cb.bind(this),
+ null, null, null, "_session_auth_2");
+
+ this.send($iq({type: "set", id: "_session_auth_2"})
+ .c('session', {xmlns: Strophe.NS.SESSION})
+ .tree());
+ } else {
+ this.authenticated = true;
+ this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+ }
+ }
+ } else {
+ Strophe.info("SASL binding failed.");
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ }
+ },
+
+ /** PrivateFunction: _sasl_session_cb
+ * _Private_ handler to finish successful SASL connection.
+ *
+ * This sets Connection.authenticated to true on success, which
+ * starts the processing of user handlers.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_session_cb: function (elem)
+ {
+ if (elem.getAttribute("type") == "result") {
+ this.authenticated = true;
+ this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+ } else if (elem.getAttribute("type") == "error") {
+ Strophe.info("Session creation failed.");
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_failure_cb
+ * _Private_ handler for SASL authentication failure.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_failure_cb: function (elem)
+ {
+ // delete unneeded handlers
+ if (this._sasl_success_handler) {
+ this.deleteHandler(this._sasl_success_handler);
+ this._sasl_success_handler = null;
+ }
+ if (this._sasl_challenge_handler) {
+ this.deleteHandler(this._sasl_challenge_handler);
+ this._sasl_challenge_handler = null;
+ }
+
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ },
+
+ /** PrivateFunction: _auth2_cb
+ * _Private_ handler to finish legacy authentication.
+ *
+ * This handler is called when the result from the jabber:iq:auth
+ * stanza is returned.
+ *
+ * Parameters:
+ * (XMLElement) elem - The stanza that triggered the callback.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _auth2_cb: function (elem)
+ {
+ if (elem.getAttribute("type") == "result") {
+ this.authenticated = true;
+ this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+ } else if (elem.getAttribute("type") == "error") {
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ this.disconnect();
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: _addSysTimedHandler
+ * _Private_ function to add a system level timed handler.
+ *
+ * This function is used to add a Strophe.TimedHandler for the
+ * library code. System timed handlers are allowed to run before
+ * authentication is complete.
+ *
+ * Parameters:
+ * (Integer) period - The period of the handler.
+ * (Function) handler - The callback function.
+ */
+ _addSysTimedHandler: function (period, handler)
+ {
+ var thand = new Strophe.TimedHandler(period, handler);
+ thand.user = false;
+ this.addTimeds.push(thand);
+ return thand;
+ },
+
+ /** PrivateFunction: _addSysHandler
+ * _Private_ function to add a system level stanza handler.
+ *
+ * This function is used to add a Strophe.Handler for the
+ * library code. System stanza handlers are allowed to run before
+ * authentication is complete.
+ *
+ * Parameters:
+ * (Function) handler - The callback function.
+ * (String) ns - The namespace to match.
+ * (String) name - The stanza name to match.
+ * (String) type - The stanza type attribute to match.
+ * (String) id - The stanza id attribute to match.
+ */
+ _addSysHandler: function (handler, ns, name, type, id)
+ {
+ var hand = new Strophe.Handler(handler, ns, name, type, id);
+ hand.user = false;
+ this.addHandlers.push(hand);
+ return hand;
+ },
+
+ /** PrivateFunction: _onDisconnectTimeout
+ * _Private_ timeout handler for handling non-graceful disconnection.
+ *
+ * If the graceful disconnect process does not complete within the
+ * time allotted, this handler finishes the disconnect anyway.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _onDisconnectTimeout: function ()
+ {
+ Strophe.info("_onDisconnectTimeout was called");
+
+ // cancel all remaining requests and clear the queue
+ var req;
+ while (this._requests.length > 0) {
+ req = this._requests.pop();
+ req.abort = true;
+ req.xhr.abort();
+ // jslint complains, but this is fine. setting to empty func
+ // is necessary for IE6
+ req.xhr.onreadystatechange = function () {};
+ }
+
+ // actually disconnect
+ this._doDisconnect();
+
+ return false;
+ },
+
+ /** PrivateFunction: _onIdle
+ * _Private_ handler to process events during idle cycle.
+ *
+ * This handler is called every 100ms to fire timed handlers that
+ * are ready and keep poll requests going.
+ */
+ _onIdle: function ()
+ {
+ var i, thand, since, newList;
+
+ // add timed handlers scheduled for addition
+ // NOTE: we add before remove in the case a timed handler is
+ // added and then deleted before the next _onIdle() call.
+ while (this.addTimeds.length > 0) {
+ this.timedHandlers.push(this.addTimeds.pop());
+ }
+
+ // remove timed handlers that have been scheduled for deletion
+ while (this.removeTimeds.length > 0) {
+ thand = this.removeTimeds.pop();
+ i = this.timedHandlers.indexOf(thand);
+ if (i >= 0) {
+ this.timedHandlers.splice(i, 1);
+ }
+ }
+
+ // call ready timed handlers
+ var now = new Date().getTime();
+ newList = [];
+ for (i = 0; i < this.timedHandlers.length; i++) {
+ thand = this.timedHandlers[i];
+ if (this.authenticated || !thand.user) {
+ since = thand.lastCalled + thand.period;
+ if (since - now <= 0) {
+ if (thand.run()) {
+ newList.push(thand);
+ }
+ } else {
+ newList.push(thand);
+ }
+ }
+ }
+ this.timedHandlers = newList;
+
+ var body, time_elapsed;
+
+ // if no requests are in progress, poll
+ if (this.authenticated && this._requests.length === 0 &&
+ this._data.length === 0 && !this.disconnecting) {
+ Strophe.info("no requests during idle cycle, sending " +
+ "blank request");
+ this._data.push(null);
+ }
+
+ if (this._requests.length < 2 && this._data.length > 0 &&
+ !this.paused) {
+ body = this._buildBody();
+ for (i = 0; i < this._data.length; i++) {
+ if (this._data[i] !== null) {
+ if (this._data[i] === "restart") {
+ body.attrs({
+ to: this.domain,
+ "xml:lang": "en",
+ "xmpp:restart": "true",
+ "xmlns:xmpp": Strophe.NS.BOSH
+ });
+ } else {
+ body.cnode(this._data[i]).up();
+ }
+ }
+ }
+ delete this._data;
+ this._data = [];
+ this._requests.push(
+ new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._dataRecv.bind(this)),
+ body.tree().getAttribute("rid")));
+ this._processRequest(this._requests.length - 1);
+ }
+
+ if (this._requests.length > 0) {
+ time_elapsed = this._requests[0].age();
+ if (this._requests[0].dead !== null) {
+ if (this._requests[0].timeDead() >
+ Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) {
+ this._throttledRequestHandler();
+ }
+ }
+
+ if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) {
+ Strophe.warn("Request " +
+ this._requests[0].id +
+ " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) +
+ " seconds since last activity");
+ this._throttledRequestHandler();
+ }
+ }
+
+ // reactivate the timer
+ clearTimeout(this._idleTimeout);
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+ }
+};
+
+if (callback) {
+ callback(Strophe, $build, $msg, $iq, $pres);
+}
+
+})(function () {
+ window.Strophe = arguments[0];
+ window.$build = arguments[1];
+ window.$msg = arguments[2];
+ window.$iq = arguments[3];
+ window.$pres = arguments[4];
+});
diff --git a/contrib/jitsimeetbridge/unjingle/unjingle.js b/contrib/jitsimeetbridge/unjingle/unjingle.js
new file mode 100644
index 0000000000..3dfe759914
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/unjingle.js
@@ -0,0 +1,48 @@
+var strophe = require("./strophe/strophe.js").Strophe;
+
+var Strophe = strophe.Strophe;
+var $iq = strophe.$iq;
+var $msg = strophe.$msg;
+var $build = strophe.$build;
+var $pres = strophe.$pres;
+
+var jsdom = require("jsdom");
+var window = jsdom.jsdom().parentWindow;
+var $ = require('jquery')(window);
+
+var stropheJingle = require("./strophe.jingle.sdp.js");
+
+
+var input = '';
+
+process.stdin.on('readable', function() {
+ var chunk = process.stdin.read();
+ if (chunk !== null) {
+ input += chunk;
+ }
+});
+
+process.stdin.on('end', function() {
+ if (process.argv[2] == '--jingle') {
+ var elem = $(input);
+ // app does:
+ // sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
+ //console.log(elem.find('>content'));
+ var sdp = new stropheJingle.SDP('');
+ sdp.fromJingle(elem);
+ console.log(sdp.raw);
+ } else if (process.argv[2] == '--sdp') {
+ var sdp = new stropheJingle.SDP(input);
+ var accept = $iq({to: '%(tojid)s',
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ //action: 'session-accept',
+ action: '%(action)s',
+ initiator: '%(initiator)s',
+ responder: '%(responder)s',
+ sid: '%(sid)s' });
+ sdp.toJingle(accept, 'responder');
+ console.log(Strophe.serialize(accept));
+ }
+});
+
From 481f5a2cbe0dae73f96adf4b47a2a86b3d6973e9 Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 17:20:02 +0000
Subject: [PATCH 03/18] Remove editor junk
---
contrib/jitsimeetbridge/unjingle/.swp | Bin 12288 -> 0 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 contrib/jitsimeetbridge/unjingle/.swp
diff --git a/contrib/jitsimeetbridge/unjingle/.swp b/contrib/jitsimeetbridge/unjingle/.swp
deleted file mode 100644
index 449c08da561e6bb3b4429ae8e8817bf8c07463d1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 12288
zcmeHNOK;;g5O#X&t!V#%+M-bRpqP4D5@8!ioGhAl69g!-MSG(t%2uKWSEBMb^+)!{
zw8su5+iN*;)<}wM4>STxqBt|(d>now!3oj(vvbWmvm;A2OuNt0yqf5BndAG6bKDMNQ`ZO;Ns&+VAAW4l-soDACn6!DDCOw-
z0&bM7;zb^@=EnL0Zd6Gm=fdF)#GIE!&3A<)oL9J+0mv`{Tb7YrBoyEsUtOHMI|UE4
z$OYina+0x$PU2qSKxAqFBP_%M
zK!h;yBXtWQI35mJC5q}WV#B-|wi9Hmh*h*qxMcFlyi4!_oM$Xb$SSYtn3IRJAdw*+
zIMXDPrU{eU+zVb;x<=E2-jqZD8IF$-utpao&shp+Er7~N2}}uAF0Ha82QV4awOf+b
zU}d6)lFdkcagn`>d-m(mtP|H9ZN)XE$4!SVbmZ)GVOx8-fb81bFK6dh9Sqy4_e1bE
z?2n#dZ=%u4!|bACzh?p_a(6h`m&EllYysGh0MlA0AXv)yqR5%SZlxe>R?=fVcXf_&1tC*smUAHHrH1rpcp
z&%Zf$%s&6+kHG4)$Tsl>u@#1%Z5pv-Muvm@zz9*~86ow224^GRw=@^UBhPi2F$&y~
z;aJo+h;P{jVwUN9mQP&D`XhC7*(X?^pRSjE(F`|t@N`@X%8jhPm-@LOYSXbONe<#)
z3aySyGN0%y5`kj|UdnGO+tW3E2g2p1MHVgQ+`JWtq3eo?ArL$#ioO2CwS)JV;`FVK6bDj;1dTQpW>s_@o)nPkg|pg=gTg&Pm5`L
z)$6yIHg2y@uVwcAB`D@bhY3KXI~_)@M^hMa_$2+ZyDjI*Rgmp$qEnhdkFUe_Do#pS
zyU+HP6MFL^2lJws8zsv^>1C_Wt?
z;HCneEx}J&cLk@B?Y$-V;@$85a8#YKku|&g@Ke$!XdMYQ&45KC;u{Pejts|zFJhs<
z@IpTzw&@|td<{9SN05z-*kZPnU}}&kCPr)rPBd~yJ|W9ThHm>>!CpTt?GdaGahH*O
X(QH+rlHlh^@UVTOfjymkm=690r{`%W
From 7993e3d10dd5b33f6443b974b36b988130f1b156 Mon Sep 17 00:00:00 2001
From: Mark Haines
Date: Tue, 2 Dec 2014 17:20:02 +0000
Subject: [PATCH 04/18] SYN-187: Set a more sensible default for the
content_addr
---
synapse/config/server.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/synapse/config/server.py b/synapse/config/server.py
index 814a4c349b..f8a0844b8c 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -35,8 +35,11 @@ class ServerConfig(Config):
if not args.content_addr:
host = args.server_name
if ':' not in host:
- host = "%s:%d" % (host, args.bind_port)
- args.content_addr = "https://%s" % (host,)
+ host = "%s:%d" % (host, args.unsecure_port)
+ else:
+ host = host.split(':')[0]
+ host = "%s:%d" % (host, args.unsecure_port)
+ args.content_addr = "http://%s" % (host,)
self.content_addr = args.content_addr
From 23a2c42469ab7af46ffb44de2bfeae1240930a22 Mon Sep 17 00:00:00 2001
From: Erik Johnston
Date: Tue, 2 Dec 2014 17:45:40 +0000
Subject: [PATCH 05/18] Bump version
---
CHANGES.rst | 7 +++++++
VERSION | 2 +-
synapse/__init__.py | 2 +-
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 6779a36f72..477a404721 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,10 @@
+Changes in synapse 0.5.3c (2014-12-02)
+======================================
+
+ * Change the default value for the `content_addr` option to use the HTTP
+ listener, as by default the HTTPS listener will be using a self-signed
+ certificate.
+
Changes in synapse 0.5.3 (2014-11-27)
=====================================
diff --git a/VERSION b/VERSION
index dc74c5626d..b6f145ad19 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.5.3a
+0.5.3c
diff --git a/synapse/__init__.py b/synapse/__init__.py
index 658574dab9..0b43932305 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -16,4 +16,4 @@
""" This is a reference implementation of a synapse home server.
"""
-__version__ = "0.5.3a"
+__version__ = "0.5.3c"
From 0d149ae6e96ba8378ace9ebdb2dea41de8dcb11d Mon Sep 17 00:00:00 2001
From: Matthew Hodgson
Date: Fri, 28 Nov 2014 06:44:50 +0000
Subject: [PATCH 06/18] a simple matrix->verto bot, distantly related to the
irc bridge
---
contrib/vertobot/.gitignore | 1 +
contrib/vertobot/bot.pl | 288 ++++++++++++++++++++++++++++
contrib/vertobot/config.yaml | 33 ++++
contrib/vertobot/cpanfile | 17 ++
contrib/vertobot/verto-example.json | 207 ++++++++++++++++++++
5 files changed, 546 insertions(+)
create mode 100644 contrib/vertobot/.gitignore
create mode 100644 contrib/vertobot/bot.pl
create mode 100644 contrib/vertobot/config.yaml
create mode 100644 contrib/vertobot/cpanfile
create mode 100644 contrib/vertobot/verto-example.json
diff --git a/contrib/vertobot/.gitignore b/contrib/vertobot/.gitignore
new file mode 100644
index 0000000000..e501ebe177
--- /dev/null
+++ b/contrib/vertobot/.gitignore
@@ -0,0 +1 @@
+vuc.yaml
diff --git a/contrib/vertobot/bot.pl b/contrib/vertobot/bot.pl
new file mode 100644
index 0000000000..30cde41ce7
--- /dev/null
+++ b/contrib/vertobot/bot.pl
@@ -0,0 +1,288 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use 5.010; # //
+use IO::Socket::SSL qw(SSL_VERIFY_NONE);
+use IO::Async::Loop;
+use Net::Async::WebSocket::Client;
+use Net::Async::Matrix 0.11;
+use JSON;
+use YAML;
+use Data::UUID;
+use Getopt::Long;
+use Digest::SHA qw( hmac_sha1_base64 );
+use Data::Dumper;
+
+binmode STDOUT, ":encoding(UTF-8)";
+binmode STDERR, ":encoding(UTF-8)";
+
+my $loop = IO::Async::Loop->new;
+# Net::Async::HTTP + SSL + IO::Poll doesn't play well. See
+# https://rt.cpan.org/Ticket/Display.html?id=93107
+ref $loop eq "IO::Async::Loop::Poll" and
+ warn "Using SSL with IO::Poll causes known memory-leaks!!\n";
+
+GetOptions(
+ 'C|config=s' => \my $CONFIG,
+ 'eval-from=s' => \my $EVAL_FROM,
+) or exit 1;
+
+if( defined $EVAL_FROM ) {
+ # An emergency 'eval() this file' hack
+ $SIG{HUP} = sub {
+ my $code = do {
+ open my $fh, "<", $EVAL_FROM or warn( "Cannot read - $!" ), return;
+ local $/; <$fh>
+ };
+
+ eval $code or warn "Cannot eval() - $@";
+ };
+}
+
+defined $CONFIG or die "Must supply --config\n";
+
+my %CONFIG = %{ YAML::LoadFile( $CONFIG ) };
+
+my %MATRIX_CONFIG = %{ $CONFIG{matrix} };
+# No harm in always applying this
+$MATRIX_CONFIG{SSL_verify_mode} = SSL_VERIFY_NONE;
+
+# Track every Room object, so we can ->leave them all on shutdown
+my %bot_matrix_rooms;
+
+my $bridgestate = {};
+my $roomid_by_callid = {};
+
+my $bot_verto = Net::Async::WebSocket::Client->new(
+ on_frame => sub {
+ my ( $self, $frame ) = @_;
+ warn "[Verto] receiving $frame";
+ on_verto_json($frame);
+ },
+);
+$loop->add( $bot_verto );
+
+my $bot_matrix = Net::Async::Matrix->new(
+ %MATRIX_CONFIG,
+ on_log => sub { warn "log: @_\n" },
+ on_invite => sub {
+ my ($matrix, $invite) = @_;
+ warn "[Matrix] invited to: " . $invite->{room_id} . " by " . $invite->{inviter} . "\n";
+
+ $matrix->join_room( $invite->{room_id} )->get;
+ },
+ on_room_new => sub {
+ my ($matrix, $room) = @_;
+
+ warn "[Matrix] have a room ID: " . $room->room_id . "\n";
+
+ $bot_matrix_rooms{$room->room_id} = $room;
+
+ # log in to verto on behalf of this room
+ my $sessid = lc new Data::UUID->create_str();
+ $bridgestate->{$room->room_id}->{sessid} = $sessid;
+
+ $room->configure(
+ on_message => \&on_room_message,
+ );
+
+ Future->wait_all(
+ send_verto_json_request("login", {
+ 'login' => $CONFIG{'verto-dialog-params'}{'login'},
+ 'passwd' => $CONFIG{'verto-config'}{'passwd'},
+ 'sessid' => $sessid,
+ }),
+ )->get;
+
+ # we deliberately don't paginate the room, as we only care about
+ # new calls
+ },
+ on_unknown_event => \&on_unknown_event,
+ on_error => sub {
+ print STDERR "Matrix failure: @_\n";
+ },
+);
+$loop->add( $bot_matrix );
+
+sub on_unknown_event
+{
+ my ($matrix, $event) = @_;
+ print Dumper($event);
+
+ my $room_id = $event->{room_id};
+ my %dp = %{$CONFIG{'verto-dialog-params'}};
+ $dp{callID} = $bridgestate->{$room_id}->{callid};
+
+ if ($event->{type} eq 'm.call.invite') {
+ $bridgestate->{$room_id}->{matrix_callid} = $event->{content}->{call_id};
+ $bridgestate->{$room_id}->{callid} = lc new Data::UUID->create_str();
+ $bridgestate->{$room_id}->{offer} = $event->{content}->{offer}->{sdp};
+ $bridgestate->{$room_id}->{gathered_candidates} = 0;
+ $roomid_by_callid->{ $bridgestate->{$room_id}->{callid} } = $room_id;
+ # no trickle ICE in verto apparently
+ }
+ elsif ($event->{type} eq 'm.call.candidates') {
+ # XXX: compare call IDs
+ if (!$bridgestate->{$room_id}->{gathered_candidates}) {
+ $bridgestate->{$room_id}->{gathered_candidates} = 1;
+ my $offer = $bridgestate->{$room_id}->{offer};
+ my $candidate_block = "";
+ foreach (@{$event->{content}->{candidates}}) {
+ $candidate_block .= "a=" . $_->{candidate} . "\r\n";
+ }
+ # XXX: collate using the right m= line - for now assume audio call
+ $offer =~ s/(a=rtcp.*[\r\n]+)/$1$candidate_block/;
+
+ send_verto_json_request("verto.invite", {
+ "sdp" => $offer,
+ "dialogParams" => \%dp,
+ "sessid" => $bridgestate->{$room_id}->{sessid},
+ })->get;
+ }
+ else {
+ # ignore them, as no trickle ICE, although we might as well
+ # batch them up
+ # foreach (@{$event->{content}->{candidates}}) {
+ # push @{$bridgestate->{$room_id}->{candidates}}, $_;
+ # }
+ }
+ }
+ elsif ($event->{type} eq 'm.call.hangup') {
+ send_verto_json_request("verto.bye", {
+ "dialogParams" => \%dp,
+ "sessid" => $bridgestate->{$room_id}->{sessid},
+ })->get;
+ }
+ else {
+ warn "Unhandled event: $event->{type}";
+ }
+}
+
+sub on_room_message
+{
+ my ($room, $from, $content) = @_;
+ my $room_id = $room->room_id;
+ warn "[Matrix] in $room_id: $from: " . $content->{body} . "\n";
+}
+
+my $verto_connecting = $loop->new_future;
+$bot_verto->connect(
+ %{ $CONFIG{"verto-bot"} },
+ on_connected => sub {
+ warn("[Verto] connected to websocket");
+ $verto_connecting->done($bot_verto) if not $verto_connecting->is_done;
+ },
+ on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
+ on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
+);
+
+Future->needs_all(
+ $bot_matrix->login( %{ $CONFIG{"matrix-bot"} } )->then( sub {
+ $bot_matrix->start;
+ }),
+
+ $verto_connecting,
+)->get;
+
+$loop->attach_signal(
+ PIPE => sub { warn "pipe\n" }
+);
+$loop->attach_signal(
+ INT => sub { $loop->stop },
+);
+$loop->attach_signal(
+ TERM => sub { $loop->stop },
+);
+
+eval {
+ $loop->run;
+} or my $e = $@;
+
+# When the bot gets shut down, have it leave the rooms so it's clear to observers
+# that it is no longer running.
+# if( $CONFIG{"leave-on-shutdown"} // 1 ) {
+# print STDERR "Removing bot from Matrix rooms...\n";
+# Future->wait_all( map { $_->leave->else_done() } values %bot_matrix_rooms )->get;
+# }
+# else {
+# print STDERR "Leaving bot users in Matrix rooms.\n";
+# }
+
+die $e if $e;
+
+exit 0;
+
+{
+ my $json_id;
+ my $requests;
+
+ sub send_verto_json_request
+ {
+ $json_id ||= 1;
+
+ my ($method, $params) = @_;
+ my $json = {
+ jsonrpc => "2.0",
+ method => $method,
+ params => $params,
+ id => $json_id,
+ };
+ my $text = JSON->new->encode( $json );
+ warn "[Verto] sending $text";
+ $bot_verto->send_frame ( $text );
+ my $request = $loop->new_future;
+ $requests->{$json_id} = $request;
+ $json_id++;
+ return $request;
+ }
+
+ sub send_verto_json_response
+ {
+ my ($result, $id) = @_;
+ my $json = {
+ jsonrpc => "2.0",
+ result => $result,
+ id => $id,
+ };
+ my $text = JSON->new->encode( $json );
+ warn "[Verto] sending $text";
+ $bot_verto->send_frame ( $text );
+ }
+
+ sub on_verto_json
+ {
+ my $json = JSON->new->decode( $_[0] );
+ if ($json->{method}) {
+ if ($json->{method} eq 'verto.answer') {
+
+ my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
+ my $room = $bot_matrix_rooms{$room_id};
+
+ # HACK HACK HACK HACK
+ $room->_do_POST_json( "/send/m.call.answer", {
+ call_id => $bridgestate->{$room_id}->{matrix_callid},
+ version => 0,
+ answer => {
+ sdp => $json->{params}->{sdp},
+ type => "answer",
+ },
+ })->then( sub {
+ send_verto_json_response( {
+ method => "verto.answer",
+ }, $json->{id});
+ })->get;
+ }
+ else {
+ warn ("[Verto] unhandled method: " . $json->{method});
+ }
+ }
+ elsif ($json->{result}) {
+ $requests->{$json->{id}}->done($json->{result});
+ }
+ elsif ($json->{error}) {
+ $requests->{$json->{id}}->fail($json->{error}->{message}, $json->{error});
+ }
+ }
+}
+
diff --git a/contrib/vertobot/config.yaml b/contrib/vertobot/config.yaml
new file mode 100644
index 0000000000..b251e405f3
--- /dev/null
+++ b/contrib/vertobot/config.yaml
@@ -0,0 +1,33 @@
+# Generic Matrix connection params
+matrix:
+ server: 'matrix.org'
+ SSL: 1
+
+# Bot-user connection details
+matrix-bot:
+ user_id: '@vertobot:matrix.org'
+ password: ''
+
+# Extra registration params required for creating new "ghost" users
+matrix-register:
+ captcha_bypass_secret: ''
+
+verto-bot:
+ host: webrtc.freeswitch.org
+ service: 8081
+ url: "ws://webrtc.freeswitch.org:8081/"
+
+verto-config:
+ passwd: 1234
+
+verto-dialog-params:
+ useVideo: false
+ useStereo: false
+ tag: "webcam"
+ login: "1008@webrtc.freeswitch.org"
+ destination_number: "9664"
+ caller_id_name: "FreeSWITCH User"
+ caller_id_number: "1008"
+ callID: ""
+ remote_caller_id_name: "Outbound Call"
+ remote_caller_id_number: "9664"
diff --git a/contrib/vertobot/cpanfile b/contrib/vertobot/cpanfile
new file mode 100644
index 0000000000..239258d4ac
--- /dev/null
+++ b/contrib/vertobot/cpanfile
@@ -0,0 +1,17 @@
+requires 'parent', 0;
+requires 'Future', '>= 0.29';
+requires 'Net::Async::Matrix', '>= 0.11';
+requires 'Net::Async::Matrix::Utils';
+requires 'Net::Async::WebSocket::Protocol', 0;
+requires 'Data::UUID', 0;
+requires 'IO::Async', '>= 0.63';
+requires 'IO::Async::SSL', 0;
+requires 'IO::Socket::SSL', 0;
+requires 'YAML', 0;
+requires 'JSON', 0;
+requires 'Getopt::Long', 0;
+
+on 'test' => sub {
+ requires 'Test::More', '>= 0.98';
+};
+
diff --git a/contrib/vertobot/verto-example.json b/contrib/vertobot/verto-example.json
new file mode 100644
index 0000000000..e0230498a6
--- /dev/null
+++ b/contrib/vertobot/verto-example.json
@@ -0,0 +1,207 @@
+# JSON is shown in *reverse* chronological order.
+# Send v. Receive is implicit.
+
+{
+ "jsonrpc": "2.0",
+ "id": 7,
+ "result": {
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "message": "CALL ENDED",
+ "causeCode": 16,
+ "cause": "NORMAL_CLEARING",
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "verto.bye",
+ "params": {
+ "dialogParams": {
+ "useVideo": false,
+ "useStereo": true,
+ "tag": "webcam",
+ "login": "1008@webrtc.freeswitch.org",
+ "destination_number": "9664",
+ "caller_id_name": "FreeSWITCH User",
+ "caller_id_number": "1008",
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "remote_caller_id_name": "Outbound Call",
+ "remote_caller_id_number": "9664"
+ },
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 7
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 6,
+ "result": {
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "action": "toggleHold",
+ "holdState": "active",
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "verto.modify",
+ "params": {
+ "action": "toggleHold",
+ "dialogParams": {
+ "useVideo": false,
+ "useStereo": true,
+ "tag": "webcam",
+ "login": "1008@webrtc.freeswitch.org",
+ "destination_number": "9664",
+ "caller_id_name": "FreeSWITCH User",
+ "caller_id_number": "1008",
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "remote_caller_id_name": "Outbound Call",
+ "remote_caller_id_number": "9664"
+ },
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 6
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 5,
+ "result": {
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "action": "toggleHold",
+ "holdState": "held",
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "verto.modify",
+ "params": {
+ "action": "toggleHold",
+ "dialogParams": {
+ "useVideo": false,
+ "useStereo": true,
+ "tag": "webcam",
+ "login": "1008@webrtc.freeswitch.org",
+ "destination_number": "9664",
+ "caller_id_name": "FreeSWITCH User",
+ "caller_id_number": "1008",
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "remote_caller_id_name": "Outbound Call",
+ "remote_caller_id_number": "9664"
+ },
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 5
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 349819,
+ "result": {
+ "method": "verto.answer"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 349819,
+ "method": "verto.answer",
+ "params": {
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "sdp": "v=0\no=FreeSWITCH 1417101432 1417101433 IN IP4 209.105.235.10\ns=FreeSWITCH\nc=IN IP4 209.105.235.10\nt=0 0\na=msid-semantic: WMS jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vq\nm=audio 30134 RTP/SAVPF 111 126\na=rtpmap:111 opus/48000/2\na=fmtp:111 minptime=10; stereo=1\na=rtpmap:126 telephone-event/8000\na=silenceSupp:off - - - -\na=ptime:20\na=sendrecv\na=fingerprint:sha-256 F8:72:18:E9:72:89:99:22:5B:F8:B6:C6:C6:0D:C5:9B:B2:FB:BC:CA:8D:AB:13:8A:66:E1:37:38:A0:16:AA:41\na=rtcp-mux\na=rtcp:30134 IN IP4 209.105.235.10\na=ssrc:210967934 cname:rOIEajpw4FocakWY\na=ssrc:210967934 msid:jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vq a0\na=ssrc:210967934 mslabel:jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vq\na=ssrc:210967934 label:jA3rmwLVwUq1iE6TYEYHeLk2YTUlh1Vqa0\na=ice-ufrag:OKwTmGLapwmxn7OF\na=ice-pwd:MmaMwq8rVmtWxfLbQ7U2Ew3T\na=candidate:2372654928 1 udp 659136 209.105.235.10 30134 typ host generation 0\n"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 4,
+ "result": {
+ "message": "CALL CREATED",
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "verto.invite",
+ "params": {
+ "sdp": "v=0\r\no=- 1381685806032722557 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE audio\r\na=msid-semantic: WMS 6OOMyGAyJakjwaOOBtV7WcBCCuIW6PpuXsNg\r\nm=audio 63088 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 81.138.8.249\r\na=rtcp:63088 IN IP4 81.138.8.249\r\na=candidate:460398169 1 udp 2122260223 10.10.79.10 49945 typ host generation 0\r\na=candidate:460398169 2 udp 2122260223 10.10.79.10 49945 typ host generation 0\r\na=candidate:3460887983 1 udp 2122194687 192.168.1.64 63088 typ host generation 0\r\na=candidate:3460887983 2 udp 2122194687 192.168.1.64 63088 typ host generation 0\r\na=candidate:945327227 1 udp 1685987071 81.138.8.249 63088 typ srflx raddr 192.168.1.64 rport 63088 generation 0\r\na=candidate:945327227 2 udp 1685987071 81.138.8.249 63088 typ srflx raddr 192.168.1.64 rport 63088 generation 0\r\na=candidate:1441981097 1 tcp 1518280447 10.10.79.10 0 typ host tcptype active generation 0\r\na=candidate:1441981097 2 tcp 1518280447 10.10.79.10 0 typ host tcptype active generation 0\r\na=candidate:2160789855 1 tcp 1518214911 192.168.1.64 0 typ host tcptype active generation 0\r\na=candidate:2160789855 2 tcp 1518214911 192.168.1.64 0 typ host tcptype active generation 0\r\na=ice-ufrag:cP4qeRhn0LpcpA88\r\na=ice-pwd:fREmgSkXsDLGUUH1bwfrBQhW\r\na=ice-options:google-ice\r\na=fingerprint:sha-256 AF:35:64:1B:62:8A:EF:27:AE:2B:88:2E:FE:78:29:0B:08:DA:64:6C:DE:02:57:E3:EE:B1:D7:86:B8:36:8F:B0\r\na=setup:actpass\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10; stereo=1\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\na=ssrc:558827154 cname:vdKHBNqa17t2gmE3\r\na=ssrc:558827154 msid:6OOMyGAyJakjwaOOBtV7WcBCCuIW6PpuXsNg bf1303fb-9833-4d7d-b9e4-b32cfe04acc3\r\na=ssrc:558827154 mslabel:6OOMyGAyJakjwaOOBtV7WcBCCuIW6PpuXsNg\r\na=ssrc:558827154 label:bf1303fb-9833-4d7d-b9e4-b32cfe04acc3\r\n",
+ "dialogParams": {
+ "useVideo": false,
+ "useStereo": true,
+ "tag": "webcam",
+ "login": "1008@webrtc.freeswitch.org",
+ "destination_number": "9664",
+ "caller_id_name": "FreeSWITCH User",
+ "caller_id_number": "1008",
+ "callID": "12795aa6-2a8d-84ee-ce63-2e82ffe825ef",
+ "remote_caller_id_name": "Outbound Call",
+ "remote_caller_id_number": "9664"
+ },
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 4
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 3,
+ "result": {
+ "message": "logged in",
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "error": {
+ "code": -32000,
+ "message": "Authentication Required"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "login",
+ "params": {
+ "login": "1008@webrtc.freeswitch.org",
+ "passwd": "1234",
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 3
+}
+
+{
+ "jsonrpc": "2.0",
+ "id": 2,
+ "error": {
+ "code": -32000,
+ "message": "Authentication Required"
+ }
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "login",
+ "params": {
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 1
+}
+
+{
+ "jsonrpc": "2.0",
+ "method": "login",
+ "params": {
+ "sessid": "03a11060-3e14-23b6-c620-51b892c52983"
+ },
+ "id": 2
+}
From 72c1cc743ef42bfd7e1760b40fc1fc8ac798a930 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson
Date: Fri, 28 Nov 2014 06:55:05 +0000
Subject: [PATCH 07/18] clean up a vertobot a bit
---
contrib/vertobot/bot.pl | 1 -
contrib/vertobot/config.yaml | 4 ----
2 files changed, 5 deletions(-)
diff --git a/contrib/vertobot/bot.pl b/contrib/vertobot/bot.pl
index 30cde41ce7..0c693cdc1d 100644
--- a/contrib/vertobot/bot.pl
+++ b/contrib/vertobot/bot.pl
@@ -11,7 +11,6 @@ use JSON;
use YAML;
use Data::UUID;
use Getopt::Long;
-use Digest::SHA qw( hmac_sha1_base64 );
use Data::Dumper;
binmode STDOUT, ":encoding(UTF-8)";
diff --git a/contrib/vertobot/config.yaml b/contrib/vertobot/config.yaml
index b251e405f3..04403670a9 100644
--- a/contrib/vertobot/config.yaml
+++ b/contrib/vertobot/config.yaml
@@ -8,10 +8,6 @@ matrix-bot:
user_id: '@vertobot:matrix.org'
password: ''
-# Extra registration params required for creating new "ghost" users
-matrix-register:
- captcha_bypass_secret: ''
-
verto-bot:
host: webrtc.freeswitch.org
service: 8081
From 9f4abd59f53b84e18659928a7abf566995544597 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson
Date: Fri, 28 Nov 2014 14:43:28 +0000
Subject: [PATCH 08/18] ignore rogue matrix call events; support verto.media
RPC as a horrible hack; fix NA::Matrix dep
---
contrib/vertobot/.gitignore | 3 ++-
contrib/vertobot/bot.pl | 49 +++++++++++++++++++++++--------------
contrib/vertobot/cpanfile | 2 +-
3 files changed, 33 insertions(+), 21 deletions(-)
mode change 100644 => 100755 contrib/vertobot/bot.pl
diff --git a/contrib/vertobot/.gitignore b/contrib/vertobot/.gitignore
index e501ebe177..071a780574 100644
--- a/contrib/vertobot/.gitignore
+++ b/contrib/vertobot/.gitignore
@@ -1 +1,2 @@
-vuc.yaml
+vucbot.yaml
+vertobot.yaml
diff --git a/contrib/vertobot/bot.pl b/contrib/vertobot/bot.pl
old mode 100644
new mode 100755
index 0c693cdc1d..0852905081
--- a/contrib/vertobot/bot.pl
+++ b/contrib/vertobot/bot.pl
@@ -6,7 +6,7 @@ use 5.010; # //
use IO::Socket::SSL qw(SSL_VERIFY_NONE);
use IO::Async::Loop;
use Net::Async::WebSocket::Client;
-use Net::Async::Matrix 0.11;
+use Net::Async::Matrix 0.11_002;
use JSON;
use YAML;
use Data::UUID;
@@ -148,10 +148,15 @@ sub on_unknown_event
}
}
elsif ($event->{type} eq 'm.call.hangup') {
- send_verto_json_request("verto.bye", {
- "dialogParams" => \%dp,
- "sessid" => $bridgestate->{$room_id}->{sessid},
- })->get;
+ if ($bridgestate->{$room_id}->{matrix_callid} eq $event->{content}->{call_id}) {
+ send_verto_json_request("verto.bye", {
+ "dialogParams" => \%dp,
+ "sessid" => $bridgestate->{$room_id}->{sessid},
+ })->get;
+ }
+ else {
+ warn "Ignoring unrecognised callid: ".$event->{content}->{call_id};
+ }
}
else {
warn "Unhandled event: $event->{type}";
@@ -253,27 +258,33 @@ exit 0;
{
my $json = JSON->new->decode( $_[0] );
if ($json->{method}) {
- if ($json->{method} eq 'verto.answer') {
+ if (($json->{method} eq 'verto.answer' && $json->{params}->{sdp}) ||
+ $json->{method} eq 'verto.media') {
my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
my $room = $bot_matrix_rooms{$room_id};
- # HACK HACK HACK HACK
- $room->_do_POST_json( "/send/m.call.answer", {
- call_id => $bridgestate->{$room_id}->{matrix_callid},
- version => 0,
- answer => {
- sdp => $json->{params}->{sdp},
- type => "answer",
- },
- })->then( sub {
- send_verto_json_response( {
- method => "verto.answer",
- }, $json->{id});
- })->get;
+ if ($json->{params}->{sdp}) {
+ # HACK HACK HACK HACK
+ $room->_do_POST_json( "/send/m.call.answer", {
+ call_id => $bridgestate->{$room_id}->{matrix_callid},
+ version => 0,
+ answer => {
+ sdp => $json->{params}->{sdp},
+ type => "answer",
+ },
+ })->then( sub {
+ send_verto_json_response( {
+ method => $json->{method},
+ }, $json->{id});
+ })->get;
+ }
}
else {
warn ("[Verto] unhandled method: " . $json->{method});
+ send_verto_json_response( {
+ method => $json->{method},
+ }, $json->{id});
}
}
elsif ($json->{result}) {
diff --git a/contrib/vertobot/cpanfile b/contrib/vertobot/cpanfile
index 239258d4ac..c29fcaa6f6 100644
--- a/contrib/vertobot/cpanfile
+++ b/contrib/vertobot/cpanfile
@@ -1,6 +1,6 @@
requires 'parent', 0;
requires 'Future', '>= 0.29';
-requires 'Net::Async::Matrix', '>= 0.11';
+requires 'Net::Async::Matrix', '>= 0.11_002';
requires 'Net::Async::Matrix::Utils';
requires 'Net::Async::WebSocket::Protocol', 0;
requires 'Data::UUID', 0;
From 68cdcbdcabfe13d535706c31ca99176b10822d14 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson
Date: Fri, 28 Nov 2014 16:16:41 +0000
Subject: [PATCH 09/18] fix IO::Async abuse - thanks leo
---
contrib/vertobot/bot.pl | 20 +++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/contrib/vertobot/bot.pl b/contrib/vertobot/bot.pl
index 0852905081..828fc48786 100755
--- a/contrib/vertobot/bot.pl
+++ b/contrib/vertobot/bot.pl
@@ -62,6 +62,8 @@ my $bot_verto = Net::Async::WebSocket::Client->new(
);
$loop->add( $bot_verto );
+my $sessid = lc new Data::UUID->create_str();
+
my $bot_matrix = Net::Async::Matrix->new(
%MATRIX_CONFIG,
on_log => sub { warn "log: @_\n" },
@@ -79,20 +81,18 @@ my $bot_matrix = Net::Async::Matrix->new(
$bot_matrix_rooms{$room->room_id} = $room;
# log in to verto on behalf of this room
- my $sessid = lc new Data::UUID->create_str();
$bridgestate->{$room->room_id}->{sessid} = $sessid;
$room->configure(
on_message => \&on_room_message,
);
- Future->wait_all(
- send_verto_json_request("login", {
+ my $f = send_verto_json_request("login", {
'login' => $CONFIG{'verto-dialog-params'}{'login'},
'passwd' => $CONFIG{'verto-config'}{'passwd'},
'sessid' => $sessid,
- }),
- )->get;
+ });
+ $matrix->adopt_future($f);
# we deliberately don't paginate the room, as we only care about
# new calls
@@ -133,11 +133,12 @@ sub on_unknown_event
# XXX: collate using the right m= line - for now assume audio call
$offer =~ s/(a=rtcp.*[\r\n]+)/$1$candidate_block/;
- send_verto_json_request("verto.invite", {
+ my $f = send_verto_json_request("verto.invite", {
"sdp" => $offer,
"dialogParams" => \%dp,
"sessid" => $bridgestate->{$room_id}->{sessid},
- })->get;
+ });
+ $matrix->adopt_future($f);
}
else {
# ignore them, as no trickle ICE, although we might as well
@@ -149,10 +150,11 @@ sub on_unknown_event
}
elsif ($event->{type} eq 'm.call.hangup') {
if ($bridgestate->{$room_id}->{matrix_callid} eq $event->{content}->{call_id}) {
- send_verto_json_request("verto.bye", {
+ my $f = send_verto_json_request("verto.bye", {
"dialogParams" => \%dp,
"sessid" => $bridgestate->{$room_id}->{sessid},
- })->get;
+ });
+ $matrix->adopt_future($f);
}
else {
warn "Ignoring unrecognised callid: ".$event->{content}->{call_id};
From 293e89e2daca83673678e5b1635350c2dd3fd889 Mon Sep 17 00:00:00 2001
From: Matthew Hodgson
Date: Sun, 30 Nov 2014 23:21:47 +0000
Subject: [PATCH 10/18] re-add OSX troubleshooting instructions (not sure how
they got lost)
---
README.rst | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.rst b/README.rst
index 0b2b00412d..8459bcac2e 100644
--- a/README.rst
+++ b/README.rst
@@ -130,6 +130,9 @@ failing, e.g.::
$ pip install --user twisted
+On OSX, if you encounter clang: error: unknown argument: '-mno-fused-madd' you
+will need to export CFLAGS=-Qunused-arguments.
+
Running Your Homeserver
=======================
From 48f90036d8b011be3a3ea327ca2619aeb97b72b1 Mon Sep 17 00:00:00 2001
From: David Baker
Date: Tue, 2 Dec 2014 12:01:30 +0000
Subject: [PATCH 11/18] Add non-working jitsi meet bridge
---
contrib/jitsimeetbridge/jitsimeetbridge.py | 260 ++
.../syweb-jitsi-conference.patch | 188 +
contrib/jitsimeetbridge/unjingle/.swp | Bin 0 -> 12288 bytes
.../unjingle/strophe.jingle.sdp.js | 712 ++++
.../unjingle/strophe.jingle.sdp.util.js | 408 +++
.../unjingle/strophe/XMLHttpRequest.js | 254 ++
.../unjingle/strophe/base64.js | 83 +
.../jitsimeetbridge/unjingle/strophe/md5.js | 279 ++
.../unjingle/strophe/strophe.js | 3256 +++++++++++++++++
contrib/jitsimeetbridge/unjingle/unjingle.js | 48 +
10 files changed, 5488 insertions(+)
create mode 100644 contrib/jitsimeetbridge/jitsimeetbridge.py
create mode 100644 contrib/jitsimeetbridge/syweb-jitsi-conference.patch
create mode 100644 contrib/jitsimeetbridge/unjingle/.swp
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/base64.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/md5.js
create mode 100644 contrib/jitsimeetbridge/unjingle/strophe/strophe.js
create mode 100644 contrib/jitsimeetbridge/unjingle/unjingle.js
diff --git a/contrib/jitsimeetbridge/jitsimeetbridge.py b/contrib/jitsimeetbridge/jitsimeetbridge.py
new file mode 100644
index 0000000000..dbc6f6ffa5
--- /dev/null
+++ b/contrib/jitsimeetbridge/jitsimeetbridge.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+"""
+This is an attempt at bridging matrix clients into a Jitis meet room via Matrix
+video call. It uses hard-coded xml strings overg XMPP BOSH. It can display one
+of the streams from the Jitsi bridge until the second lot of SDP comes down and
+we set the remote SDP at which point the stream ends. Our video never gets to
+the bridge.
+
+Requires:
+npm install jquery jsdom
+"""
+
+import gevent
+import grequests
+from BeautifulSoup import BeautifulSoup
+import json
+import urllib
+import subprocess
+import time
+
+#ACCESS_TOKEN="" #
+
+MATRIXBASE = 'https://matrix.org/_matrix/client/api/v1/'
+MYUSERNAME = '@davetest:matrix.org'
+
+HTTPBIND = 'https://meet.jit.si/http-bind'
+#HTTPBIND = 'https://jitsi.vuc.me/http-bind'
+#ROOMNAME = "matrix"
+ROOMNAME = "pibble"
+
+HOST="guest.jit.si"
+#HOST="jitsi.vuc.me"
+
+TURNSERVER="turn.guest.jit.si"
+#TURNSERVER="turn.jitsi.vuc.me"
+
+ROOMDOMAIN="meet.jit.si"
+#ROOMDOMAIN="conference.jitsi.vuc.me"
+
+class TrivialMatrixClient:
+ def __init__(self, access_token):
+ self.token = None
+ self.access_token = access_token
+
+ def getEvent(self):
+ while True:
+ url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
+ if self.token:
+ url += "&from="+self.token
+ req = grequests.get(url)
+ resps = grequests.map([req])
+ obj = json.loads(resps[0].content)
+ print "incoming from matrix",obj
+ if 'end' not in obj:
+ continue
+ self.token = obj['end']
+ if len(obj['chunk']):
+ return obj['chunk'][0]
+
+ def joinRoom(self, roomId):
+ url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
+ print url
+ headers={ 'Content-Type': 'application/json' }
+ req = grequests.post(url, headers=headers, data='{}')
+ resps = grequests.map([req])
+ obj = json.loads(resps[0].content)
+ print "response: ",obj
+
+ def sendEvent(self, roomId, evType, event):
+ url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
+ print url
+ print json.dumps(event)
+ headers={ 'Content-Type': 'application/json' }
+ req = grequests.post(url, headers=headers, data=json.dumps(event))
+ resps = grequests.map([req])
+ obj = json.loads(resps[0].content)
+ print "response: ",obj
+
+
+
+xmppClients = {}
+
+
+def matrixLoop():
+ while True:
+ ev = matrixCli.getEvent()
+ print ev
+ if ev['type'] == 'm.room.member':
+ print 'membership event'
+ if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
+ roomId = ev['room_id']
+ print "joining room %s" % (roomId)
+ matrixCli.joinRoom(roomId)
+ elif ev['type'] == 'm.room.message':
+ if ev['room_id'] in xmppClients:
+ print "already have a bridge for that user, ignoring"
+ continue
+ print "got message, connecting"
+ xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
+ gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
+ elif ev['type'] == 'm.call.invite':
+ print "Incoming call"
+ #sdp = ev['content']['offer']['sdp']
+ #print "sdp: %s" % (sdp)
+ #xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
+ #gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
+ elif ev['type'] == 'm.call.answer':
+ print "Call answered"
+ sdp = ev['content']['answer']['sdp']
+ if ev['room_id'] not in xmppClients:
+ print "We didn't have a call for that room"
+ continue
+ # should probably check call ID too
+ xmppCli = xmppClients[ev['room_id']]
+ xmppCli.sendAnswer(sdp)
+ elif ev['type'] == 'm.call.hangup':
+ if ev['room_id'] in xmppClients:
+ xmppClients[ev['room_id']].stop()
+ del xmppClients[ev['room_id']]
+
+class TrivialXmppClient:
+ def __init__(self, matrixRoom, userId):
+ self.rid = 0
+ self.matrixRoom = matrixRoom
+ self.userId = userId
+ self.running = True
+
+ def stop(self):
+ self.running = False
+
+ def nextRid(self):
+ self.rid += 1
+ return '%d' % (self.rid)
+
+ def sendIq(self, xml):
+ fullXml = "%s" % (self.nextRid(), self.sid, xml)
+ #print "\t>>>%s" % (fullXml)
+ return self.xmppPoke(fullXml)
+
+ def xmppPoke(self, xml):
+ headers = {'Content-Type': 'application/xml'}
+ req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
+ resps = grequests.map([req])
+ obj = BeautifulSoup(resps[0].content)
+ return obj
+
+ def sendAnswer(self, answer):
+ print "sdp from matrix client",answer
+ p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ jingle, out_err = p.communicate(answer)
+ jingle = jingle % {
+ 'tojid': self.callfrom,
+ 'action': 'session-accept',
+ 'initiator': self.callfrom,
+ 'responder': self.jid,
+ 'sid': self.callsid
+ }
+ print "answer jingle from sdp",jingle
+ res = self.sendIq(jingle)
+ print "reply from answer: ",res
+
+ self.ssrcs = {}
+ jingleSoup = BeautifulSoup(jingle)
+ for cont in jingleSoup.iq.jingle.findAll('content'):
+ if cont.description:
+ self.ssrcs[cont['name']] = cont.description['ssrc']
+ print "my ssrcs:",self.ssrcs
+
+ gevent.joinall([
+ gevent.spawn(self.advertiseSsrcs)
+ ])
+
+ def advertiseSsrcs(self):
+ time.sleep(7)
+ print "SSRC spammer started"
+ while self.running:
+ ssrcMsg = "%(nick)s" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
+ res = self.sendIq(ssrcMsg)
+ print "reply from ssrc announce: ",res
+ time.sleep(10)
+
+
+
+ def xmppLoop(self):
+ self.matrixCallId = time.time()
+ res = self.xmppPoke("" % (self.nextRid(), HOST))
+
+ print res
+ self.sid = res.body['sid']
+ print "sid %s" % (self.sid)
+
+ res = self.sendIq("")
+
+ res = self.xmppPoke("" % (self.nextRid(), self.sid, HOST))
+
+ res = self.sendIq("")
+ print res
+
+ self.jid = res.body.iq.bind.jid.string
+ print "jid: %s" % (self.jid)
+ self.shortJid = self.jid.split('-')[0]
+
+ res = self.sendIq("")
+
+ #randomthing = res.body.iq['to']
+ #whatsitpart = randomthing.split('-')[0]
+
+ #print "other random bind thing: %s" % (randomthing)
+
+ # advertise preence to the jitsi room, with our nick
+ res = self.sendIq("%s" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
+ self.muc = {'users': []}
+ for p in res.body.findAll('presence'):
+ u = {}
+ u['shortJid'] = p['from'].split('/')[1]
+ if p.c and p.c.nick:
+ u['nick'] = p.c.nick.string
+ self.muc['users'].append(u)
+ print "muc: ",self.muc
+
+ # wait for stuff
+ while True:
+ print "waiting..."
+ res = self.sendIq("")
+ print "got from stream: ",res
+ if res.body.iq:
+ jingles = res.body.iq.findAll('jingle')
+ if len(jingles):
+ self.callfrom = res.body.iq['from']
+ self.handleInvite(jingles[0])
+ elif 'type' in res.body and res.body['type'] == 'terminate':
+ self.running = False
+ del xmppClients[self.matrixRoom]
+ return
+
+ def handleInvite(self, jingle):
+ self.initiator = jingle['initiator']
+ self.callsid = jingle['sid']
+ p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ print "raw jingle invite",str(jingle)
+ sdp, out_err = p.communicate(str(jingle))
+ print "transformed remote offer sdp",sdp
+ inviteEvent = {
+ 'offer': {
+ 'type': 'offer',
+ 'sdp': sdp
+ },
+ 'call_id': self.matrixCallId,
+ 'version': 0,
+ 'lifetime': 30000
+ }
+ matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
+
+matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
+
+gevent.joinall([
+ gevent.spawn(matrixLoop)
+])
+
diff --git a/contrib/jitsimeetbridge/syweb-jitsi-conference.patch b/contrib/jitsimeetbridge/syweb-jitsi-conference.patch
new file mode 100644
index 0000000000..aed23c78aa
--- /dev/null
+++ b/contrib/jitsimeetbridge/syweb-jitsi-conference.patch
@@ -0,0 +1,188 @@
+diff --git a/syweb/webclient/app/components/matrix/matrix-call.js b/syweb/webclient/app/components/matrix/matrix-call.js
+index 9fbfff0..dc68077 100644
+--- a/syweb/webclient/app/components/matrix/matrix-call.js
++++ b/syweb/webclient/app/components/matrix/matrix-call.js
+@@ -16,6 +16,45 @@ limitations under the License.
+
+ 'use strict';
+
++
++function sendKeyframe(pc) {
++ console.log('sendkeyframe', pc.iceConnectionState);
++ if (pc.iceConnectionState !== 'connected') return; // safe...
++ pc.setRemoteDescription(
++ pc.remoteDescription,
++ function () {
++ pc.createAnswer(
++ function (modifiedAnswer) {
++ pc.setLocalDescription(
++ modifiedAnswer,
++ function () {
++ // noop
++ },
++ function (error) {
++ console.log('triggerKeyframe setLocalDescription failed', error);
++ messageHandler.showError();
++ }
++ );
++ },
++ function (error) {
++ console.log('triggerKeyframe createAnswer failed', error);
++ messageHandler.showError();
++ }
++ );
++ },
++ function (error) {
++ console.log('triggerKeyframe setRemoteDescription failed', error);
++ messageHandler.showError();
++ }
++ );
++}
++
++
++
++
++
++
++
+ var forAllVideoTracksOnStream = function(s, f) {
+ var tracks = s.getVideoTracks();
+ for (var i = 0; i < tracks.length; i++) {
+@@ -83,7 +122,7 @@ angular.module('MatrixCall', [])
+ }
+
+ // FIXME: we should prevent any calls from being placed or accepted before this has finished
+- MatrixCall.getTurnServer();
++ //MatrixCall.getTurnServer();
+
+ MatrixCall.CALL_TIMEOUT = 60000;
+ MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
+@@ -132,6 +171,22 @@ angular.module('MatrixCall', [])
+ pc.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
+ pc.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
+ pc.onaddstream = function(s) { self.onAddStream(s); };
++
++ var datachan = pc.createDataChannel('RTCDataChannel', {
++ reliable: false
++ });
++ console.log("data chan: "+datachan);
++ datachan.onopen = function() {
++ console.log("data channel open");
++ };
++ datachan.onmessage = function() {
++ console.log("data channel message");
++ };
++ pc.ondatachannel = function(event) {
++ console.log("have data channel");
++ event.channel.binaryType = 'blob';
++ };
++
+ return pc;
+ }
+
+@@ -200,6 +255,12 @@ angular.module('MatrixCall', [])
+ }, this.msg.lifetime - event.age);
+ };
+
++ MatrixCall.prototype.receivedInvite = function(event) {
++ console.log("Got second invite for call "+this.call_id);
++ this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
++ };
++
++
+ // perverse as it may seem, sometimes we want to instantiate a call with a hangup message
+ // (because when getting the state of the room on load, events come in reverse order and
+ // we want to remember that a call has been hung up)
+@@ -349,7 +410,7 @@ angular.module('MatrixCall', [])
+ 'mandatory': {
+ 'OfferToReceiveAudio': true,
+ 'OfferToReceiveVideo': this.type == 'video'
+- },
++ }
+ };
+ this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
+ // This can't be in an apply() because it's called by a predecessor call under glare conditions :(
+@@ -359,8 +420,20 @@ angular.module('MatrixCall', [])
+ MatrixCall.prototype.gotLocalIceCandidate = function(event) {
+ if (event.candidate) {
+ console.log("Got local ICE "+event.candidate.sdpMid+" candidate: "+event.candidate.candidate);
+- this.sendCandidate(event.candidate);
+- }
++ //this.sendCandidate(event.candidate);
++ } else {
++ console.log("have all candidates, sending answer");
++ var content = {
++ version: 0,
++ call_id: this.call_id,
++ answer: this.peerConn.localDescription
++ };
++ this.sendEventWithRetry('m.call.answer', content);
++ var self = this;
++ $rootScope.$apply(function() {
++ self.state = 'connecting';
++ });
++ }
+ }
+
+ MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
+@@ -418,15 +491,6 @@ angular.module('MatrixCall', [])
+ console.log("Created answer: "+description);
+ var self = this;
+ this.peerConn.setLocalDescription(description, function() {
+- var content = {
+- version: 0,
+- call_id: self.call_id,
+- answer: self.peerConn.localDescription
+- };
+- self.sendEventWithRetry('m.call.answer', content);
+- $rootScope.$apply(function() {
+- self.state = 'connecting';
+- });
+ }, function() { console.log("Error setting local description!"); } );
+ };
+
+@@ -448,6 +512,9 @@ angular.module('MatrixCall', [])
+ $rootScope.$apply(function() {
+ self.state = 'connected';
+ self.didConnect = true;
++ /*$timeout(function() {
++ sendKeyframe(self.peerConn);
++ }, 1000);*/
+ });
+ } else if (this.peerConn.iceConnectionState == 'failed') {
+ this.hangup('ice_failed');
+@@ -518,6 +585,7 @@ angular.module('MatrixCall', [])
+
+ MatrixCall.prototype.onRemoteStreamEnded = function(event) {
+ console.log("Remote stream ended");
++ return;
+ var self = this;
+ $rootScope.$apply(function() {
+ self.state = 'ended';
+diff --git a/syweb/webclient/app/components/matrix/matrix-phone-service.js b/syweb/webclient/app/components/matrix/matrix-phone-service.js
+index 55dbbf5..272fa27 100644
+--- a/syweb/webclient/app/components/matrix/matrix-phone-service.js
++++ b/syweb/webclient/app/components/matrix/matrix-phone-service.js
+@@ -48,6 +48,13 @@ angular.module('matrixPhoneService', [])
+ return;
+ }
+
++ // do we already have an entry for this call ID?
++ var existingEntry = matrixPhoneService.allCalls[msg.call_id];
++ if (existingEntry) {
++ existingEntry.receivedInvite(msg);
++ return;
++ }
++
+ var call = undefined;
+ if (!isLive) {
+ // if this event wasn't live then this call may already be over
+@@ -108,7 +115,7 @@ angular.module('matrixPhoneService', [])
+ call.hangup();
+ }
+ } else {
+- $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
++ $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
+ }
+ } else if (event.type == 'm.call.answer') {
+ var call = matrixPhoneService.allCalls[msg.call_id];
diff --git a/contrib/jitsimeetbridge/unjingle/.swp b/contrib/jitsimeetbridge/unjingle/.swp
new file mode 100644
index 0000000000000000000000000000000000000000..449c08da561e6bb3b4429ae8e8817bf8c07463d1
GIT binary patch
literal 12288
zcmeHNOK;;g5O#X&t!V#%+M-bRpqP4D5@8!ioGhAl69g!-MSG(t%2uKWSEBMb^+)!{
zw8su5+iN*;)<}wM4>STxqBt|(d>now!3oj(vvbWmvm;A2OuNt0yqf5BndAG6bKDMNQ`ZO;Ns&+VAAW4l-soDACn6!DDCOw-
z0&bM7;zb^@=EnL0Zd6Gm=fdF)#GIE!&3A<)oL9J+0mv`{Tb7YrBoyEsUtOHMI|UE4
z$OYina+0x$PU2qSKxAqFBP_%M
zK!h;yBXtWQI35mJC5q}WV#B-|wi9Hmh*h*qxMcFlyi4!_oM$Xb$SSYtn3IRJAdw*+
zIMXDPrU{eU+zVb;x<=E2-jqZD8IF$-utpao&shp+Er7~N2}}uAF0Ha82QV4awOf+b
zU}d6)lFdkcagn`>d-m(mtP|H9ZN)XE$4!SVbmZ)GVOx8-fb81bFK6dh9Sqy4_e1bE
z?2n#dZ=%u4!|bACzh?p_a(6h`m&EllYysGh0MlA0AXv)yqR5%SZlxe>R?=fVcXf_&1tC*smUAHHrH1rpcp
z&%Zf$%s&6+kHG4)$Tsl>u@#1%Z5pv-Muvm@zz9*~86ow224^GRw=@^UBhPi2F$&y~
z;aJo+h;P{jVwUN9mQP&D`XhC7*(X?^pRSjE(F`|t@N`@X%8jhPm-@LOYSXbONe<#)
z3aySyGN0%y5`kj|UdnGO+tW3E2g2p1MHVgQ+`JWtq3eo?ArL$#ioO2CwS)JV;`FVK6bDj;1dTQpW>s_@o)nPkg|pg=gTg&Pm5`L
z)$6yIHg2y@uVwcAB`D@bhY3KXI~_)@M^hMa_$2+ZyDjI*Rgmp$qEnhdkFUe_Do#pS
zyU+HP6MFL^2lJws8zsv^>1C_Wt?
z;HCneEx}J&cLk@B?Y$-V;@$85a8#YKku|&g@Ke$!XdMYQ&45KC;u{Pejts|zFJhs<
z@IpTzw&@|td<{9SN05z-*kZPnU}}&kCPr)rPBd~yJ|W9ThHm>>!CpTt?GdaGahH*O
X(QH+rlHlh^@UVTOfjymkm=690r{`%W
literal 0
HcmV?d00001
diff --git a/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js
new file mode 100644
index 0000000000..e99dd7bf96
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js
@@ -0,0 +1,712 @@
+/* jshint -W117 */
+// SDP STUFF
+function SDP(sdp) {
+ this.media = sdp.split('\r\nm=');
+ for (var i = 1; i < this.media.length; i++) {
+ this.media[i] = 'm=' + this.media[i];
+ if (i != this.media.length - 1) {
+ this.media[i] += '\r\n';
+ }
+ }
+ this.session = this.media.shift() + '\r\n';
+ this.raw = this.session + this.media.join('');
+}
+
+exports.SDP = SDP;
+
+var jsdom = require("jsdom");
+var window = jsdom.jsdom().parentWindow;
+var $ = require('jquery')(window);
+
+var SDPUtil = require('./strophe.jingle.sdp.util.js').SDPUtil;
+
+/**
+ * Returns map of MediaChannel mapped per channel idx.
+ */
+SDP.prototype.getMediaSsrcMap = function() {
+ var self = this;
+ var media_ssrcs = {};
+ for (channelNum = 0; channelNum < self.media.length; channelNum++) {
+ modified = true;
+ tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc:');
+ var type = SDPUtil.parse_mid(SDPUtil.find_line(self.media[channelNum], 'a=mid:'));
+ var channel = new MediaChannel(channelNum, type);
+ media_ssrcs[channelNum] = channel;
+ tmp.forEach(function (line) {
+ var linessrc = line.substring(7).split(' ')[0];
+ // allocate new ChannelSsrc
+ if(!channel.ssrcs[linessrc]) {
+ channel.ssrcs[linessrc] = new ChannelSsrc(linessrc, type);
+ }
+ channel.ssrcs[linessrc].lines.push(line);
+ });
+ tmp = SDPUtil.find_lines(self.media[channelNum], 'a=ssrc-group:');
+ tmp.forEach(function(line){
+ var semantics = line.substr(0, idx).substr(13);
+ var ssrcs = line.substr(14 + semantics.length).split(' ');
+ if (ssrcs.length != 0) {
+ var ssrcGroup = new ChannelSsrcGroup(semantics, ssrcs);
+ channel.ssrcGroups.push(ssrcGroup);
+ }
+ });
+ }
+ return media_ssrcs;
+};
+/**
+ * Returns true if this SDP contains given SSRC.
+ * @param ssrc the ssrc to check.
+ * @returns {boolean} true if this SDP contains given SSRC.
+ */
+SDP.prototype.containsSSRC = function(ssrc) {
+ var channels = this.getMediaSsrcMap();
+ var contains = false;
+ Object.keys(channels).forEach(function(chNumber){
+ var channel = channels[chNumber];
+ //console.log("Check", channel, ssrc);
+ if(Object.keys(channel.ssrcs).indexOf(ssrc) != -1){
+ contains = true;
+ }
+ });
+ return contains;
+};
+
+/**
+ * Returns map of MediaChannel that contains only media not contained in otherSdp. Mapped by channel idx.
+ * @param otherSdp the other SDP to check ssrc with.
+ */
+SDP.prototype.getNewMedia = function(otherSdp) {
+
+ // this could be useful in Array.prototype.
+ function arrayEquals(array) {
+ // if the other array is a falsy value, return
+ if (!array)
+ return false;
+
+ // compare lengths - can save a lot of time
+ if (this.length != array.length)
+ return false;
+
+ for (var i = 0, l=this.length; i < l; i++) {
+ // Check if we have nested arrays
+ if (this[i] instanceof Array && array[i] instanceof Array) {
+ // recurse into the nested arrays
+ if (!this[i].equals(array[i]))
+ return false;
+ }
+ else if (this[i] != array[i]) {
+ // Warning - two different object instances will never be equal: {x:20} != {x:20}
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var myMedia = this.getMediaSsrcMap();
+ var othersMedia = otherSdp.getMediaSsrcMap();
+ var newMedia = {};
+ Object.keys(othersMedia).forEach(function(channelNum) {
+ var myChannel = myMedia[channelNum];
+ var othersChannel = othersMedia[channelNum];
+ if(!myChannel && othersChannel) {
+ // Add whole channel
+ newMedia[channelNum] = othersChannel;
+ return;
+ }
+ // Look for new ssrcs accross the channel
+ Object.keys(othersChannel.ssrcs).forEach(function(ssrc) {
+ if(Object.keys(myChannel.ssrcs).indexOf(ssrc) === -1) {
+ // Allocate channel if we've found ssrc that doesn't exist in our channel
+ if(!newMedia[channelNum]){
+ newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
+ }
+ newMedia[channelNum].ssrcs[ssrc] = othersChannel.ssrcs[ssrc];
+ }
+ });
+
+ // Look for new ssrc groups across the channels
+ othersChannel.ssrcGroups.forEach(function(otherSsrcGroup){
+
+ // try to match the other ssrc-group with an ssrc-group of ours
+ var matched = false;
+ for (var i = 0; i < myChannel.ssrcGroups.length; i++) {
+ var mySsrcGroup = myChannel.ssrcGroups[i];
+ if (otherSsrcGroup.semantics == mySsrcGroup.semantics
+ && arrayEquals.apply(otherSsrcGroup.ssrcs, [mySsrcGroup.ssrcs])) {
+
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ // Allocate channel if we've found an ssrc-group that doesn't
+ // exist in our channel
+
+ if(!newMedia[channelNum]){
+ newMedia[channelNum] = new MediaChannel(othersChannel.chNumber, othersChannel.mediaType);
+ }
+ newMedia[channelNum].ssrcGroups.push(otherSsrcGroup);
+ }
+ });
+ });
+ return newMedia;
+};
+
+// remove iSAC and CN from SDP
+SDP.prototype.mangle = function () {
+ var i, j, mline, lines, rtpmap, newdesc;
+ for (i = 0; i < this.media.length; i++) {
+ lines = this.media[i].split('\r\n');
+ lines.pop(); // remove empty last element
+ mline = SDPUtil.parse_mline(lines.shift());
+ if (mline.media != 'audio')
+ continue;
+ newdesc = '';
+ mline.fmt.length = 0;
+ for (j = 0; j < lines.length; j++) {
+ if (lines[j].substr(0, 9) == 'a=rtpmap:') {
+ rtpmap = SDPUtil.parse_rtpmap(lines[j]);
+ if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
+ continue;
+ mline.fmt.push(rtpmap.id);
+ newdesc += lines[j] + '\r\n';
+ } else {
+ newdesc += lines[j] + '\r\n';
+ }
+ }
+ this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
+ this.media[i] += newdesc;
+ }
+ this.raw = this.session + this.media.join('');
+};
+
+// remove lines matching prefix from session section
+SDP.prototype.removeSessionLines = function(prefix) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.session, prefix);
+ lines.forEach(function(line) {
+ self.session = self.session.replace(line + '\r\n', '');
+ });
+ this.raw = this.session + this.media.join('');
+ return lines;
+}
+// remove lines matching prefix from a media section specified by mediaindex
+// TODO: non-numeric mediaindex could match mid
+SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
+ var self = this;
+ var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
+ lines.forEach(function(line) {
+ self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
+ });
+ this.raw = this.session + this.media.join('');
+ return lines;
+}
+
+// add content's to a jingle element
+SDP.prototype.toJingle = function (elem, thecreator) {
+ var i, j, k, mline, ssrc, rtpmap, tmp, line, lines;
+ var self = this;
+ // new bundle plan
+ if (SDPUtil.find_line(this.session, 'a=group:')) {
+ lines = SDPUtil.find_lines(this.session, 'a=group:');
+ for (i = 0; i < lines.length; i++) {
+ tmp = lines[i].split(' ');
+ var semantics = tmp.shift().substr(8);
+ elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
+ for (j = 0; j < tmp.length; j++) {
+ elem.c('content', {name: tmp[j]}).up();
+ }
+ elem.up();
+ }
+ }
+ // old bundle plan, to be removed
+ var bundle = [];
+ if (SDPUtil.find_line(this.session, 'a=group:BUNDLE')) {
+ bundle = SDPUtil.find_line(this.session, 'a=group:BUNDLE ').split(' ');
+ bundle.shift();
+ }
+ for (i = 0; i < this.media.length; i++) {
+ mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
+ if (!(mline.media === 'audio' ||
+ mline.media === 'video' ||
+ mline.media === 'application'))
+ {
+ continue;
+ }
+ if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
+ ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
+ } else {
+ ssrc = false;
+ }
+
+ elem.c('content', {creator: thecreator, name: mline.media});
+ if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
+ // prefer identifier from a=mid if present
+ var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
+ elem.attrs({ name: mid });
+
+ // old BUNDLE plan, to be removed
+ if (bundle.indexOf(mid) !== -1) {
+ elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
+ bundle.splice(bundle.indexOf(mid), 1);
+ }
+ }
+
+ if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
+ {
+ elem.c('description',
+ {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
+ media: mline.media });
+ if (ssrc) {
+ elem.attrs({ssrc: ssrc});
+ }
+ for (j = 0; j < mline.fmt.length; j++) {
+ rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
+ elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
+ // put any 'a=fmtp:' + mline.fmt[j] lines into
+ if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
+ tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
+ for (k = 0; k < tmp.length; k++) {
+ elem.c('parameter', tmp[k]).up();
+ }
+ }
+ this.RtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
+
+ elem.up();
+ }
+ if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
+ elem.c('encryption', {required: 1});
+ var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
+ crypto.forEach(function(line) {
+ elem.c('crypto', SDPUtil.parse_crypto(line)).up();
+ });
+ elem.up(); // end of encryption
+ }
+
+ if (ssrc) {
+ // new style mapping
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ // FIXME: group by ssrc and support multiple different ssrcs
+ var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
+ ssrclines.forEach(function(line) {
+ idx = line.indexOf(' ');
+ var linessrc = line.substr(0, idx).substr(7);
+ if (linessrc != ssrc) {
+ elem.up();
+ ssrc = linessrc;
+ elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ }
+ var kv = line.substr(idx + 1);
+ elem.c('parameter');
+ if (kv.indexOf(':') == -1) {
+ elem.attrs({ name: kv });
+ } else {
+ elem.attrs({ name: kv.split(':', 2)[0] });
+ elem.attrs({ value: kv.split(':', 2)[1] });
+ }
+ elem.up();
+ });
+ elem.up();
+
+ // old proprietary mapping, to be removed at some point
+ tmp = SDPUtil.parse_ssrc(this.media[i]);
+ tmp.xmlns = 'http://estos.de/ns/ssrc';
+ tmp.ssrc = ssrc;
+ elem.c('ssrc', tmp).up(); // ssrc is part of description
+
+ // XEP-0339 handle ssrc-group attributes
+ var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
+ ssrc_group_lines.forEach(function(line) {
+ idx = line.indexOf(' ');
+ var semantics = line.substr(0, idx).substr(13);
+ var ssrcs = line.substr(14 + semantics.length).split(' ');
+ if (ssrcs.length != 0) {
+ elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
+ ssrcs.forEach(function(ssrc) {
+ elem.c('source', { ssrc: ssrc })
+ .up();
+ });
+ elem.up();
+ }
+ });
+ }
+
+ if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
+ elem.c('rtcp-mux').up();
+ }
+
+ // XEP-0293 -- map a=rtcp-fb:*
+ this.RtcpFbToJingle(i, elem, '*');
+
+ // XEP-0294
+ if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
+ lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
+ for (j = 0; j < lines.length; j++) {
+ tmp = SDPUtil.parse_extmap(lines[j]);
+ elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
+ uri: tmp.uri,
+ id: tmp.value });
+ if (tmp.hasOwnProperty('direction')) {
+ switch (tmp.direction) {
+ case 'sendonly':
+ elem.attrs({senders: 'responder'});
+ break;
+ case 'recvonly':
+ elem.attrs({senders: 'initiator'});
+ break;
+ case 'sendrecv':
+ elem.attrs({senders: 'both'});
+ break;
+ case 'inactive':
+ elem.attrs({senders: 'none'});
+ break;
+ }
+ }
+ // TODO: handle params
+ elem.up();
+ }
+ }
+ elem.up(); // end of description
+ }
+
+ // map ice-ufrag/pwd, dtls fingerprint, candidates
+ this.TransportToJingle(i, elem);
+
+ if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
+ elem.attrs({senders: 'both'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
+ elem.attrs({senders: 'initiator'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
+ elem.attrs({senders: 'responder'});
+ } else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
+ elem.attrs({senders: 'none'});
+ }
+ if (mline.port == '0') {
+ // estos hack to reject an m-line
+ elem.attrs({senders: 'rejected'});
+ }
+ elem.up(); // end of content
+ }
+ elem.up();
+ return elem;
+};
+
+SDP.prototype.TransportToJingle = function (mediaindex, elem) {
+ var i = mediaindex;
+ var tmp;
+ var self = this;
+ elem.c('transport');
+
+ // XEP-0343 DTLS/SCTP
+ if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
+ {
+ var sctpmap = SDPUtil.find_line(
+ this.media[i], 'a=sctpmap:', self.session);
+ if (sctpmap)
+ {
+ var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
+ elem.c('sctpmap',
+ {
+ xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
+ number: sctpAttrs[0], /* SCTP port */
+ protocol: sctpAttrs[1], /* protocol */
+ });
+ // Optional stream count attribute
+ if (sctpAttrs.length > 2)
+ elem.attrs({ streams: sctpAttrs[2]});
+ elem.up();
+ }
+ }
+ // XEP-0320
+ var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
+ fingerprints.forEach(function(line) {
+ tmp = SDPUtil.parse_fingerprint(line);
+ tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
+ elem.c('fingerprint').t(tmp.fingerprint);
+ delete tmp.fingerprint;
+ line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
+ if (line) {
+ tmp.setup = line.substr(8);
+ }
+ elem.attrs(tmp);
+ elem.up(); // end of fingerprint
+ });
+ tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
+ if (tmp) {
+ tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
+ elem.attrs(tmp);
+ // XEP-0176
+ if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
+ var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
+ lines.forEach(function (line) {
+ elem.c('candidate', SDPUtil.candidateToJingle(line)).up();
+ });
+ }
+ }
+ elem.up(); // end of transport
+}
+
+SDP.prototype.RtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
+ var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
+ lines.forEach(function (line) {
+ var tmp = SDPUtil.parse_rtcpfb(line);
+ if (tmp.type == 'trr-int') {
+ elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
+ elem.up();
+ } else {
+ elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
+ if (tmp.params.length > 0) {
+ elem.attrs({'subtype': tmp.params[0]});
+ }
+ elem.up();
+ }
+ });
+};
+
+SDP.prototype.RtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
+ var media = '';
+ var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ if (tmp.length) {
+ media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
+ if (tmp.attr('value')) {
+ media += tmp.attr('value');
+ } else {
+ media += '0';
+ }
+ media += '\r\n';
+ }
+ tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
+ tmp.each(function () {
+ media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
+ if ($(this).attr('subtype')) {
+ media += ' ' + $(this).attr('subtype');
+ }
+ media += '\r\n';
+ });
+ return media;
+};
+
+// construct an SDP from a jingle stanza
+SDP.prototype.fromJingle = function (jingle) {
+ var self = this;
+ this.raw = 'v=0\r\n' +
+ 'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
+ 's=-\r\n' +
+ 't=0 0\r\n';
+ // http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
+ if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
+ $(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
+ var contents = $(group).find('>content').map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (contents.length > 0) {
+ self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ } else if ($(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').length) {
+ // temporary namespace, not to be used. to be removed soon.
+ $(jingle).find('>group[xmlns="urn:ietf:rfc:5888"]').each(function (idx, group) {
+ var contents = $(group).find('>content').map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (group.getAttribute('type') !== null && contents.length > 0) {
+ self.raw += 'a=group:' + group.getAttribute('type') + ' ' + contents.join(' ') + '\r\n';
+ }
+ });
+ } else {
+ // for backward compability, to be removed soon
+ // assume all contents are in the same bundle group, can be improved upon later
+ var bundle = $(jingle).find('>content').filter(function (idx, content) {
+ //elem.c('bundle', {xmlns:'http://estos.de/ns/bundle'});
+ return $(content).find('>bundle').length > 0;
+ }).map(function (idx, content) {
+ return content.getAttribute('name');
+ }).get();
+ if (bundle.length) {
+ this.raw += 'a=group:BUNDLE ' + bundle.join(' ') + '\r\n';
+ }
+ }
+
+ this.session = this.raw;
+ jingle.find('>content').each(function () {
+ var m = self.jingle2media($(this));
+ self.media.push(m);
+ });
+
+ // reconstruct msid-semantic -- apparently not necessary
+ /*
+ var msid = SDPUtil.parse_ssrc(this.raw);
+ if (msid.hasOwnProperty('mslabel')) {
+ this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
+ }
+ */
+
+ this.raw = this.session + this.media.join('');
+};
+
+// translate a jingle content element into an an SDP media part
+SDP.prototype.jingle2media = function (content) {
+ var media = '',
+ desc = content.find('description'),
+ ssrc = desc.attr('ssrc'),
+ self = this,
+ tmp;
+ var sctp = content.find(
+ '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
+
+ tmp = { media: desc.attr('media') };
+ tmp.port = '1';
+ if (content.attr('senders') == 'rejected') {
+ // estos hack to reject an m-line.
+ tmp.port = '0';
+ }
+ if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
+ if (sctp.length)
+ tmp.proto = 'DTLS/SCTP';
+ else
+ tmp.proto = 'RTP/SAVPF';
+ } else {
+ tmp.proto = 'RTP/AVPF';
+ }
+ if (!sctp.length)
+ {
+ tmp.fmt = desc.find('payload-type').map(
+ function () { return this.getAttribute('id'); }).get();
+ media += SDPUtil.build_mline(tmp) + '\r\n';
+ }
+ else
+ {
+ media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
+ media += 'a=sctpmap:' + sctp.attr('number') +
+ ' ' + sctp.attr('protocol');
+
+ var streamCount = sctp.attr('streams');
+ if (streamCount)
+ media += ' ' + streamCount + '\r\n';
+ else
+ media += '\r\n';
+ }
+
+ media += 'c=IN IP4 0.0.0.0\r\n';
+ if (!sctp.length)
+ media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
+ //tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
+ tmp = content.find('>bundle>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
+ //console.log('transports: '+content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]').length);
+ //console.log('bundle.transports: '+content.find('>bundle>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]').length);
+ //console.log("tmp fingerprint: "+tmp.find('>fingerprint').innerHTML);
+ if (tmp.length) {
+ if (tmp.attr('ufrag')) {
+ media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
+ }
+ if (tmp.attr('pwd')) {
+ media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
+ }
+ tmp.find('>fingerprint').each(function () {
+ // FIXME: check namespace at some point
+ media += 'a=fingerprint:' + this.getAttribute('hash');
+ media += ' ' + $(this).text();
+ media += '\r\n';
+ //console.log("mline "+media);
+ if (this.getAttribute('setup')) {
+ media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
+ }
+ });
+ }
+ switch (content.attr('senders')) {
+ case 'initiator':
+ media += 'a=sendonly\r\n';
+ break;
+ case 'responder':
+ media += 'a=recvonly\r\n';
+ break;
+ case 'none':
+ media += 'a=inactive\r\n';
+ break;
+ case 'both':
+ media += 'a=sendrecv\r\n';
+ break;
+ }
+ media += 'a=mid:' + content.attr('name') + '\r\n';
+ /*if (content.attr('name') == 'video') {
+ media += 'a=x-google-flag:conference' + '\r\n';
+ }*/
+
+ //
+ // see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
+ // and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
+ if (desc.find('rtcp-mux').length) {
+ media += 'a=rtcp-mux\r\n';
+ }
+
+ if (desc.find('encryption').length) {
+ desc.find('encryption>crypto').each(function () {
+ media += 'a=crypto:' + this.getAttribute('tag');
+ media += ' ' + this.getAttribute('crypto-suite');
+ media += ' ' + this.getAttribute('key-params');
+ if (this.getAttribute('session-params')) {
+ media += ' ' + this.getAttribute('session-params');
+ }
+ media += '\r\n';
+ });
+ }
+ desc.find('payload-type').each(function () {
+ media += SDPUtil.build_rtpmap(this) + '\r\n';
+ if ($(this).find('>parameter').length) {
+ media += 'a=fmtp:' + this.getAttribute('id') + ' ';
+ media += $(this).find('parameter').map(function () { return (this.getAttribute('name') ? (this.getAttribute('name') + '=') : '') + this.getAttribute('value'); }).get().join('; ');
+ media += '\r\n';
+ }
+ // xep-0293
+ media += self.RtcpFbFromJingle($(this), this.getAttribute('id'));
+ });
+
+ // xep-0293
+ media += self.RtcpFbFromJingle(desc, '*');
+
+ // xep-0294
+ tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
+ tmp.each(function () {
+ media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
+ });
+
+ content.find('>bundle>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
+ media += SDPUtil.candidateFromJingle(this);
+ });
+
+ // XEP-0339 handle ssrc-group attributes
+ tmp = content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
+ var semantics = this.getAttribute('semantics');
+ var ssrcs = $(this).find('>source').map(function() {
+ return this.getAttribute('ssrc');
+ }).get();
+
+ if (ssrcs.length != 0) {
+ media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
+ }
+ });
+
+ tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
+ tmp.each(function () {
+ var ssrc = this.getAttribute('ssrc');
+ $(this).find('>parameter').each(function () {
+ media += 'a=ssrc:' + ssrc + ' ' + this.getAttribute('name');
+ if (this.getAttribute('value') && this.getAttribute('value').length)
+ media += ':' + this.getAttribute('value');
+ media += '\r\n';
+ });
+ });
+
+ if (tmp.length === 0) {
+ // fallback to proprietary mapping of a=ssrc lines
+ tmp = content.find('description>ssrc[xmlns="http://estos.de/ns/ssrc"]');
+ if (tmp.length) {
+ media += 'a=ssrc:' + ssrc + ' cname:' + tmp.attr('cname') + '\r\n';
+ media += 'a=ssrc:' + ssrc + ' msid:' + tmp.attr('msid') + '\r\n';
+ media += 'a=ssrc:' + ssrc + ' mslabel:' + tmp.attr('mslabel') + '\r\n';
+ media += 'a=ssrc:' + ssrc + ' label:' + tmp.attr('label') + '\r\n';
+ }
+ }
+ return media;
+};
+
diff --git a/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js
new file mode 100644
index 0000000000..042a123c32
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.util.js
@@ -0,0 +1,408 @@
+/**
+ * Contains utility classes used in SDP class.
+ *
+ */
+
+/**
+ * Class holds a=ssrc lines and media type a=mid
+ * @param ssrc synchronization source identifier number(a=ssrc lines from SDP)
+ * @param type media type eg. "audio" or "video"(a=mid frm SDP)
+ * @constructor
+ */
+function ChannelSsrc(ssrc, type) {
+ this.ssrc = ssrc;
+ this.type = type;
+ this.lines = [];
+}
+
+/**
+ * Class holds a=ssrc-group: lines
+ * @param semantics
+ * @param ssrcs
+ * @constructor
+ */
+function ChannelSsrcGroup(semantics, ssrcs, line) {
+ this.semantics = semantics;
+ this.ssrcs = ssrcs;
+}
+
+/**
+ * Helper class represents media channel. Is a container for ChannelSsrc, holds channel idx and media type.
+ * @param channelNumber channel idx in SDP media array.
+ * @param mediaType media type(a=mid)
+ * @constructor
+ */
+function MediaChannel(channelNumber, mediaType) {
+ /**
+ * SDP channel number
+ * @type {*}
+ */
+ this.chNumber = channelNumber;
+ /**
+ * Channel media type(a=mid)
+ * @type {*}
+ */
+ this.mediaType = mediaType;
+ /**
+ * The maps of ssrc numbers to ChannelSsrc objects.
+ */
+ this.ssrcs = {};
+
+ /**
+ * The array of ChannelSsrcGroup objects.
+ * @type {Array}
+ */
+ this.ssrcGroups = [];
+}
+
+SDPUtil = {
+ iceparams: function (mediadesc, sessiondesc) {
+ var data = null;
+ if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
+ SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
+ data = {
+ ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
+ pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
+ };
+ }
+ return data;
+ },
+ parse_iceufrag: function (line) {
+ return line.substring(12);
+ },
+ build_iceufrag: function (frag) {
+ return 'a=ice-ufrag:' + frag;
+ },
+ parse_icepwd: function (line) {
+ return line.substring(10);
+ },
+ build_icepwd: function (pwd) {
+ return 'a=ice-pwd:' + pwd;
+ },
+ parse_mid: function (line) {
+ return line.substring(6);
+ },
+ parse_mline: function (line) {
+ var parts = line.substring(2).split(' '),
+ data = {};
+ data.media = parts.shift();
+ data.port = parts.shift();
+ data.proto = parts.shift();
+ if (parts[parts.length - 1] === '') { // trailing whitespace
+ parts.pop();
+ }
+ data.fmt = parts;
+ return data;
+ },
+ build_mline: function (mline) {
+ return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
+ },
+ parse_rtpmap: function (line) {
+ var parts = line.substring(9).split(' '),
+ data = {};
+ data.id = parts.shift();
+ parts = parts[0].split('/');
+ data.name = parts.shift();
+ data.clockrate = parts.shift();
+ data.channels = parts.length ? parts.shift() : '1';
+ return data;
+ },
+ /**
+ * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
+ * @param line eg. "a=sctpmap:5000 webrtc-datachannel"
+ * @returns [SCTP port number, protocol, streams]
+ */
+ parse_sctpmap: function (line)
+ {
+ var parts = line.substring(10).split(' ');
+ var sctpPort = parts[0];
+ var protocol = parts[1];
+ // Stream count is optional
+ var streamCount = parts.length > 2 ? parts[2] : null;
+ return [sctpPort, protocol, streamCount];// SCTP port
+ },
+ build_rtpmap: function (el) {
+ var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
+ if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
+ line += '/' + el.getAttribute('channels');
+ }
+ return line;
+ },
+ parse_crypto: function (line) {
+ var parts = line.substring(9).split(' '),
+ data = {};
+ data.tag = parts.shift();
+ data['crypto-suite'] = parts.shift();
+ data['key-params'] = parts.shift();
+ if (parts.length) {
+ data['session-params'] = parts.join(' ');
+ }
+ return data;
+ },
+ parse_fingerprint: function (line) { // RFC 4572
+ var parts = line.substring(14).split(' '),
+ data = {};
+ data.hash = parts.shift();
+ data.fingerprint = parts.shift();
+ // TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
+ return data;
+ },
+ parse_fmtp: function (line) {
+ var parts = line.split(' '),
+ i, key, value,
+ data = [];
+ parts.shift();
+ parts = parts.join(' ').split(';');
+ for (i = 0; i < parts.length; i++) {
+ key = parts[i].split('=')[0];
+ while (key.length && key[0] == ' ') {
+ key = key.substring(1);
+ }
+ value = parts[i].split('=')[1];
+ if (key && value) {
+ data.push({name: key, value: value});
+ } else if (key) {
+ // rfc 4733 (DTMF) style stuff
+ data.push({name: '', value: key});
+ }
+ }
+ return data;
+ },
+ parse_icecandidate: function (line) {
+ var candidate = {},
+ elems = line.split(' ');
+ candidate.foundation = elems[0].substring(12);
+ candidate.component = elems[1];
+ candidate.protocol = elems[2].toLowerCase();
+ candidate.priority = elems[3];
+ candidate.ip = elems[4];
+ candidate.port = elems[5];
+ // elems[6] => "typ"
+ candidate.type = elems[7];
+ candidate.generation = 0; // default value, may be overwritten below
+ for (var i = 8; i < elems.length; i += 2) {
+ switch (elems[i]) {
+ case 'raddr':
+ candidate['rel-addr'] = elems[i + 1];
+ break;
+ case 'rport':
+ candidate['rel-port'] = elems[i + 1];
+ break;
+ case 'generation':
+ candidate.generation = elems[i + 1];
+ break;
+ case 'tcptype':
+ candidate.tcptype = elems[i + 1];
+ break;
+ default: // TODO
+ console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
+ }
+ }
+ candidate.network = '1';
+ candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
+ return candidate;
+ },
+ build_icecandidate: function (cand) {
+ var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
+ line += ' ';
+ switch (cand.type) {
+ case 'srflx':
+ case 'prflx':
+ case 'relay':
+ if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
+ line += 'raddr';
+ line += ' ';
+ line += cand['rel-addr'];
+ line += ' ';
+ line += 'rport';
+ line += ' ';
+ line += cand['rel-port'];
+ line += ' ';
+ }
+ break;
+ }
+ if (cand.hasOwnAttribute('tcptype')) {
+ line += 'tcptype';
+ line += ' ';
+ line += cand.tcptype;
+ line += ' ';
+ }
+ line += 'generation';
+ line += ' ';
+ line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
+ return line;
+ },
+ parse_ssrc: function (desc) {
+ // proprietary mapping of a=ssrc lines
+ // TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
+ // and parse according to that
+ var lines = desc.split('\r\n'),
+ data = {};
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, 7) == 'a=ssrc:') {
+ var idx = lines[i].indexOf(' ');
+ data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
+ }
+ }
+ return data;
+ },
+ parse_rtcpfb: function (line) {
+ var parts = line.substr(10).split(' ');
+ var data = {};
+ data.pt = parts.shift();
+ data.type = parts.shift();
+ data.params = parts;
+ return data;
+ },
+ parse_extmap: function (line) {
+ var parts = line.substr(9).split(' ');
+ var data = {};
+ data.value = parts.shift();
+ if (data.value.indexOf('/') != -1) {
+ data.direction = data.value.substr(data.value.indexOf('/') + 1);
+ data.value = data.value.substr(0, data.value.indexOf('/'));
+ } else {
+ data.direction = 'both';
+ }
+ data.uri = parts.shift();
+ data.params = parts;
+ return data;
+ },
+ find_line: function (haystack, needle, sessionpart) {
+ var lines = haystack.split('\r\n');
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, needle.length) == needle) {
+ return lines[i];
+ }
+ }
+ if (!sessionpart) {
+ return false;
+ }
+ // search session part
+ lines = sessionpart.split('\r\n');
+ for (var j = 0; j < lines.length; j++) {
+ if (lines[j].substring(0, needle.length) == needle) {
+ return lines[j];
+ }
+ }
+ return false;
+ },
+ find_lines: function (haystack, needle, sessionpart) {
+ var lines = haystack.split('\r\n'),
+ needles = [];
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].substring(0, needle.length) == needle)
+ needles.push(lines[i]);
+ }
+ if (needles.length || !sessionpart) {
+ return needles;
+ }
+ // search session part
+ lines = sessionpart.split('\r\n');
+ for (var j = 0; j < lines.length; j++) {
+ if (lines[j].substring(0, needle.length) == needle) {
+ needles.push(lines[j]);
+ }
+ }
+ return needles;
+ },
+ candidateToJingle: function (line) {
+ // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
+ //
+ if (line.indexOf('candidate:') === 0) {
+ line = 'a=' + line;
+ } else if (line.substring(0, 12) != 'a=candidate:') {
+ console.log('parseCandidate called with a line that is not a candidate line');
+ console.log(line);
+ return null;
+ }
+ if (line.substring(line.length - 2) == '\r\n') // chomp it
+ line = line.substring(0, line.length - 2);
+ var candidate = {},
+ elems = line.split(' '),
+ i;
+ if (elems[6] != 'typ') {
+ console.log('did not find typ in the right place');
+ console.log(line);
+ return null;
+ }
+ candidate.foundation = elems[0].substring(12);
+ candidate.component = elems[1];
+ candidate.protocol = elems[2].toLowerCase();
+ candidate.priority = elems[3];
+ candidate.ip = elems[4];
+ candidate.port = elems[5];
+ // elems[6] => "typ"
+ candidate.type = elems[7];
+
+ candidate.generation = '0'; // default, may be overwritten below
+ for (i = 8; i < elems.length; i += 2) {
+ switch (elems[i]) {
+ case 'raddr':
+ candidate['rel-addr'] = elems[i + 1];
+ break;
+ case 'rport':
+ candidate['rel-port'] = elems[i + 1];
+ break;
+ case 'generation':
+ candidate.generation = elems[i + 1];
+ break;
+ case 'tcptype':
+ candidate.tcptype = elems[i + 1];
+ break;
+ default: // TODO
+ console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
+ }
+ }
+ candidate.network = '1';
+ candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
+ return candidate;
+ },
+ candidateFromJingle: function (cand) {
+ var line = 'a=candidate:';
+ line += cand.getAttribute('foundation');
+ line += ' ';
+ line += cand.getAttribute('component');
+ line += ' ';
+ line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this
+ line += ' ';
+ line += cand.getAttribute('priority');
+ line += ' ';
+ line += cand.getAttribute('ip');
+ line += ' ';
+ line += cand.getAttribute('port');
+ line += ' ';
+ line += 'typ';
+ line += ' ' + cand.getAttribute('type');
+ line += ' ';
+ switch (cand.getAttribute('type')) {
+ case 'srflx':
+ case 'prflx':
+ case 'relay':
+ if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
+ line += 'raddr';
+ line += ' ';
+ line += cand.getAttribute('rel-addr');
+ line += ' ';
+ line += 'rport';
+ line += ' ';
+ line += cand.getAttribute('rel-port');
+ line += ' ';
+ }
+ break;
+ }
+ if (cand.getAttribute('protocol').toLowerCase() == 'tcp') {
+ line += 'tcptype';
+ line += ' ';
+ line += cand.getAttribute('tcptype');
+ line += ' ';
+ }
+ line += 'generation';
+ line += ' ';
+ line += cand.getAttribute('generation') || '0';
+ return line + '\r\n';
+ }
+};
+
+exports.SDPUtil = SDPUtil;
+
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js b/contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js
new file mode 100644
index 0000000000..9c45c2df18
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/XMLHttpRequest.js
@@ -0,0 +1,254 @@
+/**
+ * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
+ *
+ * This can be used with JS designed for browsers to improve reuse of code and
+ * allow the use of existing libraries.
+ *
+ * Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.
+ *
+ * @todo SSL Support
+ * @author Dan DeFelippi
+ * @license MIT
+ */
+
+var Url = require("url")
+ ,sys = require("util");
+
+exports.XMLHttpRequest = function() {
+ /**
+ * Private variables
+ */
+ var self = this;
+ var http = require('http');
+ var https = require('https');
+
+ // Holds http.js objects
+ var client;
+ var request;
+ var response;
+
+ // Request settings
+ var settings = {};
+
+ // Set some default headers
+ var defaultHeaders = {
+ "User-Agent": "node.js",
+ "Accept": "*/*",
+ };
+
+ var headers = defaultHeaders;
+
+ /**
+ * Constants
+ */
+ this.UNSENT = 0;
+ this.OPENED = 1;
+ this.HEADERS_RECEIVED = 2;
+ this.LOADING = 3;
+ this.DONE = 4;
+
+ /**
+ * Public vars
+ */
+ // Current state
+ this.readyState = this.UNSENT;
+
+ // default ready state change handler in case one is not set or is set late
+ this.onreadystatechange = function() {};
+
+ // Result & response
+ this.responseText = "";
+ this.responseXML = "";
+ this.status = null;
+ this.statusText = null;
+
+ /**
+ * Open the connection. Currently supports local server requests.
+ *
+ * @param string method Connection method (eg GET, POST)
+ * @param string url URL for the connection.
+ * @param boolean async Asynchronous connection. Default is true.
+ * @param string user Username for basic authentication (optional)
+ * @param string password Password for basic authentication (optional)
+ */
+ this.open = function(method, url, async, user, password) {
+ settings = {
+ "method": method,
+ "url": url,
+ "async": async || null,
+ "user": user || null,
+ "password": password || null
+ };
+
+ this.abort();
+
+ setState(this.OPENED);
+ };
+
+ /**
+ * Sets a header for the request.
+ *
+ * @param string header Header name
+ * @param string value Header value
+ */
+ this.setRequestHeader = function(header, value) {
+ headers[header] = value;
+ };
+
+ /**
+ * Gets a header from the server response.
+ *
+ * @param string header Name of header to get.
+ * @return string Text of the header or null if it doesn't exist.
+ */
+ this.getResponseHeader = function(header) {
+ if (this.readyState > this.OPENED && response.headers[header]) {
+ return header + ": " + response.headers[header];
+ }
+
+ return null;
+ };
+
+ /**
+ * Gets all the response headers.
+ *
+ * @return string
+ */
+ this.getAllResponseHeaders = function() {
+ if (this.readyState < this.HEADERS_RECEIVED) {
+ throw "INVALID_STATE_ERR: Headers have not been received.";
+ }
+ var result = "";
+
+ for (var i in response.headers) {
+ result += i + ": " + response.headers[i] + "\r\n";
+ }
+ return result.substr(0, result.length - 2);
+ };
+
+ /**
+ * Sends the request to the server.
+ *
+ * @param string data Optional data to send as request body.
+ */
+ this.send = function(data) {
+ if (this.readyState != this.OPENED) {
+ throw "INVALID_STATE_ERR: connection must be opened before send() is called";
+ }
+
+ var ssl = false;
+ var url = Url.parse(settings.url);
+
+ // Determine the server
+ switch (url.protocol) {
+ case 'https:':
+ ssl = true;
+ // SSL & non-SSL both need host, no break here.
+ case 'http:':
+ var host = url.hostname;
+ break;
+
+ case undefined:
+ case '':
+ var host = "localhost";
+ break;
+
+ default:
+ throw "Protocol not supported.";
+ }
+
+ // Default to port 80. If accessing localhost on another port be sure
+ // to use http://localhost:port/path
+ var port = url.port || (ssl ? 443 : 80);
+ // Add query string if one is used
+ var uri = url.pathname + (url.search ? url.search : '');
+
+ // Set the Host header or the server may reject the request
+ this.setRequestHeader("Host", host);
+
+ // Set content length header
+ if (settings.method == "GET" || settings.method == "HEAD") {
+ data = null;
+ } else if (data) {
+ this.setRequestHeader("Content-Length", Buffer.byteLength(data));
+
+ if (!headers["Content-Type"]) {
+ this.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
+ }
+ }
+
+ // Use the proper protocol
+ var doRequest = ssl ? https.request : http.request;
+
+ var options = {
+ host: host,
+ port: port,
+ path: uri,
+ method: settings.method,
+ headers: headers,
+ agent: false
+ };
+
+ var req = doRequest(options, function(res) {
+ response = res;
+ response.setEncoding("utf8");
+
+ setState(self.HEADERS_RECEIVED);
+ self.status = response.statusCode;
+
+ response.on('data', function(chunk) {
+ // Make sure there's some data
+ if (chunk) {
+ self.responseText += chunk;
+ }
+ setState(self.LOADING);
+ });
+
+ response.on('end', function() {
+ setState(self.DONE);
+ });
+
+ response.on('error', function() {
+ self.handleError(error);
+ });
+ }).on('error', function(error) {
+ self.handleError(error);
+ });
+
+ req.setHeader("Connection", "Close");
+
+ // Node 0.4 and later won't accept empty data. Make sure it's needed.
+ if (data) {
+ req.write(data);
+ }
+
+ req.end();
+ };
+
+ this.handleError = function(error) {
+ this.status = 503;
+ this.statusText = error;
+ this.responseText = error.stack;
+ setState(this.DONE);
+ };
+
+ /**
+ * Aborts a request.
+ */
+ this.abort = function() {
+ headers = defaultHeaders;
+ this.readyState = this.UNSENT;
+ this.responseText = "";
+ this.responseXML = "";
+ };
+
+ /**
+ * Changes readyState and calls onreadystatechange.
+ *
+ * @param int state New state
+ */
+ var setState = function(state) {
+ self.readyState = state;
+ self.onreadystatechange();
+ }
+};
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/base64.js b/contrib/jitsimeetbridge/unjingle/strophe/base64.js
new file mode 100644
index 0000000000..418caac050
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/base64.js
@@ -0,0 +1,83 @@
+// This code was written by Tyler Akins and has been placed in the
+// public domain. It would be nice if you left this header intact.
+// Base64 code from Tyler Akins -- http://rumkin.com
+
+var Base64 = (function () {
+ var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+ var obj = {
+ /**
+ * Encodes a string in base64
+ * @param {String} input The string to encode in base64.
+ */
+ encode: function (input) {
+ var output = "";
+ var chr1, chr2, chr3;
+ var enc1, enc2, enc3, enc4;
+ var i = 0;
+
+ do {
+ chr1 = input.charCodeAt(i++);
+ chr2 = input.charCodeAt(i++);
+ chr3 = input.charCodeAt(i++);
+
+ enc1 = chr1 >> 2;
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+ enc4 = chr3 & 63;
+
+ if (isNaN(chr2)) {
+ enc3 = enc4 = 64;
+ } else if (isNaN(chr3)) {
+ enc4 = 64;
+ }
+
+ output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) +
+ keyStr.charAt(enc3) + keyStr.charAt(enc4);
+ } while (i < input.length);
+
+ return output;
+ },
+
+ /**
+ * Decodes a base64 string.
+ * @param {String} input The string to decode.
+ */
+ decode: function (input) {
+ var output = "";
+ var chr1, chr2, chr3;
+ var enc1, enc2, enc3, enc4;
+ var i = 0;
+
+ // remove all characters that are not A-Z, a-z, 0-9, +, /, or =
+ input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
+
+ do {
+ enc1 = keyStr.indexOf(input.charAt(i++));
+ enc2 = keyStr.indexOf(input.charAt(i++));
+ enc3 = keyStr.indexOf(input.charAt(i++));
+ enc4 = keyStr.indexOf(input.charAt(i++));
+
+ chr1 = (enc1 << 2) | (enc2 >> 4);
+ chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+ chr3 = ((enc3 & 3) << 6) | enc4;
+
+ output = output + String.fromCharCode(chr1);
+
+ if (enc3 != 64) {
+ output = output + String.fromCharCode(chr2);
+ }
+ if (enc4 != 64) {
+ output = output + String.fromCharCode(chr3);
+ }
+ } while (i < input.length);
+
+ return output;
+ }
+ };
+
+ return obj;
+})();
+
+// Nodify
+exports.Base64 = Base64;
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/md5.js b/contrib/jitsimeetbridge/unjingle/strophe/md5.js
new file mode 100644
index 0000000000..5334325e2f
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/md5.js
@@ -0,0 +1,279 @@
+/*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+var MD5 = (function () {
+ /*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+ var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+ var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
+ var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
+
+ /*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+ var safe_add = function (x, y) {
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+ };
+
+ /*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+ var bit_rol = function (num, cnt) {
+ return (num << cnt) | (num >>> (32 - cnt));
+ };
+
+ /*
+ * Convert a string to an array of little-endian words
+ * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
+ */
+ var str2binl = function (str) {
+ var bin = [];
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < str.length * chrsz; i += chrsz)
+ {
+ bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
+ }
+ return bin;
+ };
+
+ /*
+ * Convert an array of little-endian words to a string
+ */
+ var binl2str = function (bin) {
+ var str = "";
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < bin.length * 32; i += chrsz)
+ {
+ str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
+ }
+ return str;
+ };
+
+ /*
+ * Convert an array of little-endian words to a hex string.
+ */
+ var binl2hex = function (binarray) {
+ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var str = "";
+ for(var i = 0; i < binarray.length * 4; i++)
+ {
+ str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
+ hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
+ }
+ return str;
+ };
+
+ /*
+ * Convert an array of little-endian words to a base-64 string
+ */
+ var binl2b64 = function (binarray) {
+ var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var str = "";
+ var triplet, j;
+ for(var i = 0; i < binarray.length * 4; i += 3)
+ {
+ triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) |
+ (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) |
+ ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
+ for(j = 0; j < 4; j++)
+ {
+ if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; }
+ else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); }
+ }
+ }
+ return str;
+ };
+
+ /*
+ * These functions implement the four basic operations the algorithm uses.
+ */
+ var md5_cmn = function (q, a, b, x, s, t) {
+ return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b);
+ };
+
+ var md5_ff = function (a, b, c, d, x, s, t) {
+ return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+ };
+
+ var md5_gg = function (a, b, c, d, x, s, t) {
+ return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+ };
+
+ var md5_hh = function (a, b, c, d, x, s, t) {
+ return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+ };
+
+ var md5_ii = function (a, b, c, d, x, s, t) {
+ return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+ };
+
+ /*
+ * Calculate the MD5 of an array of little-endian words, and a bit length
+ */
+ var core_md5 = function (x, len) {
+ /* append padding */
+ x[len >> 5] |= 0x80 << ((len) % 32);
+ x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+
+ var olda, oldb, oldc, oldd;
+ for (var i = 0; i < x.length; i += 16)
+ {
+ olda = a;
+ oldb = b;
+ oldc = c;
+ oldd = d;
+
+ a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+ d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
+ c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
+ b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+ a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+ d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
+ c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+ b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
+ a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
+ d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+ c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
+ b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
+ a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
+ d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
+ c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
+ b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
+
+ a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+ d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+ c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
+ b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
+ a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+ d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
+ c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
+ b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
+ a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
+ d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
+ c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
+ b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
+ a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
+ d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+ c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
+ b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+ a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
+ d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+ c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
+ b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
+ a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+ d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
+ c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
+ b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
+ a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
+ d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
+ c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
+ b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
+ a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+ d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
+ c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
+ b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+ a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+ d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
+ c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
+ b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
+ a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
+ d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+ c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
+ b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+ a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
+ d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
+ c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+ b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
+ a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+ d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
+ c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
+ b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ }
+ return [a, b, c, d];
+ };
+
+
+ /*
+ * Calculate the HMAC-MD5, of a key and some data
+ */
+ var core_hmac_md5 = function (key, data) {
+ var bkey = str2binl(key);
+ if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); }
+
+ var ipad = new Array(16), opad = new Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5C5C5C5C;
+ }
+
+ var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
+ return core_md5(opad.concat(hash), 512 + 128);
+ };
+
+ var obj = {
+ /*
+ * These are the functions you'll usually want to call.
+ * They take string arguments and return either hex or base-64 encoded
+ * strings.
+ */
+ hexdigest: function (s) {
+ return binl2hex(core_md5(str2binl(s), s.length * chrsz));
+ },
+
+ b64digest: function (s) {
+ return binl2b64(core_md5(str2binl(s), s.length * chrsz));
+ },
+
+ hash: function (s) {
+ return binl2str(core_md5(str2binl(s), s.length * chrsz));
+ },
+
+ hmac_hexdigest: function (key, data) {
+ return binl2hex(core_hmac_md5(key, data));
+ },
+
+ hmac_b64digest: function (key, data) {
+ return binl2b64(core_hmac_md5(key, data));
+ },
+
+ hmac_hash: function (key, data) {
+ return binl2str(core_hmac_md5(key, data));
+ },
+
+ /*
+ * Perform a simple self-test to see if the VM is working
+ */
+ test: function () {
+ return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72";
+ }
+ };
+
+ return obj;
+})();
+
+// Nodify
+exports.MD5 = MD5;
diff --git a/contrib/jitsimeetbridge/unjingle/strophe/strophe.js b/contrib/jitsimeetbridge/unjingle/strophe/strophe.js
new file mode 100644
index 0000000000..06d426cdec
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/strophe/strophe.js
@@ -0,0 +1,3256 @@
+/*
+ This program is distributed under the terms of the MIT license.
+ Please see the LICENSE file for details.
+
+ Copyright 2006-2008, OGG, LLC
+*/
+
+/* jslint configuration: */
+/*global document, window, setTimeout, clearTimeout, console,
+ XMLHttpRequest, ActiveXObject,
+ Base64, MD5,
+ Strophe, $build, $msg, $iq, $pres */
+
+/** File: strophe.js
+ * A JavaScript library for XMPP BOSH.
+ *
+ * This is the JavaScript version of the Strophe library. Since JavaScript
+ * has no facilities for persistent TCP connections, this library uses
+ * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate
+ * a persistent, stateful, two-way connection to an XMPP server. More
+ * information on BOSH can be found in XEP 124.
+ */
+
+/** PrivateFunction: Function.prototype.bind
+ * Bind a function to an instance.
+ *
+ * This Function object extension method creates a bound method similar
+ * to those in Python. This means that the 'this' object will point
+ * to the instance you want. See
+ * MDC's bind() documentation and
+ * Bound Functions and Function Imports in JavaScript
+ * for a complete explanation.
+ *
+ * This extension already exists in some browsers (namely, Firefox 3), but
+ * we provide it to support those that don't.
+ *
+ * Parameters:
+ * (Object) obj - The object that will become 'this' in the bound function.
+ * (Object) argN - An option argument that will be prepended to the
+ * arguments given for the function call
+ *
+ * Returns:
+ * The bound function.
+ */
+
+/* Make it work on node.js: Nodify
+ *
+ * Steps:
+ * 1. Create the global objects: window, document, Base64, MD5 and XMLHttpRequest
+ * 2. Use the node-XMLHttpRequest module.
+ * 3. Use jsdom for the document object - since it supports DOM functions.
+ * 4. Replace all calls to childNodes with _childNodes (since the former doesn't
+ * seem to work on jsdom).
+ * 5. While getting the response from XMLHttpRequest, manually convert the text
+ * data to XML.
+ * 6. All calls to nodeName should replaced by nodeName.toLowerCase() since jsdom
+ * seems to always convert node names to upper case.
+ *
+ */
+var XMLHttpRequest = require('./XMLHttpRequest.js').XMLHttpRequest;
+var Base64 = require('./base64.js').Base64;
+var MD5 = require('./md5.js').MD5;
+var jsdom = require("jsdom").jsdom;
+
+document = jsdom(""),
+
+window = {
+ XMLHttpRequest: XMLHttpRequest,
+ Base64: Base64,
+ MD5: MD5
+};
+
+exports.Strophe = window;
+
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function (obj /*, arg1, arg2, ... */)
+ {
+ var func = this;
+ var _slice = Array.prototype.slice;
+ var _concat = Array.prototype.concat;
+ var _args = _slice.call(arguments, 1);
+
+ return function () {
+ return func.apply(obj ? obj : this,
+ _concat.call(_args,
+ _slice.call(arguments, 0)));
+ };
+ };
+}
+
+/** PrivateFunction: Array.prototype.indexOf
+ * Return the index of an object in an array.
+ *
+ * This function is not supplied by some JavaScript implementations, so
+ * we provide it if it is missing. This code is from:
+ * http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf
+ *
+ * Parameters:
+ * (Object) elt - The object to look for.
+ * (Integer) from - The index from which to start looking. (optional).
+ *
+ * Returns:
+ * The index of elt in the array or -1 if not found.
+ */
+if (!Array.prototype.indexOf)
+{
+ Array.prototype.indexOf = function(elt /*, from*/)
+ {
+ var len = this.length;
+
+ var from = Number(arguments[1]) || 0;
+ from = (from < 0) ? Math.ceil(from) : Math.floor(from);
+ if (from < 0) {
+ from += len;
+ }
+
+ for (; from < len; from++) {
+ if (from in this && this[from] === elt) {
+ return from;
+ }
+ }
+
+ return -1;
+ };
+}
+
+/* All of the Strophe globals are defined in this special function below so
+ * that references to the globals become closures. This will ensure that
+ * on page reload, these references will still be available to callbacks
+ * that are still executing.
+ */
+
+(function (callback) {
+var Strophe;
+
+/** Function: $build
+ * Create a Strophe.Builder.
+ * This is an alias for 'new Strophe.Builder(name, attrs)'.
+ *
+ * Parameters:
+ * (String) name - The root element name.
+ * (Object) attrs - The attributes for the root element in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $build(name, attrs) { return new Strophe.Builder(name, attrs); }
+/** Function: $msg
+ * Create a Strophe.Builder with a element as the root.
+ *
+ * Parmaeters:
+ * (Object) attrs - The element attributes in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $msg(attrs) { return new Strophe.Builder("message", attrs); }
+/** Function: $iq
+ * Create a Strophe.Builder with an element as the root.
+ *
+ * Parameters:
+ * (Object) attrs - The element attributes in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $iq(attrs) { return new Strophe.Builder("iq", attrs); }
+/** Function: $pres
+ * Create a Strophe.Builder with a element as the root.
+ *
+ * Parameters:
+ * (Object) attrs - The element attributes in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder object.
+ */
+function $pres(attrs) { return new Strophe.Builder("presence", attrs); }
+
+/** Class: Strophe
+ * An object container for all Strophe library functions.
+ *
+ * This class is just a container for all the objects and constants
+ * used in the library. It is not meant to be instantiated, but to
+ * provide a namespace for library objects, constants, and functions.
+ */
+Strophe = {
+ /** Constant: VERSION
+ * The version of the Strophe library. Unreleased builds will have
+ * a version of head-HASH where HASH is a partial revision.
+ */
+ VERSION: "@VERSION@",
+
+ /** Constants: XMPP Namespace Constants
+ * Common namespace constants from the XMPP RFCs and XEPs.
+ *
+ * NS.HTTPBIND - HTTP BIND namespace from XEP 124.
+ * NS.BOSH - BOSH namespace from XEP 206.
+ * NS.CLIENT - Main XMPP client namespace.
+ * NS.AUTH - Legacy authentication namespace.
+ * NS.ROSTER - Roster operations namespace.
+ * NS.PROFILE - Profile namespace.
+ * NS.DISCO_INFO - Service discovery info namespace from XEP 30.
+ * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30.
+ * NS.MUC - Multi-User Chat namespace from XEP 45.
+ * NS.SASL - XMPP SASL namespace from RFC 3920.
+ * NS.STREAM - XMPP Streams namespace from RFC 3920.
+ * NS.BIND - XMPP Binding namespace from RFC 3920.
+ * NS.SESSION - XMPP Session namespace from RFC 3920.
+ */
+ NS: {
+ HTTPBIND: "http://jabber.org/protocol/httpbind",
+ BOSH: "urn:xmpp:xbosh",
+ CLIENT: "jabber:client",
+ AUTH: "jabber:iq:auth",
+ ROSTER: "jabber:iq:roster",
+ PROFILE: "jabber:iq:profile",
+ DISCO_INFO: "http://jabber.org/protocol/disco#info",
+ DISCO_ITEMS: "http://jabber.org/protocol/disco#items",
+ MUC: "http://jabber.org/protocol/muc",
+ SASL: "urn:ietf:params:xml:ns:xmpp-sasl",
+ STREAM: "http://etherx.jabber.org/streams",
+ BIND: "urn:ietf:params:xml:ns:xmpp-bind",
+ SESSION: "urn:ietf:params:xml:ns:xmpp-session",
+ VERSION: "jabber:iq:version",
+ STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas"
+ },
+
+ /** Function: addNamespace
+ * This function is used to extend the current namespaces in
+ * Strophe.NS. It takes a key and a value with the key being the
+ * name of the new namespace, with its actual value.
+ * For example:
+ * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub");
+ *
+ * Parameters:
+ * (String) name - The name under which the namespace will be
+ * referenced under Strophe.NS
+ * (String) value - The actual namespace.
+ */
+ addNamespace: function (name, value)
+ {
+ Strophe.NS[name] = value;
+ },
+
+ /** Constants: Connection Status Constants
+ * Connection status constants for use by the connection handler
+ * callback.
+ *
+ * Status.ERROR - An error has occurred
+ * Status.CONNECTING - The connection is currently being made
+ * Status.CONNFAIL - The connection attempt failed
+ * Status.AUTHENTICATING - The connection is authenticating
+ * Status.AUTHFAIL - The authentication attempt failed
+ * Status.CONNECTED - The connection has succeeded
+ * Status.DISCONNECTED - The connection has been terminated
+ * Status.DISCONNECTING - The connection is currently being terminated
+ * Status.ATTACHED - The connection has been attached
+ */
+ Status: {
+ ERROR: 0,
+ CONNECTING: 1,
+ CONNFAIL: 2,
+ AUTHENTICATING: 3,
+ AUTHFAIL: 4,
+ CONNECTED: 5,
+ DISCONNECTED: 6,
+ DISCONNECTING: 7,
+ ATTACHED: 8
+ },
+
+ /** Constants: Log Level Constants
+ * Logging level indicators.
+ *
+ * LogLevel.DEBUG - Debug output
+ * LogLevel.INFO - Informational output
+ * LogLevel.WARN - Warnings
+ * LogLevel.ERROR - Errors
+ * LogLevel.FATAL - Fatal errors
+ */
+ LogLevel: {
+ DEBUG: 0,
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3,
+ FATAL: 4
+ },
+
+ /** PrivateConstants: DOM Element Type Constants
+ * DOM element types.
+ *
+ * ElementType.NORMAL - Normal element.
+ * ElementType.TEXT - Text data element.
+ */
+ ElementType: {
+ NORMAL: 1,
+ TEXT: 3
+ },
+
+ /** PrivateConstants: Timeout Values
+ * Timeout values for error states. These values are in seconds.
+ * These should not be changed unless you know exactly what you are
+ * doing.
+ *
+ * TIMEOUT - Timeout multiplier. A waiting request will be considered
+ * failed after Math.floor(TIMEOUT * wait) seconds have elapsed.
+ * This defaults to 1.1, and with default wait, 66 seconds.
+ * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where
+ * Strophe can detect early failure, it will consider the request
+ * failed if it doesn't return after
+ * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed.
+ * This defaults to 0.1, and with default wait, 6 seconds.
+ */
+ TIMEOUT: 1.1,
+ SECONDARY_TIMEOUT: 0.1,
+
+ /** Function: forEachChild
+ * Map a function over some or all child elements of a given element.
+ *
+ * This is a small convenience function for mapping a function over
+ * some or all of the children of an element. If elemName is null, all
+ * children will be passed to the function, otherwise only children
+ * whose tag names match elemName will be passed.
+ *
+ * Parameters:
+ * (XMLElement) elem - The element to operate on.
+ * (String) elemName - The child element tag name filter.
+ * (Function) func - The function to apply to each child. This
+ * function should take a single argument, a DOM element.
+ */
+ forEachChild: function (elem, elemName, func)
+ {
+ var i, childNode;
+
+ for (i = 0; i < elem._childNodes.length; i++) {
+ childNode = elem._childNodes[i];
+ if (childNode.nodeType == Strophe.ElementType.NORMAL &&
+ (!elemName || this.isTagEqual(childNode, elemName))) {
+ func(childNode);
+ }
+ }
+ },
+
+ /** Function: isTagEqual
+ * Compare an element's tag name with a string.
+ *
+ * This function is case insensitive.
+ *
+ * Parameters:
+ * (XMLElement) el - A DOM element.
+ * (String) name - The element name.
+ *
+ * Returns:
+ * true if the element's tag name matches _el_, and false
+ * otherwise.
+ */
+ isTagEqual: function (el, name)
+ {
+ return el.tagName.toLowerCase() == name.toLowerCase();
+ },
+
+ /** PrivateVariable: _xmlGenerator
+ * _Private_ variable that caches a DOM document to
+ * generate elements.
+ */
+ _xmlGenerator: null,
+
+ /** PrivateFunction: _makeGenerator
+ * _Private_ function that creates a dummy XML DOM document to serve as
+ * an element and text node generator.
+ */
+ _makeGenerator: function () {
+ var doc;
+
+ if (window.ActiveXObject) {
+ doc = this._getIEXmlDom();
+ doc.appendChild(doc.createElement('strophe'));
+ } else {
+ doc = document.implementation
+ .createDocument('jabber:client', 'strophe', null);
+ }
+
+ return doc;
+ },
+
+ /** Function: xmlGenerator
+ * Get the DOM document to generate elements.
+ *
+ * Returns:
+ * The currently used DOM document.
+ */
+ xmlGenerator: function () {
+ if (!Strophe._xmlGenerator) {
+ Strophe._xmlGenerator = Strophe._makeGenerator();
+ }
+ return Strophe._xmlGenerator;
+ },
+
+ /** PrivateFunction: _getIEXmlDom
+ * Gets IE xml doc object
+ *
+ * Returns:
+ * A Microsoft XML DOM Object
+ * See Also:
+ * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx
+ */
+ _getIEXmlDom : function() {
+ var doc = null;
+ var docStrings = [
+ "Msxml2.DOMDocument.6.0",
+ "Msxml2.DOMDocument.5.0",
+ "Msxml2.DOMDocument.4.0",
+ "MSXML2.DOMDocument.3.0",
+ "MSXML2.DOMDocument",
+ "MSXML.DOMDocument",
+ "Microsoft.XMLDOM"
+ ];
+
+ for (var d = 0; d < docStrings.length; d++) {
+ if (doc === null) {
+ try {
+ doc = new ActiveXObject(docStrings[d]);
+ } catch (e) {
+ doc = null;
+ }
+ } else {
+ break;
+ }
+ }
+
+ return doc;
+ },
+
+ /** Function: xmlElement
+ * Create an XML DOM element.
+ *
+ * This function creates an XML DOM element correctly across all
+ * implementations. Note that these are not HTML DOM elements, which
+ * aren't appropriate for XMPP stanzas.
+ *
+ * Parameters:
+ * (String) name - The name for the element.
+ * (Array|Object) attrs - An optional array or object containing
+ * key/value pairs to use as element attributes. The object should
+ * be in the format {'key': 'value'} or {key: 'value'}. The array
+ * should have the format [['key1', 'value1'], ['key2', 'value2']].
+ * (String) text - The text child data for the element.
+ *
+ * Returns:
+ * A new XML DOM element.
+ */
+ xmlElement: function (name)
+ {
+ if (!name) { return null; }
+
+ var node = Strophe.xmlGenerator().createElement(name);
+
+ // FIXME: this should throw errors if args are the wrong type or
+ // there are more than two optional args
+ var a, i, k;
+ for (a = 1; a < arguments.length; a++) {
+ if (!arguments[a]) { continue; }
+ if (typeof(arguments[a]) == "string" ||
+ typeof(arguments[a]) == "number") {
+ node.appendChild(Strophe.xmlTextNode(arguments[a]));
+ } else if (typeof(arguments[a]) == "object" &&
+ typeof(arguments[a].sort) == "function") {
+ for (i = 0; i < arguments[a].length; i++) {
+ if (typeof(arguments[a][i]) == "object" &&
+ typeof(arguments[a][i].sort) == "function") {
+ node.setAttribute(arguments[a][i][0],
+ arguments[a][i][1]);
+ }
+ }
+ } else if (typeof(arguments[a]) == "object") {
+ for (k in arguments[a]) {
+ if (arguments[a].hasOwnProperty(k)) {
+ node.setAttribute(k, arguments[a][k]);
+ }
+ }
+ }
+ }
+
+ return node;
+ },
+
+ /* Function: xmlescape
+ * Excapes invalid xml characters.
+ *
+ * Parameters:
+ * (String) text - text to escape.
+ *
+ * Returns:
+ * Escaped text.
+ */
+ xmlescape: function(text)
+ {
+ text = text.replace(/\&/g, "&");
+ text = text.replace(//g, ">");
+ return text;
+ },
+
+ /** Function: xmlTextNode
+ * Creates an XML DOM text node.
+ *
+ * Provides a cross implementation version of document.createTextNode.
+ *
+ * Parameters:
+ * (String) text - The content of the text node.
+ *
+ * Returns:
+ * A new XML DOM text node.
+ */
+ xmlTextNode: function (text)
+ {
+ //ensure text is escaped
+ text = Strophe.xmlescape(text);
+
+ return Strophe.xmlGenerator().createTextNode(text);
+ },
+
+ /** Function: getText
+ * Get the concatenation of all text children of an element.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * A String with the concatenated text of all text element children.
+ */
+ getText: function (elem)
+ {
+ if (!elem) { return null; }
+
+ var str = "";
+ if (elem._childNodes.length === 0 && elem.nodeType ==
+ Strophe.ElementType.TEXT) {
+ str += elem.nodeValue;
+ }
+
+ for (var i = 0; i < elem._childNodes.length; i++) {
+ if (elem._childNodes[i].nodeType == Strophe.ElementType.TEXT) {
+ str += elem._childNodes[i].nodeValue;
+ }
+ }
+
+ return str;
+ },
+
+ /** Function: copyElement
+ * Copy an XML DOM element.
+ *
+ * This function copies a DOM element and all its descendants and returns
+ * the new copy.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * A new, copied DOM element tree.
+ */
+ copyElement: function (elem)
+ {
+ var i, el;
+ if (elem.nodeType == Strophe.ElementType.NORMAL) {
+ el = Strophe.xmlElement(elem.tagName);
+
+ for (i = 0; i < elem.attributes.length; i++) {
+ el.setAttribute(elem.attributes[i].nodeName.toLowerCase(),
+ elem.attributes[i].value);
+ }
+
+ for (i = 0; i < elem._childNodes.length; i++) {
+ el.appendChild(Strophe.copyElement(elem._childNodes[i]));
+ }
+ } else if (elem.nodeType == Strophe.ElementType.TEXT) {
+ el = Strophe.xmlTextNode(elem.nodeValue);
+ }
+
+ return el;
+ },
+
+ /** Function: escapeNode
+ * Escape the node part (also called local part) of a JID.
+ *
+ * Parameters:
+ * (String) node - A node (or local part).
+ *
+ * Returns:
+ * An escaped node (or local part).
+ */
+ escapeNode: function (node)
+ {
+ return node.replace(/^\s+|\s+$/g, '')
+ .replace(/\\/g, "\\5c")
+ .replace(/ /g, "\\20")
+ .replace(/\"/g, "\\22")
+ .replace(/\&/g, "\\26")
+ .replace(/\'/g, "\\27")
+ .replace(/\//g, "\\2f")
+ .replace(/:/g, "\\3a")
+ .replace(//g, "\\3e")
+ .replace(/@/g, "\\40");
+ },
+
+ /** Function: unescapeNode
+ * Unescape a node part (also called local part) of a JID.
+ *
+ * Parameters:
+ * (String) node - A node (or local part).
+ *
+ * Returns:
+ * An unescaped node (or local part).
+ */
+ unescapeNode: function (node)
+ {
+ return node.replace(/\\20/g, " ")
+ .replace(/\\22/g, '"')
+ .replace(/\\26/g, "&")
+ .replace(/\\27/g, "'")
+ .replace(/\\2f/g, "/")
+ .replace(/\\3a/g, ":")
+ .replace(/\\3c/g, "<")
+ .replace(/\\3e/g, ">")
+ .replace(/\\40/g, "@")
+ .replace(/\\5c/g, "\\");
+ },
+
+ /** Function: getNodeFromJid
+ * Get the node portion of a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the node.
+ */
+ getNodeFromJid: function (jid)
+ {
+ if (jid.indexOf("@") < 0) { return null; }
+ return jid.split("@")[0];
+ },
+
+ /** Function: getDomainFromJid
+ * Get the domain portion of a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the domain.
+ */
+ getDomainFromJid: function (jid)
+ {
+ var bare = Strophe.getBareJidFromJid(jid);
+ if (bare.indexOf("@") < 0) {
+ return bare;
+ } else {
+ var parts = bare.split("@");
+ parts.splice(0, 1);
+ return parts.join('@');
+ }
+ },
+
+ /** Function: getResourceFromJid
+ * Get the resource portion of a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the resource.
+ */
+ getResourceFromJid: function (jid)
+ {
+ var s = jid.split("/");
+ if (s.length < 2) { return null; }
+ s.splice(0, 1);
+ return s.join('/');
+ },
+
+ /** Function: getBareJidFromJid
+ * Get the bare JID from a JID String.
+ *
+ * Parameters:
+ * (String) jid - A JID.
+ *
+ * Returns:
+ * A String containing the bare JID.
+ */
+ getBareJidFromJid: function (jid)
+ {
+ return jid ? jid.split("/")[0] : null;
+ },
+
+ /** Function: log
+ * User overrideable logging function.
+ *
+ * This function is called whenever the Strophe library calls any
+ * of the logging functions. The default implementation of this
+ * function does nothing. If client code wishes to handle the logging
+ * messages, it should override this with
+ * > Strophe.log = function (level, msg) {
+ * > (user code here)
+ * > };
+ *
+ * Please note that data sent and received over the wire is logged
+ * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput().
+ *
+ * The different levels and their meanings are
+ *
+ * DEBUG - Messages useful for debugging purposes.
+ * INFO - Informational messages. This is mostly information like
+ * 'disconnect was called' or 'SASL auth succeeded'.
+ * WARN - Warnings about potential problems. This is mostly used
+ * to report transient connection errors like request timeouts.
+ * ERROR - Some error occurred.
+ * FATAL - A non-recoverable fatal error occurred.
+ *
+ * Parameters:
+ * (Integer) level - The log level of the log message. This will
+ * be one of the values in Strophe.LogLevel.
+ * (String) msg - The log message.
+ */
+ log: function (level, msg)
+ {
+ return;
+ },
+
+ /** Function: debug
+ * Log a message at the Strophe.LogLevel.DEBUG level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ debug: function(msg)
+ {
+ this.log(this.LogLevel.DEBUG, msg);
+ },
+
+ /** Function: info
+ * Log a message at the Strophe.LogLevel.INFO level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ info: function (msg)
+ {
+ this.log(this.LogLevel.INFO, msg);
+ },
+
+ /** Function: warn
+ * Log a message at the Strophe.LogLevel.WARN level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ warn: function (msg)
+ {
+ this.log(this.LogLevel.WARN, msg);
+ },
+
+ /** Function: error
+ * Log a message at the Strophe.LogLevel.ERROR level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ error: function (msg)
+ {
+ this.log(this.LogLevel.ERROR, msg);
+ },
+
+ /** Function: fatal
+ * Log a message at the Strophe.LogLevel.FATAL level.
+ *
+ * Parameters:
+ * (String) msg - The log message.
+ */
+ fatal: function (msg)
+ {
+ this.log(this.LogLevel.FATAL, msg);
+ },
+
+ /** Function: serialize
+ * Render a DOM element and all descendants to a String.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * The serialized element tree as a String.
+ */
+ serialize: function (elem)
+ {
+ var result;
+
+ if (!elem) { return null; }
+
+ if (typeof(elem.tree) === "function") {
+ elem = elem.tree();
+ }
+
+ var nodeName = elem.nodeName.toLowerCase();
+ var i, child;
+
+ if (elem.getAttribute("_realname")) {
+ nodeName = elem.getAttribute("_realname").toLowerCase();
+ }
+
+ result = "<" + nodeName.toLowerCase();
+ for (i = 0; i < elem.attributes.length; i++) {
+ if(elem.attributes[i].nodeName.toLowerCase() != "_realname") {
+ result += " " + elem.attributes[i].nodeName.toLowerCase() +
+ "='" + elem.attributes[i].value
+ .replace(/&/g, "&")
+ .replace(/\'/g, "'")
+ .replace(/ 0) {
+ result += ">";
+ for (i = 0; i < elem._childNodes.length; i++) {
+ child = elem._childNodes[i];
+ if (child.nodeType == Strophe.ElementType.NORMAL) {
+ // normal element, so recurse
+ result += Strophe.serialize(child);
+ } else if (child.nodeType == Strophe.ElementType.TEXT) {
+ // text element
+ result += child.nodeValue;
+ }
+ }
+ result += "" + nodeName.toLowerCase() + ">";
+ } else {
+ result += "/>";
+ }
+
+ return result;
+ },
+
+ /** PrivateVariable: _requestId
+ * _Private_ variable that keeps track of the request ids for
+ * connections.
+ */
+ _requestId: 0,
+
+ /** PrivateVariable: Strophe.connectionPlugins
+ * _Private_ variable Used to store plugin names that need
+ * initialization on Strophe.Connection construction.
+ */
+ _connectionPlugins: {},
+
+ /** Function: addConnectionPlugin
+ * Extends the Strophe.Connection object with the given plugin.
+ *
+ * Paramaters:
+ * (String) name - The name of the extension.
+ * (Object) ptype - The plugin's prototype.
+ */
+ addConnectionPlugin: function (name, ptype)
+ {
+ Strophe._connectionPlugins[name] = ptype;
+ }
+};
+
+/** Class: Strophe.Builder
+ * XML DOM builder.
+ *
+ * This object provides an interface similar to JQuery but for building
+ * DOM element easily and rapidly. All the functions except for toString()
+ * and tree() return the object, so calls can be chained. Here's an
+ * example using the $iq() builder helper.
+ * > $iq({to: 'you', from: 'me', type: 'get', id: '1'})
+ * > .c('query', {xmlns: 'strophe:example'})
+ * > .c('example')
+ * > .toString()
+ * The above generates this XML fragment
+ * >
+ * >
+ * >
+ * >
+ * >
+ * The corresponding DOM manipulations to get a similar fragment would be
+ * a lot more tedious and probably involve several helper variables.
+ *
+ * Since adding children makes new operations operate on the child, up()
+ * is provided to traverse up the tree. To add two children, do
+ * > builder.c('child1', ...).up().c('child2', ...)
+ * The next operation on the Builder will be relative to the second child.
+ */
+
+/** Constructor: Strophe.Builder
+ * Create a Strophe.Builder object.
+ *
+ * The attributes should be passed in object notation. For example
+ * > var b = new Builder('message', {to: 'you', from: 'me'});
+ * or
+ * > var b = new Builder('messsage', {'xml:lang': 'en'});
+ *
+ * Parameters:
+ * (String) name - The name of the root element.
+ * (Object) attrs - The attributes for the root element in object notation.
+ *
+ * Returns:
+ * A new Strophe.Builder.
+ */
+Strophe.Builder = function (name, attrs)
+{
+ // Set correct namespace for jabber:client elements
+ if (name == "presence" || name == "message" || name == "iq") {
+ if (attrs && !attrs.xmlns) {
+ attrs.xmlns = Strophe.NS.CLIENT;
+ } else if (!attrs) {
+ attrs = {xmlns: Strophe.NS.CLIENT};
+ }
+ }
+
+ // Holds the tree being built.
+ this.nodeTree = Strophe.xmlElement(name, attrs);
+
+ // Points to the current operation node.
+ this.node = this.nodeTree;
+};
+
+Strophe.Builder.prototype = {
+ /** Function: tree
+ * Return the DOM tree.
+ *
+ * This function returns the current DOM tree as an element object. This
+ * is suitable for passing to functions like Strophe.Connection.send().
+ *
+ * Returns:
+ * The DOM tree as a element object.
+ */
+ tree: function ()
+ {
+ return this.nodeTree;
+ },
+
+ /** Function: toString
+ * Serialize the DOM tree to a String.
+ *
+ * This function returns a string serialization of the current DOM
+ * tree. It is often used internally to pass data to a
+ * Strophe.Request object.
+ *
+ * Returns:
+ * The serialized DOM tree in a String.
+ */
+ toString: function ()
+ {
+ return Strophe.serialize(this.nodeTree);
+ },
+
+ /** Function: up
+ * Make the current parent element the new current element.
+ *
+ * This function is often used after c() to traverse back up the tree.
+ * For example, to add two children to the same element
+ * > builder.c('child1', {}).up().c('child2', {});
+ *
+ * Returns:
+ * The Stophe.Builder object.
+ */
+ up: function ()
+ {
+ this.node = this.node.parentNode;
+ return this;
+ },
+
+ /** Function: attrs
+ * Add or modify attributes of the current element.
+ *
+ * The attributes should be passed in object notation. This function
+ * does not move the current element pointer.
+ *
+ * Parameters:
+ * (Object) moreattrs - The attributes to add/modify in object notation.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ attrs: function (moreattrs)
+ {
+ for (var k in moreattrs) {
+ if (moreattrs.hasOwnProperty(k)) {
+ this.node.setAttribute(k, moreattrs[k]);
+ }
+ }
+ return this;
+ },
+
+ /** Function: c
+ * Add a child to the current element and make it the new current
+ * element.
+ *
+ * This function moves the current element pointer to the child. If you
+ * need to add another child, it is necessary to use up() to go back
+ * to the parent in the tree.
+ *
+ * Parameters:
+ * (String) name - The name of the child.
+ * (Object) attrs - The attributes of the child in object notation.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ c: function (name, attrs)
+ {
+ var child = Strophe.xmlElement(name, attrs);
+ this.node.appendChild(child);
+ this.node = child;
+ return this;
+ },
+
+ /** Function: cnode
+ * Add a child to the current element and make it the new current
+ * element.
+ *
+ * This function is the same as c() except that instead of using a
+ * name and an attributes object to create the child it uses an
+ * existing DOM element object.
+ *
+ * Parameters:
+ * (XMLElement) elem - A DOM element.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ cnode: function (elem)
+ {
+ var xmlGen = Strophe.xmlGenerator();
+ var newElem = xmlGen.importNode ? xmlGen.importNode(elem, true) : Strophe.copyElement(elem);
+ this.node.appendChild(newElem);
+ this.node = newElem;
+ return this;
+ },
+
+ /** Function: t
+ * Add a child text element.
+ *
+ * This *does not* make the child the new current element since there
+ * are no children of text elements.
+ *
+ * Parameters:
+ * (String) text - The text data to append to the current element.
+ *
+ * Returns:
+ * The Strophe.Builder object.
+ */
+ t: function (text)
+ {
+ var child = Strophe.xmlTextNode(text);
+ this.node.appendChild(child);
+ return this;
+ }
+};
+
+
+/** PrivateClass: Strophe.Handler
+ * _Private_ helper class for managing stanza handlers.
+ *
+ * A Strophe.Handler encapsulates a user provided callback function to be
+ * executed when matching stanzas are received by the connection.
+ * Handlers can be either one-off or persistant depending on their
+ * return value. Returning true will cause a Handler to remain active, and
+ * returning false will remove the Handler.
+ *
+ * Users will not use Strophe.Handler objects directly, but instead they
+ * will use Strophe.Connection.addHandler() and
+ * Strophe.Connection.deleteHandler().
+ */
+
+/** PrivateConstructor: Strophe.Handler
+ * Create and initialize a new Strophe.Handler.
+ *
+ * Parameters:
+ * (Function) handler - A function to be executed when the handler is run.
+ * (String) ns - The namespace to match.
+ * (String) name - The element name to match.
+ * (String) type - The element type to match.
+ * (String) id - The element id attribute to match.
+ * (String) from - The element from attribute to match.
+ * (Object) options - Handler options
+ *
+ * Returns:
+ * A new Strophe.Handler object.
+ */
+Strophe.Handler = function (handler, ns, name, type, id, from, options)
+{
+ this.handler = handler;
+ this.ns = ns;
+ this.name = name;
+ this.type = type;
+ this.id = id;
+ this.options = options || {matchbare: false};
+
+ // default matchBare to false if undefined
+ if (!this.options.matchBare) {
+ this.options.matchBare = false;
+ }
+
+ if (this.options.matchBare) {
+ this.from = from ? Strophe.getBareJidFromJid(from) : null;
+ } else {
+ this.from = from;
+ }
+
+ // whether the handler is a user handler or a system handler
+ this.user = true;
+};
+
+Strophe.Handler.prototype = {
+ /** PrivateFunction: isMatch
+ * Tests if a stanza matches the Strophe.Handler.
+ *
+ * Parameters:
+ * (XMLElement) elem - The XML element to test.
+ *
+ * Returns:
+ * true if the stanza matches and false otherwise.
+ */
+ isMatch: function (elem)
+ {
+ var nsMatch;
+ var from = null;
+
+ if (this.options.matchBare) {
+ from = Strophe.getBareJidFromJid(elem.getAttribute('from'));
+ } else {
+ from = elem.getAttribute('from');
+ }
+
+ nsMatch = false;
+ if (!this.ns) {
+ nsMatch = true;
+ } else {
+ var that = this;
+ Strophe.forEachChild(elem, null, function (elem) {
+ if (elem.getAttribute("xmlns") == that.ns) {
+ nsMatch = true;
+ }
+ });
+
+ nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns;
+ }
+
+ if (nsMatch &&
+ (!this.name || Strophe.isTagEqual(elem, this.name)) &&
+ (!this.type || elem.getAttribute("type") == this.type) &&
+ (!this.id || elem.getAttribute("id") == this.id) &&
+ (!this.from || from == this.from)) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: run
+ * Run the callback on a matching stanza.
+ *
+ * Parameters:
+ * (XMLElement) elem - The DOM element that triggered the
+ * Strophe.Handler.
+ *
+ * Returns:
+ * A boolean indicating if the handler should remain active.
+ */
+ run: function (elem)
+ {
+ var result = null;
+ try {
+ result = this.handler(elem);
+ } catch (e) {
+ if (e.sourceURL) {
+ Strophe.fatal("error: " + this.handler +
+ " " + e.sourceURL + ":" +
+ e.line + " - " + e.name + ": " + e.message);
+ } else if (e.fileName) {
+ if (typeof(console) != "undefined") {
+ console.trace();
+ console.error(this.handler, " - error - ", e, e.message);
+ }
+ Strophe.fatal("error: " + this.handler + " " +
+ e.fileName + ":" + e.lineNumber + " - " +
+ e.name + ": " + e.message);
+ } else {
+ Strophe.fatal("error: " + this.handler);
+ }
+
+ throw e;
+ }
+
+ return result;
+ },
+
+ /** PrivateFunction: toString
+ * Get a String representation of the Strophe.Handler object.
+ *
+ * Returns:
+ * A String.
+ */
+ toString: function ()
+ {
+ return "{Handler: " + this.handler + "(" + this.name + "," +
+ this.id + "," + this.ns + ")}";
+ }
+};
+
+/** PrivateClass: Strophe.TimedHandler
+ * _Private_ helper class for managing timed handlers.
+ *
+ * A Strophe.TimedHandler encapsulates a user provided callback that
+ * should be called after a certain period of time or at regular
+ * intervals. The return value of the callback determines whether the
+ * Strophe.TimedHandler will continue to fire.
+ *
+ * Users will not use Strophe.TimedHandler objects directly, but instead
+ * they will use Strophe.Connection.addTimedHandler() and
+ * Strophe.Connection.deleteTimedHandler().
+ */
+
+/** PrivateConstructor: Strophe.TimedHandler
+ * Create and initialize a new Strophe.TimedHandler object.
+ *
+ * Parameters:
+ * (Integer) period - The number of milliseconds to wait before the
+ * handler is called.
+ * (Function) handler - The callback to run when the handler fires. This
+ * function should take no arguments.
+ *
+ * Returns:
+ * A new Strophe.TimedHandler object.
+ */
+Strophe.TimedHandler = function (period, handler)
+{
+ this.period = period;
+ this.handler = handler;
+
+ this.lastCalled = new Date().getTime();
+ this.user = true;
+};
+
+Strophe.TimedHandler.prototype = {
+ /** PrivateFunction: run
+ * Run the callback for the Strophe.TimedHandler.
+ *
+ * Returns:
+ * true if the Strophe.TimedHandler should be called again, and false
+ * otherwise.
+ */
+ run: function ()
+ {
+ this.lastCalled = new Date().getTime();
+ return this.handler();
+ },
+
+ /** PrivateFunction: reset
+ * Reset the last called time for the Strophe.TimedHandler.
+ */
+ reset: function ()
+ {
+ this.lastCalled = new Date().getTime();
+ },
+
+ /** PrivateFunction: toString
+ * Get a string representation of the Strophe.TimedHandler object.
+ *
+ * Returns:
+ * The string representation.
+ */
+ toString: function ()
+ {
+ return "{TimedHandler: " + this.handler + "(" + this.period +")}";
+ }
+};
+
+/** PrivateClass: Strophe.Request
+ * _Private_ helper class that provides a cross implementation abstraction
+ * for a BOSH related XMLHttpRequest.
+ *
+ * The Strophe.Request class is used internally to encapsulate BOSH request
+ * information. It is not meant to be used from user's code.
+ */
+
+/** PrivateConstructor: Strophe.Request
+ * Create and initialize a new Strophe.Request object.
+ *
+ * Parameters:
+ * (XMLElement) elem - The XML data to be sent in the request.
+ * (Function) func - The function that will be called when the
+ * XMLHttpRequest readyState changes.
+ * (Integer) rid - The BOSH rid attribute associated with this request.
+ * (Integer) sends - The number of times this same request has been
+ * sent.
+ */
+Strophe.Request = function (elem, func, rid, sends)
+{
+ this.id = ++Strophe._requestId;
+ this.xmlData = elem;
+ this.data = Strophe.serialize(elem);
+ // save original function in case we need to make a new request
+ // from this one.
+ this.origFunc = func;
+ this.func = func;
+ this.rid = rid;
+ this.date = NaN;
+ this.sends = sends || 0;
+ this.abort = false;
+ this.dead = null;
+ this.age = function () {
+ if (!this.date) { return 0; }
+ var now = new Date();
+ return (now - this.date) / 1000;
+ };
+ this.timeDead = function () {
+ if (!this.dead) { return 0; }
+ var now = new Date();
+ return (now - this.dead) / 1000;
+ };
+ this.xhr = this._newXHR();
+};
+
+Strophe.Request.prototype = {
+ /** PrivateFunction: getResponse
+ * Get a response from the underlying XMLHttpRequest.
+ *
+ * This function attempts to get a response from the request and checks
+ * for errors.
+ *
+ * Throws:
+ * "parsererror" - A parser error occured.
+ *
+ * Returns:
+ * The DOM element tree of the response.
+ */
+ getResponse: function ()
+ {
+ // console.log("getResponse:", this.xhr.responseXML, ":", this.xhr.responseText);
+
+ var node = null;
+ if (this.xhr.responseXML && this.xhr.responseXML.documentElement) {
+ node = this.xhr.responseXML.documentElement;
+ if (node.tagName == "parsererror") {
+ Strophe.error("invalid response received");
+ Strophe.error("responseText: " + this.xhr.responseText);
+ Strophe.error("responseXML: " +
+ Strophe.serialize(this.xhr.responseXML));
+ throw "parsererror";
+ }
+ } else if (this.xhr.responseText) {
+ // Hack for node.
+ var _div = document.createElement("div");
+ _div.innerHTML = this.xhr.responseText;
+ node = _div._childNodes[0];
+
+ Strophe.error("invalid response received");
+ Strophe.error("responseText: " + this.xhr.responseText);
+ Strophe.error("responseXML: " +
+ Strophe.serialize(this.xhr.responseXML));
+ }
+
+ return node;
+ },
+
+ /** PrivateFunction: _newXHR
+ * _Private_ helper function to create XMLHttpRequests.
+ *
+ * This function creates XMLHttpRequests across all implementations.
+ *
+ * Returns:
+ * A new XMLHttpRequest.
+ */
+ _newXHR: function ()
+ {
+ var xhr = null;
+ if (window.XMLHttpRequest) {
+ xhr = new XMLHttpRequest();
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType("text/xml");
+ }
+ } else if (window.ActiveXObject) {
+ xhr = new ActiveXObject("Microsoft.XMLHTTP");
+ }
+
+ // use Function.bind() to prepend ourselves as an argument
+ xhr.onreadystatechange = this.func.bind(null, this);
+
+ return xhr;
+ }
+};
+
+/** Class: Strophe.Connection
+ * XMPP Connection manager.
+ *
+ * Thie class is the main part of Strophe. It manages a BOSH connection
+ * to an XMPP server and dispatches events to the user callbacks as
+ * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy
+ * authentication.
+ *
+ * After creating a Strophe.Connection object, the user will typically
+ * call connect() with a user supplied callback to handle connection level
+ * events like authentication failure, disconnection, or connection
+ * complete.
+ *
+ * The user will also have several event handlers defined by using
+ * addHandler() and addTimedHandler(). These will allow the user code to
+ * respond to interesting stanzas or do something periodically with the
+ * connection. These handlers will be active once authentication is
+ * finished.
+ *
+ * To send data to the connection, use send().
+ */
+
+/** Constructor: Strophe.Connection
+ * Create and initialize a Strophe.Connection object.
+ *
+ * Parameters:
+ * (String) service - The BOSH service URL.
+ *
+ * Returns:
+ * A new Strophe.Connection object.
+ */
+Strophe.Connection = function (service)
+{
+ /* The path to the httpbind service. */
+ this.service = service;
+ /* The connected JID. */
+ this.jid = "";
+ /* request id for body tags */
+ this.rid = Math.floor(Math.random() * 4294967295);
+ /* The current session ID. */
+ this.sid = null;
+ this.streamId = null;
+ /* stream:features */
+ this.features = null;
+
+ // SASL
+ this.do_session = false;
+ this.do_bind = false;
+
+ // handler lists
+ this.timedHandlers = [];
+ this.handlers = [];
+ this.removeTimeds = [];
+ this.removeHandlers = [];
+ this.addTimeds = [];
+ this.addHandlers = [];
+
+ this._idleTimeout = null;
+ this._disconnectTimeout = null;
+
+ this.authenticated = false;
+ this.disconnecting = false;
+ this.connected = false;
+
+ this.errors = 0;
+
+ this.paused = false;
+
+ // default BOSH values
+ this.hold = 1;
+ this.wait = 60;
+ this.window = 5;
+
+ this._data = [];
+ this._requests = [];
+ this._uniqueId = Math.round(Math.random() * 10000);
+
+ this._sasl_success_handler = null;
+ this._sasl_failure_handler = null;
+ this._sasl_challenge_handler = null;
+
+ // setup onIdle callback every 1/10th of a second
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+
+ // initialize plugins
+ for (var k in Strophe._connectionPlugins) {
+ if (Strophe._connectionPlugins.hasOwnProperty(k)) {
+ var ptype = Strophe._connectionPlugins[k];
+ // jslint complaints about the below line, but this is fine
+ var F = function () {};
+ F.prototype = ptype;
+ this[k] = new F();
+ this[k].init(this);
+ }
+ }
+};
+
+Strophe.Connection.prototype = {
+ /** Function: reset
+ * Reset the connection.
+ *
+ * This function should be called after a connection is disconnected
+ * before that connection is reused.
+ */
+ reset: function ()
+ {
+ this.rid = Math.floor(Math.random() * 4294967295);
+
+ this.sid = null;
+ this.streamId = null;
+
+ // SASL
+ this.do_session = false;
+ this.do_bind = false;
+
+ // handler lists
+ this.timedHandlers = [];
+ this.handlers = [];
+ this.removeTimeds = [];
+ this.removeHandlers = [];
+ this.addTimeds = [];
+ this.addHandlers = [];
+
+ this.authenticated = false;
+ this.disconnecting = false;
+ this.connected = false;
+
+ this.errors = 0;
+
+ this._requests = [];
+ this._uniqueId = Math.round(Math.random()*10000);
+ },
+
+ /** Function: pause
+ * Pause the request manager.
+ *
+ * This will prevent Strophe from sending any more requests to the
+ * server. This is very useful for temporarily pausing while a lot
+ * of send() calls are happening quickly. This causes Strophe to
+ * send the data in a single request, saving many request trips.
+ */
+ pause: function ()
+ {
+ this.paused = true;
+ },
+
+ /** Function: resume
+ * Resume the request manager.
+ *
+ * This resumes after pause() has been called.
+ */
+ resume: function ()
+ {
+ this.paused = false;
+ },
+
+ /** Function: getUniqueId
+ * Generate a unique ID for use in elements.
+ *
+ * All stanzas are required to have unique id attributes. This
+ * function makes creating these easy. Each connection instance has
+ * a counter which starts from zero, and the value of this counter
+ * plus a colon followed by the suffix becomes the unique id. If no
+ * suffix is supplied, the counter is used as the unique id.
+ *
+ * Suffixes are used to make debugging easier when reading the stream
+ * data, and their use is recommended. The counter resets to 0 for
+ * every new connection for the same reason. For connections to the
+ * same server that authenticate the same way, all the ids should be
+ * the same, which makes it easy to see changes. This is useful for
+ * automated testing as well.
+ *
+ * Parameters:
+ * (String) suffix - A optional suffix to append to the id.
+ *
+ * Returns:
+ * A unique string to be used for the id attribute.
+ */
+ getUniqueId: function (suffix)
+ {
+ if (typeof(suffix) == "string" || typeof(suffix) == "number") {
+ return ++this._uniqueId + ":" + suffix;
+ } else {
+ return ++this._uniqueId + "";
+ }
+ },
+
+ /** Function: connect
+ * Starts the connection process.
+ *
+ * As the connection process proceeds, the user supplied callback will
+ * be triggered multiple times with status updates. The callback
+ * should take two arguments - the status code and the error condition.
+ *
+ * The status code will be one of the values in the Strophe.Status
+ * constants. The error condition will be one of the conditions
+ * defined in RFC 3920 or the condition 'strophe-parsererror'.
+ *
+ * Please see XEP 124 for a more detailed explanation of the optional
+ * parameters below.
+ *
+ * Parameters:
+ * (String) jid - The user's JID. This may be a bare JID,
+ * or a full JID. If a node is not supplied, SASL ANONYMOUS
+ * authentication will be attempted.
+ * (String) pass - The user's password.
+ * (Function) callback The connect callback function.
+ * (Integer) wait - The optional HTTPBIND wait value. This is the
+ * time the server will wait before returning an empty result for
+ * a request. The default setting of 60 seconds is recommended.
+ * Other settings will require tweaks to the Strophe.TIMEOUT value.
+ * (Integer) hold - The optional HTTPBIND hold value. This is the
+ * number of connections the server will hold at one time. This
+ * should almost always be set to 1 (the default).
+ */
+ connect: function (jid, pass, callback, wait, hold, route)
+ {
+ this.jid = jid;
+ this.pass = pass;
+ this.connect_callback = callback;
+ this.disconnecting = false;
+ this.connected = false;
+ this.authenticated = false;
+ this.errors = 0;
+
+ this.wait = wait || this.wait;
+ this.hold = hold || this.hold;
+
+ // parse jid for domain and resource
+ this.domain = Strophe.getDomainFromJid(this.jid);
+
+ // build the body tag
+ var body_attrs = {
+ to: this.domain,
+ "xml:lang": "en",
+ wait: this.wait,
+ hold: this.hold,
+ content: "text/xml; charset=utf-8",
+ ver: "1.6",
+ "xmpp:version": "1.0",
+ "xmlns:xmpp": Strophe.NS.BOSH
+ };
+ if (route) {
+ body_attrs.route = route;
+ }
+
+ var body = this._buildBody().attrs(body_attrs);
+
+ this._changeConnectStatus(Strophe.Status.CONNECTING, null);
+
+ this._requests.push(
+ new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._connect_cb.bind(this)),
+ body.tree().getAttribute("rid")));
+ this._throttledRequestHandler();
+ },
+
+ /** Function: attach
+ * Attach to an already created and authenticated BOSH session.
+ *
+ * This function is provided to allow Strophe to attach to BOSH
+ * sessions which have been created externally, perhaps by a Web
+ * application. This is often used to support auto-login type features
+ * without putting user credentials into the page.
+ *
+ * Parameters:
+ * (String) jid - The full JID that is bound by the session.
+ * (String) sid - The SID of the BOSH session.
+ * (String) rid - The current RID of the BOSH session. This RID
+ * will be used by the next request.
+ * (Function) callback The connect callback function.
+ * (Integer) wait - The optional HTTPBIND wait value. This is the
+ * time the server will wait before returning an empty result for
+ * a request. The default setting of 60 seconds is recommended.
+ * Other settings will require tweaks to the Strophe.TIMEOUT value.
+ * (Integer) hold - The optional HTTPBIND hold value. This is the
+ * number of connections the server will hold at one time. This
+ * should almost always be set to 1 (the default).
+ * (Integer) wind - The optional HTTBIND window value. This is the
+ * allowed range of request ids that are valid. The default is 5.
+ */
+ attach: function (jid, sid, rid, callback, wait, hold, wind)
+ {
+ this.jid = jid;
+ this.sid = sid;
+ this.rid = rid;
+ this.connect_callback = callback;
+
+ this.domain = Strophe.getDomainFromJid(this.jid);
+
+ this.authenticated = true;
+ this.connected = true;
+
+ this.wait = wait || this.wait;
+ this.hold = hold || this.hold;
+ this.window = wind || this.window;
+
+ this._changeConnectStatus(Strophe.Status.ATTACHED, null);
+ },
+
+ /** Function: xmlInput
+ * User overrideable function that receives XML data coming into the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.xmlInput = function (elem) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (XMLElement) elem - The XML data received by the connection.
+ */
+ xmlInput: function (elem)
+ {
+ return;
+ },
+
+ /** Function: xmlOutput
+ * User overrideable function that receives XML data sent to the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.xmlOutput = function (elem) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (XMLElement) elem - The XMLdata sent by the connection.
+ */
+ xmlOutput: function (elem)
+ {
+ return;
+ },
+
+ /** Function: rawInput
+ * User overrideable function that receives raw data coming into the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.rawInput = function (data) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (String) data - The data received by the connection.
+ */
+ rawInput: function (data)
+ {
+ return;
+ },
+
+ /** Function: rawOutput
+ * User overrideable function that receives raw data sent to the
+ * connection.
+ *
+ * The default function does nothing. User code can override this with
+ * > Strophe.Connection.rawOutput = function (data) {
+ * > (user code)
+ * > };
+ *
+ * Parameters:
+ * (String) data - The data sent by the connection.
+ */
+ rawOutput: function (data)
+ {
+ return;
+ },
+
+ /** Function: send
+ * Send a stanza.
+ *
+ * This function is called to push data onto the send queue to
+ * go out over the wire. Whenever a request is sent to the BOSH
+ * server, all pending data is sent and the queue is flushed.
+ *
+ * Parameters:
+ * (XMLElement |
+ * [XMLElement] |
+ * Strophe.Builder) elem - The stanza to send.
+ */
+ send: function (elem)
+ {
+ if (elem === null) { return ; }
+ if (typeof(elem.sort) === "function") {
+ for (var i = 0; i < elem.length; i++) {
+ this._queueData(elem[i]);
+ }
+ } else if (typeof(elem.tree) === "function") {
+ this._queueData(elem.tree());
+ } else {
+ this._queueData(elem);
+ }
+
+ this._throttledRequestHandler();
+ clearTimeout(this._idleTimeout);
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+ },
+
+ /** Function: flush
+ * Immediately send any pending outgoing data.
+ *
+ * Normally send() queues outgoing data until the next idle period
+ * (100ms), which optimizes network use in the common cases when
+ * several send()s are called in succession. flush() can be used to
+ * immediately send all pending data.
+ */
+ flush: function ()
+ {
+ // cancel the pending idle period and run the idle function
+ // immediately
+ clearTimeout(this._idleTimeout);
+ this._onIdle();
+ },
+
+ /** Function: sendIQ
+ * Helper function to send IQ stanzas.
+ *
+ * Parameters:
+ * (XMLElement) elem - The stanza to send.
+ * (Function) callback - The callback function for a successful request.
+ * (Function) errback - The callback function for a failed or timed
+ * out request. On timeout, the stanza will be null.
+ * (Integer) timeout - The time specified in milliseconds for a
+ * timeout to occur.
+ *
+ * Returns:
+ * The id used to send the IQ.
+ */
+ sendIQ: function(elem, callback, errback, timeout) {
+ var timeoutHandler = null;
+ var that = this;
+
+ if (typeof(elem.tree) === "function") {
+ elem = elem.tree();
+ }
+ var id = elem.getAttribute('id');
+
+ // inject id if not found
+ if (!id) {
+ id = this.getUniqueId("sendIQ");
+ elem.setAttribute("id", id);
+ }
+
+ var handler = this.addHandler(function (stanza) {
+ // remove timeout handler if there is one
+ if (timeoutHandler) {
+ that.deleteTimedHandler(timeoutHandler);
+ }
+
+ var iqtype = stanza.getAttribute('type');
+ if (iqtype == 'result') {
+ if (callback) {
+ callback(stanza);
+ }
+ } else if (iqtype == 'error') {
+ if (errback) {
+ errback(stanza);
+ }
+ } else {
+ throw {
+ name: "StropheError",
+ message: "Got bad IQ type of " + iqtype
+ };
+ }
+ }, null, 'iq', null, id);
+
+ // if timeout specified, setup timeout handler.
+ if (timeout) {
+ timeoutHandler = this.addTimedHandler(timeout, function () {
+ // get rid of normal handler
+ that.deleteHandler(handler);
+
+ // call errback on timeout with null stanza
+ if (errback) {
+ errback(null);
+ }
+ return false;
+ });
+ }
+
+ this.send(elem);
+
+ return id;
+ },
+
+ /** PrivateFunction: _queueData
+ * Queue outgoing data for later sending. Also ensures that the data
+ * is a DOMElement.
+ */
+ _queueData: function (element) {
+ if (element === null ||
+ !element.tagName ||
+ !element._childNodes) {
+ throw {
+ name: "StropheError",
+ message: "Cannot queue non-DOMElement."
+ };
+ }
+
+ this._data.push(element);
+ },
+
+ /** PrivateFunction: _sendRestart
+ * Send an xmpp:restart stanza.
+ */
+ _sendRestart: function ()
+ {
+ this._data.push("restart");
+
+ this._throttledRequestHandler();
+ clearTimeout(this._idleTimeout);
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+ },
+
+ /** Function: addTimedHandler
+ * Add a timed handler to the connection.
+ *
+ * This function adds a timed handler. The provided handler will
+ * be called every period milliseconds until it returns false,
+ * the connection is terminated, or the handler is removed. Handlers
+ * that wish to continue being invoked should return true.
+ *
+ * Because of method binding it is necessary to save the result of
+ * this function if you wish to remove a handler with
+ * deleteTimedHandler().
+ *
+ * Note that user handlers are not active until authentication is
+ * successful.
+ *
+ * Parameters:
+ * (Integer) period - The period of the handler.
+ * (Function) handler - The callback function.
+ *
+ * Returns:
+ * A reference to the handler that can be used to remove it.
+ */
+ addTimedHandler: function (period, handler)
+ {
+ var thand = new Strophe.TimedHandler(period, handler);
+ this.addTimeds.push(thand);
+ return thand;
+ },
+
+ /** Function: deleteTimedHandler
+ * Delete a timed handler for a connection.
+ *
+ * This function removes a timed handler from the connection. The
+ * handRef parameter is *not* the function passed to addTimedHandler(),
+ * but is the reference returned from addTimedHandler().
+ *
+ * Parameters:
+ * (Strophe.TimedHandler) handRef - The handler reference.
+ */
+ deleteTimedHandler: function (handRef)
+ {
+ // this must be done in the Idle loop so that we don't change
+ // the handlers during iteration
+ this.removeTimeds.push(handRef);
+ },
+
+ /** Function: addHandler
+ * Add a stanza handler for the connection.
+ *
+ * This function adds a stanza handler to the connection. The
+ * handler callback will be called for any stanza that matches
+ * the parameters. Note that if multiple parameters are supplied,
+ * they must all match for the handler to be invoked.
+ *
+ * The handler will receive the stanza that triggered it as its argument.
+ * The handler should return true if it is to be invoked again;
+ * returning false will remove the handler after it returns.
+ *
+ * As a convenience, the ns parameters applies to the top level element
+ * and also any of its immediate children. This is primarily to make
+ * matching /iq/query elements easy.
+ *
+ * The options argument contains handler matching flags that affect how
+ * matches are determined. Currently the only flag is matchBare (a
+ * boolean). When matchBare is true, the from parameter and the from
+ * attribute on the stanza will be matched as bare JIDs instead of
+ * full JIDs. To use this, pass {matchBare: true} as the value of
+ * options. The default value for matchBare is false.
+ *
+ * The return value should be saved if you wish to remove the handler
+ * with deleteHandler().
+ *
+ * Parameters:
+ * (Function) handler - The user callback.
+ * (String) ns - The namespace to match.
+ * (String) name - The stanza name to match.
+ * (String) type - The stanza type attribute to match.
+ * (String) id - The stanza id attribute to match.
+ * (String) from - The stanza from attribute to match.
+ * (String) options - The handler options
+ *
+ * Returns:
+ * A reference to the handler that can be used to remove it.
+ */
+ addHandler: function (handler, ns, name, type, id, from, options)
+ {
+ var hand = new Strophe.Handler(handler, ns, name, type, id, from, options);
+ this.addHandlers.push(hand);
+ return hand;
+ },
+
+ /** Function: deleteHandler
+ * Delete a stanza handler for a connection.
+ *
+ * This function removes a stanza handler from the connection. The
+ * handRef parameter is *not* the function passed to addHandler(),
+ * but is the reference returned from addHandler().
+ *
+ * Parameters:
+ * (Strophe.Handler) handRef - The handler reference.
+ */
+ deleteHandler: function (handRef)
+ {
+ // this must be done in the Idle loop so that we don't change
+ // the handlers during iteration
+ this.removeHandlers.push(handRef);
+ },
+
+ /** Function: disconnect
+ * Start the graceful disconnection process.
+ *
+ * This function starts the disconnection process. This process starts
+ * by sending unavailable presence and sending BOSH body of type
+ * terminate. A timeout handler makes sure that disconnection happens
+ * even if the BOSH server does not respond.
+ *
+ * The user supplied connection callback will be notified of the
+ * progress as this process happens.
+ *
+ * Parameters:
+ * (String) reason - The reason the disconnect is occuring.
+ */
+ disconnect: function (reason)
+ {
+ this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason);
+
+ Strophe.info("Disconnect was called because: " + reason);
+ if (this.connected) {
+ // setup timeout handler
+ this._disconnectTimeout = this._addSysTimedHandler(
+ 3000, this._onDisconnectTimeout.bind(this));
+ this._sendTerminate();
+ }
+ },
+
+ /** PrivateFunction: _changeConnectStatus
+ * _Private_ helper function that makes sure plugins and the user's
+ * callback are notified of connection status changes.
+ *
+ * Parameters:
+ * (Integer) status - the new connection status, one of the values
+ * in Strophe.Status
+ * (String) condition - the error condition or null
+ */
+ _changeConnectStatus: function (status, condition)
+ {
+ // notify all plugins listening for status changes
+ for (var k in Strophe._connectionPlugins) {
+ if (Strophe._connectionPlugins.hasOwnProperty(k)) {
+ var plugin = this[k];
+ if (plugin.statusChanged) {
+ try {
+ plugin.statusChanged(status, condition);
+ } catch (err) {
+ Strophe.error("" + k + " plugin caused an exception " +
+ "changing status: " + err);
+ }
+ }
+ }
+ }
+
+ // notify the user's callback
+ if (this.connect_callback) {
+ try {
+ this.connect_callback(status, condition);
+ } catch (e) {
+ Strophe.error("User connection callback caused an " +
+ "exception: " + e);
+ }
+ }
+ },
+
+ /** PrivateFunction: _buildBody
+ * _Private_ helper function to generate the wrapper for BOSH.
+ *
+ * Returns:
+ * A Strophe.Builder with a element.
+ */
+ _buildBody: function ()
+ {
+ var bodyWrap = $build('body', {
+ rid: this.rid++,
+ xmlns: Strophe.NS.HTTPBIND
+ });
+
+ if (this.sid !== null) {
+ bodyWrap.attrs({sid: this.sid});
+ }
+
+ return bodyWrap;
+ },
+
+ /** PrivateFunction: _removeRequest
+ * _Private_ function to remove a request from the queue.
+ *
+ * Parameters:
+ * (Strophe.Request) req - The request to remove.
+ */
+ _removeRequest: function (req)
+ {
+ Strophe.debug("removing request");
+
+ var i;
+ for (i = this._requests.length - 1; i >= 0; i--) {
+ if (req == this._requests[i]) {
+ this._requests.splice(i, 1);
+ }
+ }
+
+ // IE6 fails on setting to null, so set to empty function
+ req.xhr.onreadystatechange = function () {};
+
+ this._throttledRequestHandler();
+ },
+
+ /** PrivateFunction: _restartRequest
+ * _Private_ function to restart a request that is presumed dead.
+ *
+ * Parameters:
+ * (Integer) i - The index of the request in the queue.
+ */
+ _restartRequest: function (i)
+ {
+ var req = this._requests[i];
+ if (req.dead === null) {
+ req.dead = new Date();
+ }
+
+ this._processRequest(i);
+ },
+
+ /** PrivateFunction: _processRequest
+ * _Private_ function to process a request in the queue.
+ *
+ * This function takes requests off the queue and sends them and
+ * restarts dead requests.
+ *
+ * Parameters:
+ * (Integer) i - The index of the request in the queue.
+ */
+ _processRequest: function (i)
+ {
+ var req = this._requests[i];
+ var reqStatus = -1;
+
+ try {
+ if (req.xhr.readyState == 4) {
+ reqStatus = req.xhr.status;
+ }
+ } catch (e) {
+ Strophe.error("caught an error in _requests[" + i +
+ "], reqStatus: " + reqStatus);
+ }
+
+ if (typeof(reqStatus) == "undefined") {
+ reqStatus = -1;
+ }
+
+ // make sure we limit the number of retries
+ if (req.sends > 5) {
+ this._onDisconnectTimeout();
+ return;
+ }
+
+ var time_elapsed = req.age();
+ var primaryTimeout = (!isNaN(time_elapsed) &&
+ time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait));
+ var secondaryTimeout = (req.dead !== null &&
+ req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait));
+ var requestCompletedWithServerError = (req.xhr.readyState == 4 &&
+ (reqStatus < 1 ||
+ reqStatus >= 500));
+ if (primaryTimeout || secondaryTimeout ||
+ requestCompletedWithServerError) {
+ if (secondaryTimeout) {
+ Strophe.error("Request " +
+ this._requests[i].id +
+ " timed out (secondary), restarting");
+ }
+ req.abort = true;
+ req.xhr.abort();
+ // setting to null fails on IE6, so set to empty function
+ req.xhr.onreadystatechange = function () {};
+ this._requests[i] = new Strophe.Request(req.xmlData,
+ req.origFunc,
+ req.rid,
+ req.sends);
+ req = this._requests[i];
+ }
+
+ if (req.xhr.readyState === 0) {
+ Strophe.debug("request id " + req.id +
+ "." + req.sends + " posting");
+
+ req.date = new Date();
+ try {
+ req.xhr.open("POST", this.service, true);
+ } catch (e2) {
+ Strophe.error("XHR open failed.");
+ if (!this.connected) {
+ this._changeConnectStatus(Strophe.Status.CONNFAIL,
+ "bad-service");
+ }
+ this.disconnect();
+ return;
+ }
+
+ // Fires the XHR request -- may be invoked immediately
+ // or on a gradually expanding retry window for reconnects
+ var sendFunc = function () {
+ req.xhr.send(req.data);
+ };
+
+ // Implement progressive backoff for reconnects --
+ // First retry (send == 1) should also be instantaneous
+ if (req.sends > 1) {
+ // Using a cube of the retry number creats a nicely
+ // expanding retry window
+ var backoff = Math.pow(req.sends, 3) * 1000;
+ setTimeout(sendFunc, backoff);
+ } else {
+ sendFunc();
+ }
+
+ req.sends++;
+
+ this.xmlOutput(req.xmlData);
+ this.rawOutput(req.data);
+ } else {
+ Strophe.debug("_processRequest: " +
+ (i === 0 ? "first" : "second") +
+ " request has readyState of " +
+ req.xhr.readyState);
+ }
+ },
+
+ /** PrivateFunction: _throttledRequestHandler
+ * _Private_ function to throttle requests to the connection window.
+ *
+ * This function makes sure we don't send requests so fast that the
+ * request ids overflow the connection window in the case that one
+ * request died.
+ */
+ _throttledRequestHandler: function ()
+ {
+ if (!this._requests) {
+ Strophe.debug("_throttledRequestHandler called with " +
+ "undefined requests");
+ } else {
+ Strophe.debug("_throttledRequestHandler called with " +
+ this._requests.length + " requests");
+ }
+
+ if (!this._requests || this._requests.length === 0) {
+ return;
+ }
+
+ if (this._requests.length > 0) {
+ this._processRequest(0);
+ }
+
+ if (this._requests.length > 1 &&
+ Math.abs(this._requests[0].rid -
+ this._requests[1].rid) < this.window) {
+ this._processRequest(1);
+ }
+ },
+
+ /** PrivateFunction: _onRequestStateChange
+ * _Private_ handler for Strophe.Request state changes.
+ *
+ * This function is called when the XMLHttpRequest readyState changes.
+ * It contains a lot of error handling logic for the many ways that
+ * requests can fail, and calls the request callback when requests
+ * succeed.
+ *
+ * Parameters:
+ * (Function) func - The handler for the request.
+ * (Strophe.Request) req - The request that is changing readyState.
+ */
+ _onRequestStateChange: function (func, req)
+ {
+ Strophe.debug("request id " + req.id +
+ "." + req.sends + " state changed to " +
+ req.xhr.readyState);
+
+ if (req.abort) {
+ req.abort = false;
+ return;
+ }
+
+ // request complete
+ var reqStatus;
+ if (req.xhr.readyState == 4) {
+ reqStatus = 0;
+ try {
+ reqStatus = req.xhr.status;
+ } catch (e) {
+ // ignore errors from undefined status attribute. works
+ // around a browser bug
+ }
+
+ if (typeof(reqStatus) == "undefined") {
+ reqStatus = 0;
+ }
+
+ if (this.disconnecting) {
+ if (reqStatus >= 400) {
+ this._hitError(reqStatus);
+ return;
+ }
+ }
+
+ var reqIs0 = (this._requests[0] == req);
+ var reqIs1 = (this._requests[1] == req);
+
+ if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) {
+ // remove from internal queue
+ this._removeRequest(req);
+ Strophe.debug("request id " +
+ req.id +
+ " should now be removed");
+ }
+
+ // request succeeded
+ if (reqStatus == 200) {
+ // if request 1 finished, or request 0 finished and request
+ // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to
+ // restart the other - both will be in the first spot, as the
+ // completed request has been removed from the queue already
+ if (reqIs1 ||
+ (reqIs0 && this._requests.length > 0 &&
+ this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) {
+ this._restartRequest(0);
+ }
+ // call handler
+ Strophe.debug("request id " +
+ req.id + "." +
+ req.sends + " got 200");
+ func(req);
+ this.errors = 0;
+ } else {
+ Strophe.error("request id " +
+ req.id + "." +
+ req.sends + " error " + reqStatus +
+ " happened");
+ if (reqStatus === 0 ||
+ (reqStatus >= 400 && reqStatus < 600) ||
+ reqStatus >= 12000) {
+ this._hitError(reqStatus);
+ if (reqStatus >= 400 && reqStatus < 500) {
+ this._changeConnectStatus(Strophe.Status.DISCONNECTING,
+ null);
+ this._doDisconnect();
+ }
+ }
+ }
+
+ if (!((reqStatus > 0 && reqStatus < 500) ||
+ req.sends > 5)) {
+ this._throttledRequestHandler();
+ }
+ }
+ },
+
+ /** PrivateFunction: _hitError
+ * _Private_ function to handle the error count.
+ *
+ * Requests are resent automatically until their error count reaches
+ * 5. Each time an error is encountered, this function is called to
+ * increment the count and disconnect if the count is too high.
+ *
+ * Parameters:
+ * (Integer) reqStatus - The request status.
+ */
+ _hitError: function (reqStatus)
+ {
+ this.errors++;
+ Strophe.warn("request errored, status: " + reqStatus +
+ ", number of errors: " + this.errors);
+ if (this.errors > 4) {
+ this._onDisconnectTimeout();
+ }
+ },
+
+ /** PrivateFunction: _doDisconnect
+ * _Private_ function to disconnect.
+ *
+ * This is the last piece of the disconnection logic. This resets the
+ * connection and alerts the user's connection callback.
+ */
+ _doDisconnect: function ()
+ {
+ Strophe.info("_doDisconnect was called");
+ this.authenticated = false;
+ this.disconnecting = false;
+ this.sid = null;
+ this.streamId = null;
+ this.rid = Math.floor(Math.random() * 4294967295);
+
+ // tell the parent we disconnected
+ if (this.connected) {
+ this._changeConnectStatus(Strophe.Status.DISCONNECTED, null);
+ this.connected = false;
+ }
+
+ // delete handlers
+ this.handlers = [];
+ this.timedHandlers = [];
+ this.removeTimeds = [];
+ this.removeHandlers = [];
+ this.addTimeds = [];
+ this.addHandlers = [];
+ },
+
+ /** PrivateFunction: _dataRecv
+ * _Private_ handler to processes incoming data from the the connection.
+ *
+ * Except for _connect_cb handling the initial connection request,
+ * this function handles the incoming data for all requests. This
+ * function also fires stanza handlers that match each incoming
+ * stanza.
+ *
+ * Parameters:
+ * (Strophe.Request) req - The request that has data ready.
+ */
+ _dataRecv: function (req)
+ {
+ try {
+ var elem = req.getResponse();
+ } catch (e) {
+ if (e != "parsererror") { throw e; }
+ this.disconnect("strophe-parsererror");
+ }
+ if (elem === null) { return; }
+
+ this.xmlInput(elem);
+ this.rawInput(Strophe.serialize(elem));
+
+ // remove handlers scheduled for deletion
+ var i, hand;
+ while (this.removeHandlers.length > 0) {
+ hand = this.removeHandlers.pop();
+ i = this.handlers.indexOf(hand);
+ if (i >= 0) {
+ this.handlers.splice(i, 1);
+ }
+ }
+
+ // add handlers scheduled for addition
+ while (this.addHandlers.length > 0) {
+ this.handlers.push(this.addHandlers.pop());
+ }
+
+ // handle graceful disconnect
+ if (this.disconnecting && this._requests.length === 0) {
+ this.deleteTimedHandler(this._disconnectTimeout);
+ this._disconnectTimeout = null;
+ this._doDisconnect();
+ return;
+ }
+
+ var typ = elem.getAttribute("type");
+ var cond, conflict;
+ if (typ !== null && typ == "terminate") {
+ // Don't process stanzas that come in after disconnect
+ if (this.disconnecting) {
+ return;
+ }
+
+ // an error occurred
+ cond = elem.getAttribute("condition");
+ conflict = elem.getElementsByTagName("conflict");
+ if (cond !== null) {
+ if (cond == "remote-stream-error" && conflict.length > 0) {
+ cond = "conflict";
+ }
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
+ } else {
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+ }
+ this.disconnect();
+ return;
+ }
+
+ // send each incoming stanza through the handler chain
+ var that = this;
+ Strophe.forEachChild(elem, null, function (child) {
+ var i, newList;
+ // process handlers
+ newList = that.handlers;
+ that.handlers = [];
+ for (i = 0; i < newList.length; i++) {
+ var hand = newList[i];
+ if (hand.isMatch(child) &&
+ (that.authenticated || !hand.user)) {
+ if (hand.run(child)) {
+ that.handlers.push(hand);
+ }
+ } else {
+ that.handlers.push(hand);
+ }
+ }
+ });
+ },
+
+ /** PrivateFunction: _sendTerminate
+ * _Private_ function to send initial disconnect sequence.
+ *
+ * This is the first step in a graceful disconnect. It sends
+ * the BOSH server a terminate body and includes an unavailable
+ * presence if authentication has completed.
+ */
+ _sendTerminate: function ()
+ {
+ Strophe.info("_sendTerminate was called");
+ var body = this._buildBody().attrs({type: "terminate"});
+
+ if (this.authenticated) {
+ body.c('presence', {
+ xmlns: Strophe.NS.CLIENT,
+ type: 'unavailable'
+ });
+ }
+
+ this.disconnecting = true;
+
+ var req = new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._dataRecv.bind(this)),
+ body.tree().getAttribute("rid"));
+
+ this._requests.push(req);
+ this._throttledRequestHandler();
+ },
+
+ /** PrivateFunction: _connect_cb
+ * _Private_ handler for initial connection request.
+ *
+ * This handler is used to process the initial connection request
+ * response from the BOSH server. It is used to set up authentication
+ * handlers and start the authentication process.
+ *
+ * SASL authentication will be attempted if available, otherwise
+ * the code will fall back to legacy authentication.
+ *
+ * Parameters:
+ * (Strophe.Request) req - The current request.
+ */
+ _connect_cb: function (req)
+ {
+ Strophe.info("_connect_cb was called");
+
+ this.connected = true;
+ var bodyWrap = req.getResponse();
+ if (!bodyWrap) { return; }
+
+ this.xmlInput(bodyWrap);
+ this.rawInput(Strophe.serialize(bodyWrap));
+
+ var typ = bodyWrap.getAttribute("type");
+ var cond, conflict;
+ if (typ !== null && typ == "terminate") {
+ // an error occurred
+ cond = bodyWrap.getAttribute("condition");
+ conflict = bodyWrap.getElementsByTagName("conflict");
+ if (cond !== null) {
+ if (cond == "remote-stream-error" && conflict.length > 0) {
+ cond = "conflict";
+ }
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, cond);
+ } else {
+ this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown");
+ }
+ return;
+ }
+
+ // check to make sure we don't overwrite these if _connect_cb is
+ // called multiple times in the case of missing stream:features
+ if (!this.sid) {
+ this.sid = bodyWrap.getAttribute("sid");
+ }
+ if (!this.stream_id) {
+ this.stream_id = bodyWrap.getAttribute("authid");
+ }
+ var wind = bodyWrap.getAttribute('requests');
+ if (wind) { this.window = parseInt(wind, 10); }
+ var hold = bodyWrap.getAttribute('hold');
+ if (hold) { this.hold = parseInt(hold, 10); }
+ var wait = bodyWrap.getAttribute('wait');
+ if (wait) { this.wait = parseInt(wait, 10); }
+
+
+ var do_sasl_plain = false;
+ var do_sasl_digest_md5 = false;
+ var do_sasl_anonymous = false;
+
+ var mechanisms = bodyWrap.getElementsByTagName("mechanism");
+ var i, mech, auth_str, hashed_auth_str;
+ if (mechanisms.length > 0) {
+ for (i = 0; i < mechanisms.length; i++) {
+ mech = Strophe.getText(mechanisms[i]);
+ if (mech == 'DIGEST-MD5') {
+ do_sasl_digest_md5 = true;
+ } else if (mech == 'PLAIN') {
+ do_sasl_plain = true;
+ } else if (mech == 'ANONYMOUS') {
+ do_sasl_anonymous = true;
+ }
+ }
+ } else {
+ // we didn't get stream:features yet, so we need wait for it
+ // by sending a blank poll request
+ var body = this._buildBody();
+ this._requests.push(
+ new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._connect_cb.bind(this)),
+ body.tree().getAttribute("rid")));
+ this._throttledRequestHandler();
+ return;
+ }
+
+ if (Strophe.getNodeFromJid(this.jid) === null &&
+ do_sasl_anonymous) {
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ this.send($build("auth", {
+ xmlns: Strophe.NS.SASL,
+ mechanism: "ANONYMOUS"
+ }).tree());
+ } else if (Strophe.getNodeFromJid(this.jid) === null) {
+ // we don't have a node, which is required for non-anonymous
+ // client connections
+ this._changeConnectStatus(Strophe.Status.CONNFAIL,
+ 'x-strophe-bad-non-anon-jid');
+ this.disconnect();
+ } else if (do_sasl_digest_md5) {
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._sasl_challenge_handler = this._addSysHandler(
+ this._sasl_challenge1_cb.bind(this), null,
+ "challenge", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ this.send($build("auth", {
+ xmlns: Strophe.NS.SASL,
+ mechanism: "DIGEST-MD5"
+ }).tree());
+ } else if (do_sasl_plain) {
+ // Build the plain auth string (barejid null
+ // username null password) and base 64 encoded.
+ auth_str = Strophe.getBareJidFromJid(this.jid);
+ auth_str = auth_str + "\u0000";
+ auth_str = auth_str + Strophe.getNodeFromJid(this.jid);
+ auth_str = auth_str + "\u0000";
+ auth_str = auth_str + this.pass;
+
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ hashed_auth_str = Base64.encode(auth_str);
+ this.send($build("auth", {
+ xmlns: Strophe.NS.SASL,
+ mechanism: "PLAIN"
+ }).t(hashed_auth_str).tree());
+ } else {
+ this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null);
+ this._addSysHandler(this._auth1_cb.bind(this), null, null,
+ null, "_auth_1");
+
+ this.send($iq({
+ type: "get",
+ to: this.domain,
+ id: "_auth_1"
+ }).c("query", {
+ xmlns: Strophe.NS.AUTH
+ }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree());
+ }
+ },
+
+ /** PrivateFunction: _sasl_challenge1_cb
+ * _Private_ handler for DIGEST-MD5 SASL authentication.
+ *
+ * Parameters:
+ * (XMLElement) elem - The challenge stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_challenge1_cb: function (elem)
+ {
+ var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/;
+
+ var challenge = Base64.decode(Strophe.getText(elem));
+ var cnonce = MD5.hexdigest(Math.random() * 1234567890);
+ var realm = "";
+ var host = null;
+ var nonce = "";
+ var qop = "";
+ var matches;
+
+ // remove unneeded handlers
+ this.deleteHandler(this._sasl_failure_handler);
+
+ while (challenge.match(attribMatch)) {
+ matches = challenge.match(attribMatch);
+ challenge = challenge.replace(matches[0], "");
+ matches[2] = matches[2].replace(/^"(.+)"$/, "$1");
+ switch (matches[1]) {
+ case "realm":
+ realm = matches[2];
+ break;
+ case "nonce":
+ nonce = matches[2];
+ break;
+ case "qop":
+ qop = matches[2];
+ break;
+ case "host":
+ host = matches[2];
+ break;
+ }
+ }
+
+ var digest_uri = "xmpp/" + this.domain;
+ if (host !== null) {
+ digest_uri = digest_uri + "/" + host;
+ }
+
+ var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) +
+ ":" + realm + ":" + this.pass) +
+ ":" + nonce + ":" + cnonce;
+ var A2 = 'AUTHENTICATE:' + digest_uri;
+
+ var responseText = "";
+ responseText += 'username=' +
+ this._quote(Strophe.getNodeFromJid(this.jid)) + ',';
+ responseText += 'realm=' + this._quote(realm) + ',';
+ responseText += 'nonce=' + this._quote(nonce) + ',';
+ responseText += 'cnonce=' + this._quote(cnonce) + ',';
+ responseText += 'nc="00000001",';
+ responseText += 'qop="auth",';
+ responseText += 'digest-uri=' + this._quote(digest_uri) + ',';
+ responseText += 'response=' + this._quote(
+ MD5.hexdigest(MD5.hexdigest(A1) + ":" +
+ nonce + ":00000001:" +
+ cnonce + ":auth:" +
+ MD5.hexdigest(A2))) + ',';
+ responseText += 'charset="utf-8"';
+
+ this._sasl_challenge_handler = this._addSysHandler(
+ this._sasl_challenge2_cb.bind(this), null,
+ "challenge", null, null);
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+
+ this.send($build('response', {
+ xmlns: Strophe.NS.SASL
+ }).t(Base64.encode(responseText)).tree());
+
+ return false;
+ },
+
+ /** PrivateFunction: _quote
+ * _Private_ utility function to backslash escape and quote strings.
+ *
+ * Parameters:
+ * (String) str - The string to be quoted.
+ *
+ * Returns:
+ * quoted string
+ */
+ _quote: function (str)
+ {
+ return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
+ //" end string workaround for emacs
+ },
+
+
+ /** PrivateFunction: _sasl_challenge2_cb
+ * _Private_ handler for second step of DIGEST-MD5 SASL authentication.
+ *
+ * Parameters:
+ * (XMLElement) elem - The challenge stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_challenge2_cb: function (elem)
+ {
+ // remove unneeded handlers
+ this.deleteHandler(this._sasl_success_handler);
+ this.deleteHandler(this._sasl_failure_handler);
+
+ this._sasl_success_handler = this._addSysHandler(
+ this._sasl_success_cb.bind(this), null,
+ "success", null, null);
+ this._sasl_failure_handler = this._addSysHandler(
+ this._sasl_failure_cb.bind(this), null,
+ "failure", null, null);
+ this.send($build('response', {xmlns: Strophe.NS.SASL}).tree());
+ return false;
+ },
+
+ /** PrivateFunction: _auth1_cb
+ * _Private_ handler for legacy authentication.
+ *
+ * This handler is called in response to the initial
+ * for legacy authentication. It builds an authentication and
+ * sends it, creating a handler (calling back to _auth2_cb()) to
+ * handle the result
+ *
+ * Parameters:
+ * (XMLElement) elem - The stanza that triggered the callback.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _auth1_cb: function (elem)
+ {
+ // build plaintext auth iq
+ var iq = $iq({type: "set", id: "_auth_2"})
+ .c('query', {xmlns: Strophe.NS.AUTH})
+ .c('username', {}).t(Strophe.getNodeFromJid(this.jid))
+ .up()
+ .c('password').t(this.pass);
+
+ if (!Strophe.getResourceFromJid(this.jid)) {
+ // since the user has not supplied a resource, we pick
+ // a default one here. unlike other auth methods, the server
+ // cannot do this for us.
+ this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe';
+ }
+ iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid));
+
+ this._addSysHandler(this._auth2_cb.bind(this), null,
+ null, null, "_auth_2");
+
+ this.send(iq.tree());
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_success_cb
+ * _Private_ handler for succesful SASL authentication.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_success_cb: function (elem)
+ {
+ Strophe.info("SASL authentication succeeded.");
+
+ // remove old handlers
+ this.deleteHandler(this._sasl_failure_handler);
+ this._sasl_failure_handler = null;
+ if (this._sasl_challenge_handler) {
+ this.deleteHandler(this._sasl_challenge_handler);
+ this._sasl_challenge_handler = null;
+ }
+
+ this._addSysHandler(this._sasl_auth1_cb.bind(this), null,
+ "stream:features", null, null);
+
+ // we must send an xmpp:restart now
+ this._sendRestart();
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_auth1_cb
+ * _Private_ handler to start stream binding.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_auth1_cb: function (elem)
+ {
+ // save stream:features for future usage
+ this.features = elem;
+
+ var i, child;
+
+ for (i = 0; i < elem._childNodes.length; i++) {
+ child = elem._childNodes[i];
+ if (child.nodeName.toLowerCase() == 'bind') {
+ this.do_bind = true;
+ }
+
+ if (child.nodeName.toLowerCase() == 'session') {
+ this.do_session = true;
+ }
+ }
+
+ if (!this.do_bind) {
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ } else {
+ this._addSysHandler(this._sasl_bind_cb.bind(this), null, null,
+ null, "_bind_auth_2");
+
+ var resource = Strophe.getResourceFromJid(this.jid);
+ if (resource) {
+ this.send($iq({type: "set", id: "_bind_auth_2"})
+ .c('bind', {xmlns: Strophe.NS.BIND})
+ .c('resource', {}).t(resource).tree());
+ } else {
+ this.send($iq({type: "set", id: "_bind_auth_2"})
+ .c('bind', {xmlns: Strophe.NS.BIND})
+ .tree());
+ }
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_bind_cb
+ * _Private_ handler for binding result and session start.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_bind_cb: function (elem)
+ {
+ if (elem.getAttribute("type") == "error") {
+ Strophe.info("SASL binding failed.");
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ }
+
+ // TODO - need to grab errors
+ var bind = elem.getElementsByTagName("bind");
+ var jidNode;
+ if (bind.length > 0) {
+ // Grab jid
+ jidNode = bind[0].getElementsByTagName("jid");
+ if (jidNode.length > 0) {
+ this.jid = Strophe.getText(jidNode[0]);
+
+ if (this.do_session) {
+ this._addSysHandler(this._sasl_session_cb.bind(this),
+ null, null, null, "_session_auth_2");
+
+ this.send($iq({type: "set", id: "_session_auth_2"})
+ .c('session', {xmlns: Strophe.NS.SESSION})
+ .tree());
+ } else {
+ this.authenticated = true;
+ this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+ }
+ }
+ } else {
+ Strophe.info("SASL binding failed.");
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ }
+ },
+
+ /** PrivateFunction: _sasl_session_cb
+ * _Private_ handler to finish successful SASL connection.
+ *
+ * This sets Connection.authenticated to true on success, which
+ * starts the processing of user handlers.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_session_cb: function (elem)
+ {
+ if (elem.getAttribute("type") == "result") {
+ this.authenticated = true;
+ this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+ } else if (elem.getAttribute("type") == "error") {
+ Strophe.info("Session creation failed.");
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: _sasl_failure_cb
+ * _Private_ handler for SASL authentication failure.
+ *
+ * Parameters:
+ * (XMLElement) elem - The matching stanza.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _sasl_failure_cb: function (elem)
+ {
+ // delete unneeded handlers
+ if (this._sasl_success_handler) {
+ this.deleteHandler(this._sasl_success_handler);
+ this._sasl_success_handler = null;
+ }
+ if (this._sasl_challenge_handler) {
+ this.deleteHandler(this._sasl_challenge_handler);
+ this._sasl_challenge_handler = null;
+ }
+
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ return false;
+ },
+
+ /** PrivateFunction: _auth2_cb
+ * _Private_ handler to finish legacy authentication.
+ *
+ * This handler is called when the result from the jabber:iq:auth
+ * stanza is returned.
+ *
+ * Parameters:
+ * (XMLElement) elem - The stanza that triggered the callback.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _auth2_cb: function (elem)
+ {
+ if (elem.getAttribute("type") == "result") {
+ this.authenticated = true;
+ this._changeConnectStatus(Strophe.Status.CONNECTED, null);
+ } else if (elem.getAttribute("type") == "error") {
+ this._changeConnectStatus(Strophe.Status.AUTHFAIL, null);
+ this.disconnect();
+ }
+
+ return false;
+ },
+
+ /** PrivateFunction: _addSysTimedHandler
+ * _Private_ function to add a system level timed handler.
+ *
+ * This function is used to add a Strophe.TimedHandler for the
+ * library code. System timed handlers are allowed to run before
+ * authentication is complete.
+ *
+ * Parameters:
+ * (Integer) period - The period of the handler.
+ * (Function) handler - The callback function.
+ */
+ _addSysTimedHandler: function (period, handler)
+ {
+ var thand = new Strophe.TimedHandler(period, handler);
+ thand.user = false;
+ this.addTimeds.push(thand);
+ return thand;
+ },
+
+ /** PrivateFunction: _addSysHandler
+ * _Private_ function to add a system level stanza handler.
+ *
+ * This function is used to add a Strophe.Handler for the
+ * library code. System stanza handlers are allowed to run before
+ * authentication is complete.
+ *
+ * Parameters:
+ * (Function) handler - The callback function.
+ * (String) ns - The namespace to match.
+ * (String) name - The stanza name to match.
+ * (String) type - The stanza type attribute to match.
+ * (String) id - The stanza id attribute to match.
+ */
+ _addSysHandler: function (handler, ns, name, type, id)
+ {
+ var hand = new Strophe.Handler(handler, ns, name, type, id);
+ hand.user = false;
+ this.addHandlers.push(hand);
+ return hand;
+ },
+
+ /** PrivateFunction: _onDisconnectTimeout
+ * _Private_ timeout handler for handling non-graceful disconnection.
+ *
+ * If the graceful disconnect process does not complete within the
+ * time allotted, this handler finishes the disconnect anyway.
+ *
+ * Returns:
+ * false to remove the handler.
+ */
+ _onDisconnectTimeout: function ()
+ {
+ Strophe.info("_onDisconnectTimeout was called");
+
+ // cancel all remaining requests and clear the queue
+ var req;
+ while (this._requests.length > 0) {
+ req = this._requests.pop();
+ req.abort = true;
+ req.xhr.abort();
+ // jslint complains, but this is fine. setting to empty func
+ // is necessary for IE6
+ req.xhr.onreadystatechange = function () {};
+ }
+
+ // actually disconnect
+ this._doDisconnect();
+
+ return false;
+ },
+
+ /** PrivateFunction: _onIdle
+ * _Private_ handler to process events during idle cycle.
+ *
+ * This handler is called every 100ms to fire timed handlers that
+ * are ready and keep poll requests going.
+ */
+ _onIdle: function ()
+ {
+ var i, thand, since, newList;
+
+ // add timed handlers scheduled for addition
+ // NOTE: we add before remove in the case a timed handler is
+ // added and then deleted before the next _onIdle() call.
+ while (this.addTimeds.length > 0) {
+ this.timedHandlers.push(this.addTimeds.pop());
+ }
+
+ // remove timed handlers that have been scheduled for deletion
+ while (this.removeTimeds.length > 0) {
+ thand = this.removeTimeds.pop();
+ i = this.timedHandlers.indexOf(thand);
+ if (i >= 0) {
+ this.timedHandlers.splice(i, 1);
+ }
+ }
+
+ // call ready timed handlers
+ var now = new Date().getTime();
+ newList = [];
+ for (i = 0; i < this.timedHandlers.length; i++) {
+ thand = this.timedHandlers[i];
+ if (this.authenticated || !thand.user) {
+ since = thand.lastCalled + thand.period;
+ if (since - now <= 0) {
+ if (thand.run()) {
+ newList.push(thand);
+ }
+ } else {
+ newList.push(thand);
+ }
+ }
+ }
+ this.timedHandlers = newList;
+
+ var body, time_elapsed;
+
+ // if no requests are in progress, poll
+ if (this.authenticated && this._requests.length === 0 &&
+ this._data.length === 0 && !this.disconnecting) {
+ Strophe.info("no requests during idle cycle, sending " +
+ "blank request");
+ this._data.push(null);
+ }
+
+ if (this._requests.length < 2 && this._data.length > 0 &&
+ !this.paused) {
+ body = this._buildBody();
+ for (i = 0; i < this._data.length; i++) {
+ if (this._data[i] !== null) {
+ if (this._data[i] === "restart") {
+ body.attrs({
+ to: this.domain,
+ "xml:lang": "en",
+ "xmpp:restart": "true",
+ "xmlns:xmpp": Strophe.NS.BOSH
+ });
+ } else {
+ body.cnode(this._data[i]).up();
+ }
+ }
+ }
+ delete this._data;
+ this._data = [];
+ this._requests.push(
+ new Strophe.Request(body.tree(),
+ this._onRequestStateChange.bind(
+ this, this._dataRecv.bind(this)),
+ body.tree().getAttribute("rid")));
+ this._processRequest(this._requests.length - 1);
+ }
+
+ if (this._requests.length > 0) {
+ time_elapsed = this._requests[0].age();
+ if (this._requests[0].dead !== null) {
+ if (this._requests[0].timeDead() >
+ Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) {
+ this._throttledRequestHandler();
+ }
+ }
+
+ if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) {
+ Strophe.warn("Request " +
+ this._requests[0].id +
+ " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) +
+ " seconds since last activity");
+ this._throttledRequestHandler();
+ }
+ }
+
+ // reactivate the timer
+ clearTimeout(this._idleTimeout);
+ this._idleTimeout = setTimeout(this._onIdle.bind(this), 100);
+ }
+};
+
+if (callback) {
+ callback(Strophe, $build, $msg, $iq, $pres);
+}
+
+})(function () {
+ window.Strophe = arguments[0];
+ window.$build = arguments[1];
+ window.$msg = arguments[2];
+ window.$iq = arguments[3];
+ window.$pres = arguments[4];
+});
diff --git a/contrib/jitsimeetbridge/unjingle/unjingle.js b/contrib/jitsimeetbridge/unjingle/unjingle.js
new file mode 100644
index 0000000000..3dfe759914
--- /dev/null
+++ b/contrib/jitsimeetbridge/unjingle/unjingle.js
@@ -0,0 +1,48 @@
+var strophe = require("./strophe/strophe.js").Strophe;
+
+var Strophe = strophe.Strophe;
+var $iq = strophe.$iq;
+var $msg = strophe.$msg;
+var $build = strophe.$build;
+var $pres = strophe.$pres;
+
+var jsdom = require("jsdom");
+var window = jsdom.jsdom().parentWindow;
+var $ = require('jquery')(window);
+
+var stropheJingle = require("./strophe.jingle.sdp.js");
+
+
+var input = '';
+
+process.stdin.on('readable', function() {
+ var chunk = process.stdin.read();
+ if (chunk !== null) {
+ input += chunk;
+ }
+});
+
+process.stdin.on('end', function() {
+ if (process.argv[2] == '--jingle') {
+ var elem = $(input);
+ // app does:
+ // sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
+ //console.log(elem.find('>content'));
+ var sdp = new stropheJingle.SDP('');
+ sdp.fromJingle(elem);
+ console.log(sdp.raw);
+ } else if (process.argv[2] == '--sdp') {
+ var sdp = new stropheJingle.SDP(input);
+ var accept = $iq({to: '%(tojid)s',
+ type: 'set'})
+ .c('jingle', {xmlns: 'urn:xmpp:jingle:1',
+ //action: 'session-accept',
+ action: '%(action)s',
+ initiator: '%(initiator)s',
+ responder: '%(responder)s',
+ sid: '%(sid)s' });
+ sdp.toJingle(accept, 'responder');
+ console.log(Strophe.serialize(accept));
+ }
+});
+
From d8b35250a5ecf73ca0c2e24fe35feffa0a87590a Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 17:20:02 +0000
Subject: [PATCH 12/18] Remove editor junk
---
contrib/jitsimeetbridge/unjingle/.swp | Bin 12288 -> 0 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 contrib/jitsimeetbridge/unjingle/.swp
diff --git a/contrib/jitsimeetbridge/unjingle/.swp b/contrib/jitsimeetbridge/unjingle/.swp
deleted file mode 100644
index 449c08da561e6bb3b4429ae8e8817bf8c07463d1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 12288
zcmeHNOK;;g5O#X&t!V#%+M-bRpqP4D5@8!ioGhAl69g!-MSG(t%2uKWSEBMb^+)!{
zw8su5+iN*;)<}wM4>STxqBt|(d>now!3oj(vvbWmvm;A2OuNt0yqf5BndAG6bKDMNQ`ZO;Ns&+VAAW4l-soDACn6!DDCOw-
z0&bM7;zb^@=EnL0Zd6Gm=fdF)#GIE!&3A<)oL9J+0mv`{Tb7YrBoyEsUtOHMI|UE4
z$OYina+0x$PU2qSKxAqFBP_%M
zK!h;yBXtWQI35mJC5q}WV#B-|wi9Hmh*h*qxMcFlyi4!_oM$Xb$SSYtn3IRJAdw*+
zIMXDPrU{eU+zVb;x<=E2-jqZD8IF$-utpao&shp+Er7~N2}}uAF0Ha82QV4awOf+b
zU}d6)lFdkcagn`>d-m(mtP|H9ZN)XE$4!SVbmZ)GVOx8-fb81bFK6dh9Sqy4_e1bE
z?2n#dZ=%u4!|bACzh?p_a(6h`m&EllYysGh0MlA0AXv)yqR5%SZlxe>R?=fVcXf_&1tC*smUAHHrH1rpcp
z&%Zf$%s&6+kHG4)$Tsl>u@#1%Z5pv-Muvm@zz9*~86ow224^GRw=@^UBhPi2F$&y~
z;aJo+h;P{jVwUN9mQP&D`XhC7*(X?^pRSjE(F`|t@N`@X%8jhPm-@LOYSXbONe<#)
z3aySyGN0%y5`kj|UdnGO+tW3E2g2p1MHVgQ+`JWtq3eo?ArL$#ioO2CwS)JV;`FVK6bDj;1dTQpW>s_@o)nPkg|pg=gTg&Pm5`L
z)$6yIHg2y@uVwcAB`D@bhY3KXI~_)@M^hMa_$2+ZyDjI*Rgmp$qEnhdkFUe_Do#pS
zyU+HP6MFL^2lJws8zsv^>1C_Wt?
z;HCneEx}J&cLk@B?Y$-V;@$85a8#YKku|&g@Ke$!XdMYQ&45KC;u{Pejts|zFJhs<
z@IpTzw&@|td<{9SN05z-*kZPnU}}&kCPr)rPBd~yJ|W9ThHm>>!CpTt?GdaGahH*O
X(QH+rlHlh^@UVTOfjymkm=690r{`%W
From afb646dc1e7a91ee7a050632d1e5a5883830d18c Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 18:00:51 +0000
Subject: [PATCH 13/18] Allow GET /room/:room_id/messages without 'limit'
parameter to default to 10
---
synapse/rest/room.py | 4 +++-
synapse/streams/config.py | 6 +++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/synapse/rest/room.py b/synapse/rest/room.py
index cc6ffb9aff..3147d7a60b 100644
--- a/synapse/rest/room.py
+++ b/synapse/rest/room.py
@@ -327,7 +327,9 @@ class RoomMessageListRestServlet(RestServlet):
@defer.inlineCallbacks
def on_GET(self, request, room_id):
user = yield self.auth.get_user_by_req(request)
- pagination_config = PaginationConfig.from_request(request)
+ pagination_config = PaginationConfig.from_request(request,
+ default_limit=10,
+ )
with_feedback = "feedback" in request.args
handler = self.handlers.message_handler
msgs = yield handler.get_messages(
diff --git a/synapse/streams/config.py b/synapse/streams/config.py
index 0317e78c08..2114c940e7 100644
--- a/synapse/streams/config.py
+++ b/synapse/streams/config.py
@@ -47,7 +47,8 @@ class PaginationConfig(object):
self.limit = int(limit) if limit is not None else None
@classmethod
- def from_request(cls, request, raise_invalid_params=True):
+ def from_request(cls, request, raise_invalid_params=True,
+ default_limit=None):
def get_param(name, default=None):
lst = request.args.get(name, [])
if len(lst) > 1:
@@ -84,6 +85,9 @@ class PaginationConfig(object):
if limit is not None and not limit.isdigit():
raise SynapseError(400, "'limit' parameter must be an integer.")
+ if limit is None:
+ limit = default_limit
+
try:
return PaginationConfig(from_tok, to_tok, direction, limit)
except:
From 307f94dcbe0a78de4f5bba9aeb53102cd201a961 Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 20:51:12 +0000
Subject: [PATCH 14/18] Squash room_ids list to a set() to remove duplicates -
with TODO marker as I have no idea /why/ the dups are happening in the first
place
---
synapse/handlers/room.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 88955160c5..a000b44036 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -519,7 +519,11 @@ class RoomMemberHandler(BaseHandler):
user_id=user.to_string(), membership_list=membership_list
)
- defer.returnValue([r.room_id for r in rooms])
+ # For some reason the list of events contains duplicates
+ # TODO(paul): work out why because I really don't think it should
+ room_ids = set(r.room_id for r in rooms)
+
+ defer.returnValue(room_ids)
@defer.inlineCallbacks
def _do_local_membership_update(self, event, membership, snapshot,
From 274d137b00ceab22f01c07461bce18f8916b54bd Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 20:51:28 +0000
Subject: [PATCH 15/18] Extra verbosity on logging of received presence info
---
synapse/handlers/presence.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index b55d589daf..815d40f166 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -651,11 +651,16 @@ class PresenceHandler(BaseHandler):
logger.debug("Incoming presence update from %s", user)
observers = set(self._remote_recvmap.get(user, set()))
+ if observers:
+ logger.debug(" | %d interested local observers %r", len(observers), observers)
rm_handler = self.homeserver.get_handlers().room_member_handler
room_ids = yield rm_handler.get_rooms_for_user(user)
+ if room_ids:
+ logger.debug(" | %d interested room IDs %r", len(room_ids), room_ids)
if not observers and not room_ids:
+ logger.debug(" | no interested observers or room IDs")
continue
state = dict(push)
From 10eb8f070cc5e4fc5067c98ef699724716e33f74 Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 21:36:53 +0000
Subject: [PATCH 16/18] Workaround for non-uniqueness of room member events in
the database confusing HAVING COUNT() test of room membership intersection
(with thanks to Tom Molesworth)
---
synapse/storage/roommember.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py
index c37df59d45..05b275663e 100644
--- a/synapse/storage/roommember.py
+++ b/synapse/storage/roommember.py
@@ -218,7 +218,9 @@ class RoomMemberStore(SQLBaseStore):
"ON m.event_id = c.event_id "
"WHERE m.membership = 'join' "
"AND (%(clause)s) "
- "GROUP BY m.room_id HAVING COUNT(m.room_id) = ?"
+ # TODO(paul): We've got duplicate rows in the database somewhere
+ # so we have to DISTINCT m.user_id here
+ "GROUP BY m.room_id HAVING COUNT(DISTINCT m.user_id) = ?"
) % {"clause": user_list_clause}
args = list(user_id_list)
From 66b0596b7a0995cfc4ca72ba2062014e3f9b8577 Mon Sep 17 00:00:00 2001
From: "Paul \"LeoNerd\" Evans"
Date: Tue, 2 Dec 2014 22:28:18 +0000
Subject: [PATCH 17/18] Don't log synapse.storage at DEBUG level when under -v;
require -v -v for that
---
synapse/config/logger.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
index 8566296433..089d906fa5 100644
--- a/synapse/config/logger.py
+++ b/synapse/config/logger.py
@@ -52,12 +52,18 @@ class LoggingConfig(Config):
if self.log_config is None:
level = logging.INFO
+ level_for_storage = logging.INFO
if self.verbosity:
level = logging.DEBUG
+ if self.verbosity > 1:
+ level_for_storage = logging.DEBUG
# FIXME: we need a logging.WARN for a -q quiet option
logger = logging.getLogger('')
logger.setLevel(level)
+
+ logging.getLogger('synapse.storage').setLevel(level_for_storage)
+
formatter = logging.Formatter(log_format)
if self.log_file:
handler = logging.FileHandler(self.log_file)
From d06dfc70b0c03c5bfa4a4977e406166cef36ee8b Mon Sep 17 00:00:00 2001
From: Erik Johnston
Date: Wed, 3 Dec 2014 11:50:23 +0000
Subject: [PATCH 18/18] Fix bug where did not always resolve all the deferreds
in _attempt_new_transaction
---
synapse/federation/replication.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index 312d69fcaa..01f87fe423 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -853,7 +853,10 @@ class _TransactionQueue(object):
# Ensures we don't continue until all callbacks on that
# deferred have fired
- yield deferred
+ try:
+ yield deferred
+ except:
+ pass
logger.debug("TX [%s] Yielded to callbacks", destination)
@@ -865,7 +868,8 @@ class _TransactionQueue(object):
logger.exception(e)
for deferred in deferreds:
- deferred.errback(e)
+ if not deferred.called:
+ deferred.errback(e)
finally:
# We want to be *very* sure we delete this after we stop processing