/* Autokill list module. * * IRC Services is copyright (c) 1996-2007 Andrew Church. * E-mail: * Parts written by Andrew Kempe and others. * This program is free but copyrighted software; see the file COPYING for * details. */ #include "services.h" #include "modules.h" #include "conffile.h" #include "commands.h" #include "language.h" #include "operserv.h" #define NEED_MAKE_REASON #include "maskdata.h" #include "akill.h" /*************************************************************************/ static Module *module; static Module *module_operserv; static int cb_send_akill = -1; static int cb_send_exclude = -1; static int cb_cancel_akill = -1; static int cb_cancel_exclude = -1; static char * AutokillDBName; static char * AutokillReason; static int ImmediatelySendAutokill; static time_t AutokillExpiry; static time_t AkillChanExpiry; static time_t OperMaxExpiry; static int WallOSAkill; static int WallAutokillExpire; int EnableExclude; /* not static because main.c/do_help() needs it */ static time_t ExcludeExpiry; static char * ExcludeReason; EXPORT_VAR(int,EnableExclude) static int db_opened = 0; static void do_akill(User *u); static void do_akillchan(User *u); static void do_exclude(User *u); static Command cmds[] = { {"AKILL", do_akill, is_services_oper, OPER_HELP_AKILL, -1,-1}, {"AKILLCHAN", do_akillchan, is_services_oper, OPER_HELP_AKILLCHAN, -1,-1}, {"EXCLUDE", do_exclude, is_services_oper, OPER_HELP_EXCLUDE, -1,-1}, { NULL } }; /*************************************************************************/ /************************** Internal functions ***************************/ /*************************************************************************/ /* Callback for saving data. */ static int do_save_data(void) { sync_akill_db(AutokillDBName); return 0; } /*************************************************************************/ /* Send an autokill to the uplink server. */ static void send_akill(const MaskData *akill) { char *username, *host; static int warned_exclude = 0; /* Don't send autokills if EnableExclude but no ircd support */ if (EnableExclude && !(protocol_features & PF_AKILL_EXCL)) { if (!warned_exclude) { wallops(s_OperServ, "Warning: Autokill exclusions are enabled," " but this IRC server does not support autokill" " exclusions; autokills will not be sent to servers."); module_log("EnableExclude on server type without exclusions--" "autokill sending disabled"); warned_exclude = 1; } return; } else { warned_exclude = 0; } username = sstrdup(akill->mask); host = strchr(username, '@'); if (!host) { /* Glurp... this oughtn't happen, but if it does, let's not * play with null pointers. Yell and bail out. */ wallops(NULL, "Missing @ in autokill: %s", akill->mask); module_log("BUG: (send_akill) Missing @ in mask: %s", akill->mask); return; } *host++ = 0; call_callback_5(module, cb_send_akill, username, host, akill->expires, akill->who, make_reason(AutokillReason, akill)); free(username); } /*************************************************************************/ /* Remove an autokill from the uplink server. */ static void cancel_akill(char *mask) { char *s = strchr(mask, '@'); if (s) { *s++ = 0; call_callback_2(module, cb_cancel_akill, mask, s); } else { module_log("BUG: (cancel_akill) Missing @ in mask: %s", mask); } } /*************************************************************************/ /* Send an autokill exclusion to the uplink server. */ static void send_exclude(const MaskData *exclude) { char *username, *host; username = sstrdup(exclude->mask); host = strchr(username, '@'); if (!host) { wallops(NULL, "Missing @ in autokill exclusion: %s", exclude->mask); module_log("BUG: (send_exclude) Missing @ in mask: %s", exclude->mask); return; } *host++ = 0; call_callback_5(module, cb_send_exclude, username, host, exclude->expires, exclude->who, make_reason(ExcludeReason, exclude)); free(username); } /*************************************************************************/ /* Remove an autokill exclusion from the uplink server. */ static void cancel_exclude(char *mask) { char *s = strchr(mask, '@'); if (s) { *s++ = 0; call_callback_2(module, cb_cancel_exclude, mask, s); } else { module_log("BUG: (cancel_exclude) Missing @ in mask: %s", mask); } } /*************************************************************************/ /************************** External functions ***************************/ /*************************************************************************/ /* Does the user match any autokills? Return 1 (and kill the user) if so, * else 0. */ static int check_akill(int ac, char **av) { const char *nick = av[0], *username = av[3], *host = av[4]; char buf[BUFSIZE]; MaskData *akill; if (noakill) return 0; snprintf(buf, sizeof(buf), "%s@%s", username, host); if ((akill = get_matching_maskdata(MD_AKILL, buf)) != NULL) { if (EnableExclude && get_matching_maskdata(MD_EXCLUSION, buf)) return 0; /* Don't use kill_user(); that's for people who have already * signed on. This is called before the User structure is * created. */ send_cmd(s_OperServ, "KILL %s :%s (%s)", nick, s_OperServ, make_reason(AutokillReason, akill)); send_akill(akill); time(&akill->lastused); put_maskdata(MD_AKILL, akill); return 1; } return 0; } /*************************************************************************/ /************************** AKILL list editing ***************************/ /*************************************************************************/ /* Note that all string parameters are assumed to be non-NULL; expiry must * be set to the time when the autokill should expire (0 for none). Mask * is converted to lowercase on return. */ EXPORT_FUNC(create_akill) void create_akill(char *mask, const char *reason, const char *who, time_t expiry) { MaskData *akill; strlower(mask); if (maskdata_count(MD_AKILL) >= MAX_MASKDATA) { module_log("Attempt to add autokill to full list!"); return; } akill = scalloc(1, sizeof(*akill)); akill->mask = sstrdup(mask); akill->reason = sstrdup(reason); akill->time = time(NULL); akill->expires = expiry; strscpy(akill->who, who, NICKMAX); akill = add_maskdata(MD_AKILL, akill); if (ImmediatelySendAutokill) send_akill(akill); } /*************************************************************************/ /*************************************************************************/ /* Handle an OperServ AKILL command. */ /*************************************************************************/ static int check_add_akill(User *u, uint8 type, char *mask, time_t *expiry_ptr); static void do_add_akill(User *u, uint8 type, MaskData *md); static void do_del_akill(User *u, uint8 type, MaskData *md); static MaskDataCmdInfo akill_cmd_info = { "AKILL", /* command name */ MD_AKILL, /* MaskData type */ &AutokillExpiry, /* default expiry time pointer */ OPER_AKILL_SYNTAX, OPER_AKILL_ADD_SYNTAX, OPER_AKILL_DEL_SYNTAX, OPER_TOO_MANY_AKILLS, OPER_AKILL_EXISTS, OPER_AKILL_ADDED, OPER_AKILL_NOT_FOUND, OPER_AKILL_REMOVED, OPER_AKILL_LIST_HEADER, OPER_AKILL_LIST_FORMAT, OPER_AKILL_VIEW_FORMAT, OPER_AKILL_VIEW_UNUSED_FORMAT, OPER_AKILL_COUNT, (void (*))strlower, /* function to mangle masks for add/delete */ check_add_akill, /* function to check validity of mask on add */ do_add_akill, /* function to call on mask addition */ do_del_akill, /* function to call on mask removal */ NULL, /* function to call for unknown commands */ }; /*************************************************************************/ static void do_akill(User *u) { if (is_services_admin(u) || !OperMaxExpiry) akill_cmd_info.def_expiry_ptr = &AutokillExpiry; else akill_cmd_info.def_expiry_ptr = &OperMaxExpiry; do_maskdata_cmd(&akill_cmd_info, u); } /*************************************************************************/ static int check_add_akill(User *u, uint8 type, char *mask, time_t *expiry_ptr) { char *s, *t; time_t len; #ifdef CLEAN_COMPILE type = type; #endif if (strchr(mask, '!')) { notice_lang(s_OperServ, u, OPER_AKILL_NO_NICK); notice_lang(s_OperServ, u, BAD_USERHOST_MASK); return 0; } s = strchr(mask, '@'); if (!s || s == mask || s[1] == 0) { notice_lang(s_OperServ, u, BAD_USERHOST_MASK); return 0; } /* Make sure mask is not too general. */ *s++ = 0; if (strchr(mask,'*') != NULL && mask[strspn(mask,"*?")] == 0 && ((t = strchr(mask,'?')) == NULL || strchr(t+1,'?') == NULL) ) { /* Username part matches anything; check host part */ if (strchr(s,'*') != NULL && s[strspn(s,"*?.")] == 0 && ((t = strchr(mask,'.')) == NULL || strchr(t+1,'.') == NULL) ) { /* Hostname mask matches anything or nearly anything, so * disallow mask. */ notice_lang(s_OperServ, u, OPER_AKILL_MASK_TOO_GENERAL); return 0; } } s[-1] = '@'; /* Replace "@" that we killed above */ /* Check expiration limit for non-servadmins. */ len = *expiry_ptr - time(NULL); if (OperMaxExpiry && !is_services_admin(u) && (!*expiry_ptr || len > OperMaxExpiry) ) { notice_lang(s_OperServ, u, OPER_AKILL_EXPIRY_LIMITED, maketime(u->ngi, OperMaxExpiry, MT_DUALUNIT)); return 0; } return 1; } /*************************************************************************/ static void do_add_akill(User *u, uint8 type, MaskData *md) { #ifdef CLEAN_COMPILE type = type; #endif if (WallOSAkill) { char buf[BUFSIZE]; expires_in_lang(buf, sizeof(buf), NULL, md->expires); wallops(s_OperServ, "%s added an AKILL for \2%s\2 (%s)", u->nick, md->mask, buf); } if (ImmediatelySendAutokill) send_akill(md); } /*************************************************************************/ static void do_del_akill(User *u, uint8 type, MaskData *md) { #ifdef CLEAN_COMPILE type = type; #endif cancel_akill(md->mask); } /*************************************************************************/ /*************************************************************************/ /* Handle an OperServ AKILLCHAN command. */ /*************************************************************************/ static void do_akillchan(User *u) { char *channel, *expiry_str, *reason, *s; int kill; /* kill users in the channel? */ int32 expiry; Channel *c; struct c_userlist *cu, *cu2; int count; int old_immed; /* saved value of ImmediatelySendAutokill */ kill = 0; expiry_str = NULL; s = strtok(NULL, " "); if (s && stricmp(s,"KILL") == 0) { kill = 1; s = strtok(NULL, " "); } if (s && *s == '+') { expiry_str = s+1; s = strtok(NULL, " "); } if (!s || *s != '#') { syntax_error(s_OperServ, u, "AKILLCHAN", OPER_AKILLCHAN_SYNTAX); return; } channel = s; reason = strtok_remaining(); if (!reason) { syntax_error(s_OperServ, u, "AKILLCHAN", OPER_AKILLCHAN_SYNTAX); return; } if (!(c = get_channel(channel))) { notice_lang(s_OperServ, u, CHAN_X_NOT_IN_USE, channel); return; } if (expiry_str) { expiry = dotime(expiry_str); } else { if (!is_services_admin(u) && OperMaxExpiry && (!AkillChanExpiry || OperMaxExpiry < AkillChanExpiry) ) { expiry = OperMaxExpiry; } else { expiry = AkillChanExpiry; } } if (expiry) expiry += time(NULL); if (WallOSAkill) wallops(s_OperServ, "%s used AKILLCHAN for \2%s\2", u->nick, c->name); count = 0; old_immed = ImmediatelySendAutokill; ImmediatelySendAutokill = 1; LIST_FOREACH_SAFE (cu, c->users, cu2) { char buf[BUFSIZE]; if (is_oper(cu->user)) continue; /* Killing the user before adding the autokill opens a small hole * in that the user may be able to reconnect before the new * autokill reaches the server, but under normal conditions the * actual chance of this is vanishingly small. On the other hand, * the chance of people complaining about "user not found" errors * from ircds that kill users on autokill if we do the autokill * first is significantly greater... */ /* But as long as you kill the user first, make sure you save the * hostname _before_ it gets freed, idiot. */ snprintf(buf, sizeof(buf), "*@%s", cu->user->host); if (kill) kill_user(s_OperServ, cu->user->nick, reason); if (!get_maskdata(MD_AKILL, buf)) create_akill(buf, reason, u->nick, expiry); count++; } ImmediatelySendAutokill = old_immed; if (count == 1) { notice_lang(s_OperServ, u, kill ? OPER_AKILLCHAN_KILLED_ONE : OPER_AKILLCHAN_AKILLED_ONE); } else { notice_lang(s_OperServ, u, kill ? OPER_AKILLCHAN_KILLED : OPER_AKILLCHAN_AKILLED, count); } } /*************************************************************************/ /*************************************************************************/ /* Handle an OperServ EXCLUDE command. */ /*************************************************************************/ static int check_add_exclude(User *u, uint8 type, char *mask, time_t *expiry_ptr); static void do_add_exclude(User *u, uint8 type, MaskData *md); static void do_del_exclude(User *u, uint8 type, MaskData *md); static MaskDataCmdInfo exclude_cmd_info = { "EXCLUDE", /* command name */ MD_EXCLUSION, /* MaskData type */ &ExcludeExpiry, /* default expiry time pointer */ OPER_EXCLUDE_SYNTAX, OPER_EXCLUDE_ADD_SYNTAX, OPER_EXCLUDE_DEL_SYNTAX, OPER_TOO_MANY_EXCLUDES, OPER_EXCLUDE_EXISTS, OPER_EXCLUDE_ADDED, OPER_EXCLUDE_NOT_FOUND, OPER_EXCLUDE_REMOVED, OPER_EXCLUDE_LIST_HEADER, OPER_EXCLUDE_LIST_FORMAT, OPER_EXCLUDE_VIEW_FORMAT, OPER_EXCLUDE_VIEW_UNUSED_FORMAT, OPER_EXCLUDE_COUNT, (void (*))strlower, /* function to mangle masks for add/delete */ check_add_exclude, /* function to check validity of mask on add */ do_add_exclude, /* function to call on mask addition */ do_del_exclude, /* function to call on mask removal */ NULL, /* function to call for unknown commands */ }; /*************************************************************************/ static void do_exclude(User *u) { if (is_services_admin(u) || !OperMaxExpiry) exclude_cmd_info.def_expiry_ptr = &AutokillExpiry; else exclude_cmd_info.def_expiry_ptr = &OperMaxExpiry; do_maskdata_cmd(&exclude_cmd_info, u); } /*************************************************************************/ static int check_add_exclude(User *u, uint8 type, char *mask, time_t *expiry_ptr) { char *s; #ifdef CLEAN_COMPILE type = type; #endif s = strchr(mask, '@'); if (!s || s == mask || s[1] == 0) { notice_lang(s_OperServ, u, BAD_USERHOST_MASK); return 0; } return 1; } /*************************************************************************/ static void do_add_exclude(User *u, uint8 type, MaskData *md) { #ifdef CLEAN_COMPILE type = type; #endif if (WallOSAkill) { char buf[BUFSIZE]; expires_in_lang(buf, sizeof(buf), NULL, md->expires); wallops(s_OperServ, "%s added an EXCLUDE for \2%s\2 (%s)", u->nick, md->mask, buf); } send_exclude(md); } /*************************************************************************/ static void do_del_exclude(User *u, uint8 type, MaskData *md) { #ifdef CLEAN_COMPILE type = type; #endif cancel_exclude(md->mask); } /*************************************************************************/ /******************************* Callbacks *******************************/ /*************************************************************************/ /* Callback on connection to uplink server. */ static int do_connect(void) { if (ImmediatelySendAutokill) { MaskData *akill; for (akill = first_maskdata(MD_AKILL); akill; akill = next_maskdata(MD_AKILL) ) { send_akill(akill); } } return 0; } /*************************************************************************/ /* Callback for autokill expiration. */ static int do_expire_maskdata(uint32 type, MaskData *md) { if (type == MD_AKILL) { if (WallAutokillExpire) wallops(s_OperServ, "AKILL on %s has expired", md->mask); cancel_akill(md->mask); return 1; } return 0; } /*************************************************************************/ /* Callback to display help text for HELP AKILL. */ static int do_help(User *u, const char *param) { if (stricmp(param, "AKILL") == 0) { notice_help(s_OperServ, u, OPER_HELP_AKILL); if (OperMaxExpiry) notice_help(s_OperServ, u, OPER_HELP_AKILL_OPERMAXEXPIRY, maketime(u->ngi, OperMaxExpiry, MT_DUALUNIT)); notice_help(s_OperServ, u, OPER_HELP_AKILL_END); return 1; } else if (stricmp(param, "AKILLCHAN") == 0) { notice_help(s_OperServ, u, OPER_HELP_AKILLCHAN, maketime(u->ngi, AkillChanExpiry, 0)); return 1; } return 0; } /*************************************************************************/ /***************************** Module stuff ******************************/ /*************************************************************************/ const int32 module_version = MODULE_VERSION_CODE; ConfigDirective module_config[] = { { "AkillChanExpiry", { { CD_TIME, 0, &AkillChanExpiry } } }, { "AutokillDB", { { CD_STRING, CF_DIRREQ, &AutokillDBName } } }, { "AutokillExpiry", { { CD_TIME, 0, &AutokillExpiry } } }, { "AutokillReason", { { CD_STRING, CF_DIRREQ, &AutokillReason } } }, { "EnableExclude", { { CD_SET, 0, &EnableExclude } } }, { "ExcludeExpiry", { { CD_TIME, 0, &ExcludeExpiry } } }, { "ExcludeReason", { { CD_STRING, 0, &ExcludeReason } } }, { "ImmediatelySendAutokill",{{CD_SET, 0, &ImmediatelySendAutokill } } }, { "OperMaxExpiry", { { CD_TIME, 0, &OperMaxExpiry } } }, { "WallAutokillExpire",{{ CD_SET, 0, &WallAutokillExpire } } }, { "WallOSAkill", { { CD_SET, 0, &WallOSAkill } } }, { NULL } }; /* Pointer to EXCLUDE command record (for EnableExclude) */ static Command *cmd_EXCLUDE; /*************************************************************************/ static int do_reconfigure(int after_configure) { if (after_configure) { /* After reconfiguration: handle value changes. */ if (EnableExclude && !ExcludeReason) { module_log("EXCLUDE enabled but ExcludeReason not set; disabling" " EXCLUDE"); EnableExclude = 0; } if (EnableExclude) cmd_EXCLUDE->name = "EXCLUDE"; else cmd_EXCLUDE->name = ""; } /* if (!after_configure) */ return 0; } /*************************************************************************/ int init_module(Module *module_) { module = module_; if (EnableExclude && !ExcludeReason) { module_log("EXCLUDE enabled but ExcludeReason not set"); return 0; } module_operserv = find_module("operserv/main"); if (!module_operserv) { module_log("Main OperServ module not loaded"); return 0; } use_module(module_operserv); if (!register_commands(module_operserv, cmds)) { module_log("Unable to register commands"); exit_module(0); return 0; } cmd_EXCLUDE = lookup_cmd(module_operserv, "EXCLUDE"); if (!cmd_EXCLUDE) { module_log("BUG: unable to find EXCLUDE command entry"); exit_module(0); return 0; } if (!EnableExclude) cmd_EXCLUDE->name = ""; cb_send_akill = register_callback(module, "send_akill"); cb_send_exclude = register_callback(module, "send_exclude"); cb_cancel_akill = register_callback(module, "cancel_akill"); cb_cancel_exclude = register_callback(module, "cancel_exclude"); if (cb_send_akill < 0 || cb_send_exclude < 0 || cb_cancel_akill < 0 || cb_cancel_exclude < 0 ) { module_log("Unable to register callbacks"); exit_module(0); return 0; } if (!add_callback(NULL, "reconfigure", do_reconfigure) || !add_callback(NULL, "connect", do_connect) || !add_callback(NULL, "user check", check_akill) || !add_callback(NULL, "save data", do_save_data) || !add_callback(module_operserv, "expire maskdata", do_expire_maskdata) || !add_callback(module_operserv, "HELP", do_help) ) { module_log("Unable to add callbacks"); exit_module(0); return 0; } open_akill_db(AutokillDBName); db_opened = 1; return 1; } /*************************************************************************/ int exit_module(int shutdown_unused) { #ifdef CLEAN_COMPILE shutdown_unused = shutdown_unused; #endif if (db_opened) close_akill_db(AutokillDBName); remove_callback(NULL, "save data", do_save_data); remove_callback(NULL, "user check", check_akill); remove_callback(NULL, "connect", do_connect); remove_callback(NULL, "reconfigure", do_reconfigure); unregister_callback(module, cb_cancel_exclude); unregister_callback(module, cb_cancel_akill); unregister_callback(module, cb_send_exclude); unregister_callback(module, cb_send_akill); if (module_operserv) { remove_callback(module_operserv, "HELP", do_help); remove_callback(module_operserv, "expire maskdata",do_expire_maskdata); unregister_commands(module_operserv, cmds); unuse_module(module_operserv); module_operserv = NULL; } cmd_EXCLUDE->name = "EXCLUDE"; return 1; } /*************************************************************************/