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 []