comparison libervia/backend/plugins/plugin_xep_0353.py @ 4112:bc60875cb3b8

plugin XEP-0166, XEP-0167, XEP-0234, XEP-0353: call events management to prepare for UI: - XEP-0166: add `jingle_preflight` and `jingle_preflight_cancel` methods to prepare a jingle session, principally used by XEP-0353 to create and cancel a session - XEP-0167: preflight methods implementation, workflow split in more methods/signals to handle UI and call events (e.g.: retract or reject a call) - XEP-0234: implementation of preflight methods as they are now mandatory - XEP-0353: handle various events using the new preflight methods rel 423
author Goffi <goffi@goffi.org>
date Wed, 09 Aug 2023 00:07:37 +0200
parents 4b842c1fb686
children 0da563780ffc
comparison
equal deleted inserted replaced
4111:a8ac5e1e5848 4112:bc60875cb3b8
45 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0334"], 45 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0334"],
46 C.PI_MAIN: "XEP_0353", 46 C.PI_MAIN: "XEP_0353",
47 C.PI_HANDLER: "yes", 47 C.PI_HANDLER: "yes",
48 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), 48 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""),
49 } 49 }
50
51
52 class RejectException(exceptions.CancelError):
53
54 def __init__(self, reason: str, text: str|None = None):
55 super().__init__(text)
56 self.reason = reason
57
58
59 class RetractException(exceptions.CancelError):
60 pass
50 61
51 62
52 class XEP_0353: 63 class XEP_0353:
53 def __init__(self, host): 64 def __init__(self, host):
54 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) 65 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME]))
59 host.trigger.add( 70 host.trigger.add(
60 "XEP-0166_initiate_elt_built", 71 "XEP-0166_initiate_elt_built",
61 self._on_initiate_trigger, 72 self._on_initiate_trigger,
62 # this plugin set the resource, we want it to happen first to other trigger 73 # this plugin set the resource, we want it to happen first to other trigger
63 # can get the full peer JID 74 # can get the full peer JID
75 priority=host.trigger.MAX_PRIORITY,
76 )
77 host.trigger.add(
78 "XEP-0166_terminate",
79 self._terminate_trigger,
64 priority=host.trigger.MAX_PRIORITY, 80 priority=host.trigger.MAX_PRIORITY,
65 ) 81 )
66 host.trigger.add("message_received", self._on_message_received) 82 host.trigger.add("message_received", self._on_message_received)
67 83
68 def get_handler(self, client): 84 def get_handler(self, client):
134 response_d = defer.Deferred() 150 response_d = defer.Deferred()
135 # we wait for 2 min before cancelling the session init 151 # we wait for 2 min before cancelling the session init
136 # FIXME: let's application decide timeout? 152 # FIXME: let's application decide timeout?
137 response_d.addTimeout(2 * 60, reactor) 153 response_d.addTimeout(2 * 60, reactor)
138 client._xep_0353_pending_sessions[session["id"]] = response_d 154 client._xep_0353_pending_sessions[session["id"]] = response_d
139 await client.send_message_data(mess_data) 155 try:
140 try: 156 await client.send_message_data(mess_data)
141 accepting_jid = await response_d 157 accepting_jid = await response_d
142 except defer.TimeoutError: 158 except defer.TimeoutError:
143 log.warning( 159 log.warning(
144 _("Message initiation with {peer_jid} timed out").format( 160 _("Message initiation with {peer_jid} timed out").format(
145 peer_jid=peer_jid 161 peer_jid=peer_jid
146 ) 162 )
147 ) 163 )
164 except exceptions.CancelError as e:
165 for content in session["contents"].values():
166 await content["application"].handler.jingle_preflight_cancel(
167 client, session, e
168 )
169
170 self._j.delete_session(client, session["id"])
171 return False
148 else: 172 else:
149 if iq_elt["to"] != accepting_jid.userhost(): 173 if iq_elt["to"] != accepting_jid.userhost():
150 raise exceptions.InternalError( 174 raise exceptions.InternalError(
151 f"<jingle> 'to' attribute ({iq_elt['to']!r}) must not differ " 175 f"<jingle> 'to' attribute ({iq_elt['to']!r}) must not differ "
152 f"from bare JID of the accepting entity ({accepting_jid!r}), this " 176 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!" 177 "may be a sign of an internal bug, a hack attempt, or a MITM attack!"
154 ) 178 )
155 iq_elt["to"] = accepting_jid.full() 179 iq_elt["to"] = accepting_jid.full()
156 session["peer_jid"] = accepting_jid 180 session["peer_jid"] = accepting_jid
157 del client._xep_0353_pending_sessions[session["id"]] 181 finally:
182 del client._xep_0353_pending_sessions[session["id"]]
158 return True 183 return True
184
185 def _terminate_trigger(
186 self,
187 client: SatXMPPEntity,
188 session: dict,
189 reason_elt: domish.Element
190 ) -> bool:
191 session_id = session["id"]
192 try:
193 response_d = client._xep_0353_pending_sessions[session_id]
194 except KeyError:
195 return True
196 # we have a XEP-0353 session, that means that we are retracting a proposed session
197 mess_data = self.build_message_data(
198 client, session["peer_jid"], "retract", session_id
199 )
200 defer.ensureDeferred(client.send_message_data(mess_data))
201 response_d.errback(RetractException())
202
203 return False
159 204
160 async def _on_message_received(self, client, message_elt, post_treat): 205 async def _on_message_received(self, client, message_elt, post_treat):
161 for elt in message_elt.elements(): 206 for elt in message_elt.elements():
162 if elt.uri == NS_JINGLE_MESSAGE: 207 if elt.uri == NS_JINGLE_MESSAGE:
163 if elt.name == "propose": 208 if elt.name == "propose":
167 elif elt.name == "proceed": 212 elif elt.name == "proceed":
168 return self._handle_proceed(client, message_elt, elt) 213 return self._handle_proceed(client, message_elt, elt)
169 elif elt.name == "accept": 214 elif elt.name == "accept":
170 return self._handle_accept(client, message_elt, elt) 215 return self._handle_accept(client, message_elt, elt)
171 elif elt.name == "reject": 216 elif elt.name == "reject":
172 return self._handle_accept(client, message_elt, elt) 217 return self._handle_reject(client, message_elt, elt)
173 else: 218 else:
174 log.warning(f"invalid element: {elt.toXml}") 219 log.warning(f"invalid element: {elt.toXml}")
175 return True 220 return True
176 return True 221 return True
177 222
178 async def _handle_propose(self, client, message_elt, elt): 223 def _get_sid_and_response_d(
179 peer_jid = jid.JID(message_elt["from"]) 224 self,
180 session_id = elt["id"] 225 client: SatXMPPEntity,
181 if peer_jid.userhostJID() not in client.roster: 226 elt: domish.Element
182 app_ns = elt.description.uri 227 ) -> tuple[str, defer.Deferred]:
183 try: 228 """Retrieve session ID and response_d from response element"""
184 application = self._j.get_application(app_ns) 229 try:
185 human_name = getattr(application.handler, "human_name", application.name) 230 session_id = elt["id"]
186 except (exceptions.NotFound, AttributeError): 231 except KeyError as e:
187 if app_ns.startswith("urn:xmpp:jingle:apps:"): 232 assert elt.parent is not None
188 human_name = app_ns[21:].split(":", 1)[0].replace("-", " ").title() 233 log.warning(f"invalid proceed element in message_elt: {elt.parent.toXml()}")
189 else: 234 raise e
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: 235 try:
238 response_d = client._xep_0353_pending_sessions[session_id] 236 response_d = client._xep_0353_pending_sessions[session_id]
239 except KeyError: 237 except KeyError as e:
240 log.warning( 238 log.warning(
241 _( 239 _(
242 "no pending session found with id {session_id}, did it timed out?" 240 "no pending session found with id {session_id}, did it timed out?"
243 ).format(session_id=session_id) 241 ).format(session_id=session_id)
244 ) 242 )
243 raise e
244 return session_id, response_d
245
246 async def _handle_propose(self, client, message_elt, elt):
247 peer_jid = jid.JID(message_elt["from"])
248 local_jid = jid.JID(message_elt["to"])
249 session_id = elt["id"]
250 try:
251 desc_and_apps = [
252 (description_elt, self._j.get_application(description_elt.uri))
253 for description_elt in elt.elements()
254 if description_elt.name == "description"
255 ]
256 if not desc_and_apps:
257 raise AttributeError
258 except AttributeError:
259 log.warning(f"Invalid propose element: {message_elt.toXml()}")
260 return False
261 except exceptions.NotFound:
262 log.warning(
263 f"There is not registered application to handle this "
264 f"proposal: {elt.toXml()}"
265 )
266 return False
267
268 if not desc_and_apps:
269 log.warning("No application specified: {message_elt.toXml()}")
270 return False
271
272 session = self._j.create_session(
273 client, session_id, self._j.ROLE_RESPONDER, peer_jid, local_jid
274 )
275
276 is_in_roster = peer_jid.userhostJID() in client.roster
277 if is_in_roster:
278 # we indicate that device is ringing as explained in
279 # https://xmpp.org/extensions/xep-0353.html#ring , but we only do that if user
280 # is in roster to avoid presence leak of all our devices.
281 mess_data = self.build_message_data(client, peer_jid, "ringing", session_id)
282 await client.send_message_data(mess_data)
283
284 for description_elt, application in desc_and_apps:
285 try:
286 await application.handler.jingle_preflight(
287 client, session, description_elt
288 )
289 except exceptions.CancelError as e:
290 log.info(f"{client.profile} refused the session: {e}")
291
292 if is_in_roster:
293 # peer is in our roster, we send reject to them, ou other devices will
294 # get carbon copies
295 reject_dest_jid = peer_jid
296 else:
297 # peer is not in our roster, we send the "reject" only to our own
298 # devices to make them stop ringing/doing notification, and we don't
299 # send anything to peer to avoid presence leak.
300 reject_dest_jid = client.jid.userhostJID()
301
302 mess_data = self.build_message_data(
303 client, reject_dest_jid, "reject", session_id
304 )
305 await client.send_message_data(mess_data)
306 self._j.delete_session(client, session_id)
307
308 return False
309 except defer.CancelledError:
310 # raised when call is retracted before user can reply
311 self._j.delete_session(client, session_id)
312 return False
313
314 if peer_jid.userhostJID() not in client.roster:
315 await client.presence.available(peer_jid)
316
317 mess_data = self.build_message_data(client, peer_jid, "proceed", session_id)
318 await client.send_message_data(mess_data)
319
320 return False
321
322 def _handle_retract(self, client, message_elt, retract_elt):
323 try:
324 session = self._j.get_session(client, retract_elt["id"])
325 except KeyError:
326 log.warning(f"invalid retract element: {message_elt.toXml()}")
327 return False
328 except exceptions.NotFound:
329 log.warning(f"no session found with ID {retract_elt['id']}")
330 return False
331 log.debug(
332 f"{message_elt['from']} are retracting their proposal {retract_elt['id']}"
333 )
334 try:
335 cancellable_deferred = session["cancellable_deferred"]
336 if not cancellable_deferred:
337 raise KeyError
338 except KeyError:
339 self._j.delete_session(client, session["id"])
340 else:
341 for d in cancellable_deferred:
342 d.cancel()
343 return False
344
345 def _handle_proceed(self, client, message_elt, proceed_elt):
346 try:
347 __, response_d = self._get_sid_and_response_d(client, proceed_elt)
348 except KeyError:
245 return True 349 return True
246 350
247 response_d.callback(jid.JID(message_elt["from"])) 351 response_d.callback(jid.JID(message_elt["from"]))
248 return False 352 return False
249 353
250 def _handle_accept(self, client, message_elt, accept_elt): 354 def _handle_accept(self, client, message_elt, accept_elt):
251 pass 355 pass
252 356
253 def _handle_reject(self, client, message_elt, accept_elt): 357 def _handle_reject(self, client, message_elt, reject_elt):
254 pass 358 try:
359 __, response_d = self._get_sid_and_response_d(client, reject_elt)
360 except KeyError:
361 return True
362 reason_elt = self._j.get_reason_elt(reject_elt)
363 reason, text = self._j.parse_reason_elt(reason_elt)
364 if reason is None:
365 reason = "busy"
366
367 response_d.errback(RejectException(reason, text))
368 return False
255 369
256 370
257 @implementer(iwokkel.IDisco) 371 @implementer(iwokkel.IDisco)
258 class Handler(xmlstream.XMPPHandler): 372 class Handler(xmlstream.XMPPHandler):
259 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): 373 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):