libervia-backend/libervia/backend/plugins/plugin_xep_0353.py

263 lines
10 KiB
Python

#!/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 []