aboutsummaryrefslogtreecommitdiff
/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2025 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 "extension.h"
#include "modules/cloak.h"
#include "modules/hash.h"
#include "modules/ircv3.h"

class CustomCloakExtItem final
	: public SimpleExtItem<Cloak::Info>
{
public:
	Cloak::API cloakapi;
	Cloak::Engine& customcloak;

	CustomCloakExtItem(const WeakModulePtr& mod, Cloak::Engine& cloak)
		: SimpleExtItem<Cloak::Info>(mod, "custom-cloak", ExtensionType::USER, true)
		, cloakapi(mod)
		, customcloak(cloak)
	{
	}

	void FromInternal(Extensible* container, const std::string& value) noexcept override
	{
		if (container->extype != this->extype)
			return;

		if (value.empty())
			Unset(container, false);
		else
		{
			auto cloak = Cloak::Info::FromString(Percent::Decode(value));
			if (cloak.hostname.empty())
				return; // Malformed.

			Set(container, cloak, false);

			auto *luser = static_cast<User*>(container)->AsLocal();
			if (!luser || !cloakapi)
				return;

			auto update = false;
			if (!cloak.username.empty() && luser->GetDisplayedUser() != cloak.username)
				update = true;
			else if (cloak.hostname != luser->GetDisplayedUser())
				update = true;

			if (update && cloakapi->IsActiveCloak(customcloak))
				cloakapi->ResetCloaks(luser, true);
		}
	}

	std::string ToInternal(const Extensible* container, const ExtensionPtr& item) const noexcept override
	{
		const auto& cloak = std::static_pointer_cast<Cloak::Info>(item);
		return cloak ? Percent::Encode(cloak->ToString()) : std::string();
	}
};

class CloakAccount final
{
private:
	const std::string host;
	const std::string password;
	const std::string passwordhash;

public:
	const Cloak::Info cloak;

	CloakAccount(const std::shared_ptr<ConfigTag>& tag, const Cloak::Info &c, const std::string& p)
		: host(tag->getString("host", "*@*", 1))
		, password(p)
		, passwordhash(tag->getString("hash", "plaintext", 1))
		, cloak(c)
	{
		if (insp::equalsci(passwordhash, "plaintext"))
		{
			ServerInstance->Logs.Warning(MODNAME, "<customcloak> tag at {} contains an plain text password, this is insecure!",
				tag->source.str());
		}
	}

	bool CheckHost(LocalUser* user) const
	{
		return InspIRCd::MatchMask(host, user->GetRealUserHost(), user->GetUserAddress());
	}

	bool CheckPassword(const std::string& pass) const
	{
		return Hash::CheckPassword(password, passwordhash, pass);
	}
};

using CloakAccounts = std::unordered_map<std::string, CloakAccount>;

class CommandCustomCloak final
	: public SplitCommand
{
private:
	CustomCloakExtItem& cloakext;
	UserModeReference cloakmode;
	IRCv3::ReplyCapReference stdrplcap;

	CmdResult FailedLogin(LocalUser* user, const std::string& account)
	{
		IRCv3::WriteReply(Reply::FAIL, user, stdrplcap, this, "LOGIN_FAIL", account, FMT::format("Failed to log into the \x02{}\x02 custom cloak account.", account));
		user->CommandFloodPenalty += 2500;
		return CmdResult::FAILURE;
	}

public:
	CloakAccounts accounts;

	CommandCustomCloak(const WeakModulePtr& mod, CustomCloakExtItem& ext)
		: SplitCommand(mod, "CUSTOMCLOAK", 2)
		, cloakext(ext)
		, cloakmode(mod, "cloak")
		, stdrplcap(mod)
	{
		syntax = { "<username> <password>" };
	}

	CmdResult HandleLocal(LocalUser* user, const Params& parameters) override
	{
		// Check whether the account exists.
		auto it = accounts.find(parameters[0]);
		if (it == accounts.end())
		{
			ServerInstance->Logs.Debug(MODNAME, "{} ({}) [{}] failed to log into the \x02{}\x02 custom cloak account because no account with that name exists.",
				user->nick, user->GetRealUserHost(), user->GetAddress(), parameters[0]);
			return FailedLogin(user, parameters[0]);
		}

		// Check whether the host is correct.
		auto &account = it->second;
		if (!account.CheckHost(user))
		{
			ServerInstance->Logs.Normal(MODNAME, "{} ({}) [{}] failed to log into the \x02{}\x02 custom cloak account because they are connecting from the wrong user@host.",
				user->nick, user->GetRealUserHost(), user->GetAddress(), parameters[0]);
			return FailedLogin(user, it->first);
		}

		// Check whether the password is correct.
		if (!account.CheckPassword(parameters[1]))
		{
			ServerInstance->Logs.Normal(MODNAME, "{} ({}) [{}] failed to log into the \x02{}\x02 custom cloak account because they specified the wrong password.",
				user->nick, user->GetRealUserHost(), user->GetAddress(), parameters[0]);
			return FailedLogin(user, it->first);
		}

		// If they have reached this point then the login succeeded,
		IRCv3::WriteReply(Reply::NOTE, user, stdrplcap, this, "LOGIN_SUCCESS", it->first, account.cloak.ToString(), FMT::format("You are now logged in as \x02{}\x02; updating your cloak to \x02{}\x02.",
			it->first, account.cloak.ToString()));

		cloakext.Set(user, account.cloak);
		if (cloakext.cloakapi && cloakext.cloakapi->IsActiveCloak(cloakext.customcloak))
			cloakext.cloakapi->ResetCloaks(user, true);

		if (cloakmode && !user->IsModeSet(cloakmode))
		{
			Modes::ChangeList changelist;
			changelist.push_add(*cloakmode);
			ServerInstance->Modes.Process(ServerInstance->FakeClient, nullptr, user, changelist);
		}

		return CmdResult::SUCCESS;
	}
};

class CustomMethod final
	: public Cloak::Method
{
private:
	// The cloak to set on users.
	CustomCloakExtItem &customcloakext;

public:
	static bool created;

	CustomMethod(const Cloak::Engine* engine, const std::shared_ptr<ConfigTag>& tag, CustomCloakExtItem &ext) ATTR_NOT_NULL(2)
		: Cloak::Method(engine, tag)
		, customcloakext(ext)
	{
		created = true;
	}

	~CustomMethod()
	{
		created = false;
	}

	std::optional<Cloak::Info> Cloak(LocalUser* user) override ATTR_NOT_NULL(2)
	{
		if (!MatchesUser(user))
			return {}; // We shouldn't cloak this user.

		auto *cloak = customcloakext.Get(user);
		if (!cloak)
			return {}; // No custom cloak;

		return *cloak;
	}

	std::optional<Cloak::Info> Cloak(const std::string& hostip) override
	{
		// We can't generate custom cloaks without a user.
		return std::nullopt;
	}

	void GetLinkData(Module::LinkData& data) override
	{
		// We don't have any link data.
	}
};

bool CustomMethod::created = false;

class CustomEngine final
	: public Cloak::Engine
{
public:
	CustomCloakExtItem customcloakext;

	CustomEngine(const WeakModulePtr& mod)
		: Cloak::Engine(mod, "custom")
		, customcloakext(mod, *this)
	{
	}

	Cloak::MethodPtr Create(const std::shared_ptr<ConfigTag>& tag, bool primary) override
	{
		if (CustomMethod::created)
			throw ModuleException(this->service_creator, "You can only have one custom cloak method, at {}", tag->source.str());

		return std::make_shared<CustomMethod>(this, tag, customcloakext);
	}
};

class ModuleCloakCustom final
	: public Module
{
private:
	CommandCustomCloak cmdcustomcloak;
	CustomEngine customcloak;

public:
	ModuleCloakCustom()
		: Module(VF_VENDOR, "Adds the custom cloaking method for use with the cloak module.")
		, cmdcustomcloak(weak_from_this(), customcloak.customcloakext)
		, customcloak(weak_from_this())
	{
	}

	void ReadConfig(ConfigStatus& status) override
	{
		CloakAccounts newaccounts;
		for (const auto& [_, tag] : ServerInstance->Config->ConfTags("customcloak"))
		{
			const auto name = tag->getString("name");
			if (name.empty())
				throw ModuleException(weak_from_this(), "<customcloak:name> must not be empty, at " + tag->source.str());

			const auto password = tag->getString("password");
			if (password.empty())
				throw ModuleException(weak_from_this(), "<customcloak:password> must not be empty, at " + tag->source.str());

			Cloak::Info cloak(tag->getString("username"), tag->getString("hostname"));
			if (cloak.hostname.empty())
				throw ModuleException(weak_from_this(), "<customcloak:hostname> must not be empty, at " + tag->source.str());

			CloakAccount account(tag, cloak, password);
			if (!newaccounts.emplace(name, account).second)
				throw ModuleException(weak_from_this(), "<customcloak:name> (" + name + ") used in multiple tags, at " + tag->source.str());
		}
		std::swap(newaccounts, cmdcustomcloak.accounts);
	}
};

MODULE_INIT(ModuleCloakCustom)