Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0353.py @ 3405:ecdb3728749e
plugin XEP-0353: Jingle Message Initiation implementation:
This plugin uses the new `XEP-0166_initiate` trigger to initiate a Jingle session with
messages if the peer jid has no resource specified.
On reception, if the sender is not in our roster, a confirmation is requested to user to
avoid leaking presence and IP. If user refuses the session for somebody not in roster,
nothing is sent at all (the request is just ignored).
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 12 Nov 2020 14:53:15 +0100 |
parents | |
children | be6d91572633 |
comparison
equal
deleted
inserted
replaced
3404:26a0af6e32c1 | 3405:ecdb3728749e |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SàT plugin for Jingle Message Initiation (XEP-0353) | |
4 # Copyright (C) 2009-2020 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 zope.interface import implementer | |
20 from twisted.internet import defer | |
21 from twisted.internet import reactor | |
22 from twisted.words.protocols.jabber import xmlstream, jid | |
23 from twisted.words.xish import domish | |
24 from wokkel import disco, iwokkel | |
25 from sat.core.i18n import _, D_ | |
26 from sat.core.constants import Const as C | |
27 from sat.core import exceptions | |
28 from sat.core.log import getLogger | |
29 from sat.tools import utils | |
30 from sat.tools import xml_tools | |
31 | |
32 log = getLogger(__name__) | |
33 | |
34 | |
35 NS_JINGLE_MESSAGE = "urn:xmpp:jingle-message:0" | |
36 | |
37 PLUGIN_INFO = { | |
38 C.PI_NAME: "Jingle Message Initiation", | |
39 C.PI_IMPORT_NAME: "XEP-0353", | |
40 C.PI_TYPE: "XEP", | |
41 C.PI_MODES: C.PLUG_MODE_BOTH, | |
42 C.PI_PROTOCOLS: ["XEP-0353"], | |
43 C.PI_DEPENDENCIES: ["XEP-0166"], | |
44 C.PI_MAIN: "XEP_0353", | |
45 C.PI_HANDLER: "yes", | |
46 C.PI_DESCRIPTION: _("""Implementation of Jingle Message Initiation"""), | |
47 } | |
48 | |
49 | |
50 class XEP_0353(object): | |
51 | |
52 def __init__(self, host): | |
53 log.info(_("plugin {name} initialization").format(name=PLUGIN_INFO[C.PI_NAME])) | |
54 self.host = host | |
55 host.registerNamespace("jingle-message", NS_JINGLE_MESSAGE) | |
56 self._j = host.plugins["XEP-0166"] | |
57 host.trigger.add("XEP-0166_initiate", self._onInitiateTrigger) | |
58 host.trigger.add("messageReceived", self._onMessageReceived) | |
59 | |
60 def getHandler(self, client): | |
61 return Handler() | |
62 | |
63 def profileConnecting(self, client): | |
64 # mapping from session id to deferred used to wait for destinee answer | |
65 client._xep_0353_pending_sessions = {} | |
66 | |
67 def buildMessageData(self, client, peer_jid, verb, session_id): | |
68 mess_data = { | |
69 'from': client.jid, | |
70 'to': peer_jid, | |
71 'uid': '', | |
72 'message': {}, | |
73 'type': C.MESS_TYPE_CHAT, | |
74 'subject': {}, | |
75 'extra': {} | |
76 } | |
77 client.generateMessageXML(mess_data) | |
78 verb_elt = mess_data["xml"].addElement((NS_JINGLE_MESSAGE, verb)) | |
79 verb_elt["id"] = session_id | |
80 return mess_data | |
81 | |
82 async def _onInitiateTrigger(self, client, session, contents): | |
83 peer_jid = session['peer_jid'] | |
84 if peer_jid.resource: | |
85 return True | |
86 | |
87 if peer_jid.userhostJID() not in client.roster: | |
88 # if the contact is not in our roster, we need to send a directed presence | |
89 # according to XEP-0353 §3.1 | |
90 await client.presence.available(peer_jid) | |
91 | |
92 mess_data = self.buildMessageData(client, peer_jid, "propose", session['id']) | |
93 for content in contents: | |
94 application, app_args, app_kwargs, content_name = self._j.getContentData( | |
95 content) | |
96 try: | |
97 jingleDescriptionElt = application.handler.jingleDescriptionElt | |
98 except AttributeError: | |
99 log.debug(f"no jingleDescriptionElt set for {application.handler}") | |
100 description_elt = domish.Element((content["app_ns"], "description")) | |
101 else: | |
102 description_elt = await utils.asDeferred( | |
103 jingleDescriptionElt, | |
104 client, session, content_name, *app_args, **app_kwargs | |
105 ) | |
106 mess_data["xml"].propose.addChild(description_elt) | |
107 response_d = defer.Deferred() | |
108 # we wait for 2 min before cancelling the session init | |
109 response_d.addTimeout(2*60, reactor) | |
110 client._xep_0353_pending_sessions[session['id']] = response_d | |
111 await client.sendMessageData(mess_data) | |
112 try: | |
113 accepting_jid = await response_d | |
114 except defer.TimeoutError: | |
115 log.warning(_( | |
116 "Message initiation with {peer_jid} timed out" | |
117 ).format(peer_jid=peer_jid)) | |
118 else: | |
119 session["peer_jid"] = accepting_jid | |
120 del client._xep_0353_pending_sessions[session['id']] | |
121 return True | |
122 | |
123 async def _onMessageReceived(self, client, message_elt, post_treat): | |
124 for elt in message_elt.elements(): | |
125 if elt.uri == NS_JINGLE_MESSAGE: | |
126 if elt.name == "propose": | |
127 return await self._handlePropose(client, message_elt, elt) | |
128 elif elt.name == "retract": | |
129 return self._handleRetract(client, message_elt, elt) | |
130 elif elt.name == "proceed": | |
131 return self._handleProceed(client, message_elt, elt) | |
132 elif elt.name == "accept": | |
133 return self._handleAccept(client, message_elt, elt) | |
134 elif elt.name == "reject": | |
135 return self._handleAccept(client, message_elt, elt) | |
136 else: | |
137 log.warning(f"invalid element: {elt.toXml}") | |
138 return True | |
139 return True | |
140 | |
141 async def _handlePropose(self, client, message_elt, elt): | |
142 peer_jid = jid.JID(message_elt["from"]) | |
143 session_id = elt["id"] | |
144 if peer_jid.userhostJID() not in client.roster: | |
145 app_ns = elt.description.uri | |
146 try: | |
147 application = self._j.getApplication(app_ns) | |
148 human_name = getattr(application.handler, "human_name", application.name) | |
149 except (exceptions.NotFound, AttributeError): | |
150 if app_ns.startswith("urn:xmpp:jingle:apps:"): | |
151 human_name = app_ns[21:].split(":", 1)[0].replace('-', ' ').title() | |
152 else: | |
153 splitted_ns = app_ns.split(':') | |
154 if len(splitted_ns) > 1: | |
155 human_name = splitted_ns[-2].replace('- ', ' ').title() | |
156 else: | |
157 human_name = app_ns | |
158 | |
159 confirm_msg = D_( | |
160 "Somebody not in your contact list ({peer_jid}) wants to do a " | |
161 '"{human_name}" session with you, this would leak your presence and ' | |
162 "possibly you IP (internet localisation), do you accept?" | |
163 ).format(peer_jid=peer_jid, human_name=human_name) | |
164 confirm_title = D_("Invitation from an unknown contact") | |
165 accept = await xml_tools.deferConfirm( | |
166 self.host, confirm_msg, confirm_title, profile=client.profile, | |
167 action_extra={ | |
168 "meta_type": C.META_TYPE_NOT_IN_ROSTER_LEAK, | |
169 "meta_session_id": session_id, | |
170 "meta_from_jid": peer_jid.full(), | |
171 } | |
172 ) | |
173 if not accept: | |
174 mess_data = self.buildMessageData( | |
175 client, client.jid.userhostJID(), "reject", session_id) | |
176 await client.sendMessageData(mess_data) | |
177 # we don't sent anything to sender, to avoid leaking presence | |
178 return False | |
179 else: | |
180 await client.presence.available(peer_jid) | |
181 session_id = elt["id"] | |
182 mess_data = self.buildMessageData( | |
183 client, client.jid.userhostJID(), "accept", session_id) | |
184 await client.sendMessageData(mess_data) | |
185 mess_data = self.buildMessageData( | |
186 client, peer_jid, "proceed", session_id) | |
187 await client.sendMessageData(mess_data) | |
188 return False | |
189 | |
190 def _handleRetract(self, client, message_elt, proceed_elt): | |
191 log.warning("retract is not implemented yet") | |
192 return False | |
193 | |
194 def _handleProceed(self, client, message_elt, proceed_elt): | |
195 try: | |
196 session_id = proceed_elt["id"] | |
197 except KeyError: | |
198 log.warning(f"invalid proceed element in message_elt: {message_elt}") | |
199 return True | |
200 try: | |
201 response_d = client._xep_0353_pending_sessions[session_id] | |
202 except KeyError: | |
203 log.warning( | |
204 _("no pending session found with id {session_id}, did it timed out?") | |
205 .format(session_id=session_id) | |
206 ) | |
207 return True | |
208 | |
209 response_d.callback(jid.JID(message_elt["from"])) | |
210 return False | |
211 | |
212 def _handleAccept(self, client, message_elt, accept_elt): | |
213 pass | |
214 | |
215 def _handleReject(self, client, message_elt, accept_elt): | |
216 pass | |
217 | |
218 | |
219 @implementer(iwokkel.IDisco) | |
220 class Handler(xmlstream.XMPPHandler): | |
221 | |
222 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | |
223 return [disco.DiscoFeature(NS_JINGLE_MESSAGE)] | |
224 | |
225 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | |
226 return [] |