/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2023-2024, 2026 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 <sstream>
#include "inspircd.h"
#include "clientprotocolevent.h"
#include "modules/cloak.h"
#include "modules/ircv3.h"
#include "modules/server.h"
#include "utility/container.h"
using CloakMethodList = std::vector<Cloak::MethodPtr>;
class CommandCloak final
: public SplitCommand
{
private:
// The cloak engines from the config.
CloakMethodList& cloakmethods;
// Reference to the standard-replies cap.
IRCv3::ReplyCapReference stdrplcap;
public:
CommandCloak(const WeakModulePtr& Creator, CloakMethodList& ce)
: SplitCommand(Creator, "CLOAK", 1)
, cloakmethods(ce)
, 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 auto cloak = cloakmethod->Cloak(parameters[0]);
if (!cloak)
continue;
IRCv3::WriteReply(Reply::NOTE, user, stdrplcap, this, "CLOAK_RESULT", parameters[0], cloak->ToString(), FMT::format("Cloak #{} for {} is {} (method: {})",
++count, parameters[0], cloak->ToString(), cloakmethod->GetName()));
}
if (!count)
{
IRCv3::WriteReply(Reply::FAIL, user, stdrplcap, this, "UNABLE_TO_CLOAK", parameters[0], FMT::format("There are no methods available for cloaking {}",
parameters[0]));
}
return CmdResult::SUCCESS;
}
};
class CloakExtItem final
: public ListExtItem<Cloak::List>
{
public:
CloakExtItem(const WeakModulePtr& mod)
: ListExtItem<Cloak::List>(mod, "cloaks", ExtensionType::USER)
{
}
Cloak::Info DecodeElement(const std::string& element) const override
{
return Cloak::Info::FromString(element);
}
std::string EncodeElement(const Cloak::Info& element) const override
{
return element.ToString();
}
};
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.
CloakExtItem ext;
std::optional<Cloak::Info> GetFrontCloak(LocalUser* user)
{
auto* cloaks = GetCloaks(user);
return cloaks ? std::make_optional(cloaks->front()) : std::nullopt;
}
public:
bool recloaking = false;
CloakAPI(const WeakModulePtr& Creator, CloakMethodList& cm, ModeHandler* mh)
: Cloak::APIBase(Creator)
, cloakmethods(cm)
, cloakmode(mh)
, ext(Creator)
{
}
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 auto cloak = cloakmethod->Cloak(user);
if (cloak)
{
cloaks->push_back(*cloak);
ServerInstance->Logs.Debug(MODNAME, "Cloaked {} ({}) [{}] as {} using the {} method.",
user->uuid, user->GetAddress(), user->GetRealHost(), cloak->ToString(),
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 auto oldcloak = GetFrontCloak(user); // This should always be non-nullopt.
ext.Unset(user);
if (!resetdisplay || !user->IsModeSet(cloakmode))
return; // Not resetting the display or not cloaked.
const auto newcloak = GetFrontCloak(user);
if (oldcloak.has_value() == newcloak.has_value() && *oldcloak == *newcloak)
return; // New front cloak is the same.
Modes::ChangeList changelist;
changelist.push_remove(cloakmode);
if (newcloak)
{
recloaking = true;
changelist.push_add(cloakmode);
}
ServerInstance->Modes.Process(ServerInstance->FakeClient, nullptr, user, changelist);
recloaking = false;
}
};
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 (cloakapi.recloaking)
return false; // Recloaking is always allowed.
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(const WeakModulePtr& 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
auto* user = dest->AsLocal();
if (!user)
{
// Remote setters broadcast mode before host while local setters do the opposite.
active = source->IsLocal() ? 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 && !cloakapi.recloaking)
user->CommandFloodPenalty += 5'000;
if (!change.adding)
{
// Remove the mode and restore their real host.
user->SetMode(this, false);
if (!cloakapi.recloaking)
{
user->ChangeDisplayedUser(user->GetRealUser());
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.
auto &cloak = cloaks->front();
if (!cloak.username.empty())
user->ChangeDisplayedUser(cloak.username);
user->ChangeDisplayedHost(cloak.hostname);
user->SetMode(this, true);
return true;
}
return false;
}
};
class ModuleCloak final
: public Module
, public ServerProtocol::SyncEventListener
{
private:
CloakAPI cloakapi;
CloakMethodList cloakmethods;
CommandCloak cloakcmd;
CloakMode cloakmode;
void DisableMode(User* user)
{
user->SetMode(cloakmode, false);
auto* luser = user->AsLocal();
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.")
, ServerProtocol::SyncEventListener(weak_from_this())
, cloakapi(weak_from_this(), cloakmethods, &cloakmode)
, cloakcmd(weak_from_this(), cloakmethods)
, cloakmode(weak_from_this(), cloakapi)
{
}
void ReadConfig(ConfigStatus& status) override
{
auto tags = ServerInstance->Config->ConfTags("cloak");
if (tags.empty())
throw ModuleException(weak_from_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(weak_from_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::Engine", method);
if (!service)
throw ModuleException(weak_from_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
{
LinkData data;
this->GetLinkData(data);
// 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) 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);
data["method"] = cloakmethod->GetName();
}
void Prioritize() override
{
ServerInstance->Modules.SetPriority(shared_from_this(), I_OnCheckListEntry, 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.
auto &cloak = cloaks->front();
if (!cloak.username.empty())
user->ChangeDisplayedUser(cloak.username);
user->ChangeDisplayedHost(cloak.hostname);
}
else
{
// The user has no cloak list; unset mode and revert to the real host.
DisableMode(user);
user->ChangeDisplayedUser(user->GetRealUser());
user->ChangeDisplayedHost(user->GetRealHost());
}
}
}
ModResult OnCheckListEntry(ListModeBase* lm, User* user, Channel* chan, const std::string& mask, bool full) override
{
auto* lu = user->AsLocal();
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.hostname == user->GetDisplayedHost() && (cloak.username.empty() || cloak.username == user->GetDisplayedUser()))
continue; // This is checked by the core.
const auto &ruser = cloak.username.empty() ? user->GetRealUser() : cloak.username;
const auto &duser = cloak.username.empty() ? user->GetDisplayedUser() : cloak.username;
if (full)
{
auto cloakmask = FMT::format("{}!{}@{}", user->nick, ruser, cloak.hostname);
if (InspIRCd::Match(cloakmask, mask))
return MOD_RES_DENY;
}
auto cloakmask = FMT::format("{}!{}@{}", user->nick, duser, cloak.hostname);
if (InspIRCd::Match(cloakmask, mask))
return MOD_RES_DENY;
}
return MOD_RES_PASSTHRU;
}
void OnServiceDel(Service::Provider& 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.service_name, methods);
}
}
void OnSyncNetwork(Server& server) override
{
if (cloakmethods.empty())
return;
std::stringstream buf;
for (auto mit = cloakmethods.begin(); mit != cloakmethods.end(); ++mit)
{
if (mit != cloakmethods.begin())
buf << ' ';
// All cloak methods have their name listed.
const auto& cloakmethod = *mit;
buf << cloakmethod->GetName();
LinkData data;
cloakmethod->GetLinkData(data);
if (data.empty())
continue; // This method has no data so thats all we need to do.
// Serialize the data in the same format as CAPAB MODULES.
buf << '=';
for (auto dit = data.begin(); dit != data.end(); ++dit)
{
if (dit != data.begin())
buf << '&';
buf << dit->first << '=' << Percent::Encode(dit->second);
}
}
server.SendMetadata("cloakmethods", buf.str());
}
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, const std::shared_ptr<ConnectClass>& oldklass, 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)