Compare commits

..

2 Commits

Author SHA1 Message Date
b7dec705c3 fix(autocomplete): guard history completion safely
All checks were successful
CI Code / Check spelling (pull_request) Successful in 12s
CI Code / Check coding style (pull_request) Successful in 23s
CI Code / Code Coverage (pull_request) Successful in 3m40s
CI Code / Linux (debian) (pull_request) Successful in 5m14s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m20s
CI Code / Linux (arch) (pull_request) Successful in 7m23s
Prevent autocomplete crashes on asset failures when executing history subcommands without an active connection by validating the session state beforehand. Standardize JID resolution across command handlers to ensure consistent contact lookup behavior.

Use auto_gchar gchar instead of GString for performance and readability purposes.
2026-07-11 16:09:36 +00:00
b208e48ee1 feat(cli): resolve contact names to JIDs in multiple commands and improve autocomplete
Introduce _resolve_contact_jid helper to convert contact names to JIDs.
Apply this helper to /caps, /software, /disco, /lastactivity, and /ping
commands, allowing users to specify contacts by name instead of full JIDs.

Enhance command-line autocomplete by adding _disco_autocomplete and
_roster_jid_autocomplete functions. Update /caps autocomplete to utilize
the new roster resolution logic and ensure it only returns results when
connected.
2026-07-11 15:54:32 +00:00
21 changed files with 133 additions and 612 deletions

View File

@@ -1,44 +0,0 @@
name: Publish Docker image
on:
push:
branches: [master]
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v7
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
with:
images: cproofdev/cproof
tags: |
type=raw,value=dev,enable={{is_default_branch}}
type=sha,prefix=nightly-,enable={{is_default_branch}}
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@2bd26e71295ee32cbf6a73510d165bf7232460f3
with:
context: .
file: ./ci/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -236,10 +236,7 @@ omemo_sources = \
src/tools/aesgcm_download.h src/tools/aesgcm_download.c
omemo_unittest_sources = \
tests/unittests/omemo/stub_omemo.c \
tests/unittests/omemo/test_omemo_crypto.c \
tests/unittests/omemo/test_omemo_crypto.h \
src/omemo/crypto.c
tests/unittests/omemo/stub_omemo.c
if BUILD_PYTHON_API
core_sources += $(python_sources)

View File

@@ -1,106 +0,0 @@
# =============================================================================
# Stage 1: Build
# =============================================================================
FROM ubuntu:26.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
autoconf \
automake \
autoconf-archive \
ca-certificates \
libtool \
pkg-config \
git \
wget \
libglib2.0-dev \
libcurl4-openssl-dev \
libsqlite3-dev \
libncurses-dev \
libreadline-dev \
libnotify-dev \
python3-dev \
python3 \
libgpgme-dev \
libotr5-dev \
libgtk-3-dev \
libgdk-pixbuf-2.0-dev \
libsignal-protocol-c-dev \
libgcrypt20-dev \
libqrencode-dev \
libxss-dev \
libcmocka-dev \
&& rm -rf /var/lib/apt/lists/*
# Build libstrophe from source (not available as Ubuntu package)
RUN git clone --depth 1 https://git.jabber.space/devs/libstrophe-gh-mirror.git /tmp/libstrophe \
&& cd /tmp/libstrophe \
&& autoreconf -i \
&& ./configure --prefix=/usr \
&& make -j$(nproc) \
&& make install \
&& rm -rf /tmp/libstrophe
WORKDIR /src
COPY . .
RUN ./bootstrap.sh \
&& ./configure \
--enable-notifications \
--enable-icons-and-clipboard \
--enable-otr \
--enable-pgp \
--enable-omemo \
--enable-plugins \
--enable-c-plugins \
--enable-python-plugins \
--with-xscreensaver \
--enable-omemo-qrcode \
--enable-gdk-pixbuf \
&& make -j$(nproc) \
&& make install
# =============================================================================
# Stage 2: Runtime (minimal)
# =============================================================================
FROM ubuntu:26.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libglib2.0-0t64 \
libcurl4t64 \
libsqlite3-0 \
libncursesw6 \
libreadline8t64 \
libnotify4 \
python3 \
libpython3.14 \
libgpgme45 \
libotr5t64 \
libgtk-3-0t64 \
libgdk-pixbuf-2.0-0 \
libsignal-protocol-c2.3.2 \
libgcrypt20 \
libqrencode4 \
libxss1 \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
COPY --from=builder /usr/local/bin/profanity /usr/local/bin/profanity
COPY --from=builder /usr/local/lib/libprofanity* /usr/local/lib/
COPY --from=builder /usr/local/include/profapi.h /usr/local/include/
COPY --from=builder /usr/local/share/profanity /usr/local/share/profanity
COPY --from=builder /usr/lib/libstrophe.so* /usr/lib/
RUN ldconfig
RUN ldd /usr/local/bin/profanity | grep 'not found' && exit 1 || true
RUN mkdir -p /root/.config/profanity
ENTRYPOINT ["profanity"]

View File

@@ -110,6 +110,8 @@ static char* _software_autocomplete(ProfWin* window, const char* const input, gb
static char* _url_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _executable_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _lastactivity_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _disco_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _roster_jid_autocomplete(const char* const input, const char* const prefix, gboolean previous);
static char* _intype_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _mood_autocomplete(ProfWin* window, const char* const input, gboolean previous);
static char* _strophe_autocomplete(ProfWin* window, const char* const input, gboolean previous);
@@ -122,6 +124,7 @@ static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean
static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
static char* _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previous);
static gboolean _is_connected(void);
static Autocomplete commands_ac;
static Autocomplete who_room_ac;
@@ -1426,6 +1429,7 @@ cmd_ac_init(void)
g_hash_table_insert(ac_funcs, "/color", _color_autocomplete);
g_hash_table_insert(ac_funcs, "/connect", _connect_autocomplete);
g_hash_table_insert(ac_funcs, "/console", _console_autocomplete);
g_hash_table_insert(ac_funcs, "/disco", _disco_autocomplete);
g_hash_table_insert(ac_funcs, "/correct", _correct_autocomplete);
g_hash_table_insert(ac_funcs, "/correction", _correction_autocomplete);
g_hash_table_insert(ac_funcs, "/executable", _executable_autocomplete);
@@ -1862,10 +1866,12 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
}
gchar* history_jid_subcmds[] = { "/history verify", "/history export", "/history import" };
for (size_t i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) {
result = autocomplete_param_with_func(input, history_jid_subcmds[i], roster_barejid_autocomplete, previous, NULL);
if (result) {
return result;
if (conn_status == JABBER_CONNECTED) {
for (size_t i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) {
result = autocomplete_param_with_func(input, history_jid_subcmds[i], roster_barejid_autocomplete, previous, NULL);
if (result) {
return result;
}
}
}
@@ -1941,7 +1947,6 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
Autocomplete completer;
} ac_cmds[] = {
{ "/prefs", prefs_ac },
{ "/disco", disco_ac },
{ "/room", room_ac },
{ "/mainwin", winpos_ac },
{ "/inputwin", winpos_ac },
@@ -1991,6 +1996,31 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
return NULL;
}
static gboolean
_is_connected(void)
{
return connection_get_status() == JABBER_CONNECTED;
}
static char*
_roster_jid_autocomplete(const char* const input, const char* const prefix, gboolean previous)
{
if (!_is_connected()) {
return NULL;
}
char* result = NULL;
result = autocomplete_param_with_func(input, prefix, roster_contact_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, prefix, roster_barejid_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, prefix, roster_fulljid_autocomplete, previous, NULL);
return result;
}
static char*
_caps_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
@@ -2006,20 +2036,29 @@ _caps_autocomplete(ProfWin* window, const char* const input, gboolean previous)
if (nick_ac) {
result = autocomplete_param_with_ac(input, "/caps", nick_ac, TRUE, previous);
}
} else if (connection_get_status() == JABBER_CONNECTED) {
result = autocomplete_param_with_func(input, "/caps", roster_contact_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/caps", roster_barejid_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/caps", roster_fulljid_autocomplete, previous, NULL);
} else {
result = _roster_jid_autocomplete(input, "/caps", previous);
}
return result;
}
static char*
_disco_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
result = autocomplete_param_with_ac(input, "/disco", disco_ac, TRUE, previous);
if (result) {
return result;
}
result = _roster_jid_autocomplete(input, "/disco info", previous);
if (result) {
return result;
}
result = _roster_jid_autocomplete(input, "/disco items", previous);
return result;
}
static char*
_sub_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
@@ -3698,7 +3737,7 @@ _win_autocomplete(ProfWin* window, const char* const input, gboolean previous)
}
char* unquoted = strip_arg_quotes(input);
result = autocomplete_param_with_func(unquoted, "/win", roster_contact_autocomplete, previous, NULL);
result = _roster_jid_autocomplete(input, "/win", previous);
free(unquoted);
return result;
}
@@ -4032,18 +4071,14 @@ _invite_autocomplete(ProfWin* window, const char* const input, gboolean previous
return result;
}
jabber_conn_status_t conn_status = connection_get_status();
result = _roster_jid_autocomplete(input, "/invite send", previous);
if (result) {
return result;
}
if (conn_status == JABBER_CONNECTED) {
result = autocomplete_param_with_func(input, "/invite send", roster_contact_autocomplete, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/invite decline", muc_invites_find, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/invite decline", muc_invites_find, previous, NULL);
if (result) {
return result;
}
return NULL;
@@ -4059,43 +4094,38 @@ _status_autocomplete(ProfWin* window, const char* const input, gboolean previous
return result;
}
jabber_conn_status_t conn_status = connection_get_status();
// complete with: online, away etc.
result = autocomplete_param_with_ac(input, "/status set", account_status_ac, TRUE, previous);
if (result) {
return result;
}
if (conn_status == JABBER_CONNECTED) {
// Remove quote character before and after names when doing autocomplete
char* unquoted = strip_arg_quotes(input);
// complete with: online, away etc.
result = autocomplete_param_with_ac(input, "/status set", account_status_ac, TRUE, previous);
if (result) {
return result;
}
// Remove quote character before and after names when doing autocomplete
char* unquoted = strip_arg_quotes(input);
// MUC completion with nicknames
if (window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)window;
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid);
if (nick_ac) {
result = autocomplete_param_with_ac(unquoted, "/status get", nick_ac, TRUE, previous);
if (result) {
free(unquoted);
return result;
}
}
// roster completion
} else {
result = autocomplete_param_with_func(unquoted, "/status get", roster_contact_autocomplete, previous, NULL);
// MUC completion with nicknames
if (window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)window;
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid);
if (nick_ac) {
result = autocomplete_param_with_ac(unquoted, "/status get", nick_ac, TRUE, previous);
if (result) {
free(unquoted);
return result;
}
}
free(unquoted);
// roster completion
} else {
result = _roster_jid_autocomplete(input, "/status get", previous);
if (result) {
free(unquoted);
return result;
}
}
free(unquoted);
return NULL;
}
@@ -4202,14 +4232,12 @@ _software_autocomplete(ProfWin* window, const char* const input, gboolean previo
{
char* result = NULL;
if (window->type == WIN_CHAT) {
if (window->type == WIN_CHAT && _is_connected()) {
ProfChatWin* chatwin = (ProfChatWin*)window;
assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
GString* search_str = g_string_new("/software ");
g_string_append(search_str, chatwin->barejid);
result = autocomplete_param_with_func(search_str->str, "/software", roster_fulljid_autocomplete, previous, NULL);
g_string_free(search_str, TRUE);
auto_gchar gchar* search_str = g_strdup_printf("/software %s", chatwin->barejid);
result = autocomplete_param_with_func(search_str, "/software", roster_fulljid_autocomplete, previous, NULL);
} else if (window->type == WIN_MUC) {
ProfMucWin* mucwin = (ProfMucWin*)window;
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
@@ -4223,10 +4251,7 @@ _software_autocomplete(ProfWin* window, const char* const input, gboolean previo
}
}
} else {
result = autocomplete_param_with_func(input, "/software", roster_fulljid_autocomplete, previous, NULL);
if (result) {
return result;
}
result = _roster_jid_autocomplete(input, "/software", previous);
}
return result;
@@ -4325,16 +4350,11 @@ _lastactivity_autocomplete(ProfWin* window, const char* const input, gboolean pr
return result;
}
jabber_conn_status_t conn_status = connection_get_status();
if (conn_status == JABBER_CONNECTED) {
result = autocomplete_param_with_func(input, "/lastactivity set", prefs_autocomplete_boolean_choice, previous, NULL);
if (result) {
return result;
}
result = autocomplete_param_with_func(input, "/lastactivity get", roster_barejid_autocomplete, previous, NULL);
result = autocomplete_param_with_func(input, "/lastactivity set", prefs_autocomplete_boolean_choice, previous, NULL);
if (result) {
return result;
}
result = _roster_jid_autocomplete(input, "/lastactivity get", previous);
return result;
}
@@ -4514,19 +4534,19 @@ _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous)
} else {
char* unquoted = strip_arg_quotes(input);
result = autocomplete_param_with_func(unquoted, "/vcard get", roster_contact_autocomplete, previous, NULL);
result = _roster_jid_autocomplete(input, "/vcard get", previous);
if (result) {
free(unquoted);
return result;
}
result = autocomplete_param_with_func(unquoted, "/vcard photo open", roster_contact_autocomplete, previous, NULL);
result = _roster_jid_autocomplete(input, "/vcard photo open", previous);
if (result) {
free(unquoted);
return result;
}
result = autocomplete_param_with_func(unquoted, "/vcard photo save", roster_contact_autocomplete, previous, NULL);
result = _roster_jid_autocomplete(input, "/vcard photo save", previous);
if (result) {
free(unquoted);
return result;

View File

@@ -110,6 +110,15 @@ static gboolean _cmd_execute_default(ProfWin* window, const char* inp);
static gboolean _cmd_execute_alias(ProfWin* window, const char* const inp, gboolean* ran);
static gboolean
_download_install_plugin(ProfWin* window, gchar* url, gchar* path);
static const gchar*
_resolve_contact_jid(const char* const name)
{
char* barejid = roster_barejid_from_name(name);
if (barejid == NULL) {
barejid = (char*)name;
}
return barejid;
}
static void
_vcard_editor_finished_cb(gchar* message, void* user_data)
@@ -3414,7 +3423,7 @@ cmd_caps(ProfWin* window, const char* const command, gchar** args)
case WIN_CONSOLE:
case WIN_XML:
if (args[0]) {
auto_jid Jid* jid = jid_create(args[0]);
auto_jid Jid* jid = jid_create(_resolve_contact_jid(args[0]));
if (jid == NULL || jid->fulljid == NULL) {
cons_show("You must provide a full jid to the /caps command.");
@@ -3456,7 +3465,7 @@ cmd_caps(ProfWin* window, const char* const command, gchar** args)
}
static void
_send_software_version_iq_to_fulljid(char* request)
_send_software_version_iq_to_fulljid(const gchar* const request)
{
auto_jid Jid* jid = jid_create(request);
@@ -3497,7 +3506,7 @@ cmd_software(ProfWin* window, const char* const command, gchar** args)
break;
case WIN_CHAT:
if (args[0]) {
_send_software_version_iq_to_fulljid(args[0]);
_send_software_version_iq_to_fulljid(_resolve_contact_jid(args[0]));
break;
} else {
ProfChatWin* chatwin = (ProfChatWin*)window;
@@ -3521,7 +3530,7 @@ cmd_software(ProfWin* window, const char* const command, gchar** args)
}
case WIN_CONSOLE:
if (args[0]) {
_send_software_version_iq_to_fulljid(args[0]);
_send_software_version_iq_to_fulljid(_resolve_contact_jid(args[0]));
} else {
cons_show("You must provide a jid to the /software command.");
}
@@ -4831,7 +4840,7 @@ cmd_disco(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
auto_gchar gchar* jid = g_strdup_printf("%s", args[1] ?: connection_get_jid()->domainpart);
auto_gchar gchar* jid = g_strdup_printf("%s", args[1] ? _resolve_contact_jid(args[1]) : connection_get_jid()->domainpart);
if (g_strcmp0(args[0], "info") == 0) {
iq_disco_info_request(jid);
@@ -5053,7 +5062,7 @@ cmd_lastactivity(ProfWin* window, const char* const command, gchar** args)
if (args[1] == NULL) {
iq_last_activity_request(connection_get_jid()->domainpart);
} else {
iq_last_activity_request(args[1]);
iq_last_activity_request(_resolve_contact_jid(args[1]));
}
return TRUE;
}
@@ -6406,17 +6415,17 @@ cmd_ping(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
if (args[0] != NULL && caps_jid_has_feature(args[0], XMPP_FEATURE_PING) == FALSE) {
cons_show("%s does not support ping requests.", args[0]);
return TRUE;
}
iq_send_ping(args[0]);
if (args[0] == NULL) {
cons_show("Pinged server…");
if (args[0] != NULL) {
const gchar* ping_target = _resolve_contact_jid(args[0]);
if (caps_jid_has_feature(ping_target, XMPP_FEATURE_PING) == FALSE) {
cons_show("%s does not support ping requests.", ping_target);
return TRUE;
}
iq_send_ping(ping_target);
cons_show("Pinged %s…", ping_target);
} else {
cons_show("Pinged %s…", args[0]);
iq_send_ping(NULL);
cons_show("Pinged server…");
}
return TRUE;
}

View File

@@ -455,32 +455,6 @@ str_xml_sanitize(const char* const str)
return g_string_free(sanitized, FALSE);
}
gchar*
redact_secrets(const char* const str)
{
if (str == NULL) {
return NULL;
}
// SASL exchanges and <password> elements carry credentials — strip their content before logging
static gsize init = 0;
static GRegex* secret_regex = NULL;
if (g_once_init_enter(&init)) {
secret_regex = g_regex_new(
"(<(?:auth|response|challenge|success|password|digest)\\b[^>]*>)[^<]+(</(?:auth|response|challenge|success|password|digest)>)",
0, 0, NULL);
g_once_init_leave(&init, 1);
}
if (secret_regex == NULL) {
return g_strdup(str);
}
auto_gchar gchar* valid = g_utf8_make_valid(str, -1); // invalid UTF-8 would make the regex fail open
gchar* redacted = g_regex_replace(secret_regex, valid, -1, 0, "\\1[REDACTED]\\2", 0, NULL);
return redacted ? redacted : g_steal_pointer(&valid);
}
char*
release_get_latest(void)
{

View File

@@ -161,7 +161,6 @@ gboolean strtoi_range(const char* str, int* saveptr, int min, int max, char** er
gsize g_diff_to_gsize(const void* end, const void* start);
int utf8_display_len(const char* const str);
gchar* str_xml_sanitize(const char* const str);
gchar* redact_secrets(const char* const str);
gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...) __attribute__((sentinel));
gboolean valid_tls_policy_option(const char* is);

View File

@@ -39,7 +39,6 @@
#include <sys/statvfs.h>
#include <sqlite3.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -125,15 +124,6 @@ _get_db_filename(ProfAccount* account)
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
}
static int
_quick_check_cb(void* intact, int argc, char** argv, char** column_names)
{
if (argc > 0 && argv[0] && strcmp(argv[0], "ok") == 0) {
*(gboolean*)intact = TRUE;
}
return 0;
}
static gboolean
_sqlite_init(ProfAccount* account)
{
@@ -159,20 +149,6 @@ _sqlite_init(ProfAccount* account)
return FALSE;
}
g_chmod(filename, S_IRUSR | S_IWUSR); // history holds plaintext; journal/WAL files inherit these perms
// catch corruption before running queries or migrations against the file
char* check_err = NULL;
gboolean intact = FALSE;
ret = sqlite3_exec(g_chatlog_database, "PRAGMA quick_check(1);", _quick_check_cb, &intact, &check_err);
if (ret != SQLITE_OK || !intact) {
log_error("Chat history database failed integrity check (%s): %s", filename,
check_err ? check_err : "quick_check did not return 'ok'");
sqlite3_free(check_err);
_db_teardown("_sqlite_init(quick_check)");
return FALSE;
}
char* err_msg = NULL;
int db_version = _get_db_version();
@@ -715,7 +691,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
original_message_id = tmp ? tmp : original_message_id;
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s.", from_jid->barejid, from_jid_orig, message->replace_id);
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
sqlite3_finalize(lmc_stmt);
return;
@@ -738,7 +714,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
sqlite3_stmt* stmt;
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s", message->id, message->stanzaid, from_jid->barejid);
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
}
sqlite3_finalize(stmt);
@@ -769,7 +745,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
return;
}
log_debug("Writing message to DB (id: %s, stanza_id: %s, type: %s)", message->id, message->stanzaid, type); // no query text: it embeds the plaintext body
log_debug("Writing to DB. Query: %s", query);
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
if (err_msg) {
@@ -781,7 +757,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
} else {
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
if (inserted_rows_count < 1) {
log_error("SQLite did not insert message (rows: %d, id: %s)", inserted_rows_count, message->id);
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
}
}
}

View File

@@ -83,7 +83,6 @@ sv_ev_login_account_success(char* account_name, gboolean secured)
if (!log_database_init(account)) {
log_error("Failed to initialize database for account: %s", account->jid);
cons_show_error("Chat history storage is unavailable for this session, messages will not be saved. See the log for details.");
}
vcard_user_refresh();
avatar_pep_subscribe();

View File

@@ -286,8 +286,7 @@ log_stderr_handler(void)
for (int i = 0; i < size; ++i) {
if (buf[i] == '\n') {
auto_gchar gchar* redacted = redact_secrets(s->str); // third-party libs may echo credentials
log_msg(stderr_level, "stderr", redacted);
log_msg(stderr_level, "stderr", s->str);
g_string_assign(s, "");
} else
g_string_append_c(s, buf[i]);
@@ -295,8 +294,7 @@ log_stderr_handler(void)
} while (1);
if (s->len > 0 && s->str[0] != '\0') {
auto_gchar gchar* redacted = redact_secrets(s->str);
log_msg(stderr_level, "stderr", redacted);
log_msg(stderr_level, "stderr", s->str);
g_string_assign(s, "");
}
}

View File

@@ -14,8 +14,6 @@
#include <libotr/message.h>
#include <libotr/sm.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <sys/stat.h>
#include "log.h"
#include "chatlog.h"
@@ -116,8 +114,6 @@ cb_write_fingerprints(void* opdata)
if (err != GPG_ERR_NO_ERROR) {
log_error("Failed to write fingerprints file");
cons_show_error("Failed to write fingerprints file");
} else {
g_chmod(fpsfilename, S_IRUSR | S_IWUSR);
}
}
@@ -380,7 +376,6 @@ otr_keygen(ProfAccount* account)
cons_show_error("Failed to generate private key");
return;
}
g_chmod(keysfilename->str, S_IRUSR | S_IWUSR);
log_info("Private key generated");
cons_show("");
cons_show("Private key generation complete.");
@@ -395,7 +390,6 @@ otr_keygen(ProfAccount* account)
cons_show_error("Failed to create fingerprints file");
return;
}
g_chmod(fpsfilename->str, S_IRUSR | S_IWUSR);
log_info("Fingerprints file created");
err = otrl_privkey_read(user_state, keysfilename->str);

View File

@@ -13,12 +13,10 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <curl/curl.h>
#include <gio/gio.h>
#include <glib/gstdio.h>
#include <pthread.h>
#include <assert.h>
#include <errno.h>
@@ -64,28 +62,14 @@ aesgcm_file_get(void* userdata)
return NULL;
}
// Decrypt into a temporary file next to the target and rename it into
// place only after the GCM tag verified, so tampered or truncated
// content never appears at the destination path.
auto_gchar gchar* partname = g_strdup_printf("%s.part.XXXXXX", aesgcm_dl->filename);
gint outfd = g_mkstemp(partname);
if (outfd == -1) {
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
"Downloading '%s' failed: Unable to open "
"output file at '%s' for writing (%s).",
https_url, aesgcm_dl->filename,
g_strerror(errno));
return NULL;
}
FILE* outfh = fdopen(outfd, "wb");
// Open the target file for storing the cleartext.
auto_FILE FILE* outfh = fopen(aesgcm_dl->filename, "wb");
if (outfh == NULL) {
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
"Downloading '%s' failed: Unable to open "
"output file at '%s' for writing (%s).",
https_url, aesgcm_dl->filename,
g_strerror(errno));
close(outfd);
remove(partname);
return NULL;
}
@@ -106,8 +90,6 @@ aesgcm_file_get(void* userdata)
ssize_t* p_bytes_received = http_file_get(http_dl);
if (!p_bytes_received) {
fclose(outfh);
remove(partname);
return NULL;
}
ssize_t bytes_received = *p_bytes_received;
@@ -120,8 +102,6 @@ aesgcm_file_get(void* userdata)
"temporary file at '%s' for reading (%s).",
aesgcm_dl->url, tmpname,
g_strerror(errno));
fclose(outfh);
remove(partname);
return NULL;
}
@@ -130,32 +110,20 @@ aesgcm_file_get(void* userdata)
bytes_received, fragment);
fclose(tmpfh);
remove(tmpname);
fclose(outfh);
gboolean saved = FALSE;
if (crypt_res != GPG_ERR_NO_ERROR) {
remove(partname);
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
"Downloading '%s' failed: Failed to decrypt "
"file (%s).",
https_url, gcry_strerror(crypt_res));
} else if (g_rename(partname, aesgcm_dl->filename) != 0) {
remove(partname);
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
"Downloading '%s' failed: Unable to move "
"decrypted file to '%s' (%s).",
https_url, aesgcm_dl->filename,
g_strerror(errno));
} else {
saved = TRUE;
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ONLINE, ENTRY_COMPLETED,
"Downloading '%s': done\nSaved to '%s'",
aesgcm_dl->url, aesgcm_dl->filename);
win_mark_received(aesgcm_dl->window, aesgcm_dl->id);
}
// never hand an unverified file to the external command
if (saved && aesgcm_dl->cmd_template != NULL) {
if (aesgcm_dl->cmd_template != NULL) {
gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template,
aesgcm_dl->filename,
aesgcm_dl->filename);
@@ -172,8 +140,8 @@ aesgcm_file_get(void* userdata)
}
g_strfreev(argv);
free(aesgcm_dl->cmd_template);
}
free(aesgcm_dl->cmd_template);
free(aesgcm_dl->id);
free(aesgcm_dl->filename);

View File

@@ -1104,8 +1104,7 @@ _xmpp_file_logger(void* const userdata, const xmpp_log_level_t xmpp_level, const
break;
}
auto_gchar gchar* redacted = redact_secrets(msg); // raw traffic contains SASL/register credentials
log_msg(prof_level, area, redacted);
log_msg(prof_level, area, msg);
if ((g_strcmp0(area, "xmpp") == 0) || (g_strcmp0(area, "conn")) == 0) {
sv_ev_xmpp_stanza(msg);

View File

@@ -255,9 +255,6 @@ main(int argc, char* argv[])
PROF_FUNC_TEST(message_db_history_verify),
PROF_FUNC_TEST(message_db_history_lmc),
PROF_FUNC_TEST(message_db_history_multi_resource),
#ifdef HAVE_SQLITE
PROF_FUNC_TEST(message_db_corrupt_database_degrades_gracefully),
#endif
/* Basic message send/receive */
PROF_FUNC_TEST(message_send),

View File

@@ -536,41 +536,3 @@ message_db_history_multi_resource(void** state)
assert_true(prof_output_regex("Buddy1/laptop"));
assert_true(prof_output_regex("Buddy1/tablet"));
}
/*
* Test: corrupt chatlog.db degrades gracefully (issue #146, REQ-RES-02).
*
* A chatlog.db with a valid SQLite magic but garbage content is planted
* before connecting. Database init must fail cleanly: the user gets a
* console warning, the session stays up, and the client stays responsive.
*/
void
message_db_corrupt_database_degrades_gracefully(void** state)
{
const char* xdg_data = getenv("XDG_DATA_HOME");
assert_non_null(xdg_data);
GString* db_file = g_string_new(xdg_data);
g_string_append(db_file, "/profanity/database/stabber_at_localhost");
assert_int_equal(0, g_mkdir_with_parents(db_file->str, 0700));
g_string_append(db_file, "/chatlog.db");
/* valid 16-byte SQLite header magic followed by garbage: sqlite3_open
* succeeds (lazy open), the integrity gate must catch it */
FILE* db = fopen(db_file->str, "wb");
assert_non_null(db);
assert_int_equal(16, fwrite("SQLite format 3", 1, 16, db));
for (int i = 0; i < 4096; i++) {
fputc(0xA5, db);
}
fclose(db);
g_string_free(db_file, TRUE);
prof_connect();
assert_true(prof_output_exact("Chat history storage is unavailable for this session"));
/* client is still alive and responsive after the failed DB init */
prof_input("/autoping set 60");
assert_true(prof_output_exact("Autoping interval set to 60 seconds."));
}

View File

@@ -11,4 +11,3 @@ void message_db_history_service_chars(void** state);
void message_db_history_verify(void** state);
void message_db_history_lmc(void** state);
void message_db_history_multi_resource(void** state);
void message_db_corrupt_database_degrades_gracefully(void** state);

View File

@@ -1,152 +0,0 @@
/*
* test_omemo_crypto.c
*
* Unit tests for the OMEMO AES-256-GCM file crypto (src/omemo/crypto.c).
* The decrypt direction streams plaintext before the tag is checked, so
* callers rely on the returned error code to discard unverified output —
* these tests pin that contract (issue #146, REQ-CRY-06).
*/
#include "config.h"
#include <glib.h>
#include <stdio.h>
#include <string.h>
#include "prof_cmocka.h"
#ifdef HAVE_OMEMO
#include "omemo/omemo.h"
#include "omemo/crypto.h"
#define TAG_LENGTH 16
static const unsigned char PLAINTEXT[] = "at-rest integrity check payload: 0123456789abcdef";
// gcrypt secure memory must be set up exactly once per process
static int
_crypto_init_once(void)
{
static gboolean done = FALSE;
static int rc = 0;
if (!done) {
rc = omemo_crypto_init();
done = TRUE;
}
return rc;
}
static off_t
_file_size(FILE* fh)
{
fseeko(fh, 0, SEEK_END);
off_t size = ftello(fh);
rewind(fh);
return size;
}
// encrypt PLAINTEXT with a fixed key/nonce into a fresh tmpfile
static FILE*
_encrypted_tmpfile(unsigned char* key, unsigned char* nonce)
{
memset(key, 0x42, OMEMO_AESGCM_KEY_LENGTH);
memset(nonce, 0x24, OMEMO_AESGCM_NONCE_LENGTH);
FILE* plain = tmpfile();
FILE* cipher = tmpfile();
assert_non_null(plain);
assert_non_null(cipher);
assert_int_equal(sizeof(PLAINTEXT), fwrite(PLAINTEXT, 1, sizeof(PLAINTEXT), plain));
rewind(plain);
assert_int_equal(GPG_ERR_NO_ERROR,
aes256gcm_crypt_file(plain, cipher, (off_t)sizeof(PLAINTEXT), key, nonce, TRUE));
fclose(plain);
rewind(cipher);
return cipher;
}
// corrupt one byte at offset (negative counts from the end), return reopened stream
static FILE*
_flip_byte(FILE* cipher, long offset)
{
off_t size = _file_size(cipher);
unsigned char* buf = g_malloc(size);
assert_int_equal(size, fread(buf, 1, size, cipher));
fclose(cipher);
long pos = offset >= 0 ? offset : (long)size + offset;
buf[pos] ^= 0xFF;
FILE* tampered = tmpfile();
assert_non_null(tampered);
assert_int_equal(size, fwrite(buf, 1, size, tampered));
rewind(tampered);
g_free(buf);
return tampered;
}
void
aes256gcm_crypt_file__roundtrip_succeeds(void** state)
{
assert_int_equal(0, _crypto_init_once());
unsigned char key[OMEMO_AESGCM_KEY_LENGTH];
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
FILE* cipher = _encrypted_tmpfile(key, nonce);
off_t cipher_size = _file_size(cipher);
assert_int_equal((off_t)sizeof(PLAINTEXT) + TAG_LENGTH, cipher_size);
FILE* decrypted = tmpfile();
assert_non_null(decrypted);
assert_int_equal(GPG_ERR_NO_ERROR,
aes256gcm_crypt_file(cipher, decrypted, cipher_size, key, nonce, FALSE));
unsigned char readback[sizeof(PLAINTEXT)];
rewind(decrypted);
assert_int_equal(sizeof(PLAINTEXT), fread(readback, 1, sizeof(readback), decrypted));
assert_memory_equal(PLAINTEXT, readback, sizeof(PLAINTEXT));
fclose(cipher);
fclose(decrypted);
}
void
aes256gcm_crypt_file__rejects_tampered_tag(void** state)
{
assert_int_equal(0, _crypto_init_once());
unsigned char key[OMEMO_AESGCM_KEY_LENGTH];
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
FILE* cipher = _flip_byte(_encrypted_tmpfile(key, nonce), -1); // last tag byte
FILE* decrypted = tmpfile();
assert_non_null(decrypted);
gcry_error_t res = aes256gcm_crypt_file(cipher, decrypted, _file_size(cipher), key, nonce, FALSE);
assert_int_not_equal(GPG_ERR_NO_ERROR, res);
fclose(cipher);
fclose(decrypted);
}
void
aes256gcm_crypt_file__rejects_tampered_ciphertext(void** state)
{
assert_int_equal(0, _crypto_init_once());
unsigned char key[OMEMO_AESGCM_KEY_LENGTH];
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
FILE* cipher = _flip_byte(_encrypted_tmpfile(key, nonce), 0); // first payload byte
FILE* decrypted = tmpfile();
assert_non_null(decrypted);
gcry_error_t res = aes256gcm_crypt_file(cipher, decrypted, _file_size(cipher), key, nonce, FALSE);
assert_int_not_equal(GPG_ERR_NO_ERROR, res);
fclose(cipher);
fclose(decrypted);
}
#endif

View File

@@ -1,8 +0,0 @@
/* test_omemo_crypto.h
*
* Unit tests for OMEMO AES-256-GCM file crypto (issue #146, REQ-CRY-06)
*/
void aes256gcm_crypt_file__roundtrip_succeeds(void** state);
void aes256gcm_crypt_file__rejects_tampered_tag(void** state);
void aes256gcm_crypt_file__rejects_tampered_ciphertext(void** state);

View File

@@ -1384,55 +1384,3 @@ str_xml_sanitize__strips_illegal_characters(void** state)
assert_string_equal("UTF-8: üñîçøðé and more", res5);
g_free(res5);
}
void
redact_secrets__masks_credentials(void** state)
{
// NULL input
assert_null(redact_secrets(NULL));
// Plain text and non-secret XML pass through unchanged
gchar* res1 = redact_secrets("hello world");
assert_string_equal("hello world", res1);
g_free(res1);
gchar* res2 = redact_secrets("<message><body>secret-looking text</body></message>");
assert_string_equal("<message><body>secret-looking text</body></message>", res2);
g_free(res2);
// SASL auth payload is redacted, envelope kept
gchar* res3 = redact_secrets("SENT: <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AGFsaWNlAHBhc3N3b3Jk</auth>");
assert_string_equal("SENT: <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>[REDACTED]</auth>", res3);
g_free(res3);
// SASL challenge/response round-trip
gchar* res4 = redact_secrets("<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cj1abc</challenge>");
assert_string_equal("<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>[REDACTED]</challenge>", res4);
g_free(res4);
gchar* res5 = redact_secrets("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>Yz1iaXdz</response>");
assert_string_equal("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>[REDACTED]</response>", res5);
g_free(res5);
// Empty SASL response element has no content to redact
gchar* res6 = redact_secrets("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
assert_string_equal("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>", res6);
g_free(res6);
// XEP-0077 registration: password redacted, username kept
gchar* res7 = redact_secrets("<query xmlns='jabber:iq:register'><username>alice</username><password>hunter2</password></query>");
assert_string_equal("<query xmlns='jabber:iq:register'><username>alice</username><password>[REDACTED]</password></query>", res7);
g_free(res7);
// XEP-0078 legacy auth: password-derived digest redacted
gchar* res8 = redact_secrets("<query xmlns='jabber:iq:auth'><username>alice</username><digest>48fc78be9ec8f86d8ce1c39ebd7a5b4c9d0e2f13</digest><resource>tui</resource></query>");
assert_string_equal("<query xmlns='jabber:iq:auth'><username>alice</username><digest>[REDACTED]</digest><resource>tui</resource></query>", res8);
g_free(res8);
// invalid UTF-8 must not make redaction fail open
gchar* res9 = redact_secrets("\xFF garbage <password>hunter2</password>");
assert_non_null(res9);
assert_null(strstr(res9, "hunter2"));
assert_non_null(strstr(res9, "[REDACTED]"));
g_free(res9);
}

View File

@@ -64,6 +64,5 @@ void valid_tls_policy_option__is__correct_for_various_inputs(void** state);
void get_mentions__tests__various(void** state);
void release_is_new__tests__various(void** state);
void str_xml_sanitize__strips_illegal_characters(void** state);
void redact_secrets__masks_credentials(void** state);
#endif

View File

@@ -48,7 +48,6 @@
#include "test_ai_client.h"
#include "test_database_export.h"
#include "test_database_stress.h"
#include "omemo/test_omemo_crypto.h"
#define muc_unit_test(f) cmocka_unit_test_setup_teardown(f, muc_before_test, muc_after_test)
@@ -688,12 +687,6 @@ main(int argc, char* argv[])
cmocka_unit_test(get_mentions__tests__various),
cmocka_unit_test(release_is_new__tests__various),
cmocka_unit_test(str_xml_sanitize__strips_illegal_characters),
cmocka_unit_test(redact_secrets__masks_credentials),
#ifdef HAVE_OMEMO
cmocka_unit_test(aes256gcm_crypt_file__roundtrip_succeeds),
cmocka_unit_test(aes256gcm_crypt_file__rejects_tampered_tag),
cmocka_unit_test(aes256gcm_crypt_file__rejects_tampered_ciphertext),
#endif
cmocka_unit_test_setup_teardown(plugins_get_command_names__returns__no_commands,
load_preferences,