changeset 3094:c3cb18236bdf

jp (file): new `get` command + encryption with upload: - new file/get command let download a file from URL. It handles `aesgcm:` scheme by thanks to backend decryption on the fly. - new `--encrypt` option for upload. When used, the file will be encrypted using AES-GCM algorithm, and the `aesgcm:` URL will be returned - for both commands, the XMLUI note is displayed in case of error
author Goffi <goffi@goffi.org>
date Fri, 20 Dec 2019 12:28:04 +0100
parents d909473a76cc
children fb49587f55c2
files doc/jp/file.rst sat_frontends/jp/cmd_file.py
diffstat 2 files changed, 119 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/doc/jp/file.rst	Fri Dec 20 12:28:04 2019 +0100
+++ b/doc/jp/file.rst	Fri Dec 20 12:28:04 2019 +0100
@@ -118,6 +118,23 @@
 
   $ jp file receive --multiple --path ~/Downloads/Louise louise@example.org
 
+get
+===
+
+Download a file from an URI. This commands handle URI scheme common with XMPP, so in
+addition to ``http`` and ``https``, you can use it with ``aesgcm`` scheme (encrypted files
+with key in URL, this is notably used with OMEMO encryption).
+
+As usual, you can use ``-P, --progress`` to see a progress bar.
+
+example
+-------
+
+Download an encrypted file with a progress bar, and save it to current working directory
+with the same name as in the URL (``some_image.jpg``). The URL fragment part (after ``#``)
+is used for decryption, so be sure to not leak the URL when you manipulate one::
+
+  $ jp file get -P "aesgcm://upload.example.org/wvgSUlURU_UPspAv/some_image.jpg#7d8509c43479591f8d8492f84369875ca983db58f43225c40229eb06d05b2037c841b2346c9642a88ba4a91aa96a0e8f"
 
 upload
 ======
@@ -132,6 +149,11 @@
 
 As usual, you can use ``-P, --progress`` to see a progress bar.
 
+You can encrypt the file using ``AES GCM`` with the ``-e, --encrypt`` argument. You will
+then get an ``aesgcm://`` link instead of the usual ``https``, this link contains the
+decryption key (in the fragment part) so be sure to not leak it and to transmit it only
+over encrypted communication channels.
+
 .. _XEP-0363 (HTTP File Upload): XEP-0363: HTTP File Upload
 
 example
@@ -141,6 +163,10 @@
 
   $ jp file upload -P ~/Documents/something_interesting.odt
 
+Encrypt and upload a document to server::
+
+  $ jp file upload -P -e ~/Documents/something_secret.odt
+
 share
 =====
 
--- a/sat_frontends/jp/cmd_file.py	Fri Dec 20 12:28:04 2019 +0100
+++ b/sat_frontends/jp/cmd_file.py	Fri Dec 20 12:28:04 2019 +0100
@@ -19,6 +19,7 @@
 
 
 from . import base
+from . import xmlui_manager
 import sys
 import os
 import os.path
@@ -29,6 +30,8 @@
 from sat_frontends.jp import common
 from sat_frontends.tools import jid
 from sat.tools.common.ansi import ANSI as A
+from urllib.parse import urlparse
+from pathlib import Path
 import tempfile
 import xml.etree.ElementTree as ET  # FIXME: used temporarily to manage XMLUI
 import json
@@ -254,7 +257,8 @@
             path = os.path.abspath(self.filename)
 
         if os.path.exists(path) and not self.args.force:
-            message = _(f"File {path} already exists! Do you want to overwrite?")
+            message = _("File {path} already exists! Do you want to overwrite?").format(
+                path = path)
             await self.host.confirmOrQuit(message, _("file request cancelled"))
 
         self.full_dest_jid = await self.host.get_full_jid(self.args.jid)
@@ -413,6 +417,77 @@
         await self.start_answering()
 
 
+class Get(base.CommandBase):
+
+    def __init__(self, host):
+        super(Get, self).__init__(
+            host, "get", use_progress=True, use_verbose=True,
+            help=_("download a file from URI")
+        )
+
+    def add_parser_options(self):
+        self.parser.add_argument(
+            '-o', '--dest_file', type=str, default='',
+            help=_("destination file (DEFAULT: filename from URL)")
+            )
+        self.parser.add_argument(
+            "-f",
+            "--force",
+            action="store_true",
+            help=_("overwrite existing file without confirmation"),
+        )
+        self.parser.add_argument("uri", type=str, help=_("URI of the file to retrieve"))
+
+    async def onProgressStarted(self, metadata):
+        self.disp(_("File download started"), 2)
+
+    async def onProgressFinished(self, metadata):
+        self.disp(_("File downloaded successfully"), 2)
+
+    async def onProgressError(self, error_msg):
+        self.disp(_("Error while downloading file: {}").format(error_msg), error=True)
+
+    async def gotId(self, data):
+        """Called when a progress id has been received"""
+        try:
+            await self.set_progress_id(data["progress"])
+        except KeyError:
+            if 'xmlui' in data:
+                ui = xmlui_manager.create(self.host, data['xmlui'])
+                await ui.show()
+            else:
+                self.disp(_("Can't download file"), error=True)
+            self.host.quit(C.EXIT_ERROR)
+
+    async def start(self):
+        uri = self.args.uri
+        dest_file = self.args.dest_file
+        if not dest_file:
+            parsed_uri = urlparse(uri)
+            dest_file = Path(parsed_uri.path).name.strip() or "downloaded_file"
+
+        dest_file = Path(dest_file).expanduser().resolve()
+        if dest_file.exists() and not self.args.force:
+            message = _("File {path} already exists! Do you want to overwrite?").format(
+                path = dest_file)
+            await self.host.confirmOrQuit(message, _("file download cancelled"))
+
+        options = {}
+
+        try:
+            download_data = await self.host.bridge.fileDownload(
+                uri,
+                str(dest_file),
+                data_format.serialise(options),
+                self.profile,
+            )
+        except Exception as e:
+            self.disp(f"error while trying to download a file: {e}", error=True)
+            self.host.quit(C.EXIT_BRIDGE_ERRBACK)
+        else:
+            await self.gotId(download_data)
+
+
 class Upload(base.CommandBase):
     def __init__(self, host):
         super(Upload, self).__init__(
@@ -420,6 +495,12 @@
         )
 
     def add_parser_options(self):
+        self.parser.add_argument(
+            "-e",
+            "--encrypt",
+            action="store_true",
+            help=_("encrypt file using AES-GCM"),
+        )
         self.parser.add_argument("file", type=str, help=_("file to upload"))
         self.parser.add_argument(
             "jid",
@@ -429,7 +510,7 @@
         self.parser.add_argument(
             "--ignore-tls-errors",
             action="store_true",
-            help=_("ignore invalide TLS certificate"),
+            help=_(r"ignore invalide TLS certificate (/!\ Dangerous /!\)"),
         )
 
     async def onProgressStarted(self, metadata):
@@ -443,7 +524,7 @@
             self.disp("download URL not found in metadata")
         else:
             self.disp(_("URL to retrieve the file:"), 1)
-            # XXX: url is display alone on a line to make parsing easier
+            # XXX: url is displayed alone on a line to make parsing easier
             self.disp(url)
 
     async def onProgressError(self, error_msg):
@@ -458,8 +539,11 @@
         try:
             await self.set_progress_id(data["progress"])
         except KeyError:
-            # TODO: if 'xmlui' key is present, manage xmlui message display
-            self.disp(_("Can't upload file"), error=True)
+            if 'xmlui' in data:
+                ui = xmlui_manager.create(self.host, data['xmlui'])
+                await ui.show()
+            else:
+                self.disp(_("Can't upload file"), error=True)
             self.host.quit(C.EXIT_ERROR)
 
     async def start(self):
@@ -479,6 +563,8 @@
         options = {}
         if self.args.ignore_tls_errors:
             options["ignore_tls_errors"] = True
+        if self.args.encrypt:
+            options["encryption"] = C.ENC_AES_GCM
 
         path = os.path.abspath(file_)
         try:
@@ -490,7 +576,7 @@
                 self.profile,
             )
         except Exception as e:
-            self.disp(f"can't while trying to upload a file: {e}", error=True)
+            self.disp(f"error while trying to upload a file: {e}", error=True)
             self.host.quit(C.EXIT_BRIDGE_ERRBACK)
         else:
             await self.gotId(upload_data, file_)
@@ -727,7 +813,7 @@
 
 
 class File(base.CommandBase):
-    subcommands = (Send, Request, Receive, Upload, Share)
+    subcommands = (Send, Request, Receive, Get, Upload, Share)
 
     def __init__(self, host):
         super(File, self).__init__(