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