1
0
mirror of https://github.com/anope/anope.git synced 2026-06-25 04:56:39 +02:00
Files
anope/modules/nickserv/nickserv.cpp
T
Sadie Powell aef1925b1d Fix finding gettext on Windows.
Unfortunately we need to move our escape character from 0x1A to 0x1B
because newer versions of gettext treat the former as an EOF character.
2026-02-08 00:23:41 +00:00

696 lines
18 KiB
C++

// Anope IRC Services <https://www.anope.org/>
//
// Copyright (C) 2003-2026 Anope Contributors
//
// Anope is free software. You can use, modify, and/or distribute it under the
// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt
// for the complete terms of this license and docs/AUTHORS.txt for a list of
// contributors.
//
// Based on the original code of Epona by Lara
// Based on the original code of Services by Andy Church
//
// SPDX-License-Identifier: GPL-2.0-only
#include "module.h"
#include "modules/nickserv/service.h"
#define MORE_OBSCURE_PASSWORD _("Please try again with a more obscure password. " \
"Passwords should not be something that could be easily guessed (e.g. your " \
"real name or your nick) and cannot contain a space character.")
namespace
{
Anope::string enforcer_user, enforcer_host, enforcer_real;
}
class NickServCollide;
static std::set<NickServCollide *> collides;
/** Timer for colliding nicks to force people off of nicknames
*/
class NickServCollide final
: public Timer
{
Reference<User> u;
time_t ts;
Reference<NickAlias> na;
public:
NickServCollide(Module *me, User *user, NickAlias *nick, time_t delay)
: Timer(me, delay)
, u(user)
, ts(user->timestamp)
, na(nick)
{
collides.insert(this);
}
~NickServCollide() override
{
collides.erase(this);
}
User *GetUser()
{
return u;
}
NickAlias *GetNick()
{
return na;
}
void Tick() override
{
if (!u || !na || !NickServ::service)
return;
/* If they identified or don't exist anymore, don't kill them. */
if (u->Account() == na->nc || u->timestamp > ts)
return;
NickServ::service->Collide(u, na);
}
};
/** Timer for removing HELD status from nicks.
*/
class NickServHeld final
: public Timer
{
Reference<NickAlias> na;
Anope::string nick;
public:
NickServHeld(Module *me, NickAlias *n, time_t l)
: Timer(me, l)
, na(n)
, nick(na->nick)
{
n->Extend<bool>("HELD");
}
void Tick() override
{
if (na)
na->Shrink<bool>("HELD");
}
};
class NickServRelease;
static Anope::map<NickServRelease *> NickServReleases;
/** Timer for releasing nicks to be available for use
*/
class NickServRelease final
: public User
, public Timer
{
Anope::string nick;
public:
NickServRelease(Module *me, NickAlias *na, time_t delay)
: User(na->nick, enforcer_user, enforcer_host, "", "", Me, enforcer_real, Anope::CurTime, "", {}, IRCD->UID_Retrieve(), nullptr)
, Timer(me, delay)
, nick(na->nick)
{
/* Erase the current release timer and use the new one */
Anope::map<NickServRelease *>::iterator nit = NickServReleases.find(this->nick);
if (nit != NickServReleases.end())
{
IRCD->SendQuit(nit->second);
delete nit->second;
}
NickServReleases.emplace(this->nick, this);
IRCD->SendClientIntroduction(this);
}
~NickServRelease() override
{
IRCD->SendQuit(this);
NickServReleases.erase(this->nick);
}
void Tick() override
{
}
};
class NickServCore final
: public Module
, public NickServ::Service
{
Reference<BotInfo> NickServ;
std::vector<Anope::string> defaults;
ExtensibleItem<bool> held, collided;
void OnCancel(User *u, NickAlias *na)
{
if (collided.HasExt(na))
{
collided.Unset(na);
new NickServHeld(this, na, Config->GetModule(this).Get<time_t>("releasetimeout", "1m"));
if (IRCD->CanSVSHold)
IRCD->SendSVSHold(na->nick, Config->GetModule(this).Get<time_t>("releasetimeout", "1m"));
else
new NickServRelease(this, na, Config->GetModule(this).Get<time_t>("releasetimeout", "1m"));
}
}
public:
NickServCore(const Anope::string &modname, const Anope::string &creator)
: Module(modname, creator, PSEUDOCLIENT | VENDOR)
, NickServ::Service(this)
, held(this, "HELD")
, collided(this, "COLLIDED")
{
}
~NickServCore() override
{
OnShutdown();
}
void OnShutdown() override
{
/* On shutdown, restart, or mod unload, remove all of our holds for nicks (svshold or qlines)
* because some IRCds do not allow us to have these automatically expire
*/
for (const auto &[_, na] : *NickAliasList)
this->Release(na);
}
void OnRestart() override
{
OnShutdown();
}
bool IsGuestNick(const Anope::string &nick) const override
{
const auto guestnick = Config->GetModule(this).Get<Anope::string>("guestnick", "Guest####");
if (guestnick.empty())
return false; // No guest nick.
const auto minlen = std::min(nick.length(), guestnick.length());
for (size_t idx = 0; idx < minlen; ++idx)
{
if (guestnick[idx] == '#' && !isdigit(nick[idx]))
return false;
if (Anope::tolower(guestnick[idx]) != Anope::tolower(nick[idx]))
return false;
}
return true;
}
void Validate(User *u) override
{
NickAlias *na = NickAlias::Find(u->nick);
if (!na)
return;
EventReturn MOD_RESULT;
FOREACH_RESULT(OnNickValidate, MOD_RESULT, (u, na));
if (MOD_RESULT == EVENT_STOP)
{
this->Collide(u, na);
return;
}
else if (MOD_RESULT == EVENT_ALLOW)
return;
if (Config->GetModule("nickserv").Get<bool>("nonicknameownership"))
return;
if (na->nc->HasExt("PROTECT"))
{
auto &block = Config->GetModule(this);
auto protectafter = na->nc->GetExt<time_t>("PROTECT_AFTER");
auto protect = protectafter ? *protectafter : block.Get<time_t>("defaultprotect", "1m");
protect = std::clamp(protect, block.Get<time_t>("minprotect", "10s"), block.Get<time_t>("maxprotect", "10m"));
u->SendMessage(NickServ, _(
"This nickname is registered and has protection enabled. If it belongs to you, "
"type \002%s\033\037password\037\002 to identify to your account."
),
NickServ->GetQueryCommand("nickserv/identify", u->nick).c_str()
);
if (protect)
{
u->SendMessage(NickServ, _("Your nickname will be changed in %s if you do not identify."),
Anope::Duration(protect, u->Account()).c_str());
new NickServCollide(this, u, na, protect);
}
else
{
this->Collide(u, na);
}
}
}
void OnUserLogin(User *u) override
{
NickAlias *na = NickAlias::Find(u->nick);
if (na && *na->nc == u->Account() && !Config->GetModule("nickserv").Get<bool>("nonicknameownership") && !na->nc->HasExt("UNCONFIRMED"))
u->SetMode(NickServ, "REGISTERED");
const Anope::string &modesonid = Config->GetModule(this).Get<Anope::string>("modesonid");
if (!modesonid.empty())
u->SetModes(NickServ, modesonid);
}
void Collide(User *u, NickAlias *na) override
{
if (na)
collided.Set(na);
if (IRCD->CanSVSNick)
{
auto guestnickok = false;
Anope::string guestnick;
for (auto i = 0; i < 10; ++i)
{
guestnick.clear();
for (auto guestnickchr : Config->GetModule(this).Get<Anope::string>("guestnick", "Guest####").substr(0, IRCD->MaxNick))
{
if (guestnickchr == '#')
guestnick.append(Anope::ToString(abs(Anope::RandomNumber()) % 10));
else
guestnick.push_back(guestnickchr);
}
// A guest nick is valid if it is non-empty and is not in use.
if (!guestnick.empty() && !User::Find(guestnick, true))
{
guestnickok = true;
break;
}
}
// If we can't find a guest nick and the IRCd supports
// uids then we should use that as the backup guest
// nickname.
if (!guestnickok && IRCD->RequiresID)
{
guestnickok = true;
guestnick = u->GetUID();
}
if (guestnickok)
{
u->SendMessage(*NickServ, _("Your nickname is now being changed to \002%s\002"), guestnick.c_str());
IRCD->SendForceNickChange(u, guestnick, Anope::CurTime);
return;
}
}
// We can't change the user's nickname or we can't find an
// acceptable guest nick, give them the boot.
u->Kill(*NickServ, "Enforcement of services protected nickname");
}
void Release(NickAlias *na) override
{
if (held.HasExt(na))
{
if (IRCD->CanSVSHold)
IRCD->SendSVSHoldDel(na->nick);
else
{
User *u = User::Find(na->nick);
if (u && u->server == Me)
{
u->Quit();
}
}
held.Unset(na);
}
collided.Unset(na); /* clear pending collide */
}
void OnReload(Configuration::Conf &conf) override
{
const auto &modconf = conf.GetModule(this);
const Anope::string &nsnick = modconf.Get<const Anope::string>("client");
if (nsnick.empty())
throw ConfigException(Module::name + ": <client> must be defined");
BotInfo *bi = BotInfo::Find(nsnick, true);
if (!bi)
throw ConfigException(Module::name + ": no bot named " + nsnick);
NickServ = bi;
spacesepstream(modconf.Get<const Anope::string>("defaults", "memo_signon memo_receive")).GetTokens(defaults);
if (defaults.empty())
{
defaults.emplace_back("MEMO_SIGNON");
defaults.emplace_back("MEMO_RECEIVE");
}
else if (defaults[0].equals_ci("none"))
defaults.clear();
enforcer_user = modconf.Get<const Anope::string>("enforceruser", "enforcer");
enforcer_host = modconf.Get<const Anope::string>("enforcerhost", Me->GetName());
enforcer_real = modconf.Get<const Anope::string>("enforcerreal", "Services Enforcer");
}
void OnDelNick(NickAlias *na) override
{
User *u = User::Find(na->nick);
if (u && u->Account() == na->nc)
{
IRCD->SendLogout(u);
u->RemoveMode(NickServ, "REGISTERED");
u->Logout();
}
}
void OnDelCore(NickCore *nc) override
{
Log(NickServ, "nick") << "Deleting account " << nc->display;
/* Clean up this nick core from any users online */
for (std::list<User *>::iterator it = nc->users.begin(); it != nc->users.end();)
{
User *user = *it++;
IRCD->SendLogout(user);
user->RemoveMode(NickServ, "REGISTERED");
user->Logout();
FOREACH_MOD(OnNickLogout, (user));
}
nc->users.clear();
}
void OnChangeCoreDisplay(NickCore *nc, const Anope::string &newdisplay) override
{
Log(LOG_NORMAL, "nick", NickServ) << "Changing " << nc->display << " account display nickname to " << newdisplay;
}
void OnNickIdentify(User *u) override
{
const auto &block = Config->GetModule(this);
if (block.Get<bool>("modeonid", "yes"))
{
for (const auto &[_, cc] : u->chans)
{
Channel *c = cc->chan;
if (c)
c->SetCorrectModes(u, true);
}
}
const Anope::string &modesonid = block.Get<const Anope::string>("modesonid");
if (!modesonid.empty())
u->SetModes(NickServ, modesonid);
if (block.Get<bool>("forceemail", "yes") && u->Account()->email.empty())
{
u->SendMessage(NickServ, _(
"You must now supply an email address for your nick. This email address will "
"allow you to recover your account in case you forget your password. Type "
"\002%s\033\037email\037\002 in order to set your email address."
),
NickServ->GetQueryCommand("nickserv/set/email").c_str());
}
for (auto *c : collides)
{
if (c->GetUser() == u && c->GetNick() && c->GetNick()->nc == u->Account())
{
delete c;
break;
}
}
}
void OnNickGroup(User *u, NickAlias *target) override
{
if (!target->nc->HasExt("UNCONFIRMED"))
u->SetMode(NickServ, "REGISTERED");
}
void OnNickUpdate(User *u) override
{
for (const auto &[_, cc] : u->chans)
{
Channel *c = cc->chan;
if (c)
c->SetCorrectModes(u, true);
}
}
void OnUserConnect(User *u, bool &exempt) override
{
if (u->Quitting() || !u->server->IsSynced() || u->server->IsULined())
return;
const NickAlias *na = NickAlias::Find(u->nick);
const Anope::string &unregistered_notice = Config->GetModule(this).Get<const Anope::string>("unregistered_notice");
if (!Config->GetModule("nickserv").Get<bool>("nonicknameownership") && !unregistered_notice.empty() && !na && !u->IsIdentified())
{
auto msg = Anope::Template(unregistered_notice, {
{ "nick", u->nick },
});
u->SendMessage(NickServ, msg);
}
else if (na && !u->IsIdentified(true))
this->Validate(u);
}
void OnPostUserLogoff(User *u) override
{
NickAlias *na = NickAlias::Find(u->nick);
if (na)
OnCancel(u, na);
}
void OnServerSync(Server *s) override
{
for (const auto &[_, u] : UserListByNick)
{
if (u->server == s)
{
if (u->HasMode("REGISTERED") && !u->IsIdentified(true))
u->RemoveMode(NickServ, "REGISTERED");
if (!u->IsIdentified())
this->Validate(u);
}
}
}
void OnUserNickChange(User *u, const Anope::string &oldnick) override
{
NickAlias *old_na = NickAlias::Find(oldnick), *na = NickAlias::Find(u->nick);
/* If the new nick isn't registered or it's registered and not yours */
if (!na || na->nc != u->Account())
{
/* Remove +r, but keep an account associated with the user */
u->RemoveMode(NickServ, "REGISTERED");
this->Validate(u);
}
else
{
/* Reset +r and re-send account (even though it really should be set at this point) */
IRCD->SendLogin(u, na);
if (!Config->GetModule("nickserv").Get<bool>("nonicknameownership") && na->nc == u->Account() && !na->nc->HasExt("UNCONFIRMED"))
u->SetMode(NickServ, "REGISTERED");
Log(u, "", NickServ) << u->GetMask() << " automatically identified for account " << u->Account()->display;
}
if (!u->nick.equals_ci(oldnick) && old_na)
OnCancel(u, old_na);
}
void OnUserModeSet(const MessageSource &setter, User *u, const Anope::string &mname) override
{
if (u->server->IsSynced() && mname == "REGISTERED" && !u->IsIdentified(true))
u->RemoveMode(NickServ, mname);
}
EventReturn OnPreHelp(CommandSource &source, const std::vector<Anope::string> &params) override
{
if (!params.empty() || source.c || source.service != *NickServ)
return EVENT_CONTINUE;
if (!Config->GetModule("nickserv").Get<bool>("nonicknameownership"))
{
source.Reply(_(
"\002%s\002 allows you to register a nickname and "
"prevent others from using it. The following "
"commands allow for registration and maintenance of "
"nicknames; to use them, type \002%s\033\037command\037\002. "
"For more information on a specific command, type "
"\002%s\033\037command\037\002."
),
NickServ->nick.c_str(),
NickServ->GetQueryCommand().c_str(),
NickServ->GetQueryCommand({}, source.command).c_str());
}
else
{
source.Reply(_(
"\002%s\002 allows you to register an account. "
"The following commands allow for registration and maintenance of "
"accounts; to use them, type \002%s\033\037command\037\002. "
"For more information on a specific command, type "
"\002%s\033\037command\037\002."
),
NickServ->nick.c_str(),
NickServ->GetQueryCommand().c_str(),
NickServ->GetQueryCommand({}, source.command).c_str());
}
return EVENT_CONTINUE;
}
void OnPostHelp(CommandSource &source, const std::vector<Anope::string> &params) override
{
if (!params.empty() || source.c || source.service != *NickServ)
return;
if (source.IsServicesOper())
{
source.Reply(" ");
source.Reply(_(
"Services Operators can also drop any nickname without needing "
"to identify for the nick, and may view the access list for "
"any nickname."
));
}
time_t nickserv_expire = Config->GetModule(this).Get<time_t>("expire", "1y");
if (nickserv_expire)
{
source.Reply(" ");
source.Reply(_(
"Accounts that are not used anymore are subject to "
"the automatic expiration, i.e. they will be deleted "
"after %s if not used."
),
Anope::Duration(nickserv_expire, source.nc).c_str());
}
}
void OnNickCoreCreate(NickCore *nc) override
{
/* Set default flags */
for (const auto &def : defaults)
nc->Extend<bool>(def.upper());
}
void OnUserQuit(User *u, const Anope::string &msg) override
{
if (u->server && !u->server->GetQuitReason().empty() && Config->GetModule(this).Get<bool>("hidenetsplitquit"))
return;
/* Update last quit and last seen for the user */
NickAlias *na = NickAlias::Find(u->nick);
if (na && !na->nc->HasExt("NS_SUSPENDED") && u->IsIdentified(true))
{
na->last_seen = Anope::CurTime;
na->last_quit = msg;
}
}
void OnExpireTick() override
{
if (Anope::NoExpire || Anope::ReadOnly)
return;
time_t nickserv_expire = Config->GetModule(this).Get<time_t>("expire", "1y");
for (nickalias_map::const_iterator it = NickAliasList->begin(), it_end = NickAliasList->end(); it != it_end; )
{
NickAlias *na = it->second;
++it;
User *u = User::Find(na->nick, true);
if (u && u->IsIdentified(true))
na->last_seen = Anope::CurTime;
bool expire = false;
if (nickserv_expire && Anope::CurTime - na->last_seen >= nickserv_expire)
expire = true;
if (na->nc->na == na && na->nc->aliases->size() > 1 && Config->GetModule("nickserv").Get<bool>("preservedisplay"))
expire = false;
FOREACH_MOD(OnPreNickExpire, (na, expire));
if (expire)
{
Log(LOG_NORMAL, "nickserv/expire", NickServ) << "Expiring nickname " << na->nick << " (account: " << na->nc->display << ") (email: " << (na->nc->email.empty() ? "none" : na->nc->email) << ")";
FOREACH_MOD(OnNickExpire, (na));
delete na;
}
}
}
void OnNickInfo(CommandSource &source, NickAlias *na, InfoFormatter &info, bool show_hidden) override
{
if (!na->nc->HasExt("UNCONFIRMED"))
{
time_t nickserv_expire = Config->GetModule(this).Get<time_t>("expire", "1y");
if (!na->HasExt("NS_NO_EXPIRE") && nickserv_expire && !Anope::NoExpire && (source.HasPriv("nickserv/auspex") || na->last_seen != Anope::CurTime))
info[_("Expires")] = Anope::strftime(na->last_seen + nickserv_expire, source.GetAccount());
}
else
{
time_t unconfirmed_expire = Config->GetModule("ns_register").Get<time_t>("unconfirmedexpire", "1d");
info[_("Expires")] = Anope::strftime(na->registered + unconfirmed_expire, source.GetAccount());
}
}
EventReturn OnPasswordValidate(CommandSource &source, NickCore *nc, const Anope::string &pass) override
{
if (pass.equals_ci(source.GetNick()))
{
source.Reply(MORE_OBSCURE_PASSWORD);
return EVENT_STOP;
}
const auto *u = source.GetUser();
if (u && (u->GetIdent().equals_ci(pass) || u->realname.equals_ci(pass)))
{
source.Reply(MORE_OBSCURE_PASSWORD);
return EVENT_STOP;
}
if (nc && nc->display.equals_ci(pass))
{
source.Reply(MORE_OBSCURE_PASSWORD);
return EVENT_STOP;
}
const auto minpasslen = Config->GetModule("nickserv").Get<size_t>("minpasslen", "10");
if (pass.length() < minpasslen)
{
source.Reply(_("Your password is too short. It must be longer than %zu characters."), minpasslen);
return EVENT_STOP;
}
const auto maxpasslen = Config->GetModule("nickserv").Get<size_t>("maxpasslen", "50");
if (pass.length() > maxpasslen)
{
source.Reply(_("Your password is too long. It must be shorter than %zu characters."), maxpasslen);
return EVENT_STOP;
}
return EVENT_CONTINUE;
}
};
MODULE_INIT(NickServCore)