diff options
| author | 2024-07-17 00:06:50 +0100 | |
|---|---|---|
| committer | 2024-07-17 00:06:50 +0100 | |
| commit | 889d521e05f2e922683d48c74eb00290e7a2f548 (patch) | |
| tree | b4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /modules/codepage.cpp | |
| parent | Use std::endian from C++20 in the sha1 module. (diff) | |
Shuffle the modules about a bit.
Diffstat (limited to 'modules/codepage.cpp')
| -rw-r--r-- | modules/codepage.cpp | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/modules/codepage.cpp b/modules/codepage.cpp new file mode 100644 index 000000000..fe6aafad1 --- /dev/null +++ b/modules/codepage.cpp @@ -0,0 +1,446 @@ +/* + * 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, std::string& compatdata) 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, std::string& compatdata) 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(','); + } + + compatdata = fmt::format("front={}&middle={}&map={}", data["front"], data["middle"], data["map"]); + } + + 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, std::string& compatdata) override + { + codepage->GetLinkData(data, compatdata); + } +}; + +MODULE_INIT(ModuleCodepage) |
