diff --git a/src/modules/require-module.c b/src/modules/require-module.c index c0cb3d5dc..4d8abfda4 100644 --- a/src/modules/require-module.c +++ b/src/modules/require-module.c @@ -21,11 +21,14 @@ #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", - "Check for required modules across the network", + "5.0.1", + "Require/deny modules across the network", "UnrealIRCd Team", "unrealircd-5", }; @@ -37,9 +40,17 @@ struct _denymod { 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); @@ -50,22 +61,13 @@ 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); -int reqmods_configtest_set(ConfigFile *cf, ConfigEntry *ce, int type, int *errs); -int reqmods_configrun_set(ConfigFile *cf, ConfigEntry *ce, int type); - CMD_FUNC(cmd_smod); int reqmods_hook_serverconnect(Client *client); // Globals extern MODVAR Module *Modules; DenyMod *DenyModList = NULL; - -struct cfgstruct { - int squit_on_deny; - int squit_on_missing; - int squit_on_mismatch; -}; -static struct cfgstruct cfg; +ReqMod *ReqModList = NULL; MOD_TEST() { @@ -77,8 +79,6 @@ MOD_INIT() { MARK_AS_OFFICIAL_MODULE(modinfo); MARK_AS_GLOBAL_MODULE(modinfo); - memset(&cfg, 0, sizeof(cfg)); - cfg.squit_on_deny = 1; 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); @@ -97,16 +97,26 @@ MOD_LOAD() MOD_UNLOAD() { - DenyMod *dmod, *next; - for (dmod = DenyModList; dmod; dmod = next) + DenyMod *dmod, *dnext; + ReqMod *rmod, *rnext; + for (dmod = DenyModList; dmod; dmod = dnext) { - next = dmod->next; + 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; } @@ -137,6 +147,17 @@ DenyMod *find_denymod_byname(char *name) 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) @@ -145,9 +166,6 @@ int reqmods_configtest(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) if (type == CONFIG_REQUIRE) return reqmods_configtest_require(cf, ce, type, errs); - if (type == CONFIG_SET) - return reqmods_configtest_set(cf, ce, type, errs); - return 0; } @@ -159,9 +177,6 @@ int reqmods_configrun(ConfigFile *cf, ConfigEntry *ce, int type) if (type == CONFIG_REQUIRE) return reqmods_configrun_require(cf, ce, type); - if (type == CONFIG_SET) - return reqmods_configrun_set(cf, ce, type); - return 0; } @@ -169,13 +184,13 @@ int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs { int errors = 0; ConfigEntry *cep; - int has_name; + int has_name, has_reason; // We are only interested in deny module { } if (strcmp(ce->ce_vardata, "module")) return 0; - has_name = 0; + has_name = has_reason = 0; for (cep = ce->ce_entries; cep; cep = cep->ce_next) { if (!strlen(cep->ce_varname)) @@ -194,6 +209,12 @@ int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs if (!strcmp(cep->ce_varname, "name")) { + if (has_name) + { + config_error("%s:%i: duplicate %s for deny module { } block", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + continue; + } + // We do a loose check here because a module might not be fully loaded yet if (find_modptr_byname(cep->ce_vardata, 0)) { @@ -205,7 +226,17 @@ int reqmods_configtest_deny(ConfigFile *cf, ConfigEntry *ce, int type, int *errs } if (!strcmp(cep->ce_varname, "reason")) // Optional + { + // Still check for duplicate directives though + if (has_reason) + { + config_error("%s:%i: duplicate %s for deny module { } block", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + errors++; + continue; + } + has_reason = 1; continue; + } config_error("%s:%i: unknown directive %s for deny module { } block", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); errors++; @@ -256,13 +287,13 @@ int reqmods_configtest_require(ConfigFile *cf, ConfigEntry *ce, int type, int *e { int errors = 0; ConfigEntry *cep; - int has_name; + int has_name, has_minversion; // We are only interested in require module { } if (strcmp(ce->ce_vardata, "module")) return 0; - has_name = 0; + has_name = has_minversion = 0; for (cep = ce->ce_entries; cep; cep = cep->ce_next) { if (!strlen(cep->ce_varname)) @@ -281,17 +312,36 @@ int reqmods_configtest_require(ConfigFile *cf, ConfigEntry *ce, int type, int *e if (!strcmp(cep->ce_varname, "name")) { + if (has_name) + { + config_error("%s:%i: duplicate %s for require module { } block", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + continue; + } + if (!find_modptr_byname(cep->ce_vardata, 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->ce_vardata); errors++; } - // Let's be nice and let configrun handle the module flags + // Let's be nice and let configrun handle adding this module to the list has_name = 1; continue; } + if (!strcmp(cep->ce_varname, "min-version")) // Optional + { + // Still check for duplicate directives though + if (has_minversion) + { + config_error("%s:%i: duplicate %s for require module { } block", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); + 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->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); errors++; @@ -311,10 +361,14 @@ int reqmods_configrun_require(ConfigFile *cf, ConfigEntry *ce, int type) { ConfigEntry *cep; Module *mod; + ReqMod *rmod; + char *name, *minversion; if (strcmp(ce->ce_vardata, "module")) return 0; + rmod = safe_alloc(sizeof(ReqMod)); + name = minversion = NULL; for (cep = ce->ce_entries; cep; cep = cep->ce_next) { if (!strcmp(cep->ce_varname, "name")) @@ -322,91 +376,35 @@ int reqmods_configrun_require(ConfigFile *cf, ConfigEntry *ce, int type) if (!(mod = find_modptr_byname(cep->ce_vardata, 0))) { // Something went very wrong :D - config_error("[require-module] [BUG?] Passed configtest_require() but not configrun_require() for module '%s' (seems to not be loaded after all)", cep->ce_vardata); + config_warn("[require-module] [BUG?] Passed configtest_require() but not configrun_require() for module '%s' (seems to not be loaded after all)", cep->ce_vardata); continue; } - // Just add the global flag so we don't have to keep a separate list for required modules too =] - if (!(mod->options & MOD_OPT_GLOBAL)) - mod->options |= MOD_OPT_GLOBAL; + name = cep->ce_vardata; + continue; + } + + if (!strcmp(cep->ce_varname, "minversion")) + { + minversion = cep->ce_vardata; continue; } } - return 1; -} - -int reqmods_configtest_set(ConfigFile *cf, ConfigEntry *ce, int type, int *errs) -{ - int errors = 0; - ConfigEntry *cep; - - // We are only interested in set::require-module - if (strcmp(ce->ce_varname, "require-module")) - return 0; - - for (cep = ce->ce_entries; cep; cep = cep->ce_next) - { - if (!strlen(cep->ce_varname)) - { - config_error("%s:%i: blank set::require-module directive", cep->ce_fileptr->cf_filename, cep->ce_varlinenum); - errors++; - continue; - } - - if (!cep->ce_vardata || !strlen(cep->ce_vardata)) - { - config_error("%s:%i: blank set::require-module::%s without value", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); - errors++; - continue; - } - - if (!strcmp(cep->ce_varname, "squit-on-deny") || !strcmp(cep->ce_varname, "squit-on-missing") || !strcmp(cep->ce_varname, "squit-on-mismatch")) - continue; - - config_error("%s:%i: unknown directive set::require-module::%s", cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname); - errors++; - } - - *errs = errors; - return errors ? -1 : 1; -} - -int reqmods_configrun_set(ConfigFile *cf, ConfigEntry *ce, int type) -{ - ConfigEntry *cep; - - // We are only interested in set::require-module - if (strcmp(ce->ce_varname, "require-module")) - return 0; - - for (cep = ce->ce_entries; cep; cep = cep->ce_next) - { - if (!strcmp(cep->ce_varname, "squit-on-deny")) - { - cfg.squit_on_deny = config_checkval(cep->ce_vardata, CFG_YESNO); - continue; - } - - if (!strcmp(cep->ce_varname, "squit-on-missing")) - { - cfg.squit_on_missing = config_checkval(cep->ce_vardata, CFG_YESNO); - continue; - } - - if (!strcmp(cep->ce_varname, "squit-on-mismatch")) - { - cfg.squit_on_mismatch = config_checkval(cep->ce_vardata, CFG_YESNO); - continue; - } - } + // While technically an error, let's not kill the entire server over it + if (!name) + return 1; + safe_strdup(rmod->name, name); + if (minversion) + safe_strdup(rmod->minversion, minversion); + AddListItem(rmod, ReqModList); return 1; } CMD_FUNC(cmd_smod) { - char flag, name[64], *version; + char modflag, name[64], *version; char buf[BUFSIZE]; char *tmp, *p, *modbuf; Module *mod; @@ -423,10 +421,16 @@ CMD_FUNC(cmd_smod) 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 */ - flag = *modbuf; // Get the local/global flag (FIXME: parses only first letter atm) + 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 @@ -435,41 +439,45 @@ CMD_FUNC(cmd_smod) continue; /* malformed request */ *version++ = '\0'; - // Even if a denied module is only required locally, maybe still prevent a server that uses it from linking in + // 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 - sendto_umode_global(UMODE_OPER, "Server %s is using module '%s' which is specified in a deny module { } config block (reason: %s)", client->name, name, dmod->reason); - if (cfg.squit_on_deny) - abort = 1; + sendto_umode_global(UMODE_OPER, "Server %s is using module '%s', which is specified in a deny module { } config block (reason: %s)", client->name, name, 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 :> - if (!(mod = find_modptr_byname(name, 1))) + 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 action if the module seems to be locally required only + * 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 (flag == 'G') + if (modflag == 'R') { - sendto_umode_global(UMODE_OPER, "Globally required module '%s' wasn't (fully) loaded or is missing entirely", name); - if (cfg.squit_on_missing) - abort = 1; + // 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 + sendto_umode_global(UMODE_OPER, "Required module wasn't (fully) loaded or is missing entirely: %s", name); + abort = 1; // Always SQUIT here too (explicitly required by admins) } + + else if (modflag == 'G') + sendto_umode_global(UMODE_OPER, "[WARN] Module marked as global wasn't (fully) loaded or is missing entirely: %s", name); + continue; } - /* A strcasecmp() suffices because the version string only has to *start* with a digit, it can have e.g. "-alpha" at the end - * We also check the module version for locally required modules (for completeness) - */ - if (!version || strcasecmp(mod->header->version, version)) - { - // Version mismatches can be (and are) reported on both ends separately, so a local server notice is enough - sendto_umode(UMODE_OPER, "Version mismatch for module '%s' (ours: %s, theirs: %s)", name, mod->header->version, version); - if (cfg.squit_on_mismatch) - abort = 1; + // 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) + { + sendto_umode_global(UMODE_OPER, "Module version mismatch for required module '%s' (should be equal to or greater than %s but we're running %s)", name, version, mod->header->version); + abort = 1; } } @@ -486,10 +494,13 @@ 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 =] @@ -501,14 +512,29 @@ int reqmods_hook_serverconnect(Client *client) 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) { - /* At this stage we don't care if the module isn't global (or not fully loaded), we'll dump all modules - * so we can properly deny certain ones across the network - */ - ircsnprintf(modbuf, sizeof(modbuf), "%c:%s:%s", ((mod->options & MOD_OPT_GLOBAL) ? 'G' : 'L'), mod->header->name, mod->header->version); + 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 string might be cut off + 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); @@ -516,11 +542,10 @@ int reqmods_hook_serverconnect(Client *client) len = 0; } - ircsnprintf(sendbuf + len, sizeof(sendbuf) - len, "%s%s", (len > 0 ? " " : ""), modbuf); - /* 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;