diff options
Diffstat (limited to 'modules/operserv/os_forbid.cpp')
-rw-r--r-- | modules/operserv/os_forbid.cpp | 600 |
1 files changed, 600 insertions, 0 deletions
diff --git a/modules/operserv/os_forbid.cpp b/modules/operserv/os_forbid.cpp new file mode 100644 index 000000000..032cf0da7 --- /dev/null +++ b/modules/operserv/os_forbid.cpp @@ -0,0 +1,600 @@ +/* OperServ core functions + * + * (C) 2003-2024 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" +#include "modules/os_forbid.h" + +static ServiceReference<NickServService> nickserv("NickServService", "NickServ"); + +struct ForbidDataImpl final + : ForbidData + , Serializable +{ + ForbidDataImpl() : Serializable("ForbidData") { } + void Serialize(Serialize::Data &data) const override; + static Serializable *Unserialize(Serializable *obj, Serialize::Data &data); +}; + +void ForbidDataImpl::Serialize(Serialize::Data &data) const +{ + data.Store("mask", this->mask); + data.Store("creator", this->creator); + data.Store("reason", this->reason); + data.Store("created", this->created); + data.Store("expires", this->expires); + data.Store("type", this->type); +} + +Serializable *ForbidDataImpl::Unserialize(Serializable *obj, Serialize::Data &data) +{ + if (!forbid_service) + return NULL; + + ForbidDataImpl *fb; + if (obj) + fb = anope_dynamic_static_cast<ForbidDataImpl *>(obj); + else + fb = new ForbidDataImpl(); + + data["mask"] >> fb->mask; + data["creator"] >> fb->creator; + data["reason"] >> fb->reason; + data["created"] >> fb->created; + data["expires"] >> fb->expires; + unsigned int t; + data["type"] >> t; + fb->type = static_cast<ForbidType>(t); + + if (t > FT_SIZE - 1) + return NULL; + + if (!obj) + forbid_service->AddForbid(fb); + return fb; +} + +class MyForbidService final + : public ForbidService +{ + Serialize::Checker<std::vector<ForbidData *>[FT_SIZE - 1]> forbid_data; + + inline std::vector<ForbidData *>& forbids(unsigned t) { return (*this->forbid_data)[t - 1]; } + + void Expire(ForbidData *fd, unsigned ft, size_t idx) + { + Anope::string typestr; + switch (ft) + { + case FT_NICK: + typestr = "nick"; + break; + case FT_CHAN: + typestr = "chan"; + break; + case FT_EMAIL: + typestr = "email"; + break; + case FT_REGISTER: + typestr = "register"; + break; + default: + typestr = "unknown"; + break; + } + + Log(LOG_NORMAL, "expire/forbid", Config->GetClient("OperServ")) << "Expiring forbid for " << fd->mask << " type " << typestr; + this->forbids(ft).erase(this->forbids(ft).begin() + idx); + delete fd; + } + +public: + MyForbidService(Module *m) : ForbidService(m), forbid_data("ForbidData") { } + + ~MyForbidService() override + { + for (const auto *forbid : GetForbids()) + delete forbid; + } + + void AddForbid(ForbidData *d) override + { + this->forbids(d->type).push_back(d); + } + + void RemoveForbid(ForbidData *d) override + { + std::vector<ForbidData *>::iterator it = std::find(this->forbids(d->type).begin(), this->forbids(d->type).end(), d); + if (it != this->forbids(d->type).end()) + this->forbids(d->type).erase(it); + delete d; + } + + ForbidData *CreateForbid() override + { + return new ForbidDataImpl(); + } + + ForbidData *FindForbid(const Anope::string &mask, ForbidType ftype) override + { + for (unsigned i = this->forbids(ftype).size(); i > 0; --i) + { + ForbidData *d = this->forbids(ftype)[i - 1]; + + if (!Anope::NoExpire && d->expires && Anope::CurTime >= d->expires) + { + Expire(d, ftype, i - 1); + continue; + } + + if (Anope::Match(mask, d->mask, false, true)) + return d; + } + return NULL; + } + + ForbidData *FindForbidExact(const Anope::string &mask, ForbidType ftype) override + { + for (unsigned i = this->forbids(ftype).size(); i > 0; --i) + { + ForbidData *d = this->forbids(ftype)[i - 1]; + + if (!Anope::NoExpire && d->expires && Anope::CurTime >= d->expires) + { + Expire(d, ftype, i - 1); + continue; + } + + if (d->mask.equals_ci(mask)) + return d; + } + return NULL; + } + + std::vector<ForbidData *> GetForbids() override + { + std::vector<ForbidData *> f; + for (unsigned j = FT_NICK; j < FT_SIZE; ++j) + for (unsigned i = this->forbids(j).size(); i > 0; --i) + { + ForbidData *d = this->forbids(j).at(i - 1); + + if (d->expires && !Anope::NoExpire && Anope::CurTime >= d->expires) + Expire(d, j, i - 1); + else + f.push_back(d); + } + + return f; + } +}; + +class CommandOSForbid final + : public Command +{ + ServiceReference<ForbidService> fs; +public: + CommandOSForbid(Module *creator) : Command(creator, "operserv/forbid", 1, 5), fs("ForbidService", "forbid") + { + this->SetDesc(_("Forbid usage of nicknames, channels, and emails")); + this->SetSyntax(_("ADD {NICK|CHAN|EMAIL|REGISTER} [+\037expiry\037] \037entry\037 \037reason\037")); + this->SetSyntax(_("DEL {NICK|CHAN|EMAIL|REGISTER} \037entry\037")); + this->SetSyntax("LIST [NICK|CHAN|EMAIL|REGISTER]"); + } + + void Execute(CommandSource &source, const std::vector<Anope::string> ¶ms) override + { + if (!this->fs) + return; + + const Anope::string &command = params[0]; + const Anope::string &subcommand = params.size() > 1 ? params[1] : ""; + + ForbidType ftype = FT_SIZE; + if (subcommand.equals_ci("NICK")) + ftype = FT_NICK; + else if (subcommand.equals_ci("CHAN")) + ftype = FT_CHAN; + else if (subcommand.equals_ci("EMAIL")) + ftype = FT_EMAIL; + else if (subcommand.equals_ci("REGISTER")) + ftype = FT_REGISTER; + + if (command.equals_ci("ADD") && params.size() > 3 && ftype != FT_SIZE) + { + const Anope::string &expiry = params[2][0] == '+' ? params[2] : ""; + const Anope::string &entry = !expiry.empty() ? params[3] : params[2]; + Anope::string reason; + if (expiry.empty()) + reason = params[3] + " "; + if (params.size() > 4) + reason += params[4]; + reason.trim(); + + if (entry.replace_all_cs("?*", "").empty()) + { + source.Reply(_("The mask must contain at least one non wildcard character.")); + return; + } + + time_t expiryt = 0; + + if (!expiry.empty()) + { + expiryt = Anope::DoTime(expiry); + if (expiryt < 0) + { + source.Reply(BAD_EXPIRY_TIME); + return; + } + else if (expiryt) + expiryt += Anope::CurTime; + } + + NickAlias *target = NickAlias::Find(entry); + if (target != NULL && Config->GetModule("nickserv")->Get<bool>("secureadmins", "yes") && target->nc->IsServicesOper()) + { + source.Reply(ACCESS_DENIED); + return; + } + + ForbidData *d = this->fs->FindForbidExact(entry, ftype); + bool created = false; + if (d == NULL) + { + d = new ForbidDataImpl(); + created = true; + } + + d->mask = entry; + d->creator = source.GetNick(); + d->reason = reason; + d->created = Anope::CurTime; + d->expires = expiryt; + d->type = ftype; + if (created) + this->fs->AddForbid(d); + + if (Anope::ReadOnly) + source.Reply(READ_ONLY_MODE); + + Log(LOG_ADMIN, source, this) << "to add a forbid on " << entry << " of type " << subcommand; + source.Reply(_("Added a forbid on %s of type %s to expire on %s."), entry.c_str(), subcommand.lower().c_str(), d->expires ? Anope::strftime(d->expires, source.GetAccount()).c_str() : "never"); + + /* apply forbid */ + switch (ftype) + { + case FT_NICK: + { + int na_matches = 0; + + for (const auto &[_, user] : UserListByNick) + module->OnUserNickChange(user, ""); + + for (nickalias_map::const_iterator it = NickAliasList->begin(), it_end = NickAliasList->end(); it != it_end;) + { + NickAlias *na = it->second; + ++it; + + d = this->fs->FindForbid(na->nick, FT_NICK); + if (d == NULL) + continue; + + ++na_matches; + + delete na; + } + + source.Reply(_("\002%d\002 nickname(s) dropped."), na_matches); + break; + } + case FT_CHAN: + { + int chan_matches = 0, ci_matches = 0; + + for (channel_map::const_iterator it = ChannelList.begin(), it_end = ChannelList.end(); it != it_end;) + { + Channel *c = it->second; + ++it; + + d = this->fs->FindForbid(c->name, FT_CHAN); + if (d == NULL) + continue; + + ServiceReference<ChanServService> chanserv("ChanServService", "ChanServ"); + BotInfo *OperServ = Config->GetClient("OperServ"); + if (IRCD->CanSQLineChannel && OperServ) + { + time_t inhabit = Config->GetModule("chanserv")->Get<time_t>("inhabit", "1m"); + XLine x(c->name, OperServ->nick, Anope::CurTime + inhabit, d->reason); + IRCD->SendSQLine(NULL, &x); + } + else if (chanserv) + { + chanserv->Hold(c); + } + + ++chan_matches; + + for (Channel::ChanUserList::const_iterator cit = c->users.begin(), cit_end = c->users.end(); cit != cit_end;) + { + User *u = cit->first; + ++cit; + + if (u->server == Me || u->HasMode("OPER")) + continue; + + reason = Anope::printf(Language::Translate(u, _("This channel has been forbidden: %s")), d->reason.c_str()); + + c->Kick(source.service, u, reason); + } + } + + for (registered_channel_map::const_iterator it = RegisteredChannelList->begin(); it != RegisteredChannelList->end();) + { + ChannelInfo *ci = it->second; + ++it; + + d = this->fs->FindForbid(ci->name, FT_CHAN); + if (d == NULL) + continue; + + ++ci_matches; + + delete ci; + } + + source.Reply(_("\002%d\002 channel(s) cleared, and \002%d\002 channel(s) dropped."), chan_matches, ci_matches); + + break; + } + default: + break; + } + + } + else if (command.equals_ci("DEL") && params.size() > 2 && ftype != FT_SIZE) + { + const Anope::string &entry = params[2]; + + ForbidData *d = this->fs->FindForbidExact(entry, ftype); + if (d != NULL) + { + if (Anope::ReadOnly) + source.Reply(READ_ONLY_MODE); + + Log(LOG_ADMIN, source, this) << "to remove forbid on " << d->mask << " of type " << subcommand; + source.Reply(_("%s deleted from the %s forbid list."), d->mask.c_str(), subcommand.c_str()); + this->fs->RemoveForbid(d); + } + else + source.Reply(_("Forbid on %s was not found."), entry.c_str()); + } + else if (command.equals_ci("LIST")) + { + const std::vector<ForbidData *> &forbids = this->fs->GetForbids(); + if (forbids.empty()) + source.Reply(_("Forbid list is empty.")); + else + { + ListFormatter list(source.GetAccount()); + list.AddColumn(_("Mask")).AddColumn(_("Type")).AddColumn(_("Creator")).AddColumn(_("Expires")).AddColumn(_("Reason")); + + size_t shown = 0; + for (auto *forbid : forbids) + { + if (ftype != FT_SIZE && ftype != forbid->type) + continue; + + Anope::string stype; + if (forbid->type == FT_NICK) + stype = "NICK"; + else if (forbid->type == FT_CHAN) + stype = "CHAN"; + else if (forbid->type == FT_EMAIL) + stype = "EMAIL"; + else if (forbid->type == FT_REGISTER) + stype = "REGISTER"; + else + continue; + + ListFormatter::ListEntry entry; + entry["Mask"] = forbid->mask; + entry["Type"] = stype; + entry["Creator"] = forbid->creator; + entry["Expires"] = forbid->expires ? Anope::strftime(forbid->expires, NULL, true).c_str() : Language::Translate(source.GetAccount(), _("Never")); + entry["Reason"] = forbid->reason; + list.AddEntry(entry); + ++shown; + } + + if (!shown) + { + source.Reply(_("There are no forbids of type %s."), subcommand.upper().c_str()); + } + else + { + source.Reply(_("Forbid list:")); + + std::vector<Anope::string> replies; + list.Process(replies); + + for (const auto &reply : replies) + source.Reply(reply); + + if (shown >= forbids.size()) + source.Reply(_("End of forbid list.")); + else + source.Reply(_("End of forbid list - %zu/%zu entries shown."), shown, forbids.size()); + } + } + } + else + this->OnSyntaxError(source, command); + + return; + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_("Forbid allows you to forbid usage of certain nicknames, channels,\n" + "and email addresses. Wildcards are accepted for all entries.")); + + const Anope::string ®exengine = 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 pattern in // if this is desired."), regexengine.c_str()); + } + + return true; + } +}; + +class OSForbid final + : public Module +{ + MyForbidService forbidService; + Serialize::Type forbiddata_type; + CommandOSForbid commandosforbid; + +public: + OSForbid(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR), + forbidService(this), forbiddata_type("ForbidData", ForbidDataImpl::Unserialize), commandosforbid(this) + { + + } + + void OnUserConnect(User *u, bool &exempt) override + { + if (u->Quitting() || exempt) + return; + + this->OnUserNickChange(u, ""); + } + + void OnUserNickChange(User *u, const Anope::string &) override + { + if (u->HasMode("OPER")) + return; + + ForbidData *d = this->forbidService.FindForbid(u->nick, FT_NICK); + if (d != NULL) + { + BotInfo *bi = Config->GetClient("NickServ"); + if (!bi) + bi = Config->GetClient("OperServ"); + if (bi) + u->SendMessage(bi, _("This nickname has been forbidden: %s"), d->reason.c_str()); + if (nickserv) + nickserv->Collide(u, NULL); + } + } + + EventReturn OnCheckKick(User *u, Channel *c, Anope::string &mask, Anope::string &reason) override + { + BotInfo *OperServ = Config->GetClient("OperServ"); + if (u->HasMode("OPER") || !OperServ) + return EVENT_CONTINUE; + + ForbidData *d = this->forbidService.FindForbid(c->name, FT_CHAN); + if (d != NULL) + { + ServiceReference<ChanServService> chanserv("ChanServService", "ChanServ"); + if (IRCD->CanSQLineChannel) + { + time_t inhabit = Config->GetModule("chanserv")->Get<time_t>("inhabit", "1m"); + XLine x(c->name, OperServ->nick, Anope::CurTime + inhabit, d->reason); + IRCD->SendSQLine(NULL, &x); + } + else if (chanserv) + { + chanserv->Hold(c); + } + + reason = Anope::printf(Language::Translate(u, _("This channel has been forbidden: %s")), d->reason.c_str()); + + return EVENT_STOP; + } + + return EVENT_CONTINUE; + } + + EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector<Anope::string> ¶ms) override + { + if (command->name == "nickserv/info" && !params.empty() && params[0][0] != '=') + { + ForbidData *d = this->forbidService.FindForbid(params[0], FT_NICK); + if (d != NULL) + { + if (source.IsOper()) + source.Reply(_("Nick \002%s\002 is forbidden by %s: %s"), params[0].c_str(), d->creator.c_str(), d->reason.c_str()); + else + source.Reply(_("Nick \002%s\002 is forbidden."), params[0].c_str()); + return EVENT_STOP; + } + } + else if (command->name == "chanserv/info" && params.size() > 0) + { + ForbidData *d = this->forbidService.FindForbid(params[0], FT_CHAN); + if (d != NULL) + { + if (source.IsOper()) + source.Reply(_("Channel \002%s\002 is forbidden by %s: %s"), params[0].c_str(), d->creator.c_str(), d->reason.c_str()); + else + source.Reply(_("Channel \002%s\002 is forbidden."), params[0].c_str()); + return EVENT_STOP; + } + } + else if (source.IsOper()) + return EVENT_CONTINUE; + else if (command->name == "nickserv/register" && params.size() > 1) + { + ForbidData *d = this->forbidService.FindForbid(source.GetNick(), FT_REGISTER); + if (d != NULL) + { + source.Reply(NICK_CANNOT_BE_REGISTERED, source.GetNick().c_str()); + return EVENT_STOP; + } + + d = this->forbidService.FindForbid(params[1], FT_EMAIL); + if (d != NULL) + { + source.Reply(_("Your email address is not allowed, choose a different one.")); + return EVENT_STOP; + } + } + else if (command->name == "nickserv/set/email" && params.size() > 0) + { + ForbidData *d = this->forbidService.FindForbid(params[0], FT_EMAIL); + if (d != NULL) + { + source.Reply(_("Your email address is not allowed, choose a different one.")); + return EVENT_STOP; + } + } + else if (command->name == "chanserv/register" && !params.empty()) + { + ForbidData *d = this->forbidService.FindForbid(params[0], FT_REGISTER); + if (d != NULL) + { + source.Reply(CHAN_X_INVALID, params[0].c_str()); + return EVENT_STOP; + } + } + + return EVENT_CONTINUE; + } +}; + +MODULE_INIT(OSForbid) |