Mercurial > libervia-backend
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 |