# HG changeset patch # User Goffi # Date 1576841284 -3600 # Node ID c3cb18236bdf0f1a26ba878f856048a84f9cff2e # Parent d909473a76ccda488b8b76c9d2071b306f3bdfbd 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 diff -r d909473a76cc -r c3cb18236bdf doc/jp/file.rst --- 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 ===== diff -r d909473a76cc -r c3cb18236bdf sat_frontends/jp/cmd_file.py --- 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__(