From 1d5e9afc425581fa2bd44e22dec5ece5ab10f0e1 Mon Sep 17 00:00:00 2001 From: Jean Chevronnet Date: Wed, 3 Jul 2024 11:05:40 +0200 Subject: [PATCH 01/25] add new module for profiles link in /whois response. --- 4/m_profileLink.cpp | 73 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 4/m_profileLink.cpp diff --git a/4/m_profileLink.cpp b/4/m_profileLink.cpp new file mode 100644 index 00000000..19f70feb --- /dev/null +++ b/4/m_profileLink.cpp @@ -0,0 +1,73 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2021 Sadie Powell + * + * 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 . + */ + +/// $ModAuthor: Jean Chevronnet +/// $ModDepends: core 4 +/// $ModDesc: Adds a profile link to the WHOIS response for registered users, ignoring services, bots. + +#include "inspircd.h" +#include "modules/whois.h" +#include "modules/account.h" + +enum +{ + // Define a custom WHOIS numeric reply for the profile link. + RPL_WHOISPROFILE = 320, +}; + +class ModuleProfileLink final + : public Module + , public Whois::EventListener +{ +private: + Account::API accountapi; + +public: + ModuleProfileLink() + : Module(VF_OPTCOMMON, "Adds a profile link to the WHOIS response for registered users, ignoring services, bots, and ulines.") + , Whois::EventListener(this) + , accountapi(this) + { + } + + void OnWhois(Whois::Context& whois) override + { + User* target = whois.GetTarget(); + + // Skip services, bots. + if (target->server->IsService() || target->IsModeSet('B')) + return; + + // Check if the user has an account name (is registered). + const std::string* account = accountapi->GetAccountName(target); + if (account) + { + // Construct the profile URL using the user's account name. + const std::string profileUrl = "https://www.reseau-entrenous.fr/profil/" + *account; + // Send the profile URL in the WHOIS response. + whois.SendLine(RPL_WHOISPROFILE, target->nick, "Profil: " + profileUrl); + } + else + { + // Indicate that the account is not registered. + whois.SendLine(RPL_WHOISPROFILE, target->nick, "Profil: L'utilisateur n'est pas connecté ou le compte n'est pas enregistré."); + } + } +}; + +MODULE_INIT(ModuleProfileLink) From 94416908d5fe755c0713a70d396e36982b3347e5 Mon Sep 17 00:00:00 2001 From: Jean Chevronnet Date: Wed, 3 Jul 2024 11:07:51 +0200 Subject: [PATCH 02/25] remove ulines thing --- 4/m_profileLink.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/4/m_profileLink.cpp b/4/m_profileLink.cpp index 19f70feb..794ccdbc 100644 --- a/4/m_profileLink.cpp +++ b/4/m_profileLink.cpp @@ -39,7 +39,7 @@ class ModuleProfileLink final public: ModuleProfileLink() - : Module(VF_OPTCOMMON, "Adds a profile link to the WHOIS response for registered users, ignoring services, bots, and ulines.") + : Module(VF_OPTCOMMON, "Adds a profile link to the WHOIS response for registered users, ignoring services, bots.") , Whois::EventListener(this) , accountapi(this) { From 1907dfe1c88be2ce070396261d64cc447fe08201 Mon Sep 17 00:00:00 2001 From: revrse <129334336+revrsedev@users.noreply.github.com> Date: Fri, 5 Jul 2024 11:48:30 +0200 Subject: [PATCH 03/25] Create m_ipinfo_io.cpp --- 4/m_ipinfo_io.cpp | 189 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 4/m_ipinfo_io.cpp diff --git a/4/m_ipinfo_io.cpp b/4/m_ipinfo_io.cpp new file mode 100644 index 00000000..186ebc54 --- /dev/null +++ b/4/m_ipinfo_io.cpp @@ -0,0 +1,189 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2024 Jean Chevronnet + * +* This file contains a third party module for InspIRCd. 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 . + */ + +/// $ModAuthor: Jean Chevronnet (reverse) +/// $ModDesc: Ip information from Ipinfo.io in /WHOIS (only irc operators), found more information at https://ipinfo.io/developers. +/// $ModDepends: core 4 +/// $ModConfig: +/// $CompilerFlags: find_compiler_flags("RapidJSON") +/// $CompilerFlags: find_compiler_flags("libcurl") +/// $LinkerFlags: find_linker_flags("libcurl") + +#include "inspircd.h" +#include "extension.h" +#include "modules/httpd.h" +#include "modules/stats.h" +#include "modules/whois.h" +#include +#include +#include +#include +#include + +enum +{ + // deff custom numeric for this information in whois + RPL_WHOISIPINFO = 695, +}; + +class IPInfoResolver +{ +private: + std::thread worker; + std::mutex mtx; + StringExtItem& cachedinfo; + std::string apikey; + std::string theiruuid; + + static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) + { + s->append(static_cast(contents), size * nmemb); + return size * nmemb; + } + + void DoRequest(LocalUser* user) + { + CURL* curl; + CURLcode res; + std::string response; + + curl_global_init(CURL_GLOBAL_DEFAULT); + curl = curl_easy_init(); + if (curl) + { + std::string url = "http://ipinfo.io/" + user->client_sa.addr() + "?token=" + apikey; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + std::lock_guard lock(mtx); + ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to get data for %s: %s", user->nick.c_str(), curl_easy_strerror(res)); + } + else + { + ParseResponse(user, response); + } + curl_easy_cleanup(curl); + } + curl_global_cleanup(); + } + + void ParseResponse(LocalUser* user, const std::string& response) + { + rapidjson::Document document; + if (document.Parse(response.c_str()).HasParseError()) + { + std::lock_guard lock(mtx); + ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for %s: %s", user->nick.c_str(), rapidjson::GetParseError_En(document.GetParseError())); + return; + } + + std::string city = document.HasMember("city") ? document["city"].GetString() : "Unknown"; + std::string region = document.HasMember("region") ? document["region"].GetString() : "Unknown"; + std::string country = document.HasMember("country") ? document["country"].GetString() : "Unknown"; + std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; + + std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; + cachedinfo.Set(user, info); + + std::lock_guard lock(mtx); + user->WriteNumeric(RPL_WHOISIPINFO, user->nick, "hes/her ip information: " + info); + } + +public: + IPInfoResolver(Module* Creator, LocalUser* user, StringExtItem& cache, const std::string& key) + : cachedinfo(cache), apikey(key), theiruuid(user->uuid) + { + worker = std::thread(&IPInfoResolver::DoRequest, this, user); + } + + ~IPInfoResolver() + { + if (worker.joinable()) + { + worker.join(); + } + } +}; + +class ModuleIPInfo : public Module, public Stats::EventListener, public Whois::EventListener +{ +private: + StringExtItem cachedinfo; + std::string apikey; + +public: + ModuleIPInfo() + : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") + , Stats::EventListener(this) + , Whois::EventListener(this) + , cachedinfo(this, "ipinfo", ExtensionType::USER) + { + } + + void ReadConfig(ConfigStatus& status) override + { + auto& tag = ServerInstance->Config->ConfValue("ipinfo"); + apikey = tag->getString("apikey", "", 1); + + const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); + for (const auto& user : users) + { + cachedinfo.Unset(user); + } + } + + void OnWhois(Whois::Context& whois) override + { + User* target = whois.GetTarget(); + + if (target->server->IsService() || target->IsModeSet('B')) + return; + + if (!whois.GetSource()->IsOper()) + return; + + const std::string* cached = cachedinfo.Get(target); + if (cached) + { + whois.SendLine(RPL_WHOISIPINFO, "hes/her ip information(cached): " + *cached); + } + else + { + new IPInfoResolver(this, IS_LOCAL(target), cachedinfo, apikey); + } + } + + ModResult OnStats(Stats::Context& stats) override + { + return MOD_RES_PASSTHRU; + } + + void OnUnloadModule(Module* mod) override + { + const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); + for (const auto& user : users) + { + cachedinfo.Unset(user); + } + } +}; + +MODULE_INIT(ModuleIPInfo) From a48d250aa4d26c7d1e8cf6f543403e3c405125d2 Mon Sep 17 00:00:00 2001 From: revrse <129334336+revrsedev@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:31:01 +0200 Subject: [PATCH 04/25] Update m_ipinfo_io.cpp Changes after https://github.com/inspircd/inspircd-contrib/pull/276. --- 4/m_ipinfo_io.cpp | 97 ++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/4/m_ipinfo_io.cpp b/4/m_ipinfo_io.cpp index 186ebc54..8105cb67 100644 --- a/4/m_ipinfo_io.cpp +++ b/4/m_ipinfo_io.cpp @@ -3,7 +3,7 @@ * * Copyright (C) 2024 Jean Chevronnet * -* This file contains a third party module for InspIRCd. You can + * This file contains a third party module for InspIRCd. 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. * @@ -27,28 +27,28 @@ #include "inspircd.h" #include "extension.h" #include "modules/httpd.h" -#include "modules/stats.h" #include "modules/whois.h" +#include "thread.h" #include #include #include -#include #include +#include enum { - // deff custom numeric for this information in whois + // Define a custom WHOIS numeric reply for the IP info. RPL_WHOISIPINFO = 695, }; -class IPInfoResolver +class IPInfoResolver : public Thread { private: - std::thread worker; std::mutex mtx; StringExtItem& cachedinfo; std::string apikey; std::string theiruuid; + LocalUser* user; static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) { @@ -56,7 +56,7 @@ class IPInfoResolver return size * nmemb; } - void DoRequest(LocalUser* user) + void OnStart() override { CURL* curl; CURLcode res; @@ -66,7 +66,7 @@ class IPInfoResolver curl = curl_easy_init(); if (curl) { - std::string url = "http://ipinfo.io/" + user->client_sa.addr() + "?token=" + apikey; + std::string url = "https://ipinfo.io/" + user->client_sa.addr() + "?token=" + apikey; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); @@ -85,13 +85,13 @@ class IPInfoResolver curl_global_cleanup(); } - void ParseResponse(LocalUser* user, const std::string& response) + void ParseResponse(LocalUser* localuser, const std::string& response) { rapidjson::Document document; if (document.Parse(response.c_str()).HasParseError()) { std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for %s: %s", user->nick.c_str(), rapidjson::GetParseError_En(document.GetParseError())); + ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for %s: %s", localuser->nick.c_str(), rapidjson::GetParseError_En(document.GetParseError())); return; } @@ -101,38 +101,59 @@ class IPInfoResolver std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; - cachedinfo.Set(user, info); + cachedinfo.Set(localuser, info); std::lock_guard lock(mtx); - user->WriteNumeric(RPL_WHOISIPINFO, user->nick, "hes/her ip information: " + info); + localuser->WriteNumeric(RPL_WHOISIPINFO, localuser->nick, "ip info: " + info); } public: - IPInfoResolver(Module* Creator, LocalUser* user, StringExtItem& cache, const std::string& key) - : cachedinfo(cache), apikey(key), theiruuid(user->uuid) + IPInfoResolver(Module* Creator, LocalUser* localuser, StringExtItem& cache, const std::string& key) + : Thread(), cachedinfo(cache), apikey(key), theiruuid(localuser->uuid), user(localuser) { - worker = std::thread(&IPInfoResolver::DoRequest, this, user); - } - - ~IPInfoResolver() - { - if (worker.joinable()) + if (localuser->client_sa.is_ip()) { - worker.join(); + this->Start(); } } }; -class ModuleIPInfo : public Module, public Stats::EventListener, public Whois::EventListener +class ModuleIPInfo : public Module, public Whois::EventListener { private: StringExtItem cachedinfo; std::string apikey; + bool IsPrivateIP(const std::string& ip) + { + // Regex patterns for private IPv4 addresses + std::regex private_ipv4_patterns[] = { + std::regex("^10\\..*"), + std::regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"), + std::regex("^192\\.168\\..*") + }; + + // Check IPv4 addresses + for (const auto& pattern : private_ipv4_patterns) + { + if (std::regex_match(ip, pattern)) + return true; + } + + // Check IPv6 link-local addresses (fe80::/10) + if (ip.find("fe80:") == 0) + return true; + + // Check loopback addresses + if (ip == "127.0.0.1" || ip == "::1") + return true; + + return false; + } + public: ModuleIPInfo() : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") - , Stats::EventListener(this) , Whois::EventListener(this) , cachedinfo(this, "ipinfo", ExtensionType::USER) { @@ -160,30 +181,30 @@ class ModuleIPInfo : public Module, public Stats::EventListener, public Whois::E if (!whois.GetSource()->IsOper()) return; + if (!target->client_sa.is_ip()) + { + whois.SendLine(RPL_WHOISIPINFO, "ip info: ip not found, possible UNIX socket user."); + return; + } + + // Check for private IP addresses + if (IsPrivateIP(target->client_sa.addr())) + { + whois.SendLine(RPL_WHOISIPINFO, "ip info: user is connecting from a private ip address."); + return; + } + const std::string* cached = cachedinfo.Get(target); if (cached) { - whois.SendLine(RPL_WHOISIPINFO, "hes/her ip information(cached): " + *cached); + whois.SendLine(RPL_WHOISIPINFO, "ip info(cached): " + *cached); } else { new IPInfoResolver(this, IS_LOCAL(target), cachedinfo, apikey); } } - - ModResult OnStats(Stats::Context& stats) override - { - return MOD_RES_PASSTHRU; - } - - void OnUnloadModule(Module* mod) override - { - const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); - for (const auto& user : users) - { - cachedinfo.Unset(user); - } - } }; MODULE_INIT(ModuleIPInfo) + From db925ccf9b5ea5be10b4db3ff5b4d9b772c595f6 Mon Sep 17 00:00:00 2001 From: revrse <129334336+revrsedev@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:47:58 +0200 Subject: [PATCH 05/25] Update m_ipinfo_io.cpp Use RPL_WHOISSPECIAL (320) --- 4/m_ipinfo_io.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/4/m_ipinfo_io.cpp b/4/m_ipinfo_io.cpp index 8105cb67..a7a0dae9 100644 --- a/4/m_ipinfo_io.cpp +++ b/4/m_ipinfo_io.cpp @@ -35,12 +35,6 @@ #include #include -enum -{ - // Define a custom WHOIS numeric reply for the IP info. - RPL_WHOISIPINFO = 695, -}; - class IPInfoResolver : public Thread { private: @@ -104,7 +98,7 @@ class IPInfoResolver : public Thread cachedinfo.Set(localuser, info); std::lock_guard lock(mtx); - localuser->WriteNumeric(RPL_WHOISIPINFO, localuser->nick, "ip info: " + info); + localuser->WriteNumeric(RPL_WHOISSPECIAL, localuser->nick, "ip info: " + info); } public: @@ -183,21 +177,21 @@ class ModuleIPInfo : public Module, public Whois::EventListener if (!target->client_sa.is_ip()) { - whois.SendLine(RPL_WHOISIPINFO, "ip info: ip not found, possible UNIX socket user."); + whois.SendLine(RPL_WHOISSPECIAL, "ip info: no ip address found, maybe using UNIX socket connection."); return; } // Check for private IP addresses if (IsPrivateIP(target->client_sa.addr())) { - whois.SendLine(RPL_WHOISIPINFO, "ip info: user is connecting from a private ip address."); + whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); return; } const std::string* cached = cachedinfo.Get(target); if (cached) { - whois.SendLine(RPL_WHOISIPINFO, "ip info(cached): " + *cached); + whois.SendLine(RPL_WHOISSPECIAL, "ip info(cached): " + *cached); } else { @@ -208,3 +202,4 @@ class ModuleIPInfo : public Module, public Whois::EventListener MODULE_INIT(ModuleIPInfo) + From 651d665a553753af6b9b32e6b5590cb8ed1f8c99 Mon Sep 17 00:00:00 2001 From: revrse <129334336+revrsedev@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:55:37 +0200 Subject: [PATCH 06/25] Update m_profileLink.cpp Changes after https://github.com/inspircd/inspircd-contrib/pull/276 --- 4/m_profileLink.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/4/m_profileLink.cpp b/4/m_profileLink.cpp index 794ccdbc..23ceb5ce 100644 --- a/4/m_profileLink.cpp +++ b/4/m_profileLink.cpp @@ -19,6 +19,7 @@ /// $ModAuthor: Jean Chevronnet /// $ModDepends: core 4 /// $ModDesc: Adds a profile link to the WHOIS response for registered users, ignoring services, bots. +/// $ModConfig: #include "inspircd.h" #include "modules/whois.h" @@ -36,21 +37,34 @@ class ModuleProfileLink final { private: Account::API accountapi; + std::string profileBaseUrl; + UserModeReference botmode; public: ModuleProfileLink() : Module(VF_OPTCOMMON, "Adds a profile link to the WHOIS response for registered users, ignoring services, bots.") , Whois::EventListener(this) , accountapi(this) + , botmode(this, "bot") { } + void ReadConfig(ConfigStatus& status) override + { + auto& tag = ServerInstance->Config->ConfValue("profilelink"); + profileBaseUrl = tag->getString("baseurl"); + } + void OnWhois(Whois::Context& whois) override { User* target = whois.GetTarget(); // Skip services, bots. - if (target->server->IsService() || target->IsModeSet('B')) + if (target->server->IsService() || target->IsModeSet(botmode)) + return; + + // Ensure the account module is loaded before attempting to get the account name. + if (!accountapi) return; // Check if the user has an account name (is registered). @@ -58,14 +72,14 @@ class ModuleProfileLink final if (account) { // Construct the profile URL using the user's account name. - const std::string profileUrl = "https://www.reseau-entrenous.fr/profil/" + *account; + const std::string profileUrl = profileBaseUrl + *account; // Send the profile URL in the WHOIS response. - whois.SendLine(RPL_WHOISPROFILE, target->nick, "Profil: " + profileUrl); + whois.SendLine(RPL_WHOISPROFILE, "*", "Profil: " + profileUrl); } else { // Indicate that the account is not registered. - whois.SendLine(RPL_WHOISPROFILE, target->nick, "Profil: L'utilisateur n'est pas connecté ou le compte n'est pas enregistré."); + whois.SendLine(RPL_WHOISPROFILE, "*", "Profil: L'utilisateur n'est pas connecté ou le compte n'est pas enregistré."); } } }; From c74beb18ca6997c2274bb820a81f5f301c109d20 Mon Sep 17 00:00:00 2001 From: revrse <129334336+revrsedev@users.noreply.github.com> Date: Fri, 5 Jul 2024 22:40:20 +0200 Subject: [PATCH 07/25] Update m_ipinfo_io.cpp This should deal with IS_REMOTE() properly, shared truth all servers on the network with StringExtItem. --- 4/m_ipinfo_io.cpp | 53 +++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/4/m_ipinfo_io.cpp b/4/m_ipinfo_io.cpp index a7a0dae9..0766038e 100644 --- a/4/m_ipinfo_io.cpp +++ b/4/m_ipinfo_io.cpp @@ -42,7 +42,7 @@ class IPInfoResolver : public Thread StringExtItem& cachedinfo; std::string apikey; std::string theiruuid; - LocalUser* user; + User* resolved_user; // Renamed to avoid shadowing static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) { @@ -60,7 +60,7 @@ class IPInfoResolver : public Thread curl = curl_easy_init(); if (curl) { - std::string url = "https://ipinfo.io/" + user->client_sa.addr() + "?token=" + apikey; + std::string url = "https://ipinfo.io/" + resolved_user->client_sa.addr() + "?token=" + apikey; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); @@ -68,24 +68,24 @@ class IPInfoResolver : public Thread if (res != CURLE_OK) { std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to get data for %s: %s", user->nick.c_str(), curl_easy_strerror(res)); + ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to get data for %s: %s", resolved_user->nick.c_str(), curl_easy_strerror(res)); } else { - ParseResponse(user, response); + ParseResponse(resolved_user, response); } curl_easy_cleanup(curl); } curl_global_cleanup(); } - void ParseResponse(LocalUser* localuser, const std::string& response) + void ParseResponse(User* user, const std::string& response) { rapidjson::Document document; if (document.Parse(response.c_str()).HasParseError()) { std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for %s: %s", localuser->nick.c_str(), rapidjson::GetParseError_En(document.GetParseError())); + ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for %s: %s", user->nick.c_str(), rapidjson::GetParseError_En(document.GetParseError())); return; } @@ -95,17 +95,17 @@ class IPInfoResolver : public Thread std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; - cachedinfo.Set(localuser, info); + cachedinfo.Set(user, info); std::lock_guard lock(mtx); - localuser->WriteNumeric(RPL_WHOISSPECIAL, localuser->nick, "ip info: " + info); + user->WriteNumeric(RPL_WHOISSPECIAL, user->nick, "ip info: " + info); } public: - IPInfoResolver(Module* Creator, LocalUser* localuser, StringExtItem& cache, const std::string& key) - : Thread(), cachedinfo(cache), apikey(key), theiruuid(localuser->uuid), user(localuser) + IPInfoResolver(Module* Creator, User* user, StringExtItem& cache, const std::string& key) + : Thread(), cachedinfo(cache), apikey(key), theiruuid(user->uuid), resolved_user(user) { - if (localuser->client_sa.is_ip()) + if (user->client_sa.is_ip()) { this->Start(); } @@ -149,7 +149,7 @@ class ModuleIPInfo : public Module, public Whois::EventListener ModuleIPInfo() : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") , Whois::EventListener(this) - , cachedinfo(this, "ipinfo", ExtensionType::USER) + , cachedinfo(this, "ipinfo", ExtensionType::USER, true) { } @@ -169,7 +169,11 @@ class ModuleIPInfo : public Module, public Whois::EventListener { User* target = whois.GetTarget(); - if (target->server->IsService() || target->IsModeSet('B')) + if (target->server->IsService()) + return; + + UserModeReference botmode(this, "bot"); + if (target->IsModeSet(botmode)) return; if (!whois.GetSource()->IsOper()) @@ -177,25 +181,33 @@ class ModuleIPInfo : public Module, public Whois::EventListener if (!target->client_sa.is_ip()) { - whois.SendLine(RPL_WHOISSPECIAL, "ip info: no ip address found, maybe using UNIX socket connection."); + whois.SendLine(RPL_WHOISSPECIAL, "ip info: no IP address found, maybe using UNIX socket connection."); return; } // Check for private IP addresses if (IsPrivateIP(target->client_sa.addr())) { - whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); + whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); return; } - const std::string* cached = cachedinfo.Get(target); - if (cached) + if (IS_LOCAL(target)) { - whois.SendLine(RPL_WHOISSPECIAL, "ip info(cached): " + *cached); + const std::string* cached = cachedinfo.Get(target); + if (cached) + { + whois.SendLine(RPL_WHOISSPECIAL, "ip info (cached): " + *cached); + } + else + { + new IPInfoResolver(this, target, cachedinfo, apikey); + } } - else + else if (IS_REMOTE(target)) { - new IPInfoResolver(this, IS_LOCAL(target), cachedinfo, apikey); + // Send request to remote server to fetch IP info + whois.SendLine(RPL_WHOISSPECIAL, "ip info: target is a remote user."); } } }; @@ -203,3 +215,4 @@ class ModuleIPInfo : public Module, public Whois::EventListener MODULE_INIT(ModuleIPInfo) + From b24f15dac101dc6cd4cf6a28f9fdf591f9405728 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Sun, 7 Jul 2024 09:18:14 +0200 Subject: [PATCH 08/25] Update m_ipinfo_io.cpp Changes todo on #276 --- 4/m_ipinfo_io.cpp | 345 +++++++++++++++++++++++----------------------- 1 file changed, 170 insertions(+), 175 deletions(-) diff --git a/4/m_ipinfo_io.cpp b/4/m_ipinfo_io.cpp index 0766038e..01232ce8 100644 --- a/4/m_ipinfo_io.cpp +++ b/4/m_ipinfo_io.cpp @@ -1,199 +1,203 @@ -/* - * InspIRCd -- Internet Relay Chat Daemon - * - * Copyright (C) 2024 Jean Chevronnet - * - * This file contains a third party module for InspIRCd. 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 . - */ - -/// $ModAuthor: Jean Chevronnet (reverse) -/// $ModDesc: Ip information from Ipinfo.io in /WHOIS (only irc operators), found more information at https://ipinfo.io/developers. -/// $ModDepends: core 4 -/// $ModConfig: -/// $CompilerFlags: find_compiler_flags("RapidJSON") -/// $CompilerFlags: find_compiler_flags("libcurl") -/// $LinkerFlags: find_linker_flags("libcurl") - -#include "inspircd.h" -#include "extension.h" -#include "modules/httpd.h" -#include "modules/whois.h" -#include "thread.h" -#include -#include -#include -#include -#include - -class IPInfoResolver : public Thread -{ -private: - std::mutex mtx; - StringExtItem& cachedinfo; - std::string apikey; - std::string theiruuid; - User* resolved_user; // Renamed to avoid shadowing - - static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) + /* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2024 Jean Chevronnet + * + * This file contains a third party module for InspIRCd. 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 . + */ + + /// $ModAuthor: Jean Chevronnet (reverse) + /// $ModDesc: Ip information from Ipinfo.io in /WHOIS (only irc operators), found more information at https://ipinfo.io/developers. + /// $ModDepends: core 4 + /// $ModConfig: + /// $CompilerFlags: find_compiler_flags("RapidJSON") + /// $CompilerFlags: find_compiler_flags("libcurl") + /// $LinkerFlags: find_linker_flags("libcurl") + + #include "inspircd.h" + #include "extension.h" + #include "modules/httpd.h" + #include "modules/whois.h" + #include "thread.h" + #include + #include + #include + #include + #include + #include + + class IPInfoResolver : public Thread { - s->append(static_cast(contents), size * nmemb); - return size * nmemb; - } - - void OnStart() override - { - CURL* curl; - CURLcode res; - std::string response; + private: + std::mutex mtx; + StringExtItem& cachedinfo; + std::string apikey; + std::string theiruuid; + User* resolved_user; // Renamed to avoid shadowing + + static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) + { + s->append(static_cast(contents), size * nmemb); + return size * nmemb; + } - curl_global_init(CURL_GLOBAL_DEFAULT); - curl = curl_easy_init(); - if (curl) + void OnStart() override { - std::string url = "https://ipinfo.io/" + resolved_user->client_sa.addr() + "?token=" + apikey; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - res = curl_easy_perform(curl); - if (res != CURLE_OK) - { - std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to get data for %s: %s", resolved_user->nick.c_str(), curl_easy_strerror(res)); - } - else + CURL* curl; + CURLcode res; + std::string response; + + curl_global_init(CURL_GLOBAL_DEFAULT); + curl = curl_easy_init(); + if (curl) { - ParseResponse(resolved_user, response); + std::string url = "https://ipinfo.io/" + resolved_user->client_sa.addr() + "?token=" + apikey; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + res = curl_easy_perform(curl); + if (res != CURLE_OK) + { + std::lock_guard lock(mtx); + ServerInstance->SNO.WriteGlobalSno('a', fmt::format("IPInfo: Failed to get data for {}: {}", resolved_user->nick, curl_easy_strerror(res))); + } + else + { + ParseResponse(resolved_user, response); + } + curl_easy_cleanup(curl); } - curl_easy_cleanup(curl); + curl_global_cleanup(); } - curl_global_cleanup(); - } - void ParseResponse(User* user, const std::string& response) - { - rapidjson::Document document; - if (document.Parse(response.c_str()).HasParseError()) + void ParseResponse(User* user, const std::string& response) { - std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for %s: %s", user->nick.c_str(), rapidjson::GetParseError_En(document.GetParseError())); - return; - } + rapidjson::Document document; + if (document.Parse(response.c_str()).HasParseError()) + { + std::lock_guard lock(mtx); + ServerInstance->SNO.WriteGlobalSno('a', fmt::format("IPInfo: Failed to parse JSON for {}: {}", user->nick, rapidjson::GetParseError_En(document.GetParseError()))); + return; + } - std::string city = document.HasMember("city") ? document["city"].GetString() : "Unknown"; - std::string region = document.HasMember("region") ? document["region"].GetString() : "Unknown"; - std::string country = document.HasMember("country") ? document["country"].GetString() : "Unknown"; - std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; + std::string city = document.HasMember("city") ? document["city"].GetString() : "Unknown"; + std::string region = document.HasMember("region") ? document["region"].GetString() : "Unknown"; + std::string country = document.HasMember("country") ? document["country"].GetString() : "Unknown"; + std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; - std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; - cachedinfo.Set(user, info); + std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; + cachedinfo.Set(user, info); - std::lock_guard lock(mtx); - user->WriteNumeric(RPL_WHOISSPECIAL, user->nick, "ip info: " + info); - } + std::lock_guard lock(mtx); + user->WriteNumeric(RPL_WHOISSPECIAL, user->nick, "ip info: " + info); + } -public: - IPInfoResolver(Module* Creator, User* user, StringExtItem& cache, const std::string& key) - : Thread(), cachedinfo(cache), apikey(key), theiruuid(user->uuid), resolved_user(user) - { - if (user->client_sa.is_ip()) + public: + IPInfoResolver(Module* Creator, User* user, StringExtItem& cache, const std::string& key) + : Thread(), cachedinfo(cache), apikey(key), theiruuid(user->uuid), resolved_user(user) { - this->Start(); + if (user->client_sa.is_ip()) + { + this->Start(); + } } - } -}; - -class ModuleIPInfo : public Module, public Whois::EventListener -{ -private: - StringExtItem cachedinfo; - std::string apikey; + }; - bool IsPrivateIP(const std::string& ip) + class ModuleIPInfo : public Module, public Whois::EventListener { - // Regex patterns for private IPv4 addresses - std::regex private_ipv4_patterns[] = { - std::regex("^10\\..*"), - std::regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"), - std::regex("^192\\.168\\..*") - }; - - // Check IPv4 addresses - for (const auto& pattern : private_ipv4_patterns) + private: + StringExtItem cachedinfo; + std::string apikey; + + bool IsPrivateIP(const std::string& ip) { - if (std::regex_match(ip, pattern)) + // Regex patterns for private IPv4 addresses + std::regex private_ipv4_patterns[] = { + std::regex("^10\\..*"), + std::regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"), + std::regex("^192\\.168\\..*") + }; + + // Check IPv4 addresses + for (const auto& pattern : private_ipv4_patterns) + { + if (std::regex_match(ip, pattern)) + return true; + } + + // Check IPv6 link-local addresses (fe80::/10) + if (ip.find("fe80:") == 0) return true; - } - // Check IPv6 link-local addresses (fe80::/10) - if (ip.find("fe80:") == 0) - return true; + // Check loopback addresses + if (ip == "127.0.0.1" || ip == "::1") + return true; - // Check loopback addresses - if (ip == "127.0.0.1" || ip == "::1") - return true; + return false; + } - return false; - } + public: + ModuleIPInfo() + : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") + , Whois::EventListener(this) + , cachedinfo(this, "ipinfo", ExtensionType::USER, true) // Enable synchronization across the network + { + } -public: - ModuleIPInfo() - : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") - , Whois::EventListener(this) - , cachedinfo(this, "ipinfo", ExtensionType::USER, true) - { - } + void ReadConfig(ConfigStatus& status) override + { + auto& tag = ServerInstance->Config->ConfValue("ipinfo"); + apikey = tag->getString("apikey", ""); - void ReadConfig(ConfigStatus& status) override - { - auto& tag = ServerInstance->Config->ConfValue("ipinfo"); - apikey = tag->getString("apikey", "", 1); + if (apikey.empty()) + { + throw ModuleException(this, " No APIKEY? This is a required configuration option."); + } - const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); - for (const auto& user : users) - { - cachedinfo.Unset(user); + const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); + for (const auto& user : users) + { + cachedinfo.Unset(user); + } } - } - void OnWhois(Whois::Context& whois) override - { - User* target = whois.GetTarget(); + void OnWhois(Whois::Context& whois) override + { + User* target = whois.GetTarget(); - if (target->server->IsService()) - return; + if (target->server->IsService()) + return; - UserModeReference botmode(this, "bot"); - if (target->IsModeSet(botmode)) - return; + UserModeReference botmode(this, "bot"); + if (target->IsModeSet(botmode)) + return; - if (!whois.GetSource()->IsOper()) - return; + if (!whois.GetSource()->IsOper()) + return; - if (!target->client_sa.is_ip()) - { - whois.SendLine(RPL_WHOISSPECIAL, "ip info: no IP address found, maybe using UNIX socket connection."); - return; - } + if (!target->client_sa.is_ip()) + { + whois.SendLine(RPL_WHOISSPECIAL, "ip info: no IP address found, maybe using UNIX socket connection."); + return; + } - // Check for private IP addresses - if (IsPrivateIP(target->client_sa.addr())) - { - whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); - return; - } + // Check for private IP addresses + if (IsPrivateIP(target->client_sa.addr())) + { + whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); + return; + } - if (IS_LOCAL(target)) - { const std::string* cached = cachedinfo.Get(target); if (cached) { @@ -204,15 +208,6 @@ class ModuleIPInfo : public Module, public Whois::EventListener new IPInfoResolver(this, target, cachedinfo, apikey); } } - else if (IS_REMOTE(target)) - { - // Send request to remote server to fetch IP info - whois.SendLine(RPL_WHOISSPECIAL, "ip info: target is a remote user."); - } - } -}; - -MODULE_INIT(ModuleIPInfo) - - + }; + MODULE_INIT(ModuleIPInfo) From d2e18fccfa5bf1887261a6052b2ef3748c0a047b Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Sun, 7 Jul 2024 15:29:54 +0200 Subject: [PATCH 09/25] Update m_ipinfo_io.cpp Change after https://github.com/inspircd/inspircd-contrib/pull/276#discussion_r1667690772 --- 4/m_ipinfo_io.cpp | 354 +++++++++++++++++++++++----------------------- 1 file changed, 177 insertions(+), 177 deletions(-) diff --git a/4/m_ipinfo_io.cpp b/4/m_ipinfo_io.cpp index 01232ce8..e36766f0 100644 --- a/4/m_ipinfo_io.cpp +++ b/4/m_ipinfo_io.cpp @@ -1,213 +1,213 @@ /* - * InspIRCd -- Internet Relay Chat Daemon - * - * Copyright (C) 2024 Jean Chevronnet - * - * This file contains a third party module for InspIRCd. 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 . - */ - - /// $ModAuthor: Jean Chevronnet (reverse) - /// $ModDesc: Ip information from Ipinfo.io in /WHOIS (only irc operators), found more information at https://ipinfo.io/developers. - /// $ModDepends: core 4 - /// $ModConfig: - /// $CompilerFlags: find_compiler_flags("RapidJSON") - /// $CompilerFlags: find_compiler_flags("libcurl") - /// $LinkerFlags: find_linker_flags("libcurl") - - #include "inspircd.h" - #include "extension.h" - #include "modules/httpd.h" - #include "modules/whois.h" - #include "thread.h" - #include - #include - #include - #include - #include - #include - - class IPInfoResolver : public Thread + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2024 Jean Chevronnet + * + * This file contains a third party module for InspIRCd. 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 . + */ + +/// $ModAuthor: Jean Chevronnet (reverse) +/// $ModDesc: Ip information from Ipinfo.io in /WHOIS (only irc operators), found more information at https://ipinfo.io/developers. +/// $ModDepends: core 4 +/// $ModConfig: +/// $CompilerFlags: find_compiler_flags("RapidJSON") +/// $CompilerFlags: find_compiler_flags("libcurl") +/// $LinkerFlags: find_linker_flags("libcurl") + +#include "inspircd.h" +#include "extension.h" +#include "modules/httpd.h" +#include "modules/whois.h" +#include "thread.h" +#include +#include +#include +#include +#include +#include + +class IPInfoResolver : public Thread +{ +private: + std::mutex mtx; + StringExtItem& cachedinfo; + std::string apikey; + std::string theiruuid; + User* resolved_user; // Renamed to avoid shadowing + + static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) { - private: - std::mutex mtx; - StringExtItem& cachedinfo; - std::string apikey; - std::string theiruuid; - User* resolved_user; // Renamed to avoid shadowing - - static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s) - { - s->append(static_cast(contents), size * nmemb); - return size * nmemb; - } + s->append(static_cast(contents), size * nmemb); + return size * nmemb; + } - void OnStart() override - { - CURL* curl; - CURLcode res; - std::string response; + void OnStart() override + { + CURL* curl; + CURLcode res; + std::string response; - curl_global_init(CURL_GLOBAL_DEFAULT); - curl = curl_easy_init(); - if (curl) + curl_global_init(CURL_GLOBAL_DEFAULT); + curl = curl_easy_init(); + if (curl) + { + std::string url = "https://ipinfo.io/" + resolved_user->client_sa.addr() + "?token=" + apikey; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + res = curl_easy_perform(curl); + if (res != CURLE_OK) { - std::string url = "https://ipinfo.io/" + resolved_user->client_sa.addr() + "?token=" + apikey; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - res = curl_easy_perform(curl); - if (res != CURLE_OK) - { - std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', fmt::format("IPInfo: Failed to get data for {}: {}", resolved_user->nick, curl_easy_strerror(res))); - } - else - { - ParseResponse(resolved_user, response); - } - curl_easy_cleanup(curl); + std::lock_guard lock(mtx); + ServerInstance->SNO.WriteGlobalSno('a', fmt::format("IPInfo: Failed to get data for {}: {}", resolved_user->nick, curl_easy_strerror(res))); } - curl_global_cleanup(); + else + { + ParseResponse(resolved_user, response); + } + curl_easy_cleanup(curl); } + curl_global_cleanup(); + } - void ParseResponse(User* user, const std::string& response) + void ParseResponse(User* user, const std::string& response) + { + rapidjson::Document document; + if (document.Parse(response.c_str()).HasParseError()) { - rapidjson::Document document; - if (document.Parse(response.c_str()).HasParseError()) - { - std::lock_guard lock(mtx); - ServerInstance->SNO.WriteGlobalSno('a', fmt::format("IPInfo: Failed to parse JSON for {}: {}", user->nick, rapidjson::GetParseError_En(document.GetParseError()))); - return; - } + std::lock_guard lock(mtx); + ServerInstance->SNO.WriteGlobalSno('a', "IPInfo: Failed to parse JSON for {}: {}", user->nick, rapidjson::GetParseError_En(document.GetParseError())); + return; + } - std::string city = document.HasMember("city") ? document["city"].GetString() : "Unknown"; - std::string region = document.HasMember("region") ? document["region"].GetString() : "Unknown"; - std::string country = document.HasMember("country") ? document["country"].GetString() : "Unknown"; - std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; + std::string city = document.HasMember("city") ? document["city"].GetString() : "Unknown"; + std::string region = document.HasMember("region") ? document["region"].GetString() : "Unknown"; + std::string country = document.HasMember("country") ? document["country"].GetString() : "Unknown"; + std::string org = document.HasMember("org") ? document["org"].GetString() : "Unknown"; - std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; - cachedinfo.Set(user, info); + std::string info = "City: " + city + ", Region: " + region + ", Country: " + country + ", Org: " + org; + cachedinfo.Set(user, info); - std::lock_guard lock(mtx); - user->WriteNumeric(RPL_WHOISSPECIAL, user->nick, "ip info: " + info); - } + std::lock_guard lock(mtx); + user->WriteNumeric(RPL_WHOISSPECIAL, user->nick, "ip info: " + info); + } - public: - IPInfoResolver(Module* Creator, User* user, StringExtItem& cache, const std::string& key) - : Thread(), cachedinfo(cache), apikey(key), theiruuid(user->uuid), resolved_user(user) +public: + IPInfoResolver(Module* Creator, User* user, StringExtItem& cache, const std::string& key) + : Thread(), cachedinfo(cache), apikey(key), theiruuid(user->uuid), resolved_user(user) + { + if (user->client_sa.is_ip()) { - if (user->client_sa.is_ip()) - { - this->Start(); - } + this->Start(); } - }; + } +}; - class ModuleIPInfo : public Module, public Whois::EventListener - { - private: - StringExtItem cachedinfo; - std::string apikey; +class ModuleIPInfo : public Module, public Whois::EventListener +{ +private: + StringExtItem cachedinfo; + std::string apikey; - bool IsPrivateIP(const std::string& ip) + bool IsPrivateIP(const std::string& ip) + { + // Regex patterns for private IPv4 addresses + std::regex private_ipv4_patterns[] = { + std::regex("^10\\..*"), + std::regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"), + std::regex("^192\\.168\\..*") + }; + + // Check IPv4 addresses + for (const auto& pattern : private_ipv4_patterns) { - // Regex patterns for private IPv4 addresses - std::regex private_ipv4_patterns[] = { - std::regex("^10\\..*"), - std::regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"), - std::regex("^192\\.168\\..*") - }; - - // Check IPv4 addresses - for (const auto& pattern : private_ipv4_patterns) - { - if (std::regex_match(ip, pattern)) - return true; - } - - // Check IPv6 link-local addresses (fe80::/10) - if (ip.find("fe80:") == 0) + if (std::regex_match(ip, pattern)) return true; + } - // Check loopback addresses - if (ip == "127.0.0.1" || ip == "::1") - return true; + // Check IPv6 link-local addresses (fe80::/10) + if (ip.find("fe80:") == 0) + return true; - return false; - } + // Check loopback addresses + if (ip == "127.0.0.1" || ip == "::1") + return true; - public: - ModuleIPInfo() - : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") - , Whois::EventListener(this) - , cachedinfo(this, "ipinfo", ExtensionType::USER, true) // Enable synchronization across the network - { - } + return false; + } - void ReadConfig(ConfigStatus& status) override - { - auto& tag = ServerInstance->Config->ConfValue("ipinfo"); - apikey = tag->getString("apikey", ""); +public: + ModuleIPInfo() + : Module(VF_VENDOR, "Adds IPinfo.io information to WHOIS responses for opers, using a configured API key.") + , Whois::EventListener(this) + , cachedinfo(this, "ipinfo", ExtensionType::USER, true) // Enable synchronization across the network + { + } - if (apikey.empty()) - { - throw ModuleException(this, " No APIKEY? This is a required configuration option."); - } + void ReadConfig(ConfigStatus& status) override + { + auto& tag = ServerInstance->Config->ConfValue("ipinfo"); + apikey = tag->getString("apikey", ""); - const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); - for (const auto& user : users) - { - cachedinfo.Unset(user); - } + if (apikey.empty()) + { + throw ModuleException(this, " No APIKEY? This is a required configuration option."); } - void OnWhois(Whois::Context& whois) override + const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers(); + for (const auto& user : users) { - User* target = whois.GetTarget(); + cachedinfo.Unset(user); + } + } - if (target->server->IsService()) - return; + void OnWhois(Whois::Context& whois) override + { + User* target = whois.GetTarget(); - UserModeReference botmode(this, "bot"); - if (target->IsModeSet(botmode)) - return; + if (target->server->IsService()) + return; - if (!whois.GetSource()->IsOper()) - return; + UserModeReference botmode(this, "bot"); + if (target->IsModeSet(botmode)) + return; - if (!target->client_sa.is_ip()) - { - whois.SendLine(RPL_WHOISSPECIAL, "ip info: no IP address found, maybe using UNIX socket connection."); - return; - } + if (!whois.GetSource()->IsOper()) + return; - // Check for private IP addresses - if (IsPrivateIP(target->client_sa.addr())) - { - whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); - return; - } + if (!target->client_sa.is_ip()) + { + whois.SendLine(RPL_WHOISSPECIAL, "ip info: no IP address found, maybe using UNIX socket connection."); + return; + } - const std::string* cached = cachedinfo.Get(target); - if (cached) - { - whois.SendLine(RPL_WHOISSPECIAL, "ip info (cached): " + *cached); - } - else - { - new IPInfoResolver(this, target, cachedinfo, apikey); - } + // Check for private IP addresses + if (IsPrivateIP(target->client_sa.addr())) + { + whois.SendLine(RPL_WHOISSPECIAL, "ip info: user is connecting from a private IP address."); + return; + } + + const std::string* cached = cachedinfo.Get(target); + if (cached) + { + whois.SendLine(RPL_WHOISSPECIAL, "ip info (cached): " + *cached); + } + else + { + new IPInfoResolver(this, target, cachedinfo, apikey); } - }; + } +}; - MODULE_INIT(ModuleIPInfo) +MODULE_INIT(ModuleIPInfo) From 92e60bc129345aa0c14099908b23b3debfa8c874 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 9 Jul 2024 10:03:35 +0200 Subject: [PATCH 10/25] Create m_randomidxlines.cpp New module, add a random ID to xlines. --- 4/m_randomidxlines.cpp | 96 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 4/m_randomidxlines.cpp diff --git a/4/m_randomidxlines.cpp b/4/m_randomidxlines.cpp new file mode 100644 index 00000000..f82eb414 --- /dev/null +++ b/4/m_randomidxlines.cpp @@ -0,0 +1,96 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2024 Jean reverse Chevronnet + * + * This program is distributed 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 . + */ + +/// $ModAuthor: Jean reverse Chevronnet +/// $ModDesc: Enhances /zline, /gline, /kline, /kill and similar commands by adding a random ID to the end for better log identification. +/// $ModDepends: core 4 + +#include "inspircd.h" +#include "xline.h" +#include + +class ModuleRandomIDxLines : public Module +{ +private: + std::string GenerateRandomID() + { + // Use static variables to avoid reinitializing random number generator each time + static std::random_device rd; + static std::mt19937 gen(rd()); + static std::uniform_int_distribution dis(100000000000000000, 999999999999999999); + uint64_t random_number = dis(gen); + return std::to_string(random_number); + } + + void AppendRandomID(std::string& message) + { + // Generate the random ID to append + std::string random_id = " - ID: " + GenerateRandomID(); + // Ensure the message length does not exceed the limit after appending + size_t max_reason_length = 510 - random_id.length(); // 510 to account for possible CR LF at the end + if (message.length() > max_reason_length) + { + message = message.substr(0, max_reason_length); + } + // Append the random ID + message += random_id; + } + + ModResult HandleLineCommand(const std::string& command, User* source, CommandBase::Params& parameters) + { + if (parameters.size() > 1) + { + // Append random ID to the existing reason parameter + AppendRandomID(parameters[1]); + } + else + { + // Create a new reason with the random ID if it doesn't exist + std::string id_message = "- ID: " + GenerateRandomID(); + AppendRandomID(id_message); + parameters.push_back(id_message); + } + + // Log the command with the appended random ID + std::string log_message = fmt::format("{} {} {}: {}", source->nick, command, parameters[0], parameters[1]); + ServerInstance->SNO.WriteToSnoMask('a', log_message); + + return MOD_RES_PASSTHRU; + } + +public: + ModuleRandomIDxLines() + : Module(VF_VENDOR, "Enhances /zline, /gline, /kline, /kill and similar commands by adding a random ID to the end for better log identification.") + { + } + + ModResult OnPreCommand(std::string& command, CommandBase::Params& parameters, LocalUser* user, bool validated) override + { + if (!validated) + return MOD_RES_PASSTHRU; + + // Handle specific commands to append the random ID + if (command == "ZLINE" || command == "GLINE" || command == "KLINE" || command == "KILL") + { + return HandleLineCommand(command, user, parameters); + } + + return MOD_RES_PASSTHRU; + } +}; + +MODULE_INIT(ModuleRandomIDxLines) From 686662f3ec4a1ac16f494cd8ed5203157943e5da Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:21:27 +0200 Subject: [PATCH 11/25] Update m_randomidxlines.cpp changes after https://github.com/inspircd/inspircd-contrib/pull/277 --- 4/m_randomidxlines.cpp | 63 +++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/4/m_randomidxlines.cpp b/4/m_randomidxlines.cpp index f82eb414..880f973c 100644 --- a/4/m_randomidxlines.cpp +++ b/4/m_randomidxlines.cpp @@ -10,7 +10,7 @@ * 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 . */ @@ -28,47 +28,63 @@ class ModuleRandomIDxLines : public Module private: std::string GenerateRandomID() { - // Use static variables to avoid reinitializing random number generator each time static std::random_device rd; static std::mt19937 gen(rd()); - static std::uniform_int_distribution dis(100000000000000000, 999999999999999999); - uint64_t random_number = dis(gen); - return std::to_string(random_number); + static std::uniform_int_distribution dis(1000000000, 9999999999); + return ConvToStr(dis(gen)); } void AppendRandomID(std::string& message) { - // Generate the random ID to append std::string random_id = " - ID: " + GenerateRandomID(); - // Ensure the message length does not exceed the limit after appending size_t max_reason_length = 510 - random_id.length(); // 510 to account for possible CR LF at the end if (message.length() > max_reason_length) { message = message.substr(0, max_reason_length); } - // Append the random ID message += random_id; } - ModResult HandleLineCommand(const std::string& command, User* source, CommandBase::Params& parameters) + bool IsValidHostMask(const std::string& mask) { - if (parameters.size() > 1) + return mask.find('@') != std::string::npos || (mask.length() > 0 && mask[0] == '@'); + } + + bool IsValidDuration(const std::string& duration) + { + for (char c : duration) { - // Append random ID to the existing reason parameter - AppendRandomID(parameters[1]); + if (!isdigit(c) && c != 's' && c != 'm' && c != 'h' && c != 'd' && c != 'w') + return false; } - else + return true; + } + + bool XLineExists(const std::string& command, const std::string& target) + { + if (command == "ZLINE") + return ServerInstance->XLines->MatchesLine("Z", target) != nullptr; + else if (command == "GLINE") + return ServerInstance->XLines->MatchesLine("G", target) != nullptr; + else if (command == "KLINE") + return ServerInstance->XLines->MatchesLine("K", target) != nullptr; + return false; + } + + ModResult HandleLineCommand(const std::string& command, User* source, CommandBase::Params& parameters) + { + // Only add random ID if there are enough parameters to set a line (target, duration, reason) + if (parameters.size() > 2 && IsValidHostMask(parameters[0]) && IsValidDuration(parameters[1])) { - // Create a new reason with the random ID if it doesn't exist - std::string id_message = "- ID: " + GenerateRandomID(); - AppendRandomID(id_message); - parameters.push_back(id_message); + if (!XLineExists(command, parameters[0])) + { + // Append random ID to the existing reason parameter + AppendRandomID(parameters.back()); + std::string log_message = INSP_FORMAT("{} {} {}: {}", source->nick, command, parameters[0], parameters.back()); + ServerInstance->SNO.WriteToSnoMask('a', log_message); + } } - // Log the command with the appended random ID - std::string log_message = fmt::format("{} {} {}: {}", source->nick, command, parameters[0], parameters[1]); - ServerInstance->SNO.WriteToSnoMask('a', log_message); - return MOD_RES_PASSTHRU; } @@ -83,8 +99,8 @@ class ModuleRandomIDxLines : public Module if (!validated) return MOD_RES_PASSTHRU; - // Handle specific commands to append the random ID - if (command == "ZLINE" || command == "GLINE" || command == "KLINE" || command == "KILL") + // Handle commands + if ((command == "ZLINE" || command == "GLINE" || command == "KLINE" || command == "KILL")) { return HandleLineCommand(command, user, parameters); } @@ -94,3 +110,4 @@ class ModuleRandomIDxLines : public Module }; MODULE_INIT(ModuleRandomIDxLines) + From f5e1bdffe344499943f05d42aaa9143f123c1a8c Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Wed, 10 Jul 2024 00:23:03 +0200 Subject: [PATCH 12/25] Update m_randomidxlines.cpp Use Duration::IsValid(str) for duration validation. --- 4/m_randomidxlines.cpp | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/4/m_randomidxlines.cpp b/4/m_randomidxlines.cpp index 880f973c..8ec45afe 100644 --- a/4/m_randomidxlines.cpp +++ b/4/m_randomidxlines.cpp @@ -7,12 +7,9 @@ * 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. + * 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 . + * You should have received a copy of the GNU General Public License along with this program. If not, see . */ /// $ModAuthor: Jean reverse Chevronnet @@ -21,6 +18,7 @@ #include "inspircd.h" #include "xline.h" +#include "timeutils.h" #include class ModuleRandomIDxLines : public Module @@ -52,12 +50,7 @@ class ModuleRandomIDxLines : public Module bool IsValidDuration(const std::string& duration) { - for (char c : duration) - { - if (!isdigit(c) && c != 's' && c != 'm' && c != 'h' && c != 'd' && c != 'w') - return false; - } - return true; + return Duration::IsValid(duration); } bool XLineExists(const std::string& command, const std::string& target) @@ -80,7 +73,7 @@ class ModuleRandomIDxLines : public Module { // Append random ID to the existing reason parameter AppendRandomID(parameters.back()); - std::string log_message = INSP_FORMAT("{} {} {}: {}", source->nick, command, parameters[0], parameters.back()); + std::string log_message = fmt::format("{} {} {}: {}", source->nick, command, parameters[0], parameters.back()); ServerInstance->SNO.WriteToSnoMask('a', log_message); } } @@ -99,7 +92,7 @@ class ModuleRandomIDxLines : public Module if (!validated) return MOD_RES_PASSTHRU; - // Handle commands + // Handle specific commands to append the random ID only when creating or modifying a line if ((command == "ZLINE" || command == "GLINE" || command == "KLINE" || command == "KILL")) { return HandleLineCommand(command, user, parameters); From bb23989a37ad36e2f94b71b41fab1b40456eb183 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Wed, 10 Jul 2024 00:23:59 +0200 Subject: [PATCH 13/25] Update m_randomidxlines.cpp ups --- 4/m_randomidxlines.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/4/m_randomidxlines.cpp b/4/m_randomidxlines.cpp index 8ec45afe..31e493a4 100644 --- a/4/m_randomidxlines.cpp +++ b/4/m_randomidxlines.cpp @@ -73,7 +73,7 @@ class ModuleRandomIDxLines : public Module { // Append random ID to the existing reason parameter AppendRandomID(parameters.back()); - std::string log_message = fmt::format("{} {} {}: {}", source->nick, command, parameters[0], parameters.back()); + std::string log_message = INSP_FORMAT("{} {} {}: {}", source->nick, command, parameters[0], parameters.back()); ServerInstance->SNO.WriteToSnoMask('a', log_message); } } From 2ad4274cb5fe51f8dce9ff8a8c9fe434ae28bfb0 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Sat, 13 Jul 2024 17:26:12 +0200 Subject: [PATCH 14/25] Update m_censor.cpp Enahnce for mixedcharacterUTF8 spams. --- 4/m_censor.cpp | 195 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 33 deletions(-) diff --git a/4/m_censor.cpp b/4/m_censor.cpp index 8129aea9..555ed050 100644 --- a/4/m_censor.cpp +++ b/4/m_censor.cpp @@ -28,13 +28,15 @@ /// $ModDepends: core 4 /// $ModDesc: Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages. - #include "inspircd.h" #include "modules/exemption.h" #include "numerichelper.h" - +#include "utility/string.h" +#include +#include +​ typedef insp::flat_map CensorMap; - +​ class ModuleCensor : public Module { private: @@ -42,22 +44,128 @@ class ModuleCensor : public Module CensorMap censors; SimpleUserMode cu; SimpleChannelMode cc; - + std::vector allowed_smileys; +​ + bool IsMixedUTF8(const std::string& text) + { + if (text.empty()) + return false; +​ + enum ScriptType { SCRIPT_UNKNOWN, SCRIPT_LATIN, SCRIPT_NONLATIN }; + ScriptType detected = SCRIPT_UNKNOWN; +​ + for (const auto& c : text) + { + if (static_cast(c) < 128) + continue; // ASCII characters are ignored +​ + if (std::isalpha(static_cast(c))) + { + ScriptType current = std::islower(static_cast(c)) || std::isupper(static_cast(c)) ? SCRIPT_LATIN : SCRIPT_NONLATIN; + if (detected == SCRIPT_UNKNOWN) + { + detected = current; + } + else if (detected != current) + { + return true; // Mixed scripts detected + } + } + } +​ + return false; + } +​ + // Helper function to convert UTF-8 string to UTF-32 + std::u32string to_utf32(const std::string& utf8) + { + std::wstring_convert, char32_t> convert; + return convert.from_bytes(utf8); + } +​ + // Helper function to convert UTF-32 character to UTF-8 + std::string to_utf8(char32_t utf32_char) + { + std::wstring_convert, char32_t> convert; + return convert.to_bytes(utf32_char); + } +​ + bool IsAllowed(const std::string& text) + { + static const std::string allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "; + std::u32string utf32_text = to_utf32(text); +​ + for (char32_t c : utf32_text) + { + std::string utf8_char = to_utf8(c); + if (allowed_chars.find(utf8_char) != std::string::npos) + { + continue; + } + else if (std::find(allowed_smileys.begin(), allowed_smileys.end(), c) != allowed_smileys.end()) + { + continue; + } + else + { + return false; + } + } + return true; + } +​ public: ModuleCensor() - : Module(VF_NONE, "Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages.") + : Module(VF_NONE, "Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages and blocks messages with mixed UTF-8 scripts, only allowing certain Unicode smileys.") , exemptionprov(this) , cu(this, "u_censor", 'G') , cc(this, "censor", 'G') { } - - // format of a config entry is +​ + void ReadConfig(ConfigStatus& status) override + { + CensorMap newcensors; + allowed_smileys.clear(); + for (const auto& [_, badword_tag] : ServerInstance->Config->ConfTags("badword")) + { + const std::string text = badword_tag->getString("text"); + if (text.empty()) + throw ModuleException(this, " is empty! at " + badword_tag->source.str()); +​ + const std::string replace = badword_tag->getString("replace"); + newcensors[text] = replace; + } + censors.swap(newcensors); +​ + for (const auto& [_, smileys_tag] : ServerInstance->Config->ConfTags("allowedsmileys")) + { + const std::string smileys = smileys_tag->getString("smiley"); + if (smileys.empty()) + throw ModuleException(this, " is empty! at " + smileys_tag->source.str()); +​ + std::istringstream iss(smileys); + std::string smiley; + while (iss >> smiley) + { + if (smiley.substr(0, 2) != "U+") + throw ModuleException(this, "Invalid format for smiley in at " + smileys_tag->source.str()); +​ + char32_t smiley_char = static_cast(std::stoul(smiley.substr(2), nullptr, 16)); + allowed_smileys.push_back(smiley_char); + } + } + } +​ ModResult OnUserPreMessage(User* user, MessageTarget& target, MessageDetails& details) override { if (!IS_LOCAL(user)) return MOD_RES_PASSTHRU; - +​ + // Allow IRC operators to bypass the restrictions + if (user->IsOper()) + return MOD_RES_PASSTHRU; +​ switch (target.type) { case MessageTarget::TYPE_USER: @@ -67,23 +175,48 @@ class ModuleCensor : public Module return MOD_RES_PASSTHRU; break; } - +​ case MessageTarget::TYPE_CHANNEL: { auto* targchan = target.Get(); if (!targchan->IsModeSet(cc)) return MOD_RES_PASSTHRU; - +​ ModResult result = exemptionprov.Check(user, targchan, "censor"); if (result == MOD_RES_ALLOW) return MOD_RES_PASSTHRU; break; } - +​ default: return MOD_RES_PASSTHRU; } - +​ + if (IsMixedUTF8(details.text) || !IsAllowed(details.text)) + { + const std::string msg = "Your message contained disallowed characters and was blocked. IRC operator's has been notified (AntiSpam purpose)."; +​ + // Announce to opers + std::string oper_announcement; + if (target.type == MessageTarget::TYPE_CHANNEL) + { + auto* targchan = target.Get(); + oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} in channel {} sent a message containing disallowed characters: '{}', which was blocked.", user->nick, targchan->name, details.text); + } + else + { + auto* targuser = target.Get(); + oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} sent a private message to {} containing disallowed characters: '{}', which was blocked.", user->nick, targuser->nick, details.text); + } + ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); +​ + if (target.type == MessageTarget::TYPE_CHANNEL) + user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); + else + user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); + return MOD_RES_DENY; + } +​ for (const auto& [find, replace] : censors) { size_t censorpos; @@ -92,37 +225,33 @@ class ModuleCensor : public Module if (replace.empty()) { const std::string msg = INSP_FORMAT("Your message to this channel contained a banned phrase ({}) and was blocked.", find); +​ + // Announce to opers + std::string oper_announcement; + if (target.type == MessageTarget::TYPE_CHANNEL) + { + auto* targchan = target.Get(); + oper_announcement = INSP_FORMAT("BannedPhrase notice: User {} in channel {} sent a message containing banned phrase ({}): '{}', which was blocked.", user->nick, targchan->name, find, details.text); + } + else + { + auto* targuser = target.Get(); + oper_announcement = INSP_FORMAT("BannedPhrase notice: User {} sent a private message to {} containing banned phrase ({}): '{}', which was blocked.", user->nick, targuser->nick, find, details.text); + } + ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); +​ if (target.type == MessageTarget::TYPE_CHANNEL) user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); else user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); return MOD_RES_DENY; } - +​ details.text.replace(censorpos, find.size(), replace); } } return MOD_RES_PASSTHRU; } - - void ReadConfig(ConfigStatus& status) override - { - /* - * reload our config file on rehash - we must destroy and re-allocate the classes - * to call the constructor again and re-read our data. - */ - CensorMap newcensors; - for (const auto& [_, tag] : ServerInstance->Config->ConfTags("badword")) - { - const std::string text = tag->getString("text"); - if (text.empty()) - throw ModuleException(this, " is empty! at " + tag->source.str()); - - const std::string replace = tag->getString("replace"); - newcensors[text] = replace; - } - censors.swap(newcensors); - } }; - +​ MODULE_INIT(ModuleCensor) From dc10c9107ce02172655ffc2efd9bf1325cc811d9 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Sat, 13 Jul 2024 19:03:04 +0200 Subject: [PATCH 15/25] Update m_censor.cpp Changes: Use of regex to match all emojis. Block all unicodes. Notify opers. --- 4/m_censor.cpp | 115 +++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/4/m_censor.cpp b/4/m_censor.cpp index 555ed050..dc4b20c3 100644 --- a/4/m_censor.cpp +++ b/4/m_censor.cpp @@ -28,15 +28,23 @@ /// $ModDepends: core 4 /// $ModDesc: Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages. + +///$CompilerFlags: find_compiler_flags("icu-uc") +///$LinkerFlags: find_linker_flags("icu-uc") +///$CompilerFlags: find_compiler_flags("icu-i18n") +/// $LinkerFlags: find_linker_flags("icu-i18n") + #include "inspircd.h" #include "modules/exemption.h" #include "numerichelper.h" #include "utility/string.h" #include #include -​ +#include +#include + typedef insp::flat_map CensorMap; -​ + class ModuleCensor : public Module { private: @@ -44,21 +52,21 @@ class ModuleCensor : public Module CensorMap censors; SimpleUserMode cu; SimpleChannelMode cc; - std::vector allowed_smileys; -​ + std::unique_ptr emoji_pattern; + bool IsMixedUTF8(const std::string& text) { if (text.empty()) return false; -​ + enum ScriptType { SCRIPT_UNKNOWN, SCRIPT_LATIN, SCRIPT_NONLATIN }; ScriptType detected = SCRIPT_UNKNOWN; -​ + for (const auto& c : text) { if (static_cast(c) < 128) continue; // ASCII characters are ignored -​ + if (std::isalpha(static_cast(c))) { ScriptType current = std::islower(static_cast(c)) || std::isupper(static_cast(c)) ? SCRIPT_LATIN : SCRIPT_NONLATIN; @@ -72,29 +80,43 @@ class ModuleCensor : public Module } } } -​ + return false; } -​ + // Helper function to convert UTF-8 string to UTF-32 std::u32string to_utf32(const std::string& utf8) { std::wstring_convert, char32_t> convert; return convert.from_bytes(utf8); } -​ + // Helper function to convert UTF-32 character to UTF-8 std::string to_utf8(char32_t utf32_char) { std::wstring_convert, char32_t> convert; return convert.to_bytes(utf32_char); } -​ + + bool IsEmojiOnly(const std::string& text) + { + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeString ustr(text.c_str(), "UTF-8"); + std::unique_ptr emoji_matcher(emoji_pattern->matcher(ustr, status)); + if (U_FAILURE(status)) + { + throw ModuleException(this, "Failed to create regex matcher for emojis"); + } + + // Check if the entire text is matched by the emoji pattern + return emoji_matcher->matches(status, status); + } + bool IsAllowed(const std::string& text) { static const std::string allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "; std::u32string utf32_text = to_utf32(text); -​ + for (char32_t c : utf32_text) { std::string utf8_char = to_utf8(c); @@ -102,18 +124,19 @@ class ModuleCensor : public Module { continue; } - else if (std::find(allowed_smileys.begin(), allowed_smileys.end(), c) != allowed_smileys.end()) + else if (IsEmojiOnly(utf8_char)) { - continue; + continue; // Allow emojis matching the regex } else { return false; } } + return true; } -​ + public: ModuleCensor() : Module(VF_NONE, "Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages and blocks messages with mixed UTF-8 scripts, only allowing certain Unicode smileys.") @@ -121,51 +144,38 @@ class ModuleCensor : public Module , cu(this, "u_censor", 'G') , cc(this, "censor", 'G') { + UErrorCode status = U_ZERO_ERROR; + emoji_pattern = std::unique_ptr(icu::RegexPattern::compile("\\p{Emoji}", 0, status)); + if (U_FAILURE(status)) + { + throw ModuleException(this, "Failed to compile emoji regex pattern"); + } } -​ + void ReadConfig(ConfigStatus& status) override { CensorMap newcensors; - allowed_smileys.clear(); for (const auto& [_, badword_tag] : ServerInstance->Config->ConfTags("badword")) { const std::string text = badword_tag->getString("text"); if (text.empty()) throw ModuleException(this, " is empty! at " + badword_tag->source.str()); -​ + const std::string replace = badword_tag->getString("replace"); newcensors[text] = replace; } censors.swap(newcensors); -​ - for (const auto& [_, smileys_tag] : ServerInstance->Config->ConfTags("allowedsmileys")) - { - const std::string smileys = smileys_tag->getString("smiley"); - if (smileys.empty()) - throw ModuleException(this, " is empty! at " + smileys_tag->source.str()); -​ - std::istringstream iss(smileys); - std::string smiley; - while (iss >> smiley) - { - if (smiley.substr(0, 2) != "U+") - throw ModuleException(this, "Invalid format for smiley in at " + smileys_tag->source.str()); -​ - char32_t smiley_char = static_cast(std::stoul(smiley.substr(2), nullptr, 16)); - allowed_smileys.push_back(smiley_char); - } - } } -​ + ModResult OnUserPreMessage(User* user, MessageTarget& target, MessageDetails& details) override { if (!IS_LOCAL(user)) return MOD_RES_PASSTHRU; -​ + // Allow IRC operators to bypass the restrictions if (user->IsOper()) return MOD_RES_PASSTHRU; -​ + switch (target.type) { case MessageTarget::TYPE_USER: @@ -175,27 +185,27 @@ class ModuleCensor : public Module return MOD_RES_PASSTHRU; break; } -​ + case MessageTarget::TYPE_CHANNEL: { auto* targchan = target.Get(); if (!targchan->IsModeSet(cc)) return MOD_RES_PASSTHRU; -​ + ModResult result = exemptionprov.Check(user, targchan, "censor"); if (result == MOD_RES_ALLOW) return MOD_RES_PASSTHRU; break; } -​ + default: return MOD_RES_PASSTHRU; } -​ + if (IsMixedUTF8(details.text) || !IsAllowed(details.text)) { - const std::string msg = "Your message contained disallowed characters and was blocked. IRC operator's has been notified (AntiSpam purpose)."; -​ + const std::string msg = "Your message contained disallowed characters and was blocked. IRC operators have been notified (Spamfilter purpose)."; + // Announce to opers std::string oper_announcement; if (target.type == MessageTarget::TYPE_CHANNEL) @@ -209,14 +219,14 @@ class ModuleCensor : public Module oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} sent a private message to {} containing disallowed characters: '{}', which was blocked.", user->nick, targuser->nick, details.text); } ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); -​ + if (target.type == MessageTarget::TYPE_CHANNEL) user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); else user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); return MOD_RES_DENY; } -​ + for (const auto& [find, replace] : censors) { size_t censorpos; @@ -224,8 +234,8 @@ class ModuleCensor : public Module { if (replace.empty()) { - const std::string msg = INSP_FORMAT("Your message to this channel contained a banned phrase ({}) and was blocked.", find); -​ + const std::string msg = INSP_FORMAT("Your message to this channel contained a banned phrase ({}) and was blocked. IRC operators have been notified (Spamfilter purpose).", find); + // Announce to opers std::string oper_announcement; if (target.type == MessageTarget::TYPE_CHANNEL) @@ -239,19 +249,20 @@ class ModuleCensor : public Module oper_announcement = INSP_FORMAT("BannedPhrase notice: User {} sent a private message to {} containing banned phrase ({}): '{}', which was blocked.", user->nick, targuser->nick, find, details.text); } ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); -​ + if (target.type == MessageTarget::TYPE_CHANNEL) user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); else user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); return MOD_RES_DENY; } -​ + details.text.replace(censorpos, find.size(), replace); } } return MOD_RES_PASSTHRU; } }; -​ + MODULE_INIT(ModuleCensor) + From c64e5cce1c9863176e1dd1af02f092ad44a1b548 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Sat, 13 Jul 2024 21:11:28 +0200 Subject: [PATCH 16/25] Update m_censor.cpp Changes: * Ignore ASCII * Improuve regex * add kiwiirc emoticons. --- 4/m_censor.cpp | 133 ++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/4/m_censor.cpp b/4/m_censor.cpp index dc4b20c3..ed1a023e 100644 --- a/4/m_censor.cpp +++ b/4/m_censor.cpp @@ -1,39 +1,3 @@ -/* - * InspIRCd -- Internet Relay Chat Daemon - * - * Copyright (C) 2018 linuxdaemon - * Copyright (C) 2013, 2017-2018, 2020-2021 Sadie Powell - * Copyright (C) 2012-2013 Attila Molnar - * Copyright (C) 2012, 2019 Robby - * Copyright (C) 2009-2010 Daniel De Graaf - * Copyright (C) 2008 Thomas Stagner - * Copyright (C) 2007 Dennis Friis - * Copyright (C) 2005, 2008 Robin Burchell - * Copyright (C) 2004, 2006, 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 . - */ - -/// $ModAuthor: InspIRCd Developers -/// $ModDepends: core 4 -/// $ModDesc: Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages. - - -///$CompilerFlags: find_compiler_flags("icu-uc") -///$LinkerFlags: find_linker_flags("icu-uc") -///$CompilerFlags: find_compiler_flags("icu-i18n") -/// $LinkerFlags: find_linker_flags("icu-i18n") - #include "inspircd.h" #include "modules/exemption.h" #include "numerichelper.h" @@ -53,6 +17,11 @@ class ModuleCensor : public Module SimpleUserMode cu; SimpleChannelMode cc; std::unique_ptr emoji_pattern; + std::unique_ptr whitelist_pattern; + std::unique_ptr kiwiirc_pattern; + std::string emoji_regex_str; + std::string whitelist_regex_str; + std::string kiwiirc_regex_str; bool IsMixedUTF8(const std::string& text) { @@ -105,36 +74,47 @@ class ModuleCensor : public Module std::unique_ptr emoji_matcher(emoji_pattern->matcher(ustr, status)); if (U_FAILURE(status)) { - throw ModuleException(this, "Failed to create regex matcher for emojis"); + ServerInstance->Logs.Normal(MODNAME, "Failed to create regex matcher for emojis: %s", u_errorName(status)); + return false; } // Check if the entire text is matched by the emoji pattern - return emoji_matcher->matches(status, status); + return emoji_matcher->matches(status); + } + + bool IsKiwiIRCOnly(const std::string& text) + { + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeString ustr(text.c_str(), "UTF-8"); + std::unique_ptr kiwiirc_matcher(kiwiirc_pattern->matcher(ustr, status)); + if (U_FAILURE(status)) + { + ServerInstance->Logs.Normal(MODNAME, "Failed to create regex matcher for KiwiIRC: %s", u_errorName(status)); + return false; + } + + // Check if the entire text is matched by the KiwiIRC pattern + return kiwiirc_matcher->matches(status); } bool IsAllowed(const std::string& text) { - static const std::string allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "; - std::u32string utf32_text = to_utf32(text); + // Allow ASCII characters and common symbols by default + if (std::all_of(text.begin(), text.end(), [](unsigned char c) { return c >= 32 && c <= 126; })) + { + return true; + } - for (char32_t c : utf32_text) + UErrorCode status = U_ZERO_ERROR; + icu::UnicodeString ustr(text.c_str(), "UTF-8"); + std::unique_ptr whitelist_matcher(whitelist_pattern->matcher(ustr, status)); + if (U_FAILURE(status)) { - std::string utf8_char = to_utf8(c); - if (allowed_chars.find(utf8_char) != std::string::npos) - { - continue; - } - else if (IsEmojiOnly(utf8_char)) - { - continue; // Allow emojis matching the regex - } - else - { - return false; - } + ServerInstance->Logs.Normal(MODNAME, "Failed to create regex matcher for whitelist: %s", u_errorName(status)); + return false; } - return true; + return whitelist_matcher->matches(status) || IsEmojiOnly(text) || IsKiwiIRCOnly(text); } public: @@ -144,12 +124,6 @@ class ModuleCensor : public Module , cu(this, "u_censor", 'G') , cc(this, "censor", 'G') { - UErrorCode status = U_ZERO_ERROR; - emoji_pattern = std::unique_ptr(icu::RegexPattern::compile("\\p{Emoji}", 0, status)); - if (U_FAILURE(status)) - { - throw ModuleException(this, "Failed to compile emoji regex pattern"); - } } void ReadConfig(ConfigStatus& status) override @@ -165,6 +139,32 @@ class ModuleCensor : public Module newcensors[text] = replace; } censors.swap(newcensors); + + const auto& tag = ServerInstance->Config->ConfValue("censorplus"); + emoji_regex_str = tag->getString("emojiregex", "^[\\p{Emoji}]+$"); + whitelist_regex_str = tag->getString("whitelistregex", "^[\\p{Latin}\\p{Common} ]+$"); + kiwiirc_regex_str = tag->getString("kiwiircregex", "[:;][-~]?[)DdpP]|O[:;]3"); + + UErrorCode icu_status = U_ZERO_ERROR; + emoji_pattern = std::unique_ptr(icu::RegexPattern::compile(icu::UnicodeString::fromUTF8(emoji_regex_str), 0, icu_status)); + if (U_FAILURE(icu_status)) + { + throw ModuleException(this, INSP_FORMAT("Failed to compile emoji regex pattern: {}", u_errorName(icu_status))); + } + + icu_status = U_ZERO_ERROR; + whitelist_pattern = std::unique_ptr(icu::RegexPattern::compile(icu::UnicodeString::fromUTF8(whitelist_regex_str), 0, icu_status)); + if (U_FAILURE(icu_status)) + { + throw ModuleException(this, INSP_FORMAT("Failed to compile whitelist regex pattern: {}", u_errorName(icu_status))); + } + + icu_status = U_ZERO_ERROR; + kiwiirc_pattern = std::unique_ptr(icu::RegexPattern::compile(icu::UnicodeString::fromUTF8(kiwiirc_regex_str), 0, icu_status)); + if (U_FAILURE(icu_status)) + { + throw ModuleException(this, INSP_FORMAT("Failed to compile KiwiIRC regex pattern: {}", u_errorName(icu_status))); + } } ModResult OnUserPreMessage(User* user, MessageTarget& target, MessageDetails& details) override @@ -212,18 +212,16 @@ class ModuleCensor : public Module { auto* targchan = target.Get(); oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} in channel {} sent a message containing disallowed characters: '{}', which was blocked.", user->nick, targchan->name, details.text); + ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); + user->WriteNumeric(Numerics::CannotSendTo(targchan, msg)); } else { auto* targuser = target.Get(); oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} sent a private message to {} containing disallowed characters: '{}', which was blocked.", user->nick, targuser->nick, details.text); + ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); + user->WriteNumeric(Numerics::CannotSendTo(targuser, msg)); } - ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); - - if (target.type == MessageTarget::TYPE_CHANNEL) - user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); - else - user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); return MOD_RES_DENY; } @@ -265,4 +263,3 @@ class ModuleCensor : public Module }; MODULE_INIT(ModuleCensor) - From c01f4b970feac61f1f458ae444b48b6e12d2b81c Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Sat, 13 Jul 2024 21:12:11 +0200 Subject: [PATCH 17/25] Update m_censor.cpp --- 4/m_censor.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/4/m_censor.cpp b/4/m_censor.cpp index ed1a023e..1b37f1b5 100644 --- a/4/m_censor.cpp +++ b/4/m_censor.cpp @@ -1,3 +1,23 @@ + * + * 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 . + */ + +/// $ModAuthor: InspIRCd Developers +/// $ModDepends: core 4 +/// $ModDesc: Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages. + + +///$CompilerFlags: find_compiler_flags("icu-uc") +///$LinkerFlags: find_linker_flags("icu-uc") +///$CompilerFlags: find_compiler_flags("icu-i18n") +/// $LinkerFlags: find_linker_flags("icu-i18n") + #include "inspircd.h" #include "modules/exemption.h" #include "numerichelper.h" From 17cad1a16dedb6dc9a4610847aa555057e00013f Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:30:56 +0100 Subject: [PATCH 18/25] Create m_whoisgeolite.cpp --- m_whoisgeolite.cpp | 139 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 m_whoisgeolite.cpp diff --git a/m_whoisgeolite.cpp b/m_whoisgeolite.cpp new file mode 100644 index 00000000..70d41a0e --- /dev/null +++ b/m_whoisgeolite.cpp @@ -0,0 +1,139 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2024 reverse + * + * This file contains a third-party module for InspIRCd. 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 . + */ + +/// $ModAuthor: reverse +/// $ModDesc: Adds city information to WHOIS using the MaxMind database. +/// $ModConfig: add path +/// $ModDepends: core 4 + +/// $CompilerFlags: find_compiler_flags("libmaxminddb") +/// $LinkerFlags: find_linker_flags("libmaxminddb") + +/// $PackageInfo: require_system("alpine") libmaxminddb-dev pkgconf +/// $PackageInfo: require_system("arch") pkgconf libmaxminddb +/// $PackageInfo: require_system("darwin") libmaxminddb pkg-config +/// $PackageInfo: require_system("debian~") libmaxminddb-dev pkg-config +/// $PackageInfo: require_system("rhel~") pkg-config libmaxminddb-devel + +#include "inspircd.h" +#include "modules/whois.h" +#include + +class ModuleWhoisGeoLite final + : public Module + , public Whois::EventListener +{ +private: + MMDB_s mmdb; // MaxMind database object + bool db_loaded; // Flag to indicate if the database was successfully loaded + std::string dbpath; + +public: + ModuleWhoisGeoLite() + : Module(VF_OPTCOMMON, "Adds city information to WHOIS using the MaxMind database.") + , Whois::EventListener(this), db_loaded(false) + { + } + + void ReadConfig(ConfigStatus& status) override + { + // Load configuration and get the path to the GeoLite2 database + auto& tag = ServerInstance->Config->ConfValue("geolite"); + dbpath = tag->getString("dbpath", "/etc/GeoLite2-City.mmdb"); + + // Attempt to open the MaxMind GeoLite2-City database + int status_open = MMDB_open(dbpath.c_str(), MMDB_MODE_MMAP, &mmdb); + if (status_open != MMDB_SUCCESS) { + ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Failed to open GeoLite2 database: " + std::string(MMDB_strerror(status_open))); + db_loaded = false; + } else { + ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Successfully opened GeoLite2 database."); + db_loaded = true; + } + } + + void OnWhois(Whois::Context& whois) override + { + User* source = whois.GetSource(); // The user issuing the WHOIS command + User* target = whois.GetTarget(); // The user being WHOIS'd + + // Only allow IRC operators to see the city information + if (!source->IsOper()) { + return; + } + + // Check if the user is remote + if (!IS_LOCAL(target)) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: User is connected from a remote server."); + return; + } + + LocalUser* luser = IS_LOCAL(target); + + // Ensure the user is local and has a valid IP address + if (!luser || !luser->client_sa.is_ip()) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: No valid IP address (possibly using a Unix socket)."); + return; + } + + // Ensure the MaxMind database is loaded + if (!db_loaded) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: GeoLite2 database not loaded."); + return; + } + + // Perform the GeoLite2 lookup using the socket address + int gai_error = 0; + const struct sockaddr* addr = reinterpret_cast(&luser->client_sa); + MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&mmdb, addr, &gai_error); + + if (gai_error != 0) { + ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: getaddrinfo error: " + std::string(gai_strerror(gai_error))); + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (lookup error)."); + return; + } + + if (!result.found_entry) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (no database entry)."); + return; + } + + // Retrieve the city name + MMDB_entry_data_s city_data = {}; + int status = MMDB_get_value(&result.entry, &city_data, "city", "names", "en", nullptr); + + if (status == MMDB_SUCCESS && city_data.has_data) { + // If the city is found, add it to the WHOIS response + std::string city(city_data.utf8_string, city_data.data_size); + whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: " + city); + } else { + // City not found + whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: Unknown."); + } + } + + ~ModuleWhoisGeoLite() override + { + // Close the MaxMind database when the module is unloaded + if (db_loaded) { + MMDB_close(&mmdb); + } + } +}; + +MODULE_INIT(ModuleWhoisGeoLite) From 4bf479859a63f2ca0975c0a607390bd7cd970976 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:31:30 +0100 Subject: [PATCH 19/25] Delete m_whoisgeolite.cpp --- m_whoisgeolite.cpp | 139 --------------------------------------------- 1 file changed, 139 deletions(-) delete mode 100644 m_whoisgeolite.cpp diff --git a/m_whoisgeolite.cpp b/m_whoisgeolite.cpp deleted file mode 100644 index 70d41a0e..00000000 --- a/m_whoisgeolite.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * InspIRCd -- Internet Relay Chat Daemon - * - * Copyright (C) 2024 reverse - * - * This file contains a third-party module for InspIRCd. 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 . - */ - -/// $ModAuthor: reverse -/// $ModDesc: Adds city information to WHOIS using the MaxMind database. -/// $ModConfig: add path -/// $ModDepends: core 4 - -/// $CompilerFlags: find_compiler_flags("libmaxminddb") -/// $LinkerFlags: find_linker_flags("libmaxminddb") - -/// $PackageInfo: require_system("alpine") libmaxminddb-dev pkgconf -/// $PackageInfo: require_system("arch") pkgconf libmaxminddb -/// $PackageInfo: require_system("darwin") libmaxminddb pkg-config -/// $PackageInfo: require_system("debian~") libmaxminddb-dev pkg-config -/// $PackageInfo: require_system("rhel~") pkg-config libmaxminddb-devel - -#include "inspircd.h" -#include "modules/whois.h" -#include - -class ModuleWhoisGeoLite final - : public Module - , public Whois::EventListener -{ -private: - MMDB_s mmdb; // MaxMind database object - bool db_loaded; // Flag to indicate if the database was successfully loaded - std::string dbpath; - -public: - ModuleWhoisGeoLite() - : Module(VF_OPTCOMMON, "Adds city information to WHOIS using the MaxMind database.") - , Whois::EventListener(this), db_loaded(false) - { - } - - void ReadConfig(ConfigStatus& status) override - { - // Load configuration and get the path to the GeoLite2 database - auto& tag = ServerInstance->Config->ConfValue("geolite"); - dbpath = tag->getString("dbpath", "/etc/GeoLite2-City.mmdb"); - - // Attempt to open the MaxMind GeoLite2-City database - int status_open = MMDB_open(dbpath.c_str(), MMDB_MODE_MMAP, &mmdb); - if (status_open != MMDB_SUCCESS) { - ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Failed to open GeoLite2 database: " + std::string(MMDB_strerror(status_open))); - db_loaded = false; - } else { - ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Successfully opened GeoLite2 database."); - db_loaded = true; - } - } - - void OnWhois(Whois::Context& whois) override - { - User* source = whois.GetSource(); // The user issuing the WHOIS command - User* target = whois.GetTarget(); // The user being WHOIS'd - - // Only allow IRC operators to see the city information - if (!source->IsOper()) { - return; - } - - // Check if the user is remote - if (!IS_LOCAL(target)) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: User is connected from a remote server."); - return; - } - - LocalUser* luser = IS_LOCAL(target); - - // Ensure the user is local and has a valid IP address - if (!luser || !luser->client_sa.is_ip()) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: No valid IP address (possibly using a Unix socket)."); - return; - } - - // Ensure the MaxMind database is loaded - if (!db_loaded) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: GeoLite2 database not loaded."); - return; - } - - // Perform the GeoLite2 lookup using the socket address - int gai_error = 0; - const struct sockaddr* addr = reinterpret_cast(&luser->client_sa); - MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&mmdb, addr, &gai_error); - - if (gai_error != 0) { - ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: getaddrinfo error: " + std::string(gai_strerror(gai_error))); - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (lookup error)."); - return; - } - - if (!result.found_entry) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (no database entry)."); - return; - } - - // Retrieve the city name - MMDB_entry_data_s city_data = {}; - int status = MMDB_get_value(&result.entry, &city_data, "city", "names", "en", nullptr); - - if (status == MMDB_SUCCESS && city_data.has_data) { - // If the city is found, add it to the WHOIS response - std::string city(city_data.utf8_string, city_data.data_size); - whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: " + city); - } else { - // City not found - whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: Unknown."); - } - } - - ~ModuleWhoisGeoLite() override - { - // Close the MaxMind database when the module is unloaded - if (db_loaded) { - MMDB_close(&mmdb); - } - } -}; - -MODULE_INIT(ModuleWhoisGeoLite) From cc0a8d695020d5a186f95de9308fd9bdb232669e Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:31:55 +0100 Subject: [PATCH 20/25] Create m_whoisgeolite.cpp --- 4/m_whoisgeolite.cpp | 139 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 4/m_whoisgeolite.cpp diff --git a/4/m_whoisgeolite.cpp b/4/m_whoisgeolite.cpp new file mode 100644 index 00000000..70d41a0e --- /dev/null +++ b/4/m_whoisgeolite.cpp @@ -0,0 +1,139 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2024 reverse + * + * This file contains a third-party module for InspIRCd. 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 . + */ + +/// $ModAuthor: reverse +/// $ModDesc: Adds city information to WHOIS using the MaxMind database. +/// $ModConfig: add path +/// $ModDepends: core 4 + +/// $CompilerFlags: find_compiler_flags("libmaxminddb") +/// $LinkerFlags: find_linker_flags("libmaxminddb") + +/// $PackageInfo: require_system("alpine") libmaxminddb-dev pkgconf +/// $PackageInfo: require_system("arch") pkgconf libmaxminddb +/// $PackageInfo: require_system("darwin") libmaxminddb pkg-config +/// $PackageInfo: require_system("debian~") libmaxminddb-dev pkg-config +/// $PackageInfo: require_system("rhel~") pkg-config libmaxminddb-devel + +#include "inspircd.h" +#include "modules/whois.h" +#include + +class ModuleWhoisGeoLite final + : public Module + , public Whois::EventListener +{ +private: + MMDB_s mmdb; // MaxMind database object + bool db_loaded; // Flag to indicate if the database was successfully loaded + std::string dbpath; + +public: + ModuleWhoisGeoLite() + : Module(VF_OPTCOMMON, "Adds city information to WHOIS using the MaxMind database.") + , Whois::EventListener(this), db_loaded(false) + { + } + + void ReadConfig(ConfigStatus& status) override + { + // Load configuration and get the path to the GeoLite2 database + auto& tag = ServerInstance->Config->ConfValue("geolite"); + dbpath = tag->getString("dbpath", "/etc/GeoLite2-City.mmdb"); + + // Attempt to open the MaxMind GeoLite2-City database + int status_open = MMDB_open(dbpath.c_str(), MMDB_MODE_MMAP, &mmdb); + if (status_open != MMDB_SUCCESS) { + ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Failed to open GeoLite2 database: " + std::string(MMDB_strerror(status_open))); + db_loaded = false; + } else { + ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Successfully opened GeoLite2 database."); + db_loaded = true; + } + } + + void OnWhois(Whois::Context& whois) override + { + User* source = whois.GetSource(); // The user issuing the WHOIS command + User* target = whois.GetTarget(); // The user being WHOIS'd + + // Only allow IRC operators to see the city information + if (!source->IsOper()) { + return; + } + + // Check if the user is remote + if (!IS_LOCAL(target)) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: User is connected from a remote server."); + return; + } + + LocalUser* luser = IS_LOCAL(target); + + // Ensure the user is local and has a valid IP address + if (!luser || !luser->client_sa.is_ip()) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: No valid IP address (possibly using a Unix socket)."); + return; + } + + // Ensure the MaxMind database is loaded + if (!db_loaded) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: GeoLite2 database not loaded."); + return; + } + + // Perform the GeoLite2 lookup using the socket address + int gai_error = 0; + const struct sockaddr* addr = reinterpret_cast(&luser->client_sa); + MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&mmdb, addr, &gai_error); + + if (gai_error != 0) { + ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: getaddrinfo error: " + std::string(gai_strerror(gai_error))); + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (lookup error)."); + return; + } + + if (!result.found_entry) { + whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (no database entry)."); + return; + } + + // Retrieve the city name + MMDB_entry_data_s city_data = {}; + int status = MMDB_get_value(&result.entry, &city_data, "city", "names", "en", nullptr); + + if (status == MMDB_SUCCESS && city_data.has_data) { + // If the city is found, add it to the WHOIS response + std::string city(city_data.utf8_string, city_data.data_size); + whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: " + city); + } else { + // City not found + whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: Unknown."); + } + } + + ~ModuleWhoisGeoLite() override + { + // Close the MaxMind database when the module is unloaded + if (db_loaded) { + MMDB_close(&mmdb); + } + } +}; + +MODULE_INIT(ModuleWhoisGeoLite) From f34b7843a2071e0e03720a36d68015fa5eee99b5 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:54:39 +0100 Subject: [PATCH 21/25] Update m_censor.cpp --- 4/m_censor.cpp | 233 ++++++++----------------------------------------- 1 file changed, 38 insertions(+), 195 deletions(-) diff --git a/4/m_censor.cpp b/4/m_censor.cpp index 1b37f1b5..8129aea9 100644 --- a/4/m_censor.cpp +++ b/4/m_censor.cpp @@ -1,3 +1,19 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2018 linuxdaemon + * Copyright (C) 2013, 2017-2018, 2020-2021 Sadie Powell + * Copyright (C) 2012-2013 Attila Molnar + * Copyright (C) 2012, 2019 Robby + * Copyright (C) 2009-2010 Daniel De Graaf + * Copyright (C) 2008 Thomas Stagner + * Copyright (C) 2007 Dennis Friis + * Copyright (C) 2005, 2008 Robin Burchell + * Copyright (C) 2004, 2006, 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 @@ -13,19 +29,9 @@ /// $ModDesc: Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages. -///$CompilerFlags: find_compiler_flags("icu-uc") -///$LinkerFlags: find_linker_flags("icu-uc") -///$CompilerFlags: find_compiler_flags("icu-i18n") -/// $LinkerFlags: find_linker_flags("icu-i18n") - #include "inspircd.h" #include "modules/exemption.h" #include "numerichelper.h" -#include "utility/string.h" -#include -#include -#include -#include typedef insp::flat_map CensorMap; @@ -36,166 +42,22 @@ class ModuleCensor : public Module CensorMap censors; SimpleUserMode cu; SimpleChannelMode cc; - std::unique_ptr emoji_pattern; - std::unique_ptr whitelist_pattern; - std::unique_ptr kiwiirc_pattern; - std::string emoji_regex_str; - std::string whitelist_regex_str; - std::string kiwiirc_regex_str; - - bool IsMixedUTF8(const std::string& text) - { - if (text.empty()) - return false; - - enum ScriptType { SCRIPT_UNKNOWN, SCRIPT_LATIN, SCRIPT_NONLATIN }; - ScriptType detected = SCRIPT_UNKNOWN; - - for (const auto& c : text) - { - if (static_cast(c) < 128) - continue; // ASCII characters are ignored - - if (std::isalpha(static_cast(c))) - { - ScriptType current = std::islower(static_cast(c)) || std::isupper(static_cast(c)) ? SCRIPT_LATIN : SCRIPT_NONLATIN; - if (detected == SCRIPT_UNKNOWN) - { - detected = current; - } - else if (detected != current) - { - return true; // Mixed scripts detected - } - } - } - - return false; - } - - // Helper function to convert UTF-8 string to UTF-32 - std::u32string to_utf32(const std::string& utf8) - { - std::wstring_convert, char32_t> convert; - return convert.from_bytes(utf8); - } - - // Helper function to convert UTF-32 character to UTF-8 - std::string to_utf8(char32_t utf32_char) - { - std::wstring_convert, char32_t> convert; - return convert.to_bytes(utf32_char); - } - - bool IsEmojiOnly(const std::string& text) - { - UErrorCode status = U_ZERO_ERROR; - icu::UnicodeString ustr(text.c_str(), "UTF-8"); - std::unique_ptr emoji_matcher(emoji_pattern->matcher(ustr, status)); - if (U_FAILURE(status)) - { - ServerInstance->Logs.Normal(MODNAME, "Failed to create regex matcher for emojis: %s", u_errorName(status)); - return false; - } - - // Check if the entire text is matched by the emoji pattern - return emoji_matcher->matches(status); - } - - bool IsKiwiIRCOnly(const std::string& text) - { - UErrorCode status = U_ZERO_ERROR; - icu::UnicodeString ustr(text.c_str(), "UTF-8"); - std::unique_ptr kiwiirc_matcher(kiwiirc_pattern->matcher(ustr, status)); - if (U_FAILURE(status)) - { - ServerInstance->Logs.Normal(MODNAME, "Failed to create regex matcher for KiwiIRC: %s", u_errorName(status)); - return false; - } - - // Check if the entire text is matched by the KiwiIRC pattern - return kiwiirc_matcher->matches(status); - } - - bool IsAllowed(const std::string& text) - { - // Allow ASCII characters and common symbols by default - if (std::all_of(text.begin(), text.end(), [](unsigned char c) { return c >= 32 && c <= 126; })) - { - return true; - } - - UErrorCode status = U_ZERO_ERROR; - icu::UnicodeString ustr(text.c_str(), "UTF-8"); - std::unique_ptr whitelist_matcher(whitelist_pattern->matcher(ustr, status)); - if (U_FAILURE(status)) - { - ServerInstance->Logs.Normal(MODNAME, "Failed to create regex matcher for whitelist: %s", u_errorName(status)); - return false; - } - - return whitelist_matcher->matches(status) || IsEmojiOnly(text) || IsKiwiIRCOnly(text); - } public: ModuleCensor() - : Module(VF_NONE, "Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages and blocks messages with mixed UTF-8 scripts, only allowing certain Unicode smileys.") + : Module(VF_NONE, "Allows the server administrator to define inappropriate phrases that are not allowed to be used in private or channel messages.") , exemptionprov(this) , cu(this, "u_censor", 'G') , cc(this, "censor", 'G') { } - void ReadConfig(ConfigStatus& status) override - { - CensorMap newcensors; - for (const auto& [_, badword_tag] : ServerInstance->Config->ConfTags("badword")) - { - const std::string text = badword_tag->getString("text"); - if (text.empty()) - throw ModuleException(this, " is empty! at " + badword_tag->source.str()); - - const std::string replace = badword_tag->getString("replace"); - newcensors[text] = replace; - } - censors.swap(newcensors); - - const auto& tag = ServerInstance->Config->ConfValue("censorplus"); - emoji_regex_str = tag->getString("emojiregex", "^[\\p{Emoji}]+$"); - whitelist_regex_str = tag->getString("whitelistregex", "^[\\p{Latin}\\p{Common} ]+$"); - kiwiirc_regex_str = tag->getString("kiwiircregex", "[:;][-~]?[)DdpP]|O[:;]3"); - - UErrorCode icu_status = U_ZERO_ERROR; - emoji_pattern = std::unique_ptr(icu::RegexPattern::compile(icu::UnicodeString::fromUTF8(emoji_regex_str), 0, icu_status)); - if (U_FAILURE(icu_status)) - { - throw ModuleException(this, INSP_FORMAT("Failed to compile emoji regex pattern: {}", u_errorName(icu_status))); - } - - icu_status = U_ZERO_ERROR; - whitelist_pattern = std::unique_ptr(icu::RegexPattern::compile(icu::UnicodeString::fromUTF8(whitelist_regex_str), 0, icu_status)); - if (U_FAILURE(icu_status)) - { - throw ModuleException(this, INSP_FORMAT("Failed to compile whitelist regex pattern: {}", u_errorName(icu_status))); - } - - icu_status = U_ZERO_ERROR; - kiwiirc_pattern = std::unique_ptr(icu::RegexPattern::compile(icu::UnicodeString::fromUTF8(kiwiirc_regex_str), 0, icu_status)); - if (U_FAILURE(icu_status)) - { - throw ModuleException(this, INSP_FORMAT("Failed to compile KiwiIRC regex pattern: {}", u_errorName(icu_status))); - } - } - + // format of a config entry is ModResult OnUserPreMessage(User* user, MessageTarget& target, MessageDetails& details) override { if (!IS_LOCAL(user)) return MOD_RES_PASSTHRU; - // Allow IRC operators to bypass the restrictions - if (user->IsOper()) - return MOD_RES_PASSTHRU; - switch (target.type) { case MessageTarget::TYPE_USER: @@ -222,29 +84,6 @@ class ModuleCensor : public Module return MOD_RES_PASSTHRU; } - if (IsMixedUTF8(details.text) || !IsAllowed(details.text)) - { - const std::string msg = "Your message contained disallowed characters and was blocked. IRC operators have been notified (Spamfilter purpose)."; - - // Announce to opers - std::string oper_announcement; - if (target.type == MessageTarget::TYPE_CHANNEL) - { - auto* targchan = target.Get(); - oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} in channel {} sent a message containing disallowed characters: '{}', which was blocked.", user->nick, targchan->name, details.text); - ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); - user->WriteNumeric(Numerics::CannotSendTo(targchan, msg)); - } - else - { - auto* targuser = target.Get(); - oper_announcement = INSP_FORMAT("MixedCharacterUTF8 notice: User {} sent a private message to {} containing disallowed characters: '{}', which was blocked.", user->nick, targuser->nick, details.text); - ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); - user->WriteNumeric(Numerics::CannotSendTo(targuser, msg)); - } - return MOD_RES_DENY; - } - for (const auto& [find, replace] : censors) { size_t censorpos; @@ -252,22 +91,7 @@ class ModuleCensor : public Module { if (replace.empty()) { - const std::string msg = INSP_FORMAT("Your message to this channel contained a banned phrase ({}) and was blocked. IRC operators have been notified (Spamfilter purpose).", find); - - // Announce to opers - std::string oper_announcement; - if (target.type == MessageTarget::TYPE_CHANNEL) - { - auto* targchan = target.Get(); - oper_announcement = INSP_FORMAT("BannedPhrase notice: User {} in channel {} sent a message containing banned phrase ({}): '{}', which was blocked.", user->nick, targchan->name, find, details.text); - } - else - { - auto* targuser = target.Get(); - oper_announcement = INSP_FORMAT("BannedPhrase notice: User {} sent a private message to {} containing banned phrase ({}): '{}', which was blocked.", user->nick, targuser->nick, find, details.text); - } - ServerInstance->SNO.WriteGlobalSno('a', oper_announcement); - + const std::string msg = INSP_FORMAT("Your message to this channel contained a banned phrase ({}) and was blocked.", find); if (target.type == MessageTarget::TYPE_CHANNEL) user->WriteNumeric(Numerics::CannotSendTo(target.Get(), msg)); else @@ -280,6 +104,25 @@ class ModuleCensor : public Module } return MOD_RES_PASSTHRU; } + + void ReadConfig(ConfigStatus& status) override + { + /* + * reload our config file on rehash - we must destroy and re-allocate the classes + * to call the constructor again and re-read our data. + */ + CensorMap newcensors; + for (const auto& [_, tag] : ServerInstance->Config->ConfTags("badword")) + { + const std::string text = tag->getString("text"); + if (text.empty()) + throw ModuleException(this, " is empty! at " + tag->source.str()); + + const std::string replace = tag->getString("replace"); + newcensors[text] = replace; + } + censors.swap(newcensors); + } }; MODULE_INIT(ModuleCensor) From acc6b26cb3ea1509a1d1b5bf37b6f527123efe08 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:20:19 +0100 Subject: [PATCH 22/25] Update m_whoisgeolite.cpp Check all Sadie's reviews. --- 4/m_whoisgeolite.cpp | 71 ++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/4/m_whoisgeolite.cpp b/4/m_whoisgeolite.cpp index 70d41a0e..5a168184 100644 --- a/4/m_whoisgeolite.cpp +++ b/4/m_whoisgeolite.cpp @@ -17,8 +17,8 @@ */ /// $ModAuthor: reverse -/// $ModDesc: Adds city information to WHOIS using the MaxMind database. -/// $ModConfig: add path +/// $ModDesc: Adds city and country information to WHOIS using the MaxMind database. +/// $ModConfig: loadmodule / add path /// $ModDepends: core 4 /// $CompilerFlags: find_compiler_flags("libmaxminddb") @@ -33,6 +33,7 @@ #include "inspircd.h" #include "modules/whois.h" #include +#include "extension.h" class ModuleWhoisGeoLite final : public Module @@ -40,13 +41,14 @@ class ModuleWhoisGeoLite final { private: MMDB_s mmdb; // MaxMind database object - bool db_loaded; // Flag to indicate if the database was successfully loaded std::string dbpath; + StringExtItem country_item; // For storing the country information public: ModuleWhoisGeoLite() - : Module(VF_OPTCOMMON, "Adds city information to WHOIS using the MaxMind database.") - , Whois::EventListener(this), db_loaded(false) + : Module(VF_OPTCOMMON, "Adds city and country information to WHOIS using the MaxMind database.") + , Whois::EventListener(this) + , country_item(this, "geo-lite-country", ExtensionType::USER) // Corrected initialization { } @@ -59,11 +61,12 @@ class ModuleWhoisGeoLite final // Attempt to open the MaxMind GeoLite2-City database int status_open = MMDB_open(dbpath.c_str(), MMDB_MODE_MMAP, &mmdb); if (status_open != MMDB_SUCCESS) { - ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Failed to open GeoLite2 database: " + std::string(MMDB_strerror(status_open))); - db_loaded = false; + // Construct the error message + std::string error_msg = "GeoLite2: Failed to open GeoLite2 database: " + std::string(MMDB_strerror(status_open)); + // Throw exception with module pointer and error message + throw ModuleException(this, error_msg.c_str()); } else { ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Successfully opened GeoLite2 database."); - db_loaded = true; } } @@ -72,14 +75,20 @@ class ModuleWhoisGeoLite final User* source = whois.GetSource(); // The user issuing the WHOIS command User* target = whois.GetTarget(); // The user being WHOIS'd - // Only allow IRC operators to see the city information + // Only allow IRC operators to see the city and country information if (!source->IsOper()) { return; } - // Check if the user is remote + // Check if the user is remote (i.e., not local) if (!IS_LOCAL(target)) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: User is connected from a remote server."); + // If the country information is available for the remote user, send it + const std::string* country = country_item.Get(target); + if (country) { + whois.SendLine(RPL_WHOISSPECIAL, "is connecting from Country: " + *country); + } else { + whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown (no country data)."); + } return; } @@ -87,53 +96,49 @@ class ModuleWhoisGeoLite final // Ensure the user is local and has a valid IP address if (!luser || !luser->client_sa.is_ip()) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: No valid IP address (possibly using a Unix socket)."); - return; - } - - // Ensure the MaxMind database is loaded - if (!db_loaded) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: GeoLite2 database not loaded."); + whois.SendLine(RPL_WHOISSPECIAL, "City: No valid IP address (possibly using a Unix socket)."); return; } // Perform the GeoLite2 lookup using the socket address int gai_error = 0; - const struct sockaddr* addr = reinterpret_cast(&luser->client_sa); + const struct sockaddr* addr = &luser->client_sa.sa; // Use sa directly without casting MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&mmdb, addr, &gai_error); if (gai_error != 0) { ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: getaddrinfo error: " + std::string(gai_strerror(gai_error))); - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (lookup error)."); + whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown (lookup error)."); return; } if (!result.found_entry) { - whois.SendLine(RPL_WHOISSPECIAL, "*", "City: Unknown (no database entry)."); + whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown (no database entry)."); return; } - // Retrieve the city name + // Retrieve the city and country information MMDB_entry_data_s city_data = {}; + MMDB_entry_data_s country_data = {}; int status = MMDB_get_value(&result.entry, &city_data, "city", "names", "en", nullptr); + status = MMDB_get_value(&result.entry, &country_data, "country", "names", "en", nullptr); - if (status == MMDB_SUCCESS && city_data.has_data) { - // If the city is found, add it to the WHOIS response - std::string city(city_data.utf8_string, city_data.data_size); - whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: " + city); - } else { - // City not found - whois.SendLine(RPL_WHOISSPECIAL, "*", "is connecting from City: Unknown."); - } + // If the city and country are found, add it to the WHOIS response + std::string city = (status == MMDB_SUCCESS && city_data.has_data) ? std::string(city_data.utf8_string, city_data.data_size) : "Unknown"; + std::string country = (status == MMDB_SUCCESS && country_data.has_data) ? std::string(country_data.utf8_string, country_data.data_size) : "Unknown"; + + // Store the country information for remote users to access + country_item.Set(target, country); + + // Send the WHOIS response with city and country information + whois.SendLine(RPL_WHOISSPECIAL, "is connecting from City: " + city + ", Country: " + country); } ~ModuleWhoisGeoLite() override { // Close the MaxMind database when the module is unloaded - if (db_loaded) { - MMDB_close(&mmdb); - } + MMDB_close(&mmdb); } + }; MODULE_INIT(ModuleWhoisGeoLite) From f7680f8b25225ac18ed561f5c36924d6ea658cf3 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:32:53 +0100 Subject: [PATCH 23/25] Update m_whoisgeolite.cpp --- 4/m_whoisgeolite.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/4/m_whoisgeolite.cpp b/4/m_whoisgeolite.cpp index 5a168184..840731d4 100644 --- a/4/m_whoisgeolite.cpp +++ b/4/m_whoisgeolite.cpp @@ -18,7 +18,7 @@ /// $ModAuthor: reverse /// $ModDesc: Adds city and country information to WHOIS using the MaxMind database. -/// $ModConfig: loadmodule / add path +/// $ModConfig: /// $ModDepends: core 4 /// $CompilerFlags: find_compiler_flags("libmaxminddb") @@ -48,7 +48,7 @@ class ModuleWhoisGeoLite final ModuleWhoisGeoLite() : Module(VF_OPTCOMMON, "Adds city and country information to WHOIS using the MaxMind database.") , Whois::EventListener(this) - , country_item(this, "geo-lite-country", ExtensionType::USER) // Corrected initialization + , country_item(this, "geo-lite-country", ExtensionType::USER, true) // Corrected synchronisation { } @@ -56,7 +56,10 @@ class ModuleWhoisGeoLite final { // Load configuration and get the path to the GeoLite2 database auto& tag = ServerInstance->Config->ConfValue("geolite"); - dbpath = tag->getString("dbpath", "/etc/GeoLite2-City.mmdb"); + + // Use PrependConfig to ensure the path is resolved correctly + dbpath = ServerInstance->Config->Paths.PrependConfig(tag->getString("dbpath", "data/GeoLite2-City.mmdb")); + // Attempt to open the MaxMind GeoLite2-City database int status_open = MMDB_open(dbpath.c_str(), MMDB_MODE_MMAP, &mmdb); From 23d7e92c3a83386ae22e7d6eee72df9b2267d704 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:14:00 +0100 Subject: [PATCH 24/25] Update m_whoisgeolite.cpp --- 4/m_whoisgeolite.cpp | 98 +++++++++++++------------------------------- 1 file changed, 29 insertions(+), 69 deletions(-) diff --git a/4/m_whoisgeolite.cpp b/4/m_whoisgeolite.cpp index 840731d4..95630ebc 100644 --- a/4/m_whoisgeolite.cpp +++ b/4/m_whoisgeolite.cpp @@ -18,130 +18,90 @@ /// $ModAuthor: reverse /// $ModDesc: Adds city and country information to WHOIS using the MaxMind database. -/// $ModConfig: +/// $ModConfig: /// $ModDepends: core 4 -/// $CompilerFlags: find_compiler_flags("libmaxminddb") -/// $LinkerFlags: find_linker_flags("libmaxminddb") - -/// $PackageInfo: require_system("alpine") libmaxminddb-dev pkgconf -/// $PackageInfo: require_system("arch") pkgconf libmaxminddb -/// $PackageInfo: require_system("darwin") libmaxminddb pkg-config -/// $PackageInfo: require_system("debian~") libmaxminddb-dev pkg-config -/// $PackageInfo: require_system("rhel~") pkg-config libmaxminddb-devel +/// $LinkerFlags: -lmaxminddb #include "inspircd.h" #include "modules/whois.h" -#include #include "extension.h" +#include -class ModuleWhoisGeoLite final - : public Module - , public Whois::EventListener +class ModuleWhoisGeoLite final : public Module, public Whois::EventListener { private: - MMDB_s mmdb; // MaxMind database object - std::string dbpath; - StringExtItem country_item; // For storing the country information + MMDB_s mmdb; // MaxMind database object + std::string dbpath; // Path to the GeoLite2 database + StringExtItem country_item; // Extension item for storing city and country info public: ModuleWhoisGeoLite() : Module(VF_OPTCOMMON, "Adds city and country information to WHOIS using the MaxMind database.") , Whois::EventListener(this) - , country_item(this, "geo-lite-country", ExtensionType::USER, true) // Corrected synchronisation + , country_item(this, "geo-lite-country", ExtensionType::USER, true) // Sync Servers { } void ReadConfig(ConfigStatus& status) override { - // Load configuration and get the path to the GeoLite2 database auto& tag = ServerInstance->Config->ConfValue("geolite"); - - // Use PrependConfig to ensure the path is resolved correctly dbpath = ServerInstance->Config->Paths.PrependConfig(tag->getString("dbpath", "data/GeoLite2-City.mmdb")); - - // Attempt to open the MaxMind GeoLite2-City database int status_open = MMDB_open(dbpath.c_str(), MMDB_MODE_MMAP, &mmdb); if (status_open != MMDB_SUCCESS) { - // Construct the error message std::string error_msg = "GeoLite2: Failed to open GeoLite2 database: " + std::string(MMDB_strerror(status_open)); - // Throw exception with module pointer and error message throw ModuleException(this, error_msg.c_str()); - } else { - ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: Successfully opened GeoLite2 database."); } } void OnWhois(Whois::Context& whois) override { - User* source = whois.GetSource(); // The user issuing the WHOIS command - User* target = whois.GetTarget(); // The user being WHOIS'd + User* source = whois.GetSource(); + User* target = whois.GetTarget(); - // Only allow IRC operators to see the city and country information - if (!source->IsOper()) { + if (!source->IsOper()) return; - } - // Check if the user is remote (i.e., not local) - if (!IS_LOCAL(target)) { - // If the country information is available for the remote user, send it - const std::string* country = country_item.Get(target); - if (country) { - whois.SendLine(RPL_WHOISSPECIAL, "is connecting from Country: " + *country); - } else { - whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown (no country data)."); - } - return; + const std::string* info = country_item.Get(target); + if (info && !info->empty()) { + whois.SendLine(RPL_WHOISSPECIAL, "is connecting from " + *info); + } else { + whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown, Country: Unknown"); } + } - LocalUser* luser = IS_LOCAL(target); - - // Ensure the user is local and has a valid IP address - if (!luser || !luser->client_sa.is_ip()) { - whois.SendLine(RPL_WHOISSPECIAL, "City: No valid IP address (possibly using a Unix socket)."); + void OnChangeRemoteAddress(LocalUser* user) override + { + if (!user->client_sa.is_ip()) { + country_item.Unset(user); return; } - // Perform the GeoLite2 lookup using the socket address int gai_error = 0; - const struct sockaddr* addr = &luser->client_sa.sa; // Use sa directly without casting + const struct sockaddr* addr = &user->client_sa.sa; MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&mmdb, addr, &gai_error); - if (gai_error != 0) { - ServerInstance->SNO.WriteGlobalSno('a', "GeoLite2: getaddrinfo error: " + std::string(gai_strerror(gai_error))); - whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown (lookup error)."); - return; - } - - if (!result.found_entry) { - whois.SendLine(RPL_WHOISSPECIAL, "City: Unknown (no database entry)."); + if (gai_error != 0 || !result.found_entry) { + country_item.Unset(user); return; } - // Retrieve the city and country information MMDB_entry_data_s city_data = {}; MMDB_entry_data_s country_data = {}; - int status = MMDB_get_value(&result.entry, &city_data, "city", "names", "en", nullptr); - status = MMDB_get_value(&result.entry, &country_data, "country", "names", "en", nullptr); + int status_city = MMDB_get_value(&result.entry, &city_data, "city", "names", "en", nullptr); + int status_country = MMDB_get_value(&result.entry, &country_data, "country", "names", "en", nullptr); - // If the city and country are found, add it to the WHOIS response - std::string city = (status == MMDB_SUCCESS && city_data.has_data) ? std::string(city_data.utf8_string, city_data.data_size) : "Unknown"; - std::string country = (status == MMDB_SUCCESS && country_data.has_data) ? std::string(country_data.utf8_string, country_data.data_size) : "Unknown"; + std::string city = (status_city == MMDB_SUCCESS && city_data.has_data) ? std::string(city_data.utf8_string, city_data.data_size) : "Unknown"; + std::string country = (status_country == MMDB_SUCCESS && country_data.has_data) ? std::string(country_data.utf8_string, country_data.data_size) : "Unknown"; - // Store the country information for remote users to access - country_item.Set(target, country); - - // Send the WHOIS response with city and country information - whois.SendLine(RPL_WHOISSPECIAL, "is connecting from City: " + city + ", Country: " + country); + country_item.Set(user, "City: " + city + ", Country: " + country); } ~ModuleWhoisGeoLite() override { - // Close the MaxMind database when the module is unloaded MMDB_close(&mmdb); } - }; MODULE_INIT(ModuleWhoisGeoLite) From 478f7161d6c2e78a283ec4facc811433b8d16943 Mon Sep 17 00:00:00 2001 From: reverse <129334336+revrsedev@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:15:57 +0100 Subject: [PATCH 25/25] Update m_whoisgeolite.cpp --- 4/m_whoisgeolite.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/4/m_whoisgeolite.cpp b/4/m_whoisgeolite.cpp index 95630ebc..789bb61a 100644 --- a/4/m_whoisgeolite.cpp +++ b/4/m_whoisgeolite.cpp @@ -21,7 +21,15 @@ /// $ModConfig: /// $ModDepends: core 4 -/// $LinkerFlags: -lmaxminddb + +/// $CompilerFlags: find_compiler_flags("libmaxminddb") +/// $LinkerFlags: find_linker_flags("libmaxminddb") + +/// $PackageInfo: require_system("alpine") libmaxminddb-dev pkgconf +/// $PackageInfo: require_system("arch") pkgconf libmaxminddb +/// $PackageInfo: require_system("darwin") libmaxminddb pkg-config +/// $PackageInfo: require_system("debian~") libmaxminddb-dev pkg-config +/// $PackageInfo: require_system("rhel~") pkg-config libmaxminddb-devel #include "inspircd.h" #include "modules/whois.h" @@ -105,3 +113,4 @@ class ModuleWhoisGeoLite final : public Module, public Whois::EventListener }; MODULE_INIT(ModuleWhoisGeoLite) +