aboutsummaryrefslogtreecommitdiffstats
path: root/modules/httpd_stats.cpp
diff options
context:
space:
mode:
authorGravatar Sadie Powell2024-07-17 00:06:50 +0100
committerGravatar Sadie Powell2024-07-17 00:06:50 +0100
commit889d521e05f2e922683d48c74eb00290e7a2f548 (patch)
treeb4c56bd4cdf0881601c7e4d6ff571e491af10bf4 /modules/httpd_stats.cpp
parentUse std::endian from C++20 in the sha1 module. (diff)
Shuffle the modules about a bit.
Diffstat (limited to 'modules/httpd_stats.cpp')
-rw-r--r--modules/httpd_stats.cpp506
1 files changed, 506 insertions, 0 deletions
diff --git a/modules/httpd_stats.cpp b/modules/httpd_stats.cpp
new file mode 100644
index 000000000..cc86c8830
--- /dev/null
+++ b/modules/httpd_stats.cpp
@@ -0,0 +1,506 @@
+/*
+ * InspIRCd -- Internet Relay Chat Daemon
+ *
+ * Copyright (C) 2019 linuxdaemon <linuxdaemon.irc@gmail.com>
+ * Copyright (C) 2016 Johanna A
+ * Copyright (C) 2013, 2020-2023 Sadie Powell <sadie@witchery.services>
+ * Copyright (C) 2013, 2015 Attila Molnar <attilamolnar@hush.com>
+ * Copyright (C) 2012 Robby <robby@chatbelgie.be>
+ * Copyright (C) 2009 Daniel De Graaf <danieldg@inspircd.org>
+ * Copyright (C) 2008 Robin Burchell <robin+git@viroteck.net>
+ * Copyright (C) 2008 Pippijn van Steenhoven <pip88nl@gmail.com>
+ * Copyright (C) 2007 John Brooks <john@jbrooks.io>
+ * Copyright (C) 2007 Dennis Friis <peavey@inspircd.org>
+ * Copyright (C) 2006-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 "inspircd.h"
+#include "extension.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)
+ {
+ if (value.empty())
+ data << '<' << name << "/>";
+ else
+ data << '<' << name << '>' << Sanitize(value) << "</" << 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)
+ .EndBlock();
+ }
+ serializer.EndBlock();
+ }
+
+ void ServerInfo(XMLSerializer& serializer)
+ {
+ serializer.BeginBlock("server")
+ .Attribute("id", ServerInstance->Config->ServerId)
+ .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->GetUsers().size())
+ .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("nickchanged", u->nickchanged)
+ .Attribute("modes", u->GetModeLetters().substr(1))
+ .Attribute("realuser", u->GetRealUser())
+ .Attribute("displayuser", u->GetDisplayedUser())
+ .Attribute("ipaddress", u->GetAddress());
+
+ if (u->IsAway())
+ {
+ serializer.Attribute("away", u->away->message)
+ .Attribute("awaytime", u->away->time);
+ }
+
+ if (u->IsOper())
+ serializer.Attribute("opertype", u->oper->GetType());
+
+ 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->IsFullyConnected())
+ 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 (insp::equalsci(sortmethod, "nick"))
+ orderby = OB_NICK;
+ else if (insp::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->IsFullyConnected())
+ 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;
+
+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;
+ }
+
+ ModResult OnHTTPRequest(HTTPRequest& request) override
+ {
+ if (request.GetPath().compare(0, 6, "/stats"))
+ return MOD_RES_PASSTHRU;
+
+ ServerInstance->Logs.Debug(MODNAME, "Handling HTTP request for {}", request.GetPath());
+
+ 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")
+ {
+ Stats::ListUsers(serializer, request.GetParsedURI().query_params);
+ }
+ 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)