mirror of
https://github.com/unrealircd/unrealircd.git
synced 2026-07-06 22:43:12 +02:00
9691a6d819
switches like antimixedutf8 did, and counts the number of characters used per unicode block. Potentially more can be added later, this is flexible and modules can add stuff (..well not yet.. the struct is missing some members..). Use it from antimixedutf8 so that it now uses the new code, which is similar to what I made and then reverted in July 2023: https://github.com/unrealircd/unrealircd/commit/3e2f668f10fccedfd035526d7b20d7ca6819a8ae ..except that it now calculated in src/modules/utf8functions.c. But yeah, this needs more testing and possibly (default) score adjustments to deal with false positives !! And a warning in release notes :D Put the text analysis in ClientContext member textanalysis, so typically accessed through clictx->textanalysis. Note that this struct can (and often is) NULL, for example if it is a remote client, if it is not a PRIVMSG/NOTICE (will improve later) or if the utf8functions module is not loaded (to keep things optional). BREAKING CHANGE is that ClientContext is now passed in the HOOKTYPE_CAN_SEND_TO_CHANNEL and HOOKTYPE_CAN_SEND_TO_USER hooks. So HOOKTYPE_CAN_SEND_TO_USER prototype changed from: int hooktype_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype); To: int hooktype_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype, ClientContext *clictx); And HOOKTYPE_CAN_SEND_TO_CHANNEL prototype changes from: int hooktype_can_send_to_channel(Client *client, Channel *channel, Membership *member, const char **text, const char **errmsg, SendType sendtype); To: int hooktype_can_send_to_channel(Client *client, Channel *channel, Membership *member, const char **text, const char **errmsg, SendType sendtype, ClientContext *clictx); A side-affect of this change for antimixedutf8 purposes is that, while the analysis is only done once per line, the 'actions' are performed for each target, so the action will run 4 times for "PRIVMSG a,b,c,d :text" although that may not be important in practice. Just mentioning.
475 lines
14 KiB
C
475 lines
14 KiB
C
/*
|
|
* Restrict specific commands unless certain conditions have been met
|
|
* (C) Copyright 2019 Gottem and the UnrealIRCd team
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 1, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "unrealircd.h"
|
|
|
|
ModuleHeader MOD_HEADER = {
|
|
"restrict-commands",
|
|
"1.0.2",
|
|
"Restrict specific commands unless certain conditions have been met",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-6",
|
|
};
|
|
|
|
typedef struct RestrictedCommand RestrictedCommand;
|
|
struct RestrictedCommand {
|
|
RestrictedCommand *prev, *next;
|
|
char *cmd;
|
|
char *conftag;
|
|
SecurityGroup *except;
|
|
};
|
|
|
|
typedef struct {
|
|
char *conftag;
|
|
char *cmd;
|
|
} CmdMap;
|
|
|
|
// Forward declarations
|
|
const char *find_cmd_byconftag(const char *conftag);
|
|
RestrictedCommand *find_restrictions_bycmd(const char *cmd);
|
|
RestrictedCommand *find_restrictions_byconftag(const char *conftag);
|
|
int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
|
int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
|
|
int rcmd_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype, ClientContext *clictx);
|
|
int rcmd_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype, ClientContext *clictx);
|
|
int rcmd_can_join(Client *client, Channel *channel, const char *key, char **errmsg);
|
|
int rcmd_block_message(Client *client, const char *destination, const char *text, SendType sendtype, const char **errmsg, const char *display, const char *conftag);
|
|
int rcmd_block_join(Client *client, Channel *channel, const char **errmsg);
|
|
CMD_OVERRIDE_FUNC(rcmd_override);
|
|
|
|
// Globals
|
|
static ModuleInfo ModInf;
|
|
RestrictedCommand *RestrictedCommandList = NULL;
|
|
CmdMap conf_cmdmaps[] = {
|
|
// These are special cases in which we can't override the command, so they are handled through hooks instead
|
|
{ "channel-message", "PRIVMSG" },
|
|
{ "channel-notice", "NOTICE" },
|
|
{ "channel-create", "JOIN" },
|
|
{ "private-message", "PRIVMSG" },
|
|
{ "private-notice", "NOTICE" },
|
|
{ NULL, NULL, }, // REQUIRED for the loop to properly work
|
|
};
|
|
|
|
MOD_TEST()
|
|
{
|
|
memcpy(&ModInf, modinfo, modinfo->size);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, rcmd_configtest);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_INIT()
|
|
{
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, rcmd_configrun);
|
|
|
|
// Due to the nature of PRIVMSG/NOTICE we're gonna need to hook into PRE_* stuff instead of using command overrides
|
|
HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_CHANNEL, -1000000, rcmd_can_send_to_channel);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CAN_SEND_TO_USER, -1000000, rcmd_can_send_to_user);
|
|
// JOIN too
|
|
HookAdd(modinfo->handle, HOOKTYPE_CAN_JOIN, 0, rcmd_can_join);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_LOAD()
|
|
{
|
|
if (ModuleGetError(modinfo->handle) != MODERR_NOERROR)
|
|
{
|
|
config_error("A critical error occurred when loading module %s: %s", MOD_HEADER.name, ModuleGetErrorStr(modinfo->handle));
|
|
return MOD_FAILED;
|
|
}
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_UNLOAD()
|
|
{
|
|
RestrictedCommand *rcmd, *next;
|
|
for (rcmd = RestrictedCommandList; rcmd; rcmd = next)
|
|
{
|
|
next = rcmd->next;
|
|
safe_free(rcmd->conftag);
|
|
safe_free(rcmd->cmd);
|
|
free_security_group(rcmd->except);
|
|
DelListItem(rcmd, RestrictedCommandList);
|
|
safe_free(rcmd);
|
|
}
|
|
RestrictedCommandList = NULL;
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
const char *find_cmd_byconftag(const char *conftag) {
|
|
CmdMap *cmap;
|
|
for (cmap = conf_cmdmaps; cmap->conftag; cmap++)
|
|
{
|
|
if (!strcmp(cmap->conftag, conftag))
|
|
return cmap->cmd;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
RestrictedCommand *find_restrictions_bycmd(const char *cmd) {
|
|
RestrictedCommand *rcmd;
|
|
for (rcmd = RestrictedCommandList; rcmd; rcmd = rcmd->next)
|
|
{
|
|
if (!strcasecmp(rcmd->cmd, cmd))
|
|
return rcmd;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
RestrictedCommand *find_restrictions_byconftag(const char *conftag) {
|
|
RestrictedCommand *rcmd;
|
|
for (rcmd = RestrictedCommandList; rcmd; rcmd = rcmd->next)
|
|
{
|
|
if (rcmd->conftag && !strcmp(rcmd->conftag, conftag))
|
|
return rcmd;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int rcmd_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
int errors = 0;
|
|
int warn_disable = 0;
|
|
ConfigEntry *cep, *cep2;
|
|
|
|
// We are only interested in set::restrict-commands
|
|
if (type != CONFIG_SET)
|
|
return 0;
|
|
|
|
if (!ce || strcmp(ce->name, "restrict-commands"))
|
|
return 0;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
for (cep2 = cep->items; cep2; cep2 = cep2->next)
|
|
{
|
|
if (!strcmp(cep2->name, "disable"))
|
|
{
|
|
config_warn("%s:%i: set::restrict-commands::%s: the 'disable' option has been removed.",
|
|
cep2->file->filename, cep2->line_number, cep->name);
|
|
if (!warn_disable)
|
|
{
|
|
config_warn("Simply remove 'disable yes;' from the configuration file and "
|
|
"it will have the same effect without it (will disable the command).");
|
|
warn_disable = 1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "except"))
|
|
{
|
|
test_match_block(cf, cep2, &errors);
|
|
continue;
|
|
}
|
|
|
|
if (!cep2->value)
|
|
{
|
|
config_error("%s:%i: blank set::restrict-commands::%s:%s without value", cep2->file->filename, cep2->line_number, cep->name, cep2->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "connect-delay"))
|
|
{
|
|
long v = config_checkval(cep2->value, CFG_TIME);
|
|
if ((v < 1) || (v > 3600))
|
|
{
|
|
config_error("%s:%i: set::restrict-commands::%s::connect-delay should be in range 1-3600", cep2->file->filename, cep2->line_number, cep->name);
|
|
errors++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "exempt-identified"))
|
|
continue;
|
|
|
|
if (!strcmp(cep2->name, "exempt-webirc"))
|
|
continue;
|
|
|
|
if (!strcmp(cep2->name, "exempt-tls"))
|
|
continue;
|
|
|
|
if (!strcmp(cep2->name, "exempt-reputation-score"))
|
|
{
|
|
int v = atoi(cep2->value);
|
|
if (v <= 0)
|
|
{
|
|
config_error("%s:%i: set::restrict-commands::%s::exempt-reputation-score must be greater than 0", cep2->file->filename, cep2->line_number, cep->name);
|
|
errors++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
config_error("%s:%i: unknown directive set::restrict-commands::%s::%s", cep2->file->filename, cep2->line_number, cep->name, cep2->name);
|
|
errors++;
|
|
}
|
|
}
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
int rcmd_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
|
|
{
|
|
ConfigEntry *cep, *cep2;
|
|
const char *cmd, *conftag;
|
|
RestrictedCommand *rcmd;
|
|
|
|
// We are only interested in set::restrict-commands
|
|
if (type != CONFIG_SET)
|
|
return 0;
|
|
|
|
if (!ce || strcmp(ce->name, "restrict-commands"))
|
|
return 0;
|
|
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
// May need to switch some stuff around for special cases where the config directive doesn't match the actual command
|
|
conftag = NULL;
|
|
if ((cmd = find_cmd_byconftag(cep->name)))
|
|
conftag = cep->name;
|
|
else
|
|
cmd = cep->name;
|
|
|
|
// Try to add override before even allocating the struct so we can bail early
|
|
// Also don't override anything from the conf_cmdmaps[] list because those are handled through hooks instead
|
|
if (!conftag)
|
|
{
|
|
// Let's hope nobody tries to unload the module for PRIVMSG/NOTICE :^)
|
|
if (!CommandExists(cmd))
|
|
{
|
|
config_warn("[restrict-commands] Command '%s' does not exist. Did you mistype? Or is the module providing it not loaded?", cmd);
|
|
continue;
|
|
}
|
|
if (find_restrictions_bycmd(cmd))
|
|
{
|
|
config_warn("[restrict-commands] Multiple set::restrict-commands items for command '%s'. "
|
|
"Only one config block will be effective.",
|
|
cmd);
|
|
continue;
|
|
}
|
|
if (!CommandOverrideAdd(ModInf.handle, cmd, 0, rcmd_override))
|
|
{
|
|
config_warn("[restrict-commands] Failed to add override for '%s' (NO RESTRICTIONS APPLY)", cmd);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
rcmd = safe_alloc(sizeof(RestrictedCommand));
|
|
safe_strdup(rcmd->cmd, cmd);
|
|
safe_strdup(rcmd->conftag, conftag);
|
|
rcmd->except = safe_alloc(sizeof(SecurityGroup));
|
|
|
|
for (cep2 = cep->items; cep2; cep2 = cep2->next)
|
|
{
|
|
if (!strcmp(cep2->name, "except"))
|
|
{
|
|
conf_match_block(cf, cep2, &rcmd->except);
|
|
continue;
|
|
}
|
|
|
|
if (!cep2->value)
|
|
continue;
|
|
|
|
if (!strcmp(cep2->name, "connect-delay"))
|
|
{
|
|
rcmd->except->connect_time = config_checkval(cep2->value, CFG_TIME);
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "exempt-identified"))
|
|
{
|
|
rcmd->except->identified = config_checkval(cep2->value, CFG_YESNO);
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "exempt-webirc"))
|
|
{
|
|
rcmd->except->webirc = config_checkval(cep2->value, CFG_YESNO);
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "exempt-tls"))
|
|
{
|
|
rcmd->except->tls = config_checkval(cep2->value, CFG_YESNO);
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep2->name, "exempt-reputation-score"))
|
|
{
|
|
rcmd->except->reputation_score = atoi(cep2->value);
|
|
continue;
|
|
}
|
|
}
|
|
AddListItem(rcmd, RestrictedCommandList);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
int rcmd_canbypass(Client *client, RestrictedCommand *rcmd, crule_context *context)
|
|
{
|
|
if (!client || !rcmd)
|
|
return 1;
|
|
if (user_allowed_by_security_group_context(client, rcmd->except, context))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
int rcmd_can_send_to_channel(Client *client, Channel *channel, Membership *lp, const char **msg, const char **errmsg, SendType sendtype, ClientContext *clictx)
|
|
{
|
|
if (rcmd_block_message(client, channel->name, *msg, sendtype, errmsg, "channel", (sendtype == SEND_TYPE_NOTICE ? "channel-notice" : "channel-message")))
|
|
return HOOK_DENY;
|
|
|
|
return HOOK_CONTINUE;
|
|
}
|
|
|
|
int rcmd_can_send_to_user(Client *client, Client *target, const char **text, const char **errmsg, SendType sendtype, ClientContext *clictx)
|
|
{
|
|
// Need a few extra exceptions for user messages only =]
|
|
if ((client == target) || IsULine(target))
|
|
return HOOK_CONTINUE; /* bypass/exempt */
|
|
|
|
if (rcmd_block_message(client, target->name, *text, sendtype, errmsg, "user", (sendtype == SEND_TYPE_NOTICE ? "private-notice" : "private-message")))
|
|
return HOOK_DENY;
|
|
|
|
return HOOK_CONTINUE;
|
|
}
|
|
|
|
int rcmd_block_message(Client *client, const char *destination, const char *text, SendType sendtype, const char **errmsg, const char *display, const char *conftag)
|
|
{
|
|
crule_context context;
|
|
RestrictedCommand *rcmd;
|
|
static char errbuf[256];
|
|
|
|
// Let's allow non-local users, opers and U:Lines early =]
|
|
if (!MyUser(client) || !client->local || IsOper(client) || IsULine(client))
|
|
return 0;
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.client = client;
|
|
context.destination = destination;
|
|
|
|
rcmd = find_restrictions_byconftag(conftag);
|
|
if (rcmd && !rcmd_canbypass(client, rcmd, &context))
|
|
{
|
|
int notice = (sendtype == SEND_TYPE_NOTICE ? 1 : 0); // temporary hack FIXME !!!
|
|
if (rcmd->except->connect_time)
|
|
{
|
|
ircsnprintf(errbuf, sizeof(errbuf),
|
|
"You cannot send %ss to %ss until you've been connected for %ld seconds or more",
|
|
(notice ? "notice" : "message"), display, rcmd->except->connect_time);
|
|
} else {
|
|
ircsnprintf(errbuf, sizeof(errbuf),
|
|
"Sending of %ss to %ss been disabled by the network administrators",
|
|
(notice ? "notice" : "message"), display);
|
|
}
|
|
*errmsg = errbuf;
|
|
return 1;
|
|
}
|
|
|
|
// No restrictions apply, process command as normal =]
|
|
return 0;
|
|
}
|
|
|
|
CMD_OVERRIDE_FUNC(rcmd_override)
|
|
{
|
|
RestrictedCommand *rcmd;
|
|
crule_context context;
|
|
|
|
if (!MyUser(client) || !client->local || IsOper(client) || IsULine(client))
|
|
{
|
|
CALL_NEXT_COMMAND_OVERRIDE();
|
|
return;
|
|
}
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.client = client;
|
|
|
|
rcmd = find_restrictions_bycmd(ovr->command->cmd);
|
|
if (rcmd && !rcmd_canbypass(client, rcmd, &context))
|
|
{
|
|
if (rcmd->except->connect_time)
|
|
{
|
|
sendnumericfmt(client, ERR_UNKNOWNCOMMAND,
|
|
"%s :You must be connected for at least %ld seconds before you can use this command",
|
|
ovr->command->cmd, rcmd->except->connect_time);
|
|
} else {
|
|
sendnumericfmt(client, ERR_UNKNOWNCOMMAND,
|
|
"%s :This command is disabled by the network administrator",
|
|
ovr->command->cmd);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// No restrictions apply, process command as normal =]
|
|
CALL_NEXT_COMMAND_OVERRIDE();
|
|
}
|
|
|
|
|
|
int rcmd_can_join(Client *client, Channel *channel, const char *key, char **errmsg)
|
|
{
|
|
const char *join_err = NULL;
|
|
|
|
// If the channel already existed before, no restriction
|
|
if (channel->users || has_channel_mode(channel, 'P'))
|
|
return 0;
|
|
|
|
else if (rcmd_block_join(client, channel, &join_err))
|
|
{
|
|
static char formatted_errmsg[512];
|
|
snprintf(formatted_errmsg, sizeof(formatted_errmsg), "JOIN :%s", join_err);
|
|
|
|
*errmsg = formatted_errmsg;
|
|
return ERR_CANNOTDOCOMMAND;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int rcmd_block_join(Client *client, Channel *channel, const char **errmsg)
|
|
{
|
|
RestrictedCommand *rcmd;
|
|
crule_context context;
|
|
static char errbuf[256];
|
|
|
|
if (!MyUser(client) || !client->local || IsOper(client) || IsULine(client))
|
|
return 0;
|
|
|
|
memset(&context, 0, sizeof(context));
|
|
context.client = client;
|
|
context.destination = channel->name;
|
|
|
|
rcmd = find_restrictions_byconftag("channel-create");
|
|
if (rcmd && !rcmd_canbypass(client, rcmd, &context))
|
|
{
|
|
if (rcmd->except->connect_time)
|
|
{
|
|
ircsnprintf(errbuf, sizeof(errbuf),
|
|
"You cannot create new channels until you have been connected for %ld seconds or more.", rcmd->except->connect_time);
|
|
} else {
|
|
ircsnprintf(errbuf, sizeof(errbuf),
|
|
"Creation of new channels has been restricted by the network administrator.");
|
|
}
|
|
*errmsg = errbuf;
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|