unrealircd- supernets unrealircd source & configuration |
git clone git://git.acid.vegas/unrealircd.git |
Log | Files | Refs | Archive | README | LICENSE |
whowasdb.c (16308B)
1 /* 2 * Stores WHOWAS history in a .db file 3 * (C) Copyright 2023 Syzop 4 * License: GPLv2 or later 5 */ 6 7 #include "unrealircd.h" 8 9 ModuleHeader MOD_HEADER = { 10 "whowasdb", 11 "1.0", 12 "Stores and retrieves WHOWAS history", 13 "UnrealIRCd Team", 14 "unrealircd-6", 15 }; 16 17 /* Our header */ 18 #define WHOWASDB_HEADER 0x57484F57 19 /* Database version */ 20 #define WHOWASDB_VERSION 100 21 /* Save whowas of users to file every <this> seconds */ 22 #define WHOWASDB_SAVE_EVERY 300 23 /* The very first save after boot, apply this delta, this 24 * so we don't coincide with other (potentially) expensive 25 * I/O events like saving tkldb. 26 */ 27 #define WHOWASDB_SAVE_EVERY_DELTA -60 28 29 #define MAGIC_WHOWASDB_START 0x11111111 30 #define MAGIC_WHOWASDB_END 0x22222222 31 32 // #undef BENCHMARK 33 34 /* Yeah, W_SAFE_PROPERTY raises "the address .. will always evaluate to true" warnings, 35 * disabling it in the entire file for now... 36 */ 37 #if defined(__GNUC__) 38 #pragma GCC diagnostic ignored "-Waddress" 39 #endif 40 41 #define WARN_WRITE_ERROR(fname) \ 42 do { \ 43 unreal_log(ULOG_ERROR, "whowasdb", "WHOWASDB_FILE_WRITE_ERROR", NULL, \ 44 "[whowasdb] Error writing to temporary database file $filename: $system_error", \ 45 log_data_string("filename", fname), \ 46 log_data_string("system_error", unrealdb_get_error_string())); \ 47 } while(0) 48 49 #define W_SAFE(x) \ 50 do { \ 51 if (!(x)) { \ 52 WARN_WRITE_ERROR(tmpfname); \ 53 unrealdb_close(db); \ 54 return 0; \ 55 } \ 56 } while(0) 57 58 #define W_SAFE_PROPERTY(db, x, y) \ 59 do { \ 60 if (x && y && (!unrealdb_write_str(db, x) || !unrealdb_write_str(db, y))) \ 61 { \ 62 WARN_WRITE_ERROR(tmpfname); \ 63 unrealdb_close(db); \ 64 return 0; \ 65 } \ 66 } while(0) 67 68 #define IsMDErr(x, y, z) \ 69 do { \ 70 if (!(x)) { \ 71 config_error("A critical error occurred when registering ModData for %s: %s", MOD_HEADER.name, ModuleGetErrorStr((z)->handle)); \ 72 return MOD_FAILED; \ 73 } \ 74 } while(0) 75 76 /* Structs */ 77 struct cfgstruct { 78 char *database; 79 char *db_secret; 80 }; 81 82 /* Forward declarations */ 83 void whowasdb_moddata_free(ModData *md); 84 void setcfg(struct cfgstruct *cfg); 85 void freecfg(struct cfgstruct *cfg); 86 int whowasdb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); 87 int whowasdb_config_posttest(int *errs); 88 int whowasdb_config_run(ConfigFile *cf, ConfigEntry *ce, int type); 89 EVENT(write_whowasdb_evt); 90 int write_whowasdb(void); 91 int write_whowas_entry(UnrealDB *db, const char *tmpfname, WhoWas *e); 92 int read_whowasdb(void); 93 94 /* External variables */ 95 extern WhoWas MODVAR WHOWAS[NICKNAMEHISTORYLENGTH]; 96 extern WhoWas MODVAR *WHOWASHASH[WHOWAS_HASH_TABLE_SIZE]; 97 extern MODVAR int whowas_next; 98 99 /* Global variables */ 100 static uint32_t whowasdb_version = WHOWASDB_VERSION; 101 static struct cfgstruct cfg; 102 static struct cfgstruct test; 103 104 static long whowasdb_next_event = 0; 105 106 MOD_TEST() 107 { 108 memset(&cfg, 0, sizeof(cfg)); 109 memset(&test, 0, sizeof(test)); 110 setcfg(&test); 111 HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, whowasdb_config_test); 112 HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, whowasdb_config_posttest); 113 return MOD_SUCCESS; 114 } 115 116 MOD_INIT() 117 { 118 MARK_AS_OFFICIAL_MODULE(modinfo); 119 120 LoadPersistentLong(modinfo, whowasdb_next_event); 121 122 setcfg(&cfg); 123 124 HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, whowasdb_config_run); 125 return MOD_SUCCESS; 126 } 127 128 MOD_LOAD() 129 { 130 if (!whowasdb_next_event) 131 { 132 /* If this is the first time that our module is loaded, then read the database. */ 133 if (!read_whowasdb()) 134 { 135 char fname[512]; 136 snprintf(fname, sizeof(fname), "%s.corrupt", cfg.database); 137 if (rename(cfg.database, fname) == 0) 138 config_warn("[whowasdb] Existing database renamed to %s and starting a new one...", fname); 139 else 140 config_warn("[whowasdb] Failed to rename database from %s to %s: %s", cfg.database, fname, strerror(errno)); 141 } 142 whowasdb_next_event = TStime() + WHOWASDB_SAVE_EVERY + WHOWASDB_SAVE_EVERY_DELTA; 143 } 144 EventAdd(modinfo->handle, "whowasdb_write_whowasdb", write_whowasdb_evt, NULL, 1000, 0); 145 if (ModuleGetError(modinfo->handle) != MODERR_NOERROR) 146 { 147 config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle)); 148 return MOD_FAILED; 149 } 150 return MOD_SUCCESS; 151 } 152 153 MOD_UNLOAD() 154 { 155 if (loop.terminating) 156 write_whowasdb(); 157 freecfg(&test); 158 freecfg(&cfg); 159 SavePersistentLong(modinfo, whowasdb_next_event); 160 return MOD_SUCCESS; 161 } 162 163 void whowasdb_moddata_free(ModData *md) 164 { 165 if (md->i) 166 md->i = 0; 167 } 168 169 void setcfg(struct cfgstruct *cfg) 170 { 171 // Default: data/whowas.db 172 safe_strdup(cfg->database, "whowas.db"); 173 convert_to_absolute_path(&cfg->database, PERMDATADIR); 174 } 175 176 void freecfg(struct cfgstruct *cfg) 177 { 178 safe_free(cfg->database); 179 safe_free(cfg->db_secret); 180 } 181 182 int whowasdb_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) 183 { 184 int errors = 0; 185 ConfigEntry *cep; 186 187 // We are only interested in set::whowasdb::database 188 if (type != CONFIG_SET) 189 return 0; 190 191 if (!ce || strcmp(ce->name, "whowasdb")) 192 return 0; 193 194 for (cep = ce->items; cep; cep = cep->next) 195 { 196 if (!cep->value) 197 { 198 config_error("%s:%i: blank set::whowasdb::%s without value", cep->file->filename, cep->line_number, cep->name); 199 errors++; 200 } else 201 if (!strcmp(cep->name, "database")) 202 { 203 convert_to_absolute_path(&cep->value, PERMDATADIR); 204 safe_strdup(test.database, cep->value); 205 } else 206 if (!strcmp(cep->name, "db-secret")) 207 { 208 const char *err; 209 if ((err = unrealdb_test_secret(cep->value))) 210 { 211 config_error("%s:%i: set::whowasdb::db-secret: %s", cep->file->filename, cep->line_number, err); 212 errors++; 213 continue; 214 } 215 safe_strdup(test.db_secret, cep->value); 216 } else 217 { 218 config_error("%s:%i: unknown directive set::whowasdb::%s", cep->file->filename, cep->line_number, cep->name); 219 errors++; 220 } 221 } 222 223 *errs = errors; 224 return errors ? -1 : 1; 225 } 226 227 int whowasdb_config_posttest(int *errs) 228 { 229 int errors = 0; 230 char *errstr; 231 232 if (test.database && ((errstr = unrealdb_test_db(test.database, test.db_secret)))) 233 { 234 config_error("[whowasdb] %s", errstr); 235 errors++; 236 } 237 238 *errs = errors; 239 return errors ? -1 : 1; 240 } 241 242 int whowasdb_config_run(ConfigFile *cf, ConfigEntry *ce, int type) 243 { 244 ConfigEntry *cep; 245 246 // We are only interested in set::whowasdb::database 247 if (type != CONFIG_SET) 248 return 0; 249 250 if (!ce || strcmp(ce->name, "whowasdb")) 251 return 0; 252 253 for (cep = ce->items; cep; cep = cep->next) 254 { 255 if (!strcmp(cep->name, "database")) 256 safe_strdup(cfg.database, cep->value); 257 else if (!strcmp(cep->name, "db-secret")) 258 safe_strdup(cfg.db_secret, cep->value); 259 } 260 return 1; 261 } 262 263 EVENT(write_whowasdb_evt) 264 { 265 if (whowasdb_next_event > TStime()) 266 return; 267 whowasdb_next_event = TStime() + WHOWASDB_SAVE_EVERY; 268 write_whowasdb(); 269 } 270 271 int count_whowas_and_user_entries(void) 272 { 273 int i; 274 int cnt = 0; 275 Client *client; 276 277 for (i=0; i < NICKNAMEHISTORYLENGTH; i++) 278 { 279 WhoWas *e = &WHOWAS[i]; 280 if (e->name) 281 cnt++; 282 } 283 284 list_for_each_entry(client, &client_list, client_node) 285 if (IsUser(client)) 286 cnt++; 287 288 return cnt; 289 } 290 291 int write_whowasdb(void) 292 { 293 char tmpfname[512]; 294 UnrealDB *db; 295 WhoWas *e; 296 Client *client; 297 int cnt, i; 298 #ifdef BENCHMARK 299 struct timeval tv_alpha, tv_beta; 300 301 gettimeofday(&tv_alpha, NULL); 302 #endif 303 304 // Write to a tempfile first, then rename it if everything succeeded 305 snprintf(tmpfname, sizeof(tmpfname), "%s.%x.tmp", cfg.database, getrandom32()); 306 db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret); 307 if (!db) 308 { 309 WARN_WRITE_ERROR(tmpfname); 310 return 0; 311 } 312 313 W_SAFE(unrealdb_write_int32(db, WHOWASDB_HEADER)); 314 W_SAFE(unrealdb_write_int32(db, whowasdb_version)); 315 316 cnt = count_whowas_and_user_entries(); 317 W_SAFE(unrealdb_write_int64(db, cnt)); 318 319 for (i=0; i < NICKNAMEHISTORYLENGTH; i++) 320 { 321 WhoWas *e = &WHOWAS[i]; 322 if (e->name) 323 { 324 if (!write_whowas_entry(db, tmpfname, e)) 325 return 0; 326 } 327 } 328 329 /* Add all the currently connected users to WHOWAS history (as if they left just now) */ 330 list_for_each_entry(client, &client_list, client_node) 331 { 332 if (IsUser(client)) 333 { 334 WhoWas *e = safe_alloc(sizeof(WhoWas)); 335 int ret; 336 337 create_whowas_entry(client, e, WHOWAS_EVENT_SERVER_TERMINATING); 338 ret = write_whowas_entry(db, tmpfname, e); 339 free_whowas_fields(e); 340 safe_free(e); 341 342 if (ret == 0) 343 return 0; 344 } 345 } 346 347 348 // Everything seems to have gone well, attempt to close and rename the tempfile 349 if (!unrealdb_close(db)) 350 { 351 WARN_WRITE_ERROR(tmpfname); 352 return 0; 353 } 354 355 #ifdef _WIN32 356 /* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */ 357 unlink(cfg.database); 358 #endif 359 if (rename(tmpfname, cfg.database) < 0) 360 { 361 config_error("[whowasdb] Error renaming '%s' to '%s': %s (DATABASE NOT SAVED)", tmpfname, cfg.database, strerror(errno)); 362 return 0; 363 } 364 #ifdef BENCHMARK 365 gettimeofday(&tv_beta, NULL); 366 config_status("[whowasdb] Benchmark: SAVE DB: %ld microseconds", 367 ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec)); 368 #endif 369 return 1; 370 } 371 372 int write_whowas_entry(UnrealDB *db, const char *tmpfname, WhoWas *e) 373 { 374 char connected_since[64]; 375 char logontime[64]; 376 char logofftime[64]; 377 char event[16]; 378 379 snprintf(connected_since, sizeof(connected_since), "%lld", (long long)e->connected_since); 380 snprintf(logontime, sizeof(logontime), "%lld", (long long)e->logon); 381 snprintf(logofftime, sizeof(logofftime), "%lld", (long long)e->logoff); 382 snprintf(event, sizeof(event), "%d", e->event); 383 384 W_SAFE(unrealdb_write_int32(db, MAGIC_WHOWASDB_START)); 385 W_SAFE_PROPERTY(db, "nick", e->name); 386 W_SAFE_PROPERTY(db, "event", event); 387 W_SAFE_PROPERTY(db, "connected_since", connected_since); 388 W_SAFE_PROPERTY(db, "logontime", logontime); 389 W_SAFE_PROPERTY(db, "logofftime", logofftime); 390 W_SAFE_PROPERTY(db, "username", e->username); 391 W_SAFE_PROPERTY(db, "hostname", e->hostname); 392 W_SAFE_PROPERTY(db, "ip", e->ip); 393 W_SAFE_PROPERTY(db, "realname", e->realname); 394 W_SAFE_PROPERTY(db, "server", e->servername); 395 W_SAFE_PROPERTY(db, "virthost", e->virthost); 396 W_SAFE_PROPERTY(db, "account", e->account); 397 W_SAFE_PROPERTY(db, "end", ""); 398 W_SAFE(unrealdb_write_int32(db, MAGIC_WHOWASDB_END)); 399 return 1; 400 } 401 402 #define FreeWhowasEntry() \ 403 do { \ 404 /* Some of these might be NULL */ \ 405 safe_free(key); \ 406 safe_free(value); \ 407 safe_free(nick); \ 408 safe_free(username); \ 409 safe_free(hostname); \ 410 safe_free(ip); \ 411 safe_free(realname); \ 412 connected_since = logontime = logofftime = 0; \ 413 event = 0; \ 414 safe_free(server); \ 415 safe_free(virthost); \ 416 safe_free(account); \ 417 } while(0) 418 419 #define R_SAFE(x) \ 420 do { \ 421 if (!(x)) { \ 422 config_warn("[whowasdb] Read error from database file '%s' (possible corruption): %s", cfg.database, unrealdb_get_error_string()); \ 423 unrealdb_close(db); \ 424 FreeWhowasEntry(); \ 425 return 0; \ 426 } \ 427 } while(0) 428 429 int read_whowasdb(void) 430 { 431 UnrealDB *db; 432 uint32_t version; 433 int added = 0; 434 int i; 435 uint64_t count = 0; 436 uint32_t magic; 437 char *key = NULL; 438 char *value = NULL; 439 char *nick = NULL; 440 char *username = NULL; 441 char *hostname = NULL; 442 char *ip = NULL; 443 char *realname = NULL; 444 long long connected_since = 0; 445 long long logontime = 0; 446 long long logofftime = 0; 447 int event = 0; 448 char *server = NULL; 449 char *virthost = NULL; 450 char *account = NULL; 451 #ifdef BENCHMARK 452 struct timeval tv_alpha, tv_beta; 453 454 gettimeofday(&tv_alpha, NULL); 455 #endif 456 457 db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, cfg.db_secret); 458 if (!db) 459 { 460 if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND) 461 { 462 /* Database does not exist. Could be first boot */ 463 config_warn("[whowasdb] No database present at '%s', will start a new one", cfg.database); 464 return 1; 465 } else 466 if (unrealdb_get_error_code() == UNREALDB_ERROR_NOTCRYPTED) 467 { 468 /* Re-open as unencrypted */ 469 db = unrealdb_open(cfg.database, UNREALDB_MODE_READ, NULL); 470 if (!db) 471 { 472 /* This should actually never happen, unless some weird I/O error */ 473 config_warn("[whowasdb] Unable to open the database file '%s': %s", cfg.database, unrealdb_get_error_string()); 474 return 0; 475 } 476 } else 477 { 478 config_warn("[whowasdb] Unable to open the database file '%s' for reading: %s", cfg.database, unrealdb_get_error_string()); 479 return 0; 480 } 481 } 482 483 R_SAFE(unrealdb_read_int32(db, &version)); 484 if (version != WHOWASDB_HEADER) 485 { 486 config_warn("[whowasdb] Database '%s' is not a whowas db (incorrect header)", cfg.database); 487 unrealdb_close(db); 488 return 0; 489 } 490 R_SAFE(unrealdb_read_int32(db, &version)); 491 if (version > whowasdb_version) 492 { 493 config_warn("[whowasdb] Database '%s' has a wrong version: expected it to be <= %u but got %u instead", cfg.database, whowasdb_version, version); 494 unrealdb_close(db); 495 return 0; 496 } 497 498 R_SAFE(unrealdb_read_int64(db, &count)); 499 500 for (i=1; i <= count; i++) 501 { 502 // Variables 503 key = value = NULL; 504 nick = username = hostname = ip = realname = virthost = account = server = NULL; 505 connected_since = logontime = logofftime = 0; 506 event = 0; 507 508 R_SAFE(unrealdb_read_int32(db, &magic)); 509 if (magic != MAGIC_WHOWASDB_START) 510 { 511 config_error("[whowasdb] Corrupt database (%s) - whowasdb magic start is 0x%x. Further reading aborted.", cfg.database, magic); 512 break; 513 } 514 while(1) 515 { 516 R_SAFE(unrealdb_read_str(db, &key)); 517 R_SAFE(unrealdb_read_str(db, &value)); 518 if (!strcmp(key, "nick")) 519 { 520 safe_strdup(nick, value); 521 } else 522 if (!strcmp(key, "username")) 523 { 524 safe_strdup(username, value); 525 } else 526 if (!strcmp(key, "hostname")) 527 { 528 safe_strdup(hostname, value); 529 } else 530 if (!strcmp(key, "ip")) 531 { 532 safe_strdup(ip, value); 533 } else 534 if (!strcmp(key, "realname")) 535 { 536 safe_strdup(realname, value); 537 } else 538 if (!strcmp(key, "connected_since")) 539 { 540 connected_since = atoll(value); 541 safe_free(value); 542 } else 543 if (!strcmp(key, "logontime")) 544 { 545 logontime = atoll(value); 546 safe_free(value); 547 } else 548 if (!strcmp(key, "logofftime")) 549 { 550 logofftime = atoll(value); 551 safe_free(value); 552 } else 553 if (!strcmp(key, "event")) 554 { 555 event = atoi(value); 556 if ((event < WHOWAS_LOWEST_EVENT) || (event > WHOWAS_HIGHEST_EVENT)) 557 event = WHOWAS_EVENT_QUIT; /* safety */ 558 safe_free(value); 559 } else 560 if (!strcmp(key, "server")) 561 { 562 safe_strdup(server, value); 563 } else 564 if (!strcmp(key, "virthost")) 565 { 566 safe_strdup(virthost, value); 567 } else 568 if (!strcmp(key, "account")) 569 { 570 safe_strdup(account, value); 571 } else 572 if (!strcmp(key, "end")) 573 { 574 safe_free(key); 575 safe_free(value); 576 break; /* DONE! */ 577 } 578 safe_free(key); 579 safe_free(value); 580 } 581 R_SAFE(unrealdb_read_int32(db, &magic)); 582 if (magic != MAGIC_WHOWASDB_END) 583 { 584 config_error("[whowasdb] Corrupt database (%s) - whowasdb magic end is 0x%x. Further reading aborted.", cfg.database, magic); 585 FreeWhowasEntry(); 586 break; 587 } 588 589 if (nick && username && hostname && realname) 590 { 591 WhoWas *e = &WHOWAS[whowas_next]; 592 if (e->hashv != -1) 593 free_whowas_fields(e); 594 /* Set values */ 595 //unreal_log(ULOG_DEBUG, "whowasdb", "WHOWASDB_READ_RECORD", NULL, 596 // "[whowasdb] Adding '$nick'...", 597 // log_data_string("nick", nick)); 598 e->hashv = hash_whowas_name(nick); 599 e->event = event; 600 e->connected_since = connected_since; 601 e->logon = logontime; 602 e->logoff = logofftime; 603 safe_strdup(e->name, nick); 604 safe_strdup(e->username, username); 605 safe_strdup(e->hostname, hostname); 606 safe_strdup(e->ip, ip); 607 if (virthost) 608 safe_strdup(e->virthost, virthost); 609 else 610 safe_strdup(e->virthost, ""); 611 e->servername = find_or_add(server); /* scache */ 612 safe_strdup(e->realname, realname); 613 safe_strdup(e->account, account); 614 e->online = NULL; 615 /* Server is special - scache shit */ 616 /* Add to hash table */ 617 add_whowas_to_list(&WHOWASHASH[e->hashv], e); 618 /* And advance pointer (well, integer) */ 619 whowas_next++; 620 if (whowas_next == NICKNAMEHISTORYLENGTH) 621 whowas_next = 0; 622 } 623 624 FreeWhowasEntry(); 625 added++; 626 } 627 628 unrealdb_close(db); 629 630 if (added) 631 config_status("[whowasdb] Added %d WHOWAS items", added); 632 #ifdef BENCHMARK 633 gettimeofday(&tv_beta, NULL); 634 unreal_log(ULOG_DEBUG, "whowasdb", "WHOWASDB_BENCHMARK", NULL, 635 "[whowasdb] Benchmark: LOAD DB: $time_msec microseconds", 636 log_data_integer("time_msec", ((tv_beta.tv_sec - tv_alpha.tv_sec) * 1000000) + (tv_beta.tv_usec - tv_alpha.tv_usec))); 637 #endif 638 return 1; 639 } 640 #undef FreeWhowasEntry 641 #undef R_SAFE