diff options
| author | 2024-07-17 00:06:50 +0100 | |
|---|---|---|
| committer | 2024-07-17 00:06:50 +0100 | |
| commit | 889d521e05f2e922683d48c74eb00290e7a2f548 (patch) | |
| tree | b4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /modules/cloak.cpp | |
| parent | Use std::endian from C++20 in the sha1 module. (diff) | |
Shuffle the modules about a bit.
Diffstat (limited to 'modules/cloak.cpp')
| -rw-r--r-- | modules/cloak.cpp | 462 |
1 files changed, 462 insertions, 0 deletions
diff --git a/modules/cloak.cpp b/modules/cloak.cpp new file mode 100644 index 000000000..ed5e90929 --- /dev/null +++ b/modules/cloak.cpp @@ -0,0 +1,462 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 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 "clientprotocolevent.h" +#include "modules/cloak.h" +#include "modules/ircv3_replies.h" +#include "utility/map.h" + +typedef std::vector<Cloak::MethodPtr> CloakMethodList; + +class CommandCloak final + : public SplitCommand +{ +private: + // The cloak engines from the config. + CloakMethodList& cloakmethods; + + // API for sending a FAIL message. + IRCv3::Replies::Note failrpl; + + // API for sending a NOTE message. + IRCv3::Replies::Note noterpl; + + // Reference to the standard-replies cap. + IRCv3::Replies::CapReference stdrplcap; + +public: + CommandCloak(Module* Creator, CloakMethodList& ce) + : SplitCommand(Creator, "CLOAK", 1) + , cloakmethods(ce) + , failrpl(Creator) + , noterpl(Creator) + , stdrplcap(Creator) + { + access_needed = CmdAccess::OPERATOR; + syntax = { "<host>" }; + } + + CmdResult HandleLocal(LocalUser* user, const Params& parameters) override + { + size_t count = 0; + for (const auto& cloakmethod : cloakmethods) + { + const std::string cloak = cloakmethod->Generate(parameters[0]); + if (cloak.empty()) + continue; + + noterpl.SendIfCap(user, stdrplcap, this, "CLOAK_RESULT", parameters[0], cloak, fmt::format("Cloak #{} for {} is {} (method: {})", + ++count, parameters[0], cloak, cloakmethod->GetName())); + } + + if (!count) + { + failrpl.SendIfCap(user, stdrplcap, this, "UNABLE_TO_CLOAK", parameters[0], fmt::format("There are no methods available for cloaking {}", + parameters[0])); + } + + return CmdResult::SUCCESS; + } +}; + +class CloakAPI final + : public Cloak::APIBase +{ +private: + // The cloak providers from the config. + CloakMethodList& cloakmethods; + + // The mode which marks a user as being cloaked. + ModeHandler* cloakmode; + + // Holds the list of cloaks for a user. + ListExtItem<Cloak::List> ext; + + std::string GetFrontCloak(LocalUser* user) + { + auto* cloaks = GetCloaks(user); + return cloaks ? cloaks->front() : ""; + } + +public: + CloakAPI(Module* Creator, CloakMethodList& cm, ModeHandler* mh) + : Cloak::APIBase(Creator) + , cloakmethods(cm) + , cloakmode(mh) + , ext(Creator, "cloaks", ExtensionType::USER) + { + } + + Cloak::List* GetCloaks(LocalUser* user) override + { + if (user->quitting || !(user->connected & User::CONN_NICKUSER)) + return nullptr; // This user isn't at a point where they can change their cloak. + + if (!user->GetClass()->config->getBool("usecloak", true)) + return nullptr; + + auto* cloaks = ext.Get(user); + if (!cloaks) + { + // The list doesn't exist so try to create it. + cloaks = new Cloak::List(); + for (const auto& cloakmethod : cloakmethods) + { + const std::string cloak = cloakmethod->Generate(user); + if (!cloak.empty()) + { + cloaks->push_back(cloak); + + ServerInstance->Logs.Debug(MODNAME, "Cloaked {} ({}) [{}] as {} using the {} method.", + user->uuid, user->GetAddress(), user->GetRealHost(), cloak, + cloakmethod->GetName()); + } + else + { + ServerInstance->Logs.Debug(MODNAME, "Unable to cloak {} ({}) [{}] using the {} method.", + user->uuid, user->GetAddress(), user->GetRealHost(), cloakmethod->GetName()); + } + } + ext.Set(user, cloaks); + } + return cloaks->empty() ? nullptr : cloaks; + } + + bool IsActiveCloak(const Cloak::Engine& engine) override + { + for (const auto& cloakmethod : cloakmethods) + { + if (cloakmethod->IsProvidedBy(engine)) + return true; + } + return false; + } + + void ResetCloaks(LocalUser* user, bool resetdisplay) override + { + if (user->quitting || !(user->connected & User::CONN_NICKUSER)) + return; // This user isn't at a point where they can change their cloak. + + const std::string oldcloak = GetFrontCloak(user); + ext.Unset(user); + + if (!resetdisplay || !user->IsModeSet(cloakmode)) + return; // Not resetting the display or not cloaked. + + const std::string newcloak = GetFrontCloak(user); + if (oldcloak == newcloak) + return; // New front cloak is the same. + + Modes::ChangeList changelist; + changelist.push_remove(cloakmode); + if (!newcloak.empty()) + changelist.push_add(cloakmode); + ServerInstance->Modes.Process(ServerInstance->FakeClient, nullptr, user, changelist); + } +}; + +class CloakMode final + : public ModeHandler +{ +private: + // The public API for the cloak system. + CloakAPI& cloakapi; + + // The number of times the last user has set/unset this mode at once. + size_t prevcount = 0; + + // The time at which the last user set/unset this mode. + time_t prevtime = 0; + + // The UUID of the last user to set/unset this mode. + std::string prevuuid; + + bool CheckSpam(User* user) + { + if (user->uuid == prevuuid && prevtime == ServerInstance->Time()) + { + // The user has changed this mode already recently. Have they done + // it too much? + return ++prevcount > 2; + } + + // This is the first time the user has executed the mode recently so its fine. + prevcount = 0; + prevtime = ServerInstance->Time(); + prevuuid = user->uuid; + return false; + } + +public: + // Whether the mode has recently been changed. + bool active = false; + + CloakMode(Module* Creator, CloakAPI& api) + : ModeHandler(Creator, "cloak", 'x', PARAM_NONE, MODETYPE_USER) + , cloakapi(api) + { + } + + bool OnModeChange(User* source, User* dest, Channel* channel, Modes::Change& change) override + { + // For remote users blindly allow this + LocalUser* user = IS_LOCAL(dest); + if (!user) + { + // Remote setters broadcast mode before host while local setters do the opposite. + active = IS_LOCAL(source) ? change.adding : !change.adding; + dest->SetMode(this, change.adding); + return true; + } + + // Don't allow the mode change if its a no-op or a spam change. + if (change.adding == user->IsModeSet(this) || CheckSpam(user)) + return false; + + // Penalise changing the mode to avoid spam. + if (source == dest) + user->CommandFloodPenalty += 5'000; + + if (!change.adding) + { + // Remove the mode and restore their real host. + user->SetMode(this, false); + user->ChangeDisplayedHost(user->GetRealHost()); + return true; + } + + // If a user is not fully connected and their displayed hostname is + // different to their real hostname they probably had a vhost set on + // them by services. We should avoid automatically setting cloak on + // them in this case. + if (!user->IsFullyConnected() && user->GetRealHost() != user->GetDisplayedHost()) + return false; + + auto* cloaks = cloakapi.GetCloaks(user); + if (cloaks) + { + // We were able to generate cloaks for this user. + user->ChangeDisplayedHost(cloaks->front()); + user->SetMode(this, true); + return true; + } + return false; + } +}; + +class ModuleCloak final + : public Module +{ +private: + CloakAPI cloakapi; + CloakMethodList cloakmethods; + CommandCloak cloakcmd; + CloakMode cloakmode; + + void DisableMode(User* user) + { + user->SetMode(cloakmode, false); + + auto* luser = IS_LOCAL(user); + if (luser) + { + Modes::ChangeList changelist; + changelist.push_remove(&cloakmode); + ClientProtocol::Events::Mode modeevent(ServerInstance->FakeClient, nullptr, luser, changelist); + luser->Send(modeevent); + } + } + +public: + ModuleCloak() + : Module(VF_VENDOR | VF_COMMON, "Adds user mode x (cloak) which allows user hostnames to be hidden.") + , cloakapi(this, cloakmethods, &cloakmode) + , cloakcmd(this, cloakmethods) + , cloakmode(this, cloakapi) + { + } + + void ReadConfig(ConfigStatus& status) override + { + auto tags = ServerInstance->Config->ConfTags("cloak"); + if (tags.empty()) + throw ModuleException(this, "You have loaded the cloak module but not configured any <cloak> tags!"); + + bool primary = true; + CloakMethodList newcloakmethods; + for (const auto& [_, tag] : tags) + { + const std::string method = tag->getString("method"); + if (method.empty()) + throw ModuleException(this, "<cloak:method> must be set to the name of a cloak engine, at " + tag->source.str()); + + auto* service = ServerInstance->Modules.FindDataService<Cloak::Engine>("cloak/" + method); + if (!service) + throw ModuleException(this, "<cloak> tag was set to non-existent cloak method \"" + method + "\", at " + tag->source.str()); + + newcloakmethods.push_back(service->Create(tag, primary)); + primary = false; + } + + // The cloak configuration was valid so we can apply it. + cloakmethods.swap(newcloakmethods); + } + + void CompareLinkData(const LinkData& otherdata, LinkDataDiff& diffs) override + { + std::string unused; + LinkData data; + this->GetLinkData(data, unused); + + // If the only difference is the method then just include that. + insp::map::difference(data, otherdata, diffs); + auto it = diffs.find("method"); + if (it != diffs.end()) + diffs = { *it }; + } + + void GetLinkData(LinkData& data, std::string& compatdata) override + { + if (cloakmethods.empty()) + return; + + Cloak::MethodPtr cloakmethod; + for (const auto& method : cloakmethods) + { + if (method->IsLinkSensitive()) + { + // This cloak method really wants to be the link method so prefer it. + cloakmethod = method; + break; + } + } + + // If no methods are link sensitive we just use the first. + if (!cloakmethod) + cloakmethod = cloakmethods.front(); + + cloakmethod->GetLinkData(data, compatdata); + + data["method"] = cloakmethod->GetName(); + if (compatdata.empty()) + compatdata.assign(cloakmethod->GetName()); + } + + void Prioritize() override + { + ServerInstance->Modules.SetPriority(this, I_OnCheckBan, PRIORITY_LAST); + } + + void OnChangeHost(User* user, const std::string& host) override + { + if (user->IsModeSet(cloakmode) && !cloakmode.active) + DisableMode(user); + + cloakmode.active = false; + } + + void OnChangeRemoteAddress(LocalUser* user) override + { + // Remove the cloaks so we can generate new ones. + cloakapi.ResetCloaks(user, false); + + // If a user is using a cloak then update it. + auto* cloaks = cloakapi.GetCloaks(user); + if (user->IsModeSet(cloakmode)) + { + if (cloaks) + { + // The user has a new cloak list; pick the first. + user->ChangeDisplayedHost(cloaks->front()); + } + else + { + // The user has no cloak list; unset mode and revert to the real host. + DisableMode(user); + user->ChangeDisplayedHost(user->GetRealHost()); + } + } + } + + ModResult OnCheckBan(User* user, Channel* chan, const std::string& mask) override + { + LocalUser* lu = IS_LOCAL(user); + if (!lu) + return MOD_RES_PASSTHRU; // We don't have cloaks for remote users. + + auto* cloaks = cloakapi.GetCloaks(lu); + if (!cloaks) + return MOD_RES_PASSTHRU; // No cloaks, nothing to check. + + // Check if they have a cloaked host but are not using it. + for (const auto& cloak : *cloaks) + { + if (cloak == user->GetDisplayedHost()) + continue; // This is checked by the core. + + std::string cloakmask = user->nick + "!" + user->GetRealUser() + "@" + cloak; + if (InspIRCd::Match(cloakmask, mask)) + return MOD_RES_DENY; + + cloakmask = user->nick + "!" + user->GetDisplayedUser() + "@" + cloak; + if (InspIRCd::Match(cloakmask, mask)) + return MOD_RES_DENY; + } + return MOD_RES_PASSTHRU; + } + + void OnServiceDel(ServiceProvider& service) override + { + size_t methods = 0; + for (auto it = cloakmethods.begin(); it != cloakmethods.end(); ) + { + auto cloakmethod = *it; + if (cloakmethod->IsProvidedBy(service)) + { + it = cloakmethods.erase(it); + methods++; + continue; + } + it++; + } + + if (methods) + { + ServerInstance->SNO.WriteGlobalSno('a', "The {} hash provider was unloaded; removing {} cloak methods until the next rehash.", + service.name.substr(6), methods); + } + } + + void OnUserConnect(LocalUser* user) override + { + // Generate cloaks now if they do not already exist so opers can /CHECK + // this user if need be. + cloakapi.GetCloaks(user); + } + + void OnPostChangeConnectClass(LocalUser* user, bool force) override + { + // Reset the cloaks so if the user is moving into a class with <connect usecloak="no"> they + // will be decloaked. + cloakapi.ResetCloaks(user, true); + } +}; + +MODULE_INIT(ModuleCloak) |
