unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

channeldb.c (15122B)

      1 /*
      2  * Stores channel settings for +P channels in a .db file
      3  * (C) Copyright 2019 Syzop, Gottem and the UnrealIRCd team
      4  * License: GPLv2 or later
      5  */
      6 
      7 #include "unrealircd.h"
      8 
      9 ModuleHeader MOD_HEADER = {
     10 	"channeldb",
     11 	"1.0",
     12 	"Stores and retrieves channel settings for persistent (+P) channels",
     13 	"UnrealIRCd Team",
     14 	"unrealircd-6",
     15 };
     16 
     17 /* Database version */
     18 #define CHANNELDB_VERSION 100
     19 /* Save channels to file every <this> seconds */
     20 #define CHANNELDB_SAVE_EVERY 300
     21 /* The very first save after boot, apply this delta, this
     22  * so we don't coincide with other (potentially) expensive
     23  * I/O events like saving tkldb.
     24  */
     25 #define CHANNELDB_SAVE_EVERY_DELTA -15
     26 
     27 #define MAGIC_CHANNEL_START	0x11111111
     28 #define MAGIC_CHANNEL_END	0x22222222
     29 
     30 // #undef BENCHMARK
     31 
     32 #define WARN_WRITE_ERROR(fname) \
     33 	do { \
     34 		unreal_log(ULOG_ERROR, "channeldb", "CHANNELDB_FILE_WRITE_ERROR", NULL, \
     35 			   "[channeldb] Error writing to temporary database file $filename: $system_error", \
     36 			   log_data_string("filename", fname), \
     37 			   log_data_string("system_error", unrealdb_get_error_string())); \
     38 	} while(0)
     39 
     40 #define W_SAFE(x) \
     41 	do { \
     42 		if (!(x)) { \
     43 			WARN_WRITE_ERROR(tmpfname); \
     44 			unrealdb_close(db); \
     45 			return 0; \
     46 		} \
     47 	} while(0)
     48 
     49 #define IsMDErr(x, y, z) \
     50 	do { \
     51 		if (!(x)) { \
     52 			config_error("A critical error occurred when registering ModData for %s: %s", MOD_HEADER.name, ModuleGetErrorStr((z)->handle)); \
     53 			return MOD_FAILED; \
     54 		} \
     55 	} while(0)
     56 
     57 /* Structs */
     58 struct cfgstruct {
     59 	char *database;
     60 	char *db_secret;
     61 };
     62 
     63 /* Forward declarations */
     64 void channeldb_moddata_free(ModData *md);
     65 void setcfg(struct cfgstruct *cfg);
     66 void freecfg(struct cfgstruct *cfg);
     67 int channeldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
     68 int channeldb_config_posttest(int *errs);
     69 int channeldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
     70 EVENT(write_channeldb_evt);
     71 int write_channeldb(void);
     72 int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel);
     73 int read_channeldb(void);
     74 
     75 /* Global variables */
     76 static uint32_t channeldb_version = CHANNELDB_VERSION;
     77 static struct cfgstruct cfg;
     78 static struct cfgstruct test;
     79 
     80 static long channeldb_next_event = 0;
     81 
     82 MOD_TEST()
     83 {
     84 	memset(&cfg, 0, sizeof(cfg));
     85 	memset(&test, 0, sizeof(test));
     86 	setcfg(&test);
     87 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, channeldb_config_test);
     88 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, channeldb_config_posttest);
     89 	return MOD_SUCCESS;
     90 }
     91 
     92 MOD_INIT()
     93 {
     94 	MARK_AS_OFFICIAL_MODULE(modinfo);
     95 	/* We must unload early, when all channel modes and such are still in place: */
     96 	ModuleSetOptions(modinfo->handle, MOD_OPT_PRIORITY, -99999999);
     97 
     98 	LoadPersistentLong(modinfo, channeldb_next_event);
     99 
    100 	setcfg(&cfg);
    101 
    102 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, channeldb_config_run);
    103 	return MOD_SUCCESS;
    104 }
    105 
    106 MOD_LOAD()
    107 {
    108 	if (!channeldb_next_event)
    109 	{
    110 		/* If this is the first time that our module is loaded, then read the database. */
    111 		if (!read_channeldb())
    112 		{
    113 			char fname[512];
    114 			snprintf(fname, sizeof(fname), "%s.corrupt", cfg.database);
    115 			if (rename(cfg.database, fname) == 0)
    116 				config_warn("[channeldb] Existing database renamed to %s and starting a new one...", fname);
    117 			else
    118 				config_warn("[channeldb] Failed to rename database from %s to %s: %s", cfg.database, fname, strerror(errno));
    119 		}
    120 		channeldb_next_event = TStime() + CHANNELDB_SAVE_EVERY + CHANNELDB_SAVE_EVERY_DELTA;
    121 	}
    122 	EventAdd(modinfo->handle, "channeldb_write_channeldb", write_channeldb_evt, NULL, 1000, 0);
    123 	if (ModuleGetError(modinfo->handle) != MODERR_NOERROR)
    124 	{
    125 		config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
    126 		return MOD_FAILED;
    127 	}
    128 	return MOD_SUCCESS;
    129 }
    130 
    131 MOD_UNLOAD()
    132 {
    133 	if (loop.terminating)
    134 		write_channeldb();
    135 	freecfg(&test);
    136 	freecfg(&cfg);
    137 	SavePersistentLong(modinfo, channeldb_next_event);
    138 	return MOD_SUCCESS;
    139 }
    140 
    141 void channeldb_moddata_free(ModData *md)
    142 {
    143 	if (md->i)
    144 		md->i = 0;
    145 }
    146 
    147 void setcfg(struct cfgstruct *cfg)
    148 {
    149 	// Default: data/channel.db
    150 	safe_strdup(cfg->database, "channel.db");
    151 	convert_to_absolute_path(&cfg->database, PERMDATADIR);
    152 }
    153 
    154 void freecfg(struct cfgstruct *cfg)
    155 {
    156 	safe_free(cfg->database);
    157 	safe_free(cfg->db_secret);
    158 }
    159 
    160 int channeldb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
    161 {
    162 	int errors = 0;
    163 	ConfigEntry *cep;
    164 
    165 	// We are only interested in set::channeldb::database
    166 	if (type != CONFIG_SET)
    167 		return 0;
    168 
    169 	if (!ce || strcmp(ce->name, "channeldb"))
    170 		return 0;
    171 
    172 	for (cep = ce->items; cep; cep = cep->next)
    173 	{
    174 		if (!cep->value)
    175 		{
    176 			config_error("%s:%i: blank set::channeldb::%s without value", cep->file->filename, cep->line_number, cep->name);
    177 			errors++;
    178 		} else
    179 		if (!strcmp(cep->name, "database"))
    180 		{
    181 			convert_to_absolute_path(&cep->value, PERMDATADIR);
    182 			safe_strdup(test.database, cep->value);
    183 		} else
    184 		if (!strcmp(cep->name, "db-secret"))
    185 		{
    186 			const char *err;
    187 			if ((err = unrealdb_test_secret(cep->value)))
    188 			{
    189 				config_error("%s:%i: set::channeldb::db-secret: %s", cep->file->filename, cep->line_number, err);
    190 				errors++;
    191 				continue;
    192 			}
    193 			safe_strdup(test.db_secret, cep->value);
    194 		} else
    195 		{
    196 			config_error("%s:%i: unknown directive set::channeldb::%s", cep->file->filename, cep->line_number, cep->name);
    197 			errors++;
    198 		}
    199 	}
    200 
    201 	*errs = errors;
    202 	return errors ? -1 : 1;
    203 }
    204 
    205 int channeldb_config_posttest(int *errs)
    206 {
    207 	int errors = 0;
    208 	char *errstr;
    209 
    210 	if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret))))
    211 	{
    212 		config_error("[channeldb] %s", errstr);
    213 		errors++;
    214 	}
    215 
    216 	*errs = errors;
    217 	return errors ? -1 : 1;
    218 }
    219 
    220 int channeldb_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
    221 {
    222 	ConfigEntry *cep;
    223 
    224 	// We are only interested in set::channeldb::database
    225 	if (type != CONFIG_SET)
    226 		return 0;
    227 
    228 	if (!ce || strcmp(ce->name, "channeldb"))
    229 		return 0;
    230 
    231 	for (cep = ce->items; cep; cep = cep->next)
    232 	{
    233 		if (!strcmp(cep->name, "database"))
    234 			safe_strdup(cfg.database, cep->value);
    235 		else if (!strcmp(cep->name, "db-secret"))
    236 			safe_strdup(cfg.db_secret, cep->value);
    237 	}
    238 	return 1;
    239 }
    240 
    241 EVENT(write_channeldb_evt)
    242 {
    243 	if (channeldb_next_event > TStime())
    244 		return;
    245 	channeldb_next_event = TStime() + CHANNELDB_SAVE_EVERY;
    246 	write_channeldb();
    247 }
    248 
    249 int write_channeldb(void)
    250 {
    251 	char tmpfname[512];
    252 	UnrealDB *db;
    253 	Channel *channel;
    254 	int cnt = 0;
    255 #ifdef BENCHMARK
    256 	struct timeval tv_alpha, tv_beta;
    257 
    258 	gettimeofday(&tv_alpha, NULL);
    259 #endif
    260 
    261 	// Write to a tempfile first, then rename it if everything succeeded
    262 	snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32());
    263 	db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
    264 	if (!db)
    265 	{
    266 		WARN_WRITE_ERROR(tmpfname);
    267 		return 0;
    268 	}
    269 
    270 	W_SAFE(unrealdb_write_int32(db, channeldb_version));
    271 
    272 	/* First, count +P channels and write the count to the database */
    273 	for (channel = channels; channel; channel=channel->nextch)
    274 		if (has_channel_mode(channel, 'P'))
    275 			cnt++;
    276 	W_SAFE(unrealdb_write_int64(db, cnt));
    277 
    278 	for (channel = channels; channel; channel=channel->nextch)
    279 	{
    280 		/* We only care about +P (persistent) channels */
    281 		if (has_channel_mode(channel, 'P'))
    282 		{
    283 			if (!write_channel_entry(db, tmpfname, channel))
    284 				return 0;
    285 		}
    286 	}
    287 
    288 	// Everything seems to have gone well, attempt to close and rename the tempfile
    289 	if (!unrealdb_close(db))
    290 	{
    291 		WARN_WRITE_ERROR(tmpfname);
    292 		return 0;
    293 	}
    294 
    295 #ifdef _WIN32
    296 	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
    297 	unlink(cfg.database);
    298 #endif
    299 	if (rename(tmpfname, cfg.database) < 0)
    300 	{
    301 		config_error("[channeldb] Error renaming '%s' to '%s': %s (DATABASE NOT SAVED)", tmpfname, cfg.database, strerror(errno));
    302 		return 0;
    303 	}
    304 #ifdef BENCHMARK
    305 	gettimeofday(&tv_beta, NULL);
    306 	config_status("[channeldb] Benchmark: SAVE DB: %ld microseconds",
    307 		((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec));
    308 #endif
    309 	return 1;
    310 }
    311 
    312 int write_listmode(UnrealDB *db, const char *tmpfname, Ban *lst)
    313 {
    314 	Ban *l;
    315 	int cnt = 0;
    316 
    317 	/* First count and write the list count */
    318 	for (l = lst; l; l = l->next)
    319 		cnt++;
    320 	W_SAFE(unrealdb_write_int32(db, cnt));
    321 
    322 	for (l = lst; l; l = l->next)
    323 	{
    324 		/* The entry, setby, seton */
    325 		W_SAFE(unrealdb_write_str(db, l->banstr));
    326 		W_SAFE(unrealdb_write_str(db, l->who));
    327 		W_SAFE(unrealdb_write_int64(db, l->when));
    328 	}
    329 	return 1;
    330 }
    331 
    332 int write_channel_entry(UnrealDB *db, const char *tmpfname, Channel *channel)
    333 {
    334 	char modebuf[BUFSIZE], parabuf[BUFSIZE];
    335 
    336 	W_SAFE(unrealdb_write_int32(db, MAGIC_CHANNEL_START));
    337 	/* Channel name */
    338 	W_SAFE(unrealdb_write_str(db, channel->name));
    339 	/* Channel creation time */
    340 	W_SAFE(unrealdb_write_int64(db, channel->creationtime));
    341 	/* Topic (topic, setby, seton) */
    342 	W_SAFE(unrealdb_write_str(db, channel->topic));
    343 	W_SAFE(unrealdb_write_str(db, channel->topic_nick));
    344 	W_SAFE(unrealdb_write_int64(db, channel->topic_time));
    345 	/* Basic channel modes (eg: +sntkl key 55) */
    346 	channel_modes(&me, modebuf, parabuf, sizeof(modebuf), sizeof(parabuf), channel, 1);
    347 	W_SAFE(unrealdb_write_str(db, modebuf));
    348 	W_SAFE(unrealdb_write_str(db, parabuf));
    349 	/* Mode lock */
    350 	W_SAFE(unrealdb_write_str(db, channel->mode_lock));
    351 	/* List modes (bans, exempts, invex) */
    352 	if (!write_listmode(db, tmpfname, channel->banlist))
    353 		return 0;
    354 	if (!write_listmode(db, tmpfname, channel->exlist))
    355 		return 0;
    356 	if (!write_listmode(db, tmpfname, channel->invexlist))
    357 		return 0;
    358 	W_SAFE(unrealdb_write_int32(db, MAGIC_CHANNEL_END));
    359 	return 1;
    360 }
    361 
    362 #define R_SAFE(x) \
    363 	do { \
    364 		if (!(x)) { \
    365 			config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
    366 			if (e) \
    367 			{ \
    368 				safe_free(e->banstr); \
    369 				safe_free(e->who); \
    370 				safe_free(e); \
    371 			} \
    372 			return 0; \
    373 		} \
    374 	} while(0)
    375 
    376 int read_listmode(UnrealDB *db, Ban **lst)
    377 {
    378 	uint32_t total;
    379 	uint64_t when;
    380 	int i;
    381 	Ban *e = NULL;
    382 
    383 	R_SAFE(unrealdb_read_int32(db, &total));
    384 
    385 	for (i = 0; i < total; i++)
    386 	{
    387 		const char *str;
    388 		e = safe_alloc(sizeof(Ban));
    389 		R_SAFE(unrealdb_read_str(db, &e->banstr));
    390 		R_SAFE(unrealdb_read_str(db, &e->who));
    391 		R_SAFE(unrealdb_read_int64(db, &when));
    392 		str = clean_ban_mask(e->banstr, MODE_ADD, &me, 0);
    393 		if (str == NULL)
    394 		{
    395 			/* Skip this item */
    396 			config_warn("[channeldb] listmode skipped (no longer valid?): %s", e->banstr);
    397 			safe_free(e->banstr);
    398 			safe_free(e->who);
    399 			safe_free(e);
    400 			continue;
    401 		}
    402 		safe_strdup(e->banstr, str);
    403 
    404 		if (ban_exists(*lst, e->banstr))
    405 		{
    406 			/* Free again - duplicate item */
    407 			safe_free(e->banstr);
    408 			safe_free(e->who);
    409 			safe_free(e);
    410 		} else {
    411 			/* Add to list */
    412 			e->when = when;
    413 			e->next = *lst;
    414 			*lst = e;
    415 		}
    416 	}
    417 
    418 	return 1;
    419 }
    420 #undef R_SAFE
    421 
    422 #define FreeChannelEntry() \
    423  	do { \
    424 		/* Some of these might be NULL */ \
    425 		safe_free(chname); \
    426 		safe_free(topic); \
    427 		safe_free(topic_nick); \
    428 		safe_free(modes1); \
    429 		safe_free(modes2); \
    430 		safe_free(mode_lock); \
    431 	} while(0)
    432 
    433 #define R_SAFE(x) \
    434 	do { \
    435 		if (!(x)) { \
    436 			config_warn("[channeldb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \
    437 			unrealdb_close(db); \
    438 			FreeChannelEntry(); \
    439 			return 0; \
    440 		} \
    441 	} while(0)
    442 
    443 int read_channeldb(void)
    444 {
    445 	UnrealDB *db;
    446 	uint32_t version;
    447 	int added = 0;
    448 	int i;
    449 	uint64_t count = 0;
    450 	uint32_t magic;
    451 	// Variables for the channels
    452 	// Some of them need to be declared and NULL initialised early due to the macro FreeChannelEntry() being used by R_SAFE() on error
    453 	char *chname = NULL;
    454 	uint64_t creationtime = 0;
    455 	char *topic = NULL;
    456 	char *topic_nick = NULL;
    457 	uint64_t topic_time = 0;
    458 	char *modes1 = NULL;
    459 	char *modes2 = NULL;
    460 	char *mode_lock = NULL;
    461 #ifdef BENCHMARK
    462 	struct timeval tv_alpha, tv_beta;
    463 
    464 	gettimeofday(&tv_alpha, NULL);
    465 #endif
    466 
    467 	db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret);
    468 	if (!db)
    469 	{
    470 		if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
    471 		{
    472 			/* Database does not exist. Could be first boot */
    473 			config_warn("[channeldb] No database present at '%s', will start a new one", cfg.database);
    474 			return 1;
    475 		} else
    476 		if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED)
    477 		{
    478 			/* Re-open as unencrypted */
    479 			db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL);
    480 			if (!db)
    481 			{
    482 				/* This should actually never happen, unless some weird I/O error */
    483 				config_warn("[channeldb] Unable to open the database file '%s': %s", cfg.database, unrealdb_get_error_string());
    484 				return 0;
    485 			}
    486 		} else
    487 		{
    488 			config_warn("[channeldb] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string());
    489 			return 0;
    490 		}
    491 	}
    492 
    493 	R_SAFE(unrealdb_read_int32(db, &version));
    494 	if (version > channeldb_version)
    495 	{
    496 		config_warn("[channeldb] Database '%s' has a wrong version: expected it to be <= %u but got %u instead", cfg.database, channeldb_version, version);
    497 		unrealdb_close(db);
    498 		return 0;
    499 	}
    500 
    501 	R_SAFE(unrealdb_read_int64(db, &count));
    502 
    503 	for (i=1; i <= count; i++)
    504 	{
    505 		// Variables
    506 		chname = NULL;
    507 		creationtime = 0;
    508 		topic = NULL;
    509 		topic_nick = NULL;
    510 		topic_time = 0;
    511 		modes1 = NULL;
    512 		modes2 = NULL;
    513 		mode_lock = NULL;
    514 		
    515 		Channel *channel;
    516 		R_SAFE(unrealdb_read_int32(db, &magic));
    517 		if (magic != MAGIC_CHANNEL_START)
    518 		{
    519 			config_error("[channeldb] Corrupt database (%s) - channel magic start is 0x%x. Further reading aborted.", cfg.database, magic);
    520 			break;
    521 		}
    522 		R_SAFE(unrealdb_read_str(db, &chname));
    523 		R_SAFE(unrealdb_read_int64(db, &creationtime));
    524 		R_SAFE(unrealdb_read_str(db, &topic));
    525 		R_SAFE(unrealdb_read_str(db, &topic_nick));
    526 		R_SAFE(unrealdb_read_int64(db, &topic_time));
    527 		R_SAFE(unrealdb_read_str(db, &modes1));
    528 		R_SAFE(unrealdb_read_str(db, &modes2));
    529 		R_SAFE(unrealdb_read_str(db, &mode_lock));
    530 		/* If we got this far, we can create/initialize the channel with the above */
    531 		channel = make_channel(chname);
    532 		if (IsInvalidChannelTS(creationtime))
    533 			channel->creationtime = TStime();
    534 		else
    535 			channel->creationtime = creationtime;
    536 		safe_strdup(channel->topic, topic);
    537 		safe_strdup(channel->topic_nick, topic_nick);
    538 		channel->topic_time = topic_time;
    539 		safe_strdup(channel->mode_lock, mode_lock);
    540 		set_channel_mode(channel, NULL, modes1, modes2);
    541 		R_SAFE(read_listmode(db, &channel->banlist));
    542 		R_SAFE(read_listmode(db, &channel->exlist));
    543 		R_SAFE(read_listmode(db, &channel->invexlist));
    544 		R_SAFE(unrealdb_read_int32(db, &magic));
    545 		FreeChannelEntry();
    546 		added++;
    547 		if (magic != MAGIC_CHANNEL_END)
    548 		{
    549 			config_error("[channeldb] Corrupt database (%s) - channel magic end is 0x%x. Further reading aborted.", cfg.database, magic);
    550 			break;
    551 		}
    552 	}
    553 
    554 	unrealdb_close(db);
    555 
    556 	if (added)
    557 		config_status("[channeldb] Added %d persistent channels (+P)", added);
    558 #ifdef BENCHMARK
    559 	gettimeofday(&tv_beta, NULL);
    560 	unreal_log(ULOG_DEBUG, "channeldb", "CHANNELDB_BENCHMARK", NULL,
    561 	           "[channeldb] Benchmark: LOAD DB: $time_msec microseconds",
    562 	           log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)));
    563 #endif
    564 	return 1;
    565 }
    566 #undef FreeChannelEntry
    567 #undef R_SAFE