unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive

restrict-commands.c (13891B)

      1 /*
      2  * Restrict specific commands unless certain conditions have been met
      3  * (C) Copyright 2019 Gottem and the UnrealIRCd team
      4  *
      5  * This program is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU General Public License as published by
      7  * the Free Software Foundation; either version 1, or (at your option)
      8  * any later version.
      9  *
     10  * This program is distributed in the hope that it will be useful,
     11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  * GNU General Public License for more details.
     14  *
     15  * You should have received a copy of the GNU General Public License
     16  * along with this program; if not, write to the Free Software
     17  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     18  */
     19 
     20 #include "unrealircd.h"
     21 
     22 ModuleHeader MOD_HEADER = {
     23 	"restrict-commands",
     24 	"1.0.2",
     25 	"Restrict specific commands unless certain conditions have been met",
     26 	"UnrealIRCd Team",
     27 	"unrealircd-6",
     28 };
     29 
     30 typedef struct RestrictedCommand RestrictedCommand;
     31 struct RestrictedCommand {
     32 	RestrictedCommand *prev, *next;
     33 	char *cmd;
     34 	char *conftag;
     35 	SecurityGroup *except;
     36 };
     37 
     38 typedef struct {
     39 	char *conftag;
     40 	char *cmd;
     41 } CmdMap;
     42 
     43 // Forward declarations
     44 const char *find_cmd_byconftag(const char *conftag);
     45 RestrictedCommand *find_restrictions_bycmd(const char *cmd);
     46 RestrictedCommand *find_restrictions_byconftag(const char *conftag);
     47 int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
     48 int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
     49 int rcmd_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype);
     50 int rcmd_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype);
     51 int rcmd_can_join(Client *client, Channel *channel, const char *key, char **errmsg);
     52 int rcmd_block_message(Client *client, const char *destination, const char *text, SendType sendtype, const char **errmsg, const char *display, const char *conftag);
     53 int rcmd_block_join(Client *client, Channel *channel, const char **errmsg);
     54 CMD_OVERRIDE_FUNC(rcmd_override);
     55 
     56 // Globals
     57 static ModuleInfo ModInf;
     58 RestrictedCommand *RestrictedCommandList = NULL;
     59 CmdMap conf_cmdmaps[] = {
     60 	// These are special cases in which we can't override the command, so they are handled through hooks instead
     61 	{ "channel-message", "PRIVMSG" },
     62 	{ "channel-notice", "NOTICE" },
     63 	{ "channel-create", "JOIN" },
     64 	{ "private-message", "PRIVMSG" },
     65 	{ "private-notice", "NOTICE" },
     66 	{ NULL, NULL, }, // REQUIRED for the loop to properly work
     67 };
     68 
     69 MOD_TEST()
     70 {
     71 	memcpy(&ModInf, modinfo, modinfo->size);
     72 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, rcmd_configtest);
     73 	return MOD_SUCCESS;
     74 }
     75 
     76 MOD_INIT()
     77 {
     78 	MARK_AS_OFFICIAL_MODULE(modinfo);
     79 	HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, rcmd_configrun);
     80 
     81 	// Due to the nature of PRIVMSG/NOTICE we're gonna need to hook into PRE_* stuff instead of using command overrides
     82 	HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_CHANNEL, -1000000, rcmd_can_send_to_channel);
     83 	HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_USER, -1000000, rcmd_can_send_to_user);
     84 	// JOIN too
     85 	HookAdd(modinfo->handle, HOOKTYPE_CAN_JOIN, 0, rcmd_can_join);
     86 	return MOD_SUCCESS;
     87 }
     88 
     89 MOD_LOAD()
     90 {
     91 	if (ModuleGetError(modinfo->handle) != MODERR_NOERROR)
     92 	{
     93 		config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
     94 		return MOD_FAILED;
     95 	}
     96 	return MOD_SUCCESS;
     97 }
     98 
     99 MOD_UNLOAD()
    100 {
    101 	RestrictedCommand *rcmd, *next;
    102 	for (rcmd = RestrictedCommandList; rcmd; rcmd = next)
    103 	{
    104 		next = rcmd->next;
    105 		safe_free(rcmd->conftag);
    106 		safe_free(rcmd->cmd);
    107 		free_security_group(rcmd->except);
    108 		DelListItem(rcmd, RestrictedCommandList);
    109 		safe_free(rcmd);
    110 	}
    111 	RestrictedCommandList = NULL;
    112 	return MOD_SUCCESS;
    113 }
    114 
    115 const char *find_cmd_byconftag(const char *conftag) {
    116 	CmdMap *cmap;
    117 	for (cmap = conf_cmdmaps; cmap->conftag; cmap++)
    118 	{
    119 		if (!strcmp(cmap->conftag, conftag))
    120 			return cmap->cmd;
    121 	}
    122 	return NULL;
    123 }
    124 
    125 RestrictedCommand *find_restrictions_bycmd(const char *cmd) {
    126 	RestrictedCommand *rcmd;
    127 	for (rcmd = RestrictedCommandList; rcmd; rcmd = rcmd->next)
    128 	{
    129 		if (!strcasecmp(rcmd->cmd, cmd))
    130 			return rcmd;
    131 	}
    132 	return NULL;
    133 }
    134 
    135 RestrictedCommand *find_restrictions_byconftag(const char *conftag) {
    136 	RestrictedCommand *rcmd;
    137 	for (rcmd = RestrictedCommandList; rcmd; rcmd = rcmd->next)
    138 	{
    139 		if (rcmd->conftag && !strcmp(rcmd->conftag, conftag))
    140 			return rcmd;
    141 	}
    142 	return NULL;
    143 }
    144 
    145 int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
    146 {
    147 	int errors = 0;
    148 	int warn_disable = 0;
    149 	ConfigEntry *cep, *cep2;
    150 
    151 	// We are only interested in set::restrict-commands
    152 	if (type != CONFIG_SET)
    153 		return 0;
    154 
    155 	if (!ce || strcmp(ce->name, "restrict-commands"))
    156 		return 0;
    157 
    158 	for (cep = ce->items; cep; cep = cep->next)
    159 	{
    160 		for (cep2 = cep->items; cep2; cep2 = cep2->next)
    161 		{
    162 			if (!strcmp(cep2->name, "disable"))
    163 			{
    164 				config_warn("%s:%i: set::restrict-commands::%s: the 'disable' option has been removed.",
    165 				            cep2->file->filename, cep2->line_number, cep->name);
    166 				if (!warn_disable)
    167 				{
    168 					config_warn("Simply remove 'disable yes;' from the configuration file and "
    169 				                   "it will have the same effect without it (will disable the command).");
    170 					warn_disable = 1;
    171 				}
    172 				continue;
    173 			}
    174 
    175 			if (!strcmp(cep2->name, "except"))
    176 			{
    177 				test_match_block(cf, cep2, &errors);
    178 				continue;
    179 			}
    180 
    181 			if (!cep2->value)
    182 			{
    183 				config_error("%s:%i: blank set::restrict-commands::%s:%s without value", cep2->file->filename, cep2->line_number, cep->name, cep2->name);
    184 				errors++;
    185 				continue;
    186 			}
    187 
    188 			if (!strcmp(cep2->name, "connect-delay"))
    189 			{
    190 				long v = config_checkval(cep2->value, CFG_TIME);
    191 				if ((v < 1) || (v > 3600))
    192 				{
    193 					config_error("%s:%i: set::restrict-commands::%s::connect-delay should be in range 1-3600", cep2->file->filename, cep2->line_number, cep->name);
    194 					errors++;
    195 				}
    196 				continue;
    197 			}
    198 
    199 			if (!strcmp(cep2->name, "exempt-identified"))
    200 				continue;
    201 
    202 			if (!strcmp(cep2->name, "exempt-webirc"))
    203 				continue;
    204 
    205 			if (!strcmp(cep2->name, "exempt-tls"))
    206 				continue;
    207 
    208 			if (!strcmp(cep2->name, "exempt-reputation-score"))
    209 			{
    210 				int v = atoi(cep2->value);
    211 				if (v <= 0)
    212 				{
    213 					config_error("%s:%i: set::restrict-commands::%s::exempt-reputation-score must be greater than 0", cep2->file->filename, cep2->line_number, cep->name);
    214 					errors++;
    215 				}
    216 				continue;
    217 			}
    218 
    219 			config_error("%s:%i: unknown directive set::restrict-commands::%s::%s", cep2->file->filename, cep2->line_number, cep->name, cep2->name);
    220 			errors++;
    221 		}
    222 	}
    223 
    224 	*errs = errors;
    225 	return errors ? -1 : 1;
    226 }
    227 
    228 int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
    229 {
    230 	ConfigEntry *cep, *cep2;
    231 	const char *cmd, *conftag;
    232 	RestrictedCommand *rcmd;
    233 
    234 	// We are only interested in set::restrict-commands
    235 	if (type != CONFIG_SET)
    236 		return 0;
    237 
    238 	if (!ce || strcmp(ce->name, "restrict-commands"))
    239 		return 0;
    240 
    241 	for (cep = ce->items; cep; cep = cep->next)
    242 	{
    243 		// May need to switch some stuff around for special cases where the config directive doesn't match the actual command
    244 		conftag = NULL;
    245 		if ((cmd = find_cmd_byconftag(cep->name)))
    246 			conftag = cep->name;
    247 		else
    248 			cmd = cep->name;
    249 
    250 		// Try to add override before even allocating the struct so we can bail early
    251 		// Also don't override anything from the conf_cmdmaps[] list because those are handled through hooks instead
    252 		if (!conftag)
    253 		{
    254 			// Let's hope nobody tries to unload the module for PRIVMSG/NOTICE :^)
    255 			if (!CommandExists(cmd))
    256 			{
    257 				config_warn("[restrict-commands] Command '%s' does not exist. Did you mistype? Or is the module providing it not loaded?", cmd);
    258 				continue;
    259 			}
    260 			if (find_restrictions_bycmd(cmd))
    261 			{
    262 				config_warn("[restrict-commands] Multiple set::restrict-commands items for command '%s'. "
    263 				            "Only one config block will be effective.",
    264 				            cmd);
    265 				continue;
    266 			}
    267 			if (!CommandOverrideAdd(ModInf.handle, cmd, 0, rcmd_override))
    268 			{
    269 				config_warn("[restrict-commands] Failed to add override for '%s' (NO RESTRICTIONS APPLY)", cmd);
    270 				continue;
    271 			}
    272 		}
    273 
    274 		rcmd = safe_alloc(sizeof(RestrictedCommand));
    275 		safe_strdup(rcmd->cmd, cmd);
    276 		safe_strdup(rcmd->conftag, conftag);
    277 		rcmd->except = safe_alloc(sizeof(SecurityGroup));
    278 
    279 		for (cep2 = cep->items; cep2; cep2 = cep2->next)
    280 		{
    281 			if (!strcmp(cep2->name, "except"))
    282 			{
    283 				conf_match_block(cf, cep2, &rcmd->except);
    284 				continue;
    285 			}
    286 
    287 			if (!cep2->value)
    288 				continue;
    289 
    290 			if (!strcmp(cep2->name, "connect-delay"))
    291 			{
    292 				rcmd->except->connect_time = config_checkval(cep2->value, CFG_TIME);
    293 				continue;
    294 			}
    295 
    296 			if (!strcmp(cep2->name, "exempt-identified"))
    297 			{
    298 				rcmd->except->identified = config_checkval(cep2->value, CFG_YESNO);
    299 				continue;
    300 			}
    301 
    302 			if (!strcmp(cep2->name, "exempt-webirc"))
    303 			{
    304 				rcmd->except->webirc = config_checkval(cep2->value, CFG_YESNO);
    305 				continue;
    306 			}
    307 
    308 			if (!strcmp(cep2->name, "exempt-tls"))
    309 			{
    310 				rcmd->except->tls = config_checkval(cep2->value, CFG_YESNO);
    311 				continue;
    312 			}
    313 
    314 			if (!strcmp(cep2->name, "exempt-reputation-score"))
    315 			{
    316 				rcmd->except->reputation_score = atoi(cep2->value);
    317 				continue;
    318 			}
    319 		}
    320 		AddListItem(rcmd, RestrictedCommandList);
    321 	}
    322 
    323 	return 1;
    324 }
    325 
    326 int rcmd_canbypass(Client *client, RestrictedCommand *rcmd, crule_context *context)
    327 {
    328 	if (!client || !rcmd)
    329 		return 1;
    330 	if (user_allowed_by_security_group_context(client, rcmd->except, context))
    331 		return 1;
    332 	return 0;
    333 }
    334 
    335 int rcmd_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype)
    336 {
    337 	if (rcmd_block_message(client, channel->name, *msg, sendtype, errmsg, "channel", (sendtype == SEND_TYPE_NOTICE ? "channel-notice" : "channel-message")))
    338 		return HOOK_DENY;
    339 
    340 	return HOOK_CONTINUE;
    341 }
    342 
    343 int rcmd_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype)
    344 {
    345 	// Need a few extra exceptions for user messages only =]
    346 	if ((client == target) || IsULine(target))
    347 		return HOOK_CONTINUE; /* bypass/exempt */
    348 
    349 	if (rcmd_block_message(client, target->name, *text, sendtype, errmsg, "user", (sendtype == SEND_TYPE_NOTICE ? "private-notice" : "private-message")))
    350 		return HOOK_DENY;
    351 
    352 	return HOOK_CONTINUE;
    353 }
    354 
    355 int rcmd_block_message(Client *client, const char *destination, const char *text, SendType sendtype, const char **errmsg, const char *display, const char *conftag)
    356 {
    357 	crule_context context;
    358 	RestrictedCommand *rcmd;
    359 	static char errbuf[256];
    360 
    361 	// Let's allow non-local users, opers and U:Lines early =]
    362 	if (!MyUser(client) || !client->local || IsOper(client) || IsULine(client))
    363 		return 0;
    364 
    365 	memset(&context, 0, sizeof(context));
    366 	context.client = client;
    367 	context.destination = destination;
    368 
    369 	rcmd = find_restrictions_byconftag(conftag);
    370 	if (rcmd && !rcmd_canbypass(client, rcmd, &context))
    371 	{
    372 		int notice = (sendtype == SEND_TYPE_NOTICE ? 1 : 0); // temporary hack FIXME !!!
    373 		if (rcmd->except->connect_time)
    374 		{
    375 			ircsnprintf(errbuf, sizeof(errbuf),
    376 				    "You cannot send %ss to %ss until you've been connected for %ld seconds or more",
    377 				    (notice ? "notice" : "message"), display, rcmd->except->connect_time);
    378 		} else {
    379 			ircsnprintf(errbuf, sizeof(errbuf),
    380 				    "Sending of %ss to %ss been disabled by the network administrators",
    381 				    (notice ? "notice" : "message"), display);
    382 		}
    383 		*errmsg = errbuf;
    384 		return 1;
    385 	}
    386 
    387 	// No restrictions apply, process command as normal =]
    388 	return 0;
    389 }
    390 
    391 CMD_OVERRIDE_FUNC(rcmd_override)
    392 {
    393 	RestrictedCommand *rcmd;
    394 	crule_context context;
    395 
    396 	if (!MyUser(client) || !client->local || IsOper(client) || IsULine(client))
    397 	{
    398 		CALL_NEXT_COMMAND_OVERRIDE();
    399 		return;
    400 	}
    401 
    402 	memset(&context, 0, sizeof(context));
    403 	context.client = client;
    404 
    405 	rcmd = find_restrictions_bycmd(ovr->command->cmd);
    406 	if (rcmd && !rcmd_canbypass(client, rcmd, &context))
    407 	{
    408 		if (rcmd->except->connect_time)
    409 		{
    410 			sendnumericfmt(client, ERR_UNKNOWNCOMMAND,
    411 			               "%s :You must be connected for at least %ld seconds before you can use this command",
    412 			               ovr->command->cmd, rcmd->except->connect_time);
    413 		} else {
    414 			sendnumericfmt(client, ERR_UNKNOWNCOMMAND,
    415 			               "%s :This command is disabled by the network administrator",
    416 			               ovr->command->cmd);
    417 		}
    418 		return;
    419 	}
    420 
    421 	// No restrictions apply, process command as normal =]
    422 	CALL_NEXT_COMMAND_OVERRIDE();
    423 }
    424 
    425 
    426 int rcmd_can_join(Client *client, Channel *channel, const char *key, char **errmsg)
    427 {
    428 	const char *join_err = NULL;
    429 
    430 	// If the channel already existed before, no restriction
    431 	if (channel->users || has_channel_mode(channel, 'P'))
    432 		return 0;
    433 
    434 	else if (rcmd_block_join(client, channel, &join_err))
    435 	{
    436 		static char formatted_errmsg[512];
    437 		snprintf(formatted_errmsg, sizeof(formatted_errmsg), "JOIN :%s", join_err);
    438 
    439 		*errmsg = formatted_errmsg;
    440 		return ERR_CANNOTDOCOMMAND;
    441 	}
    442 	return 0;
    443 }
    444 
    445 int rcmd_block_join(Client *client, Channel *channel, const char **errmsg)
    446 {
    447 	RestrictedCommand *rcmd;
    448 	crule_context context;
    449 	static char errbuf[256];
    450 
    451 	if (!MyUser(client) || !client->local || IsOper(client) || IsULine(client))
    452 		return 0;
    453 
    454 	memset(&context, 0, sizeof(context));
    455 	context.client = client;
    456 	context.destination = channel->name;
    457 
    458 	rcmd = find_restrictions_byconftag("channel-create");
    459 	if (rcmd && !rcmd_canbypass(client, rcmd, &context))
    460 	{
    461 		if (rcmd->except->connect_time)
    462 		{
    463 			ircsnprintf(errbuf, sizeof(errbuf),
    464 					"You cannot create new channels until you have been connected for %ld seconds or more.", rcmd->except->connect_time);
    465 		} else {
    466 			ircsnprintf(errbuf, sizeof(errbuf),
    467 					"Creation of new channels has been restricted by the network administrator.");
    468 		}
    469 		*errmsg = errbuf;
    470 		return 1;
    471 	}
    472 
    473 	return 0;
    474 }