aboutsummaryrefslogtreecommitdiffstats
/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2017-2026 Sadie Powell <sadie@sadiepowell.dev>
 *   Copyright (C) 2013-2016 Attila Molnar <attilamolnar@hush.com>
 *   Copyright (C) 2012 Robby <robby@chatbelgie.be>
 *   Copyright (C) 2009-2010 Daniel De Graaf <danieldg@inspircd.org>
 *   Copyright (C) 2008 Robin Burchell <robin+git@viroteck.net>
 *   Copyright (C) 2008 Craig Edwards <brain@inspircd.org>
 *
 * 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 "dynamic.h"
#include "modules/extban.h"
#include "utility/container.h"

#include "treeserver.h"
#include "utils.h"
#include "link.h"
#include "main.h"

struct CapabDiff final
{
	struct Config final
	{
		// The thing which differs.
		std::string what;

		// The value on the local server.
		std::optional<std::string> local;

		// The value on the remote server.
		std::optional<std::string> remote;
	};


	// Feature with different config on the local server vs the remote server.
	insp::casemapped_multimap<Config> config;

	// Feature which is not available on the local server.
	std::vector<std::string> localmissing;

	// Feature which is not available on the remote server.
	std::vector<std::string> remotemissing;

	operator bool() const
	{
		return config.empty() && localmissing.empty() && remotemissing.empty();
	}
};

namespace
{
	// A map which holds the difference between local and remote tokens.
	using TokenDiff = insp::casemapped_map<std::pair<std::optional<std::string>, std::optional<std::string>>>;

	// Builds a list of local capabilities.
	CapabData::CapabilityMap BuildCapabilityList(TreeSocket* ts)
	{
		CapabData::CapabilityMap capabilities = {
			{ "CASEMAPPING", ServerInstance->Config->CaseMapping                  },
			{ "MAXAWAY",     ConvToStr(ServerInstance->Config->Limits.MaxAway)    },
			{ "MAXCHANNEL",  ConvToStr(ServerInstance->Config->Limits.MaxChannel) },
			{ "MAXHOST",     ConvToStr(ServerInstance->Config->Limits.MaxHost)    },
			{ "MAXKEY",      ConvToStr(ServerInstance->Config->Limits.MaxKey)     },
			{ "MAXKICK",     ConvToStr(ServerInstance->Config->Limits.MaxKick)    },
			{ "MAXLINE",     ConvToStr(ServerInstance->Config->Limits.MaxLine)    },
			{ "MAXMODES",    ConvToStr(ServerInstance->Config->Limits.MaxModes)   },
			{ "MAXNICK",     ConvToStr(ServerInstance->Config->Limits.MaxNick)    },
			{ "MAXQUIT",     ConvToStr(ServerInstance->Config->Limits.MaxQuit)    },
			{ "MAXREAL",     ConvToStr(ServerInstance->Config->Limits.MaxReal)    },
			{ "MAXTOPIC",    ConvToStr(ServerInstance->Config->Limits.MaxTopic)   },
			{ "MAXUSER",     ConvToStr(ServerInstance->Config->Limits.MaxUser)    },
		};

		// If SHA256 hashing support is available then send a challenge token.
		if (ts->proto_version < PROTO_INSPIRCD_5 && ServerInstance->Modules.FindService("Hash::Provider", "sha256"))
		{
			if (ts->GetOurChallenge().empty())
				ts->SetOurChallenge(ServerInstance->GenRandomStr(20));
			capabilities["CHALLENGE"] = ts->GetOurChallenge();
		}

		ExtBan::ManagerRef extbanmgr(Utils->CreatorPtr);
		if (extbanmgr)
		{
			std::string& xbformat = capabilities["EXTBANFORMAT"];
			switch (extbanmgr->GetFormat())
			{
				case ExtBan::Format::ANY:
					xbformat = "any";
					break;
				case ExtBan::Format::NAME:
					xbformat = "name";
					break;
				case ExtBan::Format::LETTER:
					xbformat = "letter";
					break;
			}
		}
		return capabilities;
	}

	// Builds a list of the local extbans.
	CapabData::ExtBanMap BuildExtBanList()
	{
		CapabData::ExtBanMap extbans;
		ExtBan::ManagerRef extbanmgr(Utils->CreatorPtr);
		if (extbanmgr)
		{
			for (const auto& [_, extban] : extbanmgr->GetNameMap())
			{
				CapabData::ExtBanData data;
				data.name = extban->GetName();
				data.letter = extban->GetLetter();
				switch (extban->GetType())
				{
					case ExtBan::Type::ACTING:
						data.type = "acting";
						break;
					case ExtBan::Type::MATCHING:
						data.type = "matching";
						break;
				}
				extbans.emplace(data.name, std::move(data));
			}
		}
		return extbans;
	}

	// Builds a list of the local modes of the specified type.
	CapabData::ModeMap BuildModeList(ModeType mt, uint16_t protocol)
	{
		CapabData::ModeMap modes;
		for (const auto& [name, mh] : ServerInstance->Modes.GetModes(mt))
		{
			CapabData::ModeData data;
			data.letter = mh->GetModeChar();
			data.name = mh->GetName(protocol < PROTO_INSPIRCD_5);

			const auto* const pm = mh->IsPrefixMode();
			if (pm)
			{
				data.type = "prefix";
				data.prefixletter = pm->GetPrefix();
				data.prefixrank = pm->GetPrefixRank();
			}
			else if (mh->IsListMode())
				data.type = "list";
			else if (mh->NeedsParam(true))
				data.type = mh->NeedsParam(false) ? "param" : "param-set";
			else
				data.type = "simple";

			modes.emplace(data.name, std::move(data));
		}
		return modes;
	}

	// Builds a list of the local modules with the specified property.
	CapabData::ModuleMap BuildModuleList(ModuleFlags property, uint16_t protocol)
	{
		CapabData::ModuleMap modules;
		for (const auto& [name, module] : ServerInstance->Modules.GetModules())
		{
			if (!(module->properties & property))
				continue;

			// Replace m_foo.dylib with foo
			auto startpos = name.compare(0, 2, "m_", 2) ? 0 : 2;
			auto endpos = name.length() - strlen(INSPIRCD_MODULE_EXT);

			auto modname = name.substr(startpos, endpos - startpos);

			if (protocol < PROTO_INSPIRCD_5)
			{
				// BEGIN COMPATIBILITY CODE
				if (insp::casemapped_equals(modname, "sethost") || insp::casemapped_equals(modname, "setident") || insp::casemapped_equals(modname, "setname"))
				{
					// These modules were combined in v5.
					modname.replace(0, 3, "chg");
				}
				else if (insp::casemapped_equals(modname, "setidle"))
				{
					// Not VF_OPTCOMMON in v4.
					continue;
				}
				else if (insp::casemapped_equals(modname, "sacommands"))
				{
					// These modules were combined in v5.
					modules["sajoin"] = modules["sakick"] = modules["sanick"] =
						modules["sapart"] = modules["saquit"];
					continue;
				}
				// END COMPATIBILITY CODE.
			}
			module->GetLinkData(modules[modname]);
		}
		return modules;
	}

	bool CompareCapabilities(const CapabData::CapabilityMap& remote, TreeSocket* ts,
		CapabDiff& diff)
	{
		// For capabilities we only compare the common keys so we can add new
		// tokens later without breaking compatibility.
		for (const auto& [tname, tvalue] : BuildCapabilityList(ts))
		{
			auto it = remote.find(tname);
			if (it != remote.end() && it->second != tvalue)
			{
				diff.config.emplace("", CapabDiff::Config {
					.what = tname,
					.local = tvalue,
					.remote = it->second,
				});
			}
		}
		return diff;
	}

	// Compares the lists of extbans on a remote server to the local server.
	bool CompareExtBans(std::optional<CapabData::ExtBanMap>& remote, CapabDiff& diff)
	{
		// If the remote didn't send an extban list then don't compare.
		if (!remote)
			return true;

		auto local = BuildExtBanList();
		for (const auto& [_, data] : *remote)
		{
			auto extbaniter = local.find(data.name);
			if (extbaniter == local.end())
			{
				// Only exists on the remote server.
				diff.localmissing.push_back(FMT::format("{} ({}:)", data.name, data.letter));
				continue;
			}

			// Check that the extban config is the same.
			if (extbaniter->second.letter != data.letter)
			{
				diff.config.emplace(data.name, CapabDiff::Config {
					.what = "extban character",
					.local = FMT::format("+{}", extbaniter->second.letter),
					.remote = FMT::format("+{}", data.letter),
				});
			}

			if (!insp::casemapped_equals(extbaniter->second.type, data.type))
			{
				diff.config.emplace(data.name, CapabDiff::Config {
					.what = "extban type",
					.local = extbaniter->second.type,
					.remote = data.type,
				});
			}

			local.erase(extbaniter);
		}

		for (const auto& [_, data] : local)
		{
			// Only exists on the local server.
			diff.remotemissing.push_back(FMT::format("{} ({}:)", data.name, data.letter));
		}

		return diff;
	}

	// Compares the mode data sent by a remote server to that of the local server.
	void CompareModeData(const CapabData::ModeData& data, const CapabData::ModeData& otherdata,
		CapabDiff& diff)
	{
		if (data.letter != otherdata.letter)
		{
			diff.config.emplace(data.name, CapabDiff::Config {
				.what = "mode character",
				.local = FMT::format("+{}", data.letter),
				.remote = FMT::format("+{}", otherdata.letter),
			});
		}

		if (!insp::casemapped_equals(data.type, otherdata.type))
		{
			diff.config.emplace(data.name, CapabDiff::Config {
				.what = "mode type",
				.local = data.type,
				.remote = otherdata.type,
			});
		}
		else if (insp::casemapped_equals(data.type, "prefix"))
		{
			if (data.prefixletter != otherdata.prefixletter)
			{
				diff.config.emplace(data.name, CapabDiff::Config {
					.what = "prefix character",
					.local = data.prefixletter ? ConvToStr(data.prefixletter) : "",
					.remote = otherdata.prefixletter ? ConvToStr(otherdata.prefixletter) : "",
				});
			}

			if (data.prefixrank != otherdata.prefixrank)
			{
				diff.config.emplace(data.name, CapabDiff::Config {
					.what = "prefix rank",
					.local = ConvToStr(data.prefixrank),
					.remote = ConvToStr(otherdata.prefixrank),
				});
			}
		}
	}

	// Compares the lists of module on a remote server to the local server.
	bool CompareModes(ModeType mt, std::optional<CapabData::ModeMap>& remote, uint16_t protocol,
		CapabDiff& diff)
	{
		// If the remote didn't send a mode list then don't compare.
		if (!remote)
			return true;

		auto local = BuildModeList(mt, protocol);
		for (const auto& [_, data] : *remote)
		{
			auto modeiter = local.find(data.name);
			if (modeiter == local.end())
			{
				// Only exists on the remote server.
				diff.localmissing.push_back(FMT::format("{} (+{})", data.name, data.letter));
				continue;
			}

			// Check that the mode config is the same.
			CompareModeData(modeiter->second, data, diff);
			local.erase(modeiter);
		}

		for (const auto& [_, data] : local)
		{
			// Only exists on the local server.
			diff.remotemissing.push_back(FMT::format("{} (+{})", data.name, data.letter));
		}

		return diff;
	}

	// Compares the module data sent by a remote server to that of the local server.
	void CompareModuleData(const ModulePtr& mod, const Module::LinkData& otherdata,
		CapabDiff& diff)
	{
		Module::LinkDataDiff datadiff;
		mod->CompareLinkData(otherdata, datadiff);
		if (datadiff.empty())
			return;

		const auto modname = ModuleManager::ShrinkModName(mod->ModuleFile);
		for (const auto& [key, values] : datadiff)
		{
			diff.config.emplace(modname, CapabDiff::Config {
				.what = key,
				.local = values.first,
				.remote = values.second,
			});
		}
	}

	// Compares the lists of module on a remote server to the local server.
	bool CompareModules(ModuleFlags property, std::optional<CapabData::ModuleMap>& remote,
		CapabDiff& diff)
	{
		// If the remote didn't send a module list then don't compare.
		if (!remote)
			return true;

		// Retrieve the local module list.
		ModuleManager::ModuleMap local;
		for (const auto& [name, module] : ServerInstance->Modules.GetModules())
		{
			if (module->properties & property)
				local[ModuleManager::ShrinkModName(name)] = module;
		}

		for (const auto& [name, otherdata] : *remote)
		{
			auto moditer = local.find(name);
			if (moditer == local.end())
			{
				// Only exists on the remote server.
				diff.localmissing.push_back(name);
				continue;
			}

			// Parse and compare the link data.
			CompareModuleData(moditer->second, otherdata, diff);
			local.erase(moditer);
		}

		for (const auto& [name, _] : local)
		{
			// Only exists on the local server.
			diff.remotemissing.push_back(name);
		}

		return diff;
	}

	// Generates a capability list in the format "FOO=BAR BAZ=BAX".
	std::string FormatCapabilities(TreeSocket* ts)
	{
		auto first = true;
		std::stringstream capabilitystr;
		for (const auto& [capkey, capvalue] : BuildCapabilityList(ts))
		{
			if (!first)
				capabilitystr << ' ';
			capabilitystr << capkey << '=' << capvalue;
			first = false;
		}
		return capabilitystr.str();
	}

	// Generates an extban list in the format "acting:foo=a matching:bar=b".
	std::string FormatExtBans()
	{
		std::ostringstream extbans;
		for (const auto& [_, data] : BuildExtBanList())
		{
			extbans << data.type << ":" << data.name;
			if (data.letter)
				extbans << '=' << data.letter;
			extbans << ' ';
		}
		return extbans.str();
	}

	// Generates a mode list in the format "simple:foo=b prefix:123:bar=c?".
	std::string FormatModes(ModeType mt, uint16_t protocol)
	{
		std::ostringstream modes;
		for (const auto& [_, data] : BuildModeList(mt, protocol))
		{
			modes << data.type << ':';
			if (insp::casemapped_equals(data.type, "prefix"))
				modes << data.prefixrank << ':';
			modes << data.name << '=';
			if (data.prefixletter)
				modes << data.prefixletter;
			modes << data.letter << ' ';
		}
		return modes.str();
	}

	// Generates a module list in the format "m_foo.so=bar m_bar.so=baz".
	std::string FormatModules(ModuleFlags property, uint16_t protocol)
	{
		std::ostringstream modules;
		for (const auto& [module, linkdata] : BuildModuleList(property, protocol))
		{
			modules << module;
			if (!linkdata.empty())
				modules << '=' << Percent::EncodeQuery(linkdata);
			modules << ' ';
		}
		return modules.str();
	}

	void HandleDiff(TreeSocket* ts, const std::string& what, const CapabDiff& diff, bool fatal)
	{
		if (fatal)
		{
			ServerInstance->SNO.WriteToSnoMask('l', "CAPAB negotiation mismatch on link {}: {} do not match with the remote server. You will not be able to link this server until this issue is resolved.",
				ts->GetLinkName(), what);
		}
		else
			ServerInstance->SNO.WriteToSnoMask('l', "CAPAB negotiation mismatch on link {}: {} do not match with the remote server. Some functionality may behave inconsistently between servers.",
				ts->GetLinkName(), what);

		if (!diff.localmissing.empty())
			ServerInstance->SNO.WriteToSnoMask('l', "Missing on the local server: {}", insp::join(diff.localmissing));
		if (!diff.remotemissing.empty())
			ServerInstance->SNO.WriteToSnoMask('l', "Missing on the remote server: {}", insp::join(diff.remotemissing));
		if (!diff.config.empty())
		{
			ServerInstance->SNO.WriteToSnoMask('l', "Different config between the local and remote servers:");
			for (const auto& [feature, confdiff] : diff.config)
			{
				const auto localstate = confdiff.local
					? confdiff.local->empty() ? "set" : FMT::format("set to {}", *confdiff.local)
					: "not set";

				const auto remotestate = confdiff.remote
					? confdiff.remote->empty() ? "set" : FMT::format("set to {}", *confdiff.remote)
					: "not set";

				ServerInstance->SNO.WriteToSnoMask('l', "  {}{}{} {} on the local server and {} on the remote server.", feature, feature.empty() ? "" : ": ",
					confdiff.what, localstate, remotestate);
			}
		}
	}

	template <typename... Args>
	bool HandleCapabError(TreeSocket* ts, const char* format, Args&&... args)
	{
		const auto message = FMT::vformat(format, FMT::make_format_args(args...));
		ts->SendError(FMT::format("CAPAB negotiation failed: {}. See the log on {} or snomasks +Ll for more details.",
			message, ServerInstance->Config->ServerName));
		return false;
	}

	// Handles a mismatch between servers during CAPAB negotiation.
	bool HandleMismatch(TreeSocket* ts, const std::string& what, const CapabDiff& diff)
	{
		HandleDiff(ts, what, diff, !Utils->AllowMismatch);
		if (!Utils->AllowMismatch)
			return HandleCapabError(ts, "{} do not match and <spanningtree:allowmismatch> is not enabled", what);
		return true;
	}

	// Handles a fatal mismatch between servers during CAPAB negotiation.
	bool HandleMismatchFatal(TreeSocket* ts, const std::string& what, const CapabDiff& diff)
	{
		HandleDiff(ts, what, diff, true);
		return HandleCapabError(ts, "{} do not match", what);
	}

	// Parses a capability list in the format "FOO BAR=baz".
	void ParseCapabilities(const std::string& caplist, CapabData::CapabilityMap& map, TreeSocket* ts)
	{
		StringSplitter capstream(caplist);
		for (std::string cap; capstream.GetToken(cap); )
		{
			std::string capval;
			const auto split = cap.find('=');
			if (split != std::string::npos)
			{
				capval.assign(cap, split + 1);
				cap.erase(split);
			}

			// BEGIN COMPATIBILITY CODE
			if (ts->proto_version < PROTO_INSPIRCD_5 && insp::casemapped_equals(cap, "CHALLENGE"))
			{
				ts->SetTheirChallenge(capval);
				continue;
			}
			// END COMPATIBILITY CODE

			ServerInstance->Logs.Debug(MODNAME, "Parsed capability: {} {}", cap, capval);
			map.emplace(cap, capval);
		}
	}

	// Parses a challenge in the format "<algo> [<algo>]+ :<challenge>".
	void ParseChallenge(const CommandBase::Params& params, std::string& out)
	{
		for (const auto& algorithm : insp::iterator_range(params.begin() + 1, params.end() - 1))
		{
			// For now we only support HMAC-SHA-256 here.
			if (insp::casemapped_equals(algorithm, "hmac-sha256"))
			{
				out = Percent::Decode(params.back());
				ServerInstance->Logs.Debug(MODNAME, "Parsed challenge: {:?}", out);
				break;
			}
		}
	}

	// Parses an extban list in the format "type:name[=char]".
	void ParseExtBans(const std::string& extbanlist, std::optional<CapabData::ExtBanMap>& out)
	{
		auto& map = out ? *out : out.emplace();
		StringSplitter extbanstream(extbanlist);
		for (std::string extban; extbanstream.GetToken(extban); )
		{
			CapabData::ExtBanData data;

			// matching:mute=m acting:noctcp
			//         A    B        A
			const auto a = extban.find(':');
			if (a == std::string::npos)
				continue; // Malformed.

			const auto b = extban.find('=', a + 1);
			if (b == std::string::npos)
			{
				// ExtBan only has a name.
				data.name = extban.substr(a + 1);
			}
			else
			{
				// ExtBan has a name and letter.
				data.name = extban.substr(a + 1, b - a - 1);
				data.letter = extban[b + 1];
			}
			data.type = extban.substr(0, a);

			ServerInstance->Logs.Debug(MODNAME, "Parsed extban: type {} name {} letter {:?}",
				data.type, data.name, data.letter);

			map.emplace(data.name, std::move(data));
		}
	}

	// Parses a mode list in the format "type:[rank:]name=[prefixchar][char]".
	void ParseModes(const std::string& modelist, std::optional<CapabData::ModeMap>& out)
	{
		auto& map = out ? *out : out.emplace();
		StringSplitter modestream(modelist);
		for (std::string mode; modestream.GetToken(mode); )
		{
			CapabData::ModeData data;

			// list:ban=b  param-set:limit=l  param:key=k  prefix:30000:op=@o  simple:noextmsg=n
			//     A   C            A     C        A   C         A     B  C          A        C
			auto a = mode.find(':');
			if (a == std::string::npos)
				continue; // Malformed.

			// If the mode is a prefix mode then it also has a rank.
			data.type = mode.substr(0, a);
			if (insp::casemapped_equals(data.type, "prefix"))
			{
				const auto b = mode.find(':', a + 1);
				if (b == std::string::npos)
					continue; // Malformed.

				const auto rank = mode.substr(a + 1, b - a - 1);
				data.prefixrank = ConvToNum<ModeHandler::Rank>(rank);
				a = b;
			}

			const auto c = mode.find('=', a + 1);
			if (c == std::string::npos)
				continue; // Malformed.

			data.name = mode.substr(a + 1, c - a - 1);
			switch (mode.length() - c)
			{
				case 2:
					data.letter = mode[c + 1];
					break;
				case 3:
					data.prefixletter = mode[c + 1];
					data.letter = mode[c + 2];
					break;
				default:
					continue; // Malformed.
			}

			ServerInstance->Logs.Debug(MODNAME, "Parsed mode: type {} name {} letter {} prefixrank {} prefixletter {:?}",
				data.type, data.name, data.letter, data.prefixrank, data.prefixletter);

			map.emplace(data.name, std::move(data));
		}
	}

	// Parses a module list in the format "m_foo.so=bar m_bar.so=baz" to a map.
	void ParseModules(const std::string& modlist, std::optional<CapabData::ModuleMap>& out)
	{
		CapabData::ModuleMap& map = out ? *out : out.emplace();
		StringSplitter modstream(modlist);
		for (std::string mod; modstream.GetToken(mod); )
		{
			const auto split = mod.find('=');
			if (split == std::string::npos)
				map.emplace(mod, Module::LinkData()); // No link data.
			else
			{
				const auto linkdata = Percent::DecodeQuery(mod.substr(split + 1));
				map.emplace(mod.substr(0, split), linkdata);
			}
		}
	}
}

void TreeSocket::SendCapabilities(int phase)
{
	if (capab->capab_phase >= phase)
		return;

	if (capab->capab_phase < 1 && phase >= 1)
	{
		MessageBuilder("CAPAB", true)
			.Push("START", (uint16_t)PROTO_NEWEST)
			.Unicast(this);
	}

	capab->capab_phase = phase;
	if (phase < 2)
		return;


	std::vector<std::string> algorithms;
	if (proto_version >= PROTO_INSPIRCD_5)
	{
		std::vector<char> challenge(32);
		ServerInstance->GenRandom(challenge.data(), challenge.size());
		SetOurChallenge(std::string(challenge.begin(), challenge.end()));

		MessageBuilder("CAPAB", true)
			.Push("CHALLENGE")
			.Push("hmac-sha256", Percent::Encode(GetOurChallenge()))
			.Unicast(this);
	}

	MessageBuilder("CAPAB", true)
		.Push("CAPABILITIES", FormatCapabilities(this))
		.Unicast(this);
	MessageBuilder("CAPAB", true)
		.Push("MODULES", FormatModules(VF_COMMON, proto_version))
		.Unicast(this);
	MessageBuilder("CAPAB", true)
		.Push("MODSUPPORT", FormatModules(VF_OPTCOMMON, proto_version))
		.Unicast(this);
	MessageBuilder("CAPAB", true)
		.Push("CHANMODES", FormatModes(MODETYPE_CHANNEL, proto_version))
		.Unicast(this);
	MessageBuilder("CAPAB", true)
		.Push("USERMODES", FormatModes(MODETYPE_USER, proto_version))
		.Unicast(this);
	MessageBuilder("CAPAB", true)
		.Push("EXTBANS", FormatExtBans())
		.Unicast(this);
	MessageBuilder("CAPAB", true)
		.Push("END")
		.Unicast(this);
}

/* Isolate and return the elements that are different between two comma separated lists */
void TreeSocket::ListDifference(const std::string& one, const std::string& two, char sep,
		std::string& mleft, std::string& mright)
{
	std::set<std::string> values;
	StringSplitter sepleft(one, sep);
	StringSplitter sepright(two, sep);
	std::string item;
	while (sepleft.GetToken(item))
	{
		values.insert(item);
	}
	while (sepright.GetToken(item))
	{
		if (!values.erase(item))
		{
			mright.push_back(sep);
			mright.append(item);
		}
	}
	for (const auto& value : values)
	{
		mleft.push_back(sep);
		mleft.append(value);
	}
}

bool TreeSocket::Capab(const CommandBase::Params& params)
{
	if (params.empty())
		return HandleCapabError(this, "Remote server did not send a subcommand in CAPAB");

	if (insp::casemapped_equals(params[0], "START"))
	{
		if (params.size() < 2)
			return HandleCapabError(this, "Remote server did not send a protocol version in CAPAB START");

		capab->capabilities.clear();
		capab->channelmodes.reset();
		capab->extbans.reset();
		capab->optionalmodules.reset();
		capab->requiredmodules.reset();
		capab->usermodes.reset();

		proto_version = ConvToNum<uint16_t>(params[1]);
		if (proto_version < PROTO_OLDEST)
		{
			return HandleCapabError(this, "Remote server is using protocol version {} which is too old to link with this server (protocol versions {} to {} are supported)",
				proto_version, (uint16_t)PROTO_OLDEST, (uint16_t)PROTO_NEWEST);
		}

		SendCapabilities(2);
	}
	else if (insp::casemapped_equals(params[0], "END"))
	{
		CapabDiff diff;
		if (!CompareModules(VF_COMMON, this->capab->requiredmodules, diff))
			return HandleMismatchFatal(this, "Required modules", diff);

		else if (!CompareModes(MODETYPE_CHANNEL, this->capab->channelmodes, this->proto_version, diff))
			return HandleMismatchFatal(this, "Channel modes", diff);

		else if (!CompareModes(MODETYPE_USER, this->capab->usermodes, this->proto_version, diff))
			return HandleMismatchFatal(this, "User modes", diff);

		else if (!CompareModules(VF_OPTCOMMON, this->capab->optionalmodules, diff))
		{
			if (!HandleMismatch(this, "Optional modules", diff))
				return false;
		}

		else if (!CompareCapabilities(this->capab->capabilities, this, diff))
		{
			if (!HandleMismatch(this, "Capabilities", diff))
				return false;
		}

		else if (!CompareExtBans(this->capab->extbans, diff))
		{
			if (!HandleMismatch(this, "Extended bans", diff))
				return false;
		}

		if (this->LinkState == CONNECTING)
		{
			this->SendCapabilities(2);
			MessageBuilder("SERVER", true)
				.Push(ServerInstance->Config->ServerName,
					MakePass(capab->link->SendPass, capab->theirchallenge),
					ServerInstance->Config->ServerId,
					ServerInstance->Config->ServerDesc)
				.Unicast(this);
		}
	}
	else if (insp::casemapped_equals(params[0], "CHALLENGE"))
	{
		if (params.size() >= 3)
			ParseChallenge(params, capab->theirchallenge);
	}
	else if (insp::casemapped_equals(params[0], "CAPABILITIES"))
	{
		if (params.size() >= 2)
			ParseCapabilities(params[1], capab->capabilities, this);
	}
	else if (insp::casemapped_equals(params[0] , "MODULES"))
	{
		if (params.size() >= 2)
			ParseModules(params[1], capab->requiredmodules);
	}
	else if (insp::casemapped_equals(params[0], "MODSUPPORT"))
	{
		if (params.size() >= 2)
			ParseModules(params[1], capab->optionalmodules);
	}
	else if (insp::casemapped_equals(params[0], "CHANMODES"))
	{
		if (params.size() >= 2)
			ParseModes(params[1], capab->channelmodes);
	}
	else if (insp::casemapped_equals(params[0], "USERMODES"))
	{
		if (params.size() >= 2)
			ParseModes(params[1], capab->usermodes);
	}
	else if (insp::casemapped_equals(params[0], "EXTBANS"))
	{
		if (params.size() >= 2)
			ParseExtBans(params[1], capab->extbans);
	}
	return true;
}