1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-10 18:23:12 +02:00
Files
unrealircd/src/modules/channeldb.c
T
Bram Matthys b078a9c8b5 Fix cut-off and expansion issues with MODE, which is a possible problem when
using mixed UnrealIRCd 5 and UnrealIRCd 6 networks.

This is a slightly complex rewrite of make_mode_str() and do_mode(),
as we nog go from single mode lines to potentially multiple mode lines.

In short: whenever we would be near buffer cut-off point (the famous
512 byte limit) then previously we would prevent the mode, though not
succesfully in all cases where a network consists of mixed 5.x and 6.x.
From this point onward we no longer do that. Instead we convert one
MODE command to two MODE lines if that is needed.
The benefit of this is that we no longer prevent it BEFORE processing
the MODE, which is a flawed method and could be wrong (causing desyncs).
And also, we no longer partially ignore MODE lines from clients when
they would cause the limit to be exceeded, as we replace them with
two MODE lines instead.

These are more changes than I wanted at such a late point but.. they seem
to be necessary to prevent U5-U6 compatibility issues.
2021-11-19 13:53:21 +01:00

546 lines
14 KiB
C

/*
* Stores channel settings for +P channels in a .db file
* (C) Copyright 2019 Syzop, Gottem and the UnrealIRCd team
* License: GPLv2
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER = {
"channeldb",
"1.0",
"Stores and retrieves channel settings for persistent (+P) channels",
"UnrealIRCd Team",
"unrealircd-6",
};
/* Database version */
#define CHANNELDB_VERSION 100
/* Save channels to file every <this> seconds */
#define CHANNELDB_SAVE_EVERY 300
/* The very first save after boot, apply this delta, this
* so we don't coincide with other (potentially) expensive
* I/O events like saving tkldb.
*/
#define CHANNELDB_SAVE_EVERY_DELTA -15
#define MAGIC_CHANNEL_START 0x11111111
#define MAGIC_CHANNEL_END 0x22222222
// #undef BENCHMARK
#define WARN_WRITE_ERROR(fname) \
do { \
unreal_log(ULOG_ERROR, "channeldb", "CHANNELDB_FILE_WRITE_ERROR", NULL, \
"[channeldb] Error writing to temporary database file $filename: $system_error", \
log_data_string("filename", fname), \
log_data_string("system_error", unrealdb_get_error_string())); \
} while(0)
#define W_SAFE(x) \
do { \
if (!(x)) { \
WARN_WRITE_ERROR(tmpfname); \
unrealdb_close(db); \
return 0; \
} \
} while(0)
#define IsMDErr(x, y, z) \
do { \
if (!(x)) { \
config_error("A critical error occurred when registering ModData for %s: %s", MOD_HEADER.name, ModuleGetErrorStr((z)->handle)); \
return MOD_FAILED; \
} \
} while(0)
/* Structs */
struct cfgstruct {
char *database;
char *db_secret;
};
/* Forward declarations */
void channeldb_moddata_free(ModData *md);
void setcfg(struct cfgstruct *cfg);
void freecfg(struct cfgstruct *cfg);
int channeldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int channeldb_config_posttest(int *errs);
int channeldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
EVENT(write_channeldb_evt);
int write_channeldb(void);
int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel);
int read_channeldb(void);
/* Global variables */
static uint32_t channeldb_version = CHANNELDB_VERSION;
static struct cfgstruct cfg;
static struct cfgstruct test;
static long channeldb_next_event = 0;
MOD_TEST()
{
memset(&cfg, 0, sizeof(cfg));
memset(&test, 0, sizeof(test));
setcfg(&test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, channeldb_config_test);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, channeldb_config_posttest);
return MOD_SUCCESS;
}
MOD_INIT()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
/* We must unload early, when all channel modes and such are still in place: */
ModuleSetOptions(modinfo->handle, MOD_OPT_UNLOAD_PRIORITY, -99999999);
LoadPersistentLong(modinfo, channeldb_next_event);
setcfg(&cfg);
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, channeldb_config_run);
return MOD_SUCCESS;
}
MOD_LOAD()
{
if (!channeldb_next_event)
{
/* If this is the first time that our module is loaded, then read the database. */
if (!read_channeldb())
{
char fname[512];
snprintf(fname, sizeof(fname), "%s.corrupt", cfg.database);
if (rename(cfg.database, fname) == 0)
config_warn("[channeldb] Existing database renamed to %s and starting a new one...", fname);
else
config_warn("[channeldb] Failed to rename database from %s to %s: %s", cfg.database, fname, strerror(errno));
}
channeldb_next_event = TStime() + CHANNELDB_SAVE_EVERY + CHANNELDB_SAVE_EVERY_DELTA;
}
EventAdd(modinfo->handle, "channeldb_write_channeldb", write_channeldb_evt, NULL, 1000, 0);
if (ModuleGetError(modinfo->handle) != MODERR_NOERROR)
{
config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
return MOD_FAILED;
}
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
if (loop.terminating)
write_channeldb();
freecfg(&test);
freecfg(&cfg);
SavePersistentLong(modinfo, channeldb_next_event);
return MOD_SUCCESS;
}
void channeldb_moddata_free(ModData *md)
{
if (md->i)
md->i = 0;
}
void setcfg(struct cfgstruct *cfg)
{
// Default: data/channel.db
safe_strdup(cfg->database, "channel.db");
convert_to_absolute_path(&cfg->database, PERMDATADIR);
}
void freecfg(struct cfgstruct *cfg)
{
safe_free(cfg->database);
safe_free(cfg->db_secret);
}
int channeldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
// We are only interested in set::channeldb::database
if (type != CONFIG_SET)
return 0;
if (!ce || strcmp(ce->name, "channeldb"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!cep->value)
{
config_error("%s:%i: blank set::channeldb::%s without value", cep->file->filename, cep->line_number, cep->name);
errors++;
} else
if (!strcmp(cep->name, "database"))
{
convert_to_absolute_path(&cep->value, PERMDATADIR);
safe_strdup(test.database, cep->value);
} else
if (!strcmp(cep->name, "db-secret"))
{
const char *err;
if ((err = unrealdb_test_secret(cep->value)))
{
config_error("%s:%i: set::channeldb::db-secret: %s", cep->file->filename, cep->line_number, err);
errors++;
continue;
}
safe_strdup(test.db_secret, cep->value);
} else
{
config_error("%s:%i: unknown directive set::channeldb::%s", cep->file->filename, cep->line_number, cep->name);
errors++;
}
}
*errs = errors;
return errors ? -1 : 1;
}
int channeldb_config_posttest(int *errs)
{
int errors = 0;
char *errstr;
if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
{
config_error("[channeldb] %s", errstr);
errors++;
}
*errs = errors;
return errors ? -1 : 1;
}
int channeldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep;
// We are only interested in set::channeldb::database
if (type != CONFIG_SET)
return 0;
if (!ce || strcmp(ce->name, "channeldb"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "database"))
safe_strdup(cfg.database, cep->value);
else if (!strcmp(cep->name, "db-secret"))
safe_strdup(cfg.db_secret, cep->value);
}
return 1;
}
EVENT(write_channeldb_evt)
{
if (channeldb_next_event > TStime())
return;
channeldb_next_event = TStime() + CHANNELDB_SAVE_EVERY;
write_channeldb();
}
int write_channeldb(void)
{
char tmpfname[512];
UnrealDB *db;
Channel *channel;
int cnt = 0;
#ifdef BENCHMARK
struct timeval tv_alpha, tv_beta;
gettimeofday(&tv_alpha, NULL);
#endif
// Write to a tempfile first, then rename it if everything succeeded
snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
if (!db)
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
W_SAFE(unrealdb_write_int32(db, channeldb_version));
/* First, count +P channels and write the count to the database */
for (channel = channels; channel; channel=channel->nextch)
if (has_channel_mode(channel, 'P'))
cnt++;
W_SAFE(unrealdb_write_int64(db, cnt));
for (channel = channels; channel; channel=channel->nextch)
{
/* We only care about +P (persistent) channels */
if (has_channel_mode(channel, 'P'))
{
if (!write_channel_entry(db, tmpfname, channel))
return 0;
}
}
// Everything seems to have gone well, attempt to close and rename the tempfile
if (!unrealdb_close(db))
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
#ifdef _WIN32
/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
unlink(cfg.database);
#endif
if (rename(tmpfname, cfg.database) < 0)
{
config_error("[channeldb] Error renaming '%s' to '%s': %s (DATABASE NOT SAVED)", tmpfname, cfg.database, strerror(errno));
return 0;
}
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
config_status("[channeldb] Benchmark: SAVE DB: %ld microseconds",
((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec));
#endif
return 1;
}
int write_listmode(UnrealDB *db, const char *tmpfname, Ban *lst)
{
Ban *l;
int cnt = 0;
/* First count and write the list count */
for (l = lst; l; l = l->next)
cnt++;
W_SAFE(unrealdb_write_int32(db, cnt));
for (l = lst; l; l = l->next)
{
/* The entry, setby, seton */
W_SAFE(unrealdb_write_str(db, l->banstr));
W_SAFE(unrealdb_write_str(db, l->who));
W_SAFE(unrealdb_write_int64(db, l->when));
}
return 1;
}
int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel)
{
char modebuf[BUFSIZE], parabuf[BUFSIZE];
W_SAFE(unrealdb_write_int32(db, MAGIC_CHANNEL_START));
/* Channel name */
W_SAFE(unrealdb_write_str(db, channel->name));
/* Channel creation time */
W_SAFE(unrealdb_write_int64(db, channel->creationtime));
/* Topic (topic, setby, seton) */
W_SAFE(unrealdb_write_str(db, channel->topic));
W_SAFE(unrealdb_write_str(db, channel->topic_nick));
W_SAFE(unrealdb_write_int64(db, channel->topic_time));
/* Basic channel modes (eg: +sntkl key 55) */
channel_modes(&me, modebuf, parabuf, sizeof(modebuf), sizeof(parabuf), channel, 1);
W_SAFE(unrealdb_write_str(db, modebuf));
W_SAFE(unrealdb_write_str(db, parabuf));
/* Mode lock */
W_SAFE(unrealdb_write_str(db, channel->mode_lock));
/* List modes (bans, exempts, invex) */
if (!write_listmode(db, tmpfname, channel->banlist))
return 0;
if (!write_listmode(db, tmpfname, channel->exlist))
return 0;
if (!write_listmode(db, tmpfname, channel->invexlist))
return 0;
W_SAFE(unrealdb_write_int32(db, MAGIC_CHANNEL_END));
return 1;
}
#define R_SAFE(x) \
do { \
if (!(x)) { \
config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
if (e) \
{ \
safe_free(e->banstr); \
safe_free(e->who); \
safe_free(e); \
} \
return 0; \
} \
} while(0)
int read_listmode(UnrealDB *db, Ban **lst)
{
uint32_t total;
uint64_t when;
int i;
Ban *e = NULL;
R_SAFE(unrealdb_read_int32(db, &total));
for (i = 0; i < total; i++)
{
e = safe_alloc(sizeof(Ban));
R_SAFE(unrealdb_read_str(db, &e->banstr));
R_SAFE(unrealdb_read_str(db, &e->who));
R_SAFE(unrealdb_read_int64(db, &when));
e->when = when;
e->next = *lst;
*lst = e;
}
return 1;
}
#undef R_SAFE
#define FreeChannelEntry() \
do { \
/* Some of these might be NULL */ \
safe_free(chname); \
safe_free(topic); \
safe_free(topic_nick); \
safe_free(modes1); \
safe_free(modes2); \
safe_free(mode_lock); \
} while(0)
#define R_SAFE(x) \
do { \
if (!(x)) { \
config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
unrealdb_close(db); \
FreeChannelEntry(); \
return 0; \
} \
} while(0)
int read_channeldb(void)
{
UnrealDB *db;
uint32_t version;
int added = 0;
int i;
uint64_t count = 0;
uint32_t magic;
// Variables for the channels
// Some of them need to be declared and NULL initialised early due to the macro FreeChannelEntry() being used by R_SAFE() on error
char *chname = NULL;
uint64_t creationtime = 0;
char *topic = NULL;
char *topic_nick = NULL;
uint64_t topic_time = 0;
char *modes1 = NULL;
char *modes2 = NULL;
char *mode_lock = NULL;
#ifdef BENCHMARK
struct timeval tv_alpha, tv_beta;
gettimeofday(&tv_alpha, NULL);
#endif
db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
if (!db)
{
if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
{
/* Database does not exist. Could be first boot */
config_warn("[channeldb] No database present at '%s', will start a new one", cfg.database);
return 1;
} else
if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
{
/* Re-open as unencrypted */
db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
if (!db)
{
/* This should actually never happen, unless some weird I/O error */
config_warn("[channeldb] Unable to open the database file '%s': %s", cfg.database, unrealdb_get_error_string());
return 0;
}
} else
{
config_warn("[channeldb] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
return 0;
}
}
R_SAFE(unrealdb_read_int32(db, &version));
if (version > channeldb_version)
{
config_warn("[channeldb] Database '%s' has a wrong version: expected it to be <= %u but got %u instead", cfg.database, channeldb_version, version);
unrealdb_close(db);
return 0;
}
R_SAFE(unrealdb_read_int64(db, &count));
for (i=1; i <= count; i++)
{
// Variables
chname = NULL;
creationtime = 0;
topic = NULL;
topic_nick = NULL;
topic_time = 0;
modes1 = NULL;
modes2 = NULL;
mode_lock = NULL;
Channel *channel;
R_SAFE(unrealdb_read_int32(db, &magic));
if (magic != MAGIC_CHANNEL_START)
{
config_error("[channeldb] Corrupt database (%s) - channel magic start is 0x%x. Further reading aborted.", cfg.database, magic);
break;
}
R_SAFE(unrealdb_read_str(db, &chname));
R_SAFE(unrealdb_read_int64(db, &creationtime));
R_SAFE(unrealdb_read_str(db, &topic));
R_SAFE(unrealdb_read_str(db, &topic_nick));
R_SAFE(unrealdb_read_int64(db, &topic_time));
R_SAFE(unrealdb_read_str(db, &modes1));
R_SAFE(unrealdb_read_str(db, &modes2));
R_SAFE(unrealdb_read_str(db, &mode_lock));
/* If we got this far, we can create/initialize the channel with the above */
channel = make_channel(chname);
if (IsInvalidChannelTS(creationtime))
channel->creationtime = TStime();
else
channel->creationtime = creationtime;
safe_strdup(channel->topic, topic);
safe_strdup(channel->topic_nick, topic_nick);
channel->topic_time = topic_time;
safe_strdup(channel->mode_lock, mode_lock);
set_channel_mode(channel, modes1, modes2);
R_SAFE(read_listmode(db, &channel->banlist));
R_SAFE(read_listmode(db, &channel->exlist));
R_SAFE(read_listmode(db, &channel->invexlist));
R_SAFE(unrealdb_read_int32(db, &magic));
FreeChannelEntry();
added++;
if (magic != MAGIC_CHANNEL_END)
{
config_error("[channeldb] Corrupt database (%s) - channel magic end is 0x%x. Further reading aborted.", cfg.database, magic);
break;
}
}
unrealdb_close(db);
if (added)
config_status("[channeldb] Added %d persistent channels (+P)", added);
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
unreal_log(ULOG_DEBUG, "channeldb", "CHANNELDB_BENCHMARK", NULL,
"[channeldb] Benchmark: LOAD DB: $time_msec microseconds",
log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
return 1;
}
#undef FreeChannelEntry
#undef R_SAFE