Mercurial > libervia-backend
view sat/plugins/plugin_xep_0353.py @ 4044:3900626bc100
plugin XEP-0166: refactoring, and various improvments:
- add models for transport and applications handlers and linked data
- split models into separate file
- some type hints
- some documentation comments
- add actions to prepare confirmation, useful to do initial parsing of all contents
- application arg/kwargs and some transport data can be initialised during Jingle
`initiate` call, this is notably useful when a call is made with transport data (this is
the call for A/V calls where codecs and ICE candidate can be specified when starting a
call)
- session data can be specified during Jingle `initiate` call
- new `store_in_session` argument in `_parse_elements`, which can be used to avoid
race-condition when a context element (<decription> or <transport>) is being parsed for
an action while an other action happens (like `transport-info`)
- don't sed `sid` in `transport_elt` during a `transport-info` action anymore in
`build_action`: this is specific to Jingle File Transfer and has been moved there
rel 419
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 15 May 2023 16:23:11 +0200 |
parents | 877145b4ba01 |
children | c23cad65ae99 |
line wrap: on
line source
#!/usr/bin/env python3 # SàT plugin for Jingle Message Initiation (XEP-0353) # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # 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 Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from zope.interface import implementer from twisted.internet import defer from twisted.internet import reactor from twisted.words.protocols.jabber import xmlstream, jid, error from twisted.words.xish import domish from wokkel import disco, iwokkel from sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core import exceptions from sat.core.log import getLogger from sat.tools import utils from sat.tools import xml_tools log = getLogger(__name__) NS_JINGLE_MESSAGE = "urn:xmpp:jingle-message:0" PLUGIN_INFO = { C.PI_NAME: "Jingle Message Initiation", C.PI_IMPORT_NAME: "XEP-0353", C.PI_TYPE: "XEP", C.PI_MODES: [C.PLUG_MODE_CLIENT], C.PI_PROTOCOLS: ["XEP-0353"], C.PI_DEPENDENCIES: ["XEP-0166"], C.PI_MAIN: "XEP_0353", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), } class XEP_0353: def __init__(self, host): log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) self.host = host host.register_namespace("jingle-message", NS_JINGLE_MESSAGE) self._j = host.plugins["XEP-0166"] host.trigger.add("XEP-0166_initiate", self._on_initiate_trigger) host.trigger.add("messageReceived", self._on_message_received) def get_handler(self, client): return Handler() def profile_connecting(self, client): # mapping from session id to deferred used to wait for destinee answer client._xep_0353_pending_sessions = {} def build_message_data(self, client, peer_jid, verb, session_id): mess_data = { 'from': client.jid, 'to': peer_jid, 'uid': '', 'message': {}, 'type': C.MESS_TYPE_CHAT, 'subject': {}, 'extra': {} } client.generate_message_xml(mess_data) verb_elt = mess_data["xml"].addElement((NS_JINGLE_MESSAGE, verb)) verb_elt["id"] = session_id return mess_data async def _on_initiate_trigger(self, client, session, contents): # FIXME: check that at least one resource of the peer_jid can handle the feature peer_jid = session['peer_jid'] if peer_jid.resource: return True try: infos = await self.host.memory.disco.get_infos(client, peer_jid) except error.StanzaError as e: if e.condition == "service-unavailable": categories = {} else: raise e else: categories = {c for c, __ in infos.identities} if "component" in categories: # we don't use message initiation with components return True if peer_jid.userhostJID() not in client.roster: # if the contact is not in our roster, we need to send a directed presence # according to XEP-0353 §3.1 await client.presence.available(peer_jid) mess_data = self.build_message_data(client, peer_jid, "propose", session['id']) for content in contents: content_data = self._j.get_content_data( content) try: jingle_description_elt = ( content_data.application.handler.jingle_description_elt ) except AttributeError: log.debug( "no jingle_description_elt set for " f"{content_data.application.handler}" ) description_elt = domish.Element((content["app_ns"], "description")) else: description_elt = await utils.as_deferred( jingle_description_elt, client, session, content_data.content_name, *content_data.app_args, **content_data.app_kwargs ) mess_data["xml"].propose.addChild(description_elt) response_d = defer.Deferred() # we wait for 2 min before cancelling the session init # response_d.addTimeout(2*60, reactor) # FIXME: let's application decide timeout? response_d.addTimeout(2, reactor) client._xep_0353_pending_sessions[session['id']] = response_d await client.send_message_data(mess_data) try: accepting_jid = await response_d except defer.TimeoutError: log.warning(_( "Message initiation with {peer_jid} timed out" ).format(peer_jid=peer_jid)) else: session["peer_jid"] = accepting_jid del client._xep_0353_pending_sessions[session['id']] return True async def _on_message_received(self, client, message_elt, post_treat): for elt in message_elt.elements(): if elt.uri == NS_JINGLE_MESSAGE: if elt.name == "propose": return await self._handle_propose(client, message_elt, elt) elif elt.name == "retract": return self._handle_retract(client, message_elt, elt) elif elt.name == "proceed": return self._handle_proceed(client, message_elt, elt) elif elt.name == "accept": return self._handle_accept(client, message_elt, elt) elif elt.name == "reject": return self._handle_accept(client, message_elt, elt) else: log.warning(f"invalid element: {elt.toXml}") return True return True async def _handle_propose(self, client, message_elt, elt): peer_jid = jid.JID(message_elt["from"]) session_id = elt["id"] if peer_jid.userhostJID() not in client.roster: app_ns = elt.description.uri try: application = self._j.get_application(app_ns) human_name = getattr(application.handler, "human_name", application.name) except (exceptions.NotFound, AttributeError): if app_ns.startswith("urn:xmpp:jingle:apps:"): human_name = app_ns[21:].split(":", 1)[0].replace('-', ' ').title() else: splitted_ns = app_ns.split(':') if len(splitted_ns) > 1: human_name = splitted_ns[-2].replace('- ', ' ').title() else: human_name = app_ns confirm_msg = D_( "Somebody not in your contact list ({peer_jid}) wants to do a " '"{human_name}" session with you, this would leak your presence and ' "possibly you IP (internet localisation), do you accept?" ).format(peer_jid=peer_jid, human_name=human_name) confirm_title = D_("Invitation from an unknown contact") accept = await xml_tools.defer_confirm( self.host, confirm_msg, confirm_title, profile=client.profile, action_extra={ "type": C.META_TYPE_NOT_IN_ROSTER_LEAK, "session_id": session_id, "from_jid": peer_jid.full(), } ) if not accept: mess_data = self.build_message_data( client, client.jid.userhostJID(), "reject", session_id) await client.send_message_data(mess_data) # we don't sent anything to sender, to avoid leaking presence return False else: await client.presence.available(peer_jid) session_id = elt["id"] # FIXME: accept is not used anymore in new specification, check it and remove it mess_data = self.build_message_data( client, client.jid.userhostJID(), "accept", session_id) await client.send_message_data(mess_data) mess_data = self.build_message_data( client, peer_jid, "proceed", session_id) await client.send_message_data(mess_data) return False def _handle_retract(self, client, message_elt, proceed_elt): log.warning("retract is not implemented yet") return False def _handle_proceed(self, client, message_elt, proceed_elt): try: session_id = proceed_elt["id"] except KeyError: log.warning(f"invalid proceed element in message_elt: {message_elt}") return True try: response_d = client._xep_0353_pending_sessions[session_id] except KeyError: log.warning( _("no pending session found with id {session_id}, did it timed out?") .format(session_id=session_id) ) return True response_d.callback(jid.JID(message_elt["from"])) return False def _handle_accept(self, client, message_elt, accept_elt): pass def _handle_reject(self, client, message_elt, accept_elt): pass @implementer(iwokkel.IDisco) class Handler(xmlstream.XMPPHandler): def getDiscoInfo(self, requestor, target, nodeIdentifier=""): return [disco.DiscoFeature(NS_JINGLE_MESSAGE)] def getDiscoItems(self, requestor, target, nodeIdentifier=""): return []