comparison sat/plugins/plugin_xep_0234.py @ 3040:fee60f17ebac

jp: jp asyncio port: /!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\ This patch implements the port of jp to asyncio, so it is now correctly using the bridge asynchronously, and it can be used with bridges like `pb`. This also simplify the code, notably for things which were previously implemented with many callbacks (like pagination with RSM). During the process, some behaviours have been modified/fixed, in jp and backends, check diff for details.
author Goffi <goffi@goffi.org>
date Wed, 25 Sep 2019 08:56:41 +0200
parents ab2696e34d29
children 9d0df638c8b4
comparison
equal deleted inserted replaced
3039:a1bc34f90fa5 3040:fee60f17ebac
53 C.PI_MAIN: "XEP_0234", 53 C.PI_MAIN: "XEP_0234",
54 C.PI_HANDLER: "yes", 54 C.PI_HANDLER: "yes",
55 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), 55 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""),
56 } 56 }
57 57
58 EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash"} 58 EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"}
59 Range = namedtuple("Range", ("offset", "length")) 59 Range = namedtuple("Range", ("offset", "length"))
60 60
61 61
62 class XEP_0234(object): 62 class XEP_0234(object):
63 # TODO: assure everything is closed when file is sent or session terminate is received 63 # TODO: assure everything is closed when file is sent or session terminate is received
104 """ 104 """
105 return "{}_{}".format(session["id"], content_name) 105 return "{}_{}".format(session["id"], content_name)
106 106
107 # generic methods 107 # generic methods
108 108
109 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, 109 def buildFileElement(self, name=None, file_hash=None, hash_algo=None, size=None,
110 mime_type=None, desc=None, modified=None, transfer_range=None, path=None, 110 mime_type=None, desc=None, modified=None, transfer_range=None, path=None,
111 namespace=None, file_elt=None, **kwargs): 111 namespace=None, file_elt=None, **kwargs):
112 """Generate a <file> element with available metadata 112 """Generate a <file> element with available metadata
113 113
114 @param file_hash(unicode, None): hash of the file 114 @param file_hash(unicode, None): hash of the file
175 if kwargs: 175 if kwargs:
176 file_data = file_data.copy() 176 file_data = file_data.copy()
177 file_data.update(kwargs) 177 file_data.update(kwargs)
178 return self.buildFileElement(**file_data) 178 return self.buildFileElement(**file_data)
179 179
180 def parseFileElement( 180 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None,
181 self, 181 keep_empty_range=False,):
182 file_elt,
183 file_data=None,
184 given=False,
185 parent_elt=None,
186 keep_empty_range=False,
187 ):
188 """Parse a <file> element and file dictionary accordingly 182 """Parse a <file> element and file dictionary accordingly
189 183
190 @param file_data(dict, None): dict where the data will be set 184 @param file_data(dict, None): dict where the data will be set
191 following keys will be set (and overwritten if they already exist): 185 following keys will be set (and overwritten if they already exist):
192 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range 186 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range
193 if None, a new dict is created 187 if None, a new dict is created
194 @param given(bool): if True, prefix hash key with "given_" 188 @param given(bool): if True, prefix hash key with "given_"
195 @param parent_elt(domish.Element, None): parent of the file element 189 @param parent_elt(domish.Element, None): parent of the file element
196 if set, file_elt must not be set 190 if set, file_elt must not be set
197 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None) 191 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset
198 empty range are useful to know if a peer_jid can handle range 192 and length are None).
193 Empty range is useful to know if a peer_jid can handle range
199 @return (dict): file_data 194 @return (dict): file_data
200 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements 195 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new
196 elements
201 @raise exceptions.NotFound: there is not <file> element in parent_elt 197 @raise exceptions.NotFound: there is not <file> element in parent_elt
202 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT 198 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
203 """ 199 """
204 if parent_elt is not None: 200 if parent_elt is not None:
205 if file_elt is not None: 201 if file_elt is not None:
228 name = file_data.get("name") 224 name = file_data.get("name")
229 if name == "..": 225 if name == "..":
230 # we don't want to go to parent dir when joining to a path 226 # we don't want to go to parent dir when joining to a path
231 name = "--" 227 name = "--"
232 file_data["name"] = name 228 file_data["name"] = name
233 elif name is not None and "/" in name or "\\" in name: 229 elif name is not None and ("/" in name or "\\" in name):
234 file_data["name"] = regex.pathEscape(name) 230 file_data["name"] = regex.pathEscape(name)
235 231
236 try: 232 try:
237 file_data["mime_type"] = str( 233 file_data["mime_type"] = str(
238 next(file_elt.elements(NS_JINGLE_FT, "media-type")) 234 next(file_elt.elements(NS_JINGLE_FT, "media-type"))
333 ) 329 )
334 progress_id = yield progress_id_d 330 progress_id = yield progress_id_d
335 defer.returnValue(progress_id) 331 defer.returnValue(progress_id)
336 332
337 def _fileJingleRequest( 333 def _fileJingleRequest(
338 self, 334 self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,
339 peer_jid, 335 profile=C.PROF_KEY_NONE):
340 filepath,
341 name="",
342 file_hash="",
343 hash_algo="",
344 extra=None,
345 profile=C.PROF_KEY_NONE,
346 ):
347 client = self.host.getClient(profile) 336 client = self.host.getClient(profile)
348 return self.fileJingleRequest( 337 return self.fileJingleRequest(
349 client, 338 client,
350 jid.JID(peer_jid), 339 jid.JID(peer_jid),
351 filepath, 340 filepath,
355 extra or None, 344 extra or None,
356 ) 345 )
357 346
358 @defer.inlineCallbacks 347 @defer.inlineCallbacks
359 def fileJingleRequest( 348 def fileJingleRequest(
360 self, 349 self, client, peer_jid, filepath, name=None, file_hash=None, hash_algo=None,
361 client, 350 extra=None):
362 peer_jid,
363 filepath,
364 name=None,
365 file_hash=None,
366 hash_algo=None,
367 extra=None,
368 ):
369 """Request a file using jingle file transfer 351 """Request a file using jingle file transfer
370 352
371 @param peer_jid(jid.JID): destinee jid 353 @param peer_jid(jid.JID): destinee jid
372 @param filepath(str): absolute path where the file will be downloaded 354 @param filepath(str): absolute path where the file will be downloaded
373 @param name(unicode, None): name of the file 355 @param name(unicode, None): name of the file
566 client, session["peer_jid"], content_data, file_data, stream_object=True 548 client, session["peer_jid"], content_data, file_data, stream_object=True
567 ) 549 )
568 d.addCallback(gotConfirmation) 550 d.addCallback(gotConfirmation)
569 return d 551 return d
570 552
553 @defer.inlineCallbacks
571 def jingleHandler(self, client, action, session, content_name, desc_elt): 554 def jingleHandler(self, client, action, session, content_name, desc_elt):
572 content_data = session["contents"][content_name] 555 content_data = session["contents"][content_name]
573 application_data = content_data["application_data"] 556 application_data = content_data["application_data"]
574 if action in (self._j.A_ACCEPTED_ACK,): 557 if action in (self._j.A_ACCEPTED_ACK,):
575 pass 558 pass
577 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file")) 560 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file"))
578 try: 561 try:
579 next(file_elt.elements(NS_JINGLE_FT, "range")) 562 next(file_elt.elements(NS_JINGLE_FT, "range"))
580 except StopIteration: 563 except StopIteration:
581 # initiator doesn't manage <range>, but we do so we advertise it 564 # initiator doesn't manage <range>, but we do so we advertise it
582 #  FIXME: to be checked 565 # FIXME: to be checked
583 log.debug("adding <range> element") 566 log.debug("adding <range> element")
584 file_elt.addElement("range") 567 file_elt.addElement("range")
585 elif action == self._j.A_SESSION_ACCEPT: 568 elif action == self._j.A_SESSION_ACCEPT:
586 assert not "stream_object" in content_data 569 assert not "stream_object" in content_data
587 file_data = application_data["file_data"] 570 file_data = application_data["file_data"]
594 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file")) 577 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file"))
595 size_elt = next(file_elt.elements(NS_JINGLE_FT, "size")) 578 size_elt = next(file_elt.elements(NS_JINGLE_FT, "size"))
596 size = int(str(size_elt)) 579 size = int(str(size_elt))
597 except (StopIteration, ValueError): 580 except (StopIteration, ValueError):
598 size = None 581 size = None
599 # XXX: hash security is not critical here, so we just take the higher mandatory one 582 # XXX: hash security is not critical here, so we just take the higher
583 # mandatory one
600 hasher = file_data["hash_hasher"] = self._hash.getHasher() 584 hasher = file_data["hash_hasher"] = self._hash.getHasher()
601 content_data["stream_object"] = stream.FileStreamObject( 585 progress_id = self.getProgressId(session, content_name)
602 self.host, 586 try:
603 client, 587 content_data["stream_object"] = stream.FileStreamObject(
604 file_path, 588 self.host,
605 mode="wb", 589 client,
606 uid=self.getProgressId(session, content_name), 590 file_path,
607 size=size, 591 mode="wb",
608 data_cb=lambda data: hasher.update(data), 592 uid=progress_id,
609 ) 593 size=size,
594 data_cb=lambda data: hasher.update(data),
595 )
596 except Exception as e:
597 self.host.bridge.progressError(
598 progress_id, C.PROGRESS_ERROR_FAILED, client.profile
599 )
600 yield self._j.terminate(
601 client, self._j.REASON_FAILED_APPLICATION, session)
602 raise e
610 else: 603 else:
611 # we are sending the file 604 # we are sending the file
612 size = file_data["size"] 605 size = file_data["size"]
613 # XXX: hash security is not critical here, so we just take the higher mandatory one 606 # XXX: hash security is not critical here, so we just take the higher
607 # mandatory one
614 hasher = file_data["hash_hasher"] = self._hash.getHasher() 608 hasher = file_data["hash_hasher"] = self._hash.getHasher()
615 content_data["stream_object"] = stream.FileStreamObject( 609 content_data["stream_object"] = stream.FileStreamObject(
616 self.host, 610 self.host,
617 client, 611 client,
618 file_path, 612 file_path,
623 finished_d = content_data["finished_d"] = defer.Deferred() 617 finished_d = content_data["finished_d"] = defer.Deferred()
624 args = [client, session, content_name, content_data] 618 args = [client, session, content_name, content_data]
625 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) 619 finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args)
626 else: 620 else:
627 log.warning("FIXME: unmanaged action {}".format(action)) 621 log.warning("FIXME: unmanaged action {}".format(action))
628 return desc_elt 622 defer.returnValue(desc_elt)
629 623
630 def jingleSessionInfo(self, client, action, session, content_name, jingle_elt): 624 def jingleSessionInfo(self, client, action, session, content_name, jingle_elt):
631 """Called on session-info action 625 """Called on session-info action
632 626
633 manage checksum, and ignore <received/> element 627 manage checksum, and ignore <received/> element
677 # progress is the only way to tell to frontends that session has been declined 671 # progress is the only way to tell to frontends that session has been declined
678 progress_id = self.getProgressId(session, content_name) 672 progress_id = self.getProgressId(session, content_name)
679 self.host.bridge.progressError( 673 self.host.bridge.progressError(
680 progress_id, C.PROGRESS_ERROR_DECLINED, client.profile 674 progress_id, C.PROGRESS_ERROR_DECLINED, client.profile
681 ) 675 )
676 elif not jingle_elt.success:
677 progress_id = self.getProgressId(session, content_name)
678 first_child = jingle_elt.firstChildElement()
679 if first_child is not None:
680 reason = first_child.name
681 else:
682 reason = C.PROGRESS_ERROR_FAILED
683 self.host.bridge.progressError(
684 progress_id, reason, client.profile
685 )
682 686
683 def _sendCheckSum(self, client, session, content_name, content_data): 687 def _sendCheckSum(self, client, session, content_name, content_data):
684 """Send the session-info with the hash checksum""" 688 """Send the session-info with the hash checksum"""
685 file_data = content_data["application_data"]["file_data"] 689 file_data = content_data["application_data"]["file_data"]
686 hasher = file_data["hash_hasher"] 690 hasher = file_data["hash_hasher"]
719 self._j.delayedContentTerminate(client, session, content_name) 723 self._j.delayedContentTerminate(client, session, content_name)
720 content_data["stream_object"].close() 724 content_data["stream_object"].close()
721 return True 725 return True
722 return False 726 return False
723 hasher = file_data["hash_hasher"] 727 hasher = file_data["hash_hasher"]
724 hash_ = hasher.hexdigest().encode('utf-8') 728 hash_ = hasher.hexdigest()
725 729
726 if hash_ == given_hash: 730 if hash_ == given_hash:
727 log.info("Hash checked, file was successfully transfered: {}".format(hash_)) 731 log.info(f"Hash checked, file was successfully transfered: {hash_}")
728 progress_metadata = { 732 progress_metadata = {
729 "hash": hash_, 733 "hash": hash_,
730 "hash_algo": file_data["hash_algo"], 734 "hash_algo": file_data["hash_algo"],
731 "hash_verified": C.BOOL_TRUE, 735 "hash_verified": C.BOOL_TRUE,
732 } 736 }