Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0353.py @ 4055:38819c69aa39
plugin XEP-0353: update according to XEP changes + <description> fix:
- `XEP-0166_initiate_elt_built` trigger is now used to have the `<description>` element
built, so they can be used with there correct arguments in the jingle initiation message
- add message processing hint as it is now specified in the XEP
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 29 May 2023 13:32:40 +0200 |
parents | c23cad65ae99 |
children |
comparison
equal
deleted
inserted
replaced
4054:4c8bf67bfbeb | 4055:38819c69aa39 |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | 2 |
3 # SàT plugin for Jingle Message Initiation (XEP-0353) | 3 # Libervia plugin for Jingle Message Initiation (XEP-0353) |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | 4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) |
5 | 5 |
6 # This program is free software: you can redistribute it and/or modify | 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 | 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 | 8 # the Free Software Foundation, either version 3 of the License, or |
14 # GNU Affero General Public License for more details. | 14 # GNU Affero General Public License for more details. |
15 | 15 |
16 # You should have received a copy of the GNU Affero General Public License | 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/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 from zope.interface import implementer | |
20 from twisted.internet import defer | 19 from twisted.internet import defer |
21 from twisted.internet import reactor | 20 from twisted.internet import reactor |
22 from twisted.words.protocols.jabber import xmlstream, jid, error | 21 from twisted.words.protocols.jabber import error, jid |
22 from twisted.words.protocols.jabber import xmlstream | |
23 from twisted.words.xish import domish | 23 from twisted.words.xish import domish |
24 from wokkel import disco, iwokkel | 24 from wokkel import disco, iwokkel |
25 from sat.core.i18n import _, D_ | 25 from zope.interface import implementer |
26 | |
27 from sat.core import exceptions | |
26 from sat.core.constants import Const as C | 28 from sat.core.constants import Const as C |
27 from sat.core import exceptions | 29 from sat.core.core_types import SatXMPPEntity |
30 from sat.core.i18n import D_, _ | |
28 from sat.core.log import getLogger | 31 from sat.core.log import getLogger |
29 from sat.tools import utils | |
30 from sat.tools import xml_tools | 32 from sat.tools import xml_tools |
31 | 33 |
32 log = getLogger(__name__) | 34 log = getLogger(__name__) |
33 | 35 |
34 | 36 |
38 C.PI_NAME: "Jingle Message Initiation", | 40 C.PI_NAME: "Jingle Message Initiation", |
39 C.PI_IMPORT_NAME: "XEP-0353", | 41 C.PI_IMPORT_NAME: "XEP-0353", |
40 C.PI_TYPE: "XEP", | 42 C.PI_TYPE: "XEP", |
41 C.PI_MODES: [C.PLUG_MODE_CLIENT], | 43 C.PI_MODES: [C.PLUG_MODE_CLIENT], |
42 C.PI_PROTOCOLS: ["XEP-0353"], | 44 C.PI_PROTOCOLS: ["XEP-0353"], |
43 C.PI_DEPENDENCIES: ["XEP-0166"], | 45 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0334"], |
44 C.PI_MAIN: "XEP_0353", | 46 C.PI_MAIN: "XEP_0353", |
45 C.PI_HANDLER: "yes", | 47 C.PI_HANDLER: "yes", |
46 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), | 48 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), |
47 } | 49 } |
48 | 50 |
49 | 51 |
50 class XEP_0353: | 52 class XEP_0353: |
51 | |
52 def __init__(self, host): | 53 def __init__(self, host): |
53 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) | 54 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) |
54 self.host = host | 55 self.host = host |
55 host.register_namespace("jingle-message", NS_JINGLE_MESSAGE) | 56 host.register_namespace("jingle-message", NS_JINGLE_MESSAGE) |
56 self._j = host.plugins["XEP-0166"] | 57 self._j = host.plugins["XEP-0166"] |
57 host.trigger.add("XEP-0166_initiate", self._on_initiate_trigger) | 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 ) | |
58 host.trigger.add("message_received", self._on_message_received) | 66 host.trigger.add("message_received", self._on_message_received) |
59 | 67 |
60 def get_handler(self, client): | 68 def get_handler(self, client): |
61 return Handler() | 69 return Handler() |
62 | 70 |
64 # mapping from session id to deferred used to wait for destinee answer | 72 # mapping from session id to deferred used to wait for destinee answer |
65 client._xep_0353_pending_sessions = {} | 73 client._xep_0353_pending_sessions = {} |
66 | 74 |
67 def build_message_data(self, client, peer_jid, verb, session_id): | 75 def build_message_data(self, client, peer_jid, verb, session_id): |
68 mess_data = { | 76 mess_data = { |
69 'from': client.jid, | 77 "from": client.jid, |
70 'to': peer_jid, | 78 "to": peer_jid, |
71 'uid': '', | 79 "uid": "", |
72 'message': {}, | 80 "message": {}, |
73 'type': C.MESS_TYPE_CHAT, | 81 "type": C.MESS_TYPE_CHAT, |
74 'subject': {}, | 82 "subject": {}, |
75 'extra': {} | 83 "extra": {}, |
76 } | 84 } |
77 client.generate_message_xml(mess_data) | 85 client.generate_message_xml(mess_data) |
78 verb_elt = mess_data["xml"].addElement((NS_JINGLE_MESSAGE, verb)) | 86 message_elt = mess_data["xml"] |
87 verb_elt = message_elt.addElement((NS_JINGLE_MESSAGE, verb)) | |
79 verb_elt["id"] = session_id | 88 verb_elt["id"] = session_id |
89 self._h.add_hint_elements(message_elt, [self._h.HINT_STORE]) | |
80 return mess_data | 90 return mess_data |
81 | 91 |
82 async def _on_initiate_trigger(self, client, session, contents): | 92 async def _on_initiate_trigger( |
83 # FIXME: check that at least one resource of the peer_jid can handle the feature | 93 self, |
84 peer_jid = session['peer_jid'] | 94 client: SatXMPPEntity, |
95 session: dict, | |
96 iq_elt: domish.Element, | |
97 jingle_elt: domish.Element, | |
98 ) -> bool: | |
99 peer_jid = session["peer_jid"] | |
85 if peer_jid.resource: | 100 if peer_jid.resource: |
86 return True | 101 return True |
87 | 102 |
88 try: | 103 try: |
89 infos = await self.host.memory.disco.get_infos(client, peer_jid) | 104 infos = await self.host.memory.disco.get_infos(client, peer_jid) |
101 if peer_jid.userhostJID() not in client.roster: | 116 if peer_jid.userhostJID() not in client.roster: |
102 # if the contact is not in our roster, we need to send a directed presence | 117 # if the contact is not in our roster, we need to send a directed presence |
103 # according to XEP-0353 §3.1 | 118 # according to XEP-0353 §3.1 |
104 await client.presence.available(peer_jid) | 119 await client.presence.available(peer_jid) |
105 | 120 |
106 mess_data = self.build_message_data(client, peer_jid, "propose", session['id']) | 121 mess_data = self.build_message_data(client, peer_jid, "propose", session["id"]) |
107 for content in contents: | 122 message_elt = mess_data["xml"] |
108 content_data = self._j.get_content_data( | 123 for content_data in session["contents"].values(): |
109 content) | 124 # we get the full element build by the application plugin |
110 try: | 125 jingle_description_elt = content_data["application_data"]["desc_elt"] |
111 jingle_description_elt = ( | 126 # and copy it to only keep the root <description> element, no children |
112 content_data.application.handler.jingle_description_elt | 127 description_elt = domish.Element( |
113 ) | 128 (jingle_description_elt.uri, jingle_description_elt.name), |
114 except AttributeError: | 129 defaultUri=jingle_description_elt.defaultUri, |
115 log.debug( | 130 attribs=jingle_description_elt.attributes, |
116 "no jingle_description_elt set for " | 131 localPrefixes=jingle_description_elt.localPrefixes, |
117 f"{content_data.application.handler}" | 132 ) |
118 ) | 133 message_elt.propose.addChild(description_elt) |
119 description_elt = domish.Element((content["app_ns"], "description")) | |
120 else: | |
121 description_elt = await utils.as_deferred( | |
122 jingle_description_elt, | |
123 client, session, content_data.content_name, *content_data.app_args, | |
124 **content_data.app_kwargs | |
125 ) | |
126 mess_data["xml"].propose.addChild(description_elt) | |
127 response_d = defer.Deferred() | 134 response_d = defer.Deferred() |
128 # we wait for 2 min before cancelling the session init | 135 # we wait for 2 min before cancelling the session init |
129 # response_d.addTimeout(2*60, reactor) | |
130 # FIXME: let's application decide timeout? | 136 # FIXME: let's application decide timeout? |
131 response_d.addTimeout(2, reactor) | 137 response_d.addTimeout(2 * 60, reactor) |
132 client._xep_0353_pending_sessions[session['id']] = response_d | 138 client._xep_0353_pending_sessions[session["id"]] = response_d |
133 await client.send_message_data(mess_data) | 139 await client.send_message_data(mess_data) |
134 try: | 140 try: |
135 accepting_jid = await response_d | 141 accepting_jid = await response_d |
136 except defer.TimeoutError: | 142 except defer.TimeoutError: |
137 log.warning(_( | 143 log.warning( |
138 "Message initiation with {peer_jid} timed out" | 144 _("Message initiation with {peer_jid} timed out").format( |
139 ).format(peer_jid=peer_jid)) | 145 peer_jid=peer_jid |
146 ) | |
147 ) | |
140 else: | 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() | |
141 session["peer_jid"] = accepting_jid | 156 session["peer_jid"] = accepting_jid |
142 del client._xep_0353_pending_sessions[session['id']] | 157 del client._xep_0353_pending_sessions[session["id"]] |
143 return True | 158 return True |
144 | 159 |
145 async def _on_message_received(self, client, message_elt, post_treat): | 160 async def _on_message_received(self, client, message_elt, post_treat): |
146 for elt in message_elt.elements(): | 161 for elt in message_elt.elements(): |
147 if elt.uri == NS_JINGLE_MESSAGE: | 162 if elt.uri == NS_JINGLE_MESSAGE: |
168 try: | 183 try: |
169 application = self._j.get_application(app_ns) | 184 application = self._j.get_application(app_ns) |
170 human_name = getattr(application.handler, "human_name", application.name) | 185 human_name = getattr(application.handler, "human_name", application.name) |
171 except (exceptions.NotFound, AttributeError): | 186 except (exceptions.NotFound, AttributeError): |
172 if app_ns.startswith("urn:xmpp:jingle:apps:"): | 187 if app_ns.startswith("urn:xmpp:jingle:apps:"): |
173 human_name = app_ns[21:].split(":", 1)[0].replace('-', ' ').title() | 188 human_name = app_ns[21:].split(":", 1)[0].replace("-", " ").title() |
174 else: | 189 else: |
175 splitted_ns = app_ns.split(':') | 190 splitted_ns = app_ns.split(":") |
176 if len(splitted_ns) > 1: | 191 if len(splitted_ns) > 1: |
177 human_name = splitted_ns[-2].replace('- ', ' ').title() | 192 human_name = splitted_ns[-2].replace("- ", " ").title() |
178 else: | 193 else: |
179 human_name = app_ns | 194 human_name = app_ns |
180 | 195 |
181 confirm_msg = D_( | 196 confirm_msg = D_( |
182 "Somebody not in your contact list ({peer_jid}) wants to do a " | 197 "Somebody not in your contact list ({peer_jid}) wants to do a " |
183 '"{human_name}" session with you, this would leak your presence and ' | 198 '"{human_name}" session with you, this would leak your presence and ' |
184 "possibly you IP (internet localisation), do you accept?" | 199 "possibly you IP (internet localisation), do you accept?" |
185 ).format(peer_jid=peer_jid, human_name=human_name) | 200 ).format(peer_jid=peer_jid, human_name=human_name) |
186 confirm_title = D_("Invitation from an unknown contact") | 201 confirm_title = D_("Invitation from an unknown contact") |
187 accept = await xml_tools.defer_confirm( | 202 accept = await xml_tools.defer_confirm( |
188 self.host, confirm_msg, confirm_title, profile=client.profile, | 203 self.host, |
204 confirm_msg, | |
205 confirm_title, | |
206 profile=client.profile, | |
189 action_extra={ | 207 action_extra={ |
190 "type": C.META_TYPE_NOT_IN_ROSTER_LEAK, | 208 "type": C.META_TYPE_NOT_IN_ROSTER_LEAK, |
191 "session_id": session_id, | 209 "session_id": session_id, |
192 "from_jid": peer_jid.full(), | 210 "from_jid": peer_jid.full(), |
193 } | 211 }, |
194 ) | 212 ) |
195 if not accept: | 213 if not accept: |
196 mess_data = self.build_message_data( | 214 mess_data = self.build_message_data( |
197 client, client.jid.userhostJID(), "reject", session_id) | 215 client, client.jid.userhostJID(), "reject", session_id |
216 ) | |
198 await client.send_message_data(mess_data) | 217 await client.send_message_data(mess_data) |
199 # we don't sent anything to sender, to avoid leaking presence | 218 # we don't sent anything to sender, to avoid leaking presence |
200 return False | 219 return False |
201 else: | 220 else: |
202 await client.presence.available(peer_jid) | 221 await client.presence.available(peer_jid) |
203 session_id = elt["id"] | 222 session_id = elt["id"] |
204 # FIXME: accept is not used anymore in new specification, check it and remove it | 223 mess_data = self.build_message_data(client, peer_jid, "proceed", session_id) |
205 mess_data = self.build_message_data( | |
206 client, client.jid.userhostJID(), "accept", session_id) | |
207 await client.send_message_data(mess_data) | |
208 mess_data = self.build_message_data( | |
209 client, peer_jid, "proceed", session_id) | |
210 await client.send_message_data(mess_data) | 224 await client.send_message_data(mess_data) |
211 return False | 225 return False |
212 | 226 |
213 def _handle_retract(self, client, message_elt, proceed_elt): | 227 def _handle_retract(self, client, message_elt, proceed_elt): |
214 log.warning("retract is not implemented yet") | 228 log.warning("retract is not implemented yet") |
222 return True | 236 return True |
223 try: | 237 try: |
224 response_d = client._xep_0353_pending_sessions[session_id] | 238 response_d = client._xep_0353_pending_sessions[session_id] |
225 except KeyError: | 239 except KeyError: |
226 log.warning( | 240 log.warning( |
227 _("no pending session found with id {session_id}, did it timed out?") | 241 _( |
228 .format(session_id=session_id) | 242 "no pending session found with id {session_id}, did it timed out?" |
243 ).format(session_id=session_id) | |
229 ) | 244 ) |
230 return True | 245 return True |
231 | 246 |
232 response_d.callback(jid.JID(message_elt["from"])) | 247 response_d.callback(jid.JID(message_elt["from"])) |
233 return False | 248 return False |
239 pass | 254 pass |
240 | 255 |
241 | 256 |
242 @implementer(iwokkel.IDisco) | 257 @implementer(iwokkel.IDisco) |
243 class Handler(xmlstream.XMPPHandler): | 258 class Handler(xmlstream.XMPPHandler): |
244 | |
245 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 259 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
246 return [disco.DiscoFeature(NS_JINGLE_MESSAGE)] | 260 return [disco.DiscoFeature(NS_JINGLE_MESSAGE)] |
247 | 261 |
248 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 262 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |
249 return [] | 263 return [] |