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 }