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