diff --git a/CHANGES.rst b/CHANGES.rst index 5a284c385..2c57a57a0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,39 @@ +Changes in synapse 0.5.4 (2014-12-03) +===================================== + + * Fix presence bug where some rooms did not display presence updates for + remote users. + * Do not log SQL timing log lines when started with "-v" + * Fix potential memory leak. + +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) +===================================== + + * Fix bug that caused joining a remote room to fail if a single event was not + signed correctly. + * Fix bug which caused servers to continuously try and fetch events from other + servers. + +Changes in synapse 0.5.2 (2014-11-26) +===================================== + +Fix major bug that caused rooms to disappear from peoples initial sync. + +Changes in synapse 0.5.1 (2014-11-26) +===================================== +See UPGRADES.rst for specific instructions on how to upgrade. + + * Fix bug where we served up an Event that did not match its signatures. + * Fix regression where we no longer correctly handled the case where a + homeserver receives an event for a room it doesn't recognise (but is in.) + Changes in synapse 0.5.0 (2014-11-19) ===================================== This release includes changes to the federation protocol and client-server API diff --git a/README.rst b/README.rst index 6048c25a1..8459bcac2 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 + python-pip python-setuptools sqlite3 \ + libssl-dev Installing prerequisites on Mac OS X:: @@ -129,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 ======================= diff --git a/VERSION b/VERSION index 8f0916f76..7d8568351 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.5.4 diff --git a/contrib/jitsimeetbridge/jitsimeetbridge.py b/contrib/jitsimeetbridge/jitsimeetbridge.py new file mode 100644 index 000000000..dbc6f6ffa --- /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 000000000..aed23c78a --- /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/strophe.jingle.sdp.js b/contrib/jitsimeetbridge/unjingle/strophe.jingle.sdp.js new file mode 100644 index 000000000..e99dd7bf9 --- /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 000000000..042a123c3 --- /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 000000000..9c45c2df1 --- /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 000000000..418caac05 --- /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 000000000..5334325e2 --- /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 000000000..06d426cde --- /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 += ""; + } 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 000000000..3dfe75991 --- /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)); + } +}); + diff --git a/contrib/vertobot/.gitignore b/contrib/vertobot/.gitignore new file mode 100644 index 000000000..071a78057 --- /dev/null +++ b/contrib/vertobot/.gitignore @@ -0,0 +1,2 @@ +vucbot.yaml +vertobot.yaml diff --git a/contrib/vertobot/bot.pl b/contrib/vertobot/bot.pl new file mode 100755 index 000000000..828fc4878 --- /dev/null +++ b/contrib/vertobot/bot.pl @@ -0,0 +1,300 @@ +#!/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_002; +use JSON; +use YAML; +use Data::UUID; +use Getopt::Long; +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 $sessid = lc new Data::UUID->create_str(); + +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 + $bridgestate->{$room->room_id}->{sessid} = $sessid; + + $room->configure( + on_message => \&on_room_message, + ); + + my $f = send_verto_json_request("login", { + 'login' => $CONFIG{'verto-dialog-params'}{'login'}, + 'passwd' => $CONFIG{'verto-config'}{'passwd'}, + 'sessid' => $sessid, + }); + $matrix->adopt_future($f); + + # 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/; + + my $f = send_verto_json_request("verto.invite", { + "sdp" => $offer, + "dialogParams" => \%dp, + "sessid" => $bridgestate->{$room_id}->{sessid}, + }); + $matrix->adopt_future($f); + } + 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') { + if ($bridgestate->{$room_id}->{matrix_callid} eq $event->{content}->{call_id}) { + my $f = send_verto_json_request("verto.bye", { + "dialogParams" => \%dp, + "sessid" => $bridgestate->{$room_id}->{sessid}, + }); + $matrix->adopt_future($f); + } + else { + warn "Ignoring unrecognised callid: ".$event->{content}->{call_id}; + } + } + 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' && $json->{params}->{sdp}) || + $json->{method} eq 'verto.media') { + + my $room_id = $roomid_by_callid->{$json->{params}->{callID}}; + my $room = $bot_matrix_rooms{$room_id}; + + 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}) { + $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 000000000..04403670a --- /dev/null +++ b/contrib/vertobot/config.yaml @@ -0,0 +1,29 @@ +# Generic Matrix connection params +matrix: + server: 'matrix.org' + SSL: 1 + +# Bot-user connection details +matrix-bot: + user_id: '@vertobot:matrix.org' + password: '' + +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 000000000..c29fcaa6f --- /dev/null +++ b/contrib/vertobot/cpanfile @@ -0,0 +1,17 @@ +requires 'parent', 0; +requires 'Future', '>= 0.29'; +requires 'Net::Async::Matrix', '>= 0.11_002'; +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 000000000..e0230498a --- /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 +} diff --git a/scripts/check_signature.py b/scripts/check_signature.py index e146e18e2..59e3d603a 100644 --- a/scripts/check_signature.py +++ b/scripts/check_signature.py @@ -23,7 +23,7 @@ def get_targets(server_name): for srv in answers: yield (srv.target, srv.port) except dns.resolver.NXDOMAIN: - yield (server_name, 8480) + yield (server_name, 8448) def get_server_keys(server_name, target, port): url = "https://%s:%i/_matrix/key/v1" % (target, port) diff --git a/setup.py b/setup.py index 249819caf..34012bc7c 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ setup( description="Reference Synapse Home Server", install_requires=[ "syutil==0.0.2", - "matrix_angular_sdk==0.5.0", + "matrix_angular_sdk==0.5.3b", "Twisted>=14.0.0", "service_identity>=1.0.0", "pyopenssl>=0.14", @@ -45,8 +45,8 @@ setup( ], dependency_links=[ "https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2", - "https://github.com/pyca/pynacl/tarball/52dbe2dc33f1#egg=pynacl-0.3.0", - "https://github.com/matrix-org/matrix-angular-sdk/tarball/v0.5.0/#egg=matrix_angular_sdk-0.5.0", + "https://github.com/pyca/pynacl/tarball/d4d3175589b892f6ea7c22f466e0e223853516fa#egg=pynacl-0.3.0", + "https://github.com/matrix-org/matrix-angular-sdk/tarball/v0.5.3b/#egg=matrix_angular_sdk-0.5.3b", ], setup_requires=[ "setuptools_trial", @@ -60,6 +60,6 @@ setup( entry_points=""" [console_scripts] synctl=synapse.app.synctl:main - synapse-homeserver=synapse.app.homeserver:run + synapse-homeserver=synapse.app.homeserver:main """ ) diff --git a/synapse/__init__.py b/synapse/__init__.py index 14564e735..723e15d50 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.0" +__version__ = "0.5.4" diff --git a/synapse/api/auth.py b/synapse/api/auth.py index c4597c175..2b0475543 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -38,79 +38,66 @@ class Auth(object): self.store = hs.get_datastore() self.state = hs.get_state_handler() - def check(self, event, raises=False): + def check(self, event, auth_events): """ Checks if this event is correctly authed. Returns: True if the auth checks pass. - Raises: - AuthError if there was a problem authorising this event. This will - be raised only if raises=True. """ try: - if hasattr(event, "room_id"): - if event.old_state_events is None: - # Oh, we don't know what the state of the room was, so we - # are trusting that this is allowed (at least for now) - logger.warn("Trusting event: %s", event.event_id) - return True - - if hasattr(event, "outlier") and event.outlier is True: - # TODO (erikj): Auth for outliers is done differently. - return True - - if event.type == RoomCreateEvent.TYPE: - # FIXME - return True - - # FIXME: Temp hack - if event.type == RoomAliasesEvent.TYPE: - return True - - if event.type == RoomMemberEvent.TYPE: - allowed = self.is_membership_change_allowed(event) - if allowed: - logger.debug("Allowing! %s", event) - else: - logger.debug("Denying! %s", event) - return allowed - - self.check_event_sender_in_room(event) - self._can_send_event(event) - - if event.type == RoomPowerLevelsEvent.TYPE: - self._check_power_levels(event) - - if event.type == RoomRedactionEvent.TYPE: - self._check_redaction(event) - - logger.debug("Allowing! %s", event) + if not hasattr(event, "room_id"): + raise AuthError(500, "Event has no room_id: %s" % event) + if auth_events is None: + # Oh, we don't know what the state of the room was, so we + # are trusting that this is allowed (at least for now) + logger.warn("Trusting event: %s", event.event_id) return True - else: - raise AuthError(500, "Unknown event: %s" % event) + + if event.type == RoomCreateEvent.TYPE: + # FIXME + return True + + # FIXME: Temp hack + if event.type == RoomAliasesEvent.TYPE: + return True + + if event.type == RoomMemberEvent.TYPE: + allowed = self.is_membership_change_allowed( + event, auth_events + ) + if allowed: + logger.debug("Allowing! %s", event) + else: + logger.debug("Denying! %s", event) + return allowed + + self.check_event_sender_in_room(event, auth_events) + self._can_send_event(event, auth_events) + + if event.type == RoomPowerLevelsEvent.TYPE: + self._check_power_levels(event, auth_events) + + if event.type == RoomRedactionEvent.TYPE: + self._check_redaction(event, auth_events) + + logger.debug("Allowing! %s", event) except AuthError as e: logger.info( "Event auth check failed on event %s with msg: %s", event, e.msg ) logger.info("Denying! %s", event) - if raises: - raise - - return False + raise @defer.inlineCallbacks def check_joined_room(self, room_id, user_id): - try: - member = yield self.store.get_room_member( - room_id=room_id, - user_id=user_id - ) - self._check_joined_room(member, user_id, room_id) - defer.returnValue(member) - except AttributeError: - pass - defer.returnValue(None) + member = yield self.state.get_current_state( + room_id=room_id, + event_type=RoomMemberEvent.TYPE, + state_key=user_id + ) + self._check_joined_room(member, user_id, room_id) + defer.returnValue(member) @defer.inlineCallbacks def check_host_in_room(self, room_id, host): @@ -130,9 +117,9 @@ class Auth(object): defer.returnValue(False) - def check_event_sender_in_room(self, event): + def check_event_sender_in_room(self, event, auth_events): key = (RoomMemberEvent.TYPE, event.user_id, ) - member_event = event.state_events.get(key) + member_event = auth_events.get(key) return self._check_joined_room( member_event, @@ -147,14 +134,14 @@ class Auth(object): )) @log_function - def is_membership_change_allowed(self, event): + def is_membership_change_allowed(self, event, auth_events): membership = event.content["membership"] # Check if this is the room creator joining: if len(event.prev_events) == 1 and Membership.JOIN == membership: # Get room creation event: key = (RoomCreateEvent.TYPE, "", ) - create = event.old_state_events.get(key) + create = auth_events.get(key) if create and event.prev_events[0][0] == create.event_id: if create.content["creator"] == event.state_key: return True @@ -163,19 +150,19 @@ class Auth(object): # get info about the caller key = (RoomMemberEvent.TYPE, event.user_id, ) - caller = event.old_state_events.get(key) + caller = auth_events.get(key) caller_in_room = caller and caller.membership == Membership.JOIN caller_invited = caller and caller.membership == Membership.INVITE # get info about the target key = (RoomMemberEvent.TYPE, target_user_id, ) - target = event.old_state_events.get(key) + target = auth_events.get(key) target_in_room = target and target.membership == Membership.JOIN key = (RoomJoinRulesEvent.TYPE, "", ) - join_rule_event = event.old_state_events.get(key) + join_rule_event = auth_events.get(key) if join_rule_event: join_rule = join_rule_event.content.get( "join_rule", JoinRules.INVITE @@ -186,11 +173,13 @@ class Auth(object): user_level = self._get_power_level_from_event_state( event, event.user_id, + auth_events, ) ban_level, kick_level, redact_level = ( self._get_ops_level_from_event_state( - event + event, + auth_events, ) ) @@ -213,7 +202,10 @@ class Auth(object): # Invites are valid iff caller is in the room and target isn't. if not caller_in_room: # caller isn't joined - raise AuthError(403, "You are not in room %s." % event.room_id) + raise AuthError( + 403, + "%s not in room %s." % (event.user_id, event.room_id,) + ) elif target_in_room: # the target is already in the room. raise AuthError(403, "%s is already in the room." % target_user_id) @@ -236,7 +228,10 @@ class Auth(object): # TODO (erikj): Implement kicks. if not caller_in_room: # trying to leave a room you aren't joined - raise AuthError(403, "You are not in room %s." % event.room_id) + raise AuthError( + 403, + "%s not in room %s." % (target_user_id, event.room_id,) + ) elif target_user_id != event.user_id: if kick_level: kick_level = int(kick_level) @@ -260,9 +255,9 @@ class Auth(object): return True - def _get_power_level_from_event_state(self, event, user_id): + def _get_power_level_from_event_state(self, event, user_id, auth_events): key = (RoomPowerLevelsEvent.TYPE, "", ) - power_level_event = event.old_state_events.get(key) + power_level_event = auth_events.get(key) level = None if power_level_event: level = power_level_event.content.get("users", {}).get(user_id) @@ -270,16 +265,16 @@ class Auth(object): level = power_level_event.content.get("users_default", 0) else: key = (RoomCreateEvent.TYPE, "", ) - create_event = event.old_state_events.get(key) + create_event = auth_events.get(key) if (create_event is not None and create_event.content["creator"] == user_id): return 100 return level - def _get_ops_level_from_event_state(self, event): + def _get_ops_level_from_event_state(self, event, auth_events): key = (RoomPowerLevelsEvent.TYPE, "", ) - power_level_event = event.old_state_events.get(key) + power_level_event = auth_events.get(key) if power_level_event: return ( @@ -375,6 +370,11 @@ class Auth(object): key = (RoomMemberEvent.TYPE, event.user_id, ) member_event = event.old_state_events.get(key) + key = (RoomCreateEvent.TYPE, "", ) + create_event = event.old_state_events.get(key) + if create_event: + auth_events.append(create_event.event_id) + if join_rule_event: join_rule = join_rule_event.content.get("join_rule") is_public = join_rule == JoinRules.PUBLIC if join_rule else False @@ -406,9 +406,9 @@ class Auth(object): event.auth_events = zip(auth_events, hashes) @log_function - def _can_send_event(self, event): + def _can_send_event(self, event, auth_events): key = (RoomPowerLevelsEvent.TYPE, "", ) - send_level_event = event.old_state_events.get(key) + send_level_event = auth_events.get(key) send_level = None if send_level_event: send_level = send_level_event.content.get("events", {}).get( @@ -432,6 +432,7 @@ class Auth(object): user_level = self._get_power_level_from_event_state( event, event.user_id, + auth_events, ) if user_level: @@ -468,14 +469,16 @@ class Auth(object): return True - def _check_redaction(self, event): + def _check_redaction(self, event, auth_events): user_level = self._get_power_level_from_event_state( event, event.user_id, + auth_events, ) _, _, redact_level = self._get_ops_level_from_event_state( - event + event, + auth_events, ) if user_level < redact_level: @@ -484,7 +487,7 @@ class Auth(object): "You don't have permission to redact events" ) - def _check_power_levels(self, event): + def _check_power_levels(self, event, auth_events): user_list = event.content.get("users", {}) # Validate users for k, v in user_list.items(): @@ -499,7 +502,7 @@ class Auth(object): raise SynapseError(400, "Not a valid power level: %s" % (v,)) key = (event.type, event.state_key, ) - current_state = event.old_state_events.get(key) + current_state = auth_events.get(key) if not current_state: return @@ -507,6 +510,7 @@ class Auth(object): user_level = self._get_power_level_from_event_state( event, event.user_id, + auth_events, ) # Check other levels: diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index 8a35b4cb7..22939d011 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -125,6 +125,7 @@ class SynapseEvent(JsonEncodedObject): pdu_json.pop("outlier", None) pdu_json.pop("replaces_state", None) pdu_json.pop("redacted", None) + pdu_json.pop("prev_content", None) state_hash = pdu_json.pop("state_hash", None) if state_hash is not None: pdu_json.setdefault("unsigned", {})["state_hash"] = state_hash diff --git a/synapse/config/logger.py b/synapse/config/logger.py index 856629643..089d906fa 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) diff --git a/synapse/config/server.py b/synapse/config/server.py index 814a4c349..f8a0844b8 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 diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py index fa2463d4a..01f87fe42 100644 --- a/synapse/federation/replication.py +++ b/synapse/federation/replication.py @@ -281,6 +281,22 @@ class ReplicationLayer(object): defer.returnValue(pdus) + @defer.inlineCallbacks + @log_function + def get_event_auth(self, destination, context, event_id): + res = yield self.transport_layer.get_event_auth( + destination, context, event_id, + ) + + auth_chain = [ + self.event_from_pdu_json(p, outlier=True) + for p in res["auth_chain"] + ] + + auth_chain.sort(key=lambda e: e.depth) + + defer.returnValue(auth_chain) + @defer.inlineCallbacks @log_function def on_backfill_request(self, origin, context, versions, limit): @@ -481,11 +497,17 @@ class ReplicationLayer(object): # FIXME: We probably want to do something with the auth_chain given # to us - # auth_chain = [ - # Pdu(outlier=True, **p) for p in content.get("auth_chain", []) - # ] + auth_chain = [ + self.event_from_pdu_json(p, outlier=True) + for p in content.get("auth_chain", []) + ] - defer.returnValue(state) + auth_chain.sort(key=lambda e: e.depth) + + defer.returnValue({ + "state": state, + "auth_chain": auth_chain, + }) @defer.inlineCallbacks def send_invite(self, destination, context, event_id, pdu): @@ -543,20 +565,34 @@ class ReplicationLayer(object): state = None # We need to make sure we have all the auth events. - for e_id, _ in pdu.auth_events: - exists = yield self._get_persisted_pdu( - origin, - e_id, - do_auth=False - ) - - if not exists: - yield self.get_pdu( - origin, - event_id=e_id, - outlier=True, - ) - logger.debug("Processed pdu %s", e_id) + # for e_id, _ in pdu.auth_events: + # exists = yield self._get_persisted_pdu( + # origin, + # e_id, + # do_auth=False + # ) + # + # if not exists: + # try: + # logger.debug( + # "_handle_new_pdu fetch missing auth event %s from %s", + # e_id, + # origin, + # ) + # + # yield self.get_pdu( + # origin, + # event_id=e_id, + # outlier=True, + # ) + # + # logger.debug("Processed pdu %s", e_id) + # except: + # logger.warn( + # "Failed to get auth event %s from %s", + # e_id, + # origin + # ) # Get missing pdus if necessary. if not pdu.outlier: @@ -565,6 +601,11 @@ class ReplicationLayer(object): pdu.room_id ) + logger.debug( + "_handle_new_pdu min_depth for %s: %d", + pdu.room_id, min_depth + ) + if min_depth and pdu.depth > min_depth: for event_id, hashes in pdu.prev_events: exists = yield self._get_persisted_pdu( @@ -574,11 +615,14 @@ class ReplicationLayer(object): ) if not exists: - logger.debug("Requesting pdu %s", event_id) + logger.debug( + "_handle_new_pdu requesting pdu %s", + event_id + ) try: yield self.get_pdu( - pdu.origin, + origin, event_id=event_id, ) logger.debug("Processed pdu %s", event_id) @@ -588,12 +632,17 @@ class ReplicationLayer(object): else: # We need to get the state at this event, since we have reached # a backward extremity edge. + logger.debug( + "_handle_new_pdu getting state for %s", + pdu.room_id + ) state = yield self.get_state_for_context( origin, pdu.room_id, pdu.event_id, ) if not backfilled: ret = yield self.handler.on_receive_pdu( + origin, pdu, backfilled=backfilled, state=state, @@ -804,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) @@ -816,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 diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index d53cd3df3..15adc9dc2 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -78,7 +78,7 @@ class BaseHandler(object): if not suppress_auth: logger.debug("Authing...") - self.auth.check(event, raises=True) + self.auth.check(event, auth_events=event.old_state_events) logger.debug("Authed") else: logger.debug("Suppressed auth.") diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index d59221a4f..02202692d 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -53,8 +53,12 @@ class EventStreamHandler(BaseHandler): if auth_user not in self._streams_per_user: self._streams_per_user[auth_user] = 0 if auth_user in self._stop_timer_per_user: - self.clock.cancel_call_later( - self._stop_timer_per_user.pop(auth_user)) + try: + self.clock.cancel_call_later( + self._stop_timer_per_user.pop(auth_user) + ) + except: + logger.exception("Failed to cancel event timer") else: yield self.distributor.fire( "started_user_eventstream", auth_user @@ -95,10 +99,12 @@ class EventStreamHandler(BaseHandler): logger.debug( "_later stopped_user_eventstream %s", auth_user ) + + self._stop_timer_per_user.pop(auth_user, None) + yield self.distributor.fire( "stopped_user_eventstream", auth_user ) - del self._stop_timer_per_user[auth_user] logger.debug("Scheduling _later: for %s", auth_user) self._stop_timer_per_user[auth_user] = ( diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f601de448..925eb5376 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -18,13 +18,16 @@ from ._base import BaseHandler from synapse.api.events.utils import prune_event -from synapse.api.errors import AuthError, FederationError, SynapseError -from synapse.api.events.room import RoomMemberEvent +from synapse.api.errors import ( + AuthError, FederationError, SynapseError, StoreError, +) +from synapse.api.events.room import RoomMemberEvent, RoomCreateEvent from synapse.api.constants import Membership from synapse.util.logutils import log_function from synapse.util.async import run_on_reactor from synapse.crypto.event_signing import ( - compute_event_signature, check_event_content_hash + compute_event_signature, check_event_content_hash, + add_hashes_and_signatures, ) from syutil.jsonutil import encode_canonical_json @@ -98,7 +101,7 @@ class FederationHandler(BaseHandler): @log_function @defer.inlineCallbacks - def on_receive_pdu(self, pdu, backfilled, state=None): + def on_receive_pdu(self, origin, pdu, backfilled, state=None): """ Called by the ReplicationLayer when we have a new pdu. We need to do auth checks and put it through the StateHandler. """ @@ -109,7 +112,7 @@ class FederationHandler(BaseHandler): # If we are currently in the process of joining this room, then we # queue up events for later processing. if event.room_id in self.room_queues: - self.room_queues[event.room_id].append(pdu) + self.room_queues[event.room_id].append((pdu, origin)) return logger.debug("Processing event: %s", event.event_id) @@ -141,15 +144,62 @@ class FederationHandler(BaseHandler): ) event = redacted_event - is_new_state = yield self.state_handler.annotate_event_with_state( - event, - old_state=state - ) - logger.debug("Event: %s", event) + # FIXME (erikj): Awful hack to make the case where we are not currently + # in the room work + current_state = None + is_in_room = yield self.auth.check_host_in_room( + event.room_id, + self.server_name + ) + if not is_in_room and not event.outlier: + logger.debug("Got event for room we're not in.") + + replication_layer = self.replication_layer + auth_chain = yield replication_layer.get_event_auth( + origin, + context=event.room_id, + event_id=event.event_id, + ) + + for e in auth_chain: + e.outlier = True + try: + yield self._handle_new_event(e, fetch_missing=False) + except: + logger.exception( + "Failed to parse auth event %s", + e.event_id, + ) + + if not state: + state = yield replication_layer.get_state_for_context( + origin, + context=event.room_id, + event_id=event.event_id, + ) + + current_state = state + + if state: + for e in state: + e.outlier = True + try: + yield self._handle_new_event(e) + except: + logger.exception( + "Failed to parse state event %s", + e.event_id, + ) + try: - self.auth.check(event, raises=True) + yield self._handle_new_event( + event, + state=state, + backfilled=backfilled, + current_state=current_state, + ) except AuthError as e: raise FederationError( "ERROR", @@ -158,43 +208,17 @@ class FederationHandler(BaseHandler): affected=event.event_id, ) - is_new_state = is_new_state and not backfilled - - # TODO: Implement something in federation that allows us to - # respond to PDU. - - yield self.store.persist_event( - event, - backfilled, - is_new_state=is_new_state - ) - room = yield self.store.get_room(event.room_id) if not room: - # Huh, let's try and get the current state try: - yield self.replication_layer.get_state_for_context( - event.origin, event.room_id, event.event_id, - ) - - hosts = yield self.store.get_joined_hosts_for_room( - event.room_id - ) - if self.hs.hostname in hosts: - try: - yield self.store.store_room( - room_id=event.room_id, - room_creator_user_id="", - is_public=False, - ) - except: - pass - except: - logger.exception( - "Failed to get current state for room %s", - event.room_id + yield self.store.store_room( + room_id=event.room_id, + room_creator_user_id="", + is_public=False, ) + except StoreError: + logger.exception("Failed to store room.") if not backfilled: extra_users = [] @@ -255,11 +279,23 @@ class FederationHandler(BaseHandler): pdu=event ) + + defer.returnValue(pdu) @defer.inlineCallbacks def on_event_auth(self, event_id): auth = yield self.store.get_auth_chain(event_id) + + for event in auth: + event.signatures.update( + compute_event_signature( + event, + self.hs.hostname, + self.hs.config.signing_key[0] + ) + ) + defer.returnValue([e for e in auth]) @log_function @@ -276,6 +312,8 @@ class FederationHandler(BaseHandler): We suspend processing of any received events from this room until we have finished processing the join. """ + logger.debug("Joining %s to %s", joinee, room_id) + pdu = yield self.replication_layer.make_join( target_host, room_id, @@ -298,19 +336,29 @@ class FederationHandler(BaseHandler): try: event.event_id = self.event_factory.create_event_id() + event.origin = self.hs.hostname event.content = content - state = yield self.replication_layer.send_join( + if not hasattr(event, "signatures"): + event.signatures = {} + + add_hashes_and_signatures( + event, + self.hs.hostname, + self.hs.config.signing_key[0], + ) + + ret = yield self.replication_layer.send_join( target_host, event ) - logger.debug("do_invite_join state: %s", state) + state = ret["state"] + auth_chain = ret["auth_chain"] + auth_chain.sort(key=lambda e: e.depth) - yield self.state_handler.annotate_event_with_state( - event, - old_state=state - ) + logger.debug("do_invite_join auth_chain: %s", auth_chain) + logger.debug("do_invite_join state: %s", state) logger.debug("do_invite_join event: %s", event) @@ -324,34 +372,50 @@ class FederationHandler(BaseHandler): # FIXME pass + for e in auth_chain: + e.outlier = True + try: + yield self._handle_new_event(e, fetch_missing=False) + except: + logger.exception( + "Failed to parse auth event %s", + e.event_id, + ) + for e in state: # FIXME: Auth these. e.outlier = True + try: + yield self._handle_new_event( + e, + fetch_missing=True + ) + except: + logger.exception( + "Failed to parse state event %s", + e.event_id, + ) - yield self.state_handler.annotate_event_with_state( - e, - ) - - yield self.store.persist_event( - e, - backfilled=False, - is_new_state=True - ) - - yield self.store.persist_event( + yield self._handle_new_event( event, - backfilled=False, - is_new_state=True + state=state, + current_state=state, ) + + yield self.notifier.on_new_room_event( + event, extra_users=[joinee] + ) + + logger.debug("Finished joining %s to %s", joinee, room_id) finally: room_queue = self.room_queues[room_id] del self.room_queues[room_id] - for p in room_queue: + for p, origin in room_queue: try: - yield self.on_receive_pdu(p, backfilled=False) + self.on_receive_pdu(origin, p, backfilled=False) except: - pass + logger.exception("Couldn't handle pdu") defer.returnValue(True) @@ -375,7 +439,7 @@ class FederationHandler(BaseHandler): yield self.state_handler.annotate_event_with_state(event) yield self.auth.add_auth_events(event) - self.auth.check(event, raises=True) + self.auth.check(event, auth_events=event.old_state_events) pdu = event @@ -391,17 +455,7 @@ class FederationHandler(BaseHandler): event.outlier = False - state_handler = self.state_handler - is_new_state = yield state_handler.annotate_event_with_state(event) - self.auth.check(event, raises=True) - - # FIXME (erikj): All this is duplicated above :( - - yield self.store.persist_event( - event, - backfilled=False, - is_new_state=is_new_state - ) + yield self._handle_new_event(event) extra_users = [] if event.type == RoomMemberEvent.TYPE: @@ -414,7 +468,7 @@ class FederationHandler(BaseHandler): ) if event.type == RoomMemberEvent.TYPE: - if event.membership == Membership.JOIN: + if event.content["membership"] == Membership.JOIN: user = self.hs.parse_userid(event.state_key) yield self.distributor.fire( "user_joined_room", user=user, room_id=event.room_id @@ -508,7 +562,17 @@ class FederationHandler(BaseHandler): else: del results[(event.type, event.state_key)] - defer.returnValue(results.values()) + res = results.values() + for event in res: + event.signatures.update( + compute_event_signature( + event, + self.hs.hostname, + self.hs.config.signing_key[0] + ) + ) + + defer.returnValue(res) else: defer.returnValue([]) @@ -541,6 +605,17 @@ class FederationHandler(BaseHandler): ) if event: + # FIXME: This is a temporary work around where we occasionally + # return events slightly differently than when they were + # originally signed + event.signatures.update( + compute_event_signature( + event, + self.hs.hostname, + self.hs.config.signing_key[0] + ) + ) + if do_auth: in_room = yield self.auth.check_host_in_room( event.room_id, @@ -565,3 +640,78 @@ class FederationHandler(BaseHandler): ) while waiters: waiters.pop().callback(None) + + @defer.inlineCallbacks + def _handle_new_event(self, event, state=None, backfilled=False, + current_state=None, fetch_missing=True): + is_new_state = yield self.state_handler.annotate_event_with_state( + event, + old_state=state + ) + + if event.old_state_events: + known_ids = set( + [s.event_id for s in event.old_state_events.values()] + ) + for e_id, _ in event.auth_events: + if e_id not in known_ids: + e = yield self.store.get_event( + e_id, + allow_none=True, + ) + + if not e: + # TODO: Do some conflict res to make sure that we're + # not the ones who are wrong. + logger.info( + "Rejecting %s as %s not in %s", + event.event_id, e_id, known_ids, + ) + raise AuthError(403, "Auth events are stale") + + auth_events = event.old_state_events + else: + # We need to get the auth events from somewhere. + + # TODO: Don't just hit the DBs? + + auth_events = {} + for e_id, _ in event.auth_events: + e = yield self.store.get_event( + e_id, + allow_none=True, + ) + + if not e: + e = yield self.replication_layer.get_pdu( + event.origin, e_id, outlier=True + ) + + if e and fetch_missing: + try: + yield self.on_receive_pdu(event.origin, e, False) + except: + logger.exception( + "Failed to parse auth event %s", + e_id, + ) + + if not e: + logger.warn("Can't find auth event %s.", e_id) + + auth_events[(e.type, e.state_key)] = e + + if event.type == RoomMemberEvent.TYPE and not event.auth_events: + if len(event.prev_events) == 1: + c = yield self.store.get_event(event.prev_events[0][0]) + if c.type == RoomCreateEvent.TYPE: + auth_events[(c.type, c.state_key)] = c + + self.auth.check(event, auth_events=auth_events) + + yield self.store.persist_event( + event, + backfilled=backfilled, + is_new_state=(is_new_state and not backfilled), + current_state=current_state, + ) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index ae0fc43ca..42dc4d46f 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -306,7 +306,7 @@ class MessageHandler(BaseHandler): auth_user = self.hs.parse_userid(user_id) # TODO: These concurrently - state_tuples = yield self.store.get_current_state(room_id) + state_tuples = yield self.state_handler.get_current_state(room_id) state = [self.hs.serialize_event(x) for x in state_tuples] member_event = (yield self.store.get_room_member( diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index b55d589da..84a039489 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -651,12 +651,13 @@ 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 not observers and not room_ids: - continue + if room_ids: + logger.debug(" | %d interested room IDs %r", len(room_ids), room_ids) state = dict(push) del state["user_id"] @@ -678,6 +679,10 @@ class PresenceHandler(BaseHandler): self._user_cachemap_latest_serial += 1 statuscache.update(state, serial=self._user_cachemap_latest_serial) + if not observers and not room_ids: + logger.debug(" | no interested observers or room IDs") + continue + self.push_update_to_clients( observed_user=user, users_to_push=observers, @@ -799,6 +804,7 @@ class PresenceEventSource(object): ) @defer.inlineCallbacks + @log_function def get_new_events_for_user(self, user, from_key, limit): from_key = int(from_key) @@ -811,7 +817,8 @@ class PresenceEventSource(object): # TODO(paul): use a DeferredList ? How to limit concurrency. for observed_user in cachemap.keys(): cached = cachemap[observed_user] - if not (from_key < cached.serial): + + if cached.serial <= from_key: continue if (yield self.is_visible(observer_user, observed_user)): diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 88955160c..a000b4403 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, diff --git a/synapse/rest/room.py b/synapse/rest/room.py index 4f6d039b6..3147d7a60 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -148,7 +148,7 @@ class RoomStateEventRestServlet(RestServlet): content = _parse_json(request) event = self.event_factory.create_event( - etype=urllib.unquote(event_type), + etype=event_type, # already urldecoded content=content, room_id=urllib.unquote(room_id), user_id=user.to_string(), @@ -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/state.py b/synapse/state.py index 1c999e4d7..430665f7b 100644 --- a/synapse/state.py +++ b/synapse/state.py @@ -82,7 +82,7 @@ class StateHandler(object): if hasattr(event, "outlier") and event.outlier: event.state_group = None event.old_state_events = None - event.state_events = {} + event.state_events = None defer.returnValue(False) return diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index f6811a811..e4bdad98b 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -69,7 +69,7 @@ SCHEMAS = [ # Remember to update this number every time an incompatible change is made to # database schema files, so the users will be informed on server restarts. -SCHEMA_VERSION = 7 +SCHEMA_VERSION = 8 class _RollbackButIsFineException(Exception): @@ -97,7 +97,8 @@ class DataStore(RoomMemberStore, RoomStore, @defer.inlineCallbacks @log_function - def persist_event(self, event, backfilled=False, is_new_state=True): + def persist_event(self, event, backfilled=False, is_new_state=True, + current_state=None): stream_ordering = None if backfilled: if not self.min_token_deferred.called: @@ -113,6 +114,7 @@ class DataStore(RoomMemberStore, RoomStore, backfilled=backfilled, stream_ordering=stream_ordering, is_new_state=is_new_state, + current_state=current_state, ) except _RollbackButIsFineException: pass @@ -141,7 +143,7 @@ class DataStore(RoomMemberStore, RoomStore, @log_function def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None, - is_new_state=True): + is_new_state=True, current_state=None): if event.type == RoomMemberEvent.TYPE: self._store_room_member_txn(txn, event) elif event.type == FeedbackEvent.TYPE: @@ -210,8 +212,27 @@ class DataStore(RoomMemberStore, RoomStore, self._store_state_groups_txn(txn, event) + if current_state: + txn.execute( + "DELETE FROM current_state_events WHERE room_id = ?", + (event.room_id,) + ) + + for s in current_state: + self._simple_insert_txn( + txn, + "current_state_events", + { + "event_id": s.event_id, + "room_id": s.room_id, + "type": s.type, + "state_key": s.state_key, + }, + or_replace=True, + ) + is_state = hasattr(event, "state_key") and event.state_key is not None - if is_new_state and is_state: + if is_state: vals = { "event_id": event.event_id, "room_id": event.room_id, @@ -229,17 +250,18 @@ class DataStore(RoomMemberStore, RoomStore, or_replace=True, ) - self._simple_insert_txn( - txn, - "current_state_events", - { - "event_id": event.event_id, - "room_id": event.room_id, - "type": event.type, - "state_key": event.state_key, - }, - or_replace=True, - ) + if is_new_state: + self._simple_insert_txn( + txn, + "current_state_events", + { + "event_id": event.event_id, + "room_id": event.room_id, + "type": event.type, + "state_key": event.state_key, + }, + or_replace=True, + ) for e_id, h in event.prev_state: self._simple_insert_txn( @@ -316,7 +338,12 @@ class DataStore(RoomMemberStore, RoomStore, txn, event.event_id, ref_alg, ref_hash_bytes ) - self._update_min_depth_for_room_txn(txn, event.room_id, event.depth) + if not outlier: + self._update_min_depth_for_room_txn( + txn, + event.room_id, + event.depth + ) def _store_redaction(self, txn, event): txn.execute( diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index c37df59d4..05b275663 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) diff --git a/synapse/storage/schema/delta/v8.sql b/synapse/storage/schema/delta/v8.sql new file mode 100644 index 000000000..daf6646ed --- /dev/null +++ b/synapse/storage/schema/delta/v8.sql @@ -0,0 +1,34 @@ +/* Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + CREATE TABLE IF NOT EXISTS event_signatures_2 ( + event_id TEXT, + signature_name TEXT, + key_id TEXT, + signature BLOB, + CONSTRAINT uniqueness UNIQUE (event_id, signature_name, key_id) +); + +INSERT INTO event_signatures_2 (event_id, signature_name, key_id, signature) +SELECT event_id, signature_name, key_id, signature FROM event_signatures; + +DROP TABLE event_signatures; +ALTER TABLE event_signatures_2 RENAME TO event_signatures; + +CREATE INDEX IF NOT EXISTS event_signatures_id ON event_signatures ( + event_id +); + +PRAGMA user_version = 8; \ No newline at end of file diff --git a/synapse/storage/schema/event_signatures.sql b/synapse/storage/schema/event_signatures.sql index 4efa8a3e6..b6b56b47a 100644 --- a/synapse/storage/schema/event_signatures.sql +++ b/synapse/storage/schema/event_signatures.sql @@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS event_signatures ( signature_name TEXT, key_id TEXT, signature BLOB, - CONSTRAINT uniqueness UNIQUE (event_id, key_id) + CONSTRAINT uniqueness UNIQUE (event_id, signature_name, key_id) ); CREATE INDEX IF NOT EXISTS event_signatures_id ON event_signatures ( diff --git a/synapse/storage/state.py b/synapse/storage/state.py index 55ea56779..e0f44b3e5 100644 --- a/synapse/storage/state.py +++ b/synapse/storage/state.py @@ -87,7 +87,7 @@ class StateStore(SQLBaseStore): ) def _store_state_groups_txn(self, txn, event): - if not event.state_events: + if event.state_events is None: return state_group = event.state_group diff --git a/synapse/storage/stream.py b/synapse/storage/stream.py index b84735e61..3405cb365 100644 --- a/synapse/storage/stream.py +++ b/synapse/storage/stream.py @@ -283,7 +283,7 @@ class StreamStore(SQLBaseStore): sql = ( "SELECT *, (%(redacted)s) AS redacted FROM events " - "WHERE room_id = ? AND stream_ordering <= ? " + "WHERE room_id = ? AND stream_ordering <= ? AND outlier = 0 " "ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? " ) % { "redacted": del_sql, diff --git a/synapse/streams/config.py b/synapse/streams/config.py index 0317e78c0..2114c940e 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: diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index 3487a090e..33016c16e 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -42,6 +42,7 @@ class FederationTestCase(unittest.TestCase): self.auth = NonCallableMock(spec_set=[ "check", + "check_host_in_room", ]) self.hostname = "test" @@ -83,20 +84,25 @@ class FederationTestCase(unittest.TestCase): event_id="$a:b", user_id="@a:b", origin="b", + auth_events=[], hashes={"sha256":"AcLrgtUIqqwaGoHhrEvYG1YLDIsVPYJdSRGhkp3jJp8"}, ) self.datastore.persist_event.return_value = defer.succeed(None) self.datastore.get_room.return_value = defer.succeed(True) + self.auth.check_host_in_room.return_value = defer.succeed(True) - self.state_handler.annotate_event_with_state.return_value = ( - defer.succeed(False) + def annotate(ev, old_state=None): + ev.old_state_events = [] + return defer.succeed(False) + self.state_handler.annotate_event_with_state.side_effect = annotate + + yield self.handlers.federation_handler.on_receive_pdu( + "fo", pdu, False ) - yield self.handlers.federation_handler.on_receive_pdu(pdu, False) - self.datastore.persist_event.assert_called_once_with( - ANY, False, is_new_state=False + ANY, is_new_state=False, backfilled=False, current_state=None ) self.state_handler.annotate_event_with_state.assert_called_once_with( @@ -104,7 +110,7 @@ class FederationTestCase(unittest.TestCase): old_state=None, ) - self.auth.check.assert_called_once_with(ANY, raises=True) + self.auth.check.assert_called_once_with(ANY, auth_events={}) self.notifier.on_new_room_event.assert_called_once_with( ANY, diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index cbe591ab9..0279ab703 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -120,7 +120,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase): self.datastore.get_room_member.return_value = defer.succeed(None) - event.state_events = { + event.old_state_events = { (RoomMemberEvent.TYPE, "@alice:green"): self._create_member( user_id="@alice:green", room_id=room_id, @@ -129,9 +129,11 @@ class RoomMemberHandlerTestCase(unittest.TestCase): user_id="@bob:red", room_id=room_id, ), - (RoomMemberEvent.TYPE, target_user_id): event, } + event.state_events = event.old_state_events + event.state_events[(RoomMemberEvent.TYPE, target_user_id)] = event + # Actual invocation yield self.room_member_handler.change_membership(event) @@ -187,6 +189,16 @@ class RoomMemberHandlerTestCase(unittest.TestCase): (RoomMemberEvent.TYPE, user_id): event, } + event.old_state_events = { + (RoomMemberEvent.TYPE, "@alice:green"): self._create_member( + user_id="@alice:green", + room_id=room_id, + ), + } + + event.state_events = event.old_state_events + event.state_events[(RoomMemberEvent.TYPE, user_id)] = event + # Actual invocation yield self.room_member_handler.change_membership(event)