diff --git a/src/common/jingle.py b/src/common/jingle.py new file mode 100644 index 000000000..49b869a5f --- /dev/null +++ b/src/common/jingle.py @@ -0,0 +1,345 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles the jingle signalling protocol. ''' + +import xmpp + +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended=0 + pending=1 + active=2 + +class WrongState(exception): pass +class NoCommonCodec(exception): pass + +class JingleSession(object): + ''' This represents one jingle session. ''' + def __init__(self, con, weinitiate, jid): + ''' con -- connection object, + weinitiate -- boolean, are we the initiator? + jid - jid of the other entity''' + self.contents={} # negotiated contents + self.connection=con # connection to use + # our full jid + self.ourjid=gajim.get_full_jid_from_account(self.connection.name) + self.jid=jid # jid we connect to + # jid we use as the initiator + self.initiator=weinitiate and self.ourjid or self.jid + # jid we use as the responder + self.responder=weinitiate and self.jid or self.ourjid + # are we an initiator? + self.weinitiate=weinitiate + # what state is session in? (one from JingleStates) + self.state=JingleStates.ended + self.sid=con.getAnID() # sessionid + + # callbacks to call on proper contents + # use .prepend() to add new callbacks + self.callbacks=dict((key, [self.__defaultCB]) for key in + ('content-accept', 'content-add', 'content-modify', + 'content-remove', 'session-accept', 'session-info', + 'session-initiate', 'session-terminate', + 'transport-info')) + self.callbacks['iq-result']=[] + self.callbacks['iq-error']=[] + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def addContent(self, name, description, transport, profile=None): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + The protocol prohibits changing that when pending.''' + if self.state==JingleStates.pending: + raise WrongState + + content={'creator': 'initiator', + 'name': name, + 'description': description, + 'transport': transport} + if profile is not None: + content['profile']=profile + self.contents[('initiator', name)]=content + + if self.state==JingleStates.active: + pass # TODO: send proper stanza, shouldn't be needed now + + ''' Middle-level function to do stanza exchange. ''' + def startSession(self): + ''' Start session. ''' + self.__sessionInitiate(self) + + def sendSessionInfo(self): pass + def sendTransportInfo(self): pass + + ''' Callbacks. ''' + def stanzaCB(self, stanza): + ''' A callback for ConnectionJingle. It gets stanza, then + tries to send it to all internally registered callbacks. + First one to raise xmpp.NodeProcessed breaks function.''' + jingle = stanza.getTag('jingle') + error = stanza.getTag('error') + if error: + # it's an iq-error stanza + callables = 'iq-error' + else if jingle: + # it's a jingle action + action = jingle.getAttr('action') + callables = action + else: + # it's an iq-result (ack) stanza + callables = 'iq-result' + + callables = self.callbacks[callables] + + try: + for callable in callables: + callable(stanza=stanza, jingle=jingle, error=error) + except xmpp.NodeProcessed: + pass + + def __defaultCB(self, stanza, jingle, error): + ''' Default callback for action stanzas -- simple ack + and stop processing. ''' + response = stanza.buildReply('result') + self.connection.send(response) + raise xmpp.NodeProcessed + + ''' Methods that make/send proper pieces of XML. They check if the session + is in appropriate state. ''' + def makeJingle(self, action): + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.jid)) + jingle = stanza.addChild('jingle', attrs= + 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', + 'action': action, + 'initiator': self.initiator, + 'responder': self.responder, + 'sid': self.sid}) + return stanza, jingle + + def appendContent(self, jingle, content, full=True): + ''' Append element to element, + with (full=True) or without (full=False) + children. ''' + c=jingle.addChild('content', attrs={ + 'creator': content['creator'], + 'name': content['name']}) + if 'profile' in content: + c['profile']=content['profile'] + if full: + c.addChild(node=content['description']) + c.addChild(node=content['transport']) + return c + + def appendContents(self, jingle, full=True): + ''' Append all elements to .''' + # TODO: integrate with __appendContent? + # TODO: parameters 'name', 'content'? + for content in self.contents.values(): + self.__appendContent(jingle, content, full=full) + + def __sessionInitiate(self): + assert self.state==JingleStates.ended + + def __sessionAccept(self): + assert self.state==JingleStates.pending + stanza, jingle = self.__jingle('session-accept') + self.__appendContents(jingle, False) + self.connection.send(stanza) + self.state=JingleStates.active + + def __sessionInfo(self, payload=None): + assert self.state!=JingleStates.ended + stanza, jingle = self.__jingle('session-info') + if payload: + jingle.addChild(node=payload) + self.connection.send(stanza) + + def __sessionTerminate(self): + assert self.state!=JingleStates.ended + stanza, jingle = self.__jingle('session-terminate') + self.connection.send(stanza) + + def __contentAdd(self): + assert self.state==JingleStates.active + + def __contentAccept(self): + assert self.state!=JingleStates.ended + + def __contentModify(self): + assert self.state!=JingleStates.ended + + def __contentRemove(self): + assert self.state!=JingleStates.ended + + def __transportInfo(self): + assert self.state!=JingleStates.ended + + '''Callbacks''' + def sessionInitiateCB(self, stanza): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data. ''' + jingle = stanza.getTag('jingle') + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.jid = self.initiator + self.state = JingleStates.pending + self.sid = jingle['sid'] + for element in jingle.iterTags('content'): + content={'creator': 'initiator', + 'name': element['name'] + 'description': element.getTag('description'), + 'transport': element.getTag('transport')} + if element.has_attr('profile'): + content['profile']=element['profile'] + self.contents[('initiator', content['name'])]=content + + def sessionTerminateCB(self, stanza): pass + +class JingleAudioSession(object): + class Codec(object): + ''' This class keeps description of a single codec. ''' + def __init__(self, name, id=None, **params): + ''' Create new codec description. ''' + self.name = name + self.id = id + self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} + for key in ('channels', 'clockrate', 'maxptime', 'ptime'): + if key in params: + self.attrs[key]=params[key] + del params[key] + self.params = params + + def __eq__(a, b): + ''' Compare two codec descriptions. ''' + # TODO: check out what should be tested... + if a.name!=b.name: return False + # ... + return True + + def toXML(self): + return xmpp.Node('payload', + attrs=self.attrs, + payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) + + def __init__(self, con, weinitiate, jid): + JingleSession.__init__(self, con, weinitiate, jid) + if weinitiate: + pass #add voice content + self.callbacks['session-initiate'].prepend( + + self.initiator_codecs=[] + self.responder_codecs=[] + + ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' + def getOurCodecs(self, other=None): + ''' Get a list of codecs we support. Try to get them in the same + order as the codecs of our peer. If other!=None, raise + a NoCommonCodec error if no codecs both sides support (None means + we are initiating the connection and we don't know the other + peer's codecs.) ''' + # for now we "understand" only one codec -- speex with clockrate 16000 + # so we have an easy job to do... (codecs sorted in order of preference) + supported_codecs=[ + Codec('speex', clockrate='16000'), + ] + + other_l = other if other is not None else [] + our_l = supported_codecs[:] + out = [] + ids = range(128) + for codec in other: + if codec in our_l: + out.append(codec) + our_l.remove(codec) + try: ids.remove(codec.id) + except ValueError: pass # when id is not a dynamic one + + if other is not None and len(out)==0: + raise NoCommonCodec + + for codec in our_l: + if not codec.id or codec.id not in ids: + codec.id = ids.pop() + out.append(codec) + + return out + + ''' Methods for making proper pieces of XML. ''' + def __codecsList(self, codecs): + ''' Prepares a description element with codecs given as a parameter. ''' + return xmpp.Node('description', + xmlns=xmpp.NS_JINGLE_AUDIO, + payload=(codec.toXML() for codec in codecs)) + +class JingleICEUDPSession(object): + def __init__(self, con, weinitiate, jid): + pass + +class JingleVoiP(JingleSession): + ''' Jingle VoiP sessions consist of audio content transported + over an ICE UDP protocol. ''' + def __init__(*data): + JingleAudioSession.__init__(*data) + JingleICEUDPSession.__init__(*data) + +class ConnectionJingle(object): + ''' This object depends on that it is a part of Connection class. ''' + def __init__(self): + # dictionary: (jid, sessionid) => JingleSession object + self.__sessions = {} + + # dictionary: (jid, iq stanza id) => JingleSession object, + # one time callbacks + self.__iq_responses = {} + + def addJingle(self, jingle): + ''' Add a jingle session to a jingle stanza dispatcher + jingle - a JingleSession object. + ''' + self.__sessions[(jingle.jid, jingle.sid)]=jingle + + def deleteJingle(self, jingle): + ''' Remove a jingle session from a jingle stanza dispatcher ''' + del self.__session[(jingle.jid, jingle.sid)] + + def _jingleCB(self, con, stanza): + ''' The jingle stanza dispatcher. + Route jingle stanza to proper JingleSession object, + or create one if it is a new session. + TODO: Also check if the stanza isn't an error stanza, if so + route it adequatelly.''' + + # get data + jid = stanza.getFrom() + id = stanza.getID() + + if (jid, id) in self.__iq_responses.keys(): + self.__iq_responses[(jid, id)].stanzaCB(stanza) + del self.__iq_responses[(jid, id)] + raise xmpp.NodeProcessed + + jingle = stanza.getTag('jingle') + sid = jingle.getAttr('sid') + + # do we need to create a new jingle object + if (jid, sid) not in self.__sessions: + # we should check its type here... + newjingle = JingleAudioSession(con=self, weinitiate=False, jid=jid) + self.addJingle(newjingle) + + # we already have such session in dispatcher... + return self.__sessions[(jid, sid)].stanzaCB(stanza) + + def addJingleIqCallback(jid, id, jingle): + self.__iq_responses[(jid, id)]=jingle