comparison libervia/backend/plugins/plugin_xep_0166/__init__.py @ 4111:a8ac5e1e5848

plugin XEP-0166: `jingle_terminate`, session handling and reason parsing: - a new `jingle_terminate` bridge method let frontend terminate a session - methods to manage a session from another plugin - methods to parse `<reason>` element from another plugin - allow session to be created before a first jingle request is received, to prepare for `preflight` methods rel 423
author Goffi <goffi@goffi.org>
date Tue, 08 Aug 2023 23:59:24 +0200
parents 4b842c1fb686
children bc60875cb3b8
comparison
equal deleted inserted replaced
4110:b274f0d5c138 4111:a8ac5e1e5848
128 # we also keep transports by type, they are then sorted by priority 128 # we also keep transports by type, they are then sorted by priority
129 self._type_transports = { 129 self._type_transports = {
130 XEP_0166.TRANSPORT_DATAGRAM: [], 130 XEP_0166.TRANSPORT_DATAGRAM: [],
131 XEP_0166.TRANSPORT_STREAMING: [], 131 XEP_0166.TRANSPORT_STREAMING: [],
132 } 132 }
133 host.bridge.add_method(
134 "jingle_terminate",
135 ".plugin",
136 in_sign="ssss",
137 out_sign="",
138 method=self._terminate,
139 async_=True,
140 )
133 141
134 def profile_connected(self, client): 142 def profile_connected(self, client):
135 client.jingle_sessions = {} # key = sid, value = session_data 143 client.jingle_sessions = {} # key = sid, value = session_data
136 144
137 def get_handler(self, client): 145 def get_handler(self, client):
150 except KeyError: 158 except KeyError:
151 raise exceptions.NotFound( 159 raise exceptions.NotFound(
152 f"No session with SID {session_id} found" 160 f"No session with SID {session_id} found"
153 ) 161 )
154 162
155 163 def create_session(
156 def _del_session(self, client, sid): 164 self,
165 client: SatXMPPEntity,
166 sid: str,
167 role: str,
168 peer_jid: jid.JID,
169 local_jid: jid.JID|None = None,
170 **kwargs
171 ) -> dict:
172 """Create a new jingle session.
173
174 @param client: The client entity.
175 @param sid: Session ID.
176 @param role: Session role (initiator or responder).
177 @param peer_jid: JID of the peer.
178 @param local_jid: JID of the local entity.
179 If None, defaults to client.jid.
180 @param extra_data: Additional data to be added to the session. Defaults to None.
181
182 @return: The created session.
183
184 @raise ValueError: If the provided role is neither initiator nor responder.
185 """
186 # TODO: session cleaning after timeout ?
187 if role not in [XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER]:
188 raise ValueError(f"Invalid role {role}. Expected initiator or responder.")
189
190
191 session_data = {
192 "id": sid,
193 "state": STATE_PENDING,
194 "initiator": client.jid if role == XEP_0166.ROLE_INITIATOR else peer_jid,
195 "role": role,
196 "local_jid": local_jid or client.jid,
197 "peer_jid": peer_jid,
198 "started": time.time(),
199 "contents": {}
200 }
201
202 # If extra kw args are provided, merge them into the session_data
203 if kwargs:
204 session_data.update(kwargs)
205
206 # Add the session to the client's jingle sessions
207 client.jingle_sessions[sid] = session_data
208
209 return session_data
210
211
212 def delete_session(self, client, sid):
157 try: 213 try:
158 del client.jingle_sessions[sid] 214 del client.jingle_sessions[sid]
159 except KeyError: 215 except KeyError:
160 log.debug( 216 log.debug(
161 f"Jingle session id {sid!r} is unknown, nothing to delete " 217 f"Jingle session id {sid!r} is unknown, nothing to delete "
189 """ 245 """
190 iq_elt = error.StanzaError(error_condition).toResponse(request) 246 iq_elt = error.StanzaError(error_condition).toResponse(request)
191 if jingle_condition is not None: 247 if jingle_condition is not None:
192 iq_elt.error.addElement((NS_JINGLE_ERROR, jingle_condition)) 248 iq_elt.error.addElement((NS_JINGLE_ERROR, jingle_condition))
193 if error.STANZA_CONDITIONS[error_condition]["type"] == "cancel" and sid: 249 if error.STANZA_CONDITIONS[error_condition]["type"] == "cancel" and sid:
194 self._del_session(client, sid) 250 self.delete_session(client, sid)
195 log.warning( 251 log.warning(
196 "Error while managing jingle session, cancelling: {condition}".format( 252 "Error while managing jingle session, cancelling: {condition}".format(
197 condition=error_condition 253 condition=error_condition
198 ) 254 )
199 ) 255 )
200 return client.send(iq_elt) 256 return client.send(iq_elt)
201 257
202 def _terminate_eb(self, failure_): 258 def _terminate_eb(self, failure_):
203 log.warning(_("Error while terminating session: {msg}").format(msg=failure_)) 259 log.warning(_("Error while terminating session: {msg}").format(msg=failure_))
204 260
205 def terminate(self, client, reason, session, text=None): 261 def _terminate(
262 self,
263 session_id: str,
264 reason: str,
265 reason_txt: str,
266 profile: str
267 ) -> defer.Deferred:
268 client = self.host.get_client(profile)
269 session = self.get_session(client, session_id)
270 if reason not in ("", "cancel", "decline", "busy"):
271 raise ValueError(
272 'only "cancel", "decline" and "busy" and empty value are allowed'
273 )
274 return self.terminate(
275 client,
276 reason or None,
277 session,
278 text=reason_txt or None
279 )
280
281 def terminate(
282 self,
283 client: SatXMPPEntity,
284 reason: str|list[domish.Element]|None,
285 session: dict,
286 text: str|None = None
287 ) -> defer.Deferred:
206 """Terminate the session 288 """Terminate the session
207 289
208 send the session-terminate action, and delete the session data 290 send the session-terminate action, and delete the session data
209 @param reason(unicode, list[domish.Element]): if unicode, will be transformed to an element 291 @param reason: if unicode, will be transformed to an element
210 if a list of element, add them as children of the <reason/> element 292 if a list of element, add them as children of the <reason/> element
211 @param session(dict): data of the session 293 @param session: data of the session
212 """ 294 """
213 iq_elt, jingle_elt = self._build_jingle_elt( 295 iq_elt, jingle_elt = self._build_jingle_elt(
214 client, session, XEP_0166.A_SESSION_TERMINATE 296 client, session, XEP_0166.A_SESSION_TERMINATE
215 ) 297 )
216 reason_elt = jingle_elt.addElement("reason") 298 if reason is not None:
217 if isinstance(reason, str): 299 reason_elt = jingle_elt.addElement("reason")
218 reason_elt.addElement(reason) 300 if isinstance(reason, str):
301 reason_elt.addElement(reason)
302 else:
303 for elt in reason:
304 reason_elt.addChild(elt)
219 else: 305 else:
220 for elt in reason: 306 reason_elt = None
221 reason_elt.addChild(elt)
222 if text is not None: 307 if text is not None:
308 if reason_elt is None:
309 raise ValueError(
310 "You have to specify a reason if text is specified"
311 )
223 reason_elt.addElement("text", content=text) 312 reason_elt.addElement("text", content=text)
224 self._del_session(client, session["id"]) 313 if not self.host.trigger.point(
314 "XEP-0166_terminate",
315 client, session, reason_elt
316 ):
317 return defer.succeed(None)
318 self.delete_session(client, session["id"])
225 d = iq_elt.send() 319 d = iq_elt.send()
226 d.addErrback(self._terminate_eb) 320 d.addErrback(self._terminate_eb)
227 return d 321 return d
228 322
229 ## errors which doesn't imply a stanza sending ## 323 ## errors which doesn't imply a stanza sending ##
237 log.warning( 331 log.warning(
238 "Error while sending jingle <iq/> stanza: {failure_}".format( 332 "Error while sending jingle <iq/> stanza: {failure_}".format(
239 failure_=failure_.value 333 failure_=failure_.value
240 ) 334 )
241 ) 335 )
242 self._del_session(client, sid) 336 self.delete_session(client, sid)
243 337
244 def _jingle_error_cb(self, failure_, session, request, client): 338 def _jingle_error_cb(self, failure_, session, request, client):
245 """Called when something is going wrong while parsing jingle request 339 """Called when something is going wrong while parsing jingle request
246 340
247 The error condition depend of the exceptions raised: 341 The error condition depend of the exceptions raised:
460 self, 554 self,
461 client: SatXMPPEntity, 555 client: SatXMPPEntity,
462 peer_jid: jid.JID, 556 peer_jid: jid.JID,
463 contents: List[dict], 557 contents: List[dict],
464 encrypted: bool = False, 558 encrypted: bool = False,
559 sid: str|None = None,
465 **extra_data: Any 560 **extra_data: Any
466 ) -> str: 561 ) -> str:
467 """Send a session initiation request 562 """Send a session initiation request
468 563
469 @param peer_jid: jid to establith session with 564 @param peer_jid: jid to establith session with
472 - app_ns(str): namespace of the application 567 - app_ns(str): namespace of the application
473 the following keys are optional: 568 the following keys are optional:
474 - transport_type(str): type of transport to use (see XEP-0166 §8) 569 - transport_type(str): type of transport to use (see XEP-0166 §8)
475 default to TRANSPORT_STREAMING 570 default to TRANSPORT_STREAMING
476 - name(str): name of the content 571 - name(str): name of the content
477 - senders(str): One of XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER, both or none 572 - senders(str): One of XEP_0166.ROLE_INITIATOR, XEP_0166.ROLE_RESPONDER,
478 default to BOTH (see XEP-0166 §7.3) 573 both or none
574 Defaults to BOTH (see XEP-0166 §7.3)
479 - app_args(list): args to pass to the application plugin 575 - app_args(list): args to pass to the application plugin
480 - app_kwargs(dict): keyword args to pass to the application plugin 576 - app_kwargs(dict): keyword args to pass to the application plugin
481 @param encrypted: if True, session must be encrypted and "encryption" must be set 577 @param encrypted: if True, session must be encrypted and "encryption" must be set
482 to all content data of session 578 to all content data of session
483 @return: jingle session id 579 @param sid: Session ID.
580 If None, one will be generated (and used as return value)
581 @return: Sesson ID
484 """ 582 """
485 assert contents # there must be at least one content 583 assert contents # there must be at least one content
486 if (peer_jid == client.jid 584 if (peer_jid == client.jid
487 or client.is_component and peer_jid.host == client.jid.host): 585 or client.is_component and peer_jid.host == client.jid.host):
488 raise ValueError(_("You can't do a jingle session with yourself")) 586 raise ValueError(_("You can't do a jingle session with yourself"))
489 initiator = client.jid 587 if sid is None:
490 sid = str(uuid.uuid4()) 588 sid = str(uuid.uuid4())
491 # TODO: session cleaning after timeout ? 589 session = self.create_session(
492 session = client.jingle_sessions[sid] = { 590 client, sid, XEP_0166.ROLE_INITIATOR, peer_jid, **extra_data
493 "id": sid, 591 )
494 "state": STATE_PENDING, 592 initiator = session["initiator"]
495 "initiator": initiator,
496 "role": XEP_0166.ROLE_INITIATOR,
497 "local_jid": client.jid,
498 "peer_jid": peer_jid,
499 "started": time.time(),
500 "contents": {},
501 **extra_data,
502 }
503 593
504 if not await self.host.trigger.async_point( 594 if not await self.host.trigger.async_point(
505 "XEP-0166_initiate", 595 "XEP-0166_initiate",
506 client, session, contents 596 client, session, contents
507 ): 597 ):
677 except KeyError: 767 except KeyError:
678 if action == XEP_0166.A_SESSION_INITIATE: 768 if action == XEP_0166.A_SESSION_INITIATE:
679 pass 769 pass
680 elif action == XEP_0166.A_SESSION_TERMINATE: 770 elif action == XEP_0166.A_SESSION_TERMINATE:
681 log.debug( 771 log.debug(
682 "ignoring session terminate action (inexisting session id): {request_id} [{profile}]".format( 772 "ignoring session terminate action (inexisting session id): "
773 "{request_id} [{profile}]".format(
683 request_id=sid, profile=client.profile 774 request_id=sid, profile=client.profile
684 ) 775 )
685 ) 776 )
686 return 777 return
687 else: 778 else:
691 ) 782 )
692 ) 783 )
693 self.sendError(client, "item-not-found", None, request, "unknown-session") 784 self.sendError(client, "item-not-found", None, request, "unknown-session")
694 return 785 return
695 786
696 session = client.jingle_sessions[sid] = { 787 try:
697 "id": sid, 788 session = self.get_session(client, sid)
698 "state": STATE_PENDING, 789 except exceptions.NotFound:
699 "initiator": peer_jid, 790 # XXX: we store local_jid using request['to'] because for a component the
700 "role": XEP_0166.ROLE_RESPONDER, 791 # jid used may not be client.jid (if a local part is used).
701 # we store local_jid using request['to'] because for a component the jid 792 session = self.create_session(
702 # used may not be client.jid (if a local part is used). 793 client, sid, XEP_0166.ROLE_RESPONDER, peer_jid, jid.JID(request['to'])
703 "local_jid": jid.JID(request['to']), 794 )
704 "peer_jid": peer_jid,
705 "started": time.time(),
706 }
707 else: 795 else:
708 if session["peer_jid"] != peer_jid: 796 if session["peer_jid"] != peer_jid:
709 log.warning( 797 log.warning(
710 "sid conflict ({}), the jid doesn't match. Can be a collision, a hack attempt, or a bad sid generation".format( 798 "sid conflict ({}), the jid doesn't match. Can be a collision, a "
799 "hack attempt, or a bad sid generation".format(
711 sid 800 sid
712 ) 801 )
713 ) 802 )
714 self.sendError(client, "service-unavailable", sid, request) 803 self.sendError(client, "service-unavailable", sid, request)
715 return 804 return
719 raise exceptions.InternalError 808 raise exceptions.InternalError
720 809
721 if action == XEP_0166.A_SESSION_INITIATE: 810 if action == XEP_0166.A_SESSION_INITIATE:
722 await self.on_session_initiate(client, request, jingle_elt, session) 811 await self.on_session_initiate(client, request, jingle_elt, session)
723 elif action == XEP_0166.A_SESSION_TERMINATE: 812 elif action == XEP_0166.A_SESSION_TERMINATE:
724 self.on_session_terminate(client, request, jingle_elt, session) 813 await self.on_session_terminate(client, request, jingle_elt, session)
725 elif action == XEP_0166.A_SESSION_ACCEPT: 814 elif action == XEP_0166.A_SESSION_ACCEPT:
726 await self.on_session_accept(client, request, jingle_elt, session) 815 await self.on_session_accept(client, request, jingle_elt, session)
727 elif action == XEP_0166.A_SESSION_INFO: 816 elif action == XEP_0166.A_SESSION_INFO:
728 self.on_session_info(client, request, jingle_elt, session) 817 self.on_session_info(client, request, jingle_elt, session)
729 elif action == XEP_0166.A_TRANSPORT_INFO: 818 elif action == XEP_0166.A_TRANSPORT_INFO:
967 @param client: %(doc_client)s 1056 @param client: %(doc_client)s
968 @param request(domish.Element): full request 1057 @param request(domish.Element): full request
969 @param jingle_elt(domish.Element): <jingle> element 1058 @param jingle_elt(domish.Element): <jingle> element
970 @param session(dict): session data 1059 @param session(dict): session data
971 """ 1060 """
972 if "contents" in session: 1061 contents_dict = session["contents"]
1062 if contents_dict:
973 raise exceptions.InternalError( 1063 raise exceptions.InternalError(
974 "Contents dict should not already exist at this point" 1064 "Contents dict should not already be set at this point"
975 ) 1065 )
976 session["contents"] = contents_dict = {}
977 1066
978 try: 1067 try:
979 self._parse_elements( 1068 self._parse_elements(
980 jingle_elt, session, request, client, True, XEP_0166.ROLE_INITIATOR 1069 jingle_elt, session, request, client, True, XEP_0166.ROLE_INITIATOR
981 ) 1070 )
1105 ) 1194 )
1106 ) 1195 )
1107 d_list.addErrback(self._iq_error, session["id"], client) 1196 d_list.addErrback(self._iq_error, session["id"], client)
1108 return d_list 1197 return d_list
1109 1198
1110 def on_session_terminate(self, client, request, jingle_elt, session): 1199 def get_reason_elt(self, parent_elt: domish.Element) -> domish.Element:
1200 """Find a <reason> element in parent_elt
1201
1202 if none is found, add an empty one to the element
1203 @return: the <reason> element
1204 """
1205 try:
1206 return next(parent_elt.elements(NS_JINGLE, "reason"))
1207 except StopIteration:
1208 log.warning("No reason given for session termination")
1209 reason_elt = parent_elt.addElement("reason")
1210 return reason_elt
1211
1212 def parse_reason_elt(self, reason_elt: domish.Element) -> tuple[str|None, str|None]:
1213 """Parse a <reason> element
1214
1215 @return: reason found, and text if any
1216 """
1217 reason, text = None, None
1218 for elt in reason_elt.elements():
1219 if elt.uri == NS_JINGLE:
1220 if elt.name == "text":
1221 text = str(elt)
1222 else:
1223 reason = elt.name
1224
1225 if reason is None:
1226 log.debug("no reason specified,")
1227
1228 return reason, text
1229
1230 async def on_session_terminate(
1231 self,
1232 client: SatXMPPEntity,
1233 request: domish.Element,
1234 jingle_elt: domish.Element,
1235 session: dict
1236 ) -> None:
1111 # TODO: check reason, display a message to user if needed 1237 # TODO: check reason, display a message to user if needed
1112 log.debug(f"Jingle Session {session['id']} terminated") 1238 log.debug(f"Jingle Session {session['id']} terminated")
1113 try: 1239 reason_elt = self.get_reason_elt(jingle_elt)
1114 reason_elt = next(jingle_elt.elements(NS_JINGLE, "reason"))
1115 except StopIteration:
1116 log.warning("No reason given for session termination")
1117 reason_elt = jingle_elt.addElement("reason")
1118 1240
1119 terminate_defers = self._call_plugins( 1241 terminate_defers = self._call_plugins(
1120 client, 1242 client,
1121 XEP_0166.A_SESSION_TERMINATE, 1243 XEP_0166.A_SESSION_TERMINATE,
1122 session, 1244 session,
1127 elements=False, 1249 elements=False,
1128 force_element=reason_elt, 1250 force_element=reason_elt,
1129 ) 1251 )
1130 terminate_dlist = defer.DeferredList(terminate_defers) 1252 terminate_dlist = defer.DeferredList(terminate_defers)
1131 1253
1132 terminate_dlist.addCallback(lambda __: self._del_session(client, session["id"])) 1254 terminate_dlist.addCallback(lambda __: self.delete_session(client, session["id"]))
1133 client.send(xmlstream.toResponse(request, "result")) 1255 client.send(xmlstream.toResponse(request, "result"))
1134 1256
1135 async def on_session_accept(self, client, request, jingle_elt, session): 1257 async def on_session_accept(self, client, request, jingle_elt, session):
1136 """Method called once session is accepted 1258 """Method called once session is accepted
1137 1259