mirror of
git://git.acid.vegas/unrealircd.git
synced 2024-09-27 21:00:35 +00:00
577 lines
16 KiB
C
577 lines
16 KiB
C
/*
|
|
* Check for modules that are required across the network, as well as modules
|
|
* that *aren't* even allowed (deny/require module { } blocks)
|
|
* (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"
|
|
|
|
#define MSG_SMOD "SMOD"
|
|
#define SMOD_FLAG_REQUIRED 'R'
|
|
#define SMOD_FLAG_GLOBAL 'G'
|
|
#define SMOD_FLAG_LOCAL 'L'
|
|
|
|
ModuleHeader MOD_HEADER = {
|
|
"require-module",
|
|
"5.0.1",
|
|
"Require/deny modules across the network",
|
|
"UnrealIRCd Team",
|
|
"unrealircd-6",
|
|
};
|
|
|
|
typedef struct _denymod DenyMod;
|
|
struct _denymod {
|
|
DenyMod *prev, *next;
|
|
char *name;
|
|
char *reason;
|
|
};
|
|
|
|
typedef struct _requiremod ReqMod;
|
|
struct _requiremod {
|
|
ReqMod *prev, *next;
|
|
char *name;
|
|
char *minversion;
|
|
};
|
|
|
|
// Forward declarations
|
|
Module *find_modptr_byname(char *name, unsigned strict);
|
|
DenyMod *find_denymod_byname(char *name);
|
|
ReqMod *find_reqmod_byname(char *name);
|
|
|
|
int reqmods_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
|
int reqmods_configrun(ConfigFile *cf, ConfigEntry *ce, int type);
|
|
|
|
int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
|
int reqmods_configrun_deny(ConfigFile *cf, ConfigEntry *ce, int type);
|
|
|
|
int reqmods_configtest_require(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
|
int reqmods_configrun_require(ConfigFile *cf, ConfigEntry *ce, int type);
|
|
|
|
CMD_FUNC(cmd_smod);
|
|
int reqmods_hook_serverconnect(Client *client);
|
|
|
|
// Globals
|
|
extern MODVAR Module *Modules;
|
|
DenyMod *DenyModList = NULL;
|
|
ReqMod *ReqModList = NULL;
|
|
|
|
MOD_TEST()
|
|
{
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, reqmods_configtest);
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
MOD_INIT()
|
|
{
|
|
MARK_AS_OFFICIAL_MODULE(modinfo);
|
|
MARK_AS_GLOBAL_MODULE(modinfo);
|
|
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, reqmods_configrun);
|
|
HookAdd(modinfo->handle, HOOKTYPE_SERVER_CONNECT, 0, reqmods_hook_serverconnect);
|
|
CommandAdd(modinfo->handle, MSG_SMOD, cmd_smod, MAXPARA, CMD_SERVER);
|
|
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()
|
|
{
|
|
DenyMod *dmod, *dnext;
|
|
ReqMod *rmod, *rnext;
|
|
for (dmod = DenyModList; dmod; dmod = dnext)
|
|
{
|
|
dnext = dmod->next;
|
|
safe_free(dmod->name);
|
|
safe_free(dmod->reason);
|
|
DelListItem(dmod, DenyModList);
|
|
safe_free(dmod);
|
|
}
|
|
for (rmod = ReqModList; rmod; rmod = rnext)
|
|
{
|
|
rnext = rmod->next;
|
|
safe_free(rmod->name);
|
|
safe_free(rmod->minversion);
|
|
DelListItem(rmod, ReqModList);
|
|
safe_free(rmod);
|
|
}
|
|
DenyModList = NULL;
|
|
ReqModList = NULL;
|
|
return MOD_SUCCESS;
|
|
}
|
|
|
|
Module *find_modptr_byname(char *name, unsigned strict)
|
|
{
|
|
Module *mod;
|
|
for (mod = Modules; mod; mod = mod->next)
|
|
{
|
|
// Let's not be too strict with the name
|
|
if (!strcasecmp(mod->header->name, name))
|
|
{
|
|
if (strict && !(mod->flags & MODFLAG_LOADED))
|
|
mod = NULL;
|
|
return mod;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
DenyMod *find_denymod_byname(char *name)
|
|
{
|
|
DenyMod *dmod;
|
|
for (dmod = DenyModList; dmod; dmod = dmod->next)
|
|
{
|
|
if (!strcasecmp(dmod->name, name))
|
|
return dmod;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
ReqMod *find_reqmod_byname(char *name)
|
|
{
|
|
ReqMod *rmod;
|
|
for (rmod = ReqModList; rmod; rmod = rmod->next)
|
|
{
|
|
if (!strcasecmp(rmod->name, name))
|
|
return rmod;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
int reqmods_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
if (type == CONFIG_DENY)
|
|
return reqmods_configtest_deny(cf, ce, type, errs);
|
|
|
|
if (type == CONFIG_REQUIRE)
|
|
return reqmods_configtest_require(cf, ce, type, errs);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int reqmods_configrun(ConfigFile *cf, ConfigEntry *ce, int type)
|
|
{
|
|
if (type == CONFIG_DENY)
|
|
return reqmods_configrun_deny(cf, ce, type);
|
|
|
|
if (type == CONFIG_REQUIRE)
|
|
return reqmods_configrun_require(cf, ce, type);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
int errors = 0;
|
|
ConfigEntry *cep;
|
|
int has_name, has_reason;
|
|
|
|
// We are only interested in deny module { }
|
|
if (strcmp(ce->value, "module"))
|
|
return 0;
|
|
|
|
has_name = has_reason = 0;
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strlen(cep->name))
|
|
{
|
|
config_error("%s:%i: blank directive for deny module { } block", cep->file->filename, cep->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
if (!cep->value || !strlen(cep->value))
|
|
{
|
|
config_error("%s:%i: blank %s without value for deny module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep->name, "name"))
|
|
{
|
|
if (has_name)
|
|
{
|
|
config_error("%s:%i: duplicate %s for deny module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
continue;
|
|
}
|
|
|
|
// We do a loose check here because a module might not be fully loaded yet
|
|
if (find_modptr_byname(cep->value, 0))
|
|
{
|
|
config_error("[require-module] Module '%s' was specified as denied but we've actually loaded it ourselves", cep->value);
|
|
errors++;
|
|
}
|
|
has_name = 1;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep->name, "reason")) // Optional
|
|
{
|
|
// Still check for duplicate directives though
|
|
if (has_reason)
|
|
{
|
|
config_error("%s:%i: duplicate %s for deny module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
has_reason = 1;
|
|
continue;
|
|
}
|
|
|
|
config_error("%s:%i: unknown directive %s for deny module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
}
|
|
|
|
if (!has_name)
|
|
{
|
|
config_error("%s:%i: missing required 'name' directive for deny module { } block", ce->file->filename, ce->line_number);
|
|
errors++;
|
|
}
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
int reqmods_configrun_deny(ConfigFile *cf, ConfigEntry *ce, int type)
|
|
{
|
|
ConfigEntry *cep;
|
|
DenyMod *dmod;
|
|
|
|
if (strcmp(ce->value, "module"))
|
|
return 0;
|
|
|
|
dmod = safe_alloc(sizeof(DenyMod));
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "name"))
|
|
{
|
|
safe_strdup(dmod->name, cep->value);
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep->name, "reason"))
|
|
{
|
|
safe_strdup(dmod->reason, cep->value);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Just use a default reason if none was specified (since it's optional)
|
|
if (!dmod->reason || !strlen(dmod->reason))
|
|
safe_strdup(dmod->reason, "no reason");
|
|
AddListItem(dmod, DenyModList);
|
|
return 1;
|
|
}
|
|
|
|
int reqmods_configtest_require(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
|
{
|
|
int errors = 0;
|
|
ConfigEntry *cep;
|
|
int has_name, has_minversion;
|
|
|
|
// We are only interested in require module { }
|
|
if (strcmp(ce->value, "module"))
|
|
return 0;
|
|
|
|
has_name = has_minversion = 0;
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strlen(cep->name))
|
|
{
|
|
config_error("%s:%i: blank directive for require module { } block", cep->file->filename, cep->line_number);
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
if (!cep->value || !strlen(cep->value))
|
|
{
|
|
config_error("%s:%i: blank %s without value for require module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep->name, "name"))
|
|
{
|
|
if (has_name)
|
|
{
|
|
config_error("%s:%i: duplicate %s for require module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
continue;
|
|
}
|
|
|
|
if (!find_modptr_byname(cep->value, 0))
|
|
{
|
|
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);
|
|
errors++;
|
|
}
|
|
|
|
// Let's be nice and let configrun handle adding this module to the list
|
|
has_name = 1;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep->name, "min-version")) // Optional
|
|
{
|
|
// Still check for duplicate directives though
|
|
if (has_minversion)
|
|
{
|
|
config_error("%s:%i: duplicate %s for require module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
continue;
|
|
}
|
|
has_minversion = 1;
|
|
continue;
|
|
}
|
|
|
|
// Reason directive is not used for require module { }, so error on that too
|
|
config_error("%s:%i: unknown directive %s for require module { } block", cep->file->filename, cep->line_number, cep->name);
|
|
errors++;
|
|
}
|
|
|
|
if (!has_name)
|
|
{
|
|
config_error("%s:%i: missing required 'name' directive for require module { } block", ce->file->filename, ce->line_number);
|
|
errors++;
|
|
}
|
|
|
|
*errs = errors;
|
|
return errors ? -1 : 1;
|
|
}
|
|
|
|
int reqmods_configrun_require(ConfigFile *cf, ConfigEntry *ce, int type)
|
|
{
|
|
ConfigEntry *cep;
|
|
Module *mod;
|
|
ReqMod *rmod;
|
|
char *name, *minversion;
|
|
|
|
if (strcmp(ce->value, "module"))
|
|
return 0;
|
|
|
|
name = minversion = NULL;
|
|
for (cep = ce->items; cep; cep = cep->next)
|
|
{
|
|
if (!strcmp(cep->name, "name"))
|
|
{
|
|
if (!(mod = find_modptr_byname(cep->value, 0)))
|
|
{
|
|
// Something went very wrong :D
|
|
config_warn("[require-module] [BUG?] Passed configtest_require() but not configrun_require() for module '%s' (seems to not be loaded after all)", cep->value);
|
|
continue;
|
|
}
|
|
|
|
name = cep->value;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(cep->name, "min-version"))
|
|
{
|
|
minversion = cep->value;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// While technically an error, let's not kill the entire server over it
|
|
if (!name)
|
|
return 1;
|
|
|
|
rmod = safe_alloc(sizeof(ReqMod));
|
|
safe_strdup(rmod->name, name);
|
|
if (minversion)
|
|
safe_strdup(rmod->minversion, minversion);
|
|
AddListItem(rmod, ReqModList);
|
|
return 1;
|
|
}
|
|
|
|
CMD_FUNC(cmd_smod)
|
|
{
|
|
char modflag, name[64], *version;
|
|
char buf[BUFSIZE];
|
|
char *tmp, *p, *modbuf;
|
|
Module *mod;
|
|
DenyMod *dmod;
|
|
int i;
|
|
int abort;
|
|
|
|
// A non-server client shouldn't really be possible here, but still :D
|
|
if (!MyConnect(client) || !IsServer(client) || BadPtr(parv[1]))
|
|
return;
|
|
|
|
// Module strings are passed as 1 space-delimited parameter
|
|
strlcpy(buf, parv[1], sizeof(buf));
|
|
abort = 0;
|
|
for (modbuf = strtoken(&tmp, buf, " "); modbuf; modbuf = strtoken(&tmp, NULL, " "))
|
|
{
|
|
/* The order of checks is:
|
|
* 1: deny module { } -- SQUIT always
|
|
* 2 (if module not loaded): require module { } -- SQUIT always
|
|
* 3 (if module not loaded): warn, but only if MOD_OPT_GLOBAL
|
|
* 4 (optional, if module loaded only): require module::min-version
|
|
*/
|
|
p = strchr(modbuf, ':');
|
|
if (!p)
|
|
continue; /* malformed request */
|
|
modflag = *modbuf; // Get the module flag (FIXME: parses only first letter atm)
|
|
modbuf = p+1;
|
|
strlcpy(name, modbuf, sizeof(name)); // Let's work on a copy of the param
|
|
|
|
version = strchr(name, ':');
|
|
if (!version)
|
|
continue; /* malformed request */
|
|
*version++ = '\0';
|
|
|
|
// Even if a denied module is only required locally, let's still prevent a server that uses it from linking in
|
|
if ((dmod = find_denymod_byname(name)))
|
|
{
|
|
// Send this particular notice to local opers only
|
|
unreal_log(ULOG_ERROR, "link", "LINK_DENY_MODULE", client,
|
|
"Server $client is using module '$module_name', "
|
|
"which is specified in a deny module { } config block (reason: $ban_reason) -- aborting link",
|
|
log_data_string("module_name", name),
|
|
log_data_string("ban_reason", dmod->reason));
|
|
abort = 1; // Always SQUIT because it was explicitly denied by admins
|
|
continue;
|
|
}
|
|
|
|
// Doing a strict check for the module being fully loaded so we can emit an alert in that case too :>
|
|
mod = find_modptr_byname(name, 1);
|
|
if (!mod)
|
|
{
|
|
/* Since only the server missing the module will report it, we need to broadcast the warning network-wide ;]
|
|
* Obviously we won't take any real action if the module seems to be locally required only, except if it's marked as required
|
|
*/
|
|
if (modflag == 'R')
|
|
{
|
|
// 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
|
|
unreal_log(ULOG_ERROR, "link", "LINK_MISSING_REQUIRED_MODULE", client,
|
|
"Server $me is missing module '$module_name' which "
|
|
"is required by server $client. -- aborting link",
|
|
log_data_client("me", &me),
|
|
log_data_string("module_name", name));
|
|
abort = 1; // Always SQUIT here too (explicitly required by admins)
|
|
}
|
|
else if (modflag == 'G')
|
|
{
|
|
unreal_log(ULOG_WARNING, "link", "LINK_MISSING_GLOBAL_MODULE", client,
|
|
"Server $me is missing module '$module_name', which is "
|
|
"marked as global at $client",
|
|
log_data_client("me", &me),
|
|
log_data_string("module_name", name));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Further checks are only necessary for explicitly required mods
|
|
if (modflag != 'R')
|
|
continue;
|
|
|
|
// Module is loaded on both servers and the other end is require { }'ing a specific module version
|
|
// An explicit version was specified in require module { } but our module version is less than that
|
|
if (*version != '*' && strnatcasecmp(mod->header->version, version) < 0)
|
|
{
|
|
unreal_log(ULOG_ERROR, "link", "LINK_MODULE_OLD_VERSION", client,
|
|
"Server $me is using an old version of module '$module_name'. "
|
|
"Server $client requires us to have version $minimum_module_version or later (we have $our_module_version). "
|
|
"-- aborting link",
|
|
log_data_client("me", &me),
|
|
log_data_string("module_name", name),
|
|
log_data_string("minimum_module_version", version),
|
|
log_data_string("our_module_version", mod->header->version));
|
|
abort = 1;
|
|
}
|
|
}
|
|
|
|
if (abort)
|
|
{
|
|
exit_client_fmt(client, NULL, "Link aborted due to missing or banned modules (see previous errors)");
|
|
return;
|
|
}
|
|
}
|
|
|
|
int reqmods_hook_serverconnect(Client *client)
|
|
{
|
|
/* This function simply dumps a list of modules and their version to the other server,
|
|
* which will then run through the received list and check the names/versions
|
|
*/
|
|
char modflag;
|
|
char modbuf[64];
|
|
char *modversion;
|
|
/* Try to use a large buffer, but take into account the hostname, command, spaces, etc */
|
|
char sendbuf[BUFSIZE - HOSTLEN - 16];
|
|
Module *mod;
|
|
ReqMod *rmod;
|
|
size_t len, modlen;
|
|
|
|
/* Let's not have leaves directly connected to the hub send their module list to other *leaves* as well =]
|
|
* Since the hub will introduce all servers currently linked to it, this hook is actually called for every separate node
|
|
*/
|
|
if (!MyConnect(client))
|
|
return HOOK_CONTINUE;
|
|
|
|
sendbuf[0] = '\0';
|
|
len = 0;
|
|
|
|
/* 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
|
|
* certain ones across the network
|
|
* 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
|
|
*/
|
|
for (mod = Modules; mod; mod = mod->next)
|
|
{
|
|
modflag = SMOD_FLAG_LOCAL;
|
|
modversion = mod->header->version;
|
|
|
|
// require { }'d modules should be loaded on this server anyways, meaning we don't have to use a separate loop for those =]
|
|
if ((rmod = find_reqmod_byname(mod->header->name)))
|
|
{
|
|
// require module::min-version overrides the version found in the module's header
|
|
modflag = SMOD_FLAG_REQUIRED;
|
|
modversion = (rmod->minversion ? rmod->minversion : "*");
|
|
}
|
|
|
|
else if ((mod->options & MOD_OPT_GLOBAL))
|
|
modflag = SMOD_FLAG_GLOBAL;
|
|
|
|
ircsnprintf(modbuf, sizeof(modbuf), "%c:%s:%s", modflag, mod->header->name, modversion);
|
|
modlen = strlen(modbuf);
|
|
if (len + modlen + 2 > sizeof(sendbuf)) // Account for space and nullbyte, otherwise the last module entry might be cut off
|
|
{
|
|
// "Flush" current list =]
|
|
sendto_one(client, NULL, ":%s %s :%s", me.id, MSG_SMOD, sendbuf);
|
|
sendbuf[0] = '\0';
|
|
len = 0;
|
|
}
|
|
|
|
/* Maybe account for the space between modules, can't do this earlier because otherwise the ircsnprintf() would skip past the nullbyte
|
|
* of the previous module (which in turn terminates the string prematurely)
|
|
*/
|
|
ircsnprintf(sendbuf + len, sizeof(sendbuf) - len, "%s%s", (len > 0 ? " " : ""), modbuf);
|
|
if (len)
|
|
len++;
|
|
len += modlen;
|
|
}
|
|
|
|
// May have something left
|
|
if (sendbuf[0])
|
|
sendto_one(client, NULL, ":%s %s :%s", me.id, MSG_SMOD, sendbuf);
|
|
return HOOK_CONTINUE;
|
|
}
|