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 }