comparison 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
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3 # Libervia plugin for Jingle Message Initiation (XEP-0353)
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from twisted.internet import defer
20 from twisted.internet import reactor
21 from twisted.words.protocols.jabber import error, jid
22 from twisted.words.protocols.jabber import xmlstream
23 from twisted.words.xish import domish
24 from wokkel import disco, iwokkel
25 from zope.interface import implementer
26
27 from libervia.backend.core import exceptions
28 from libervia.backend.core.constants import Const as C
29 from libervia.backend.core.core_types import SatXMPPEntity
30 from libervia.backend.core.i18n import D_, _
31 from libervia.backend.core.log import getLogger
32 from libervia.backend.tools import xml_tools
33
34 log = getLogger(__name__)
35
36
37 NS_JINGLE_MESSAGE = "urn:xmpp:jingle-message:0"
38
39 PLUGIN_INFO = {
40 C.PI_NAME: "Jingle Message Initiation",
41 C.PI_IMPORT_NAME: "XEP-0353",
42 C.PI_TYPE: "XEP",
43 C.PI_MODES: [C.PLUG_MODE_CLIENT],
44 C.PI_PROTOCOLS: ["XEP-0353"],
45 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0334"],
46 C.PI_MAIN: "XEP_0353",
47 C.PI_HANDLER: "yes",
48 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""),
49 }
50
51
52 class XEP_0353:
53 def __init__(self, host):
54 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME]))
55 self.host = host
56 host.register_namespace("jingle-message", NS_JINGLE_MESSAGE)
57 self._j = host.plugins["XEP-0166"]
58 self._h = host.plugins["XEP-0334"]
59 host.trigger.add(
60 "XEP-0166_initiate_elt_built",
61 self._on_initiate_trigger,
62 # this plugin set the resource, we want it to happen first to other trigger
63 # can get the full peer JID
64 priority=host.trigger.MAX_PRIORITY,
65 )
66 host.trigger.add("message_received", self._on_message_received)
67
68 def get_handler(self, client):
69 return Handler()
70
71 def profile_connecting(self, client):
72 # mapping from session id to deferred used to wait for destinee answer
73 client._xep_0353_pending_sessions = {}
74
75 def build_message_data(self, client, peer_jid, verb, session_id):
76 mess_data = {
77 "from": client.jid,
78 "to": peer_jid,
79 "uid": "",
80 "message": {},
81 "type": C.MESS_TYPE_CHAT,
82 "subject": {},
83 "extra": {},
84 }
85 client.generate_message_xml(mess_data)
86 message_elt = mess_data["xml"]
87 verb_elt = message_elt.addElement((NS_JINGLE_MESSAGE, verb))
88 verb_elt["id"] = session_id
89 self._h.add_hint_elements(message_elt, [self._h.HINT_STORE])
90 return mess_data
91
92 async def _on_initiate_trigger(
93 self,
94 client: SatXMPPEntity,
95 session: dict,
96 iq_elt: domish.Element,
97 jingle_elt: domish.Element,
98 ) -> bool:
99 peer_jid = session["peer_jid"]
100 if peer_jid.resource:
101 return True
102
103 try:
104 infos = await self.host.memory.disco.get_infos(client, peer_jid)
105 except error.StanzaError as e:
106 if e.condition == "service-unavailable":
107 categories = {}
108 else:
109 raise e
110 else:
111 categories = {c for c, __ in infos.identities}
112 if "component" in categories:
113 # we don't use message initiation with components
114 return True
115
116 if peer_jid.userhostJID() not in client.roster:
117 # if the contact is not in our roster, we need to send a directed presence
118 # according to XEP-0353 §3.1
119 await client.presence.available(peer_jid)
120
121 mess_data = self.build_message_data(client, peer_jid, "propose", session["id"])
122 message_elt = mess_data["xml"]
123 for content_data in session["contents"].values():
124 # we get the full element build by the application plugin
125 jingle_description_elt = content_data["application_data"]["desc_elt"]
126 # and copy it to only keep the root <description> element, no children
127 description_elt = domish.Element(
128 (jingle_description_elt.uri, jingle_description_elt.name),
129 defaultUri=jingle_description_elt.defaultUri,
130 attribs=jingle_description_elt.attributes,
131 localPrefixes=jingle_description_elt.localPrefixes,
132 )
133 message_elt.propose.addChild(description_elt)
134 response_d = defer.Deferred()
135 # we wait for 2 min before cancelling the session init
136 # FIXME: let's application decide timeout?
137 response_d.addTimeout(2 * 60, reactor)
138 client._xep_0353_pending_sessions[session["id"]] = response_d
139 await client.send_message_data(mess_data)
140 try:
141 accepting_jid = await response_d
142 except defer.TimeoutError:
143 log.warning(
144 _("Message initiation with {peer_jid} timed out").format(
145 peer_jid=peer_jid
146 )
147 )
148 else:
149 if iq_elt["to"] != accepting_jid.userhost():
150 raise exceptions.InternalError(
151 f"<jingle> 'to' attribute ({iq_elt['to']!r}) must not differ "
152 f"from bare JID of the accepting entity ({accepting_jid!r}), this "
153 "may be a sign of an internal bug, a hack attempt, or a MITM attack!"
154 )
155 iq_elt["to"] = accepting_jid.full()
156 session["peer_jid"] = accepting_jid
157 del client._xep_0353_pending_sessions[session["id"]]
158 return True
159
160 async def _on_message_received(self, client, message_elt, post_treat):
161 for elt in message_elt.elements():
162 if elt.uri == NS_JINGLE_MESSAGE:
163 if elt.name == "propose":
164 return await self._handle_propose(client, message_elt, elt)
165 elif elt.name == "retract":
166 return self._handle_retract(client, message_elt, elt)
167 elif elt.name == "proceed":
168 return self._handle_proceed(client, message_elt, elt)
169 elif elt.name == "accept":
170 return self._handle_accept(client, message_elt, elt)
171 elif elt.name == "reject":
172 return self._handle_accept(client, message_elt, elt)
173 else:
174 log.warning(f"invalid element: {elt.toXml}")
175 return True
176 return True
177
178 async def _handle_propose(self, client, message_elt, elt):
179 peer_jid = jid.JID(message_elt["from"])
180 session_id = elt["id"]
181 if peer_jid.userhostJID() not in client.roster:
182 app_ns = elt.description.uri
183 try:
184 application = self._j.get_application(app_ns)
185 human_name = getattr(application.handler, "human_name", application.name)
186 except (exceptions.NotFound, AttributeError):
187 if app_ns.startswith("urn:xmpp:jingle:apps:"):
188 human_name = app_ns[21:].split(":", 1)[0].replace("-", " ").title()
189 else:
190 splitted_ns = app_ns.split(":")
191 if len(splitted_ns) > 1:
192 human_name = splitted_ns[-2].replace("- ", " ").title()
193 else:
194 human_name = app_ns
195
196 confirm_msg = D_(
197 "Somebody not in your contact list ({peer_jid}) wants to do a "
198 '"{human_name}" session with you, this would leak your presence and '
199 "possibly you IP (internet localisation), do you accept?"
200 ).format(peer_jid=peer_jid, human_name=human_name)
201 confirm_title = D_("Invitation from an unknown contact")
202 accept = await xml_tools.defer_confirm(
203 self.host,
204 confirm_msg,
205 confirm_title,
206 profile=client.profile,
207 action_extra={
208 "type": C.META_TYPE_NOT_IN_ROSTER_LEAK,
209 "session_id": session_id,
210 "from_jid": peer_jid.full(),
211 },
212 )
213 if not accept:
214 mess_data = self.build_message_data(
215 client, client.jid.userhostJID(), "reject", session_id
216 )
217 await client.send_message_data(mess_data)
218 # we don't sent anything to sender, to avoid leaking presence
219 return False
220 else:
221 await client.presence.available(peer_jid)
222 session_id = elt["id"]
223 mess_data = self.build_message_data(client, peer_jid, "proceed", session_id)
224 await client.send_message_data(mess_data)
225 return False
226
227 def _handle_retract(self, client, message_elt, proceed_elt):
228 log.warning("retract is not implemented yet")
229 return False
230
231 def _handle_proceed(self, client, message_elt, proceed_elt):
232 try:
233 session_id = proceed_elt["id"]
234 except KeyError:
235 log.warning(f"invalid proceed element in message_elt: {message_elt}")
236 return True
237 try:
238 response_d = client._xep_0353_pending_sessions[session_id]
239 except KeyError:
240 log.warning(
241 _(
242 "no pending session found with id {session_id}, did it timed out?"
243 ).format(session_id=session_id)
244 )
245 return True
246
247 response_d.callback(jid.JID(message_elt["from"]))
248 return False
249
250 def _handle_accept(self, client, message_elt, accept_elt):
251 pass
252
253 def _handle_reject(self, client, message_elt, accept_elt):
254 pass
255
256
257 @implementer(iwokkel.IDisco)
258 class Handler(xmlstream.XMPPHandler):
259 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
260 return [disco.DiscoFeature(NS_JINGLE_MESSAGE)]
261
262 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
263 return []