# HG changeset patch # User Goffi # Date 1446498161 -3600 # Node ID cbfbe028d099c2ddec9b41aa80c041a5056c38c8 # Parent eb8aae35085b6362180c9b845118598ad1a5f714 plugin XEP-0166, XEP-0234, XEP-0261: - transport and application data are now managed in separate dictionaries - new client.IQ method is used - new buildAction helper method for applications and transports - "role" is now stored in session_data - "senders" is now stored in content_data - plugin XEP-0166: "transport-info" action is now managed - plugin XEP-0166: application namespace and handler are now managed in a namedtuple - plugin XEP-0234: element is added by responder if not already present diff -r eb8aae35085b -r cbfbe028d099 src/plugins/plugin_xep_0166.py --- a/src/plugins/plugin_xep_0166.py Mon Nov 02 22:02:41 2015 +0100 +++ b/src/plugins/plugin_xep_0166.py Mon Nov 02 22:02:41 2015 +0100 @@ -28,7 +28,7 @@ # from twisted.words.xish import domish from twisted.internet import defer # from wokkel import disco, iwokkel, data_form, compat -from wokkel import disco, iwokkel, compat +from wokkel import disco, iwokkel from twisted.words.protocols.jabber import error from twisted.words.protocols.jabber import xmlstream # from sat.core import exceptions @@ -49,8 +49,6 @@ STATE_PENDING = "PENDING" STATE_ACTIVE = "ACTIVE" STATE_ENDED = "ENDED" -INITIATOR = "initiator" -RESPONDER = "responder" CONFIRM_TXT = D_("{entity} want to start a jingle session with you, do you accept ?") PLUGIN_INFO = { @@ -64,10 +62,13 @@ } +ApplicationData = namedtuple('ApplicationData', ('namespace', 'handler')) TransportData = namedtuple('TransportData', ('namespace', 'handler', 'priority')) class XEP_0166(object): + ROLE_INITIATOR = "initiator" + ROLE_RESPONDER = "responder" TRANSPORT_DATAGRAM='UDP' TRANSPORT_STREAMING='TCP' REASON_SUCCESS='success' @@ -77,6 +78,7 @@ A_SESSION_INITIATE = "session-initiate" A_SESSION_ACCEPT = "session-accept" A_SESSION_TERMINATE = "session-terminate" + A_TRANSPORT_INFO = "transport-info" # non standard actions A_PREPARE_INITIATOR = "prepare-initiator" # initiator must prepare tranfer A_PREPARE_RESPONDER = "prepare-responder" # responder must prepare tranfer @@ -111,7 +113,7 @@ ## helpers methods to build stanzas ## def _buildJingleElt(self, client, session, action): - iq_elt = compat.IQ(client.xmlstream, 'set') + iq_elt = client.IQ('set') iq_elt['from'] = client.jid.full() iq_elt['to'] = session['to_jid'].full() jingle_elt = iq_elt.addElement("jingle", NS_JINGLE) @@ -204,7 +206,8 @@ """ if namespace in self._applications: raise exceptions.ConflictError(u"Trying to register already registered namespace {}".format(namespace)) - self._applications[namespace] = handler + self._applications[namespace] = ApplicationData(namespace=namespace, handler=handler) + log.debug(u"new jingle application registered") def registerTransport(self, namespace, transport_type, handler, priority=0): """Register a transport plugin @@ -223,10 +226,38 @@ raise exceptions.ConflictError(u"Trying to register already registered namespace {}".format(namespace)) transport_data = TransportData(namespace=namespace, handler=handler, priority=priority) self._type_transports[transport_type].append(transport_data) - self._type_transports[transport_type].sort(key=lambda transport_data: transport_data.priority) + self._type_transports[transport_type].sort(key=lambda transport_data: transport_data.priority, reverse=True) self._transports[namespace] = transport_data log.debug(u"new jingle transport registered") + def buildAction(self, action, session, content_name, profile=C.PROF_KEY_NONE): + """Build an element according to requested action + + @param action(unicode): a jingle action (see XEP-0166 §7.2), + session-* actions are not managed here + @param session(dict): jingle session data + @param content_name(unicode): name of the content terminated + @param profile: %(doc_profile)s + @return (tuple[domish.Element, domish.Element]): parent element, or element, according to action + """ + client = self.host.getClient(profile) + + # we first build iq, jingle and content element which are the same in every cases + iq_elt, jingle_elt = self._buildJingleElt(client, session, action) + # FIXME: XEP-0260 § 2.3 Ex 5 has an initiator attribute, but it should not according to XEP-0166 §7.1 table 1, must be checked + content_data= session['contents'][content_name] + content_elt = jingle_elt.addElement('content') + content_elt['name'] = content_name + content_elt['creator'] = content_data['creator'] + + if action == XEP_0166.A_TRANSPORT_INFO: + context_elt = transport_elt = content_elt.addElement('transport', content_data['transport'].namespace) + transport_elt['sid'] = content_data['transport_data']['sid'] + else: + raise exceptions.InternalError(u"unmanaged action {}".format(action)) + + return iq_elt, context_elt + @defer.inlineCallbacks def initiate(self, to_jid, contents, profile=C.PROF_KEY_NONE): """Send a session initiation request @@ -239,6 +270,8 @@ - transport_type(unicode): type of transport to use (see XEP-0166 §8) default to TRANSPORT_STREAMING - name(unicode): name of the content + - senders(unicode): One of XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER, both or none + default to BOTH (see XEP-0166 §7.3) - app_args(list): args to pass to the application plugin - app_kwargs(dict): keyword args to pass to the application plugin @param profile: %(doc_profile)s @@ -251,6 +284,7 @@ session = client.jingle_sessions[sid] = {'id': sid, 'state': STATE_PENDING, 'initiator': initiator, + 'role': XEP_0166.ROLE_INITIATOR, 'to_jid': to_jid, 'started': time.time(), 'contents': {} @@ -264,21 +298,24 @@ # we get the application plugin app_ns = content['app_ns'] try: - application_handler = self._applications[app_ns] + application = self._applications[app_ns] except KeyError: raise exceptions.InternalError(u"No application registered for {}".format(app_ns)) # and the transport plugin transport_type = content.get('transport_type', XEP_0166.TRANSPORT_STREAMING) try: - transport_handler = self._type_transports[transport_type][0].handler + transport = self._type_transports[transport_type][0] except IndexError: raise exceptions.InternalError(u"No transport registered for {}".format(transport_type)) # we build the session data - content_data = {'application': application_handler, - 'transport': transport_handler, - 'creator': INITIATOR, + content_data = {'application': application, + 'application_data': {}, + 'transport': transport, + 'transport_data': {}, + 'creator': XEP_0166.ROLE_INITIATOR, + 'senders': content.get('senders', 'both'), } try: content_name = content['name'] @@ -293,16 +330,20 @@ content_elt = jingle_elt.addElement('content') content_elt['creator'] = content_data['creator'] content_elt['name'] = content_name + try: + content_elt['senders'] = content['senders'] + except KeyError: + pass # then the description element app_args = content.get('app_args', []) app_kwargs = content.get('app_kwargs', {}) app_kwargs['profile'] = profile - desc_elt = yield application_handler.jingleSessionInit(session, content_name, *app_args, **app_kwargs) + desc_elt = yield application.handler.jingleSessionInit(session, content_name, *app_args, **app_kwargs) content_elt.addChild(desc_elt) # and the transport one - transport_elt = yield transport_handler.jingleSessionInit(session, content_name, profile) + transport_elt = yield transport.handler.jingleSessionInit(session, content_name, profile) content_elt.addChild(transport_elt) d = iq_elt.send() @@ -325,7 +366,7 @@ ## defaults methods called when plugin doesn't have them ## - def jingleRequestConfirmationDefault(self, session, desc_elt, profile): + def jingleRequestConfirmationDefault(self, action, session, content_name, desc_elt, profile): """This method request confirmation for a jingle session""" log.debug(u"Using generic jingle confirmation method") return xml_tools.deferConfirm(self.host, _(CONFIRM_TXT).format(entity=session['to_jid'].full()), _('Confirm Jingle session'), profile=profile) @@ -373,6 +414,7 @@ session = client.jingle_sessions[sid] = {'id': sid, 'state': STATE_PENDING, 'initiator': to_jid, + 'role': XEP_0166.ROLE_RESPONDER, 'to_jid': to_jid, 'started': time.time(), } @@ -392,20 +434,26 @@ self.onSessionTerminate(client, request, jingle_elt, session) elif action == XEP_0166.A_SESSION_ACCEPT: self.onSessionAccept(client, request, jingle_elt, session) + elif action == XEP_0166.A_TRANSPORT_INFO: + self.onTransportInfo(client, request, jingle_elt, session) else: - raise exceptions.InternalError(u"Unknown action {}".format(session['state'])) + raise exceptions.InternalError(u"Unknown action {}".format(action)) ## Actions callbacks ## - def _parseElements(self, jingle_elt, session, request, client, new=False, creator=INITIATOR): + def _parseElements(self, jingle_elt, session, request, client, new=False, creator=ROLE_INITIATOR, with_application=True, with_transport=True): """Parse contents elements and fill contents_dict accordingly after the parsing, contents_dict will containt handlers, "desc_elt" and "transport_elt" @param jingle_elt(domish.Element): parent element, containing one or more - @param contents_dict(dict): session data for contents, the key is the name of the content + @param session(dict): session data + @param request(domish.Element): the whole request + @param client: %(doc_client)s @param new(bool): if new the content is new and must be created, else the content must exists, and session data will be filled @param creator(unicode): only used if new is True: creating pear (see § 7.3) + @param with_application(bool): if True, raise an error if there is no element else ignore it + @param with_transport(bool): if True, raise an error if there is no element else ignore it @raise exceptions.CancelError: the error is treated the calling method can cancel the treatment (i.e. return) """ contents_dict = session['contents'] @@ -419,7 +467,9 @@ if not name or name in contents_dict: self.sendError('bad-request', session['id'], request, client.profile) raise exceptions.CancelError - content_data = contents_dict[name] = {'creator': creator} + content_data = contents_dict[name] = {'creator': creator, + 'senders': content_elt.attributes.get('senders', 'both'), + } else: # the content must exist, we check it try: @@ -430,57 +480,61 @@ raise exceptions.CancelError # application - desc_elt = content_elt.description - if not desc_elt: - self.sendError('bad-request', session['id'], request, client.profile) - raise exceptions.CancelError - - if new: - # the content is new, we need to check and link the application_handler - app_ns = desc_elt.uri - if not app_ns or app_ns == NS_JINGLE: + if with_application: + desc_elt = content_elt.description + if not desc_elt: self.sendError('bad-request', session['id'], request, client.profile) raise exceptions.CancelError - try: - application_handler = self._applications[app_ns] - except KeyError: - log.warning(u"Unmanaged application namespace [{}]".format(app_ns)) - self.sendError('service-unavailable', session['id'], request, client.profile) - raise exceptions.CancelError + if new: + # the content is new, we need to check and link the application + app_ns = desc_elt.uri + if not app_ns or app_ns == NS_JINGLE: + self.sendError('bad-request', session['id'], request, client.profile) + raise exceptions.CancelError - content_data['application'] = application_handler - else: - # the content exists, we check that we have not a former desc_elt - if 'desc_elt' in content_data: - raise exceptions.InternalError(u"desc_elt should not exist at this point") + try: + application = self._applications[app_ns] + except KeyError: + log.warning(u"Unmanaged application namespace [{}]".format(app_ns)) + self.sendError('service-unavailable', session['id'], request, client.profile) + raise exceptions.CancelError - content_data['desc_elt'] = desc_elt + content_data['application'] = application + content_data['application_data'] = {} + else: + # the content exists, we check that we have not a former desc_elt + if 'desc_elt' in content_data: + raise exceptions.InternalError(u"desc_elt should not exist at this point") + + content_data['desc_elt'] = desc_elt # transport - transport_elt = content_elt.transport - if not transport_elt: - self.sendError('bad-request', session['id'], request, client.profile) - raise exceptions.CancelError - - if new: - # the content is new, we need to check and link the transport_handler - transport_ns = transport_elt.uri - if not app_ns or app_ns == NS_JINGLE: + if with_transport: + transport_elt = content_elt.transport + if not transport_elt: self.sendError('bad-request', session['id'], request, client.profile) raise exceptions.CancelError - try: - transport_handler = self._transports[transport_ns].handler - except KeyError: - raise exceptions.InternalError(u"No transport registered for namespace {}".format(transport_ns)) - content_data['transport'] = transport_handler - else: - # the content exists, we check that we have not a former transport_elt - if 'transport_elt' in content_data: - raise exceptions.InternalError(u"desc_elt should not exist at this point") + if new: + # the content is new, we need to check and link the transport + transport_ns = transport_elt.uri + if not app_ns or app_ns == NS_JINGLE: + self.sendError('bad-request', session['id'], request, client.profile) + raise exceptions.CancelError - content_data['transport_elt'] = transport_elt + try: + transport = self._transports[transport_ns] + except KeyError: + raise exceptions.InternalError(u"No transport registered for namespace {}".format(transport_ns)) + content_data['transport'] = transport + content_data['transport_data'] = {} + else: + # the content exists, we check that we have not a former transport_elt + if 'transport_elt' in content_data: + raise exceptions.InternalError(u"desc_elt should not exist at this point") + + content_data['transport_elt'] = transport_elt def _callPlugins(self, action, session, app_method_name='jingleHandler', transp_method_name='jingleHandler', app_default_cb=None, transp_default_cb=None, delete=True, elements=True, profile=C.PROF_KEY_NONE): """Call application and transport plugin methods for all contents @@ -511,7 +565,7 @@ if method_name is None: continue - handler = content_data[handler_key] + handler = content_data[handler_key].handler try: method = getattr(handler, method_name) except AttributeError: @@ -546,7 +600,7 @@ session['contents'] = contents_dict = {} try: - self._parseElements(jingle_elt, session, request, client, True, INITIATOR) + self._parseElements(jingle_elt, session, request, client, True, XEP_0166.ROLE_INITIATOR) except exceptions.CancelError: return @@ -591,19 +645,19 @@ for content_name, content_data in session['contents'].iteritems(): content_elt = jingle_elt.addElement('content') - content_elt['creator'] = INITIATOR + content_elt['creator'] = XEP_0166.ROLE_INITIATOR content_elt['name'] = content_name - application_handler = content_data['application'] - app_session_accept_cb = application_handler.jingleHandler + application = content_data['application'] + app_session_accept_cb = application.handler.jingleHandler app_d = defer.maybeDeferred(app_session_accept_cb, XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('desc_elt'), client.profile) app_d.addCallback(addElement, content_elt) defers_list.append(app_d) - transport_handler = content_data['transport'] - transport_session_accept_cb = transport_handler.jingleHandler + transport = content_data['transport'] + transport_session_accept_cb = transport.handler.jingleHandler transport_d = defer.maybeDeferred(transport_session_accept_cb, XEP_0166.A_SESSION_INITIATE, session, content_name, content_data.pop('transport_elt'), client.profile) @@ -628,7 +682,7 @@ client.xmlstream.send(xmlstream.toResponse(request, 'result')) def onSessionAccept(self, client, request, jingle_elt, session): - """Method called one sesion is accepted + """Method called once session is accepted This method is only called for initiator @param client: %(doc_client)s @@ -656,6 +710,33 @@ # after negociations we start the transfer negociate_dlist.addCallback(lambda dummy: self._callPlugins(XEP_0166.A_START, session, app_method_name=None, elements=False, profile=client.profile)) + def onTransportInfo(self, client, request, jingle_elt, session): + """Method called when a transport-info action is received from other peer + + The request is parsed, and jingleHandler is called on concerned transport plugin(s) + @param client: %(doc_client)s + @param request(domish.Element): full request + @param jingle_elt(domish.Element): the element + @param session(dict): session data + """ + log.debug(u"Jingle session {} has been accepted".format(session['id'])) + + try: + self._parseElements(jingle_elt, session, request, client, with_application=False) + except exceptions.CancelError: + return + + # The parsing was OK, we send the result + client.xmlstream.send(xmlstream.toResponse(request, 'result')) + + for content_name, content_data in session['contents'].iteritems(): + try: + transport_elt = content_data.pop('transport_elt') + except KeyError: + continue + else: + content_data['transport'].handler.jingleHandler(XEP_0166.A_TRANSPORT_INFO, session, content_name, transport_elt, client.profile) + class XEP_0166_handler(xmlstream.XMPPHandler): implements(iwokkel.IDisco) diff -r eb8aae35085b -r cbfbe028d099 src/plugins/plugin_xep_0234.py --- a/src/plugins/plugin_xep_0234.py Mon Nov 02 22:02:41 2015 +0100 +++ b/src/plugins/plugin_xep_0234.py Mon Nov 02 22:02:41 2015 +0100 @@ -93,17 +93,18 @@ @param profile: %(doc_profile)s return (defer.Deferred): True if transfer is accepted """ - file_data = content_data['file_data'] + application_data = content_data['application_data'] + file_data = application_data['file_data'] d = xml_tools.deferDialog(self.host, _(CONFIRM).format(entity=session['to_jid'].full(), **file_data), _(CONFIRM_TITLE), type_=C.XMLUI_DIALOG_FILE, options={C.XMLUI_DATA_FILETYPE: C.XMLUI_DATA_FILETYPE_DIR}, profile=profile) - d.addCallback(self._gotConfirmation, session, content_data, profile) + d.addCallback(self._gotConfirmation, session, content_data, application_data, profile) return d - def _gotConfirmation(self, data, session, content_data, profile): + def _gotConfirmation(self, data, session, content_data, application_data, profile): """Called when the permission and dest path have been received @param data(dict): xmlui data received from file dialog @@ -113,7 +114,7 @@ """ if data.get('cancelled', False): return False - file_data = content_data['file_data'] + file_data = application_data['file_data'] path = data['path'] file_data['file_path'] = file_path = os.path.join(path, file_data['name']) log.debug(u'destination file path set to {}'.format(file_path)) @@ -144,9 +145,10 @@ def jingleSessionInit(self, session, content_name, filepath, name=None, file_desc=None, profile=C.PROF_KEY_NONE): content_data = session['contents'][content_name] - assert 'file_path' not in content_data - content_data['file_path'] = filepath - file_data = content_data['file_data'] = {} + application_data = content_data['application_data'] + assert 'file_path' not in application_data + application_data['file_path'] = filepath + file_data = application_data['file_data'] = {} file_data['date'] = utils.xmpp_date() file_data['desc'] = file_desc or '' file_data['media-type'] = "application/octet-stream" # TODO @@ -190,7 +192,7 @@ # TODO: parse hash using plugin XEP-0300 - content_data['file_data'] = file_data + content_data['application_data']['file_data'] = file_data # now we actualy request permission to user return self._getDestDir(session, content_data, profile) @@ -198,12 +200,21 @@ def jingleHandler(self, action, session, content_name, desc_elt, profile): content_data = session['contents'][content_name] - if action in (self._j.A_SESSION_INITIATE, self._j.A_ACCEPTED_ACK): + application_data = content_data['application_data'] + if action in (self._j.A_ACCEPTED_ACK,): pass + elif action == self._j.A_SESSION_INITIATE: + file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() + try: + file_elt.elements(NS_JINGLE_FT, 'range').next() + except StopIteration: + # initiator doesn't manage , but we do so we advertise it + log.debug("adding element") + file_elt.addElement('range') elif action == self._j.A_SESSION_ACCEPT: assert not 'file_obj' in content_data - file_path = content_data['file_path'] - size = content_data['file_data']['size'] + file_path = application_data['file_path'] + size = application_data['file_data']['size'] file_obj = content_data['file_obj'] = self._f.File(self.host, file_path, size=size, diff -r eb8aae35085b -r cbfbe028d099 src/plugins/plugin_xep_0261.py --- a/src/plugins/plugin_xep_0261.py Mon Nov 02 22:02:41 2015 +0100 +++ b/src/plugins/plugin_xep_0261.py Mon Nov 02 22:02:41 2015 +0100 @@ -60,24 +60,26 @@ def jingleSessionInit(self, session, content_name, profile): transport_elt = domish.Element((NS_JINGLE_IBB, "transport")) content_data = session['contents'][content_name] - content_data['ibb_block_size'] = self._ibb.BLOCK_SIZE - transport_elt['block-size'] = unicode(content_data['ibb_block_size']) - transport_elt['sid'] = content_data['ibb_sid'] = unicode(uuid.uuid4()) + transport_data = content_data['transport_data'] + transport_data['block_size'] = self._ibb.BLOCK_SIZE + transport_elt['block-size'] = unicode(transport_data['block_size']) + transport_elt['sid'] = transport_data['sid'] = unicode(uuid.uuid4()) return transport_elt def jingleHandler(self, action, session, content_name, transport_elt, profile): content_data = session['contents'][content_name] + transport_data = content_data['transport_data'] if action in (self._j.A_SESSION_ACCEPT, self._j.A_ACCEPTED_ACK): pass elif action == self._j.A_SESSION_INITIATE: - content_data['ibb_sid'] = transport_elt['sid'] + transport_data['sid'] = transport_elt['sid'] elif action in (self._j.A_START, self._j.A_PREPARE_RESPONDER): to_jid = session['to_jid'] - sid = content_data['ibb_sid'] + sid = transport_data['sid'] file_obj = content_data['file_obj'] args = [session, content_name, profile] if action == self._j.A_START: - block_size = content_data['ibb_block_size'] + block_size = transport_data['block_size'] d = self._ibb.startStream(file_obj, to_jid, sid, block_size, profile) d.addErrback(self._streamEb, *args) else: