Mercurial > libervia-backend
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 } |