comparison libervia/backend/plugins/plugin_xep_0234.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_0234.py@2ced30f6d5de
children bc60875cb3b8
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3 # SàT plugin for Jingle File Transfer (XEP-0234)
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 collections import namedtuple
20 import mimetypes
21 import os.path
22
23 from twisted.internet import defer
24 from twisted.internet import reactor
25 from twisted.internet import error as internet_error
26 from twisted.python import failure
27 from twisted.words.protocols.jabber import jid
28 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
29 from twisted.words.xish import domish
30 from wokkel import disco, iwokkel
31 from zope.interface import implementer
32
33 from libervia.backend.core import exceptions
34 from libervia.backend.core.constants import Const as C
35 from libervia.backend.core.i18n import D_, _
36 from libervia.backend.core.log import getLogger
37 from libervia.backend.tools import utils
38 from libervia.backend.tools import stream
39 from libervia.backend.tools.common import date_utils
40 from libervia.backend.tools.common import regex
41
42
43 log = getLogger(__name__)
44
45 NS_JINGLE_FT = "urn:xmpp:jingle:apps:file-transfer:5"
46
47 PLUGIN_INFO = {
48 C.PI_NAME: "Jingle File Transfer",
49 C.PI_IMPORT_NAME: "XEP-0234",
50 C.PI_TYPE: "XEP",
51 C.PI_MODES: C.PLUG_MODE_BOTH,
52 C.PI_PROTOCOLS: ["XEP-0234"],
53 C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0300", "FILE"],
54 C.PI_MAIN: "XEP_0234",
55 C.PI_HANDLER: "yes",
56 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""),
57 }
58
59 EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"}
60 Range = namedtuple("Range", ("offset", "length"))
61
62
63 class XEP_0234:
64 # TODO: assure everything is closed when file is sent or session terminate is received
65 # TODO: call self._f.unregister when unloading order will be managing (i.e. when
66 # dependencies will be unloaded at the end)
67 Range = Range # we copy the class here, so it can be used by other plugins
68 name = PLUGIN_INFO[C.PI_NAME]
69 human_name = D_("file transfer")
70
71 def __init__(self, host):
72 log.info(_("plugin Jingle File Transfer initialization"))
73 self.host = host
74 host.register_namespace("jingle-ft", NS_JINGLE_FT)
75 self._j = host.plugins["XEP-0166"] # shortcut to access jingle
76 self._j.register_application(NS_JINGLE_FT, self)
77 self._f = host.plugins["FILE"]
78 self._f.register(self, priority=10000)
79 self._hash = self.host.plugins["XEP-0300"]
80 host.bridge.add_method(
81 "file_jingle_send",
82 ".plugin",
83 in_sign="ssssa{ss}s",
84 out_sign="",
85 method=self._file_send,
86 async_=True,
87 )
88 host.bridge.add_method(
89 "file_jingle_request",
90 ".plugin",
91 in_sign="sssssa{ss}s",
92 out_sign="s",
93 method=self._file_jingle_request,
94 async_=True,
95 )
96
97 def get_handler(self, client):
98 return XEP_0234_handler()
99
100 def get_progress_id(self, session, content_name):
101 """Return a unique progress ID
102
103 @param session(dict): jingle session
104 @param content_name(unicode): name of the content
105 @return (unicode): unique progress id
106 """
107 return "{}_{}".format(session["id"], content_name)
108
109 async def can_handle_file_send(self, client, peer_jid, filepath):
110 if peer_jid.resource:
111 return await self.host.hasFeature(client, NS_JINGLE_FT, peer_jid)
112 else:
113 # if we have a bare jid, Jingle Message Initiation will be tried
114 return True
115
116 # generic methods
117
118 def build_file_element(
119 self, client, name=None, file_hash=None, hash_algo=None, size=None,
120 mime_type=None, desc=None, modified=None, transfer_range=None, path=None,
121 namespace=None, file_elt=None, **kwargs):
122 """Generate a <file> element with available metadata
123
124 @param file_hash(unicode, None): hash of the file
125 empty string to set <hash-used/> element
126 @param hash_algo(unicode, None): hash algorithm used
127 if file_hash is None and hash_algo is set, a <hash-used/> element will be
128 generated
129 @param transfer_range(Range, None): where transfer must start/stop
130 @param modified(int, unicode, None): date of last modification
131 0 to use current date
132 int to use an unix timestamp
133 else must be an unicode string which will be used as it (it must be an XMPP
134 time)
135 @param file_elt(domish.Element, None): element to use
136 None to create a new one
137 @param **kwargs: data for plugin extension (ignored by default)
138 @return (domish.Element): generated element
139 @trigger XEP-0234_buildFileElement(file_elt, extra_args): can be used to extend
140 elements to add
141 """
142 if file_elt is None:
143 file_elt = domish.Element((NS_JINGLE_FT, "file"))
144 for name, value in (
145 ("name", name),
146 ("size", size),
147 ("media-type", mime_type),
148 ("desc", desc),
149 ("path", path),
150 ("namespace", namespace),
151 ):
152 if value is not None:
153 file_elt.addElement(name, content=str(value))
154
155 if modified is not None:
156 if isinstance(modified, int):
157 file_elt.addElement("date", utils.xmpp_date(modified or None))
158 else:
159 file_elt.addElement("date", modified)
160 elif "created" in kwargs:
161 file_elt.addElement("date", utils.xmpp_date(kwargs.pop("created")))
162
163 range_elt = file_elt.addElement("range")
164 if transfer_range is not None:
165 if transfer_range.offset is not None:
166 range_elt["offset"] = transfer_range.offset
167 if transfer_range.length is not None:
168 range_elt["length"] = transfer_range.length
169 if file_hash is not None:
170 if not file_hash:
171 file_elt.addChild(self._hash.build_hash_used_elt())
172 else:
173 file_elt.addChild(self._hash.build_hash_elt(file_hash, hash_algo))
174 elif hash_algo is not None:
175 file_elt.addChild(self._hash.build_hash_used_elt(hash_algo))
176 self.host.trigger.point(
177 "XEP-0234_buildFileElement", client, file_elt, extra_args=kwargs)
178 if kwargs:
179 for kw in kwargs:
180 log.debug("ignored keyword: {}".format(kw))
181 return file_elt
182
183 def build_file_element_from_dict(self, client, file_data, **kwargs):
184 """like build_file_element but get values from a file_data dict
185
186 @param file_data(dict): metadata to use
187 @param **kwargs: data to override
188 """
189 if kwargs:
190 file_data = file_data.copy()
191 file_data.update(kwargs)
192 try:
193 file_data["mime_type"] = (
194 f'{file_data.pop("media_type")}/{file_data.pop("media_subtype")}'
195 )
196 except KeyError:
197 pass
198 return self.build_file_element(client, **file_data)
199
200 async def parse_file_element(
201 self, client, file_elt, file_data=None, given=False, parent_elt=None,
202 keep_empty_range=False):
203 """Parse a <file> element and file dictionary accordingly
204
205 @param file_data(dict, None): dict where the data will be set
206 following keys will be set (and overwritten if they already exist):
207 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range
208 if None, a new dict is created
209 @param given(bool): if True, prefix hash key with "given_"
210 @param parent_elt(domish.Element, None): parent of the file element
211 if set, file_elt must not be set
212 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset
213 and length are None).
214 Empty range is useful to know if a peer_jid can handle range
215 @return (dict): file_data
216 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new
217 elements
218 @raise exceptions.NotFound: there is not <file> element in parent_elt
219 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
220 """
221 if parent_elt is not None:
222 if file_elt is not None:
223 raise exceptions.InternalError(
224 "file_elt must be None if parent_elt is set"
225 )
226 try:
227 file_elt = next(parent_elt.elements(NS_JINGLE_FT, "file"))
228 except StopIteration:
229 raise exceptions.NotFound()
230 else:
231 if not file_elt or file_elt.uri != NS_JINGLE_FT:
232 raise exceptions.DataError(
233 "invalid <file> element: {stanza}".format(stanza=file_elt.toXml())
234 )
235
236 if file_data is None:
237 file_data = {}
238
239 for name in ("name", "desc", "path", "namespace"):
240 try:
241 file_data[name] = str(next(file_elt.elements(NS_JINGLE_FT, name)))
242 except StopIteration:
243 pass
244
245 name = file_data.get("name")
246 if name == "..":
247 # we don't want to go to parent dir when joining to a path
248 name = "--"
249 file_data["name"] = name
250 elif name is not None and ("/" in name or "\\" in name):
251 file_data["name"] = regex.path_escape(name)
252
253 try:
254 file_data["mime_type"] = str(
255 next(file_elt.elements(NS_JINGLE_FT, "media-type"))
256 )
257 except StopIteration:
258 pass
259
260 try:
261 file_data["size"] = int(
262 str(next(file_elt.elements(NS_JINGLE_FT, "size")))
263 )
264 except StopIteration:
265 pass
266
267 try:
268 file_data["modified"] = date_utils.date_parse(
269 next(file_elt.elements(NS_JINGLE_FT, "date"))
270 )
271 except StopIteration:
272 pass
273
274 try:
275 range_elt = next(file_elt.elements(NS_JINGLE_FT, "range"))
276 except StopIteration:
277 pass
278 else:
279 offset = range_elt.getAttribute("offset")
280 length = range_elt.getAttribute("length")
281 if offset or length or keep_empty_range:
282 file_data["transfer_range"] = Range(offset=offset, length=length)
283
284 prefix = "given_" if given else ""
285 hash_algo_key, hash_key = "hash_algo", prefix + "file_hash"
286 try:
287 file_data[hash_algo_key], file_data[hash_key] = self._hash.parse_hash_elt(
288 file_elt
289 )
290 except exceptions.NotFound:
291 pass
292
293 self.host.trigger.point("XEP-0234_parseFileElement", client, file_elt, file_data)
294
295 return file_data
296
297 # bridge methods
298
299 def _file_send(
300 self,
301 peer_jid,
302 filepath,
303 name="",
304 file_desc="",
305 extra=None,
306 profile=C.PROF_KEY_NONE,
307 ):
308 client = self.host.get_client(profile)
309 return defer.ensureDeferred(self.file_send(
310 client,
311 jid.JID(peer_jid),
312 filepath,
313 name or None,
314 file_desc or None,
315 extra or None,
316 ))
317
318 async def file_send(
319 self, client, peer_jid, filepath, name, file_desc=None, extra=None
320 ):
321 """Send a file using jingle file transfer
322
323 @param peer_jid(jid.JID): destinee jid
324 @param filepath(str): absolute path of the file
325 @param name(unicode, None): name of the file
326 @param file_desc(unicode, None): description of the file
327 @return (D(unicode)): progress id
328 """
329 progress_id_d = defer.Deferred()
330 if extra is None:
331 extra = {}
332 if file_desc is not None:
333 extra["file_desc"] = file_desc
334 encrypted = extra.pop("encrypted", False)
335 await self._j.initiate(
336 client,
337 peer_jid,
338 [
339 {
340 "app_ns": NS_JINGLE_FT,
341 "senders": self._j.ROLE_INITIATOR,
342 "app_kwargs": {
343 "filepath": filepath,
344 "name": name,
345 "extra": extra,
346 "progress_id_d": progress_id_d,
347 },
348 }
349 ],
350 encrypted = encrypted
351 )
352 return await progress_id_d
353
354 def _file_jingle_request(
355 self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,
356 profile=C.PROF_KEY_NONE):
357 client = self.host.get_client(profile)
358 return defer.ensureDeferred(self.file_jingle_request(
359 client,
360 jid.JID(peer_jid),
361 filepath,
362 name or None,
363 file_hash or None,
364 hash_algo or None,
365 extra or None,
366 ))
367
368 async def file_jingle_request(
369 self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None,
370 extra=None):
371 """Request a file using jingle file transfer
372
373 @param peer_jid(jid.JID): destinee jid
374 @param filepath(str): absolute path where the file will be downloaded
375 @param name(unicode, None): name of the file
376 @param file_hash(unicode, None): hash of the file
377 @return (D(unicode)): progress id
378 """
379 progress_id_d = defer.Deferred()
380 if extra is None:
381 extra = {}
382 if file_hash is not None:
383 if hash_algo is None:
384 raise ValueError(_("hash_algo must be set if file_hash is set"))
385 extra["file_hash"] = file_hash
386 extra["hash_algo"] = hash_algo
387 else:
388 if hash_algo is not None:
389 raise ValueError(_("file_hash must be set if hash_algo is set"))
390 await self._j.initiate(
391 client,
392 peer_jid,
393 [
394 {
395 "app_ns": NS_JINGLE_FT,
396 "senders": self._j.ROLE_RESPONDER,
397 "app_kwargs": {
398 "filepath": filepath,
399 "name": name,
400 "extra": extra,
401 "progress_id_d": progress_id_d,
402 },
403 }
404 ],
405 )
406 return await progress_id_d
407
408 # jingle callbacks
409
410 def jingle_description_elt(
411 self, client, session, content_name, filepath, name, extra, progress_id_d
412 ):
413 return domish.Element((NS_JINGLE_FT, "description"))
414
415 def jingle_session_init(
416 self, client, session, content_name, filepath, name, extra, progress_id_d
417 ):
418 if extra is None:
419 extra = {}
420 else:
421 if not EXTRA_ALLOWED.issuperset(extra):
422 raise ValueError(
423 _("only the following keys are allowed in extra: {keys}").format(
424 keys=", ".join(EXTRA_ALLOWED)
425 )
426 )
427 progress_id_d.callback(self.get_progress_id(session, content_name))
428 content_data = session["contents"][content_name]
429 application_data = content_data["application_data"]
430 assert "file_path" not in application_data
431 application_data["file_path"] = filepath
432 file_data = application_data["file_data"] = {}
433 desc_elt = self.jingle_description_elt(
434 client, session, content_name, filepath, name, extra, progress_id_d)
435 file_elt = desc_elt.addElement("file")
436
437 if content_data["senders"] == self._j.ROLE_INITIATOR:
438 # we send a file
439 if name is None:
440 name = os.path.basename(filepath)
441 file_data["date"] = utils.xmpp_date()
442 file_data["desc"] = extra.pop("file_desc", "")
443 file_data["name"] = name
444 mime_type = mimetypes.guess_type(name, strict=False)[0]
445 if mime_type is not None:
446 file_data["mime_type"] = mime_type
447 file_data["size"] = os.path.getsize(filepath)
448 if "namespace" in extra:
449 file_data["namespace"] = extra["namespace"]
450 if "path" in extra:
451 file_data["path"] = extra["path"]
452 self.build_file_element_from_dict(
453 client, file_data, file_elt=file_elt, file_hash="")
454 else:
455 # we request a file
456 file_hash = extra.pop("file_hash", "")
457 if not name and not file_hash:
458 raise ValueError(_("you need to provide at least name or file hash"))
459 if name:
460 file_data["name"] = name
461 if file_hash:
462 file_data["file_hash"] = file_hash
463 file_data["hash_algo"] = extra["hash_algo"]
464 else:
465 file_data["hash_algo"] = self._hash.get_default_algo()
466 if "namespace" in extra:
467 file_data["namespace"] = extra["namespace"]
468 if "path" in extra:
469 file_data["path"] = extra["path"]
470 self.build_file_element_from_dict(client, file_data, file_elt=file_elt)
471
472 return desc_elt
473
474 async def jingle_request_confirmation(
475 self, client, action, session, content_name, desc_elt
476 ):
477 """This method request confirmation for a jingle session"""
478 content_data = session["contents"][content_name]
479 senders = content_data["senders"]
480 if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER):
481 log.warning("Bad sender, assuming initiator")
482 senders = content_data["senders"] = self._j.ROLE_INITIATOR
483 # first we grab file informations
484 try:
485 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file"))
486 except StopIteration:
487 raise failure.Failure(exceptions.DataError)
488 file_data = {"progress_id": self.get_progress_id(session, content_name)}
489
490 if senders == self._j.ROLE_RESPONDER:
491 # we send the file
492 return await self._file_sending_request_conf(
493 client, session, content_data, content_name, file_data, file_elt
494 )
495 else:
496 # we receive the file
497 return await self._file_receiving_request_conf(
498 client, session, content_data, content_name, file_data, file_elt
499 )
500
501 async def _file_sending_request_conf(
502 self, client, session, content_data, content_name, file_data, file_elt
503 ):
504 """parse file_elt, and handle file retrieving/permission checking"""
505 await self.parse_file_element(client, file_elt, file_data)
506 content_data["application_data"]["file_data"] = file_data
507 finished_d = content_data["finished_d"] = defer.Deferred()
508
509 # confirmed_d is a deferred returning confimed value (only used if cont is False)
510 cont, confirmed_d = self.host.trigger.return_point(
511 "XEP-0234_fileSendingRequest",
512 client,
513 session,
514 content_data,
515 content_name,
516 file_data,
517 file_elt,
518 )
519 if not cont:
520 confirmed = await confirmed_d
521 if confirmed:
522 args = [client, session, content_name, content_data]
523 finished_d.addCallbacks(
524 self._finished_cb, self._finished_eb, args, None, args
525 )
526 return confirmed
527
528 log.warning(_("File continue is not implemented yet"))
529 return False
530
531 async def _file_receiving_request_conf(
532 self, client, session, content_data, content_name, file_data, file_elt
533 ):
534 """parse file_elt, and handle user permission/file opening"""
535 await self.parse_file_element(client, file_elt, file_data, given=True)
536 try:
537 hash_algo, file_data["given_file_hash"] = self._hash.parse_hash_elt(file_elt)
538 except exceptions.NotFound:
539 try:
540 hash_algo = self._hash.parse_hash_used_elt(file_elt)
541 except exceptions.NotFound:
542 raise failure.Failure(exceptions.DataError)
543
544 if hash_algo is not None:
545 file_data["hash_algo"] = hash_algo
546 file_data["hash_hasher"] = hasher = self._hash.get_hasher(hash_algo)
547 file_data["data_cb"] = lambda data: hasher.update(data)
548
549 try:
550 file_data["size"] = int(file_data["size"])
551 except ValueError:
552 raise failure.Failure(exceptions.DataError)
553
554 name = file_data["name"]
555 if "/" in name or "\\" in name:
556 log.warning(
557 "File name contain path characters, we replace them: {}".format(name)
558 )
559 file_data["name"] = name.replace("/", "_").replace("\\", "_")
560
561 content_data["application_data"]["file_data"] = file_data
562
563 # now we actualy request permission to user
564
565 # deferred to track end of transfer
566 finished_d = content_data["finished_d"] = defer.Deferred()
567 confirmed = await self._f.get_dest_dir(
568 client, session["peer_jid"], content_data, file_data, stream_object=True
569 )
570 if confirmed:
571 await self.host.trigger.async_point(
572 "XEP-0234_file_receiving_request_conf",
573 client, session, content_data, file_elt
574 )
575 args = [client, session, content_name, content_data]
576 finished_d.addCallbacks(
577 self._finished_cb, self._finished_eb, args, None, args
578 )
579 return confirmed
580
581 async def jingle_handler(self, client, action, session, content_name, desc_elt):
582 content_data = session["contents"][content_name]
583 application_data = content_data["application_data"]
584 if action in (self._j.A_ACCEPTED_ACK,):
585 pass
586 elif action == self._j.A_SESSION_INITIATE:
587 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file"))
588 try:
589 next(file_elt.elements(NS_JINGLE_FT, "range"))
590 except StopIteration:
591 # initiator doesn't manage <range>, but we do so we advertise it
592 # FIXME: to be checked
593 log.debug("adding <range> element")
594 file_elt.addElement("range")
595 elif action == self._j.A_SESSION_ACCEPT:
596 assert not "stream_object" in content_data
597 file_data = application_data["file_data"]
598 file_path = application_data["file_path"]
599 senders = content_data["senders"]
600 if senders != session["role"]:
601 # we are receiving the file
602 try:
603 # did the responder specified the size of the file?
604 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file"))
605 size_elt = next(file_elt.elements(NS_JINGLE_FT, "size"))
606 size = int(str(size_elt))
607 except (StopIteration, ValueError):
608 size = None
609 # XXX: hash security is not critical here, so we just take the higher
610 # mandatory one
611 hasher = file_data["hash_hasher"] = self._hash.get_hasher()
612 progress_id = self.get_progress_id(session, content_name)
613 try:
614 content_data["stream_object"] = stream.FileStreamObject(
615 self.host,
616 client,
617 file_path,
618 mode="wb",
619 uid=progress_id,
620 size=size,
621 data_cb=lambda data: hasher.update(data),
622 )
623 except Exception as e:
624 self.host.bridge.progress_error(
625 progress_id, C.PROGRESS_ERROR_FAILED, client.profile
626 )
627 await self._j.terminate(
628 client, self._j.REASON_FAILED_APPLICATION, session)
629 raise e
630 else:
631 # we are sending the file
632 size = file_data["size"]
633 # XXX: hash security is not critical here, so we just take the higher
634 # mandatory one
635 hasher = file_data["hash_hasher"] = self._hash.get_hasher()
636 content_data["stream_object"] = stream.FileStreamObject(
637 self.host,
638 client,
639 file_path,
640 uid=self.get_progress_id(session, content_name),
641 size=size,
642 data_cb=lambda data: hasher.update(data),
643 )
644 finished_d = content_data["finished_d"] = defer.Deferred()
645 args = [client, session, content_name, content_data]
646 finished_d.addCallbacks(self._finished_cb, self._finished_eb, args, None, args)
647 await self.host.trigger.async_point(
648 "XEP-0234_jingle_handler",
649 client, session, content_data, desc_elt
650 )
651 else:
652 log.warning("FIXME: unmanaged action {}".format(action))
653 return desc_elt
654
655 def jingle_session_info(self, client, action, session, content_name, jingle_elt):
656 """Called on session-info action
657
658 manage checksum, and ignore <received/> element
659 """
660 # TODO: manage <received/> element
661 content_data = session["contents"][content_name]
662 elts = [elt for elt in jingle_elt.elements() if elt.uri == NS_JINGLE_FT]
663 if not elts:
664 return
665 for elt in elts:
666 if elt.name == "received":
667 pass
668 elif elt.name == "checksum":
669 # we have received the file hash, we need to parse it
670 if content_data["senders"] == session["role"]:
671 log.warning(
672 "unexpected checksum received while we are the file sender"
673 )
674 raise exceptions.DataError
675 info_content_name = elt["name"]
676 if info_content_name != content_name:
677 # it was for an other content...
678 return
679 file_data = content_data["application_data"]["file_data"]
680 try:
681 file_elt = next(elt.elements(NS_JINGLE_FT, "file"))
682 except StopIteration:
683 raise exceptions.DataError
684 algo, file_data["given_file_hash"] = self._hash.parse_hash_elt(file_elt)
685 if algo != file_data.get("hash_algo"):
686 log.warning(
687 "Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo}) [{profile}]".format(
688 peer_algo=algo,
689 our_algo=file_data.get("hash_algo"),
690 profile=client.profile,
691 )
692 )
693 else:
694 self._receiver_try_terminate(
695 client, session, content_name, content_data
696 )
697 else:
698 raise NotImplementedError
699
700 def jingle_terminate(self, client, action, session, content_name, reason_elt):
701 if reason_elt.decline:
702 # progress is the only way to tell to frontends that session has been declined
703 progress_id = self.get_progress_id(session, content_name)
704 self.host.bridge.progress_error(
705 progress_id, C.PROGRESS_ERROR_DECLINED, client.profile
706 )
707 elif not reason_elt.success:
708 progress_id = self.get_progress_id(session, content_name)
709 first_child = reason_elt.firstChildElement()
710 if first_child is not None:
711 reason = first_child.name
712 if reason_elt.text is not None:
713 reason = f"{reason} - {reason_elt.text}"
714 else:
715 reason = C.PROGRESS_ERROR_FAILED
716 self.host.bridge.progress_error(
717 progress_id, reason, client.profile
718 )
719
720 def _send_check_sum(self, client, session, content_name, content_data):
721 """Send the session-info with the hash checksum"""
722 file_data = content_data["application_data"]["file_data"]
723 hasher = file_data["hash_hasher"]
724 hash_ = hasher.hexdigest()
725 log.debug("Calculated hash: {}".format(hash_))
726 iq_elt, jingle_elt = self._j.build_session_info(client, session)
727 checksum_elt = jingle_elt.addElement((NS_JINGLE_FT, "checksum"))
728 checksum_elt["creator"] = content_data["creator"]
729 checksum_elt["name"] = content_name
730 file_elt = checksum_elt.addElement("file")
731 file_elt.addChild(self._hash.build_hash_elt(hash_))
732 iq_elt.send()
733
734 def _receiver_try_terminate(
735 self, client, session, content_name, content_data, last_try=False
736 ):
737 """Try to terminate the session
738
739 This method must only be used by the receiver.
740 It check if transfer is finished, and hash available,
741 if everything is OK, it check hash and terminate the session
742 @param last_try(bool): if True this mean than session must be terminated even given hash is not available
743 @return (bool): True if session was terminated
744 """
745 if not content_data.get("transfer_finished", False):
746 return False
747 file_data = content_data["application_data"]["file_data"]
748 given_hash = file_data.get("given_file_hash")
749 if given_hash is None:
750 if last_try:
751 log.warning(
752 "sender didn't sent hash checksum, we can't check the file [{profile}]".format(
753 profile=client.profile
754 )
755 )
756 self._j.delayed_content_terminate(client, session, content_name)
757 content_data["stream_object"].close()
758 return True
759 return False
760 hasher = file_data["hash_hasher"]
761 hash_ = hasher.hexdigest()
762
763 if hash_ == given_hash:
764 log.info(f"Hash checked, file was successfully transfered: {hash_}")
765 progress_metadata = {
766 "hash": hash_,
767 "hash_algo": file_data["hash_algo"],
768 "hash_verified": C.BOOL_TRUE,
769 }
770 error = None
771 else:
772 log.warning("Hash mismatch, the file was not transfered correctly")
773 progress_metadata = None
774 error = "Hash mismatch: given={algo}:{given}, calculated={algo}:{our}".format(
775 algo=file_data["hash_algo"], given=given_hash, our=hash_
776 )
777
778 self._j.delayed_content_terminate(client, session, content_name)
779 content_data["stream_object"].close(progress_metadata, error)
780 # we may have the last_try timer still active, so we try to cancel it
781 try:
782 content_data["last_try_timer"].cancel()
783 except (KeyError, internet_error.AlreadyCalled):
784 pass
785 return True
786
787 def _finished_cb(self, __, client, session, content_name, content_data):
788 log.info("File transfer terminated")
789 if content_data["senders"] != session["role"]:
790 # we terminate the session only if we are the receiver,
791 # as recommanded in XEP-0234 §2 (after example 6)
792 content_data["transfer_finished"] = True
793 if not self._receiver_try_terminate(
794 client, session, content_name, content_data
795 ):
796 # we have not received the hash yet, we wait 5 more seconds
797 content_data["last_try_timer"] = reactor.callLater(
798 5,
799 self._receiver_try_terminate,
800 client,
801 session,
802 content_name,
803 content_data,
804 last_try=True,
805 )
806 else:
807 # we are the sender, we send the checksum
808 self._send_check_sum(client, session, content_name, content_data)
809 content_data["stream_object"].close()
810
811 def _finished_eb(self, failure, client, session, content_name, content_data):
812 log.warning("Error while streaming file: {}".format(failure))
813 content_data["stream_object"].close()
814 self._j.content_terminate(
815 client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT
816 )
817
818
819 @implementer(iwokkel.IDisco)
820 class XEP_0234_handler(XMPPHandler):
821
822 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
823 return [disco.DiscoFeature(NS_JINGLE_FT)]
824
825 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
826 return []