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:
2026-02-21 16:15:38 +03:00
parent 16ae7fb85c
commit 23723376c6
9 changed files with 182 additions and 17 deletions

View File

@@ -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,