Mercurial > libervia-backend
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", |