unrealircd

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

history_backend_mem.c (42883B)

      1 /* src/modules/history_backend_mem.c - History Backend: memory
      2  * (C) Copyright 2019-2021 Bram Matthys (Syzop) and the UnrealIRCd team
      3  * License: GPLv2 or later
      4  */
      5 #include "unrealircd.h"
      6 
      7 /* This is the memory type backend. It is optimized for speed.
      8  * For example, per-channel, it caches the field "number of lines"
      9  * and "oldest record", so frequent cleaning operations such as
     10  * "delete any record older than time T" or "keep only N lines"
     11  * are executed as fast as possible.
     12  */
     13 
     14 ModuleHeader MOD_HEADER
     15 = {
     16 	"history_backend_mem",
     17 	"2.0",
     18 	"History backend: memory",
     19 	"UnrealIRCd Team",
     20 	"unrealircd-6",
     21 };
     22 
     23 /* Defines */
     24 #define OBJECTLEN	((NICKLEN > CHANNELLEN) ? NICKLEN : CHANNELLEN)
     25 #define HISTORY_BACKEND_MEM_HASH_TABLE_SIZE 1019
     26 
     27 /* The regular history cleaning (by timer) is spread out
     28  * a bit, rather than doing ALL channels every T time.
     29  * HISTORY_SPREAD: how much to spread the "cleaning", eg 1 would be
     30  *  to clean everything in 1 go, 2 would mean the first event would
     31  *  clean half of the channels, and the 2nd event would clean the rest.
     32  *  Obviously more = better to spread the load, but doing a reasonable
     33  *  amount of work is also benefitial for performance (think: CPU cache).
     34  * HISTORY_MAX_OFF_SECS: how many seconds may the history be 'off',
     35  *  that is: how much may we store the history longer than required.
     36  * The other 2 macros are calculated based on that target.
     37  *
     38  * Update April 2021: these values are now also used for saving the
     39  * history if the persistent option is enabled. Therefore changed the
     40  * values to spread it even more out: from 16/128 to 60/300 so
     41  * in case of persistent it will save every 5 minutes.
     42  */
     43 #if 0 //was: DEBUGMODE
     44 #define HISTORY_CLEAN_PER_LOOP HISTORY_BACKEND_MEM_HASH_TABLE_SIZE
     45 #define HISTORY_TIMER_EVERY 5
     46 #else
     47 #define HISTORY_SPREAD	60
     48 #define HISTORY_MAX_OFF_SECS	300
     49 #define HISTORY_CLEAN_PER_LOOP	(HISTORY_BACKEND_MEM_HASH_TABLE_SIZE/HISTORY_SPREAD)
     50 #define HISTORY_TIMER_EVERY	(HISTORY_MAX_OFF_SECS/HISTORY_SPREAD)
     51 #endif
     52 
     53 /* Some magic numbers used in the database format */
     54 #define HISTORYDB_MAGIC_FILE_START	0xFEFEFEFE
     55 #define HISTORYDB_MAGIC_FILE_END	0xEFEFEFEF
     56 #define HISTORYDB_MAGIC_ENTRY_START	0xFFFFFFFF
     57 #define HISTORYDB_MAGIC_ENTRY_END	0xEEEEEEEE
     58 
     59 /* Definitions (structs, etc.) -- all for persistent history */
     60 struct cfgstruct {
     61 	int persist;
     62 	char *directory;
     63 	char *masterdb; /* Autogenerated for convenience, not a real config item */
     64 	char *db_secret;
     65 };
     66 
     67 typedef struct HistoryLogObject HistoryLogObject;
     68 struct HistoryLogObject {
     69 	HistoryLogObject *prev, *next;
     70 	HistoryLogLine *head; /**< Start of the log (the earliest entry) */
     71 	HistoryLogLine *tail; /**< End of the log (the latest entry) */
     72 	int num_lines; /**< Number of lines of log */
     73 	time_t oldest_t; /**< Oldest time in log */
     74 	int max_lines; /**< Maximum number of lines permitted */
     75 	long max_time; /**< Maximum number of seconds to retain history */
     76 	int dirty; /**< Dirty flag, used for disk writing */
     77 	char name[OBJECTLEN+1];
     78 };
     79 
     80 /* Global variables */
     81 struct cfgstruct cfg;
     82 struct cfgstruct test;
     83 static char *siphashkey_history_backend_mem = NULL;
     84 HistoryLogObject **history_hash_table;
     85 static long already_loaded = 0;
     86 static char *hbm_prehash = NULL;
     87 static char *hbm_posthash = NULL;
     88 
     89 /* Forward declarations */
     90 int hbm_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
     91 int hbm_config_posttest(int *errs);
     92 int hbm_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
     93 int hbm_rehash(void);
     94 int hbm_rehash_complete(void);
     95 static void setcfg(struct cfgstruct *cfg);
     96 static void freecfg(struct cfgstruct *cfg);
     97 static void hbm_init_hashes(ModuleInfo *m);
     98 static void init_history_storage(ModuleInfo *modinfo);
     99 int hbm_modechar_del(Channel *channel, int modechar);
    100 int hbm_history_add(const char *object, MessageTag *mtags, const char *line);
    101 int hbm_history_cleanup(HistoryLogObject *h);
    102 HistoryResult *hbm_history_request(const char *object, HistoryFilter *filter);
    103 int hbm_history_destroy(const char *object);
    104 int hbm_history_set_limit(const char *object, int max_lines, long max_time);
    105 EVENT(history_mem_clean);
    106 EVENT(history_mem_init);
    107 static int hbm_read_masterdb(void);
    108 static void hbm_read_dbs(void);
    109 static int hbm_read_db(const char *fname);
    110 static int hbm_write_masterdb(void);
    111 static int hbm_write_db(HistoryLogObject *h);
    112 static void hbm_delete_db(HistoryLogObject *h);
    113 static void hbm_flush(void);
    114 void hbm_generic_free(ModData *m);
    115 void hbm_free_all_history(ModData *m);
    116 
    117 MOD_TEST()
    118 {
    119 	hbm_init_hashes(modinfo);
    120 	memset(&cfg, 0, sizeof(cfg));
    121 	memset(&test, 0, sizeof(test));
    122 	setcfg(&test);
    123 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, hbm_config_test);
    124 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, hbm_config_posttest);
    125 
    126 	return MOD_SUCCESS;
    127 }
    128 
    129 MOD_INIT()
    130 {
    131 	HistoryBackendInfo hbi;
    132 
    133 	MARK_AS_OFFICIAL_MODULE(modinfo);
    134 	/* We must unload early, when all channel modes and such are still in place: */
    135 	ModuleSetOptions(modinfo->handle, MOD_OPT_PRIORITY, -99999999);
    136 
    137 	setcfg(&cfg);
    138 
    139 	LoadPersistentLong(modinfo, already_loaded);
    140 	LoadPersistentPointer(modinfo, siphashkey_history_backend_mem, hbm_generic_free);
    141 	LoadPersistentPointer(modinfo, history_hash_table, hbm_free_all_history);
    142 	if (history_hash_table == NULL)
    143 		history_hash_table = safe_alloc(sizeof(HistoryLogObject *) * HISTORY_BACKEND_MEM_HASH_TABLE_SIZE);
    144 	/* hbm_prehash & hbm_posthash already loaded in MOD_TEST through hbm_init_hashes() */
    145 
    146 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, hbm_config_run);
    147 	HookAdd(modinfo->handle, HOOKTYPE_MODECHAR_DEL, 0, hbm_modechar_del);
    148 	HookAdd(modinfo->handle, HOOKTYPE_REHASH, 0, hbm_rehash);
    149 	HookAdd(modinfo->handle, HOOKTYPE_REHASH_COMPLETE, 0, hbm_rehash_complete);
    150 
    151 	if (siphashkey_history_backend_mem == NULL)
    152 	{
    153 		siphashkey_history_backend_mem = safe_alloc(SIPHASH_KEY_LENGTH);
    154 		siphash_generate_key(siphashkey_history_backend_mem);
    155 	}
    156 
    157 	memset(&hbi, 0, sizeof(hbi));
    158 	hbi.name = "mem";
    159 	hbi.history_add = hbm_history_add;
    160 	hbi.history_request = hbm_history_request;
    161 	hbi.history_destroy = hbm_history_destroy;
    162 	hbi.history_set_limit = hbm_history_set_limit;
    163 	if (!HistoryBackendAdd(modinfo->handle, &hbi))
    164 		return MOD_FAILED;
    165 
    166 	return MOD_SUCCESS;
    167 }
    168 
    169 MOD_LOAD()
    170 {
    171 	/* Need to save these here already (after conf reading these are set),
    172 	 * as on next round the module reads it in TEST which happens before
    173 	 * the saving in MOD_UNLOAD:
    174 	 */
    175 	SavePersistentPointer(modinfo, hbm_prehash);
    176 	SavePersistentPointer(modinfo, hbm_posthash);
    177 
    178 	EventAdd(modinfo->handle, "history_mem_init", history_mem_init, NULL, 1, 1);
    179 	EventAdd(modinfo->handle, "history_mem_clean", history_mem_clean, NULL, HISTORY_TIMER_EVERY*1000, 0);
    180 	init_history_storage(modinfo);
    181 	return MOD_SUCCESS;
    182 }
    183 
    184 /* Read the .db if 'persist' mode is enabled.
    185  * Normally this would be in MOD_LOAD, but the load order always
    186  * must be: channeldb first, this module second, and since we
    187  * cannot influence the load order we do this silly trick
    188  * with a one-time 1msec event.
    189  */
    190 EVENT(history_mem_init)
    191 {
    192 	if (!already_loaded)
    193 	{
    194 		/* Initial boot / load of the module... */
    195 		already_loaded = 1;
    196 		if (cfg.persist)
    197 			hbm_read_dbs();
    198 	}
    199 }
    200 
    201 MOD_UNLOAD()
    202 {
    203 	if (loop.terminating)
    204 		hbm_flush();
    205 	freecfg(&test);
    206 	freecfg(&cfg);
    207 	SavePersistentPointer(modinfo, hbm_prehash);
    208 	SavePersistentPointer(modinfo, hbm_posthash);
    209 	SavePersistentPointer(modinfo, history_hash_table);
    210 	SavePersistentPointer(modinfo, siphashkey_history_backend_mem);
    211 	SavePersistentLong(modinfo, already_loaded);
    212 	return MOD_SUCCESS;
    213 }
    214 
    215 /** Set cfg->masterdb based on cfg->directory, for convenience */
    216 static void hbm_set_masterdb_filename(struct cfgstruct *cfg)
    217 {
    218 	char buf[512];
    219 
    220 	safe_free(cfg->masterdb);
    221 	if (cfg->directory)
    222 	{
    223 		snprintf(buf, sizeof(buf), "%s/master.db", cfg->directory);
    224 		safe_strdup(cfg->masterdb, buf);
    225 	}
    226 }
    227 
    228 /** Default configuration for set::history::channel */
    229 static void setcfg(struct cfgstruct *cfg)
    230 {
    231 	safe_strdup(cfg->directory, "history");
    232 	convert_to_absolute_path(&cfg->directory, PERMDATADIR);
    233 	hbm_set_masterdb_filename(cfg);
    234 }
    235 
    236 static void freecfg(struct cfgstruct *cfg)
    237 {
    238 	safe_free(cfg->masterdb);
    239 	safe_free(cfg->directory);
    240 	safe_free(cfg->db_secret);
    241 }
    242 
    243 static void hbm_init_hashes(ModuleInfo *modinfo)
    244 {
    245 	char buf[256];
    246 
    247 	LoadPersistentPointer(modinfo, hbm_prehash, hbm_generic_free);
    248 	LoadPersistentPointer(modinfo, hbm_posthash, hbm_generic_free);
    249 
    250 	if (!hbm_prehash)
    251 	{
    252 		gen_random_alnum(buf, 128);
    253 		safe_strdup(hbm_prehash, buf);
    254 	}
    255 
    256 	if (!hbm_posthash)
    257 	{
    258 		gen_random_alnum(buf, 128);
    259 		safe_strdup(hbm_posthash, buf);
    260 	}
    261 }
    262 
    263 /** Test the set::history::channel configuration */
    264 int hbm_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
    265 {
    266 	int errors = 0;
    267 
    268 	if ((type != CONFIG_SET_HISTORY_CHANNEL) || !ce || !ce->name)
    269 		return 0;
    270 
    271 	if (!strcmp(ce->name, "persist"))
    272 	{
    273 		if (!ce->value)
    274 		{
    275 			config_error("%s:%i: missing parameter",
    276 				ce->file->filename, ce->line_number);
    277 			errors++;
    278 		} else {
    279 			test.persist = config_checkval(ce->value, CFG_YESNO);
    280 		}
    281 	} else
    282 	if (!strcmp(ce->name, "db-secret"))
    283 	{
    284 		const char *err;
    285 		if ((err = unrealdb_test_secret(ce->value)))
    286 		{
    287 			config_error("%s:%i: set::history::channel::db-secret: %s", ce->file->filename, ce->line_number, err);
    288 			errors++;
    289 		}
    290 		safe_strdup(test.db_secret, ce->value);
    291 	} else
    292 	if (!strcmp(ce->name, "directory")) // or "path" ?
    293 	{
    294 		if (!ce->value)
    295 		{
    296 			config_error("%s:%i: missing parameter",
    297 				ce->file->filename, ce->line_number);
    298 			errors++;
    299 		} else
    300 		{
    301 			safe_strdup(test.directory, ce->value);
    302 			hbm_set_masterdb_filename(&test);
    303 		}
    304 	} else
    305 	{
    306 		return 0; /* unknown option to us, let another module handle it */
    307 	}
    308 
    309 	*errs = errors;
    310 	return errors ? -1 : 1;
    311 }
    312 
    313 /** Post-configuration test on set::history::channel */
    314 int hbm_config_posttest(int *errs)
    315 {
    316 	int errors = 0;
    317 
    318 	if (test.db_secret && !test.persist)
    319 	{
    320 		config_error("set::history::channel::db-secret is set but set::history::channel::persist is disabled, this makes no sense. "
    321 			     "Either use 'persist yes' or comment out / delete 'db-secret'.");
    322 		errors++;
    323 	} else
    324 	if (!test.db_secret && test.persist)
    325 	{
    326 		config_error("set::history::channel::db-secret needs to be set.");
    327 		errors++;
    328 	} else
    329 	if (test.db_secret && test.persist)
    330 	{
    331 		/* Configuration is good, now check if the password is correct
    332 		 * (if we can check at all, that is)...
    333 		 */
    334 		char *errstr = NULL;
    335 		if (test.masterdb && ((errstr = unrealdb_test_db(test.masterdb, test.db_secret))))
    336 		{
    337 			config_error("[history] %s", errstr);
    338 			errors++;
    339 			goto hbm_config_posttest_end;
    340 		}
    341 
    342 		/* Ensure directory exists and is writable */
    343 #ifdef _WIN32
    344 		(void)mkdir(test.directory); /* (errors ignored) */
    345 #else
    346 		(void)mkdir(test.directory, S_IRUSR|S_IWUSR|S_IXUSR); /* (errors ignored) */
    347 #endif
    348 		if (!file_exists(test.directory))
    349 		{
    350 			config_error("[history] Directory %s does not exist and could not be created",
    351 				test.directory);
    352 			errors++;
    353 		} else
    354 		{
    355 			/* Only do this if directory actually exists, hence in the 'else' block */
    356 			if (!hbm_read_masterdb())
    357 				errors++;
    358 		}
    359 	}
    360 
    361 hbm_config_posttest_end:
    362 	freecfg(&test);
    363 	setcfg(&test);
    364 	*errs = errors;
    365 	return errors ? -1 : 1;
    366 }
    367 
    368 /** Configure ourselves based on the set::history::channel settings */
    369 int hbm_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
    370 {
    371 	if ((type != CONFIG_SET_HISTORY_CHANNEL) || !ce || !ce->name)
    372 		return 0;
    373 
    374 	if (!strcmp(ce->name, "persist"))
    375 	{
    376 		cfg.persist = config_checkval(ce->value, CFG_YESNO);
    377 	} else
    378 	if (!strcmp(ce->name, "directory")) // or "path" ?
    379 	{
    380 		safe_strdup(cfg.directory, ce->value);
    381 		convert_to_absolute_path(&cfg.directory, PERMDATADIR);
    382 		hbm_set_masterdb_filename(&cfg);
    383 	} else
    384 	if (!strcmp(ce->name, "db-secret"))
    385 	{
    386 		safe_strdup(cfg.db_secret, ce->value);
    387 	} else
    388 	{
    389 		return 0; /* unknown option to us, let another module handle it */
    390 	}
    391 
    392 	return 1; /* handled by us */
    393 }
    394 
    395 int hbm_rehash(void)
    396 {
    397 	freecfg(&cfg);
    398 	setcfg(&cfg);
    399 	return 0;
    400 }
    401 
    402 int hbm_rehash_complete(void)
    403 {
    404 	return 0;
    405 }
    406 
    407 const char *history_storage_capability_parameter(Client *client)
    408 {
    409 	static char buf[128];
    410 
    411 	if (cfg.persist)
    412 		strlcpy(buf, "memory,disk=encrypted", sizeof(buf));
    413 	else
    414 		strlcpy(buf, "memory", sizeof(buf));
    415 
    416 	return buf;
    417 }
    418 
    419 static void init_history_storage(ModuleInfo *modinfo)
    420 {
    421 	ClientCapabilityInfo cap;
    422 
    423 	memset(&cap, 0, sizeof(cap));
    424 	cap.name = "unrealircd.org/history-storage";
    425 	cap.flags = CLICAP_FLAGS_ADVERTISE_ONLY;
    426 	cap.parameter = history_storage_capability_parameter;
    427 	ClientCapabilityAdd(modinfo->handle, &cap, NULL);
    428 }
    429 
    430 uint64_t hbm_hash(const char *object)
    431 {
    432 	return siphash_nocase(object, siphashkey_history_backend_mem) % HISTORY_BACKEND_MEM_HASH_TABLE_SIZE;
    433 }
    434 
    435 HistoryLogObject *hbm_find_object(const char *object)
    436 {
    437 	int hashv = hbm_hash(object);
    438 	HistoryLogObject *h;
    439 
    440 	for (h = history_hash_table[hashv]; h; h = h->next)
    441 	{
    442 		if (!strcasecmp(object, h->name))
    443 			return h;
    444 	}
    445 	return NULL;
    446 }
    447 
    448 HistoryLogObject *hbm_find_or_add_object(const char *object)
    449 {
    450 	int hashv = hbm_hash(object);
    451 	HistoryLogObject *h;
    452 
    453 	for (h = history_hash_table[hashv]; h; h = h->next)
    454 	{
    455 		if (!strcasecmp(object, h->name))
    456 			return h;
    457 	}
    458 	/* Create new one */
    459 	h = safe_alloc(sizeof(HistoryLogObject));
    460 	strlcpy(h->name, object, sizeof(h->name));
    461 	AddListItem(h, history_hash_table[hashv]);
    462 	return h;
    463 }
    464 
    465 void hbm_delete_object_hlo(HistoryLogObject *h)
    466 {
    467 	int hashv;
    468 
    469 	if (cfg.persist)
    470 		hbm_delete_db(h);
    471 
    472 	hashv = hbm_hash(h->name);
    473 	DelListItem(h, history_hash_table[hashv]);
    474 	safe_free(h);
    475 }
    476 
    477 int hbm_modechar_del(Channel *channel, int modechar)
    478 {
    479 	HistoryLogObject *h;
    480 
    481 	if (!cfg.persist)
    482 		return 0;
    483 
    484 	if ((modechar == 'P') && ((h = hbm_find_object(channel->name))))
    485 	{
    486 		/* Channel went from +P to -P and also has channel history: delete the history file */
    487 		hbm_delete_db(h);
    488 
    489 		h->dirty = 1;
    490 		/* The reason for marking the entry as 'dirty' is that someone may later
    491 		 * set the channel +P again. If we would not set the h->dirty=1 then this
    492 		 * would mean the history log would not get rewritten until someone speaks.
    493 		 */
    494 	}
    495 
    496 	return 0;
    497 }
    498 
    499 void hbm_duplicate_mtags(HistoryLogLine *l, MessageTag *m)
    500 {
    501 	MessageTag *n;
    502 
    503 	/* Duplicate all message tags */
    504 	for (; m; m = m->next)
    505 	{
    506 		n = duplicate_mtag(m);
    507 		AppendListItem(n, l->mtags);
    508 	}
    509 	n = find_mtag(l->mtags, "time");
    510 	if (!n)
    511 	{
    512 		/* This is duplicate code from src/modules/server-time.c
    513 		 * which seems silly.
    514 		 */
    515 		struct timeval t;
    516 		struct tm *tm;
    517 		time_t sec;
    518 		char buf[64];
    519 
    520 		gettimeofday(&t, NULL);
    521 		sec = t.tv_sec;
    522 		tm = gmtime(&sec);
    523 		snprintf(buf, sizeof(buf), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
    524 			tm->tm_year + 1900,
    525 			tm->tm_mon + 1,
    526 			tm->tm_mday,
    527 			tm->tm_hour,
    528 			tm->tm_min,
    529 			tm->tm_sec,
    530 			(int)(t.tv_usec / 1000));
    531 
    532 		n = safe_alloc(sizeof(MessageTag));
    533 		safe_strdup(n->name, "time");
    534 		safe_strdup(n->value, buf);
    535 		AddListItem(n, l->mtags);
    536 	}
    537 	/* Now convert the "time" message tag to something we can use in l->t */
    538 	l->t = server_time_to_unix_time(n->value);
    539 }
    540 
    541 /** Add a line to a history object */
    542 void hbm_history_add_line(HistoryLogObject *h, MessageTag *mtags, const char *line)
    543 {
    544 	HistoryLogLine *l = safe_alloc(sizeof(HistoryLogLine) + strlen(line));
    545 	strcpy(l->line, line); /* safe, see memory allocation above ^ */
    546 	hbm_duplicate_mtags(l, mtags);
    547 	if (h->tail)
    548 	{
    549 		/* append to tail */
    550 		h->tail->next = l;
    551 		l->prev = h->tail;
    552 		h->tail = l;
    553 	} else {
    554 		/* no tail, no head */
    555 		h->head = h->tail = l;
    556 	}
    557 	h->dirty = 1;
    558 	h->num_lines++;
    559 	if ((l->t < h->oldest_t) || (h->oldest_t == 0))
    560 		h->oldest_t = l->t;
    561 }
    562 
    563 /** Delete a line from a history object */
    564 void hbm_history_del_line(HistoryLogObject *h, HistoryLogLine *l)
    565 {
    566 	if (l->prev)
    567 		l->prev->next = l->next;
    568 	if (l->next)
    569 		l->next->prev = l->prev;
    570 	if (h->head == l)
    571 	{
    572 		/* New head */
    573 		h->head = l->next;
    574 	}
    575 	if (h->tail == l)
    576 	{
    577 		/* New tail */
    578 		h->tail = l->prev; /* could be NULL now */
    579 	}
    580 
    581 	free_message_tags(l->mtags);
    582 	safe_free(l);
    583 
    584 	h->dirty = 1;
    585 	h->num_lines--;
    586 
    587 	/* IMPORTANT: updating h->oldest_t takes place at the caller
    588 	 * because it is in a better position to optimize the process
    589 	 */
    590 }
    591 
    592 /** Add history entry */
    593 int hbm_history_add(const char *object, MessageTag *mtags, const char *line)
    594 {
    595 	HistoryLogObject *h = hbm_find_or_add_object(object);
    596 	if (!h->max_lines)
    597 	{
    598 		unreal_log(ULOG_WARNING, "history", "BUG_HISTORY_ADD_NO_LIMIT", NULL,
    599 		           "[BUG] hbm_history_add() called for $object, which has no limit set",
    600 		           log_data_string("object", h->name));
    601 #ifdef DEBUGMODE
    602 		abort();
    603 #else
    604 		h->max_lines = 50;
    605 		h->max_time = 86400;
    606 #endif
    607 	}
    608 	if (h->num_lines >= h->max_lines)
    609 	{
    610 		/* Delete previous line */
    611 		hbm_history_del_line(h, h->head);
    612 	}
    613 	hbm_history_add_line(h, mtags, line);
    614 	return 0;
    615 }
    616 
    617 HistoryLogLine *duplicate_log_line(HistoryLogLine *l)
    618 {
    619 	HistoryLogLine *n = safe_alloc(sizeof(HistoryLogLine) + strlen(l->line));
    620 	strcpy(n->line, l->line); /* safe, see memory allocation above ^ */
    621 	hbm_duplicate_mtags(n, l->mtags);
    622 	return n;
    623 }
    624 
    625 /** Quickly append a new line 'n' to result 'r' */
    626 static void hbm_result_append_line(HistoryResult *r, HistoryLogLine *n)
    627 {
    628 	if (!r->log)
    629 	{
    630 		/* First item */
    631 		r->log = r->log_tail = n;
    632 	} else
    633 	{
    634 		/* Quick append to tail */
    635 		r->log_tail->next = n;
    636 		n->prev = r->log_tail;
    637 		r->log_tail = n; /* we are the new tail */
    638 	}
    639 }
    640 
    641 /** Quickly prepend a new line 'n' to result 'r' */
    642 static void hbm_result_prepend_line(HistoryResult *r, HistoryLogLine *n)
    643 {
    644 	if (!r->log)
    645 		r->log_tail = n;
    646 	AddListItem(n, r->log);
    647 }
    648 
    649 /** Put lines in HistoryResult that are after a certain msgid or
    650  *  timestamp (excluding said msgid/timestamp).
    651  * @param r		The history result set that we will use
    652  * @param h		The history log object
    653  * @param filter	The filter that applies
    654  * @returns Number of lines written, note that this could be zero,
    655  *          which is a perfectly valid result.
    656  */
    657 static int hbm_return_after(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
    658 {
    659 	HistoryLogLine *l, *n;
    660 	int written = 0;
    661 	int started = 0;
    662 	MessageTag *m;
    663 
    664 	for (l = h->head; l; l = l->next)
    665 	{
    666 		/* Not started yet? Check if this is the starting point... */
    667 		if (!started)
    668 		{
    669 			if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) > 0))
    670 			{
    671 				started = 1;
    672 			} else
    673 			if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
    674 			{
    675 				started = 1;
    676 				continue;
    677 			}
    678 		}
    679 		if (started)
    680 		{
    681 			/* Check if we need to stop */
    682 			if (filter->timestamp_b && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_b) >= 0))
    683 			{
    684 				break;
    685 			} else
    686 			if (filter->msgid_b && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_b))
    687 			{
    688 				break;
    689 			}
    690 
    691 			/* Add line to the return buffer */
    692 			n = duplicate_log_line(l);
    693 			hbm_result_append_line(r, n);
    694 			if (++written >= filter->limit)
    695 				break;
    696 		}
    697 	}
    698 
    699 	return written;
    700 }
    701 
    702 /** Put lines in HistoryResult that before after a certain msgid or
    703  *  timestamp (excluding said msgid/timestamp).
    704  * @param r		The history result set that we will use
    705  * @param h		The history log object
    706  * @param filter	The filter that applies
    707  * @returns Number of lines written, note that this could be zero,
    708  *          which is a perfectly valid result.
    709  */
    710 static int hbm_return_before(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
    711 {
    712 	HistoryLogLine *l, *n;
    713 	int written = 0;
    714 	int started = 0;
    715 	MessageTag *m;
    716 
    717 	for (l = h->tail; l; l = l->prev)
    718 	{
    719 		/* Not started yet? Check if this is the starting point... */
    720 		if (!started)
    721 		{
    722 			if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) < 0))
    723 			{
    724 				started = 1;
    725 			} else
    726 			if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
    727 			{
    728 				started = 1;
    729 				continue;
    730 			}
    731 		}
    732 		if (started)
    733 		{
    734 			/* Check if we need to stop */
    735 			if (filter->timestamp_b && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_b) < 0))
    736 			{
    737 				break;
    738 			} else
    739 			if (filter->msgid_b && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_b))
    740 			{
    741 				break;
    742 			}
    743 
    744 			/* Add line to the return buffer */
    745 			n = duplicate_log_line(l);
    746 			hbm_result_prepend_line(r, n);
    747 			if (++written >= filter->limit)
    748 				break;
    749 		}
    750 	}
    751 
    752 	return written;
    753 }
    754 
    755 /** Put lines in HistoryResult that are 'latest'
    756  * @param r		The history result set that we will use
    757  * @param h		The history log object
    758  * @param filter	The filter that applies
    759  * @returns Number of lines written, note that this could be zero,
    760  *          which is a perfectly valid result.
    761  */
    762 static int hbm_return_latest(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
    763 {
    764 	HistoryLogLine *l, *n;
    765 	int written = 0;
    766 	MessageTag *m;
    767 
    768 	for (l = h->tail; l; l = l->prev)
    769 	{
    770 		if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) <= 0))
    771 			break; /* Stop now */
    772 		else
    773 		if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
    774 			break; /* Stop now */
    775 
    776 		n = duplicate_log_line(l);
    777 		hbm_result_prepend_line(r, n);
    778 		if (++written >= filter->limit)
    779 			break;
    780 	}
    781 
    782 	return written;
    783 }
    784 
    785 /** Put lines in HistoryResult based on a 'simple' request, that is: maximum lines or time
    786  * @param r		The history result set that we will use
    787  * @param h		The history log object
    788  * @param filter	The filter that applies
    789  * @returns Number of lines written, note that this could be zero,
    790  *          which is a perfectly valid result.
    791  */
    792 static int hbm_return_simple(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
    793 {
    794 	HistoryLogLine *l;
    795 	int lines_sendable = 0, lines_to_skip = 0, cnt = 0;
    796 	long redline;
    797 	int written = 0;
    798 
    799 	/* Decide on red line, under this the history is too old.
    800 	 * Filter can be more strict than history object (but not the other way around):
    801 	 */
    802 	if (filter && filter->last_seconds && (filter->last_seconds < h->max_time))
    803 		redline = TStime() - filter->last_seconds;
    804 	else
    805 		redline = TStime() - h->max_time;
    806 
    807 	/* Once the filter API expands, the following will change too.
    808 	 * For now, this is sufficient, since requests are only about lines:
    809 	 */
    810 	lines_sendable = 0;
    811 	for (l = h->head; l; l = l->next)
    812 		if (l->t >= redline)
    813 			lines_sendable++;
    814 	if (filter && (lines_sendable > filter->last_lines))
    815 		lines_to_skip = lines_sendable - filter->last_lines;
    816 
    817 	for (l = h->head; l; l = l->next)
    818 	{
    819 		/* Make sure we don't send too old entries:
    820 		 * We only have to check for time here, as line count is already
    821 		 * taken into account in hbm_history_add.
    822 		 */
    823 		if (l->t >= redline && (++cnt > lines_to_skip))
    824 		{
    825 			/* Add to result */
    826 			HistoryLogLine *n = duplicate_log_line(l);
    827 			hbm_result_append_line(r, n);
    828 			written++;
    829 		}
    830 	}
    831 
    832 	return written;
    833 }
    834 
    835 /** Put lines in HistoryResult that are 'around' a certain point.
    836  * @param r		The history result set that we will use
    837  * @param h		The history log object
    838  * @param filter	The filter that applies
    839  * @returns Number of lines written, note that this could be zero,
    840  *          which is a perfectly valid result.
    841  */
    842 static int hbm_return_around(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
    843 {
    844 	int n = 0;
    845 	int orig_limit = filter->limit;
    846 
    847 	/* First request 50% above the search term */
    848 	if (filter->limit > 1)
    849 		filter->limit = filter->limit / 2;
    850 	n = hbm_return_before(r, h, filter);
    851 	/* Then the remainder (50% or more) below the search term.
    852 	 *
    853 	 * Ok, well, unless the original limit was 1 and we already
    854 	 * sent 1 line, then we may not send anything anymore..
    855 	 */
    856 	filter->limit = orig_limit - n;
    857 	if (filter->limit > 0)
    858 		n += hbm_return_after(r, h, filter);
    859 
    860 	return n;
    861 }
    862 
    863 /** Figure out the direction (forwards or backwards) for CHATHISTORY BETWEEN request
    864  * @param h		The history log object
    865  * @param filter	The filter that applies
    866  * @returns 0 for backward searching, 1 for forward searching, -1 for invalid / not found
    867  */
    868 static int hbm_return_between_figure_out_direction(HistoryLogObject *h, HistoryFilter *filter)
    869 {
    870 	HistoryLogLine *l;
    871 	int found_a = 0;
    872 	int found_b = 0;
    873 	MessageTag *m;
    874 
    875 	/* Two timestamps? Then we can easily tell the direction. */
    876 	if (filter->timestamp_a && filter->timestamp_b)
    877 		return (strcmp(filter->timestamp_a, filter->timestamp_b) <= 0) ? 1 : 0;
    878 
    879 	for (l = h->head; l; l = l->next)
    880 	{
    881 		if (!found_a)
    882 		{
    883 			if (filter->timestamp_a && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_a) >= 0))
    884 			{
    885 				found_a = 1;
    886 			} else
    887 			if (filter->msgid_a && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_a))
    888 			{
    889 				found_a = 1;
    890 			}
    891 			if (found_a)
    892 			{
    893 				if (found_b)
    894 				{
    895 					/* B was found before A? Then the result is: backwards */
    896 					return 0;
    897 				}
    898 				if (filter->timestamp_b && (m = find_mtag(l->mtags, "time")) && m->value)
    899 				{
    900 					/* We can already resolve the direction now: */
    901 					char *timestamp_a = m->value;
    902 					return (strcmp(timestamp_a, filter->timestamp_b) <= 0) ? 1 : 0;
    903 				}
    904 			}
    905 		}
    906 		if (!found_b)
    907 		{
    908 			if (filter->timestamp_b && ((m = find_mtag(l->mtags, "time"))) && (strcmp(m->value, filter->timestamp_b) >= 0))
    909 			{
    910 				found_b = 1;
    911 			} else
    912 			if (filter->msgid_b && ((m = find_mtag(l->mtags, "msgid"))) && !strcmp(m->value, filter->msgid_b))
    913 			{
    914 				found_b = 1;
    915 			}
    916 			if (found_b)
    917 			{
    918 				if (found_a)
    919 				{
    920 					/* A was found before B? Then the result is: forwards */
    921 					return 1;
    922 				}
    923 				if (filter->timestamp_a && (m = find_mtag(l->mtags, "time")) && m->value)
    924 				{
    925 					/* We can already resolve the direction now: */
    926 					char *timestamp_b = m->value;
    927 					return (strcmp(filter->timestamp_a, timestamp_b) <= 0) ? 1 : 0;
    928 				}
    929 			}
    930 		}
    931 	}
    932 
    933 	/* Neither points were found OR
    934 	 * one of the point is a msgid that could not be found.
    935 	 */
    936 	return -1; /* Result: invalid */
    937 }
    938 
    939 /** Put lines in HistoryResult that are 'between' two points.
    940  * @param r		The history result set that we will use
    941  * @param h		The history log object
    942  * @param filter	The filter that applies
    943  * @returns Number of lines written, note that this could be zero,
    944  *          which is a perfectly valid result.
    945  */
    946 static int hbm_return_between(HistoryResult *r, HistoryLogObject *h, HistoryFilter *filter)
    947 {
    948 	int direction;
    949 
    950 	direction = hbm_return_between_figure_out_direction(h, filter);
    951 
    952 	if (direction == 1)
    953 		return hbm_return_after(r, h, filter);
    954 	else if (direction == 0)
    955 		return hbm_return_before(r, h, filter);
    956 	/* else direction is -1 which means not found / invalid */
    957 
    958 	return 0;
    959 }
    960 
    961 HistoryResult *hbm_history_request(const char *object, HistoryFilter *filter)
    962 {
    963 	HistoryResult *r;
    964 	HistoryLogObject *h = hbm_find_object(object);
    965 	HistoryLogLine *l;
    966 	int lines_sendable = 0, lines_to_skip = 0, cnt = 0;
    967 	long redline;
    968 
    969 	if (!h)
    970 		return NULL; /* nothing found */
    971 
    972 	/* Check if we need to remove some history entries due to 'time'.
    973 	 * No need to worry about 'count' as that is being taken care off
    974 	 * by hbm_history_add().
    975 	 */
    976 	if (h->oldest_t < TStime() - h->max_time)
    977 		hbm_history_cleanup(h);
    978 
    979 	r = safe_alloc(sizeof(HistoryResult));
    980 	safe_strdup(r->object, object);
    981 
    982 	switch(filter->cmd)
    983 	{
    984 		case HFC_BEFORE:
    985 			hbm_return_before(r, h, filter);
    986 			break;
    987 		case HFC_AFTER:
    988 			hbm_return_after(r, h, filter);
    989 			break;
    990 		case HFC_LATEST:
    991 			hbm_return_latest(r, h, filter);
    992 			break;
    993 		case HFC_AROUND:
    994 			hbm_return_around(r, h, filter);
    995 			break;
    996 		case HFC_BETWEEN:
    997 			hbm_return_between(r, h, filter);
    998 			break;
    999 		case HFC_SIMPLE:
   1000 			hbm_return_simple(r, h, filter);
   1001 			break;
   1002 		default:
   1003 			// unhandled
   1004 			break;
   1005 	}
   1006 
   1007 	return r;
   1008 }
   1009 
   1010 /** Clean up expired entries */
   1011 int hbm_history_cleanup(HistoryLogObject *h)
   1012 {
   1013 	HistoryLogLine *l, *l_next = NULL;
   1014 	long redline = TStime() - h->max_time;
   1015 
   1016 	/* First enforce 'h->max_time', after that enforce 'h->max_lines' */
   1017 
   1018 	/* Checking for time */
   1019 	if (h->oldest_t < redline)
   1020 	{
   1021 		h->oldest_t = 0; /* recalculate in next loop */
   1022 
   1023 		for (l = h->head; l; l = l_next)
   1024 		{
   1025 			l_next = l->next;
   1026 			if (l->t < redline)
   1027 			{
   1028 				hbm_history_del_line(h, l); /* too old, delete it */
   1029 				continue;
   1030 			}
   1031 			if ((h->oldest_t == 0) || (l->t < h->oldest_t))
   1032 				h->oldest_t = l->t;
   1033 		}
   1034 	}
   1035 
   1036 	if (h->num_lines > h->max_lines)
   1037 	{
   1038 		h->oldest_t = 0; /* recalculate in next loop */
   1039 
   1040 		for (l = h->head; l; l = l_next)
   1041 		{
   1042 			l_next = l->next;
   1043 			if (h->num_lines > h->max_lines)
   1044 			{
   1045 				hbm_history_del_line(h, l);
   1046 				continue;
   1047 			}
   1048 			if ((h->oldest_t == 0) || (l->t < h->oldest_t))
   1049 				h->oldest_t = l->t;
   1050 		}
   1051 	}
   1052 
   1053 	return 1;
   1054 }
   1055 
   1056 int hbm_history_destroy(const char *object)
   1057 {
   1058 	HistoryLogObject *h = hbm_find_object(object);
   1059 	HistoryLogLine *l, *l_next;
   1060 
   1061 	if (!h)
   1062 		return 0;
   1063 
   1064 	for (l = h->head; l; l = l_next)
   1065 	{
   1066 		l_next = l->next;
   1067 		/* We could use hbm_history_del_line() here but
   1068 		 * it does unnecessary work, this is quicker.
   1069 		 * The only danger is that we may forget to free some
   1070 		 * fields that are added later there but not here.
   1071 		 */
   1072 		free_message_tags(l->mtags);
   1073 		safe_free(l);
   1074 	}
   1075 
   1076 	hbm_delete_object_hlo(h);
   1077 	return 1;
   1078 }
   1079 
   1080 /** Set new limit on history object */
   1081 int hbm_history_set_limit(const char *object, int max_lines, long max_time)
   1082 {
   1083 	HistoryLogObject *h = hbm_find_or_add_object(object);
   1084 	h->max_lines = max_lines;
   1085 	h->max_time = max_time;
   1086 	hbm_history_cleanup(h); /* impose new restrictions */
   1087 	return 1;
   1088 }
   1089 
   1090 /** Read the master.db file, this is done at the INIT stage so we can still
   1091  * reject the configuration / boot attempt.
   1092  *
   1093  * IMPORTANT: Because we run at INIT you must use test.xyz values and not cfg.xyz!
   1094  */
   1095 static int hbm_read_masterdb(void)
   1096 {
   1097 	UnrealDB *db;
   1098 	uint32_t mdb_version;
   1099 	char *prehash = NULL;
   1100 	char *posthash = NULL;
   1101 
   1102 	db = unrealdb_open(test.masterdb, UNREALDB_MODE_READ, test.db_secret);
   1103 
   1104 	if (!db)
   1105 	{
   1106 		if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
   1107 		{
   1108 			/* Database does not exist. Could be first boot */
   1109 			config_warn("[history] No database present at '%s', will start a new one", test.masterdb);
   1110 			if (!hbm_write_masterdb())
   1111 				return 0; /* fatal error */
   1112 			return 1;
   1113 		} else
   1114 		{
   1115 			config_warn("[history] Unable to open the database file '%s' for reading: %s", test.masterdb, unrealdb_get_error_string());
   1116 			return 0;
   1117 		}
   1118 	}
   1119 
   1120 	/* Master db has an easy format:
   1121 	 * 64 bits: version number
   1122 	 * string:  pre hash
   1123 	 * string:  post hash
   1124 	 */
   1125 	if (!unrealdb_read_int32(db, &mdb_version) ||
   1126 	    !unrealdb_read_str(db, &prehash) ||
   1127 	    !unrealdb_read_str(db, &posthash))
   1128 	{
   1129 		config_error("[history] Read error from database file '%s': %s",
   1130 			test.masterdb, unrealdb_get_error_string());
   1131 		safe_free(prehash);
   1132 		safe_free(posthash);
   1133 		unrealdb_close(db);
   1134 		return 0;
   1135 	}
   1136 	unrealdb_close(db);
   1137 
   1138 	if (!prehash || !posthash)
   1139 	{
   1140 		config_error("[history] Read error from database file '%s': unexpected values encountered",
   1141 			test.masterdb);
   1142 		safe_free(prehash);
   1143 		safe_free(posthash);
   1144 		return 0;
   1145 	}
   1146 
   1147 	/* Now, safely switch over.. */
   1148 	if (hbm_prehash && !strcmp(hbm_prehash, prehash) && hbm_posthash && !strcmp(hbm_posthash, posthash))
   1149 	{
   1150 		/* Identical sets */
   1151 		safe_free(prehash);
   1152 		safe_free(posthash);
   1153 	} else {
   1154 		/* Diffferent */
   1155 		safe_free(hbm_prehash);
   1156 		safe_free(hbm_posthash);
   1157 		hbm_prehash = prehash;
   1158 		hbm_posthash = posthash;
   1159 	}
   1160 
   1161 	return 1;
   1162 }
   1163 
   1164 /** Write the master.db file. Only call this if it does not exist yet! */
   1165 static int hbm_write_masterdb(void)
   1166 {
   1167 	UnrealDB *db;
   1168 	uint32_t mdb_version;
   1169 
   1170 	if (!test.db_secret)
   1171 		abort();
   1172 
   1173 	db = unrealdb_open(test.masterdb, UNREALDB_MODE_WRITE, test.db_secret);
   1174 	if (!db)
   1175 	{
   1176 		config_error("[history] Unable to write to '%s': %s",
   1177 			test.masterdb, unrealdb_get_error_string());
   1178 		return 0;
   1179 	}
   1180 
   1181 	if (!hbm_prehash || !hbm_posthash)
   1182 		abort(); /* impossible */
   1183 
   1184 	mdb_version = 5000;
   1185 	if (!unrealdb_write_int32(db, mdb_version) ||
   1186 	    !unrealdb_write_str(db, hbm_prehash) ||
   1187 	    !unrealdb_write_str(db, hbm_posthash))
   1188 	{
   1189 		config_error("[history] Unable to write to '%s': %s",
   1190 			test.masterdb, unrealdb_get_error_string());
   1191 		return 0;
   1192 	}
   1193 	unrealdb_close(db);
   1194 	return 1;
   1195 }
   1196 
   1197 /** Read all database files (except master.db, which is already loaded) */
   1198 static void hbm_read_dbs(void)
   1199 {
   1200 	char buf[512];
   1201 #ifndef _WIN32
   1202 	struct dirent *dir;
   1203 	DIR *fd = opendir(cfg.directory);
   1204 
   1205 	if (!fd)
   1206 		return;
   1207 
   1208 	while ((dir = readdir(fd)))
   1209 	{
   1210 		char *fname = dir->d_name;
   1211 #else
   1212 	/* Windows */
   1213 	WIN32_FIND_DATA hData;
   1214 	HANDLE hFile;
   1215 	char xbuf[512];
   1216 
   1217 	snprintf(xbuf, sizeof(xbuf), "%s/*.db", cfg.directory);
   1218 
   1219 	hFile = FindFirstFile(xbuf, &hData);
   1220 	if (hFile == INVALID_HANDLE_VALUE)
   1221 		return;
   1222 
   1223 	do
   1224 	{
   1225 		char *fname = hData.cFileName;
   1226 #endif
   1227 
   1228 		/* Common section for both *NIX and Windows */
   1229 
   1230 		snprintf(buf, sizeof(buf), "%s/%s", cfg.directory, fname);
   1231 		if (filename_has_suffix(fname, ".db") && strcmp(fname, "master.db"))
   1232 		{
   1233 			if (!hbm_read_db(buf))
   1234 			{
   1235 				/* On error, we move the file to the 'bad' subdirectory,
   1236 				 * eg data/history/bad/xyz.db
   1237 				 */
   1238 				char buf2[512];
   1239 				snprintf(buf2, sizeof(buf2), "%s/bad", cfg.directory);
   1240 #ifdef _WIN32
   1241 				(void)mkdir(buf2); /* (errors ignored) */
   1242 #else
   1243 				(void)mkdir(buf2, S_IRUSR|S_IWUSR|S_IXUSR); /* (errors ignored) */
   1244 #endif
   1245 				snprintf(buf2, sizeof(buf2), "%s/bad/%s", cfg.directory, fname);
   1246 				unlink(buf2);
   1247 				(void)rename(buf, buf2);
   1248 			}
   1249 		}
   1250 
   1251 		/* End of common section */
   1252 #ifndef _WIN32
   1253 	}
   1254 	closedir(fd);
   1255 #else
   1256 	} while (FindNextFile(hFile, &hData));
   1257 	FindClose(hFile);
   1258 #endif
   1259 }
   1260 
   1261 #define RESET_VALUES_LOOP()	do { \
   1262 					safe_free(mtag_name); \
   1263 					safe_free(mtag_value); \
   1264 					safe_free(line); \
   1265 					free_message_tags(mtags); \
   1266 					mtags = NULL; \
   1267 					magic = 0; \
   1268 					line_ts = 0; \
   1269 				} while(0)
   1270 
   1271 #define R_SAFE_CLEANUP()	do { \
   1272 					unrealdb_close(db); \
   1273 					RESET_VALUES_LOOP(); \
   1274 					safe_free(prehash); \
   1275 					safe_free(posthash); \
   1276 					safe_free(object); \
   1277 				} while(0)
   1278 #define R_SAFE(x) \
   1279 	do { \
   1280 		if (!(x)) { \
   1281 			config_warn("[history] Read error from database file '%s' (possible corruption): %s", fname, unrealdb_get_error_string()); \
   1282 			R_SAFE_CLEANUP(); \
   1283 			return 0; \
   1284 		} \
   1285 	} while(0)
   1286 
   1287 
   1288 /** Read a channel history db file */
   1289 static int hbm_read_db(const char *fname)
   1290 {
   1291 	UnrealDB *db = NULL;
   1292 	// header
   1293 	uint32_t magic = 0;
   1294 	uint32_t version = 0;
   1295 	char *prehash = NULL;
   1296 	char *posthash = NULL;
   1297 	char *object = NULL;
   1298 	uint64_t max_lines = 0;
   1299 	uint64_t max_time = 0;
   1300 	// then, for each entry:
   1301 	// (magic)
   1302 	uint64_t line_ts;
   1303 	char *mtag_name = NULL;
   1304 	char *mtag_value = NULL;
   1305 	MessageTag *mtags = NULL, *m;
   1306 	char *line = NULL;
   1307 	HistoryLogObject *h;
   1308 
   1309 	db = unrealdb_open(fname, UNREALDB_MODE_READ, cfg.db_secret);
   1310 	if (!db)
   1311 	{
   1312 		config_warn("[history] Unable to open the database file '%s' for reading: %s", fname, unrealdb_get_error_string());
   1313 		return 0;
   1314 	}
   1315 
   1316 	R_SAFE(unrealdb_read_int32(db, &magic));
   1317 	if (magic != HISTORYDB_MAGIC_FILE_START)
   1318 	{
   1319 		config_warn("[history] Database '%s' has wrong magic value, possibly corrupt (0x%lx), expected HISTORYDB_MAGIC_FILE_START.",
   1320 			fname, (long)magic);
   1321 		unrealdb_close(db);
   1322 		return 0;
   1323 	}
   1324 
   1325 	/* Now do a version check */
   1326 	R_SAFE(unrealdb_read_int32(db, &version));
   1327 	if (version < 4999)
   1328 	{
   1329 		config_warn("[history] Database '%s' uses an unsupported - possibly old - format (%ld).", fname, (long)version);
   1330 		unrealdb_close(db);
   1331 		return 0;
   1332 	}
   1333 	if (version > 5000)
   1334 	{
   1335 		config_warn("[history] Database '%s' has version %lu while we only support %lu. Did you just downgrade UnrealIRCd? Sorry this is not suported",
   1336 			fname, (unsigned long)version, (unsigned long)5000);
   1337 		unrealdb_close(db);
   1338 		return 0;
   1339 	}
   1340 
   1341 	R_SAFE(unrealdb_read_str(db, &prehash));
   1342 	R_SAFE(unrealdb_read_str(db, &posthash));
   1343 
   1344 	if (!prehash || !posthash || strcmp(prehash, hbm_prehash) || strcmp(posthash, hbm_posthash))
   1345 	{
   1346 		config_warn("[history] Database '%s' does not belong to our 'master.db'. Are you mixing old with new .db files perhaps? This is not supported. File ignored.",
   1347 			fname);
   1348 		R_SAFE_CLEANUP();
   1349 		return 0;
   1350 	}
   1351 
   1352 	R_SAFE(unrealdb_read_str(db, &object));
   1353 	R_SAFE(unrealdb_read_int64(db, &max_lines));
   1354 	R_SAFE(unrealdb_read_int64(db, &max_time));
   1355 	h = hbm_find_object(object);
   1356 	if (!h)
   1357 	{
   1358 		config_warn("Channel %s does not have +H set, deleting history", object);
   1359 		R_SAFE_CLEANUP();
   1360 		unlink(fname);
   1361 		return 1; /* No problem */
   1362 	}
   1363 
   1364 	while(1)
   1365 	{
   1366 		RESET_VALUES_LOOP();
   1367 		R_SAFE(unrealdb_read_int32(db, &magic));
   1368 		if (magic == HISTORYDB_MAGIC_FILE_END)
   1369 			break; /* We're done, end gracefully */
   1370 		if (magic != HISTORYDB_MAGIC_ENTRY_START)
   1371 		{
   1372 			config_warn("[history] Read error from database file '%s': wrong magic value in entry (0x%lx), expected HISTORYDB_MAGIC_ENTRY_START",
   1373 				fname, (long)magic);
   1374 			R_SAFE_CLEANUP();
   1375 			return 0;
   1376 		}
   1377 
   1378 		R_SAFE(unrealdb_read_int64(db, &line_ts));
   1379 		while(1)
   1380 		{
   1381 			R_SAFE(unrealdb_read_str(db, &mtag_name));
   1382 			R_SAFE(unrealdb_read_str(db, &mtag_value));
   1383 			if (!mtag_name && !mtag_value)
   1384 				break; /* We're done reading mtags for this particular line */
   1385 			m = safe_alloc(sizeof(MessageTag));
   1386 			safe_strdup(m->name, mtag_name);
   1387 			safe_strdup(m->value, mtag_value);
   1388 			AppendListItem(m, mtags);
   1389 			safe_free(mtag_name);
   1390 			safe_free(mtag_value);
   1391 		}
   1392 		R_SAFE(unrealdb_read_str(db, &line));
   1393 		R_SAFE(unrealdb_read_int32(db, &magic));
   1394 		if (magic != HISTORYDB_MAGIC_ENTRY_END)
   1395 		{
   1396 			config_warn("[history] Read error from database file '%s': wrong magic value in entry (0x%lx), expected HISTORYDB_MAGIC_ENTRY_END",
   1397 				fname, (long)magic);
   1398 			R_SAFE_CLEANUP();
   1399 			return 0;
   1400 		}
   1401 		hbm_history_add(object, mtags, line);
   1402 	}
   1403 
   1404 	/* Prevent directly rewriting the channel, now that we have just read it.
   1405 	 * This could cause things not to fire in case of corner issues like
   1406 	 * hot-loading but that should be acceptable. The alternative is that
   1407 	 * all log files are written again with identical contents for no reason,
   1408 	 * which is a waste of resources.
   1409 	 */
   1410 	h->dirty = 0;
   1411 
   1412 	R_SAFE_CLEANUP();
   1413 	return 1;
   1414 }
   1415 
   1416 /** Flush all dirty logs to disk on UnrealIRCd stop */
   1417 static void hbm_flush(void)
   1418 {
   1419 	int hashnum;
   1420 	HistoryLogObject *h;
   1421 
   1422 	if (!cfg.persist)
   1423 		return; /* nothing to flush anyway */
   1424 
   1425 	for (hashnum = 0; hashnum < HISTORY_BACKEND_MEM_HASH_TABLE_SIZE; hashnum++)
   1426 	{
   1427 		for (h = history_hash_table[hashnum]; h; h = h->next)
   1428 		{
   1429 			hbm_history_cleanup(h);
   1430 			if (cfg.persist && h->dirty)
   1431 				hbm_write_db(h);
   1432 		}
   1433 	}
   1434 }
   1435 
   1436 /** Free all history.
   1437  * This is only called when the module is unloaded for good, so
   1438  * when UnrealIRCd is terminating or someone comments the module out
   1439  * and/or switches history backends.
   1440  */
   1441 void hbm_free_all_history(ModData *m)
   1442 {
   1443 	int hashnum;
   1444 	HistoryLogObject *h, *h_next;
   1445 
   1446 	for (hashnum = 0; hashnum < HISTORY_BACKEND_MEM_HASH_TABLE_SIZE; hashnum++)
   1447 	{
   1448 		for (h = history_hash_table[hashnum]; h; h = h_next)
   1449 		{
   1450 			h_next = h->next;
   1451 			hbm_history_destroy(h->name);
   1452 		}
   1453 	}
   1454 
   1455 	/* And free the hash table pointer */
   1456 	safe_free(m->ptr);
   1457 }
   1458 
   1459 /** Periodically clean the history.
   1460  * Instead of doing all channels in 1 go, we do a limited number
   1461  * of channels each call, hence the 'static int' and the do { } while
   1462  * rather than a regular for loop.
   1463  * Note that we already impose the line limit in hbm_history_add,
   1464  * so this history_mem_clean is for removals due to max_time limits.
   1465  */
   1466 EVENT(history_mem_clean)
   1467 {
   1468 	static int hashnum = 0;
   1469 	int loopcnt = 0;
   1470 	Channel *channel;
   1471 	HistoryLogObject *h;
   1472 
   1473 	do
   1474 	{
   1475 		for (h = history_hash_table[hashnum]; h; h = h->next)
   1476 		{
   1477 			hbm_history_cleanup(h);
   1478 			if (cfg.persist && h->dirty)
   1479 				hbm_write_db(h);
   1480 		}
   1481 
   1482 		hashnum++;
   1483 
   1484 		if (hashnum >= HISTORY_BACKEND_MEM_HASH_TABLE_SIZE)
   1485 			hashnum = 0;
   1486 	} while(loopcnt++ < HISTORY_CLEAN_PER_LOOP);
   1487 }
   1488 
   1489 const char *hbm_history_filename(HistoryLogObject *h)
   1490 {
   1491 	static char fname[512];
   1492 	char oname[OBJECTLEN+1];
   1493 	char hashdata[512];
   1494 	char hash[128];
   1495 
   1496 	if (!hbm_prehash || !hbm_posthash)
   1497 		abort(); /* impossible */
   1498 
   1499 	strtolower_safe(oname, h->name, sizeof(oname));
   1500 	snprintf(hashdata, sizeof(hashdata), "%s %s %s", hbm_prehash, oname, hbm_posthash);
   1501 	sha256hash(hash, hashdata, strlen(hashdata));
   1502 
   1503 	snprintf(fname, sizeof(fname), "%s/%s.db", cfg.directory, hash);
   1504 	return fname;
   1505 }
   1506 
   1507 #define WARN_WRITE_ERROR(fname) \
   1508 	do { \
   1509 		unreal_log(ULOG_ERROR, "history", "HISTORYDB_FILE_WRITE_ERROR", NULL, \
   1510 			   "[historydb] Error writing to temporary database file $filename: $system_error", \
   1511 			   log_data_string("filename", fname), \
   1512 			   log_data_string("system_error", unrealdb_get_error_string())); \
   1513 	} while(0)
   1514 
   1515 #define W_SAFE(x) \
   1516 	do { \
   1517 		if (!(x)) { \
   1518 			WARN_WRITE_ERROR(tmpfname); \
   1519 			unrealdb_close(db); \
   1520 			return 0; \
   1521 		} \
   1522 	} while(0)
   1523 
   1524 
   1525 // FIXME: the code below will cause massive floods on disk or I/O errors if hundreds of
   1526 // channel logs fail to write... fun.
   1527 static int hbm_write_db(HistoryLogObject *h)
   1528 {
   1529 	UnrealDB *db;
   1530 	const char *realfname;
   1531 	char tmpfname[512];
   1532 	HistoryLogLine *l;
   1533 	MessageTag *m;
   1534 	Channel *channel;
   1535 
   1536 	if (!cfg.db_secret)
   1537 		abort();
   1538 
   1539 	channel = find_channel(h->name);
   1540 	if (!channel || !has_channel_mode(channel, 'P'))
   1541 		return 1; /* Don't save this channel, pretend success */
   1542 
   1543 	realfname = hbm_history_filename(h);
   1544 	snprintf(tmpfname, sizeof(tmpfname), "%s.tmp", realfname);
   1545 
   1546 	db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
   1547 	if (!db)
   1548 	{
   1549 		WARN_WRITE_ERROR(tmpfname);
   1550 		return 0;
   1551 	}
   1552 
   1553 	W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_FILE_START));
   1554 	W_SAFE(unrealdb_write_int32(db, 5000)); /* VERSION */
   1555 	W_SAFE(unrealdb_write_str(db, hbm_prehash));
   1556 	W_SAFE(unrealdb_write_str(db, hbm_posthash));
   1557 	W_SAFE(unrealdb_write_str(db, h->name));
   1558 
   1559 	W_SAFE(unrealdb_write_int64(db, h->max_lines));
   1560 	W_SAFE(unrealdb_write_int64(db, h->max_time));
   1561 
   1562 	for (l = h->head; l; l = l->next)
   1563 	{
   1564 		W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_ENTRY_START));
   1565 		W_SAFE(unrealdb_write_int64(db, l->t));
   1566 		for (m = l->mtags; m; m = m->next)
   1567 		{
   1568 			W_SAFE(unrealdb_write_str(db, m->name));
   1569 			W_SAFE(unrealdb_write_str(db, m->value)); /* can be NULL */
   1570 		}
   1571 		W_SAFE(unrealdb_write_str(db, NULL));
   1572 		W_SAFE(unrealdb_write_str(db, NULL));
   1573 		W_SAFE(unrealdb_write_str(db, l->line));
   1574 		W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_ENTRY_END));
   1575 	}
   1576 	W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_FILE_END));
   1577 
   1578 	if (!unrealdb_close(db))
   1579 	{
   1580 		WARN_WRITE_ERROR(tmpfname);
   1581 		return 0;
   1582 	}
   1583 
   1584 #ifdef _WIN32
   1585 	/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
   1586 	unlink(realfname);
   1587 #endif
   1588 	if (rename(tmpfname, realfname) < 0)
   1589 	{
   1590 		config_error("[history] Error renaming '%s' to '%s': %s (HISTORY NOT SAVED)",
   1591 			tmpfname, realfname, strerror(errno));
   1592 		return 0;
   1593 	}
   1594 
   1595 	/* Now that everything was successful, clear the dirty flag */
   1596 	h->dirty = 0;
   1597 	return 1;
   1598 }
   1599 
   1600 static void hbm_delete_db(HistoryLogObject *h)
   1601 {
   1602 	UnrealDB *db;
   1603 	const char *fname;
   1604 	if (!cfg.persist || !hbm_prehash || !hbm_posthash)
   1605 	{
   1606 #ifdef DEBUGMODE
   1607 		abort(); /* we should not be called, so debug this */
   1608 #endif
   1609 		return;
   1610 	}
   1611 	fname = hbm_history_filename(h);
   1612 	unlink(fname);
   1613 }
   1614 
   1615 void hbm_generic_free(ModData *m)
   1616 {
   1617 	safe_free(m->ptr);
   1618 }