aboutsummaryrefslogtreecommitdiff
/*
 * InspIRCd -- Internet Relay Chat Daemon
 *
 *   Copyright (C) 2021 Herman <GermanAizek@yandex.ru>
 *   Copyright (C) 2019 linuxdaemon <linuxdaemon.irc@gmail.com>
 *   Copyright (C) 2018 Matt Schatz <genius3000@g3k.solutions>
 *   Copyright (C) 2016 Johanna A
 *   Copyright (C) 2013-2016 Attila Molnar <attilamolnar@hush.com>
 *   Copyright (C) 2013, 2017, 2019-2022 Sadie Powell <sadie@witchery.services>
 *   Copyright (C) 2012 Robby <robby@chatbelgie.be>
 *   Copyright (C) 2009 Uli Schlachter <psychon@inspircd.org>
 *   Copyright (C) 2009 Daniel De Graaf <danieldg@inspircd.org>
 *   Copyright (C) 2008-2009 Robin Burchell <robin+git@viroteck.net>
 *   Copyright (C) 2008 Pippijn van Steenhoven <pip88nl@gmail.com>
 *   Copyright (C) 2007 John Brooks <special@inspircd.org>
 *   Copyright (C) 2007 Dennis Friis <peavey@inspircd.org>
 *   Copyright (C) 2006-2008, 2010 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 "inspircd.h"
#include "modules/isupport.h"
#include "modules/httpd.h"
#include "xline.h"

#include <stack>

static ISupport::EventProvider* isevprov;

namespace Stats
{
	static const insp::flat_map<char, char const*> xmlentities = {
		{ '<', "lt"   },
		{ '>', "gt"   },
		{ '&', "amp"  },
		{ '"', "quot" },
	};

	std::string Sanitize(const std::string& str)
	{
		std::string ret;
		ret.reserve(str.length() * 2);

		for (const auto& chr : str)
		{
			insp::flat_map<char, char const*>::const_iterator it = xmlentities.find(chr);
			if (it != xmlentities.end())
			{
				ret += '&';
				ret += it->second;
				ret += ';';
			}
			else if (chr == 0x09 || chr == 0x0A || chr == 0x0D || ((chr >= 0x20) && (chr <= 0x7e)))
			{
				// The XML specification defines the following characters as valid inside an XML document:
				// Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
				ret += chr;
			}
			else
			{
				// If we reached this point then the string contains characters which can
				// not be represented in XML, even using a numeric escape. Therefore, we
				// Base64 encode the entire string and wrap it in a CDATA.
				ret.clear();
				ret += "<![CDATA[";
				ret += Base64::Encode(str);
				ret += "]]>";
				break;
			}
		}
		return ret;
	}

	class XMLSerializer final
	{
	private:
		std::stack<const char*> blocks;
		std::stringstream data;

	public:
		XMLSerializer& Attribute(const char* name, const std::string& value)
		{
			data << '<' << name << '>' << value << "</" << Sanitize(name) << '>';
			return *this;
		}

		template<typename Numeric>
		std::enable_if_t<std::is_arithmetic_v<Numeric>, XMLSerializer&> Attribute(const char* name, const Numeric& value)
		{
			return Attribute(name, ConvToStr(value));
		}

		std::stringstream* GetData() { return &data; }

		XMLSerializer& BeginBlock(const char* name)
		{
			blocks.push(name);
			data << '<' << name << '>';
			return *this;
		}

		XMLSerializer& EndBlock()
		{
			const char* name = blocks.top();
			data << "</" << name << '>';
			blocks.pop();
			return *this;
		}
	};

	void DumpMeta(XMLSerializer& serializer, Extensible* ext)
	{
		serializer.BeginBlock("metadata");
		for (const auto& [item, obj] : ext->GetExtList())
		{
			serializer.BeginBlock("meta")
				.Attribute("name", item->name);

			const std::string value = item->ToHuman(ext, obj);
			serializer.Attribute("value", value.empty() ? value.c_str() : nullptr);
		}
		serializer.EndBlock();
	}

	void ServerInfo(XMLSerializer& serializer)
	{
		serializer.BeginBlock("server")
			.Attribute("id", ServerInstance->Config->GetSID())
			.Attribute("name", ServerInstance->Config->ServerName)
			.Attribute("description", ServerInstance->Config->ServerDesc)
			.Attribute("customversion", ServerInstance->Config->CustomVersion)
			.Attribute("version", INSPIRCD_VERSION)
			.EndBlock();
	}

	void ISupport(XMLSerializer& serializer)
	{
		ISupport::TokenMap tokens;
		isevprov->Call(&ISupport::EventListener::OnBuildISupport, tokens);

		serializer.BeginBlock("isupport");
		for (const auto& [key, value] : tokens)
		{
			serializer.BeginBlock("token")
				.Attribute("name", key)
				.Attribute("value", value)
				.EndBlock();
		}
		serializer.EndBlock();
	}

	void General(XMLSerializer& serializer)
	{
		serializer.BeginBlock("general")
			.Attribute("usercount", ServerInstance->Users.GetUsers().size())
			.Attribute("localusercount", ServerInstance->Users.GetLocalUsers().size())
			.Attribute("channelcount", ServerInstance->Channels.GetChans().size())
			.Attribute("opercount", ServerInstance->Users.all_opers.size())
			.Attribute("socketcount", SocketEngine::GetUsedFds())
			.Attribute("socketmax", SocketEngine::GetMaxFds())
			.Attribute("boottime", ServerInstance->startup_time)
			.Attribute("currenttime", ServerInstance->Time());

		ISupport(serializer);
		serializer.EndBlock();
	}

	void XLines(XMLSerializer& serializer)
	{
		serializer.BeginBlock("xlines");
		for (const auto& xltype : ServerInstance->XLines->GetAllTypes())
		{
			XLineLookup* lookup = ServerInstance->XLines->GetAll(xltype);
			if (!lookup)
				continue;

			for (const auto& [_, xline] : *lookup)
			{
				serializer.BeginBlock("xline")
					.Attribute("type", xltype)
					.Attribute("mask", xline->Displayable())
					.Attribute("settime", xline->set_time)
					.Attribute("duration", xline->duration)
					.Attribute("reason", xline->reason)
					.EndBlock();
			}
		}
		serializer.EndBlock();
	}

	void Modules(XMLSerializer& serializer)
	{
		serializer.BeginBlock("modulelist");
		for (const auto& [modname, mod] : ServerInstance->Modules.GetModules())
		{
			serializer.BeginBlock("module")
				.Attribute("name", modname)
				.Attribute("description", mod->description)
				.EndBlock();
		}
		serializer.EndBlock();
	}

	void Channels(XMLSerializer& serializer)
	{
		serializer.BeginBlock("channellist");
		for (const auto& [_, c] : ServerInstance->Channels.GetChans())
		{
			serializer.BeginBlock("channel")
				.Attribute("channelname", c->name)
				.Attribute("usercount", c->GetUserCounter())
				.Attribute("channelmodes",c->ChanModes(true));

			if (!c->topic.empty())
			{
				serializer.BeginBlock("channeltopic")
					.Attribute("topictext", c->topic)
					.Attribute("setby", c->setby)
					.Attribute("settime", c->topicset)
					.EndBlock();
			}

			for (const auto& [__, memb] : c->GetUsers())
			{
				serializer.BeginBlock("channelmember")
					.Attribute("uid", memb->user->uuid)
					.Attribute("privs", memb->GetAllPrefixChars())
					.Attribute("modes", memb->GetAllPrefixModes());

				DumpMeta(serializer, memb);
				serializer.EndBlock();
			}

			DumpMeta(serializer, c);
			serializer.EndBlock();
		}
		serializer.EndBlock();
	}

	void DumpUser(XMLSerializer& serializer, User* u)
	{
		serializer.BeginBlock("user")
			.Attribute("nickname", u->nick)
			.Attribute("uuid", u->uuid)
			.Attribute("realhost", u->GetRealHost())
			.Attribute("displayhost", u->GetDisplayedHost())
			.Attribute("realname", u->GetRealName())
			.Attribute("server", u->server->GetName())
			.Attribute("signon", u->signon)
			.Attribute("age", u->age)
			.Attribute("modes", u->GetModeLetters().substr(1))
			.Attribute("ident", u->ident)
			.Attribute("ipaddress", u->GetIPString());

		if (u->IsAway())
		{
			serializer.Attribute("away", u->awaymsg)
				.Attribute("awaytime", u->awaytime);
		}

		if (u->IsOper())
			serializer.Attribute("opertype", u->oper->name);

		LocalUser* lu = IS_LOCAL(u);
		if (lu)
		{
			serializer.Attribute("port", lu->server_sa.port())
				.Attribute("servaddr", lu->server_sa.addr())
				.Attribute("connectclass", lu->GetClass()->GetName())
				.Attribute("lastmsg", lu->idle_lastmsg);
		}

		DumpMeta(serializer, u);
		serializer.EndBlock();
	}

	void Users(XMLSerializer& serializer)
	{
		serializer.BeginBlock("userlist");
		for (const auto& [_, u] : ServerInstance->Users.GetUsers())
		{
			if (u->registered != REG_ALL)
				continue;

			DumpUser(serializer, u);
		}
		serializer.EndBlock();
	}

	void Servers(XMLSerializer& serializer)
	{
		ProtocolInterface::ServerList sl;
		ServerInstance->PI->GetServerList(sl);

		serializer.BeginBlock("serverlist");
		for (const auto& server : sl)
		{
			serializer.BeginBlock("server")
				.Attribute("servername", server.servername)
				.Attribute("parentname", server.parentname)
				.Attribute("description", server.description)
				.Attribute("usercount", server.usercount)
				.Attribute("opercount", server.opercount)
				.Attribute("lagmillisecs", server.latencyms)
				.EndBlock();
		}
		serializer.EndBlock();
	}

	void Commands(XMLSerializer& serializer)
	{
		serializer.BeginBlock("commandlist");
		for (const auto& [cmdname, cmd] : ServerInstance->Parser.GetCommands())
		{
			serializer.BeginBlock("command")
				.Attribute("name", cmdname)
				.Attribute("usecount", cmd->use_count)
				.EndBlock();
		}
		serializer.EndBlock();
	}

	enum OrderBy
	{
		OB_NICK,
		OB_LASTMSG,

		OB_NONE
	};

	struct UserSorter final
	{
		OrderBy order;
		bool desc;

		UserSorter(OrderBy Order, bool Desc = false) : order(Order), desc(Desc) {}

		template <typename T>
		inline bool Compare(const T& a, const T& b)
		{
			return desc ? a > b : a < b;
		}

		bool operator()(User* u1, User* u2)
		{
			switch (order)
			{
				case OB_LASTMSG:
					return Compare(IS_LOCAL(u1)->idle_lastmsg, IS_LOCAL(u2)->idle_lastmsg);
				case OB_NICK:
					return Compare(u1->nick, u2->nick);
				default:
					return false;
			}
		}
	};

	void ListUsers(XMLSerializer& serializer, const HTTPQueryParameters& params)
	{
		if (params.empty())
		{
			Users(serializer);
			return;
		}

		serializer.BeginBlock("userlist");

		// Filters
		size_t limit = params.getNum<size_t>("limit");
		bool showunreg = params.getBool("showunreg");
		bool localonly = params.getBool("localonly");

		// Minimum time since a user's last message
		unsigned long min_idle = params.getDuration("minidle");
		time_t maxlastmsg = ServerInstance->Time() - min_idle;

		if (min_idle)
			// We can only check idle times on local users
			localonly = true;

		// Sorting
		const std::string& sortmethod = params.getString("sortby");
		bool desc = params.getBool("desc", false);

		OrderBy orderby;
		if (stdalgo::string::equalsci(sortmethod, "nick"))
			orderby = OB_NICK;
		else if (stdalgo::string::equalsci(sortmethod, "lastmsg"))
		{
			orderby = OB_LASTMSG;
			// We can only check idle times on local users
			localonly = true;
		}
		else
			orderby = OB_NONE;

		typedef std::list<User*> NewUserList;
		NewUserList user_list;
		for (const auto& [_, u] : ServerInstance->Users.GetUsers())
		{
			if (!showunreg && u->registered != REG_ALL)
				continue;

			LocalUser* lu = IS_LOCAL(u);
			if (localonly && !lu)
				continue;

			if (min_idle && lu->idle_lastmsg > maxlastmsg)
				continue;

			user_list.push_back(u);
		}

		UserSorter sorter(orderby, desc);
		if (sorter.order != OB_NONE && !(!localonly && sorter.order == OB_LASTMSG))
			user_list.sort(sorter);

		size_t count = 0;
		for (NewUserList::const_iterator i = user_list.begin(); i != user_list.end() && (!limit || count < limit); ++i, ++count)
			DumpUser(serializer, *i);

		serializer.EndBlock();
	}
}

class ModuleHttpStats final
	: public Module
	, public HTTPRequestEventListener
{
private:
	HTTPdAPI API;
	ISupport::EventProvider isupportevprov;
	bool enableparams = false;

public:
	ModuleHttpStats()
		: Module(VF_VENDOR, "Provides XML-serialised statistics about the server, channels, and users over HTTP via the /stats path.")
		, HTTPRequestEventListener(this)
		, API(this)
		, isupportevprov(this)
	{
		isevprov = &isupportevprov;
	}

	void ReadConfig(ConfigStatus& status) override
	{
		auto conf = ServerInstance->Config->ConfValue("httpstats");

		// Parameterized queries may cause a performance issue
		// Due to the sheer volume of data
		// So default them to disabled
		enableparams = conf->getBool("enableparams");
	}

	ModResult OnHTTPRequest(HTTPRequest& request) override
	{
		if (request.GetPath().compare(0, 6, "/stats"))
			return MOD_RES_PASSTHRU;

		ServerInstance->Logs.Debug(MODNAME, "Handling HTTP request for %s", request.GetPath().c_str());

		Stats::XMLSerializer serializer;
		serializer.BeginBlock("inspircdstats");
		if (request.GetPath() == "/stats")
		{
			Stats::ServerInfo(serializer);
			Stats::General(serializer);
			Stats::XLines(serializer);
			Stats::Modules(serializer);
			Stats::Channels(serializer);
			Stats::Users(serializer);
			Stats::Servers(serializer);
			Stats::Commands(serializer);
		}
		else if (request.GetPath() == "/stats/general")
		{
			Stats::General(serializer);
		}
		else if (request.GetPath() == "/stats/users")
		{
			if (enableparams)
				Stats::ListUsers(serializer, request.GetParsedURI().query_params);
			else
				Stats::Users(serializer);
		}
		else
		{
			return MOD_RES_PASSTHRU;
		}
		serializer.EndBlock();

		/* Send the document back to m_httpd */
		HTTPDocumentResponse response(this, request, serializer.GetData(), 200);
		response.headers.SetHeader("X-Powered-By", MODNAME);
		response.headers.SetHeader("Content-Type", "text/xml");
		API->SendResponse(response);
		return MOD_RES_DENY; // Handled
	}
};

MODULE_INIT(ModuleHttpStats)