fix: harden flatfile backend, make SQLite optional
Build system:
- Add --without-sqlite configure flag (AM_CONDITIONAL BUILD_SQLITE)
- Guard database_sqlite.c with HAVE_SQLITE in Makefile.am, database.c,
database.h and stub_database.c
- Fall back to flatfile when SQLite not compiled in
Security (database_flatfile.c):
- MAM dedup: skip incoming messages with duplicate stanza-id (archive_id)
- LMC sender validation: reject corrections from mismatched JIDs
- Add flock() advisory locking to prevent interleaved writes from
concurrent instances
- Check fprintf return when writing file header
Autocomplete (cmd_ac.c):
- Add 'flatfile' to /privacy logging autocomplete
- Add dedicated /history autocomplete with on/off/verify (was boolean-only)
Code quality:
- Fix fwrite return type: size_t not ssize_t (database_flatfile_parser.c)
- Fix mixed allocators in ff_readline: use malloc() consistently for
overlength-line fallback instead of g_strdup()
- Fix index alignment in _ff_state_extend: use local counter so index
step doesn't depend on total_lines from initial build
Documentation:
- Fix page: correct directory structure (history.log not per-day files)
- Fix page: add aid:{archive_id} to line format example
- Fix page: missing newline before .SH BUGS
This commit is contained in:
@@ -3,7 +3,6 @@ core_sources = \
|
||||
src/log.c src/common.c \
|
||||
src/chatlog.c src/chatlog.h \
|
||||
src/database.h src/database.c \
|
||||
src/database_sqlite.c \
|
||||
src/database_flatfile.c src/database_flatfile.h \
|
||||
src/database_flatfile_parser.c \
|
||||
src/database_flatfile_verify.c \
|
||||
@@ -261,6 +260,12 @@ core_sources += $(omemo_sources)
|
||||
unittest_sources += $(omemo_unittest_sources)
|
||||
endif
|
||||
|
||||
sqlite_sources = src/database_sqlite.c
|
||||
|
||||
if BUILD_SQLITE
|
||||
core_sources += $(sqlite_sources)
|
||||
endif
|
||||
|
||||
all_c_sources = $(core_sources) $(unittest_sources) \
|
||||
$(pgp_sources) $(pgp_unittest_sources) \
|
||||
$(otr4_sources) $(otr_unittest_sources) \
|
||||
|
||||
14
configure.ac
14
configure.ac
@@ -94,8 +94,17 @@ PKG_CHECK_MODULES([curl], [libcurl >= 7.62.0], [],
|
||||
[AC_CHECK_LIB([curl], [main], [],
|
||||
[AC_MSG_ERROR([libcurl 7.62.0 or higher is required])])])
|
||||
|
||||
PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [],
|
||||
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required])])
|
||||
### sqlite (optional — can be disabled with --without-sqlite)
|
||||
AC_ARG_WITH([sqlite],
|
||||
[AS_HELP_STRING([--without-sqlite], [build without SQLite support (flat-file backend only)])],
|
||||
[], [with_sqlite=yes])
|
||||
|
||||
AS_IF([test "x$with_sqlite" != "xno"],
|
||||
[PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0],
|
||||
[AC_DEFINE([HAVE_SQLITE], [1], [SQLite support])],
|
||||
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required (use --without-sqlite to disable)])])],
|
||||
[AC_MSG_NOTICE([Building without SQLite — flat-file backend only])])
|
||||
AM_CONDITIONAL([BUILD_SQLITE], [test "x$with_sqlite" != "xno"])
|
||||
|
||||
ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])])
|
||||
AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ])
|
||||
@@ -416,6 +425,7 @@ AC_OUTPUT
|
||||
AC_MSG_NOTICE([Summary of build options:
|
||||
PLATFORM : $target_os
|
||||
PACKAGE_STATUS : $PACKAGE_STATUS
|
||||
SQLite support : $with_sqlite
|
||||
GTK_VERSION : $GTK_VERSION
|
||||
LIBS : $LIBS
|
||||
Install themes : $THEMES_INSTALL
|
||||
|
||||
@@ -215,17 +215,18 @@ Or use the command:
|
||||
Flat-file logs are stored under
|
||||
.IR $XDG_DATA_HOME/profanity/flatlog/ ,
|
||||
organized as
|
||||
.IR {account_jid}/{contact_jid}/{YYYY_MM_DD}.log .
|
||||
.IR {account_jid}/{contact_jid}/history.log .
|
||||
.PP
|
||||
Each line has the format:
|
||||
.br
|
||||
.EX
|
||||
{ISO8601} [{type}|{enc}|id:{id}|corrects:{id}] {sender}: {message}
|
||||
{ISO8601} [{type}|{enc}|id:{id}|aid:{archive_id}|corrects:{id}] {sender}: {message}
|
||||
.EE
|
||||
.PP
|
||||
Use
|
||||
.B /history verify
|
||||
to check integrity of stored history (both SQLite and flat-file)..SH BUGS
|
||||
to check integrity of stored history (both SQLite and flat-file).
|
||||
.SH BUGS
|
||||
Bugs can either be reported by raising an issue at the Github issue tracker:
|
||||
.br
|
||||
.PP
|
||||
|
||||
@@ -275,6 +275,7 @@ static Autocomplete logging_ac;
|
||||
static Autocomplete logging_group_ac;
|
||||
static Autocomplete privacy_ac;
|
||||
static Autocomplete privacy_log_ac;
|
||||
static Autocomplete history_ac;
|
||||
static Autocomplete color_ac;
|
||||
static Autocomplete correction_ac;
|
||||
static Autocomplete avatar_ac;
|
||||
@@ -429,6 +430,7 @@ static Autocomplete* all_acs[] = {
|
||||
&logging_group_ac,
|
||||
&privacy_ac,
|
||||
&privacy_log_ac,
|
||||
&history_ac,
|
||||
&color_ac,
|
||||
&correction_ac,
|
||||
&avatar_ac,
|
||||
@@ -1138,6 +1140,11 @@ cmd_ac_init(void)
|
||||
autocomplete_add(privacy_log_ac, "on");
|
||||
autocomplete_add(privacy_log_ac, "off");
|
||||
autocomplete_add(privacy_log_ac, "redact");
|
||||
autocomplete_add(privacy_log_ac, "flatfile");
|
||||
|
||||
autocomplete_add(history_ac, "on");
|
||||
autocomplete_add(history_ac, "off");
|
||||
autocomplete_add(history_ac, "verify");
|
||||
|
||||
autocomplete_add(logging_group_ac, "on");
|
||||
autocomplete_add(logging_group_ac, "off");
|
||||
@@ -1776,7 +1783,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
|
||||
// autocomplete boolean settings
|
||||
gchar* boolean_choices[] = { "/beep", "/states", "/outtype", "/flash", "/splash",
|
||||
"/history", "/vercheck", "/privileges", "/wrap",
|
||||
"/vercheck", "/privileges", "/wrap",
|
||||
"/carbons", "/slashguard", "/mam", "/silence" };
|
||||
|
||||
for (int i = 0; i < ARRAY_SIZE(boolean_choices); i++) {
|
||||
@@ -1786,6 +1793,11 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
}
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_ac(input, "/history", history_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// autocomplete nickname in chat rooms
|
||||
if (window->type == WIN_MUC) {
|
||||
ProfMucWin* mucwin = (ProfMucWin*)window;
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
#include "log.h"
|
||||
#include "common.h"
|
||||
#include "config.h"
|
||||
#include "database.h"
|
||||
#include "config/preferences.h"
|
||||
#include "ui/ui.h"
|
||||
@@ -69,8 +70,13 @@ log_database_init(ProfAccount* account)
|
||||
active_db_backend = db_backend_flatfile();
|
||||
log_info("Using flat-file database backend");
|
||||
} else {
|
||||
#ifdef HAVE_SQLITE
|
||||
active_db_backend = db_backend_sqlite();
|
||||
log_info("Using SQLite database backend");
|
||||
#else
|
||||
log_info("SQLite not compiled in, falling back to flat-file backend");
|
||||
active_db_backend = db_backend_flatfile();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!active_db_backend) {
|
||||
|
||||
@@ -85,7 +85,9 @@ typedef struct db_backend_t
|
||||
extern db_backend_t* active_db_backend;
|
||||
|
||||
// Backend registry
|
||||
#ifdef HAVE_SQLITE
|
||||
db_backend_t* db_backend_sqlite(void);
|
||||
#endif
|
||||
db_backend_t* db_backend_flatfile(void);
|
||||
|
||||
// Public API (dispatches to active_db_backend)
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/file.h>
|
||||
#include <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -180,7 +181,9 @@ _ff_state_extend(ff_contact_state_t* state)
|
||||
}
|
||||
}
|
||||
|
||||
size_t msg_count = state->total_lines;
|
||||
// Use a local counter for new messages so index step alignment
|
||||
// doesn't depend on the total from the initial build.
|
||||
size_t new_count = 0;
|
||||
|
||||
while (1) {
|
||||
off_t pos = ftell(fp);
|
||||
@@ -200,7 +203,7 @@ _ff_state_extend(ff_contact_state_t* state)
|
||||
|
||||
state->total_lines++;
|
||||
|
||||
if (msg_count % FF_INDEX_STEP == 0) {
|
||||
if (new_count % FF_INDEX_STEP == 0) {
|
||||
char* space = strchr(buf, ' ');
|
||||
if (space) {
|
||||
char* ts_str = g_strndup(buf, space - buf);
|
||||
@@ -219,7 +222,7 @@ _ff_state_extend(ff_contact_state_t* state)
|
||||
g_free(ts_str);
|
||||
}
|
||||
}
|
||||
msg_count++;
|
||||
new_count++;
|
||||
free(buf);
|
||||
}
|
||||
|
||||
@@ -390,8 +393,17 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
||||
return;
|
||||
}
|
||||
|
||||
// Advisory lock to prevent interleaved writes from concurrent instances
|
||||
if (flock(fd, LOCK_EX) != 0) {
|
||||
log_warning("flatfile: flock(%s) failed (errno=%d: %s), writing anyway",
|
||||
log_path, errno, strerror(errno));
|
||||
}
|
||||
|
||||
if (is_new) {
|
||||
fprintf(fp, "%s", FLATFILE_HEADER);
|
||||
if (fprintf(fp, "%s", FLATFILE_HEADER) < 0) {
|
||||
log_error("flatfile: failed to write header to %s (errno=%d)",
|
||||
log_path, errno);
|
||||
}
|
||||
}
|
||||
|
||||
ff_write_line(fp, date_fmt, type, ff_get_message_enc_str(enc),
|
||||
@@ -399,6 +411,7 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
||||
from_barejid, from_resource, effective_msg);
|
||||
|
||||
fflush(fp);
|
||||
// fclose also releases the flock
|
||||
fclose(fp);
|
||||
g_free(effective_msg);
|
||||
}
|
||||
@@ -431,7 +444,7 @@ _flatfile_init(ProfAccount* account)
|
||||
g_hash_table_destroy(g_contact_states);
|
||||
}
|
||||
g_contact_states = g_hash_table_new_full(g_str_hash, g_str_equal,
|
||||
g_free, (GDestroyNotify)ff_state_free);
|
||||
g_free, (GDestroyNotify)ff_state_free);
|
||||
|
||||
log_info("Initialized flat-file database backend: %s", base_dir);
|
||||
return TRUE;
|
||||
@@ -453,6 +466,82 @@ _flatfile_close(void)
|
||||
// Backend callbacks: add message
|
||||
// =========================================================================
|
||||
|
||||
// =========================================================================
|
||||
// Incoming message validation helpers
|
||||
// =========================================================================
|
||||
|
||||
// Search log file for a line containing aid:{archive_id}, return TRUE if found.
|
||||
static gboolean
|
||||
_ff_has_archive_id(const char* log_path, const char* archive_id)
|
||||
{
|
||||
if (!log_path || !archive_id || strlen(archive_id) == 0)
|
||||
return FALSE;
|
||||
|
||||
FILE* fp = fopen(log_path, "r");
|
||||
if (!fp)
|
||||
return FALSE;
|
||||
|
||||
auto_gchar gchar* needle = g_strdup_printf("aid:%s", archive_id);
|
||||
gboolean found = FALSE;
|
||||
|
||||
while (!found) {
|
||||
gboolean trunc = FALSE;
|
||||
char* line = ff_readline(fp, &trunc);
|
||||
if (!line)
|
||||
break;
|
||||
if (trunc) {
|
||||
free(line);
|
||||
break;
|
||||
}
|
||||
if (strstr(line, needle)) {
|
||||
found = TRUE;
|
||||
}
|
||||
free(line);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return found;
|
||||
}
|
||||
|
||||
// Search log file for a line with stanza id matching replace_id, return the
|
||||
// from_jid of the original message (caller must g_free). Returns NULL if not found.
|
||||
static char*
|
||||
_ff_find_original_sender(const char* log_path, const char* replace_id)
|
||||
{
|
||||
if (!log_path || !replace_id || strlen(replace_id) == 0)
|
||||
return NULL;
|
||||
|
||||
FILE* fp = fopen(log_path, "r");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
|
||||
auto_gchar gchar* needle = g_strdup_printf("id:%s", replace_id);
|
||||
char* sender = NULL;
|
||||
|
||||
while (!sender) {
|
||||
gboolean trunc = FALSE;
|
||||
char* line = ff_readline(fp, &trunc);
|
||||
if (!line)
|
||||
break;
|
||||
if (trunc) {
|
||||
free(line);
|
||||
break;
|
||||
}
|
||||
if (strstr(line, needle)) {
|
||||
ff_parsed_line_t* pl = ff_parse_line(line);
|
||||
if (pl && pl->stanza_id && g_strcmp0(pl->stanza_id, replace_id) == 0) {
|
||||
sender = g_strdup(pl->from_jid);
|
||||
}
|
||||
if (pl)
|
||||
ff_parsed_line_free(pl);
|
||||
}
|
||||
free(line);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return sender;
|
||||
}
|
||||
|
||||
static void
|
||||
_flatfile_add_incoming(ProfMessage* message)
|
||||
{
|
||||
@@ -462,6 +551,39 @@ _flatfile_add_incoming(ProfMessage* message)
|
||||
const Jid* to_jid = message->to_jid ? message->to_jid : connection_get_jid();
|
||||
const char* type = ff_get_message_type_str(message->type);
|
||||
|
||||
// Determine contact JID for log path (same logic as _ff_add_message)
|
||||
const char* contact = NULL;
|
||||
const Jid* myjid = connection_get_jid();
|
||||
if (myjid && myjid->barejid && g_strcmp0(message->from_jid->barejid, myjid->barejid) == 0) {
|
||||
contact = to_jid->barejid;
|
||||
} else {
|
||||
contact = message->from_jid->barejid;
|
||||
}
|
||||
auto_gchar gchar* log_path = ff_get_log_path(contact);
|
||||
|
||||
// MAM dedup: skip if this stanza-id already exists in the log (XEP-0313 replay)
|
||||
if (message->stanzaid && !message->is_mam && log_path) {
|
||||
if (_ff_has_archive_id(log_path, message->stanzaid)) {
|
||||
log_error("flatfile: duplicate stanza-id '%s' from %s, skipping",
|
||||
message->stanzaid, message->from_jid->barejid);
|
||||
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.",
|
||||
message->from_jid->fulljid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// LMC sender validation: verify the correction comes from the original sender
|
||||
if (message->replace_id && log_path) {
|
||||
auto_gchar gchar* original_sender = _ff_find_original_sender(log_path, message->replace_id);
|
||||
if (original_sender && g_strcmp0(original_sender, message->from_jid->barejid) != 0) {
|
||||
log_error("flatfile: LMC sender mismatch — corrected msg sender: %s, original: %s, replace-id: %s",
|
||||
message->from_jid->barejid, original_sender, message->replace_id);
|
||||
cons_show_error("%s sent a message correction with mismatched sender. See log for details.",
|
||||
message->from_jid->barejid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_ff_add_message(type, message->id, message->stanzaid, message->replace_id,
|
||||
message->from_jid->barejid, message->from_jid->resourcepart,
|
||||
to_jid->barejid, message->plain,
|
||||
|
||||
@@ -369,8 +369,12 @@ ff_readline(FILE* fp, gboolean* truncated)
|
||||
}
|
||||
if (truncated)
|
||||
*truncated = FALSE;
|
||||
// Return empty string so caller's loop continues (parse will reject it)
|
||||
return g_strdup("");
|
||||
// Return empty string so caller's loop continues (parse will reject it).
|
||||
// Use malloc() so callers can always use free() consistently.
|
||||
char* empty = malloc(1);
|
||||
if (empty)
|
||||
empty[0] = '\0';
|
||||
return empty;
|
||||
}
|
||||
if (truncated)
|
||||
*truncated = FALSE;
|
||||
@@ -450,9 +454,9 @@ ff_write_line(FILE* fp, const char* timestamp, const char* type, const char* enc
|
||||
timestamp, meta->str, sender->str, safe_msg);
|
||||
|
||||
size_t to_write = full_line->len;
|
||||
ssize_t written = fwrite(full_line->str, 1, to_write, fp);
|
||||
if (written != (ssize_t)to_write) {
|
||||
log_error("flatfile: partial write (%zd/%zu)", written, to_write);
|
||||
size_t written = fwrite(full_line->str, 1, to_write, fp);
|
||||
if (written != to_write) {
|
||||
log_error("flatfile: partial write (%zu/%zu)", written, to_write);
|
||||
}
|
||||
|
||||
g_string_free(full_line, TRUE);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <glib.h>
|
||||
#include "prof_cmocka.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "database.h"
|
||||
|
||||
db_backend_t* active_db_backend = NULL;
|
||||
@@ -66,11 +67,13 @@ integrity_issue_free(integrity_issue_t* issue)
|
||||
g_free(issue);
|
||||
}
|
||||
}
|
||||
#ifdef HAVE_SQLITE
|
||||
db_backend_t*
|
||||
db_backend_sqlite(void)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
db_backend_t*
|
||||
db_backend_flatfile(void)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user