about summary refs log tree commit diff stats
path: root/src/command
diff options
context:
space:
mode:
authorMichael Vetter <jubalh@iodoru.org>2020-12-09 08:09:09 +0100
committerGitHub <noreply@github.com>2020-12-09 08:09:09 +0100
commit7a319df6c80c472adcf416c8737e7b156854352b (patch)
tree0bf423e2541b4b5f21a747d4cd88889feadaccab /src/command
parentf023d56b02eec67ea357a85d7f8ec325f87622e3 (diff)
parentd7848e38bc2d916f88e889352f337a5c617fb26d (diff)
downloadprofani-tty-7a319df6c80c472adcf416c8737e7b156854352b.tar.gz
Merge pull request #1375 from wstrm/feature/omemo-sendfile
Send and retrieve encrypted files when using OMEMO
Diffstat (limited to 'src/command')
-rw-r--r--src/command/cmd_ac.c13
-rw-r--r--src/command/cmd_defs.c134
-rw-r--r--src/command/cmd_funcs.c481
-rw-r--r--src/command/cmd_funcs.h5
4 files changed, 335 insertions, 298 deletions
diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c
index 3aeedc60..f7414bad 100644
--- a/src/command/cmd_ac.c
+++ b/src/command/cmd_ac.c
@@ -193,7 +193,6 @@ static Autocomplete otr_sendfile_ac;
 static Autocomplete omemo_ac;
 static Autocomplete omemo_log_ac;
 static Autocomplete omemo_policy_ac;
-static Autocomplete omemo_sendfile_ac;
 #endif
 static Autocomplete connect_property_ac;
 static Autocomplete tls_property_ac;
@@ -683,7 +682,6 @@ cmd_ac_init(void)
     autocomplete_add(omemo_ac, "clear_device_list");
     autocomplete_add(omemo_ac, "policy");
     autocomplete_add(omemo_ac, "char");
-    autocomplete_add(omemo_ac, "sendfile");
 
     omemo_log_ac = autocomplete_new();
     autocomplete_add(omemo_log_ac, "on");
@@ -694,10 +692,6 @@ cmd_ac_init(void)
     autocomplete_add(omemo_policy_ac, "manual");
     autocomplete_add(omemo_policy_ac, "automatic");
     autocomplete_add(omemo_policy_ac, "always");
-
-    omemo_sendfile_ac = autocomplete_new();
-    autocomplete_add(omemo_sendfile_ac, "on");
-    autocomplete_add(omemo_sendfile_ac, "off");
 #endif
 
     connect_property_ac = autocomplete_new();
@@ -1292,7 +1286,6 @@ cmd_ac_reset(ProfWin* window)
     autocomplete_reset(omemo_ac);
     autocomplete_reset(omemo_log_ac);
     autocomplete_reset(omemo_policy_ac);
-    autocomplete_reset(omemo_sendfile_ac);
 #endif
     autocomplete_reset(connect_property_ac);
     autocomplete_reset(tls_property_ac);
@@ -1450,7 +1443,6 @@ cmd_ac_uninit(void)
     autocomplete_free(omemo_ac);
     autocomplete_free(omemo_log_ac);
     autocomplete_free(omemo_policy_ac);
-    autocomplete_free(omemo_sendfile_ac);
 #endif
     autocomplete_free(connect_property_ac);
     autocomplete_free(tls_property_ac);
@@ -2510,11 +2502,6 @@ _omemo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
         return found;
     }
 
-    found = autocomplete_param_with_ac(input, "/omemo sendfile", omemo_sendfile_ac, TRUE, previous);
-    if (found) {
-        return found;
-    }
-
     jabber_conn_status_t conn_status = connection_get_status();
 
     if (conn_status == JABBER_CONNECTED) {
diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c
index 51772a27..5058a573 100644
--- a/src/command/cmd_defs.c
+++ b/src/command/cmd_defs.c
@@ -2274,55 +2274,52 @@ static struct cmd_t command_defs[] = {
     },
 
     { "/omemo",
-      parse_args, 1, 3, NULL,
-      CMD_SUBFUNCS(
-              { "gen", cmd_omemo_gen },
-              { "log", cmd_omemo_log },
-              { "start", cmd_omemo_start },
-              { "end", cmd_omemo_end },
-              { "trust", cmd_omemo_trust },
-              { "untrust", cmd_omemo_untrust },
-              { "fingerprint", cmd_omemo_fingerprint },
-              { "char", cmd_omemo_char },
-              { "policy", cmd_omemo_policy },
-              { "clear_device_list", cmd_omemo_clear_device_list },
-              { "sendfile", cmd_omemo_sendfile })
-      CMD_NOMAINFUNC
-      CMD_TAGS(
-              CMD_TAG_CHAT,
-              CMD_TAG_UI)
-      CMD_SYN(
-              "/omemo gen",
-              "/omemo log on|off|redact",
-              "/omemo start [<contact>]",
-              "/omemo trust [<contact>] <fingerprint>",
-              "/omemo end",
-              "/omemo fingerprint [<contact>]",
-              "/omemo char <char>",
-              "/omemo policy manual|automatic|always",
-              "/omemo sendfile on|off",
-              "/omemo clear_device_list")
-      CMD_DESC(
-              "OMEMO commands to manage keys, and perform encryption during chat sessions.")
-      CMD_ARGS(
-              { "gen", "Generate OMEMO crytographic materials for current account." },
-              { "start [<contact>]", "Start an OMEMO session with contact, or current recipient if omitted." },
-              { "end", "End the current OMEMO session." },
-              { "log on|off", "Enable or disable plaintext logging of OMEMO encrypted messages." },
-              { "log redact", "Log OMEMO encrypted messages, but replace the contents with [redacted]. This is the default." },
-              { "fingerprint [<contact>]", "Show contact fingerprints, or current recipient if omitted." },
-              { "char <char>", "Set the character to be displayed next to OMEMO encrypted messages." },
-              { "policy manual", "Set the global OMEMO policy to manual, OMEMO sessions must be started manually." },
-              { "policy automatic", "Set the global OMEMO policy to opportunistic, an OMEMO session will be attempted upon starting a conversation." },
-              { "policy always", "Set the global OMEMO policy to always, an error will be displayed if an OMEMO session cannot be initiated upon starting a conversation." },
-              { "sendfile on|off", "Allow /sendfile to send unencrypted files while in an OMEMO session." },
-              { "clear_device_list", "Clear your own device list on server side. Each client will reannounce itself when connected back." })
-      CMD_EXAMPLES(
-              "/omemo gen",
-              "/omemo start odin@valhalla.edda",
-              "/omemo trust c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
-              "/omemo untrust loki@valhalla.edda c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
-              "/omemo char *")
+        parse_args, 1, 3, NULL,
+        CMD_SUBFUNCS(
+            { "gen", cmd_omemo_gen },
+            { "log", cmd_omemo_log },
+            { "start", cmd_omemo_start },
+            { "end", cmd_omemo_end },
+            { "trust", cmd_omemo_trust },
+            { "untrust", cmd_omemo_untrust },
+            { "fingerprint", cmd_omemo_fingerprint },
+            { "char", cmd_omemo_char },
+            { "policy", cmd_omemo_policy },
+            { "clear_device_list", cmd_omemo_clear_device_list })
+        CMD_NOMAINFUNC
+        CMD_TAGS(
+            CMD_TAG_CHAT,
+            CMD_TAG_UI)
+        CMD_SYN(
+            "/omemo gen",
+            "/omemo log on|off|redact",
+            "/omemo start [<contact>]",
+            "/omemo trust [<contact>] <fingerprint>",
+            "/omemo end",
+            "/omemo fingerprint [<contact>]",
+            "/omemo char <char>",
+            "/omemo policy manual|automatic|always",
+            "/omemo clear_device_list")
+        CMD_DESC(
+            "OMEMO commands to manage keys, and perform encryption during chat sessions.")
+        CMD_ARGS(
+            { "gen",                     "Generate OMEMO crytographic materials for current account." },
+            { "start [<contact>]",       "Start an OMEMO session with contact, or current recipient if omitted." },
+            { "end",                     "End the current OMEMO session." },
+            { "log on|off",              "Enable or disable plaintext logging of OMEMO encrypted messages." },
+            { "log redact",              "Log OMEMO encrypted messages, but replace the contents with [redacted]. This is the default." },
+            { "fingerprint [<contact>]", "Show contact fingerprints, or current recipient if omitted." },
+            { "char <char>",             "Set the character to be displayed next to OMEMO encrypted messages." },
+            { "policy manual",           "Set the global OMEMO policy to manual, OMEMO sessions must be started manually." },
+            { "policy automatic",        "Set the global OMEMO policy to opportunistic, an OMEMO session will be attempted upon starting a conversation." },
+            { "policy always",           "Set the global OMEMO policy to always, an error will be displayed if an OMEMO session cannot be initiated upon starting a conversation." },
+            { "clear_device_list",       "Clear your own device list on server side. Each client will reannounce itself when connected back."})
+        CMD_EXAMPLES(
+            "/omemo gen",
+            "/omemo start odin@valhalla.edda",
+            "/omemo trust c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
+            "/omemo untrust loki@valhalla.edda c4f9c875-144d7a3b-0c4a05b6-ca3be51a-a037f329-0bd3ae62-07f99719-55559d2a",
+            "/omemo char *")
     },
 
     { "/save",
@@ -2377,7 +2374,7 @@ static struct cmd_t command_defs[] = {
               "Settings for consistent color generation for nicks (XEP-0392). Including corrections for Color Vision Deficiencies. "
               "Your terminal needs to support 256 colors.")
       CMD_ARGS(
-              { "on|off|redgreen|blue", "Enable or disable nick colorization for MUC nicks. 'redgreen' is for people with red/green blindess and 'blue' for people with blue blindness." },
+              { "on|off|redgreen|blue", "Enable or disable nick colorization for MUC nicks. 'redgreen' is for people with red/green blindness and 'blue' for people with blue blindness." },
               { "own on|off", "Enable color generation for own nick. If disabled the color from the color from the theme ('me') will get used." })
       CMD_EXAMPLES(
               "/color off",
@@ -2496,26 +2493,35 @@ static struct cmd_t command_defs[] = {
 
     { "/executable",
       parse_args, 2, 4, &cons_executable_setting,
-      CMD_NOSUBFUNCS
-      CMD_MAINFUNC(cmd_executable)
+      CMD_SUBFUNCS(
+              { "avatar",  cmd_executable_avatar },
+              { "urlopen", cmd_executable_urlopen },
+              { "urlsave", cmd_executable_urlsave })
+      CMD_NOMAINFUNC
       CMD_TAGS(
               CMD_TAG_DISCOVERY)
       CMD_SYN(
               "/executable avatar <cmd>",
-              "/executable urlopen (<fileType>|DEF <require_save> <cmd>",
-              "/executable urlsave (<protocol>|DEF) <cmd>")
-      CMD_DESC(
-              "Configure executable that should be called upon a certain command."
-              "Default is xdg-open.")
-      CMD_ARGS(
-              { "avatar", "Set executable that is run in /avatar open. Use your favourite image viewer." },
-              { "urlopen", "Set executable that is run in /url open for a given file type. It may be your favorite browser or a specific viewer. Use DEF to set default command for undefined file type." },
-              { "urlsave", "Set executable that is run in /url save for a given protocol. Use your favourite downloader. Use DEF to set default command for undefined protocol." })
+              "/executable urlopen set <cmdtemplate>",
+              "/executable urlopen default",
+              "/executable urlsave set <cmdtemplate>",
+              "/executable urlsave default")
+      CMD_DESC(
+              "Configure executable that should be called upon a certain command.")
+      CMD_ARGS(
+              { "avatar", "Set executable that is run by /avatar open. Use your favorite image viewer." },
+              { "urlopen set", "Set executable that is run by /url open. It may be your favorite browser or a specific viewer." },
+              { "urlopen default", "Restore to default settings." },
+              { "urlsave set", "Set executable that is run by /url save. It may be your favorite downloader.'" },
+              { "urlsave default", "Use the built-in download method for saving." })
       CMD_EXAMPLES(
               "/executable avatar xdg-open",
-              "/executable urlopen DEF false \"xdg-open %u\"",
-              "/executable urlopen html false \"firefox %u\"",
-              "/executable urlsave aesgcm \"omut -d -o %p %u\"")
+              "/executable urlopen set \"xdg-open %u\"",
+              "/executable urlopen set \"firefox %u\"",
+              "/executable urlopen default",
+              "/executable urlsave set \"wget %u -O %p\"",
+              "/executable urlsave set \"curl %u -o %p\"",
+              "/executable urlsave default")
     },
 
     { "/url",
diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c
index 0837f630..fd9d2ffd 100644
--- a/src/command/cmd_funcs.c
+++ b/src/command/cmd_funcs.c
@@ -4,6 +4,7 @@
  *
  * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
  * Copyright (C) 2019 Michael Vetter <jubalh@iodoru.org>
+ * Copyright (C) 2020 William Wennerström <william@wstrm.dev>
  *
  * This file is part of Profanity.
  *
@@ -59,6 +60,7 @@
 #include "command/cmd_funcs.h"
 #include "command/cmd_defs.h"
 #include "command/cmd_ac.h"
+#include "config/files.h"
 #include "config/accounts.h"
 #include "config/account.h"
 #include "config/preferences.h"
@@ -67,6 +69,7 @@
 #include "config/scripts.h"
 #include "event/client_events.h"
 #include "tools/http_upload.h"
+#include "tools/http_download.h"
 #include "tools/autocomplete.h"
 #include "tools/parser.h"
 #include "tools/bookmark_ignore.h"
@@ -94,6 +97,7 @@
 #ifdef HAVE_OMEMO
 #include "omemo/omemo.h"
 #include "xmpp/omemo.h"
+#include "tools/aesgcm_download.h"
 #endif
 
 #ifdef HAVE_GTK
@@ -1086,7 +1090,7 @@ _writecsv(int fd, const char* const str)
     size_t len = strlen(str);
     char* s = malloc(2 * len * sizeof(char));
     char* c = s;
-    for (int i =0; i < strlen(str); i++) {
+    for (int i = 0; i < strlen(str); i++) {
         if (str[i] != '"')
             *c++ = str[i];
         else {
@@ -4805,11 +4809,54 @@ cmd_disco(ProfWin* window, const char* const command, gchar** args)
     return TRUE;
 }
 
+// TODO: Move this into its own tools such as HTTPUpload or AESGCMDownload.
+#ifdef HAVE_OMEMO
+char*
+_add_omemo_stream(int* fd, FILE** fh, char** err)
+{
+    // Create temporary file for writing ciphertext.
+    int tmpfd;
+    char* tmpname = NULL;
+    if ((tmpfd = g_file_open_tmp("profanity.XXXXXX", &tmpname, NULL)) == -1) {
+        *err = "Unable to create temporary file for encrypted transfer.";
+        return NULL;
+    }
+    FILE* tmpfh = fdopen(tmpfd, "wb");
+
+    // The temporary ciphertext file should be removed after it has
+    // been closed.
+    remove(tmpname);
+    free(tmpname);
+
+    int crypt_res;
+    char* fragment;
+    fragment = omemo_encrypt_file(*fh, tmpfh, file_size(*fd), &crypt_res);
+    if (crypt_res != 0) {
+        fclose(tmpfh);
+        return NULL;
+    }
+
+    // Force flush as the upload will read from the same stream.
+    fflush(tmpfh);
+    rewind(tmpfh);
+
+    fclose(*fh); // Also closes descriptor.
+
+    // Switch original stream with temporary ciphertext stream.
+    *fd = tmpfd;
+    *fh = tmpfh;
+
+    return fragment;
+}
+#endif
+
 gboolean
 cmd_sendfile(ProfWin* window, const char* const command, gchar** args)
 {
     jabber_conn_status_t conn_status = connection_get_status();
     char* filename = args[0];
+    char* alt_scheme = NULL;
+    char* alt_fragment = NULL;
 
     // expand ~ to $HOME
     if (filename[0] == '~' && filename[1] == '/') {
@@ -4820,80 +4867,101 @@ cmd_sendfile(ProfWin* window, const char* const command, gchar** args)
         filename = strdup(filename);
     }
 
+    if (access(filename, R_OK) != 0) {
+        cons_show_error("Uploading '%s' failed: File not found!", filename);
+        goto out;
+    }
+
+    if (!is_regular_file(filename)) {
+        cons_show_error("Uploading '%s' failed: Not a file!", filename);
+        goto out;
+    }
+
     if (conn_status != JABBER_CONNECTED) {
         cons_show("You are not currently connected.");
-        free(filename);
-        return TRUE;
+        goto out;
     }
 
     if (window->type != WIN_CHAT && window->type != WIN_PRIVATE && window->type != WIN_MUC) {
         cons_show_error("Unsupported window for file transmission.");
-        free(filename);
-        return TRUE;
+        goto out;
+    }
+
+    int fd;
+    if ((fd = open(filename, O_RDONLY)) == -1) {
+        cons_show_error("Unable to open file descriptor for '%s'.", filename);
+        goto out;
     }
 
+    FILE* fh = fdopen(fd, "rb");
+
     switch (window->type) {
     case WIN_MUC:
-    {
-        ProfMucWin* mucwin = (ProfMucWin*)window;
-        assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
-
-        // only omemo, no pgp/otr available in MUCs
-        if (mucwin->is_omemo && !prefs_get_boolean(PREF_OMEMO_SENDFILE)) {
-            cons_show_error("Uploading unencrypted files disabled. See /omemo sendfile, /otr sendfile, /pgp sendfile.");
-            win_println(window, THEME_ERROR, "-", "Sending encrypted files via http_upload is not possible yet.");
-            free(filename);
-            return TRUE;
-        }
-        break;
-    }
     case WIN_CHAT:
     {
         ProfChatWin* chatwin = (ProfChatWin*)window;
-        assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
 
-        if ((chatwin->is_omemo && !prefs_get_boolean(PREF_OMEMO_SENDFILE))
-            || (chatwin->pgp_send && !prefs_get_boolean(PREF_PGP_SENDFILE))
-            || (chatwin->is_otr && !prefs_get_boolean(PREF_OTR_SENDFILE))) {
-            cons_show_error("Uploading unencrypted files disabled. See /omemo sendfile, /otr sendfile, /pgp sendfile.");
-            win_println(window, THEME_ERROR, "-", "Sending encrypted files via http_upload is not possible yet.");
-            free(filename);
-            return TRUE;
+#ifdef HAVE_OMEMO
+        if (chatwin->is_omemo) {
+            char* err = NULL;
+            alt_scheme = OMEMO_AESGCM_URL_SCHEME;
+            alt_fragment = _add_omemo_stream(&fd, &fh, &err);
+            if (err != NULL) {
+                cons_show_error(err);
+                win_println(window, THEME_ERROR, "-", err);
+                goto out;
+            }
+            break;
+        }
+#endif
+
+        if (window->type == WIN_CHAT) {
+            assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
+            if ((chatwin->pgp_send && !prefs_get_boolean(PREF_PGP_SENDFILE))
+                || (chatwin->is_otr && !prefs_get_boolean(PREF_OTR_SENDFILE))) {
+                cons_show_error("Uploading unencrypted files disabled. See /otr sendfile or /pgp sendfile.");
+                win_println(window, THEME_ERROR, "-", "Sending encrypted files via http_upload is not possible yet.");
+                goto out;
+            }
         }
         break;
     }
-    case WIN_PRIVATE:
-    {
-        //we don't support encryption in private muc windows
-        break;
-    }
+    case WIN_PRIVATE: // We don't support encryption in private MUC windows.
     default:
         cons_show_error("Unsupported window for file transmission.");
-        free(filename);
-        return TRUE;
-    }
-
-    if (access(filename, R_OK) != 0) {
-        cons_show_error("Uploading '%s' failed: File not found!", filename);
-        free(filename);
-        return TRUE;
-    }
-
-    if (!is_regular_file(filename)) {
-        cons_show_error("Uploading '%s' failed: Not a file!", filename);
-        free(filename);
-        return TRUE;
+        goto out;
     }
 
     HTTPUpload* upload = malloc(sizeof(HTTPUpload));
     upload->window = window;
 
-    upload->filename = filename;
-    upload->filesize = file_size(filename);
+    upload->filename = strdup(filename);
+    upload->filehandle = fh;
+    upload->filesize = file_size(fd);
     upload->mime_type = file_mime_type(filename);
 
+    if (alt_scheme != NULL) {
+        upload->alt_scheme = strdup(alt_scheme);
+    } else {
+        upload->alt_scheme = NULL;
+    }
+
+    if (alt_fragment != NULL) {
+        upload->alt_fragment = strdup(alt_fragment);
+    } else {
+        upload->alt_fragment = NULL;
+    }
+
     iq_http_upload_request(upload);
 
+out:
+#ifdef HAVE_OMEMO
+    if (alt_fragment != NULL)
+        omemo_free(alt_fragment);
+#endif
+    if (filename != NULL)
+        free(filename);
+
     return TRUE;
 }
 
@@ -8773,19 +8841,6 @@ cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args)
 }
 
 gboolean
-cmd_omemo_sendfile(ProfWin* window, const char* const command, gchar** args)
-{
-#ifdef HAVE_OMEMO
-    _cmd_set_boolean_preference(args[1], command, "Sending unencrypted files in an OMEMO session via /sendfile", PREF_OMEMO_SENDFILE);
-
-    return TRUE;
-#else
-    cons_show("This version of Profanity has not been built with OMEMO support enabled");
-    return TRUE;
-#endif
-}
-
-gboolean
 cmd_save(ProfWin* window, const char* const command, gchar** args)
 {
     log_info("Saving preferences to configuration file");
@@ -9005,6 +9060,57 @@ cmd_slashguard(ProfWin* window, const char* const command, gchar** args)
     return TRUE;
 }
 
+#ifdef HAVE_OMEMO
+void
+_url_aesgcm_method(ProfWin* window, const char* cmd_template, const char* url, const char* filename)
+{
+    AESGCMDownload* download = malloc(sizeof(AESGCMDownload));
+    download->window = window;
+    download->url = strdup(url);
+    download->filename = strdup(filename);
+    if (cmd_template != NULL) {
+        download->cmd_template = strdup(cmd_template);
+    } else {
+        download->cmd_template = NULL;
+    }
+
+    pthread_create(&(download->worker), NULL, &aesgcm_file_get, download);
+    aesgcm_download_add_download(download);
+}
+#endif
+
+void
+_url_http_method(ProfWin* window, const char* cmd_template, const char* url, const char* filename)
+{
+
+    HTTPDownload* download = malloc(sizeof(HTTPDownload));
+    download->window = window;
+    download->url = strdup(url);
+    download->filename = strdup(filename);
+    if (cmd_template != NULL) {
+        download->cmd_template = strdup(cmd_template);
+    } else {
+        download->cmd_template = NULL;
+    }
+
+    pthread_create(&(download->worker), NULL, &http_file_get, download);
+    http_download_add_download(download);
+}
+
+void
+_url_external_method(const char* cmd_template, const char* url, const char* filename)
+{
+    gchar** argv = format_call_external_argv(cmd_template, url, filename);
+
+    if (!call_external(argv, NULL, NULL)) {
+        cons_show_error("Unable to call external executable for url: check the logs for more information.");
+    } else {
+        cons_show("URL '%s' has been called with '%s'.", url, cmd_template);
+    }
+
+    g_strfreev(argv);
+}
+
 gboolean
 cmd_url_open(ProfWin* window, const char* const command, gchar** args)
 {
@@ -9013,91 +9119,45 @@ cmd_url_open(ProfWin* window, const char* const command, gchar** args)
         return TRUE;
     }
 
-    if (args[1] == NULL) {
+    gchar* url = args[1];
+    if (url == NULL) {
         cons_bad_cmd_usage(command);
         return TRUE;
     }
 
-    gboolean require_save = false;
+    gchar* scheme = NULL;
+    char* cmd_template = NULL;
+    char* filename = NULL;
 
-    gchar* fileStart = g_strrstr(args[1], "/");
-    if (fileStart == NULL) {
+    scheme = g_uri_parse_scheme(url);
+    if (scheme == NULL) {
         cons_show("URL '%s' is not valid.", args[1]);
-        return TRUE;
-    }
-
-    fileStart++;
-    if (((char*)(fileStart - 2))[0] == '/' && ((char*)(fileStart - 3))[0] == ':') {
-        // If the '/' is last character of the '://' string, there will be no suffix
-        // Therefore, it is considered that there is no file name in the URL and
-        // fileStart is set to the end of the URL.
-        fileStart = args[1] + strlen(args[1]);
-    }
-
-    gchar* suffix = NULL;
-    gchar* suffixStart = g_strrstr(fileStart, ".");
-    if (suffixStart != NULL) {
-        suffixStart++;
-        gchar* suffixEnd = g_strrstr(suffixStart, "#");
-        if (suffixEnd == NULL) {
-            suffix = g_strdup(suffixStart);
-        } else {
-            suffix = g_strndup(suffixStart, suffixEnd - suffixStart);
-        }
+        goto out;
     }
 
-    gchar** suffix_cmd_pref = prefs_get_string_list_with_option(PREF_URL_OPEN_CMD, NULL);
-    if (suffix != NULL) {
-        gchar* lowercase_suffix = g_ascii_strdown(suffix, -1);
-        g_strfreev(suffix_cmd_pref);
-        suffix_cmd_pref = prefs_get_string_list_with_option(PREF_URL_OPEN_CMD, lowercase_suffix);
-        g_free(lowercase_suffix);
-        g_free(suffix);
+    cmd_template = prefs_get_string(PREF_URL_OPEN_CMD);
+    if (cmd_template == NULL) {
+        cons_show("No default `url open` command found in executables preferences.");
+        goto out;
     }
 
-    if (0 == g_strcmp0(suffix_cmd_pref[0], "true")) {
-        require_save = true;
-    }
-
-    gchar* suffix_cmd = g_strdup(suffix_cmd_pref[1]);
-    g_strfreev(suffix_cmd_pref);
-
-    gchar* scheme = g_uri_parse_scheme(args[1]);
+#ifdef HAVE_OMEMO
+    // OMEMO URLs (aesgcm://) must be saved and decrypted before being opened.
     if (0 == g_strcmp0(scheme, "aesgcm")) {
-        require_save = true;
-    }
-    g_free(scheme);
-
-    if (require_save) {
-        gchar* save_args[] = { "open", args[1], "/tmp/profanity.tmp", NULL };
-        cmd_url_save(window, command, save_args);
+        filename = unique_filename_from_url(url, files_get_data_path(DIR_DOWNLOADS));
+        _url_aesgcm_method(window, cmd_template, url, filename);
+        goto out;
     }
+#endif
 
-    gchar** argv = g_strsplit(suffix_cmd, " ", 0);
-    guint num_args = 0;
-    while (argv[num_args]) {
-        if (0 == g_strcmp0(argv[num_args], "%u")) {
-            g_free(argv[num_args]);
-            if (require_save) {
-                argv[num_args] = g_strdup("/tmp/profanity.tmp");
-            } else {
-                argv[num_args] = g_strdup(args[1]);
-            }
-            break;
-        }
-        num_args++;
-    }
+    _url_external_method(cmd_template, url, NULL);
 
-    if (!call_external(argv, NULL, NULL)) {
-        cons_show_error("Unable to open url: check the logs for more information.");
-    }
+out:
 
-    if (require_save) {
-        g_unlink("/tmp/profanity.tmp");
-    }
+    free(cmd_template);
+    free(filename);
 
-    g_strfreev(argv);
-    g_free(suffix_cmd);
+    g_free(scheme);
 
     return TRUE;
 }
@@ -9106,7 +9166,7 @@ gboolean
 cmd_url_save(ProfWin* window, const char* const command, gchar** args)
 {
     if (window->type != WIN_CHAT && window->type != WIN_MUC && window->type != WIN_PRIVATE) {
-        cons_show("url save not supported in this window");
+        cons_show_error("`/url save` is not supported in this window.");
         return TRUE;
     }
 
@@ -9115,121 +9175,104 @@ cmd_url_save(ProfWin* window, const char* const command, gchar** args)
         return TRUE;
     }
 
-    gchar* uri = args[1];
-    gchar* target_path = g_strdup(args[2]);
+    gchar* url = args[1];
+    gchar* path = g_strdup(args[2]);
+    gchar* scheme = NULL;
+    char* filename = NULL;
+    char* cmd_template = NULL;
 
-    GFile* file = g_file_new_for_uri(uri);
-
-    gchar* target_dir = NULL;
-    gchar* base_name = NULL;
-
-    if (target_path == NULL) {
-        target_dir = g_strdup("./");
-        base_name = g_file_get_basename(file);
-        if (0 == g_strcmp0(base_name, ".")) {
-            g_free(base_name);
-            base_name = g_strdup("saved_url_content.html");
-        }
-        target_path = g_strconcat(target_dir, base_name, NULL);
-    }
-
-    if (g_file_test(target_path, G_FILE_TEST_EXISTS) && g_file_test(target_path, G_FILE_TEST_IS_DIR)) {
-        target_dir = g_strdup(target_path);
-        base_name = g_file_get_basename(file);
-        g_free(target_path);
-        target_path = g_strconcat(target_dir, "/", base_name, NULL);
-    }
-
-    g_object_unref(file);
-    file = NULL;
-
-    if (base_name == NULL) {
-        base_name = g_path_get_basename(target_path);
-        target_dir = g_path_get_dirname(target_path);
+    scheme = g_uri_parse_scheme(url);
+    if (scheme == NULL) {
+        cons_show("URL '%s' is not valid.", args[1]);
+        goto out;
     }
 
-    if (!g_file_test(target_dir, G_FILE_TEST_EXISTS) || !g_file_test(target_dir, G_FILE_TEST_IS_DIR)) {
-        cons_show("%s does not exist or is not a directory.", target_dir);
-        g_free(target_path);
-        g_free(target_dir);
-        g_free(base_name);
-        return TRUE;
+    filename = unique_filename_from_url(url, path);
+    if (filename == NULL) {
+        cons_show("Failed to generate unique filename"
+                  "from URL '%s' for path '%s'",
+                  url, path);
+        goto out;
     }
 
-    gchar* scheme = g_uri_parse_scheme(uri);
-    if (scheme == NULL) {
-        cons_show("URL '%s' is not valid.", uri);
-        g_free(target_path);
-        g_free(target_dir);
-        g_free(base_name);
-        return TRUE;
+    cmd_template = prefs_get_string(PREF_URL_SAVE_CMD);
+    if (cmd_template == NULL && (g_strcmp0(scheme, "http") == 0 || g_strcmp0(scheme, "https") == 0)) {
+        _url_http_method(window, cmd_template, url, filename);
+#ifdef HAVE_OMEMO
+    } else if (g_strcmp0(scheme, "aesgcm") == 0) {
+        _url_aesgcm_method(window, cmd_template, url, filename);
+#endif
+    } else if (cmd_template != NULL) {
+        _url_external_method(cmd_template, url, filename);
+    } else {
+        cons_show_error("No download method defined for the scheme '%s'.", scheme);
     }
 
-    gchar* scheme_cmd = NULL;
+out:
 
-    if (0 == g_strcmp0(scheme, "http")
-        || 0 == g_strcmp0(scheme, "https")
-        || 0 == g_strcmp0(scheme, "aesgcm")) {
-        scheme_cmd = prefs_get_string_with_option(PREF_URL_SAVE_CMD, scheme);
-    }
+    free(filename);
+    free(cmd_template);
 
     g_free(scheme);
+    g_free(path);
 
-    gchar** argv = g_strsplit(scheme_cmd, " ", 0);
-    g_free(scheme_cmd);
+    return TRUE;
+}
 
-    guint num_args = 0;
-    while (argv[num_args]) {
-        if (0 == g_strcmp0(argv[num_args], "%u")) {
-            g_free(argv[num_args]);
-            argv[num_args] = g_strdup(uri);
-        } else if (0 == g_strcmp0(argv[num_args], "%p")) {
-            g_free(argv[num_args]);
-            argv[num_args] = target_path;
-        }
-        num_args++;
+gboolean
+cmd_executable_avatar(ProfWin* window, const char* const command, gchar** args)
+{
+    prefs_set_string(PREF_AVATAR_CMD, args[1]);
+    cons_show("`avatar` command set to invoke '%s'", args[1]);
+    return TRUE;
+}
+
+gboolean
+cmd_executable_urlopen(ProfWin* window, const char* const command, gchar** args)
+{
+    guint num_args = g_strv_length(args);
+    if (num_args < 2) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
     }
 
-    if (!call_external(argv, NULL, NULL)) {
-        cons_show_error("Unable to save url: check the logs for more information.");
+    if (g_strcmp0(args[1], "set") == 0 && num_args >= 3) {
+        gchar* str = g_strjoinv(" ", &args[2]);
+        prefs_set_string(PREF_URL_OPEN_CMD, str);
+        cons_show("`url open` command set to invoke '%s'", str);
+        g_free(str);
+
+    } else if (g_strcmp0(args[1], "default") == 0) {
+        prefs_set_string(PREF_URL_SAVE_CMD, NULL);
+        gchar* def = prefs_get_string(PREF_URL_SAVE_CMD);
+        cons_show("`url open` command set to invoke %s (default)", def);
+        g_free(def);
     } else {
-        cons_show("URL '%s' has been saved into '%s'.", uri, target_path);
+        cons_bad_cmd_usage(command);
     }
 
-    g_free(target_dir);
-    g_free(base_name);
-    g_strfreev(argv);
-
     return TRUE;
 }
 
 gboolean
-cmd_executable(ProfWin* window, const char* const command, gchar** args)
+cmd_executable_urlsave(ProfWin* window, const char* const command, gchar** args)
 {
-    if (g_strcmp0(args[0], "avatar") == 0) {
-        prefs_set_string(PREF_AVATAR_CMD, args[1]);
-        cons_show("Avatar command set to: %s", args[1]);
-    } else if (g_strcmp0(args[0], "urlopen") == 0) {
-        if (g_strv_length(args) < 4) {
-            cons_bad_cmd_usage(command);
-            return TRUE;
-        }
 
-        gchar* str = g_strjoinv(" ", &args[3]);
-        const gchar* const list[] = { args[2], str, NULL };
-        prefs_set_string_list_with_option(PREF_URL_OPEN_CMD, args[1], list);
-        cons_show("`url open` command set to: %s for %s files", str, args[1]);
-        g_free(str);
-    } else if (g_strcmp0(args[0], "urlsave") == 0) {
-        if (g_strv_length(args) < 3) {
-            cons_bad_cmd_usage(command);
-            return TRUE;
-        }
+    guint num_args = g_strv_length(args);
+    if (num_args < 2) {
+        cons_bad_cmd_usage(command);
+        return TRUE;
+    }
 
+    if (g_strcmp0(args[1], "set") == 0 && num_args >= 3) {
         gchar* str = g_strjoinv(" ", &args[2]);
-        prefs_set_string_with_option(PREF_URL_SAVE_CMD, args[1], str);
-        cons_show("`url save` command set to: %s for scheme %s", str, args[1]);
+        prefs_set_string(PREF_URL_SAVE_CMD, str);
+        cons_show("`url save` command set to invoke '%s'", str);
         g_free(str);
+
+    } else if (g_strcmp0(args[1], "default") == 0) {
+        prefs_set_string(PREF_URL_SAVE_CMD, NULL);
+        cons_show("`url save` will use built-in download method (default)");
     } else {
         cons_bad_cmd_usage(command);
     }
diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h
index 92c81364..4955972c 100644
--- a/src/command/cmd_funcs.h
+++ b/src/command/cmd_funcs.h
@@ -223,7 +223,6 @@ gboolean cmd_omemo_trust(ProfWin* window, const char* const command, gchar** arg
 gboolean cmd_omemo_untrust(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_omemo_clear_device_list(ProfWin* window, const char* const command, gchar** args);
-gboolean cmd_omemo_sendfile(ProfWin* window, const char* const command, gchar** args);
 
 gboolean cmd_save(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_reload(ProfWin* window, const char* const command, gchar** args);
@@ -238,6 +237,8 @@ gboolean cmd_slashguard(ProfWin* window, const char* const command, gchar** args
 gboolean cmd_serversoftware(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_url_open(ProfWin* window, const char* const command, gchar** args);
 gboolean cmd_url_save(ProfWin* window, const char* const command, gchar** args);
-gboolean cmd_executable(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_avatar(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_urlopen(ProfWin* window, const char* const command, gchar** args);
+gboolean cmd_executable_urlsave(ProfWin* window, const char* const command, gchar** args);
 
 #endif