1
0
mirror of https://github.com/anope/anope.git synced 2026-06-25 08:56:39 +02:00
Files
anope/modules/operserv/os_akill.cpp
T
Sadie Powell f9911dde52 Return references instead of pointers from the config system.
We used to return NULL from these methods but now we return an empty
block so this can never actually be null now.
2025-03-02 15:27:47 +00:00

491 lines
13 KiB
C++

/* OperServ core functions
*
* (C) 2003-2025 Anope Team
* Contact us at team@anope.org
*
* Please read COPYING and README for further details.
*
* Based on the original code of Epona by Lara.
* Based on the original code of Services by Andy Church.
*/
#include "module.h"
static ServiceReference<XLineManager> akills("XLineManager", "xlinemanager/sgline");
class AkillDelCallback final
: public NumberList
{
CommandSource &source;
unsigned deleted = 0;
Command *cmd;
public:
AkillDelCallback(CommandSource &_source, const Anope::string &numlist, Command *c) : NumberList(numlist, true), source(_source), cmd(c)
{
}
~AkillDelCallback() override
{
if (!deleted)
source.Reply(_("No matching entries on the AKILL list."));
else if (deleted == 1)
source.Reply(_("Deleted 1 entry from the AKILL list."));
else
source.Reply(_("Deleted %d entries from the AKILL list."), deleted);
}
void HandleNumber(unsigned number) override
{
if (!number)
return;
XLine *x = akills->GetEntry(number - 1);
if (!x)
return;
Log(LOG_ADMIN, source, cmd) << "to remove " << x->mask << " from the list";
++deleted;
DoDel(source, x);
}
static void DoDel(CommandSource &source, XLine *x)
{
akills->DelXLine(x);
}
};
class CommandOSAKill final
: public Command
{
private:
void DoAdd(CommandSource &source, const std::vector<Anope::string> &params)
{
Anope::string expiry, mask;
if (params.size() < 2)
{
this->OnSyntaxError(source, "ADD");
return;
}
spacesepstream sep(params[1]);
sep.GetToken(mask);
if (mask[0] == '+')
{
expiry = mask;
sep.GetToken(mask);
}
time_t expires = !expiry.empty() ? Anope::DoTime(expiry) : Config->GetModule("operserv").Get<time_t>("autokillexpiry", "30d");
/* If the expiry given does not contain a final letter, it's in days,
* said the doc. Ah well.
*/
if (!expiry.empty() && isdigit(expiry[expiry.length() - 1]))
expires *= 86400;
/* Do not allow less than a minute expiry time */
if (expires && expires < 60)
{
source.Reply(BAD_EXPIRY_TIME);
return;
}
else if (expires > 0)
expires += Anope::CurTime;
if (sep.StreamEnd())
{
this->OnSyntaxError(source, "ADD");
return;
}
Anope::string reason;
if (mask.find('#') != Anope::string::npos)
{
Anope::string remaining = sep.GetRemaining();
size_t co = remaining[0] == ':' ? 0 : remaining.rfind(" :");
if (co == Anope::string::npos)
{
this->OnSyntaxError(source, "ADD");
return;
}
if (co != 0)
++co;
reason = remaining.substr(co + 1);
mask += " " + remaining.substr(0, co);
mask.trim();
}
else
reason = sep.GetRemaining();
if (mask[0] == '/' && mask[mask.length() - 1] == '/')
{
const Anope::string &regexengine = Config->GetBlock("options").Get<const Anope::string>("regexengine");
if (regexengine.empty())
{
source.Reply(_("Regex is disabled."));
return;
}
ServiceReference<RegexProvider> provider("Regex", regexengine);
if (!provider)
{
source.Reply(_("Unable to find regex engine %s."), regexengine.c_str());
return;
}
try
{
Anope::string stripped_mask = mask.substr(1, mask.length() - 2);
delete provider->Compile(stripped_mask);
}
catch (const RegexException &ex)
{
source.Reply("%s", ex.GetReason().c_str());
return;
}
}
User *targ = User::Find(mask, true);
if (targ)
mask = "*@" + targ->host;
if (Config->GetModule("operserv").Get<bool>("addakiller", "yes") && !source.GetNick().empty())
reason = "[" + source.GetNick() + "] " + reason;
if (mask.find_first_not_of("/~@.*?") == Anope::string::npos)
{
source.Reply(USERHOST_MASK_TOO_WIDE, mask.c_str());
return;
}
else if (mask.find('@') == Anope::string::npos)
{
source.Reply(BAD_USERHOST_MASK);
return;
}
auto *x = new XLine(mask, source.GetNick(), expires, reason);
if (Config->GetModule("operserv").Get<bool>("akillids"))
x->id = XLineManager::GenerateUID();
unsigned int affected = 0;
for (const auto &[_, user] : UserListByNick)
if (akills->Check(user, x))
++affected;
float percent = static_cast<float>(affected) / static_cast<float>(UserListByNick.size()) * 100.0;
if (percent > 95)
{
source.Reply(USERHOST_MASK_TOO_WIDE, mask.c_str());
Log(LOG_ADMIN, source, this) << "tried to akill " << percent << "% of the network (" << affected << " users)";
delete x;
return;
}
if (!akills->CanAdd(source, mask, expires, reason))
{
delete x;
return;
}
EventReturn MOD_RESULT;
FOREACH_RESULT(OnAddXLine, MOD_RESULT, (source, x, akills));
if (MOD_RESULT == EVENT_STOP)
{
delete x;
return;
}
akills->AddXLine(x);
if (Config->GetModule("operserv").Get<bool>("akillonadd"))
akills->Send(NULL, x);
source.Reply(_("\002%s\002 added to the AKILL list."), mask.c_str());
Log(LOG_ADMIN, source, this) << "on " << mask << " (" << x->reason << "), expires in " << (expires ? Anope::Duration(expires - Anope::CurTime) : "never") << " [affects " << affected << " user(s) (" << percent << "%)]";
if (Anope::ReadOnly)
source.Reply(READ_ONLY_MODE);
}
void DoDel(CommandSource &source, const std::vector<Anope::string> &params)
{
const Anope::string &mask = params.size() > 1 ? params[1] : "";
if (mask.empty())
{
this->OnSyntaxError(source, "DEL");
return;
}
if (akills->GetList().empty())
{
source.Reply(_("AKILL list is empty."));
return;
}
if (isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
{
AkillDelCallback list(source, mask, this);
list.Process();
}
else
{
XLine *x = akills->HasEntry(mask);
if (!x)
{
source.Reply(_("\002%s\002 not found on the AKILL list."), mask.c_str());
return;
}
do
{
FOREACH_MOD(OnDelXLine, (source, x, akills));
Log(LOG_ADMIN, source, this) << "to remove " << x->mask << " from the list";
source.Reply(_("\002%s\002 deleted from the AKILL list."), x->mask.c_str());
AkillDelCallback::DoDel(source, x);
}
while ((x = akills->HasEntry(mask)));
}
if (Anope::ReadOnly)
source.Reply(READ_ONLY_MODE);
return;
}
void ProcessList(CommandSource &source, const std::vector<Anope::string> &params, ListFormatter &list)
{
const Anope::string &mask = params.size() > 1 ? params[1] : "";
if (!mask.empty() && isdigit(mask[0]) && mask.find_first_not_of("1234567890,-") == Anope::string::npos)
{
class ListCallback final
: public NumberList
{
CommandSource &source;
ListFormatter &list;
public:
ListCallback(CommandSource &_source, ListFormatter &_list, const Anope::string &numstr) : NumberList(numstr, false), source(_source), list(_list)
{
}
void HandleNumber(unsigned number) override
{
if (!number)
return;
const XLine *x = akills->GetEntry(number - 1);
if (!x)
return;
ListFormatter::ListEntry entry;
entry["Number"] = Anope::ToString(number);
entry["Mask"] = x->mask;
entry["Creator"] = x->by;
entry["Created"] = Anope::strftime(x->created, NULL, true);
entry["Expires"] = Anope::Expires(x->expires, source.nc);
entry["ID"] = x->id;
entry["Reason"] = x->reason;
this->list.AddEntry(entry);
}
}
nl_list(source, list, mask);
nl_list.Process();
}
else
{
for (unsigned i = 0, end = akills->GetCount(); i < end; ++i)
{
const XLine *x = akills->GetEntry(i);
if (mask.empty() || mask.equals_ci(x->mask) || mask == x->id || Anope::Match(x->mask, mask, false, true))
{
ListFormatter::ListEntry entry;
entry["Number"] = Anope::ToString(i + 1);
entry["Mask"] = x->mask;
entry["Creator"] = x->by;
entry["Created"] = Anope::strftime(x->created, NULL, true);
entry["Expires"] = Anope::Expires(x->expires, source.nc);
entry["ID"] = x->id;
entry["Reason"] = x->reason;
list.AddEntry(entry);
}
}
}
if (list.IsEmpty())
source.Reply(_("No matching entries on the AKILL list."));
else
{
source.Reply(_("Current AKILL list:"));
std::vector<Anope::string> replies;
list.Process(replies);
for (const auto &reply : replies)
source.Reply(reply);
source.Reply(_("End of AKILL list."));
}
}
void DoList(CommandSource &source, const std::vector<Anope::string> &params)
{
if (akills->GetList().empty())
{
source.Reply(_("AKILL list is empty."));
return;
}
ListFormatter list(source.GetAccount());
list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Reason"));
this->ProcessList(source, params, list);
}
void DoView(CommandSource &source, const std::vector<Anope::string> &params)
{
if (akills->GetList().empty())
{
source.Reply(_("AKILL list is empty."));
return;
}
ListFormatter list(source.GetAccount());
list.AddColumn(_("Number")).AddColumn(_("Mask")).AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Expires"));
if (Config->GetModule("operserv").Get<bool>("akillids"))
list.AddColumn(_("ID"));
list.AddColumn(_("Reason"));
this->ProcessList(source, params, list);
}
void DoClear(CommandSource &source)
{
for (unsigned i = akills->GetCount(); i > 0; --i)
{
XLine *x = akills->GetEntry(i - 1);
FOREACH_MOD(OnDelXLine, (source, x, akills));
akills->DelXLine(x);
}
Log(LOG_ADMIN, source, this) << "to CLEAR the list";
source.Reply(_("The AKILL list has been cleared."));
if (Anope::ReadOnly)
source.Reply(READ_ONLY_MODE);
}
public:
CommandOSAKill(Module *creator) : Command(creator, "operserv/akill", 1, 2)
{
this->SetDesc(_("Manipulate the AKILL list"));
this->SetSyntax(_("ADD [+\037expiry\037] \037mask\037 \037reason\037"));
this->SetSyntax(_("DEL {\037mask\037 | \037entry-num\037 | \037list\037 | \037id\037}"));
this->SetSyntax(_("LIST [\037mask\037 | \037list\037 | \037id\037]"));
this->SetSyntax(_("VIEW [\037mask\037 | \037list\037 | \037id\037]"));
this->SetSyntax("CLEAR");
}
void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
{
const Anope::string &cmd = params[0];
if (!akills)
return;
if (cmd.equals_ci("ADD"))
return this->DoAdd(source, params);
else if (cmd.equals_ci("DEL"))
return this->DoDel(source, params);
else if (cmd.equals_ci("LIST"))
return this->DoList(source, params);
else if (cmd.equals_ci("VIEW"))
return this->DoView(source, params);
else if (cmd.equals_ci("CLEAR"))
return this->DoClear(source);
else
this->OnSyntaxError(source, "");
return;
}
bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
{
this->SendSyntax(source);
source.Reply(" ");
source.Reply(_("Allows Services Operators to manipulate the AKILL list. If\n"
"a user matching an AKILL mask attempts to connect, services\n"
"will issue a KILL for that user and, on supported server\n"
"types, will instruct all servers to add a ban for the mask\n"
"which the user matched.\n"
" \n"
"\002AKILL ADD\002 adds the given mask to the AKILL\n"
"list for the given reason, which \002must\002 be given.\n"
"Mask should be in the format of nick!user@host#real name,\n"
"though all that is required is user@host. If a real name is specified,\n"
"the reason must be prepended with a :.\n"
"\037expiry\037 is specified as an integer followed by one of \037d\037\n"
"(days), \037h\037 (hours), or \037m\037 (minutes). Combinations (such as\n"
"\0371h30m\037) are not permitted. If a unit specifier is not\n"
"included, the default is days (so \037+30\037 by itself means 30\n"
"days). To add an AKILL which does not expire, use \037+0\037. If the\n"
"usermask to be added starts with a \037+\037, an expiry time must\n"
"be given, even if it is the same as the default. The\n"
"current AKILL default expiry time can be found with the\n"
"\002STATS AKILL\002 command."));
const Anope::string &regexengine = Config->GetBlock("options").Get<const Anope::string>("regexengine");
if (!regexengine.empty())
{
source.Reply(" ");
source.Reply(_("Regex matches are also supported using the %s engine.\n"
"Enclose your mask in // if this is desired."), regexengine.c_str());
}
source.Reply(_(
" \n"
"The \002AKILL DEL\002 command removes the given mask from the\n"
"AKILL list if it is present. If a list of entry numbers is\n"
"given, those entries are deleted. (See the example for LIST\n"
"below.)\n"
" \n"
"The \002AKILL LIST\002 command displays the AKILL list.\n"
"If a wildcard mask is given, only those entries matching the\n"
"mask are displayed. If a list of entry numbers is given,\n"
"only those entries are shown; for example:\n"
" \002AKILL LIST 2-5,7-9\002\n"
" Lists AKILL entries numbered 2 through 5 and 7\n"
" through 9.\n"
" \n"
"\002AKILL VIEW\002 is a more verbose version of \002AKILL LIST\002, and\n"
"will show who added an AKILL, the date it was added, and when\n"
"it expires, as well as the user@host/ip mask and reason.\n"
" \n"
"\002AKILL CLEAR\002 clears all entries of the AKILL list."));
return true;
}
};
class OSAKill final
: public Module
{
CommandOSAKill commandosakill;
public:
OSAKill(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
commandosakill(this)
{
}
};
MODULE_INIT(OSAKill)