/* * InspIRCd -- Internet Relay Chat Daemon * * Copyright (C) 2021 Herman * Copyright (C) 2019 linuxdaemon * Copyright (C) 2018 Matt Schatz * Copyright (C) 2016 Johanna A * Copyright (C) 2013-2016 Attila Molnar * Copyright (C) 2013, 2017, 2019-2022 Sadie Powell * Copyright (C) 2012 Robby * Copyright (C) 2009 Uli Schlachter * Copyright (C) 2009 Daniel De Graaf * Copyright (C) 2008-2009 Robin Burchell * Copyright (C) 2008 Pippijn van Steenhoven * Copyright (C) 2007 John Brooks * Copyright (C) 2007 Dennis Friis * Copyright (C) 2006-2008, 2010 Craig Edwards * * 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 . */ #include "inspircd.h" #include "modules/isupport.h" #include "modules/httpd.h" #include "xline.h" #include static ISupport::EventProvider* isevprov; namespace Stats { static const insp::flat_map 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::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 += ""; break; } } return ret; } class XMLSerializer final { private: std::stack blocks; std::stringstream data; public: XMLSerializer& Attribute(const char* name, const std::string& value) { if (value.empty()) data << '<' << name << "/>"; else data << '<' << name << '>' << Sanitize(value) << "'; return *this; } template std::enable_if_t, 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 << "'; 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); } 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->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("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->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 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("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 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; 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)