Mercurial > libervia-backend
diff libervia/backend/plugins/plugin_xep_0353.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0353.py@38819c69aa39 |
children | bc60875cb3b8 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/plugins/plugin_xep_0353.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 + +# Libervia 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 twisted.internet import defer +from twisted.internet import reactor +from twisted.words.protocols.jabber import error, jid +from twisted.words.protocols.jabber import xmlstream +from twisted.words.xish import domish +from wokkel import disco, iwokkel +from zope.interface import implementer + +from libervia.backend.core import exceptions +from libervia.backend.core.constants import Const as C +from libervia.backend.core.core_types import SatXMPPEntity +from libervia.backend.core.i18n import D_, _ +from libervia.backend.core.log import getLogger +from libervia.backend.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", "XEP-0334"], + 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"] + self._h = host.plugins["XEP-0334"] + host.trigger.add( + "XEP-0166_initiate_elt_built", + self._on_initiate_trigger, + # this plugin set the resource, we want it to happen first to other trigger + # can get the full peer JID + priority=host.trigger.MAX_PRIORITY, + ) + host.trigger.add("message_received", 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) + message_elt = mess_data["xml"] + verb_elt = message_elt.addElement((NS_JINGLE_MESSAGE, verb)) + verb_elt["id"] = session_id + self._h.add_hint_elements(message_elt, [self._h.HINT_STORE]) + return mess_data + + async def _on_initiate_trigger( + self, + client: SatXMPPEntity, + session: dict, + iq_elt: domish.Element, + jingle_elt: domish.Element, + ) -> bool: + 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"]) + message_elt = mess_data["xml"] + for content_data in session["contents"].values(): + # we get the full element build by the application plugin + jingle_description_elt = content_data["application_data"]["desc_elt"] + # and copy it to only keep the root <description> element, no children + description_elt = domish.Element( + (jingle_description_elt.uri, jingle_description_elt.name), + defaultUri=jingle_description_elt.defaultUri, + attribs=jingle_description_elt.attributes, + localPrefixes=jingle_description_elt.localPrefixes, + ) + message_elt.propose.addChild(description_elt) + response_d = defer.Deferred() + # we wait for 2 min before cancelling the session init + # FIXME: let's application decide timeout? + response_d.addTimeout(2 * 60, 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: + if iq_elt["to"] != accepting_jid.userhost(): + raise exceptions.InternalError( + f"<jingle> 'to' attribute ({iq_elt['to']!r}) must not differ " + f"from bare JID of the accepting entity ({accepting_jid!r}), this " + "may be a sign of an internal bug, a hack attempt, or a MITM attack!" + ) + iq_elt["to"] = accepting_jid.full() + 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"] + 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 []