mirror of
https://github.com/inspircd/inspircd.git
synced 2025-03-10 02:59:01 -04:00
445 lines
13 KiB
C++
445 lines
13 KiB
C++
/*
|
|
* InspIRCd -- Internet Relay Chat Daemon
|
|
*
|
|
* Copyright (C) 2020-2023 Sadie Powell <sadie@witchery.services>
|
|
*
|
|
* This file is part of InspIRCd. InspIRCd is free software: you can
|
|
* redistribute it and/or modify it under the terms of the GNU General Public
|
|
* License as published by the Free Software Foundation, version 2.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
* details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
|
|
#include "inspircd.h"
|
|
#include "modules/isupport.h"
|
|
|
|
class Codepage
|
|
{
|
|
public:
|
|
enum class AllowCharacterResult
|
|
: uint8_t
|
|
{
|
|
// The character is allowed in a nick.
|
|
OKAY,
|
|
|
|
// The character is never valid in a nick.
|
|
NOT_VALID,
|
|
|
|
// The character is not valid at the front of a nick.
|
|
NOT_VALID_AT_FRONT,
|
|
};
|
|
|
|
// The mapping of lower case characters to upper case characters.
|
|
unsigned char casemap[UCHAR_MAX + 1];
|
|
|
|
// Initialises the Codepage class.
|
|
Codepage()
|
|
{
|
|
for (size_t i = 0; i <= UCHAR_MAX; ++i)
|
|
casemap[i] = i;
|
|
}
|
|
|
|
// Destroys the Codepage class.
|
|
virtual ~Codepage() = default;
|
|
|
|
// Specifies that a character is allowed.
|
|
virtual AllowCharacterResult AllowCharacter(uint32_t character, bool front)
|
|
{
|
|
if (front)
|
|
{
|
|
// Nicknames can not begin with a number as that would collide with
|
|
// a user identifier.
|
|
if (character >= '0' && character <= '9')
|
|
return AllowCharacterResult::NOT_VALID_AT_FRONT;
|
|
|
|
// Nicknames can not begin with a : or a $ as they have a special
|
|
// meaning within the IRC message format.
|
|
if (character == '$' || character == ':')
|
|
return AllowCharacterResult::NOT_VALID_AT_FRONT;
|
|
|
|
// Nicknames can not begin with a channel prefix character (e.g. #)
|
|
// as they are used to differentiate users from channels in message
|
|
// targets.
|
|
if (ServerInstance->Channels.IsPrefix(character))
|
|
return AllowCharacterResult::NOT_VALID_AT_FRONT;
|
|
|
|
// Nicknames can not begin with a prefix mode character (e.g. @)
|
|
// as they are used as a nick prefix when sending a STATUSMSG.
|
|
if (ServerInstance->Modes.FindPrefix(character))
|
|
return AllowCharacterResult::NOT_VALID_AT_FRONT;
|
|
}
|
|
|
|
// Nicknames can never contain NUL, CR, LF, SPACE, COMMA, PERIOD,
|
|
// ASTERISK, QUESTION MARK, EXCLAMATION MARK, or AT SIGN as they are
|
|
// either banned within an IRC message or have special meaning within
|
|
// the IRC message format.
|
|
switch (character)
|
|
{
|
|
case '\0':
|
|
case '\n':
|
|
case '\r':
|
|
case ' ':
|
|
case '!':
|
|
case '*':
|
|
case ',':
|
|
case '.':
|
|
case '?':
|
|
case '@':
|
|
return AllowCharacterResult::NOT_VALID;
|
|
}
|
|
|
|
|
|
// The character is probably okay?
|
|
return AllowCharacterResult::OKAY;
|
|
}
|
|
|
|
// Determines whether a nickname is valid.
|
|
virtual bool IsValidNick(const std::string_view& nick) = 0;
|
|
|
|
// Retrieves the link data for this codepage.
|
|
virtual void GetLinkData(Module::LinkData& data) const = 0;
|
|
|
|
// Maps an upper case character to a lower case character.
|
|
virtual bool Map(uint32_t upper, uint32_t lower) = 0;
|
|
};
|
|
|
|
class SingleByteCodepage final
|
|
: public Codepage
|
|
{
|
|
private:
|
|
// The characters which are allowed in nicknames.
|
|
CharState allowedchars;
|
|
|
|
// The characters which are allowed at the front of a nickname.
|
|
CharState allowedfrontchars;
|
|
|
|
public:
|
|
AllowCharacterResult AllowCharacter(uint32_t character, bool front) override
|
|
{
|
|
// Single byte codepage can, as their name suggests, only be one byte in size.
|
|
if (character > UCHAR_MAX)
|
|
return AllowCharacterResult::NOT_VALID;
|
|
|
|
// Check the common allowed character rules.
|
|
AllowCharacterResult result = Codepage::AllowCharacter(character, front);
|
|
if (result != AllowCharacterResult::OKAY)
|
|
return result;
|
|
|
|
// The character is okay.
|
|
allowedchars.set(character);
|
|
allowedfrontchars.set(character, front);
|
|
return AllowCharacterResult::OKAY;
|
|
}
|
|
|
|
bool IsValidNick(const std::string_view& nick) override
|
|
{
|
|
if (nick.empty() || nick.length() > ServerInstance->Config->Limits.MaxNick)
|
|
return false;
|
|
|
|
for (std::string_view::const_iterator iter = nick.begin(); iter != nick.end(); ++iter)
|
|
{
|
|
unsigned char chr = static_cast<unsigned char>(*iter);
|
|
|
|
// Check that the character is allowed at the front of the nick.
|
|
if (iter == nick.begin() && !allowedfrontchars[chr])
|
|
return false;
|
|
|
|
// Check that the character is allowed in the nick.
|
|
if (!allowedchars[chr])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void GetLinkData(Module::LinkData& data) const override
|
|
{
|
|
for (size_t i = 0; i < allowedfrontchars.size(); ++i)
|
|
if (allowedfrontchars[i])
|
|
data["front"].push_back(static_cast<unsigned char>(i));
|
|
|
|
for (size_t i = 0; i < allowedchars.size(); ++i)
|
|
if (allowedchars[i])
|
|
data["middle"].push_back(static_cast<unsigned char>(i));
|
|
|
|
for (size_t i = 0; i < sizeof(casemap); ++i)
|
|
{
|
|
if (casemap[i] == i)
|
|
continue;
|
|
|
|
data["map"].push_back(static_cast<unsigned char>(i));
|
|
data["map"].push_back(casemap[i]);
|
|
data["map"].push_back(',');
|
|
}
|
|
}
|
|
|
|
bool Map(uint32_t upper, uint32_t lower) override
|
|
{
|
|
if (upper > UCHAR_MAX || lower > UCHAR_MAX)
|
|
return false;
|
|
|
|
casemap[upper] = lower;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
class ModuleCodepage final
|
|
: public Module
|
|
, public ISupport::EventListener
|
|
{
|
|
private:
|
|
// The currently active codepage.
|
|
std::unique_ptr<Codepage> codepage = nullptr;
|
|
|
|
// The character map which was set before this module was loaded.
|
|
const unsigned char* origcasemap;
|
|
|
|
// The name of the character map which was set before this module was loaded.
|
|
const std::string origcasemapname;
|
|
|
|
// The IsNick handler which was set before this module was loaded.
|
|
const std::function<bool(const std::string_view&)> origisnick;
|
|
|
|
// The character set used for the codepage.
|
|
std::string charset;
|
|
|
|
template <typename T>
|
|
static void RehashHashmap(T& hashmap)
|
|
{
|
|
T newhash(hashmap.bucket_count());
|
|
for (const auto& [key, value] : hashmap)
|
|
newhash.emplace(key, value);
|
|
hashmap.swap(newhash);
|
|
}
|
|
|
|
static void DestroyChannel(Channel* chan)
|
|
{
|
|
// Remove all of the users from the channel. Using KICK here will mean
|
|
// the user's client will probably attempt to rejoin and will enter the
|
|
// succeeding channel. Unfortunately this is the best we can do for now.
|
|
while (!chan->userlist.empty())
|
|
chan->KickUser(ServerInstance->FakeClient, chan->userlist.begin(), "This channel does not exist anymore.");
|
|
|
|
// Remove all modes from the channel just in case one of them keeps the channel open.
|
|
Modes::ChangeList changelist;
|
|
for (const auto& [_, mh] : ServerInstance->Modes.GetModes(MODETYPE_CHANNEL))
|
|
mh->RemoveMode(chan, changelist);
|
|
ServerInstance->Modes.Process(ServerInstance->FakeClient, chan, nullptr, changelist, ModeParser::MODE_LOCALONLY);
|
|
|
|
// The channel will be destroyed automatically by CheckDestroy.
|
|
}
|
|
|
|
static void ChangeNick(User* user, const std::string& message)
|
|
{
|
|
user->WriteNumeric(RPL_SAVENICK, user->uuid, message);
|
|
user->ChangeNick(user->uuid);
|
|
}
|
|
|
|
static void CheckDuplicateChan()
|
|
{
|
|
ChannelMap duplicates;
|
|
for (auto &[_, chan] : ServerInstance->Channels.GetChans())
|
|
{
|
|
auto check = duplicates.insert(std::make_pair(chan->name, chan));
|
|
if (check.second)
|
|
continue; // No duplicate.
|
|
|
|
Channel* otherchan = check.first->second;
|
|
if (otherchan->age < chan->age)
|
|
{
|
|
// The other channel was created first.
|
|
DestroyChannel(chan);
|
|
}
|
|
else if (otherchan->age > chan->age)
|
|
{
|
|
// The other channel was created last.
|
|
DestroyChannel(otherchan);
|
|
check.first->second = chan;
|
|
}
|
|
else
|
|
{
|
|
// Both created at the same time.
|
|
DestroyChannel(chan);
|
|
DestroyChannel(otherchan);
|
|
duplicates.erase(check.first);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CheckDuplicateNick()
|
|
{
|
|
UserMap duplicates;
|
|
for (auto* user : ServerInstance->Users.GetLocalUsers())
|
|
{
|
|
if (user->nick == user->uuid)
|
|
continue; // UUID users are always unique.
|
|
|
|
auto check = duplicates.insert(std::make_pair(user->nick, user));
|
|
if (check.second)
|
|
continue; // No duplicate.
|
|
|
|
User* otheruser = check.first->second;
|
|
if (otheruser->nickchanged < user->nickchanged)
|
|
{
|
|
// The other user connected first.
|
|
ChangeNick(user, "Your nickname is no longer available.");
|
|
}
|
|
else if (otheruser->nickchanged > user->nickchanged)
|
|
{
|
|
// The other user connected last.
|
|
ChangeNick(otheruser, "Your nickname is no longer available.");
|
|
check.first->second = user;
|
|
}
|
|
else
|
|
{
|
|
// Both connected at the same time.
|
|
ChangeNick(user, "Your nickname is no longer available.");
|
|
ChangeNick(otheruser, "Your nickname is no longer available.");
|
|
duplicates.erase(check.first);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CheckInvalidNick()
|
|
{
|
|
for (auto* user : ServerInstance->Users.GetLocalUsers())
|
|
{
|
|
if (user->nick != user->uuid && !ServerInstance->IsNick(user->nick))
|
|
ChangeNick(user, "Your nickname is no longer valid.");
|
|
}
|
|
}
|
|
|
|
static void CheckRehash(unsigned char* prevmap)
|
|
{
|
|
if (!memcmp(prevmap, national_case_insensitive_map, UCHAR_MAX))
|
|
return;
|
|
|
|
RehashHashmap(ServerInstance->Users.clientlist);
|
|
RehashHashmap(ServerInstance->Users.uuidlist);
|
|
RehashHashmap(ServerInstance->Channels.GetChans());
|
|
}
|
|
|
|
static std::string GetPrintable(uint32_t chr)
|
|
{
|
|
if (isprint(chr))
|
|
return FMT::format("{} ({})", reinterpret_cast<const char*>(&chr), chr);
|
|
else
|
|
return FMT::format("{}", chr);
|
|
}
|
|
|
|
public:
|
|
ModuleCodepage()
|
|
: Module(VF_VENDOR | VF_COMMON, "Allows the server administrator to define what characters are allowed in nicknames and how characters should be compared in a case insensitive way.")
|
|
, ISupport::EventListener(this)
|
|
, origcasemap(national_case_insensitive_map)
|
|
, origcasemapname(ServerInstance->Config->CaseMapping)
|
|
, origisnick(ServerInstance->IsNick)
|
|
{
|
|
}
|
|
|
|
~ModuleCodepage() override
|
|
{
|
|
ServerInstance->IsNick = origisnick;
|
|
CheckInvalidNick();
|
|
|
|
ServerInstance->Config->CaseMapping = origcasemapname;
|
|
national_case_insensitive_map = origcasemap;
|
|
CheckDuplicateChan();
|
|
CheckDuplicateNick();
|
|
if (codepage) // nullptr if ReadConfig throws on load.
|
|
CheckRehash(codepage->casemap);
|
|
}
|
|
|
|
void ReadConfig(ConfigStatus& status) override
|
|
{
|
|
const auto& codepagetag = ServerInstance->Config->ConfValue("codepage");
|
|
|
|
const std::string name = codepagetag->getString("name");
|
|
if (name.empty())
|
|
throw ModuleException(this, "<codepage:name> is a required field!");
|
|
|
|
std::unique_ptr<Codepage> newcodepage = std::make_unique<SingleByteCodepage>();
|
|
for (const auto& [_, tag] : ServerInstance->Config->ConfTags("cpchars"))
|
|
{
|
|
uint32_t begin = tag->getNum<uint32_t>("begin", tag->getNum<uint32_t>("index", 0));
|
|
if (!begin)
|
|
throw ModuleException(this, "<cpchars> tag without index or begin specified at " + tag->source.str());
|
|
|
|
uint32_t end = tag->getNum<uint32_t>("end", begin);
|
|
if (begin > end)
|
|
throw ModuleException(this, "<cpchars:begin> must be lower than <cpchars:end> at " + tag->source.str());
|
|
|
|
bool front = tag->getBool("front", false);
|
|
for (uint32_t pos = begin; pos <= end; ++pos)
|
|
{
|
|
switch (newcodepage->AllowCharacter(pos, front))
|
|
{
|
|
case Codepage::AllowCharacterResult::OKAY:
|
|
ServerInstance->Logs.Debug(MODNAME, "Marked {} as allowed (front: {})",
|
|
GetPrintable(pos), front ? "yes" : "no");
|
|
break;
|
|
|
|
case Codepage::AllowCharacterResult::NOT_VALID:
|
|
throw ModuleException(this, FMT::format("<cpchars> tag contains a forbidden character: {} at {}",
|
|
GetPrintable(pos), tag->source.str()));
|
|
|
|
case Codepage::AllowCharacterResult::NOT_VALID_AT_FRONT:
|
|
throw ModuleException(this, FMT::format("<cpchars> tag contains a forbidden front character: {} at {}",
|
|
GetPrintable(pos), tag->source.str()));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& [_, tag] : ServerInstance->Config->ConfTags("cpcase"))
|
|
{
|
|
uint32_t lower = tag->getNum<uint32_t>("lower", 0);
|
|
if (!lower)
|
|
throw ModuleException(this, "<cpcase:lower> is required at " + tag->source.str());
|
|
|
|
uint32_t upper = tag->getNum<uint32_t>("upper", 0);
|
|
if (!upper)
|
|
throw ModuleException(this, "<cpcase:upper> is required at " + tag->source.str());
|
|
|
|
if (!newcodepage->Map(upper, lower))
|
|
throw ModuleException(this, "Malformed <cpcase> tag at " + tag->source.str());
|
|
|
|
ServerInstance->Logs.Debug(MODNAME, "Marked {} as the lower case version of {}",
|
|
GetPrintable(lower), GetPrintable(upper));
|
|
}
|
|
|
|
charset = codepagetag->getString("charset");
|
|
std::swap(codepage, newcodepage);
|
|
ServerInstance->IsNick = [this](const std::string_view& nick) { return codepage->IsValidNick(nick); };
|
|
CheckInvalidNick();
|
|
|
|
ServerInstance->Config->CaseMapping = name;
|
|
national_case_insensitive_map = codepage->casemap;
|
|
if (newcodepage) // nullptr on first read.
|
|
{
|
|
CheckDuplicateChan();
|
|
CheckDuplicateNick();
|
|
CheckRehash(newcodepage->casemap);
|
|
}
|
|
}
|
|
|
|
void OnBuildISupport(ISupport::TokenMap& tokens) override
|
|
{
|
|
if (!charset.empty())
|
|
tokens["CHARSET"] = charset;
|
|
}
|
|
|
|
void GetLinkData(LinkData& data) override
|
|
{
|
|
codepage->GetLinkData(data);
|
|
}
|
|
};
|
|
|
|
MODULE_INIT(ModuleCodepage)
|