unrealircd- supernets unrealircd source & configuration |
git clone git://git.acid.vegas/unrealircd.git |
Log | Files | Refs | Archive | README | LICENSE |
require-module.c (16708B)
1 /* 2 * Check for modules that are required across the network, as well as modules 3 * that *aren't* even allowed (deny/require module { } blocks) 4 * (C) Copyright 2019 Gottem and the UnrealIRCd team 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 1, or (at your option) 9 * any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 19 */ 20 21 #include "unrealircd.h" 22 23 #define MSG_SMOD "SMOD" 24 #define SMOD_FLAG_REQUIRED 'R' 25 #define SMOD_FLAG_GLOBAL 'G' 26 #define SMOD_FLAG_LOCAL 'L' 27 28 ModuleHeader MOD_HEADER = { 29 "require-module", 30 "5.0.1", 31 "Require/deny modules across the network", 32 "UnrealIRCd Team", 33 "unrealircd-6", 34 }; 35 36 typedef struct _denymod DenyMod; 37 struct _denymod { 38 DenyMod *prev, *next; 39 char *name; 40 char *reason; 41 }; 42 43 typedef struct _requiremod ReqMod; 44 struct _requiremod { 45 ReqMod *prev, *next; 46 char *name; 47 char *minversion; 48 }; 49 50 // Forward declarations 51 Module *find_modptr_byname(char *name, unsigned strict); 52 DenyMod *find_denymod_byname(char *name); 53 ReqMod *find_reqmod_byname(char *name); 54 55 int reqmods_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); 56 int reqmods_configrun(ConfigFile *cf, ConfigEntry *ce, int type); 57 58 int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); 59 int reqmods_configrun_deny(ConfigFile *cf, ConfigEntry *ce, int type); 60 61 int reqmods_configtest_require(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); 62 int reqmods_configrun_require(ConfigFile *cf, ConfigEntry *ce, int type); 63 64 CMD_FUNC(cmd_smod); 65 int reqmods_hook_serverconnect(Client *client); 66 67 // Globals 68 extern MODVAR Module *Modules; 69 DenyMod *DenyModList = NULL; 70 ReqMod *ReqModList = NULL; 71 72 MOD_TEST() 73 { 74 HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reqmods_configtest); 75 return MOD_SUCCESS; 76 } 77 78 MOD_INIT() 79 { 80 MARK_AS_OFFICIAL_MODULE(modinfo); 81 MARK_AS_GLOBAL_MODULE(modinfo); 82 HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reqmods_configrun); 83 HookAdd(modinfo->handle, HOOKTYPE_SERVER_CONNECT, 0, reqmods_hook_serverconnect); 84 CommandAdd(modinfo->handle, MSG_SMOD, cmd_smod, MAXPARA, CMD_SERVER); 85 return MOD_SUCCESS; 86 } 87 88 MOD_LOAD() 89 { 90 if (ModuleGetError(modinfo->handle) != MODERR_NOERROR) 91 { 92 config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle)); 93 return MOD_FAILED; 94 } 95 return MOD_SUCCESS; 96 } 97 98 MOD_UNLOAD() 99 { 100 DenyMod *dmod, *dnext; 101 ReqMod *rmod, *rnext; 102 for (dmod = DenyModList; dmod; dmod = dnext) 103 { 104 dnext = dmod->next; 105 safe_free(dmod->name); 106 safe_free(dmod->reason); 107 DelListItem(dmod, DenyModList); 108 safe_free(dmod); 109 } 110 for (rmod = ReqModList; rmod; rmod = rnext) 111 { 112 rnext = rmod->next; 113 safe_free(rmod->name); 114 safe_free(rmod->minversion); 115 DelListItem(rmod, ReqModList); 116 safe_free(rmod); 117 } 118 DenyModList = NULL; 119 ReqModList = NULL; 120 return MOD_SUCCESS; 121 } 122 123 Module *find_modptr_byname(char *name, unsigned strict) 124 { 125 Module *mod; 126 for (mod = Modules; mod; mod = mod->next) 127 { 128 // Let's not be too strict with the name 129 if (!strcasecmp(mod->header->name, name)) 130 { 131 if (strict && !(mod->flags & MODFLAG_LOADED)) 132 mod = NULL; 133 return mod; 134 } 135 } 136 return NULL; 137 } 138 139 DenyMod *find_denymod_byname(char *name) 140 { 141 DenyMod *dmod; 142 for (dmod = DenyModList; dmod; dmod = dmod->next) 143 { 144 if (!strcasecmp(dmod->name, name)) 145 return dmod; 146 } 147 return NULL; 148 } 149 150 ReqMod *find_reqmod_byname(char *name) 151 { 152 ReqMod *rmod; 153 for (rmod = ReqModList; rmod; rmod = rmod->next) 154 { 155 if (!strcasecmp(rmod->name, name)) 156 return rmod; 157 } 158 return NULL; 159 } 160 161 int reqmods_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) 162 { 163 if (type == CONFIG_DENY) 164 return reqmods_configtest_deny(cf, ce, type, errs); 165 166 if (type == CONFIG_REQUIRE) 167 return reqmods_configtest_require(cf, ce, type, errs); 168 169 return 0; 170 } 171 172 int reqmods_configrun(ConfigFile *cf, ConfigEntry *ce, int type) 173 { 174 if (type == CONFIG_DENY) 175 return reqmods_configrun_deny(cf, ce, type); 176 177 if (type == CONFIG_REQUIRE) 178 return reqmods_configrun_require(cf, ce, type); 179 180 return 0; 181 } 182 183 int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) 184 { 185 int errors = 0; 186 ConfigEntry *cep; 187 int has_name, has_reason; 188 189 // We are only interested in deny module { } 190 if (strcmp(ce->value, "module")) 191 return 0; 192 193 has_name = has_reason = 0; 194 for (cep = ce->items; cep; cep = cep->next) 195 { 196 if (!strlen(cep->name)) 197 { 198 config_error("%s:%i: blank directive for deny module { } block", cep->file->filename, cep->line_number); 199 errors++; 200 continue; 201 } 202 203 if (!cep->value || !strlen(cep->value)) 204 { 205 config_error("%s:%i: blank %s without value for deny module { } block", cep->file->filename, cep->line_number, cep->name); 206 errors++; 207 continue; 208 } 209 210 if (!strcmp(cep->name, "name")) 211 { 212 if (has_name) 213 { 214 config_error("%s:%i: duplicate %s for deny module { } block", cep->file->filename, cep->line_number, cep->name); 215 continue; 216 } 217 218 // We do a loose check here because a module might not be fully loaded yet 219 if (find_modptr_byname(cep->value, 0)) 220 { 221 config_error("[require-module] Module '%s' was specified as denied but we've actually loaded it ourselves", cep->value); 222 errors++; 223 } 224 has_name = 1; 225 continue; 226 } 227 228 if (!strcmp(cep->name, "reason")) // Optional 229 { 230 // Still check for duplicate directives though 231 if (has_reason) 232 { 233 config_error("%s:%i: duplicate %s for deny module { } block", cep->file->filename, cep->line_number, cep->name); 234 errors++; 235 continue; 236 } 237 has_reason = 1; 238 continue; 239 } 240 241 config_error("%s:%i: unknown directive %s for deny module { } block", cep->file->filename, cep->line_number, cep->name); 242 errors++; 243 } 244 245 if (!has_name) 246 { 247 config_error("%s:%i: missing required 'name' directive for deny module { } block", ce->file->filename, ce->line_number); 248 errors++; 249 } 250 251 *errs = errors; 252 return errors ? -1 : 1; 253 } 254 255 int reqmods_configrun_deny(ConfigFile *cf, ConfigEntry *ce, int type) 256 { 257 ConfigEntry *cep; 258 DenyMod *dmod; 259 260 if (strcmp(ce->value, "module")) 261 return 0; 262 263 dmod = safe_alloc(sizeof(DenyMod)); 264 for (cep = ce->items; cep; cep = cep->next) 265 { 266 if (!strcmp(cep->name, "name")) 267 { 268 safe_strdup(dmod->name, cep->value); 269 continue; 270 } 271 272 if (!strcmp(cep->name, "reason")) 273 { 274 safe_strdup(dmod->reason, cep->value); 275 continue; 276 } 277 } 278 279 // Just use a default reason if none was specified (since it's optional) 280 if (!dmod->reason || !strlen(dmod->reason)) 281 safe_strdup(dmod->reason, "no reason"); 282 AddListItem(dmod, DenyModList); 283 return 1; 284 } 285 286 int reqmods_configtest_require(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) 287 { 288 int errors = 0; 289 ConfigEntry *cep; 290 int has_name, has_minversion; 291 292 // We are only interested in require module { } 293 if (strcmp(ce->value, "module")) 294 return 0; 295 296 has_name = has_minversion = 0; 297 for (cep = ce->items; cep; cep = cep->next) 298 { 299 if (!strlen(cep->name)) 300 { 301 config_error("%s:%i: blank directive for require module { } block", cep->file->filename, cep->line_number); 302 errors++; 303 continue; 304 } 305 306 if (!cep->value || !strlen(cep->value)) 307 { 308 config_error("%s:%i: blank %s without value for require module { } block", cep->file->filename, cep->line_number, cep->name); 309 errors++; 310 continue; 311 } 312 313 if (!strcmp(cep->name, "name")) 314 { 315 if (has_name) 316 { 317 config_error("%s:%i: duplicate %s for require module { } block", cep->file->filename, cep->line_number, cep->name); 318 continue; 319 } 320 321 if (!find_modptr_byname(cep->value, 0)) 322 { 323 config_error("[require-module] Module '%s' was specified as required but we didn't even load it ourselves (maybe double check the name?)", cep->value); 324 errors++; 325 } 326 327 // Let's be nice and let configrun handle adding this module to the list 328 has_name = 1; 329 continue; 330 } 331 332 if (!strcmp(cep->name, "min-version")) // Optional 333 { 334 // Still check for duplicate directives though 335 if (has_minversion) 336 { 337 config_error("%s:%i: duplicate %s for require module { } block", cep->file->filename, cep->line_number, cep->name); 338 errors++; 339 continue; 340 } 341 has_minversion = 1; 342 continue; 343 } 344 345 // Reason directive is not used for require module { }, so error on that too 346 config_error("%s:%i: unknown directive %s for require module { } block", cep->file->filename, cep->line_number, cep->name); 347 errors++; 348 } 349 350 if (!has_name) 351 { 352 config_error("%s:%i: missing required 'name' directive for require module { } block", ce->file->filename, ce->line_number); 353 errors++; 354 } 355 356 *errs = errors; 357 return errors ? -1 : 1; 358 } 359 360 int reqmods_configrun_require(ConfigFile *cf, ConfigEntry *ce, int type) 361 { 362 ConfigEntry *cep; 363 Module *mod; 364 ReqMod *rmod; 365 char *name, *minversion; 366 367 if (strcmp(ce->value, "module")) 368 return 0; 369 370 name = minversion = NULL; 371 for (cep = ce->items; cep; cep = cep->next) 372 { 373 if (!strcmp(cep->name, "name")) 374 { 375 if (!(mod = find_modptr_byname(cep->value, 0))) 376 { 377 // Something went very wrong :D 378 config_warn("[require-module] [BUG?] Passed configtest_require() but not configrun_require() for module '%s' (seems to not be loaded after all)", cep->value); 379 continue; 380 } 381 382 name = cep->value; 383 continue; 384 } 385 386 if (!strcmp(cep->name, "min-version")) 387 { 388 minversion = cep->value; 389 continue; 390 } 391 } 392 393 // While technically an error, let's not kill the entire server over it 394 if (!name) 395 return 1; 396 397 rmod = safe_alloc(sizeof(ReqMod)); 398 safe_strdup(rmod->name, name); 399 if (minversion) 400 safe_strdup(rmod->minversion, minversion); 401 AddListItem(rmod, ReqModList); 402 return 1; 403 } 404 405 CMD_FUNC(cmd_smod) 406 { 407 char modflag, name[64], *version; 408 char buf[BUFSIZE]; 409 char *tmp, *p, *modbuf; 410 Module *mod; 411 DenyMod *dmod; 412 int i; 413 int abort; 414 415 // A non-server client shouldn't really be possible here, but still :D 416 if (!MyConnect(client) || !IsServer(client) || BadPtr(parv[1])) 417 return; 418 419 // Module strings are passed as 1 space-delimited parameter 420 strlcpy(buf, parv[1], sizeof(buf)); 421 abort = 0; 422 for (modbuf = strtoken(&tmp, buf, " "); modbuf; modbuf = strtoken(&tmp, NULL, " ")) 423 { 424 /* The order of checks is: 425 * 1: deny module { } -- SQUIT always 426 * 2 (if module not loaded): require module { } -- SQUIT always 427 * 3 (if module not loaded): warn, but only if MOD_OPT_GLOBAL 428 * 4 (optional, if module loaded only): require module::min-version 429 */ 430 p = strchr(modbuf, ':'); 431 if (!p) 432 continue; /* malformed request */ 433 modflag = *modbuf; // Get the module flag (FIXME: parses only first letter atm) 434 modbuf = p+1; 435 strlcpy(name, modbuf, sizeof(name)); // Let's work on a copy of the param 436 437 version = strchr(name, ':'); 438 if (!version) 439 continue; /* malformed request */ 440 *version++ = '\0'; 441 442 // Even if a denied module is only required locally, let's still prevent a server that uses it from linking in 443 if ((dmod = find_denymod_byname(name))) 444 { 445 // Send this particular notice to local opers only 446 unreal_log(ULOG_ERROR, "link", "LINK_DENY_MODULE", client, 447 "Server $client is using module '$module_name', " 448 "which is specified in a deny module { } config block (reason: $ban_reason) -- aborting link", 449 log_data_string("module_name", name), 450 log_data_string("ban_reason", dmod->reason)); 451 abort = 1; // Always SQUIT because it was explicitly denied by admins 452 continue; 453 } 454 455 // Doing a strict check for the module being fully loaded so we can emit an alert in that case too :> 456 mod = find_modptr_byname(name, 1); 457 if (!mod) 458 { 459 /* Since only the server missing the module will report it, we need to broadcast the warning network-wide ;] 460 * Obviously we won't take any real action if the module seems to be locally required only, except if it's marked as required 461 */ 462 if (modflag == 'R') 463 { 464 // We don't need to check the version yet because there's nothing to compare it to, so we'll treat it as if no require module::min-version was specified 465 unreal_log(ULOG_ERROR, "link", "LINK_MISSING_REQUIRED_MODULE", client, 466 "Server $me is missing module '$module_name' which " 467 "is required by server $client. -- aborting link", 468 log_data_client("me", &me), 469 log_data_string("module_name", name)); 470 abort = 1; // Always SQUIT here too (explicitly required by admins) 471 } 472 else if (modflag == 'G') 473 { 474 unreal_log(ULOG_WARNING, "link", "LINK_MISSING_GLOBAL_MODULE", client, 475 "Server $me is missing module '$module_name', which is " 476 "marked as global at $client", 477 log_data_client("me", &me), 478 log_data_string("module_name", name)); 479 } 480 continue; 481 } 482 483 // Further checks are only necessary for explicitly required mods 484 if (modflag != 'R') 485 continue; 486 487 // Module is loaded on both servers and the other end is require { }'ing a specific module version 488 // An explicit version was specified in require module { } but our module version is less than that 489 if (*version != '*' && strnatcasecmp(mod->header->version, version) < 0) 490 { 491 unreal_log(ULOG_ERROR, "link", "LINK_MODULE_OLD_VERSION", client, 492 "Server $me is using an old version of module '$module_name'. " 493 "Server $client requires us to have version $minimum_module_version or later (we have $our_module_version). " 494 "-- aborting link", 495 log_data_client("me", &me), 496 log_data_string("module_name", name), 497 log_data_string("minimum_module_version", version), 498 log_data_string("our_module_version", mod->header->version)); 499 abort = 1; 500 } 501 } 502 503 if (abort) 504 { 505 exit_client_fmt(client, NULL, "Link aborted due to missing or banned modules (see previous errors)"); 506 return; 507 } 508 } 509 510 int reqmods_hook_serverconnect(Client *client) 511 { 512 /* This function simply dumps a list of modules and their version to the other server, 513 * which will then run through the received list and check the names/versions 514 */ 515 char modflag; 516 char modbuf[64]; 517 char *modversion; 518 /* Try to use a large buffer, but take into account the hostname, command, spaces, etc */ 519 char sendbuf[BUFSIZE - HOSTLEN - 16]; 520 Module *mod; 521 ReqMod *rmod; 522 size_t len, modlen; 523 524 /* Let's not have leaves directly connected to the hub send their module list to other *leaves* as well =] 525 * Since the hub will introduce all servers currently linked to it, this hook is actually called for every separate node 526 */ 527 if (!MyConnect(client)) 528 return HOOK_CONTINUE; 529 530 sendbuf[0] = '\0'; 531 len = 0; 532 533 /* At this stage we don't care if a module isn't global (or not fully loaded), we'll dump all modules so we can properly deny 534 * certain ones across the network 535 * Also, the G flag is only used for modules that tag themselves as global, since we're keeping separate lists for require (R flag) and deny 536 */ 537 for (mod = Modules; mod; mod = mod->next) 538 { 539 modflag = SMOD_FLAG_LOCAL; 540 modversion = mod->header->version; 541 542 // require { }'d modules should be loaded on this server anyways, meaning we don't have to use a separate loop for those =] 543 if ((rmod = find_reqmod_byname(mod->header->name))) 544 { 545 // require module::min-version overrides the version found in the module's header 546 modflag = SMOD_FLAG_REQUIRED; 547 modversion = (rmod->minversion ? rmod->minversion : "*"); 548 } 549 550 else if ((mod->options & MOD_OPT_GLOBAL)) 551 modflag = SMOD_FLAG_GLOBAL; 552 553 ircsnprintf(modbuf, sizeof(modbuf), "%c:%s:%s", modflag, mod->header->name, modversion); 554 modlen = strlen(modbuf); 555 if (len + modlen + 2 > sizeof(sendbuf)) // Account for space and nullbyte, otherwise the last module entry might be cut off 556 { 557 // "Flush" current list =] 558 sendto_one(client, NULL, ":%s %s :%s", me.id, MSG_SMOD, sendbuf); 559 sendbuf[0] = '\0'; 560 len = 0; 561 } 562 563 /* Maybe account for the space between modules, can't do this earlier because otherwise the ircsnprintf() would skip past the nullbyte 564 * of the previous module (which in turn terminates the string prematurely) 565 */ 566 ircsnprintf(sendbuf + len, sizeof(sendbuf) - len, "%s%s", (len > 0 ? " " : ""), modbuf); 567 if (len) 568 len++; 569 len += modlen; 570 } 571 572 // May have something left 573 if (sendbuf[0]) 574 sendto_one(client, NULL, ":%s %s :%s", me.id, MSG_SMOD, sendbuf); 575 return HOOK_CONTINUE; 576 }