1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-06-30 08:26:38 +02:00
Files
unrealircd/src/modules/tkldb.c
T
Bram Matthys 0c080b3982 Fix crash in tkldb on 32 bit systems. Reported by k4be.
Also, get rid of compiler warnings (we can use C99 types now).
2020-04-25 09:27:31 +02:00

704 lines
19 KiB
C

/*
* Stores active *-Lines (G-Lines etc) inside a .db file for persistency
* (C) Copyright 2019 Gottem and the UnrealIRCd team
*
* This program 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 1, or (at your option)
* any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER = {
"tkldb",
"1.10",
"Stores active TKL entries (*-Lines) persistently/across IRCd restarts",
"UnrealIRCd Team",
"unrealircd-5",
};
#define TKL_DB_MAGIC 0x10101010
#define TKL_DB_VERSION 4999
#define TKL_DB_SAVE_EVERY 299
#ifdef DEBUGMODE
#define BENCHMARK
/* Benchmark results (2GHz Xeon Skylake, compiled with -O2, Linux):
* 100,000 zlines:
* - load db: 510 ms
* - save db: 72 ms
* Thus, saving does not take much time and can be done by a timer
* which executes every 5 minutes.
* Of course, exact figures will depend on the machine.
*/
#endif
#define FreeTKLRead() \
do { \
/* Some of these might be NULL */ \
if (tkl) \
free_tkl(tkl); \
} while(0)
#define WARN_WRITE_ERROR(fname) \
do { \
sendto_realops_and_log("[tkldb] Error writing to temporary database file " \
"'%s': %s (DATABASE NOT SAVED)", \
fname, strerror(errno)); \
} while(0)
#define R_SAFE(x) \
do { \
if (!(x)) { \
config_warn("[tkldb] Read error from database file '%s' (possible corruption): %s", cfg.database, strerror(errno)); \
fclose(fd); \
FreeTKLRead(); \
return 0; \
} \
} while(0)
#define W_SAFE(x) \
do { \
if (!(x)) { \
WARN_WRITE_ERROR(tmpfname); \
fclose(fd); \
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)
/* Forward declarations */
void tkldb_moddata_free(ModData *md);
void setcfg(void);
void freecfg(void);
int tkldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
int tkldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
EVENT(write_tkldb_evt);
int write_tkldb(void);
int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl);
int read_tkldb(void);
/* Globals variables */
const uint32_t tkl_db_version = TKL_DB_VERSION;
struct cfgstruct {
char *database;
};
static struct cfgstruct cfg;
static int tkls_loaded = 0;
MOD_TEST()
{
memset(&cfg, 0, sizeof(cfg));
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, tkldb_configtest);
return MOD_SUCCESS;
}
MOD_INIT()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
LoadPersistentInt(modinfo, tkls_loaded);
setcfg();
if (!tkls_loaded)
{
/* If this is the first time that our module is loaded, then
* read the TKL DB and add all *-Lines.
*/
if (!read_tkldb())
{
char fname[512];
snprintf(fname, sizeof(fname), "%s.corrupt", cfg.database);
if (rename(cfg.database, fname) == 0)
config_warn("[tkldb] Existing database renamed to %s and starting a new one...", fname);
else
config_warn("[tkldb] Failed to rename database from %s to %s: %s", cfg.database, fname, strerror(errno));
}
tkls_loaded = 1;
}
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, tkldb_configrun);
return MOD_SUCCESS;
}
MOD_LOAD()
{
EventAdd(modinfo->handle, "tkldb_write_tkldb", write_tkldb_evt, NULL, TKL_DB_SAVE_EVERY*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()
{
write_tkldb();
freecfg();
SavePersistentInt(modinfo, tkls_loaded);
return MOD_SUCCESS;
}
void tkldb_moddata_free(ModData *md)
{
if (md->i)
md->i = 0;
}
void setcfg(void)
{
// Default: data/tkl.db
safe_strdup(cfg.database, "tkl.db");
convert_to_absolute_path(&cfg.database, PERMDATADIR);
}
void freecfg(void)
{
safe_free(cfg.database);
}
int tkldb_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
// We are only interested in set::tkldb::database
if (type != CONFIG_SET)
return 0;
if (!ce || strcmp(ce->ce_varname, "tkldb"))
return 0;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!cep->ce_vardata) {
config_error("%s:%i: blank set::tkldb::%s without value", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
errors++;
continue;
}
if (!strcmp(cep->ce_varname, "database")) {
convert_to_absolute_path(&cep->ce_vardata, PERMDATADIR);
continue;
}
config_error("%s:%i: unknown directive set::tkldb::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
errors++;
}
*errs = errors;
return errors ? -1 : 1;
}
int tkldb_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep;
// We are only interested in set::tkldb::database
if (type != CONFIG_SET)
return 0;
if (!ce || strcmp(ce->ce_varname, "tkldb"))
return 0;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!strcmp(cep->ce_varname, "database"))
safe_strdup(cfg.database, cep->ce_vardata);
}
return 1;
}
EVENT(write_tkldb_evt)
{
write_tkldb();
}
int write_tkldb(void)
{
char tmpfname[512];
FILE *fd;
uint64_t tklcount;
int index, index2;
TKL *tkl;
#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.tmp", cfg.database);
fd = fopen(tmpfname, "wb");
if (!fd)
{
WARN_WRITE_ERROR(tmpfname);
return 0;
}
W_SAFE(write_int32(fd, TKL_DB_MAGIC));
W_SAFE(write_data(fd, &tkl_db_version, sizeof(tkl_db_version)));
// Count the *-Lines
tklcount = 0;
// First the ones in the hash table
for (index = 0; index < TKLIPHASHLEN1; index++)
{
for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
{
for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
{
if (tkl->flags & TKL_FLAG_CONFIG)
continue; /* config entry */
tklcount++;
}
}
}
// Then the regular *-Lines
for (index = 0; index < TKLISTLEN; index++)
{
for (tkl = tklines[index]; tkl; tkl = tkl->next)
{
if (tkl->flags & TKL_FLAG_CONFIG)
continue; /* config entry */
tklcount++;
}
}
W_SAFE(write_data(fd, &tklcount, sizeof(tklcount)));
// Now write the actual *-Lines, first the ones in the hash table
for (index = 0; index < TKLIPHASHLEN1; index++)
{
for (index2 = 0; index2 < TKLIPHASHLEN2; index2++)
{
for (tkl = tklines_ip_hash[index][index2]; tkl; tkl = tkl->next)
{
if (tkl->flags & TKL_FLAG_CONFIG)
continue; /* config entry */
if (!write_tkline(fd, tmpfname, tkl)) // write_tkline() closes the fd on errors itself
return 0;
}
}
}
// Then the regular *-Lines
for (index = 0; index < TKLISTLEN; index++)
{
for (tkl = tklines[index]; tkl; tkl = tkl->next)
{
if (tkl->flags & TKL_FLAG_CONFIG)
continue; /* config entry */
if (!write_tkline(fd, tmpfname, tkl))
return 0;
}
}
// Everything seems to have gone well, attempt to close and rename the tempfile
if (fclose(fd) != 0)
{
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)
{
sendto_realops_and_log("[tkldb] Error renaming '%s' to '%s': %s (DATABASE NOT SAVED)", tmpfname, cfg.database, strerror(errno));
return 0;
}
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
config_status("[tkldb] Benchmark: SAVE DB: %lld microseconds",
(long long)(((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
return 1;
}
/** Write a TKL entry */
int write_tkline(FILE *fd, const char *tmpfname, TKL *tkl)
{
char tkltype;
char buf[256];
/* First, write the common attributes */
tkltype = tkl_typetochar(tkl->type);
W_SAFE(write_data(fd, &tkltype, sizeof(tkltype))); // TKL char
W_SAFE(write_str(fd, tkl->set_by));
W_SAFE(write_int64(fd, tkl->set_at));
W_SAFE(write_int64(fd, tkl->expire_at));
if (TKLIsServerBan(tkl))
{
char *usermask = tkl->ptr.serverban->usermask;
if (tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT)
{
snprintf(buf, sizeof(buf), "%%%s", tkl->ptr.serverban->usermask);
usermask = buf;
}
W_SAFE(write_str(fd, usermask));
W_SAFE(write_str(fd, tkl->ptr.serverban->hostmask));
W_SAFE(write_str(fd, tkl->ptr.serverban->reason));
} else
if (TKLIsBanException(tkl))
{
char *usermask = tkl->ptr.banexception->usermask;
if (tkl->ptr.banexception->subtype & TKL_SUBTYPE_SOFT)
{
snprintf(buf, sizeof(buf), "%%%s", tkl->ptr.banexception->usermask);
usermask = buf;
}
W_SAFE(write_str(fd, usermask));
W_SAFE(write_str(fd, tkl->ptr.banexception->hostmask));
W_SAFE(write_str(fd, tkl->ptr.banexception->bantypes));
W_SAFE(write_str(fd, tkl->ptr.banexception->reason));
} else
if (TKLIsNameBan(tkl))
{
char *hold = tkl->ptr.nameban->hold ? "H" : "*";
W_SAFE(write_str(fd, hold));
W_SAFE(write_str(fd, tkl->ptr.nameban->name));
W_SAFE(write_str(fd, tkl->ptr.nameban->reason));
} else
if (TKLIsSpamfilter(tkl))
{
char *match_type = unreal_match_method_valtostr(tkl->ptr.spamfilter->match->type);
char *target = spamfilter_target_inttostring(tkl->ptr.spamfilter->target);
char action = banact_valtochar(tkl->ptr.spamfilter->action);
W_SAFE(write_str(fd, match_type));
W_SAFE(write_str(fd, tkl->ptr.spamfilter->match->str));
W_SAFE(write_str(fd, target));
W_SAFE(write_data(fd, &action, sizeof(action)));
W_SAFE(write_str(fd, tkl->ptr.spamfilter->tkl_reason));
W_SAFE(write_int64(fd, tkl->ptr.spamfilter->tkl_duration));
}
return 1;
}
/** Read all entries from the TKL db */
int read_tkldb(void)
{
FILE *fd;
TKL *tkl = NULL;
uint32_t magic = 0;
uint32_t version;
uint64_t cnt;
uint64_t tklcount = 0;
uint64_t v;
int added_cnt = 0;
char c;
char *str;
#ifdef BENCHMARK
struct timeval tv_alpha, tv_beta;
gettimeofday(&tv_alpha, NULL);
#endif
fd = fopen(cfg.database, "rb");
if (!fd)
{
if (errno == ENOENT)
{
/* Database does not exist. Could be first boot */
config_warn("[tkldb] No database present at '%s', will start a new one", cfg.database);
return 1;
} else {
config_warn("[tkldb] Unable to open database file '%s' for reading: %s", cfg.database, strerror(errno));
return 0;
}
}
/* The database starts with a "magic value" - unless it's some old version or corrupt */
R_SAFE(read_data(fd, &magic, sizeof(magic)));
if (magic != TKL_DB_MAGIC)
{
config_warn("[tkldb] Database '%s' uses an old and unsupported format OR is corrupt", cfg.database);
config_status("If you are upgrading from UnrealIRCd 4 (or 5.0.0-alpha1) then we suggest you to "
"delete the existing database. Just keep at least 1 server linked during the upgrade "
"process to preserve your global *LINES and Spamfilters.");
fclose(fd);
return 0;
}
/* Now do a version check */
R_SAFE(read_data(fd, &version, sizeof(version)));
if (version < 4999)
{
config_warn("[tkldb] Database '%s' uses an unsupport - possibly old - format (%ld).", cfg.database, (long)version);
fclose(fd);
return 0;
}
if (version > tkl_db_version)
{
config_warn("[tkldb] Database '%s' has version %lu while we only support %lu. Did you just downgrade UnrealIRCd? Sorry this is not suported",
cfg.database, (unsigned long)tkl_db_version, (unsigned long)version);
fclose(fd);
return 0;
}
R_SAFE(read_data(fd, &tklcount, sizeof(tklcount)));
for (cnt = 0; cnt < tklcount; cnt++)
{
int do_not_add = 0;
tkl = safe_alloc(sizeof(TKL));
/* First, fetch the TKL type.. */
R_SAFE(read_data(fd, &c, sizeof(c)));
tkl->type = tkl_chartotype(c);
if (!tkl->type)
{
/* We can't continue reading the DB if we don't know the TKL type,
* since we don't know how long the entry will be, we can't skip it.
* This is "impossible" anyway, unless we some day remove a TKL type
* in core UnrealIRCd. In which case we should add some skipping code
* here to gracefully handle that situation ;)
*/
config_warn("[tkldb] Invalid type '%c' encountered - STOPPED READING DATABASE!", tkl->type);
FreeTKLRead();
break; /* we MUST stop reading */
}
/* Read the common types (same for all TKLs) */
R_SAFE(read_str(fd, &tkl->set_by));
R_SAFE(read_int64(fd, &v));
tkl->set_at = v;
R_SAFE(read_int64(fd, &v));
tkl->expire_at = v;
/* Save some CPU... if it's already expired then don't bother adding */
if (tkl->expire_at != 0 && tkl->expire_at <= TStime())
do_not_add = 1;
/* Now handle all the specific types */
if (TKLIsServerBan(tkl))
{
int softban = 0;
tkl->ptr.serverban = safe_alloc(sizeof(ServerBan));
/* Usermask - but taking into account that the
* %-prefix means a soft ban.
*/
R_SAFE(read_str(fd, &str));
if (*str == '%')
{
softban = 1;
safe_strdup(tkl->ptr.serverban->usermask, str+1);
} else {
safe_strdup(tkl->ptr.serverban->usermask, str);
}
safe_free(str);
/* And the other 2 fields.. */
R_SAFE(read_str(fd, &tkl->ptr.serverban->hostmask));
R_SAFE(read_str(fd, &tkl->ptr.serverban->reason));
if (find_tkl_serverban(tkl->type, tkl->ptr.serverban->usermask,
tkl->ptr.serverban->hostmask, softban))
{
do_not_add = 1;
}
if (!do_not_add)
{
tkl_add_serverban(tkl->type, tkl->ptr.serverban->usermask,
tkl->ptr.serverban->hostmask,
tkl->ptr.serverban->reason,
tkl->set_by, tkl->expire_at,
tkl->set_at, softban, 0);
}
} else
if (TKLIsBanException(tkl))
{
int softban = 0;
tkl->ptr.banexception = safe_alloc(sizeof(BanException));
/* Usermask - but taking into account that the
* %-prefix means a soft ban.
*/
R_SAFE(read_str(fd, &str));
if (*str == '%')
{
softban = 1;
safe_strdup(tkl->ptr.banexception->usermask, str+1);
} else {
safe_strdup(tkl->ptr.banexception->usermask, str);
}
safe_free(str);
/* And the other 3 fields.. */
R_SAFE(read_str(fd, &tkl->ptr.banexception->hostmask));
R_SAFE(read_str(fd, &tkl->ptr.banexception->bantypes));
R_SAFE(read_str(fd, &tkl->ptr.banexception->reason));
if (find_tkl_banexception(tkl->type, tkl->ptr.banexception->usermask,
tkl->ptr.banexception->hostmask, softban))
{
do_not_add = 1;
}
if (!do_not_add)
{
tkl_add_banexception(tkl->type, tkl->ptr.banexception->usermask,
tkl->ptr.banexception->hostmask,
tkl->ptr.banexception->reason,
tkl->set_by, tkl->expire_at,
tkl->set_at, softban,
tkl->ptr.banexception->bantypes,
0);
}
} else
if (TKLIsNameBan(tkl))
{
tkl->ptr.nameban = safe_alloc(sizeof(NameBan));
R_SAFE(read_str(fd, &str));
if (*str == 'H')
tkl->ptr.nameban->hold = 1;
safe_free(str);
R_SAFE(read_str(fd, &tkl->ptr.nameban->name));
R_SAFE(read_str(fd, &tkl->ptr.nameban->reason));
if (find_tkl_nameban(tkl->type, tkl->ptr.nameban->name,
tkl->ptr.nameban->hold))
{
do_not_add = 1;
}
if (!do_not_add)
{
tkl_add_nameban(tkl->type, tkl->ptr.nameban->name,
tkl->ptr.nameban->hold,
tkl->ptr.nameban->reason,
tkl->set_by, tkl->expire_at,
tkl->set_at, 0);
}
} else
if (TKLIsSpamfilter(tkl))
{
int match_method;
char *err = NULL;
tkl->ptr.spamfilter = safe_alloc(sizeof(Spamfilter));
/* Match method */
R_SAFE(read_str(fd, &str));
match_method = unreal_match_method_strtoval(str);
if (!match_method)
{
config_warn("[tkldb] Unhandled spamfilter match method '%s' -- spamfilter entry not added", str);
do_not_add = 1;
}
safe_free(str);
/* Match string (eg: regex) */
R_SAFE(read_str(fd, &str));
tkl->ptr.spamfilter->match = unreal_create_match(match_method, str, &err);
if (!tkl->ptr.spamfilter->match)
{
config_warn("[tkldb] Spamfilter '%s' does not compile: %s -- spamfilter entry not added", str, err);
do_not_add = 1;
}
safe_free(str);
/* Target (eg: cpn) */
R_SAFE(read_str(fd, &str));
tkl->ptr.spamfilter->target = spamfilter_gettargets(str, NULL);
if (!tkl->ptr.spamfilter->target)
{
config_warn("[tkldb] Spamfilter '%s' without any valid targets (%s) -- spamfilter entry not added",
tkl->ptr.spamfilter->match->str, str);
do_not_add = 1;
}
safe_free(str);
/* Action */
R_SAFE(read_data(fd, &c, sizeof(c)));
tkl->ptr.spamfilter->action = banact_chartoval(c);
if (!tkl->ptr.spamfilter->action)
{
config_warn("[tkldb] Spamfilter '%s' without valid action (%c) -- spamfilter entry not added",
tkl->ptr.spamfilter->match->str, c);
do_not_add = 1;
}
R_SAFE(read_str(fd, &tkl->ptr.spamfilter->tkl_reason));
R_SAFE(read_int64(fd, &v));
tkl->ptr.spamfilter->tkl_duration = v;
if (find_tkl_spamfilter(tkl->type, tkl->ptr.spamfilter->match->str,
tkl->ptr.spamfilter->action,
tkl->ptr.spamfilter->target))
{
do_not_add = 1;
}
if (!do_not_add)
{
tkl_add_spamfilter(tkl->type, tkl->ptr.spamfilter->target,
tkl->ptr.spamfilter->action,
tkl->ptr.spamfilter->match,
tkl->set_by, tkl->expire_at, tkl->set_at,
tkl->ptr.spamfilter->tkl_duration,
tkl->ptr.spamfilter->tkl_reason,
0);
/* tkl_add_spamfilter() does not copy the match but assign it.
* so set to NULL here to avoid a read-after-free later on.
*/
tkl->ptr.spamfilter->match = NULL;
}
} else
{
config_warn("[tkldb] Unhandled type!! TKLDB is missing support for type %ld -- STOPPED reading db entries!", (long)tkl->type);
FreeTKLRead();
break; /* we MUST stop reading */
}
if (!do_not_add)
added_cnt++;
FreeTKLRead();
}
/* If everything went fine, then reading a single byte should cause an EOF error */
if (fread(&c, 1, 1, fd) == 1)
config_warn("[tkldb] Database invalid. Extra data found at end of DB file.");
fclose(fd);
if (added_cnt)
sendto_realops_and_log("[tkldb] Re-added %d *-Lines", added_cnt);
#ifdef BENCHMARK
gettimeofday(&tv_beta, NULL);
ircd_log(LOG_ERROR, "[tkldb] Benchmark: LOAD DB: %lld microseconds",
(long long)(((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
#endif
return 1;
}