diff --git a/.gitignore b/.gitignore index da19d7c6..e06f5136 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ breaks *.tar.* *.zip +*.log* diff --git a/Makefile.am b/Makefile.am index 5a4d8ffb..d40494d1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -41,8 +41,12 @@ core_sources = \ src/command/cmd_ac.h src/command/cmd_ac.c \ src/tools/parser.c \ src/tools/parser.h \ + src/tools/http_common.c \ + src/tools/http_common.h \ src/tools/http_upload.c \ src/tools/http_upload.h \ + src/tools/http_download.c \ + src/tools/http_download.h \ src/tools/bookmark_ignore.c \ src/tools/bookmark_ignore.h \ src/tools/autocomplete.c src/tools/autocomplete.h \ @@ -119,6 +123,8 @@ unittest_sources = \ tests/unittests/database/stub_database.c \ tests/unittests/config/stub_accounts.c \ tests/unittests/tools/stub_http_upload.c \ + tests/unittests/tools/stub_http_download.c \ + tests/unittests/tools/stub_aesgcm_download.c \ tests/unittests/helpers.c tests/unittests/helpers.h \ tests/unittests/test_form.c tests/unittests/test_form.h \ tests/unittests/test_common.c tests/unittests/test_common.h \ @@ -189,7 +195,8 @@ otr4_sources = \ omemo_sources = \ src/omemo/omemo.h src/omemo/omemo.c src/omemo/crypto.h src/omemo/crypto.c \ - src/omemo/store.h src/omemo/store.c src/xmpp/omemo.h src/xmpp/omemo.c + src/omemo/store.h src/omemo/store.c src/xmpp/omemo.h src/xmpp/omemo.c \ + src/tools/aesgcm_download.h src/tools/aesgcm_download.c omemo_unittest_sources = \ tests/unittests/omemo/stub_omemo.c 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 []", - "/omemo trust [] ", - "/omemo end", - "/omemo fingerprint []", - "/omemo 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 []", "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 []", "Show contact fingerprints, or current recipient if omitted." }, - { "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 []", + "/omemo trust [] ", + "/omemo end", + "/omemo fingerprint []", + "/omemo 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 []", "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 []", "Show contact fingerprints, or current recipient if omitted." }, + { "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 ", - "/executable urlopen (|DEF ", - "/executable urlsave (|DEF) ") + "/executable urlopen set ", + "/executable urlopen default", + "/executable urlsave set ", + "/executable urlsave default") CMD_DESC( - "Configure executable that should be called upon a certain command." - "Default is xdg-open.") + "Configure executable that should be called upon a certain command.") 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." }) + { "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 * Copyright (C) 2019 Michael Vetter + * Copyright (C) 2020 William Wennerström * * 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 (conn_status != JABBER_CONNECTED) { - cons_show("You are not currently connected."); - free(filename); - return TRUE; - } - - 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; - } - - 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; - } - break; - } - case WIN_PRIVATE: - { - //we don't support encryption in private muc windows - break; - } - 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; + goto out; } if (!is_regular_file(filename)) { cons_show_error("Uploading '%s' failed: Not a file!", filename); - free(filename); - return TRUE; + goto out; + } + + if (conn_status != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + goto out; + } + + if (window->type != WIN_CHAT && window->type != WIN_PRIVATE && window->type != WIN_MUC) { + cons_show_error("Unsupported window for file transmission."); + 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: + case WIN_CHAT: + { + ProfChatWin* chatwin = (ProfChatWin*)window; + +#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. + default: + cons_show_error("Unsupported window for file transmission."); + 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; } @@ -8772,19 +8840,6 @@ cmd_omemo_policy(ProfWin* window, const char* const command, gchar** args) #endif } -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) { @@ -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,92 +9119,46 @@ 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; + goto out; } - 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]); + 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; } - 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); - } - } - - 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); - } - - 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; + filename = unique_filename_from_url(url, files_get_data_path(DIR_DOWNLOADS)); + _url_aesgcm_method(window, cmd_template, url, filename); + goto out; } +#endif + + _url_external_method(cmd_template, url, NULL); + +out: + + free(cmd_template); + free(filename); + g_free(scheme); - if (require_save) { - gchar* save_args[] = { "open", args[1], "/tmp/profanity.tmp", NULL }; - cmd_url_save(window, command, save_args); - } - - 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++; - } - - if (!call_external(argv, NULL, NULL)) { - cons_show_error("Unable to open url: check the logs for more information."); - } - - if (require_save) { - g_unlink("/tmp/profanity.tmp"); - } - - g_strfreev(argv); - g_free(suffix_cmd); - 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); - } - - 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; - } - - gchar* scheme = g_uri_parse_scheme(uri); + scheme = g_uri_parse_scheme(url); 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; + cons_show("URL '%s' is not valid.", args[1]); + goto out; } - gchar* scheme_cmd = NULL; - - 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); + 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; } + 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); + } + +out: + + free(filename); + free(cmd_template); + g_free(scheme); - - gchar** argv = g_strsplit(scheme_cmd, " ", 0); - g_free(scheme_cmd); - - 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++; - } - - if (!call_external(argv, NULL, NULL)) { - cons_show_error("Unable to save url: check the logs for more information."); - } else { - cons_show("URL '%s' has been saved into '%s'.", uri, target_path); - } - - g_free(target_dir); - g_free(base_name); - g_strfreev(argv); + g_free(path); return TRUE; } gboolean -cmd_executable(ProfWin* window, const char* const command, gchar** args) +cmd_executable_avatar(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; - } + prefs_set_string(PREF_AVATAR_CMD, args[1]); + cons_show("`avatar` command set to invoke '%s'", args[1]); + 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; - } +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 (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_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_bad_cmd_usage(command); + } + + return TRUE; +} + +gboolean +cmd_executable_urlsave(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 (g_strcmp0(args[1], "set") == 0 && num_args >= 3) { + gchar* str = g_strjoinv(" ", &args[2]); + 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 diff --git a/src/common.c b/src/common.c index c0bd6525..10be280a 100644 --- a/src/common.c +++ b/src/common.c @@ -33,6 +33,9 @@ * source files in the program, then also delete it here. * */ + +#define _GNU_SOURCE 1 + #include "config.h" #include @@ -555,3 +558,107 @@ call_external(gchar** argv, gchar*** const output_ptr, gchar*** const error_ptr) return TRUE; } + +gchar** +format_call_external_argv(const char* template, const char* url, const char* filename) +{ + gchar** argv = g_strsplit(template, " ", 0); + + guint num_args = 0; + while (argv[num_args]) { + if (0 == g_strcmp0(argv[num_args], "%u") && url != NULL) { + g_free(argv[num_args]); + argv[num_args] = g_strdup(url); + } else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) { + g_free(argv[num_args]); + argv[num_args] = strdup(filename); + } + num_args++; + } + + return argv; +} + +gchar* +_unique_filename(const char* filename) +{ + gchar* unique = g_strdup(filename); + + unsigned int i = 0; + while (g_file_test(unique, G_FILE_TEST_EXISTS)) { + free(unique); + + if (i > 1000) { // Give up after 1000 attempts. + return NULL; + } + + if (asprintf(&unique, "%s.%u", filename, i) < 0) { + return NULL; + } + + i++; + } + + return unique; +} + +bool +_has_directory_suffix(const char* path) +{ + return (g_str_has_suffix(path, ".") + || g_str_has_suffix(path, "..") + || g_str_has_suffix(path, G_DIR_SEPARATOR_S)); +} + +char* +_basename_from_url(const char* url) +{ + const char* default_name = "index"; + + GFile* file = g_file_new_for_commandline_arg(url); + char* basename = g_file_get_basename(file); + + if (_has_directory_suffix(basename)) { + g_free(basename); + basename = strdup(default_name); + } + + g_object_unref(file); + + return basename; +} + +gchar* +unique_filename_from_url(const char* url, const char* path) +{ + // Default to './' as path when none has been provided. + if (path == NULL) { + path = "./"; + } + + // Resolves paths such as './../.' for path. + GFile* target = g_file_new_for_commandline_arg(path); + gchar* filename = NULL; + + if (_has_directory_suffix(path) || g_file_test(path, G_FILE_TEST_IS_DIR)) { + // The target should be used as a directory. Assume that the basename + // should be derived from the URL. + char* basename = _basename_from_url(url); + filename = g_build_filename(g_file_peek_path(target), basename, NULL); + g_free(basename); + } else { + // Just use the target as filename. + filename = g_build_filename(g_file_peek_path(target), NULL); + } + + gchar* unique_filename = _unique_filename(filename); + if (unique_filename == NULL) { + g_free(filename); + return NULL; + } + + g_object_unref(target); + g_free(filename); + + return unique_filename; +} diff --git a/src/common.h b/src/common.h index 13332f7a..088ba953 100644 --- a/src/common.h +++ b/src/common.h @@ -105,5 +105,8 @@ void get_file_paths_recursive(const char* directory, GSList** contents); char* get_random_string(int length); gboolean call_external(gchar** argv, gchar*** const output_ptr, gchar*** const error_ptr); +gchar** format_call_external_argv(const char* template, const char* url, const char* filename); + +gchar* unique_filename_from_url(const char* url, const char* path); #endif diff --git a/src/config/files.h b/src/config/files.h index d5c96b0f..42499663 100644 --- a/src/config/files.h +++ b/src/config/files.h @@ -48,15 +48,16 @@ #define FILE_PROFANITY_IDENTIFIER "profident" #define FILE_BOOKMARK_AUTOJOIN_IGNORE "bookmark_ignore" -#define DIR_THEMES "themes" -#define DIR_ICONS "icons" -#define DIR_SCRIPTS "scripts" -#define DIR_CHATLOGS "chatlogs" -#define DIR_OTR "otr" -#define DIR_PGP "pgp" -#define DIR_OMEMO "omemo" -#define DIR_PLUGINS "plugins" -#define DIR_DATABASE "database" +#define DIR_THEMES "themes" +#define DIR_ICONS "icons" +#define DIR_SCRIPTS "scripts" +#define DIR_CHATLOGS "chatlogs" +#define DIR_OTR "otr" +#define DIR_PGP "pgp" +#define DIR_OMEMO "omemo" +#define DIR_PLUGINS "plugins" +#define DIR_DATABASE "database" +#define DIR_DOWNLOADS "downloads" void files_create_directories(void); diff --git a/src/config/preferences.c b/src/config/preferences.c index e4aebdb5..9d7d4f7b 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -81,7 +81,6 @@ static const char* _get_group(preference_t pref); static const char* _get_key(preference_t pref); static gboolean _get_default_boolean(preference_t pref); static char* _get_default_string(preference_t pref); -static char** _get_default_string_list(preference_t pref); static void _prefs_load(void) @@ -171,7 +170,7 @@ _prefs_load(void) value = g_string_append(value, val); value = g_string_append(value, " %u;"); - g_key_file_set_locale_string(prefs, PREF_GROUP_EXECUTABLES, "url.open.cmd", "DEF", value->str); + g_key_file_set_locale_string(prefs, PREF_GROUP_EXECUTABLES, "url.open.cmd", "*", value->str); g_key_file_remove_key(prefs, PREF_GROUP_LOGGING, "urlopen.cmd", NULL); g_string_free(value, TRUE); @@ -530,7 +529,7 @@ prefs_get_string_with_option(preference_t pref, gchar* option) if (result == NULL) { // check for user set default - result = g_key_file_get_locale_string(prefs, group, key, "DEF", NULL); + result = g_key_file_get_locale_string(prefs, group, key, "*", NULL); if (result == NULL) { if (def) { // use hardcoded profanity default @@ -544,33 +543,6 @@ prefs_get_string_with_option(preference_t pref, gchar* option) return result; } -gchar** -prefs_get_string_list_with_option(preference_t pref, gchar* option) -{ - const char* group = _get_group(pref); - const char* key = _get_key(pref); - char** def = _get_default_string_list(pref); - - gchar** result = g_key_file_get_locale_string_list(prefs, group, key, option, NULL, NULL); - if (result) { - g_strfreev(def); - return result; - } - - result = g_key_file_get_string_list(prefs, group, key, NULL, NULL); - if (result) { - g_strfreev(def); - return result; - } - - if (def) { - return def; - } else { - g_strfreev(def); - return NULL; - } -} - void prefs_set_string(preference_t pref, char* value) { @@ -1925,7 +1897,6 @@ _get_group(preference_t pref) return PREF_GROUP_PLUGINS; case PREF_OMEMO_LOG: case PREF_OMEMO_POLICY: - case PREF_OMEMO_SENDFILE: return PREF_GROUP_OMEMO; default: return NULL; @@ -2172,8 +2143,6 @@ _get_key(preference_t pref) return "log"; case PREF_OMEMO_POLICY: return "policy"; - case PREF_OMEMO_SENDFILE: - return "sendfile"; case PREF_CORRECTION_ALLOW: return "correction.allow"; case PREF_AVATAR_CMD: @@ -2321,26 +2290,10 @@ _get_default_string(preference_t pref) return "false"; case PREF_AVATAR_CMD: return "xdg-open"; - case PREF_URL_SAVE_CMD: - return "curl -o %p %u"; - default: - return NULL; - } -} - -// the default setting for a string list type preference -// if it is not specified in .profrc -static char** -_get_default_string_list(preference_t pref) -{ - char** str_array = NULL; - - switch (pref) { case PREF_URL_OPEN_CMD: - str_array = g_malloc0(3); - str_array[0] = g_strdup("false"); - str_array[1] = g_strdup("xdg-open %u"); - return str_array; + return "xdg-open %u"; + case PREF_URL_SAVE_CMD: + return NULL; // Default to built-in method. default: return NULL; } diff --git a/src/config/preferences.h b/src/config/preferences.h index a9261853..bfad7d6b 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -165,7 +165,6 @@ typedef enum { PREF_STATUSBAR_ROOM, PREF_OMEMO_LOG, PREF_OMEMO_POLICY, - PREF_OMEMO_SENDFILE, PREF_OCCUPANTS_WRAP, PREF_CORRECTION_ALLOW, PREF_AVATAR_CMD, @@ -321,7 +320,6 @@ gboolean prefs_get_boolean(preference_t pref); void prefs_set_boolean(preference_t pref, gboolean value); char* prefs_get_string(preference_t pref); char* prefs_get_string_with_option(preference_t pref, gchar* option); -gchar** prefs_get_string_list_with_option(preference_t pref, gchar* option); void prefs_set_string(preference_t pref, char* value); void prefs_set_string_with_option(preference_t pref, char* option, char* value); void prefs_set_string_list_with_option(preference_t pref, char* option, const gchar* const* values); diff --git a/src/omemo/crypto.c b/src/omemo/crypto.c index 380551ad..a4a2d5fc 100644 --- a/src/omemo/crypto.c +++ b/src/omemo/crypto.c @@ -35,12 +35,14 @@ #include #include #include -#include #include "log.h" #include "omemo/omemo.h" #include "omemo/crypto.h" +#define AES256_GCM_TAG_LENGTH 16 +#define AES256_GCM_BUFFER_SIZE 1024 + int omemo_crypto_init(void) { @@ -373,3 +375,110 @@ out: gcry_cipher_close(hd); return res; } + +gcry_error_t +aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size, + unsigned char key[], unsigned char nonce[], bool encrypt) +{ + + if (!gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) { + fputs("libgcrypt has not been initialized\n", stderr); + abort(); + } + + if (!encrypt) { + file_size -= AES256_GCM_TAG_LENGTH; + } + + gcry_error_t res; + gcry_cipher_hd_t hd; + + res = gcry_cipher_open(&hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_GCM, + GCRY_CIPHER_SECURE); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_setkey(hd, key, OMEMO_AESGCM_KEY_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + res = gcry_cipher_setiv(hd, nonce, OMEMO_AESGCM_NONCE_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + unsigned char buffer[AES256_GCM_BUFFER_SIZE]; + + int bytes = 0; + off_t bytes_read = 0, bytes_available = 0, read_size = 0; + while (bytes_read < file_size) { + bytes_available = file_size - bytes_read; + if (!bytes_available || ferror(in) != 0) { + break; + } + + if (bytes_available < AES256_GCM_BUFFER_SIZE) { + read_size = bytes_available; + gcry_cipher_final(hd); // Signal last round of bytes. + } else { + read_size = AES256_GCM_BUFFER_SIZE; + } + + bytes = fread(buffer, 1, read_size, in); + bytes_read += bytes; + + if (encrypt) { + res = gcry_cipher_encrypt(hd, buffer, bytes, NULL, 0); + } else { + res = gcry_cipher_decrypt(hd, buffer, bytes, NULL, 0); + } + + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + fwrite(buffer, 1, bytes, out); + } + + unsigned char tag[AES256_GCM_TAG_LENGTH]; + + if (encrypt) { + // Append authentication tag at the end of the file. + res = gcry_cipher_gettag(hd, tag, AES256_GCM_TAG_LENGTH); + if (res != GPG_ERR_NO_ERROR) { + goto out; + } + + fwrite(tag, 1, AES256_GCM_TAG_LENGTH, out); + + } else { + // Read and verify authentication tag stored at the end of the file. + bytes = fread(tag, 1, AES256_GCM_TAG_LENGTH, in); + res = gcry_cipher_checktag(hd, tag, bytes); + } + +out: + gcry_cipher_close(hd); + return res; +} + +char* +aes256gcm_create_secure_fragment(unsigned char* key, unsigned char* nonce) +{ + int key_size = OMEMO_AESGCM_KEY_LENGTH; + int nonce_size = OMEMO_AESGCM_NONCE_LENGTH; + + char* fragment = gcry_malloc_secure((nonce_size + key_size) * 2 + 1); + + for (int i = 0; i < nonce_size; i++) { + sprintf(&(fragment[i * 2]), "%02x", nonce[i]); + } + + for (int i = 0; i < key_size; i++) { + sprintf(&(fragment[(i + nonce_size) * 2]), "%02x", key[i]); + } + + return fragment; +} diff --git a/src/omemo/crypto.h b/src/omemo/crypto.h index 4fb6283e..5adbffd8 100644 --- a/src/omemo/crypto.h +++ b/src/omemo/crypto.h @@ -32,7 +32,10 @@ * source files in the program, then also delete it here. * */ +#include +#include #include +#include #define AES128_GCM_KEY_LENGTH 16 #define AES128_GCM_IV_LENGTH 12 @@ -180,3 +183,9 @@ int aes128gcm_decrypt(unsigned char* plaintext, size_t* plaintext_len, const unsigned char* const ciphertext, size_t ciphertext_len, const unsigned char* const iv, size_t iv_len, const unsigned char* const key, const unsigned char* const tag); + +gcry_error_t aes256gcm_crypt_file(FILE* in, FILE* out, off_t file_size, + unsigned char key[], unsigned char nonce[], bool encrypt); + +char* aes256gcm_create_secure_fragment(unsigned char* key, + unsigned char* nonce); diff --git a/src/omemo/omemo.c b/src/omemo/omemo.c index c6c34ac1..22ada3a8 100644 --- a/src/omemo/omemo.c +++ b/src/omemo/omemo.c @@ -45,7 +45,6 @@ #include #include #include -#include #include "config/account.h" #include "config/files.h" @@ -62,6 +61,9 @@ #include "xmpp/roster_list.h" #include "xmpp/xmpp.h" +#define AESGCM_URL_NONCE_LEN (2 * OMEMO_AESGCM_NONCE_LENGTH) +#define AESGCM_URL_KEY_LEN (2 * OMEMO_AESGCM_KEY_LENGTH) + static gboolean loaded; static void _generate_pre_keys(int count); @@ -1653,3 +1655,134 @@ _generate_signed_pre_key(void) signal_protocol_signed_pre_key_store_key(omemo_ctx.store, signed_pre_key); SIGNAL_UNREF(signed_pre_key); } + +void +omemo_free(void* a) +{ + gcry_free(a); +} + +char* +omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res) +{ + unsigned char* key = gcry_random_bytes_secure( + OMEMO_AESGCM_KEY_LENGTH, + GCRY_VERY_STRONG_RANDOM); + + // Create nonce/IV with random bytes. + unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH]; + gcry_create_nonce(nonce, OMEMO_AESGCM_NONCE_LENGTH); + + char* fragment = aes256gcm_create_secure_fragment(key, nonce); + *gcry_res = aes256gcm_crypt_file(in, out, file_size, key, nonce, true); + + if (*gcry_res != GPG_ERR_NO_ERROR) { + gcry_free(fragment); + fragment = NULL; + } + + gcry_free(key); + + return fragment; +} + +void +_bytes_from_hex(const char* hex, size_t hex_size, + unsigned char* bytes, size_t bytes_size) +{ + const unsigned char ht[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567 + 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>? + 0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG + }; + const size_t ht_size = sizeof(ht); + + unsigned char b0; + unsigned char b1; + + memset(bytes, 0, bytes_size); + + for (int i = 0; (i < hex_size) && (i / 2 < bytes_size); i += 2) { + b0 = ((unsigned char)hex[i + 0] & 0x1f) ^ 0x10; + b1 = ((unsigned char)hex[i + 1] & 0x1f) ^ 0x10; + + if (b0 <= ht_size && b1 <= ht_size) { + bytes[i / 2] = (unsigned char)(ht[b0] << 4) | ht[b1]; + } + } +} + +gcry_error_t +omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment) +{ + char nonce_hex[AESGCM_URL_NONCE_LEN]; + char key_hex[AESGCM_URL_KEY_LEN]; + + const int nonce_pos = 0; + const int key_pos = AESGCM_URL_NONCE_LEN; + + memcpy(nonce_hex, &(fragment[nonce_pos]), AESGCM_URL_NONCE_LEN); + memcpy(key_hex, &(fragment[key_pos]), AESGCM_URL_KEY_LEN); + + unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH]; + unsigned char* key = gcry_malloc_secure(OMEMO_AESGCM_KEY_LENGTH); + + _bytes_from_hex(nonce_hex, AESGCM_URL_NONCE_LEN, + nonce, OMEMO_AESGCM_NONCE_LENGTH); + _bytes_from_hex(key_hex, AESGCM_URL_KEY_LEN, + key, OMEMO_AESGCM_KEY_LENGTH); + + gcry_error_t crypt_res; + crypt_res = aes256gcm_crypt_file(in, out, file_size, key, nonce, false); + + gcry_free(key); + + return crypt_res; +} + +int +omemo_parse_aesgcm_url(const char* aesgcm_url, + char** https_url, + char** fragment) +{ + CURLUcode ret; + CURLU* url = curl_url(); + + // Required to allow for the "aesgcm://" scheme that OMEMO Media Sharing + // uses. + unsigned int curl_flags = CURLU_NON_SUPPORT_SCHEME; + + ret = curl_url_set(url, CURLUPART_URL, aesgcm_url, curl_flags); + if (ret) { + goto out; + } + + ret = curl_url_get(url, CURLUPART_FRAGMENT, fragment, curl_flags); + if (ret) { + goto out; + } + + if (strlen(*fragment) != AESGCM_URL_NONCE_LEN + AESGCM_URL_KEY_LEN) { + goto out; + } + + // Clear fragment from HTTPS URL as it's not required for download. + ret = curl_url_set(url, CURLUPART_FRAGMENT, NULL, curl_flags); + if (ret) { + goto out; + } + + ret = curl_url_set(url, CURLUPART_SCHEME, "https", curl_flags); + if (ret) { + goto out; + } + + ret = curl_url_get(url, CURLUPART_URL, https_url, curl_flags); + if (ret) { + goto out; + } + +out: + curl_url_cleanup(url); + return ret; +} diff --git a/src/omemo/omemo.h b/src/omemo/omemo.h index ecfc42d9..7e7c1f14 100644 --- a/src/omemo/omemo.h +++ b/src/omemo/omemo.h @@ -33,6 +33,7 @@ * */ #include +#include #include "ui/ui.h" #include "config/account.h" @@ -40,6 +41,10 @@ #define OMEMO_ERR_UNSUPPORTED_CRYPTO -10000 #define OMEMO_ERR_GCRYPT -20000 +#define OMEMO_AESGCM_NONCE_LENGTH AES128_GCM_IV_LENGTH +#define OMEMO_AESGCM_KEY_LENGTH 32 +#define OMEMO_AESGCM_URL_SCHEME "aesgcm" + typedef enum { PROF_OMEMOPOLICY_MANUAL, PROF_OMEMOPOLICY_AUTOMATIC, @@ -95,3 +100,8 @@ void omemo_start_device_session(const char* const jid, uint32_t device_id, GList gboolean omemo_loaded(void); char* omemo_on_message_send(ProfWin* win, const char* const message, gboolean request_receipt, gboolean muc, const char* const replace_id); char* omemo_on_message_recv(const char* const from, uint32_t sid, const unsigned char* const iv, size_t iv_len, GList* keys, const unsigned char* const payload, size_t payload_len, gboolean muc, gboolean* trusted); + +char* omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res); +gcry_error_t omemo_decrypt_file(FILE* in, FILE* out, off_t file_size, const char* fragment); +void omemo_free(void* a); +int omemo_parse_aesgcm_url(const char* aesgcm_url, char** https_url, char** fragment); diff --git a/src/tools/aesgcm_download.c b/src/tools/aesgcm_download.c new file mode 100644 index 00000000..96f8d7e8 --- /dev/null +++ b/src/tools/aesgcm_download.c @@ -0,0 +1,190 @@ +/* + * aesgcm_download.c + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2012 - 2019 James Booth + * Copyright (C) 2020 William Wennerström + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#define _GNU_SOURCE 1 + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "profanity.h" +#include "event/client_events.h" +#include "tools/http_common.h" +#include "tools/aesgcm_download.h" +#include "omemo/omemo.h" +#include "config/preferences.h" +#include "ui/ui.h" +#include "ui/window.h" +#include "common.h" + +#define FALLBACK_MSG "" + +void* +aesgcm_file_get(void* userdata) +{ + AESGCMDownload* aesgcm_dl = (AESGCMDownload*)userdata; + + char* https_url = NULL; + char* fragment = NULL; + + // Convert the aesgcm:// URL to a https:// URL and extract the encoded key + // and tag stored in the URL fragment. + if (omemo_parse_aesgcm_url(aesgcm_dl->url, &https_url, &fragment) != 0) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url, + "Download failed: Cannot parse URL '%s'.", + aesgcm_dl->url); + return NULL; + } + + // Create a temporary file used for storing the ciphertext that is to be + // retrieved from the https:// URL. + gchar* tmpname = NULL; + gint tmpfd; + if ((tmpfd = g_file_open_tmp("profanity.XXXXXX", &tmpname, NULL)) == -1) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url, + "Downloading '%s' failed: Unable to create " + "temporary ciphertext file for writing " + "(%s).", + https_url, g_strerror(errno)); + return NULL; + } + + // Open the target file for storing the cleartext. + FILE* outfh = fopen(aesgcm_dl->filename, "wb"); + if (outfh == NULL) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url, + "Downloading '%s' failed: Unable to open " + "output file at '%s' for writing (%s).", + https_url, aesgcm_dl->filename, + g_strerror(errno)); + return NULL; + } + + // We wrap the HTTPDownload tool and use it for retrieving the ciphertext + // and storing it in the temporary file previously opened. + HTTPDownload* http_dl = malloc(sizeof(HTTPDownload)); + http_dl->window = aesgcm_dl->window; + http_dl->worker = aesgcm_dl->worker; + http_dl->url = strdup(https_url); + http_dl->filename = strdup(tmpname); + http_dl->cmd_template = NULL; + aesgcm_dl->http_dl = http_dl; + + http_file_get(http_dl); // TODO(wstrm): Verify result. + + FILE* tmpfh = fopen(tmpname, "rb"); + if (tmpfh == NULL) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url, + "Downloading '%s' failed: Unable to open " + "temporary file at '%s' for reading (%s).", + aesgcm_dl->url, tmpname, + g_strerror(errno)); + return NULL; + } + + gcry_error_t crypt_res; + crypt_res = omemo_decrypt_file(tmpfh, outfh, + http_dl->bytes_received, fragment); + + if (fclose(tmpfh) == EOF) { + cons_show_error(g_strerror(errno)); + } + + close(tmpfd); + remove(tmpname); + g_free(tmpname); + + if (crypt_res != GPG_ERR_NO_ERROR) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url, + "Downloading '%s' failed: Failed to decrypt " + "file (%s).", + https_url, gcry_strerror(crypt_res)); + } + + if (fclose(outfh) == EOF) { + cons_show_error(g_strerror(errno)); + } + + free(https_url); + free(fragment); + + if (aesgcm_dl->cmd_template != NULL) { + gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template, + aesgcm_dl->filename, + aesgcm_dl->filename); + + // TODO: Log the error. + if (!call_external(argv, NULL, NULL)) { + http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->url, + "Downloading '%s' failed: Unable to call " + "command '%s' with file at '%s' (%s).", + aesgcm_dl->url, + aesgcm_dl->cmd_template, + aesgcm_dl->filename, + "TODO: Log the error"); + } + + g_strfreev(argv); + free(aesgcm_dl->cmd_template); + } + + free(aesgcm_dl->filename); + free(aesgcm_dl->url); + free(aesgcm_dl); + + return NULL; +} + +void +aesgcm_download_cancel_processes(ProfWin* window) +{ + http_download_cancel_processes(window); +} + +void +aesgcm_download_add_download(AESGCMDownload* aesgcm_dl) +{ + http_download_add_download(aesgcm_dl->http_dl); +} diff --git a/src/tools/aesgcm_download.h b/src/tools/aesgcm_download.h new file mode 100644 index 00000000..c0096f1d --- /dev/null +++ b/src/tools/aesgcm_download.h @@ -0,0 +1,66 @@ +/* + * aesgcm_download.h + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2012 - 2019 James Booth + * Copyright (C) 2020 William Wennerström + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#ifndef TOOLS_AESGCM_DOWNLOAD_H +#define TOOLS_AESGCM_DOWNLOAD_H + +#ifdef PLATFORM_CYGWIN +#define SOCKET int +#endif + +#include +#include +#include "tools/http_common.h" +#include "tools/http_download.h" + +#include "ui/win_types.h" + +typedef struct aesgcm_download_t +{ + char* url; + char* filename; + char* cmd_template; + ProfWin* window; + pthread_t worker; + HTTPDownload* http_dl; +} AESGCMDownload; + +void* aesgcm_file_get(void* userdata); + +void aesgcm_download_cancel_processes(ProfWin* window); +void aesgcm_download_add_download(AESGCMDownload* download); + +#endif diff --git a/src/tools/http_common.c b/src/tools/http_common.c new file mode 100644 index 00000000..e066a6f6 --- /dev/null +++ b/src/tools/http_common.c @@ -0,0 +1,75 @@ +/* + * http_common.c + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2020 William Wennerström + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#define _GNU_SOURCE 1 + +#include +#include +#include +#include + +#include "tools/http_common.h" + +#define FALLBACK_MSG "" + +void +http_print_transfer_update(ProfWin* window, char* url, const char* fmt, ...) +{ + va_list args; + + va_start(args, fmt); + GString* msg = g_string_new(FALLBACK_MSG); + g_string_vprintf(msg, fmt, args); + va_end(args); + + win_update_entry_message(window, url, msg->str); + + g_string_free(msg, TRUE); +} + +void +http_print_transfer(ProfWin* window, char* url, const char* fmt, ...) +{ + va_list args; + + va_start(args, fmt); + GString* msg = g_string_new(FALLBACK_MSG); + g_string_vprintf(msg, fmt, args); + va_end(args); + + win_print_http_transfer(window, msg->str, url); + + g_string_free(msg, TRUE); +} diff --git a/src/tools/http_common.h b/src/tools/http_common.h new file mode 100644 index 00000000..ac51b5a8 --- /dev/null +++ b/src/tools/http_common.h @@ -0,0 +1,44 @@ +/* + * http_common.h + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2020 William Wennerström + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#ifndef TOOLS_HTTP_COMMON_H +#define TOOLS_HTTP_COMMON_H + +#include "ui/window.h" + +void http_print_transfer(ProfWin* window, char* url, const char* fmt, ...); +void http_print_transfer_update(ProfWin* window, char* url, const char* fmt, ...); + +#endif diff --git a/src/tools/http_download.c b/src/tools/http_download.c new file mode 100644 index 00000000..f97fd704 --- /dev/null +++ b/src/tools/http_download.c @@ -0,0 +1,236 @@ +/* + * http_download.c + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2012 - 2019 James Booth + * Copyright (C) 2020 William Wennerström + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#define _GNU_SOURCE 1 + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "profanity.h" +#include "event/client_events.h" +#include "tools/http_download.h" +#include "config/preferences.h" +#include "ui/ui.h" +#include "ui/window.h" +#include "common.h" + +GSList* download_processes = NULL; + +static int +_xferinfo(void* userdata, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) +{ + HTTPDownload* download = (HTTPDownload*)userdata; + + pthread_mutex_lock(&lock); + + if (download->cancel) { + pthread_mutex_unlock(&lock); + return 1; + } + + if (download->bytes_received == dlnow) { + pthread_mutex_unlock(&lock); + return 0; + } else { + download->bytes_received = dlnow; + } + + unsigned int dlperc = 0; + if (dltotal != 0) { + dlperc = (100 * dlnow) / dltotal; + } + + http_print_transfer_update(download->window, download->url, + "Downloading '%s': %d%%", download->url, dlperc); + + pthread_mutex_unlock(&lock); + + return 0; +} + +#if LIBCURL_VERSION_NUM < 0x072000 +static int +_older_progress(void* p, double dltotal, double dlnow, double ultotal, double ulnow) +{ + return _xferinfo(p, (curl_off_t)dltotal, (curl_off_t)dlnow, (curl_off_t)ultotal, (curl_off_t)ulnow); +} +#endif + +void* +http_file_get(void* userdata) +{ + HTTPDownload* download = (HTTPDownload*)userdata; + + char* err = NULL; + + CURL* curl; + CURLcode res; + + download->cancel = 0; + download->bytes_received = 0; + + pthread_mutex_lock(&lock); + http_print_transfer(download->window, download->url, + "Downloading '%s': 0%%", download->url); + + FILE* outfh = fopen(download->filename, "wb"); + if (outfh == NULL) { + http_print_transfer_update(download->window, download->url, + "Downloading '%s' failed: Unable to open " + "output file at '%s' for writing (%s).", + download->url, download->filename, + g_strerror(errno)); + return NULL; + } + + char* cert_path = prefs_get_string(PREF_TLS_CERTPATH); + pthread_mutex_unlock(&lock); + + curl_global_init(CURL_GLOBAL_ALL); + curl = curl_easy_init(); + + curl_easy_setopt(curl, CURLOPT_URL, download->url); + +#if LIBCURL_VERSION_NUM >= 0x072000 + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, _xferinfo); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, download); +#else + curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, _older_progress); + curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, download); +#endif + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)outfh); + + curl_easy_setopt(curl, CURLOPT_USERAGENT, "profanity"); + + if (cert_path) { + curl_easy_setopt(curl, CURLOPT_CAPATH, cert_path); + } + + if ((res = curl_easy_perform(curl)) != CURLE_OK) { + err = strdup(curl_easy_strerror(res)); + } + + curl_easy_cleanup(curl); + curl_global_cleanup(); + + if (fclose(outfh) == EOF) { + err = strdup(g_strerror(errno)); + } + + pthread_mutex_lock(&lock); + g_free(cert_path); + if (err) { + if (download->cancel) { + http_print_transfer_update(download->window, download->url, + "Downloading '%s' failed: " + "Download was canceled", + download->url); + } else { + http_print_transfer_update(download->window, download->url, + "Downloading '%s' failed: %s", + download->url, err); + } + free(err); + } else { + if (!download->cancel) { + http_print_transfer_update(download->window, download->url, + "Downloading '%s': done", + download->url); + win_mark_received(download->window, download->url); + } + } + + download_processes = g_slist_remove(download_processes, download); + pthread_mutex_unlock(&lock); + + if (download->cmd_template != NULL) { + gchar** argv = format_call_external_argv(download->cmd_template, + download->url, + download->filename); + + // TODO: Log the error. + if (!call_external(argv, NULL, NULL)) { + http_print_transfer_update(download->window, download->url, + "Downloading '%s' failed: Unable to call " + "command '%s' with file at '%s' (%s).", + download->url, + download->cmd_template, + download->filename, + "TODO: Log the error"); + } + + g_strfreev(argv); + free(download->cmd_template); + } + + free(download->url); + free(download->filename); + free(download); + + return NULL; +} + +void +http_download_cancel_processes(ProfWin* window) +{ + GSList* download_process = download_processes; + while (download_process) { + HTTPDownload* download = download_process->data; + if (download->window == window) { + download->cancel = 1; + break; + } + download_process = g_slist_next(download_process); + } +} + +void +http_download_add_download(HTTPDownload* download) +{ + download_processes = g_slist_append(download_processes, download); +} diff --git a/src/tools/http_download.h b/src/tools/http_download.h new file mode 100644 index 00000000..23344f6c --- /dev/null +++ b/src/tools/http_download.h @@ -0,0 +1,66 @@ +/* + * http_download.h + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2012 - 2019 James Booth + * Copyright (C) 2020 William Wennerström + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see . + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#ifndef TOOLS_HTTP_DOWNLOAD_H +#define TOOLS_HTTP_DOWNLOAD_H + +#ifdef PLATFORM_CYGWIN +#define SOCKET int +#endif + +#include +#include + +#include "ui/win_types.h" +#include "tools/http_common.h" + +typedef struct http_download_t +{ + char* url; + char* filename; + char* cmd_template; + curl_off_t bytes_received; + ProfWin* window; + pthread_t worker; + int cancel; +} HTTPDownload; + +void* http_file_get(void* userdata); + +void http_download_cancel_processes(ProfWin* window); +void http_download_add_download(HTTPDownload* download); + +#endif diff --git a/src/tools/http_upload.c b/src/tools/http_upload.c index 312fad46..fcdd582a 100644 --- a/src/tools/http_upload.c +++ b/src/tools/http_upload.c @@ -128,12 +128,41 @@ _data_callback(void* ptr, size_t size, size_t nmemb, void* data) return realsize; } +int +format_alt_url(char* original_url, char* new_scheme, char* new_fragment, char** new_url) +{ + int ret = 0; + CURLU* h = curl_url(); + + if ((ret = curl_url_set(h, CURLUPART_URL, original_url, 0)) != 0) { + goto out; + } + + if (new_scheme != NULL) { + if ((ret = curl_url_set(h, CURLUPART_SCHEME, new_scheme, CURLU_NON_SUPPORT_SCHEME)) != 0) { + goto out; + } + } + + if (new_fragment != NULL) { + if ((ret = curl_url_set(h, CURLUPART_FRAGMENT, new_fragment, 0)) != 0) { + goto out; + } + } + + ret = curl_url_get(h, CURLUPART_URL, new_url, 0); + +out: + curl_url_cleanup(h); + return ret; +} + void* http_file_put(void* userdata) { HTTPUpload* upload = (HTTPUpload*)userdata; - FILE* fd = NULL; + FILE* fh = NULL; char* err = NULL; char* content_type_header; @@ -149,7 +178,7 @@ http_file_put(void* userdata) if (asprintf(&msg, "Uploading '%s': 0%%", upload->filename) == -1) { msg = strdup(FALLBACK_MSG); } - win_print_http_upload(upload->window, msg, upload->put_url); + win_print_http_transfer(upload->window, msg, upload->put_url); free(msg); char* cert_path = prefs_get_string(PREF_TLS_CERTPATH); @@ -186,18 +215,13 @@ http_file_put(void* userdata) curl_easy_setopt(curl, CURLOPT_USERAGENT, "profanity"); - if (!(fd = fopen(upload->filename, "rb"))) { - if (asprintf(&err, "failed to open '%s'", upload->filename) == -1) { - err = NULL; - } - goto end; - } + fh = upload->filehandle; if (cert_path) { curl_easy_setopt(curl, CURLOPT_CAPATH, cert_path); } - curl_easy_setopt(curl, CURLOPT_READDATA, fd); + curl_easy_setopt(curl, CURLOPT_READDATA, fh); curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)(upload->filesize)); curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); @@ -225,12 +249,11 @@ http_file_put(void* userdata) #endif } -end: curl_easy_cleanup(curl); curl_global_cleanup(); curl_slist_free_all(headers); - if (fd) { - fclose(fd); + if (fh) { + fclose(fh); } free(content_type_header); free(output.buffer); @@ -262,30 +285,42 @@ end: win_mark_received(upload->window, upload->put_url); free(msg); - switch (upload->window->type) { - case WIN_CHAT: - { - ProfChatWin* chatwin = (ProfChatWin*)(upload->window); - assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); - cl_ev_send_msg(chatwin, upload->get_url, upload->get_url); - break; - } - case WIN_PRIVATE: - { - ProfPrivateWin* privatewin = (ProfPrivateWin*)(upload->window); - assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK); - cl_ev_send_priv_msg(privatewin, upload->get_url, upload->get_url); - break; - } - case WIN_MUC: - { - ProfMucWin* mucwin = (ProfMucWin*)(upload->window); - assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); - cl_ev_send_muc_msg(mucwin, upload->get_url, upload->get_url); - break; - } - default: - break; + char* url = NULL; + if (format_alt_url(upload->get_url, upload->alt_scheme, upload->alt_fragment, &url) != 0) { + char* msg; + if (asprintf(&msg, "Uploading '%s' failed: Bad URL ('%s')", upload->filename, upload->get_url) == -1) { + msg = strdup(FALLBACK_MSG); + } + cons_show_error(msg); + free(msg); + } else { + switch (upload->window->type) { + case WIN_CHAT: + { + ProfChatWin* chatwin = (ProfChatWin*)(upload->window); + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + cl_ev_send_msg(chatwin, url, url); + break; + } + case WIN_PRIVATE: + { + ProfPrivateWin* privatewin = (ProfPrivateWin*)(upload->window); + assert(privatewin->memcheck == PROFPRIVATEWIN_MEMCHECK); + cl_ev_send_priv_msg(privatewin, url, url); + break; + } + case WIN_MUC: + { + ProfMucWin* mucwin = (ProfMucWin*)(upload->window); + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + cl_ev_send_muc_msg(mucwin, url, url); + break; + } + default: + break; + } + + curl_free(url); } } } @@ -297,24 +332,26 @@ end: free(upload->mime_type); free(upload->get_url); free(upload->put_url); + free(upload->alt_scheme); + free(upload->alt_fragment); free(upload); return NULL; } char* -file_mime_type(const char* const file_name) +file_mime_type(const char* const filename) { char* out_mime_type; char file_header[FILE_HEADER_BYTES]; - FILE* fd; - if (!(fd = fopen(file_name, "rb"))) { + FILE* fh; + if (!(fh = fopen(filename, "rb"))) { return strdup(FALLBACK_MIMETYPE); } - size_t file_header_size = fread(file_header, 1, FILE_HEADER_BYTES, fd); - fclose(fd); + size_t file_header_size = fread(file_header, 1, FILE_HEADER_BYTES, fh); + fclose(fh); - char* content_type = g_content_type_guess(file_name, (unsigned char*)file_header, file_header_size, NULL); + char* content_type = g_content_type_guess(filename, (unsigned char*)file_header, file_header_size, NULL); if (content_type != NULL) { char* mime_type = g_content_type_get_mime_type(content_type); out_mime_type = strdup(mime_type); @@ -327,10 +364,10 @@ file_mime_type(const char* const file_name) } off_t -file_size(const char* const filename) +file_size(int filedes) { struct stat st; - stat(filename, &st); + fstat(filedes, &st); return st.st_size; } diff --git a/src/tools/http_upload.h b/src/tools/http_upload.h index 3838a5e8..4e95d4d8 100644 --- a/src/tools/http_upload.h +++ b/src/tools/http_upload.h @@ -48,11 +48,14 @@ typedef struct http_upload_t { char* filename; + FILE* filehandle; off_t filesize; curl_off_t bytes_sent; char* mime_type; char* get_url; char* put_url; + char* alt_scheme; + char* alt_fragment; ProfWin* window; pthread_t worker; int cancel; @@ -60,8 +63,8 @@ typedef struct http_upload_t void* http_file_put(void* userdata); -char* file_mime_type(const char* const file_name); -off_t file_size(const char* const file_name); +char* file_mime_type(const char* const filename); +off_t file_size(int filedes); void http_upload_cancel_processes(ProfWin* window); void http_upload_add_upload(HTTPUpload* upload); diff --git a/src/ui/console.c b/src/ui/console.c index cb2bb888..306b13d5 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -2068,17 +2068,20 @@ cons_correction_setting(void) void cons_executable_setting(void) { - char* avatar = prefs_get_string(PREF_AVATAR_CMD); + gchar* avatar = prefs_get_string(PREF_AVATAR_CMD); cons_show("Default '/avatar open' command (/executable avatar) : %s", avatar); g_free(avatar); //TODO: there needs to be a way to get all the "locales"/schemes so we can - //display the defualt openers for all filetypes - gchar** urlopen = prefs_get_string_list_with_option(PREF_URL_OPEN_CMD, ""); - cons_show("Default '/url open' command (/executable urlopen) : %s", urlopen[1]); - g_strfreev(urlopen); + //display the default openers for all filetypes + gchar* urlopen = prefs_get_string(PREF_URL_OPEN_CMD); + cons_show("Default '/url open' command (/executable urlopen) : %s", urlopen); + g_free(urlopen); - char* urlsave = prefs_get_string(PREF_URL_SAVE_CMD); + gchar* urlsave = prefs_get_string(PREF_URL_SAVE_CMD); + if (urlsave == NULL) { + urlsave = g_strdup("(built-in)"); + } cons_show("Default '/url save' command (/executable urlsave) : %s", urlsave); g_free(urlsave); } @@ -2192,12 +2195,6 @@ cons_show_omemo_prefs(void) cons_show("OMEMO char (/omemo char) : %s", ch); free(ch); - if (prefs_get_boolean(PREF_OMEMO_SENDFILE)) { - cons_show("Allow sending unencrypted files in an OMEMO session via /sendfile (/omemo sendfile): ON"); - } else { - cons_show("Allow sending unencrypted files in an OMEMO session via /sendfile (/omemo sendfile): OFF"); - } - cons_alert(NULL); } diff --git a/src/ui/window.c b/src/ui/window.c index 687af3b2..6f77f107 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -1390,7 +1390,7 @@ win_appendln_highlight(ProfWin* window, theme_item_t theme_item, const char* con } void -win_print_http_upload(ProfWin* window, const char* const message, char* url) +win_print_http_transfer(ProfWin* window, const char* const message, char* url) { win_print_outgoing_with_receipt(window, "!", NULL, message, url, NULL); } diff --git a/src/ui/window.h b/src/ui/window.h index c731d19b..7ff25a87 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -72,7 +72,7 @@ void win_println_incoming_muc_msg(ProfWin* window, char* show_char, int flags, c void win_print_outgoing_muc_msg(ProfWin* window, char* show_char, const char* const me, const char* const id, const char* const replace_id, const char* const message); void win_print_history(ProfWin* window, const ProfMessage* const message); -void win_print_http_upload(ProfWin* window, const char* const message, char* url); +void win_print_http_transfer(ProfWin* window, const char* const message, char* url); void win_newline(ProfWin* window); void win_redraw(ProfWin* window); diff --git a/tests/unittests/omemo/stub_omemo.c b/tests/unittests/omemo/stub_omemo.c index 448d5a0d..f6cc4491 100644 --- a/tests/unittests/omemo/stub_omemo.c +++ b/tests/unittests/omemo/stub_omemo.c @@ -107,3 +107,10 @@ void omemo_start_sessions(void) { } + +char* +omemo_encrypt_file(FILE* in, FILE* out, off_t file_size, int* gcry_res) +{ + return NULL; +}; +void omemo_free(void* a){}; diff --git a/tests/unittests/test_common.c b/tests/unittests/test_common.c index b8958dda..fa5dd59e 100644 --- a/tests/unittests/test_common.c +++ b/tests/unittests/test_common.c @@ -330,6 +330,186 @@ strip_quotes_strips_both(void** state) free(result); } +typedef struct +{ + char* template; + char* url; + char* filename; + char* argv; +} format_call_external_argv_t; + +void +format_call_external_argv_td(void** state) +{ + + enum table { num_tests = 4 }; + + format_call_external_argv_t tests[num_tests] = { + (format_call_external_argv_t){ + .template = "/bin/echo %u %p", + .url = "https://example.org", + .filename = "image.jpeg", + .argv = "/bin/echo https://example.org image.jpeg", + }, + (format_call_external_argv_t){ + .template = "/bin/echo %p %u", + .url = "https://example.org", + .filename = "image.jpeg", + .argv = "/bin/echo image.jpeg https://example.org", + }, + (format_call_external_argv_t){ + .template = "/bin/echo %p", + .url = "https://example.org", + .filename = "image.jpeg", + .argv = "/bin/echo image.jpeg", + }, + (format_call_external_argv_t){ + .template = "/bin/echo %u", + .url = "https://example.org", + .filename = "image.jpeg", + .argv = "/bin/echo https://example.org", + }, + }; + + gchar** got_argv = NULL; + gchar* got_argv_str = NULL; + for (int i = 0; i < num_tests; i++) { + got_argv = format_call_external_argv( + tests[i].template, + tests[i].url, + tests[i].filename); + got_argv_str = g_strjoinv(" ", got_argv); + + assert_string_equal(got_argv_str, tests[i].argv); + + g_strfreev(got_argv); + g_free(got_argv_str); + } +} + +typedef struct +{ + char* url; + char* path; + char* target; + char* basename; +} unique_filename_from_url_t; + +void +unique_filename_from_url_td(void** state) +{ + + enum table { num_tests = 15 }; + char* pwd = g_get_current_dir(); + + unique_filename_from_url_t tests[num_tests] = { + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg", + .path = "./.", + .target = pwd, + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg", + .path = NULL, + .target = pwd, + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg#somefragment", + .path = "./", + .target = pwd, + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg?query=param", + .path = "./", + .target = pwd, + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg?query=param&another=one", + .path = "./", + .target = pwd, + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg?query=param&another=one", + .path = "/tmp/", + .target = "/tmp/", + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg?query=param&another=one", + .path = "/tmp/hopefully/this/file/does/not/exist", + .target = "/tmp/hopefully/this/file/does/not/", + .basename = "exist", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/image.jpeg?query=param&another=one", + .path = "/tmp/hopefully/this/file/does/not/exist/", + .target = "/tmp/hopefully/this/file/does/not/exist/", + .basename = "image.jpeg", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/images/", + .path = "./", + .target = pwd, + .basename = "images", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/images/../../file", + .path = "./", + .target = pwd, + .basename = "file", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/images/../../file/..", + .path = "./", + .target = pwd, + .basename = "index", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/images/..//", + .path = "./", + .target = pwd, + .basename = "index", + }, + (unique_filename_from_url_t){ + .url = "https://host.test/", + .path = "./", + .target = pwd, + .basename = "index", + }, + (unique_filename_from_url_t){ + .url = "https://host.test", + .path = "./", + .target = pwd, + .basename = "index", + }, + (unique_filename_from_url_t){ + .url = "aesgcm://host.test", + .path = "./", + .target = pwd, + .basename = "index", + }, + }; + + char* got_filename = NULL; + char* exp_filename = NULL; + for (int i = 0; i < num_tests; i++) { + got_filename = unique_filename_from_url(tests[i].url, tests[i].path); + exp_filename = g_build_filename(tests[i].target, tests[i].basename, NULL); + + assert_string_equal(got_filename, exp_filename); + + free(got_filename); + free(exp_filename); + } + + g_free(pwd); +} + gboolean _lists_equal(GSList* a, GSList* b) { diff --git a/tests/unittests/test_common.h b/tests/unittests/test_common.h index b9e7291e..46d990d9 100644 --- a/tests/unittests/test_common.h +++ b/tests/unittests/test_common.h @@ -31,3 +31,5 @@ void strip_quotes_strips_last(void** state); void strip_quotes_strips_both(void** state); void prof_partial_occurrences_tests(void** state); void prof_whole_occurrences_tests(void** state); +void unique_filename_from_url_td(void** state); +void format_call_external_argv_td(void** state); diff --git a/tests/unittests/tools/stub_aesgcm_download.c b/tests/unittests/tools/stub_aesgcm_download.c new file mode 100644 index 00000000..6f4cc0ce --- /dev/null +++ b/tests/unittests/tools/stub_aesgcm_download.c @@ -0,0 +1,27 @@ +#ifndef TOOLS_AESGCM_DOWNLOAD_H +#define TOOLS_AESGCM_DOWNLOAD_H + +#include + +typedef struct prof_win_t ProfWin; +typedef struct http_download_t HTTPDownload; + +typedef struct aesgcm_download_t +{ + char* url; + char* filename; + ProfWin* window; + pthread_t worker; + HTTPDownload* http_dl; +} AESGCMDownload; + +void* +aesgcm_file_get(void* userdata) +{ + return NULL; +}; + +void aesgcm_download_cancel_processes(ProfWin* window){}; +void aesgcm_download_add_download(AESGCMDownload* download){}; + +#endif diff --git a/tests/unittests/tools/stub_http_download.c b/tests/unittests/tools/stub_http_download.c new file mode 100644 index 00000000..cc7bddc5 --- /dev/null +++ b/tests/unittests/tools/stub_http_download.c @@ -0,0 +1,30 @@ +#ifndef TOOLS_HTTP_DOWNLOAD_H +#define TOOLS_HTTP_DOWNLOAD_H + +#include +#include + +typedef struct prof_win_t ProfWin; + +typedef struct http_download_t +{ + char* url; + char* filename; + char* directory; + FILE* filehandle; + curl_off_t bytes_received; + ProfWin* window; + pthread_t worker; + int cancel; +} HTTPDownload; + +void* +http_file_get(void* userdata) +{ + return NULL; +} + +void http_download_cancel_processes(){}; +void http_download_add_download(){}; + +#endif diff --git a/tests/unittests/tools/stub_http_upload.c b/tests/unittests/tools/stub_http_upload.c index 25a81708..1b79e02d 100644 --- a/tests/unittests/tools/stub_http_upload.c +++ b/tests/unittests/tools/stub_http_upload.c @@ -20,8 +20,6 @@ typedef struct http_upload_t int cancel; } HTTPUpload; -//GSList *upload_processes; - void* http_file_put(void* userdata) { @@ -33,6 +31,7 @@ file_mime_type(const char* const file_name) { return NULL; } + off_t file_size(const char* const file_name) { diff --git a/tests/unittests/ui/stub_ui.c b/tests/unittests/ui/stub_ui.c index 192f39ee..06f0d988 100644 --- a/tests/unittests/ui/stub_ui.c +++ b/tests/unittests/ui/stub_ui.c @@ -471,6 +471,10 @@ mucwin_unset_message_char(ProfMucWin* mucwin) { } +void win_update_entry_message(ProfWin* window, const char* const id, const char* const message){}; +void win_mark_received(ProfWin* window, const char* const id){}; +void win_print_http_transfer(ProfWin* window, const char* const message, char* url){}; + void ui_show_roster(void) { diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 874f6194..7fd3b192 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -90,6 +90,8 @@ main(int argc, char* argv[]) unit_test(strip_quotes_strips_first), unit_test(strip_quotes_strips_last), unit_test(strip_quotes_strips_both), + unit_test(format_call_external_argv_td), + unit_test(unique_filename_from_url_td), unit_test(clear_empty), unit_test(reset_after_create),