comparison libervia/backend/plugins/plugin_xep_0234.py @ 4231:e11b13418ba6

plugin XEP-0353, XEP-0234, jingle: WebRTC data channel signaling implementation: Implement XEP-0343: Signaling WebRTC Data Channels in Jingle. The current version of the XEP (0.3.1) has no implementation and contains some flaws. After discussing this on xsf@, Daniel (from Conversations) mentioned that they had a sprint with Larma (from Dino) to work on another version and provided me with this link: https://gist.github.com/iNPUTmice/6c56f3e948cca517c5fb129016d99e74 . I have used it for my implementation. This implementation reuses work done on Jingle A/V call (notably XEP-0176 and XEP-0167 plugins), with adaptations. When used, XEP-0234 will not handle the file itself as it normally does. This is because WebRTC has several implementations (browser for web interface, GStreamer for others), and file/data must be handled directly by the frontend. This is particularly important for web frontends, as the file is not sent from the backend but from the end-user's browser device. Among the changes, there are: - XEP-0343 implementation. - `file_send` bridge method now use serialised dict as output. - New `BaseTransportHandler.is_usable` method which get content data and returns a boolean (default to `True`) to tell if this transport can actually be used in this context (when we are initiator). Used in webRTC case to see if call data are available. - Support of `application` media type, and everything necessary to handle data channels. - Better confirmation message, with file name, size and description when available. - When file is accepted in preflight, it is specified in following `action_new` signal for actual file transfer. This way, frontend can avoid the display or 2 confirmation messages. - XEP-0166: when not specified, default `content` name is now its index number instead of a UUID. This follows the behaviour of browsers. - XEP-0353: better handling of events such as call taken by another device. - various other updates. rel 441
author Goffi <goffi@goffi.org>
date Sat, 06 Apr 2024 12:57:23 +0200
parents 5a0bddfa34ac
children 79c8a70e1813
comparison
equal deleted inserted replaced
4230:314d3c02bb67 4231:e11b13418ba6
36 from libervia.backend.core.i18n import D_, _ 36 from libervia.backend.core.i18n import D_, _
37 from libervia.backend.core.log import getLogger 37 from libervia.backend.core.log import getLogger
38 from libervia.backend.tools import xml_tools 38 from libervia.backend.tools import xml_tools
39 from libervia.backend.tools import utils 39 from libervia.backend.tools import utils
40 from libervia.backend.tools import stream 40 from libervia.backend.tools import stream
41 from libervia.backend.tools.common import date_utils 41 from libervia.backend.tools.common import data_format, date_utils
42 from libervia.backend.tools.common import regex 42 from libervia.backend.tools.common import regex
43 from libervia.backend.tools.common.utils import get_human_size
43 44
44 from .plugin_xep_0166 import BaseApplicationHandler 45 from .plugin_xep_0166 import BaseApplicationHandler
45 46
46 47
47 log = getLogger(__name__) 48 log = getLogger(__name__)
58 C.PI_MAIN: "XEP_0234", 59 C.PI_MAIN: "XEP_0234",
59 C.PI_HANDLER: "yes", 60 C.PI_HANDLER: "yes",
60 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""), 61 C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer"""),
61 } 62 }
62 63
63 EXTRA_ALLOWED = {"path", "namespace", "file_desc", "file_hash", "hash_algo"} 64 # TODO: use a Pydantic model for extra
65 EXTRA_ALLOWED = {
66 "path", "namespace", "file_desc", "file_hash", "hash_algo", "webrtc", "call_data",
67 "size", "media_type"
68 }
64 Range = namedtuple("Range", ("offset", "length")) 69 Range = namedtuple("Range", ("offset", "length"))
65 70
66 71
67 class XEP_0234(BaseApplicationHandler): 72 class XEP_0234(BaseApplicationHandler):
68 # TODO: assure everything is closed when file is sent or session terminate is received 73 # TODO: assure everything is closed when file is sent or session terminate is received
82 self._f.register(self, priority=10000) 87 self._f.register(self, priority=10000)
83 self._hash = self.host.plugins["XEP-0300"] 88 self._hash = self.host.plugins["XEP-0300"]
84 host.bridge.add_method( 89 host.bridge.add_method(
85 "file_jingle_send", 90 "file_jingle_send",
86 ".plugin", 91 ".plugin",
87 in_sign="ssssa{ss}s", 92 in_sign="ssssss",
88 out_sign="", 93 out_sign="s",
89 method=self._file_send, 94 method=self._file_send,
90 async_=True, 95 async_=True,
91 ) 96 )
92 host.bridge.add_method( 97 host.bridge.add_method(
93 "file_jingle_request", 98 "file_jingle_request",
200 except KeyError: 205 except KeyError:
201 pass 206 pass
202 return self.build_file_element(client, **file_data) 207 return self.build_file_element(client, **file_data)
203 208
204 async def parse_file_element( 209 async def parse_file_element(
205 self, client, file_elt, file_data=None, given=False, parent_elt=None, 210 self,
206 keep_empty_range=False): 211 client: SatXMPPEntity,
207 """Parse a <file> element and file dictionary accordingly 212 file_elt: domish.Element|None,
208 213 file_data: dict | None = None,
209 @param file_data(dict, None): dict where the data will be set 214 given: bool = False,
210 following keys will be set (and overwritten if they already exist): 215 parent_elt: domish.Element | None = None,
211 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range 216 keep_empty_range: bool = False
212 if None, a new dict is created 217 ) -> dict:
213 @param given(bool): if True, prefix hash key with "given_" 218 """Parse a <file> element and updates file dictionary accordingly.
214 @param parent_elt(domish.Element, None): parent of the file element 219
215 if set, file_elt must not be set 220 @param client: The SatXMPPEntity instance.
216 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset 221 @param file_elt: The file element to parse.
217 and length are None). 222 @param file_data: Dict where the data will be set. Data are overwritten if they
218 Empty range is useful to know if a peer_jid can handle range 223 exists (see @return for details).
219 @return (dict): file_data 224 If None, a new dict is created.
220 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new 225 @param given: If True, prefix hash key with "given_".
221 elements 226 @param parent_elt: Parent of the file element. If set, file_elt must not be set.
222 @raise exceptions.NotFound: there is not <file> element in parent_elt 227 @param keep_empty_range: If True, keep empty range (i.e. range when offset
223 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT 228 and length are None). Useful to know if a peer_jid can handle range.
229
230 @return: file_data which is an updated or newly created dictionary with the following keys:
231 **name** (str)
232 Name of the file. Defaults to "unnamed" if not provided, "--" if the name is
233 "..", or a sanitized version if it contains path separators.
234
235 **file_hash** (str, optional)
236 Hash of the file. Prefixed with "given_" if `given` is True.
237
238 **hash_algo** (str, optional)
239 Algorithm used for the file hash.
240
241 **size** (int, optional)
242 Size of the file in bytes.
243
244 **mime_type** (str, optional)
245 Media type of the file.
246
247 **desc** (str, optional)
248 Description of the file.
249
250 **path** (str, optional)
251 Path of the file.
252
253 **namespace** (str, optional)
254 Namespace associated with the file.
255
256 **transfer_range** (Range, optional)
257 Range of the file transfer. Present only if offset or length are specified,
258 or if `keep_empty_range` is True.
259
260 **modified** (datetime, optional)
261 Last modified date of the file.
262
263 @trigger XEP-0234_parseFileElement(file_elt, file_data): Can be used to parse new
264 elements.
265
266 @raise exceptions.NotFound: Raised if there is no <file> element in `parent_elt`
267 or if required elements are missing.
268 @raise exceptions.DataError: Raised if `file_elt` URI is not NS_JINGLE_FT or if
269 the file element is invalid.
224 """ 270 """
271
225 if parent_elt is not None: 272 if parent_elt is not None:
226 if file_elt is not None: 273 if file_elt is not None:
227 raise exceptions.InternalError( 274 raise exceptions.InternalError(
228 "file_elt must be None if parent_elt is set" 275 "file_elt must be None if parent_elt is set"
229 ) 276 )
232 except StopIteration: 279 except StopIteration:
233 raise exceptions.NotFound() 280 raise exceptions.NotFound()
234 else: 281 else:
235 if not file_elt or file_elt.uri != NS_JINGLE_FT: 282 if not file_elt or file_elt.uri != NS_JINGLE_FT:
236 raise exceptions.DataError( 283 raise exceptions.DataError(
237 "invalid <file> element: {stanza}".format(stanza=file_elt.toXml()) 284 f"invalid <file> element: {file_elt.toXml() if file_elt else 'None'}"
238 ) 285 )
239 286
240 if file_data is None: 287 if file_data is None:
241 file_data = {} 288 file_data = {}
242 289
245 file_data[name] = str(next(file_elt.elements(NS_JINGLE_FT, name))) 292 file_data[name] = str(next(file_elt.elements(NS_JINGLE_FT, name)))
246 except StopIteration: 293 except StopIteration:
247 pass 294 pass
248 295
249 name = file_data.get("name") 296 name = file_data.get("name")
250 if name == "..": 297 if name is None:
298 file_data["name"] = "unnamed"
299 elif name == "..":
251 # we don't want to go to parent dir when joining to a path 300 # we don't want to go to parent dir when joining to a path
252 name = "--" 301 file_data["name"] = "--"
253 file_data["name"] = name 302 elif "/" in name or "\\" in name:
254 elif name is not None and ("/" in name or "\\" in name):
255 file_data["name"] = regex.path_escape(name) 303 file_data["name"] = regex.path_escape(name)
256 304
257 try: 305 try:
258 file_data["mime_type"] = str( 306 file_data["mime_type"] = str(
259 next(file_elt.elements(NS_JINGLE_FT, "media-type")) 307 next(file_elt.elements(NS_JINGLE_FT, "media-type"))
300 348
301 # bridge methods 349 # bridge methods
302 350
303 def _file_send( 351 def _file_send(
304 self, 352 self,
305 peer_jid, 353 peer_jid_s: str,
306 filepath, 354 filepath: str,
307 name="", 355 name: str,
308 file_desc="", 356 file_desc: str,
309 extra=None, 357 extra_s: str,
310 profile=C.PROF_KEY_NONE, 358 profile: str,
311 ): 359 ) -> defer.Deferred[str]:
312 client = self.host.get_client(profile) 360 client = self.host.get_client(profile)
313 return defer.ensureDeferred(self.file_send( 361 extra = data_format.deserialise(extra_s)
362 d = defer.ensureDeferred(self.file_send(
314 client, 363 client,
315 jid.JID(peer_jid), 364 jid.JID(peer_jid_s),
316 filepath, 365 filepath,
317 name or None, 366 name or None,
318 file_desc or None, 367 file_desc or None,
319 extra or None, 368 extra,
320 )) 369 ))
370 d.addCallback(data_format.serialise)
371 return d
321 372
322 async def file_send( 373 async def file_send(
323 self, client, peer_jid, filepath, name, file_desc=None, extra=None 374 self,
324 ): 375 client: SatXMPPEntity,
376 peer_jid: jid.JID,
377 filepath: str,
378 name: str|None,
379 file_desc: str|None = None,
380 extra: dict|None = None
381 ) -> dict:
325 """Send a file using jingle file transfer 382 """Send a file using jingle file transfer
326 383
327 @param peer_jid(jid.JID): destinee jid 384 @param peer_jid: destinee jid
328 @param filepath(str): absolute path of the file 385 @param filepath: absolute path of the file
329 @param name(unicode, None): name of the file 386 @param name: name of the file
330 @param file_desc(unicode, None): description of the file 387 @param file_desc: description of the file
331 @return (D(unicode)): progress id 388 @return: progress id
332 """ 389 """
333 progress_id_d = defer.Deferred() 390 progress_id_d = defer.Deferred()
334 if extra is None: 391 if extra is None:
335 extra = {} 392 extra = {}
336 if file_desc is not None: 393 if file_desc is not None:
337 extra["file_desc"] = file_desc 394 extra["file_desc"] = file_desc
338 encrypted = extra.pop("encrypted", False) 395 encrypted = extra.pop("encrypted", False)
339 await self._j.initiate( 396
397 content = {
398 "app_ns": NS_JINGLE_FT,
399 "senders": self._j.ROLE_INITIATOR,
400 "app_kwargs": {
401 "filepath": filepath,
402 "name": name,
403 "extra": extra,
404 "progress_id_d": progress_id_d,
405 },
406 }
407
408 await self.host.trigger.async_point(
409 "XEP-0234_file_jingle_send",
410 client, peer_jid, content
411 )
412
413 session_id = await self._j.initiate(
340 client, 414 client,
341 peer_jid, 415 peer_jid,
342 [ 416 [content],
343 {
344 "app_ns": NS_JINGLE_FT,
345 "senders": self._j.ROLE_INITIATOR,
346 "app_kwargs": {
347 "filepath": filepath,
348 "name": name,
349 "extra": extra,
350 "progress_id_d": progress_id_d,
351 },
352 }
353 ],
354 encrypted = encrypted 417 encrypted = encrypted
355 ) 418 )
356 return await progress_id_d 419 progress_id = await progress_id_d
420 return {
421 "progress": progress_id,
422 "session_id": session_id
423 }
357 424
358 def _file_jingle_request( 425 def _file_jingle_request(
359 self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None, 426 self, peer_jid, filepath, name="", file_hash="", hash_algo="", extra=None,
360 profile=C.PROF_KEY_NONE): 427 profile=C.PROF_KEY_NONE):
361 client = self.host.get_client(profile) 428 client = self.host.get_client(profile)
389 extra["file_hash"] = file_hash 456 extra["file_hash"] = file_hash
390 extra["hash_algo"] = hash_algo 457 extra["hash_algo"] = hash_algo
391 else: 458 else:
392 if hash_algo is not None: 459 if hash_algo is not None:
393 raise ValueError(_("file_hash must be set if hash_algo is set")) 460 raise ValueError(_("file_hash must be set if hash_algo is set"))
461
462 content = {
463 "app_ns": NS_JINGLE_FT,
464 "senders": self._j.ROLE_RESPONDER,
465 "app_kwargs": {
466 "filepath": filepath,
467 "name": name,
468 "extra": extra,
469 "progress_id_d": progress_id_d,
470 },
471 }
472
394 await self._j.initiate( 473 await self._j.initiate(
395 client, 474 client,
396 peer_jid, 475 peer_jid,
397 [ 476 [content],
398 {
399 "app_ns": NS_JINGLE_FT,
400 "senders": self._j.ROLE_RESPONDER,
401 "app_kwargs": {
402 "filepath": filepath,
403 "name": name,
404 "extra": extra,
405 "progress_id_d": progress_id_d,
406 },
407 }
408 ],
409 ) 477 )
410 return await progress_id_d 478 return await progress_id_d
411 479
412 # jingle callbacks 480 # jingle callbacks
481
482 def _get_confirm_msg(
483 self,
484 client: SatXMPPEntity,
485 peer_jid: jid.JID,
486 file_data: dict
487 ) -> tuple[bool, str, str]:
488 """Get confirmation message to display to user.
489
490 This is the message to show when a file sending request is received."""
491 file_name = file_data.get('name')
492 file_size = file_data.get('size')
493
494 if file_name:
495 file_name_msg = D_('wants to send you the file "{file_name}"').format(
496 file_name=file_name
497 )
498 else:
499 file_name_msg = D_('wants to send you an unnamed file')
500
501 if file_size is not None:
502 file_size_msg = D_("which has a size of {file_size_human}").format(
503 file_size_human=get_human_size(file_size)
504 )
505 else:
506 file_size_msg = D_("which has an unknown size")
507
508 file_description = file_data.get('desc')
509 if file_description:
510 description_msg = " Description: {}.".format(file_description)
511 else:
512 description_msg = ""
513
514 if client.roster and peer_jid.userhostJID() not in client.roster:
515 is_in_roster = False
516 confirm_msg = D_(
517 "Somebody not in your contact list ({peer_jid}) {file_name_msg} {file_size_msg}.{description_msg} "
518 "Accepting this could leak your presence and possibly your IP address. Do you accept?"
519 ).format(peer_jid=peer_jid, file_name_msg=file_name_msg, file_size_msg=file_size_msg, description_msg=description_msg)
520 confirm_title = D_("File sent from an unknown contact")
521 else:
522 is_in_roster = True
523 confirm_msg = D_(
524 "{peer_jid} {file_name_msg} {file_size_msg}.{description_msg} Do you "
525 "accept?"
526 ).format(peer_jid=peer_jid, file_name_msg=file_name_msg, file_size_msg=file_size_msg, description_msg=description_msg)
527 confirm_title = D_("File Proposed")
528
529 return (is_in_roster, confirm_msg, confirm_title)
413 530
414 async def jingle_preflight( 531 async def jingle_preflight(
415 self, 532 self,
416 client: SatXMPPEntity, 533 client: SatXMPPEntity,
417 session: dict, 534 session: dict,
433 peer_jid = session["peer_jid"] 550 peer_jid = session["peer_jid"]
434 # FIXME: has been moved from XEP-0353, but it doesn't handle correctly file 551 # FIXME: has been moved from XEP-0353, but it doesn't handle correctly file
435 # transfer (metadata are not used). We must check with other clients what is 552 # transfer (metadata are not used). We must check with other clients what is
436 # actually send, and if XEP-0353 is used, and do a better integration. 553 # actually send, and if XEP-0353 is used, and do a better integration.
437 554
438 if client.roster and peer_jid.userhostJID() not in client.roster: 555 try:
439 confirm_msg = D_( 556 file_elt = next(description_elt.elements(NS_JINGLE_FT, "file"))
440 "Somebody not in your contact list ({peer_jid}) wants to do a " 557 except StopIteration:
441 '"{human_name}" session with you, this would leak your presence and ' 558 file_data = {}
442 "possibly you IP (internet localisation), do you accept?" 559 else:
443 ).format(peer_jid=peer_jid, human_name=self.human_name) 560 file_data = await self.parse_file_element(client, file_elt)
444 confirm_title = D_("File sent from an unknown contact") 561
562 is_in_roster, confirm_msg, confirm_title = self._get_confirm_msg(
563 client, peer_jid, file_data
564 )
565 if is_in_roster:
566 action_type = C.META_TYPE_CONFIRM
567 action_subtype = C.META_TYPE_FILE
568 else:
445 action_type = C.META_TYPE_NOT_IN_ROSTER_LEAK 569 action_type = C.META_TYPE_NOT_IN_ROSTER_LEAK
446 action_subtype = None 570 action_subtype = None
447 else: 571
448 confirm_msg = D_(
449 "{peer_jid} wants to send a file to you, do you accept?"
450 ).format(peer_jid=peer_jid)
451 confirm_title = D_("File Proposed")
452 action_type = C.META_TYPE_CONFIRM
453 action_subtype = C.META_TYPE_FILE
454 action_extra = { 572 action_extra = {
455 "type": action_type, 573 "type": action_type,
456 "session_id": session_id, 574 "session_id": session_id,
457 "from_jid": peer_jid.full(), 575 "from_jid": peer_jid.full(),
576 "file_data": file_data
458 } 577 }
459 if action_subtype is not None: 578 if action_subtype is not None:
460 action_extra["subtype"] = action_subtype 579 action_extra["subtype"] = action_subtype
461 accepted = await xml_tools.defer_confirm( 580 accepted = await xml_tools.defer_confirm(
462 self.host, 581 self.host,
504 ) 623 )
505 ) 624 )
506 progress_id_d.callback(self.get_progress_id(session, content_name)) 625 progress_id_d.callback(self.get_progress_id(session, content_name))
507 content_data = session["contents"][content_name] 626 content_data = session["contents"][content_name]
508 application_data = content_data["application_data"] 627 application_data = content_data["application_data"]
628 if extra.get("webrtc"):
629 transport_data = content_data["transport_data"]
630 transport_data["webrtc"] = True
509 assert "file_path" not in application_data 631 assert "file_path" not in application_data
510 application_data["file_path"] = filepath 632 application_data["file_path"] = filepath
511 file_data = application_data["file_data"] = {} 633 file_data = application_data["file_data"] = {}
512 desc_elt = self.jingle_description_elt( 634 desc_elt = self.jingle_description_elt(
513 client, session, content_name, filepath, name, extra, progress_id_d) 635 client, session, content_name, filepath, name, extra, progress_id_d)
518 if name is None: 640 if name is None:
519 name = os.path.basename(filepath) 641 name = os.path.basename(filepath)
520 file_data["date"] = utils.xmpp_date() 642 file_data["date"] = utils.xmpp_date()
521 file_data["desc"] = extra.pop("file_desc", "") 643 file_data["desc"] = extra.pop("file_desc", "")
522 file_data["name"] = name 644 file_data["name"] = name
523 mime_type = mimetypes.guess_type(name, strict=False)[0] 645 mime_type = (
646 file_data.get("media_type") or mimetypes.guess_type(name, strict=False)[0]
647 )
524 if mime_type is not None: 648 if mime_type is not None:
525 file_data["mime_type"] = mime_type 649 file_data["mime_type"] = mime_type
526 file_data["size"] = os.path.getsize(filepath) 650 size = extra.pop("size", None)
651 if size is None:
652 size = os.path.getsize(filepath)
653 file_data["size"] = size
527 if "namespace" in extra: 654 if "namespace" in extra:
528 file_data["namespace"] = extra["namespace"] 655 file_data["namespace"] = extra["namespace"]
529 if "path" in extra: 656 if "path" in extra:
530 file_data["path"] = extra["path"] 657 file_data["path"] = extra["path"]
531 self.build_file_element_from_dict( 658 self.build_file_element_from_dict(
555 client: SatXMPPEntity, 682 client: SatXMPPEntity,
556 action: str, 683 action: str,
557 session: dict, 684 session: dict,
558 content_name: str, 685 content_name: str,
559 desc_elt: domish.Element, 686 desc_elt: domish.Element,
560 ): 687 ) -> bool:
561 """This method request confirmation for a jingle session""" 688 """This method request confirmation for a jingle session"""
562 content_data = session["contents"][content_name] 689 content_data = session["contents"][content_name]
563 senders = content_data["senders"] 690 senders = content_data["senders"]
564 if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER): 691 if senders not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER):
565 log.warning("Bad sender, assuming initiator") 692 log.warning("Bad sender, assuming initiator")
611 738
612 log.warning(_("File continue is not implemented yet")) 739 log.warning(_("File continue is not implemented yet"))
613 return False 740 return False
614 741
615 async def _file_receiving_request_conf( 742 async def _file_receiving_request_conf(
616 self, client, session, content_data, content_name, file_data, file_elt 743 self,
617 ): 744 client: SatXMPPEntity,
745 session: dict,
746 content_data: dict,
747 content_name: str,
748 file_data: dict,
749 file_elt: domish.Element
750 ) -> bool:
618 """parse file_elt, and handle user permission/file opening""" 751 """parse file_elt, and handle user permission/file opening"""
619 await self.parse_file_element(client, file_elt, file_data, given=True) 752 transport_data = content_data["transport_data"]
753 webrtc = transport_data.get("webrtc", False)
754 # file may have been already accepted in preflight
755 file_accepted = session.get("file_accepted", False)
756 file_data = await self.parse_file_element(client, file_elt, file_data, given=True)
757 # FIXME: looks redundant with code done in self.parse_file_element
620 try: 758 try:
621 hash_algo, file_data["given_file_hash"] = self._hash.parse_hash_elt(file_elt) 759 hash_algo, file_data["given_file_hash"] = self._hash.parse_hash_elt(file_elt)
622 except exceptions.NotFound: 760 except exceptions.NotFound:
623 try: 761 try:
624 hash_algo = self._hash.parse_hash_used_elt(file_elt) 762 hash_algo = self._hash.parse_hash_used_elt(file_elt)
625 except exceptions.NotFound: 763 except exceptions.NotFound:
626 raise failure.Failure(exceptions.DataError) 764 raise failure.Failure(exceptions.DataError)
627 765
628 if hash_algo is not None:
629 file_data["hash_algo"] = hash_algo
630 file_data["hash_hasher"] = hasher = self._hash.get_hasher(hash_algo)
631 file_data["data_cb"] = lambda data: hasher.update(data)
632
633 try:
634 file_data["size"] = int(file_data["size"])
635 except ValueError:
636 raise failure.Failure(exceptions.DataError)
637
638 name = file_data["name"]
639 if "/" in name or "\\" in name:
640 log.warning(
641 "File name contain path characters, we replace them: {}".format(name)
642 )
643 file_data["name"] = name.replace("/", "_").replace("\\", "_")
644
645 content_data["application_data"]["file_data"] = file_data
646
647 # now we actualy request permission to user
648
649 # deferred to track end of transfer 766 # deferred to track end of transfer
650 finished_d = content_data["finished_d"] = defer.Deferred() 767 finished_d = content_data["finished_d"] = defer.Deferred()
651 confirmed = await self._f.get_dest_dir( 768
652 client, session["peer_jid"], content_data, file_data, stream_object=True 769 if webrtc:
653 ) 770 peer_jid = session["peer_jid"]
771 __, confirm_msg, confirm_title = self._get_confirm_msg(
772 client, peer_jid, file_data
773 )
774 action_extra = {
775 "webrtc": webrtc,
776 "file_accepted": file_accepted,
777 "type": C.META_TYPE_FILE,
778 "session_id": session["id"],
779 "from_jid": peer_jid.full(),
780 "file_data": file_data,
781 "progress_id": file_data["progress_id"],
782 }
783 # we need a confirm dialog here and not a file dialog, as the file handling is
784 # managed by the frontends with webRTC.
785 confirmed = await xml_tools.defer_confirm(
786 self.host,
787 confirm_msg,
788 confirm_title,
789 profile=client.profile,
790 action_extra=action_extra
791 )
792 else:
793
794 if hash_algo is not None:
795 file_data["hash_algo"] = hash_algo
796 file_data["hash_hasher"] = hasher = self._hash.get_hasher(hash_algo)
797 file_data["data_cb"] = lambda data: hasher.update(data)
798
799 try:
800 file_data["size"] = int(file_data["size"])
801 except ValueError:
802 raise failure.Failure(exceptions.DataError)
803
804 content_data["application_data"]["file_data"] = file_data
805
806 # now we actualy request permission to user
807
808 confirmed = await self._f.get_dest_dir(
809 client, session["peer_jid"], content_data, file_data, stream_object=True
810 )
811
654 if confirmed: 812 if confirmed:
655 await self.host.trigger.async_point( 813 await self.host.trigger.async_point(
656 "XEP-0234_file_receiving_request_conf", 814 "XEP-0234_file_receiving_request_conf",
657 client, session, content_data, file_elt 815 client, session, content_data, file_elt
658 ) 816 )
659 args = [client, session, content_name, content_data] 817 args = [client, session, content_name, content_data]
660 finished_d.addCallbacks( 818 finished_d.addCallbacks(
661 self._finished_cb, self._finished_eb, args, None, args 819 self._finished_cb, self._finished_eb, args, None, args
662 ) 820 )
821
663 return confirmed 822 return confirmed
664 823
665 async def jingle_handler(self, client, action, session, content_name, desc_elt): 824 async def jingle_handler(self, client, action, session, content_name, desc_elt):
666 content_data = session["contents"][content_name] 825 content_data = session["contents"][content_name]
667 application_data = content_data["application_data"] 826 application_data = content_data["application_data"]
676 # FIXME: to be checked 835 # FIXME: to be checked
677 log.debug("adding <range> element") 836 log.debug("adding <range> element")
678 file_elt.addElement("range") 837 file_elt.addElement("range")
679 elif action == self._j.A_SESSION_ACCEPT: 838 elif action == self._j.A_SESSION_ACCEPT:
680 assert not "stream_object" in content_data 839 assert not "stream_object" in content_data
681 file_data = application_data["file_data"] 840 transport_data = content_data["transport_data"]
682 file_path = application_data["file_path"] 841 if transport_data.get("webrtc"):
683 senders = content_data["senders"] 842 # WebRTC case is special, the file is transfered by the frontend
684 if senders != session["role"]: 843 # implementation directly, there is nothing done in backend.
685 # we are receiving the file 844 log.debug(
686 try: 845 "We're using WebRTC datachannel, the frontend handles it from now on."
687 # did the responder specified the size of the file? 846 )
688 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file")) 847 else:
689 size_elt = next(file_elt.elements(NS_JINGLE_FT, "size")) 848 file_data = application_data["file_data"]
690 size = int(str(size_elt)) 849 file_path = application_data["file_path"]
691 except (StopIteration, ValueError): 850 senders = content_data["senders"]
692 size = None 851 if senders != session["role"]:
693 # XXX: hash security is not critical here, so we just take the higher 852 # we are receiving the file
694 # mandatory one 853 try:
695 hasher = file_data["hash_hasher"] = self._hash.get_hasher() 854 # did the responder specified the size of the file?
696 progress_id = self.get_progress_id(session, content_name) 855 file_elt = next(desc_elt.elements(NS_JINGLE_FT, "file"))
697 try: 856 size_elt = next(file_elt.elements(NS_JINGLE_FT, "size"))
857 size = int(str(size_elt))
858 except (StopIteration, ValueError):
859 size = None
860 # XXX: hash security is not critical here, so we just take the higher
861 # mandatory one
862 hasher = file_data["hash_hasher"] = self._hash.get_hasher()
863 progress_id = self.get_progress_id(session, content_name)
864 try:
865 content_data["stream_object"] = stream.FileStreamObject(
866 self.host,
867 client,
868 file_path,
869 mode="wb",
870 uid=progress_id,
871 size=size,
872 data_cb=lambda data: hasher.update(data),
873 )
874 except Exception as e:
875 self.host.bridge.progress_error(
876 progress_id, C.PROGRESS_ERROR_FAILED, client.profile
877 )
878 await self._j.terminate(
879 client, self._j.REASON_FAILED_APPLICATION, session)
880 raise e
881 else:
882 # we are sending the file
883 size = file_data["size"]
884 # XXX: hash security is not critical here, so we just take the higher
885 # mandatory one
886 hasher = file_data["hash_hasher"] = self._hash.get_hasher()
698 content_data["stream_object"] = stream.FileStreamObject( 887 content_data["stream_object"] = stream.FileStreamObject(
699 self.host, 888 self.host,
700 client, 889 client,
701 file_path, 890 file_path,
702 mode="wb", 891 uid=self.get_progress_id(session, content_name),
703 uid=progress_id,
704 size=size, 892 size=size,
705 data_cb=lambda data: hasher.update(data), 893 data_cb=lambda data: hasher.update(data),
706 ) 894 )
707 except Exception as e: 895
708 self.host.bridge.progress_error(
709 progress_id, C.PROGRESS_ERROR_FAILED, client.profile
710 )
711 await self._j.terminate(
712 client, self._j.REASON_FAILED_APPLICATION, session)
713 raise e
714 else:
715 # we are sending the file
716 size = file_data["size"]
717 # XXX: hash security is not critical here, so we just take the higher
718 # mandatory one
719 hasher = file_data["hash_hasher"] = self._hash.get_hasher()
720 content_data["stream_object"] = stream.FileStreamObject(
721 self.host,
722 client,
723 file_path,
724 uid=self.get_progress_id(session, content_name),
725 size=size,
726 data_cb=lambda data: hasher.update(data),
727 )
728 finished_d = content_data["finished_d"] = defer.Deferred() 896 finished_d = content_data["finished_d"] = defer.Deferred()
729 args = [client, session, content_name, content_data] 897 args = [client, session, content_name, content_data]
730 finished_d.addCallbacks(self._finished_cb, self._finished_eb, args, None, args) 898 finished_d.addCallbacks(self._finished_cb, self._finished_eb, args, None, args)
731 await self.host.trigger.async_point( 899 await self.host.trigger.async_point(
732 "XEP-0234_jingle_handler", 900 "XEP-0234_jingle_handler",