/*
* InspIRCd -- Internet Relay Chat Daemon
*
* Copyright (C) 2019-2025 Sadie Powell <sadie@witchery.services>
* Copyright (C) 2019 Matt Schatz <genius3000@g3k.solutions>
*
* This file is part of InspIRCd. InspIRCd is free software: you can
* redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, version 2.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/// BEGIN CMAKE
/// target_require_package(${TARGET} "libmaxminddb" "maxminddb" "maxminddb::maxminddb")
/// END CMAKE
/// $PackageInfo: require_system("alpine") libmaxminddb-dev pkgconf
/// $PackageInfo: require_system("arch") libmaxminddb pkgconf
/// $PackageInfo: require_system("darwin") libmaxminddb pkg-config
/// $PackageInfo: require_system("debian~") libmaxminddb-dev pkg-config
/// $PackageInfo: require_system("rhel~") libmaxminddb-devel pkgconfig
#include <maxminddb.h>
#include "inspircd.h"
#include "extension.h"
#include "modules/geolocation.h"
class GeolocationExtItem final
: public SimpleExtItem<Geolocation::Location>
{
public:
GeolocationExtItem(const WeakModulePtr& mod)
: SimpleExtItem<Geolocation::Location>(mod, "geolocation", ExtensionType::USER)
{
}
std::string ToHuman(const Extensible* container, const ExtensionPtr& item) const noexcept override
{
const auto& location = std::static_pointer_cast<Geolocation::Location>(item);
return location->GetName() + " [" + location->GetCode() + "]";
}
};
using LocationMap = insp::flat_map<std::string, std::weak_ptr<Geolocation::Location>>;
class GeolocationAPIImpl final
: public Geolocation::APIBase
{
public:
GeolocationExtItem ext;
LocationMap locations;
MMDB_s mmdb;
GeolocationAPIImpl(const WeakModulePtr& parent)
: Geolocation::APIBase(parent)
, ext(parent)
{
}
Geolocation::LocationPtr GetLocation(User* user) override
{
// If we have the location cached then use that instead.
auto location = ext.GetPtr(user);
if (!location)
return location;
// Attempt to locate this user.
location = GetLocation(user->client_sa);
if (!location)
return nullptr;
// We found the user. Cache their location for future use.
ext.Set(user, location);
return location;
}
Geolocation::LocationPtr GetLocation(irc::sockets::sockaddrs& sa) override
{
// Skip trying to look up a UNIX socket.
if (!sa.is_ip())
return nullptr;
// Attempt to look up the socket address.
int result;
MMDB_lookup_result_s lookup = MMDB_lookup_sockaddr(&mmdb, &sa.sa, &result);
if (result != MMDB_SUCCESS || !lookup.found_entry)
return nullptr;
// Attempt to retrieve the country code.
MMDB_entry_data_s country_code;
result = MMDB_get_value(&lookup.entry, &country_code, "country", "iso_code", nullptr);
if (result != MMDB_SUCCESS || !country_code.has_data || country_code.type != MMDB_DATA_TYPE_UTF8_STRING || country_code.data_size != 2)
return nullptr;
// If the country has been seen before then use our cached Location object.
const std::string code(country_code.utf8_string, country_code.data_size);
LocationMap::iterator liter = locations.find(code);
if (liter != locations.end())
{
auto ptr = liter->second.lock();
if (ptr)
return ptr; // We have a country already.
}
// Attempt to retrieve the country name.
MMDB_entry_data_s country_name;
result = MMDB_get_value(&lookup.entry, &country_name, "country", "names", "en", nullptr);
if (result != MMDB_SUCCESS || !country_name.has_data || country_name.type != MMDB_DATA_TYPE_UTF8_STRING)
return nullptr;
// Create a Location object and cache it.
const std::string cname(country_name.utf8_string, country_name.data_size);
auto location = std::make_shared<Geolocation::Location>(code, cname);
locations[code] = location;
return location;
}
};
class ModuleGeoMaxMind final
: public Module
{
private:
GeolocationAPIImpl geoapi;
public:
ModuleGeoMaxMind()
: Module(VF_VENDOR, "Allows the server to perform geolocation lookups on both IP addresses and users.")
, geoapi(weak_from_this())
{
memset(&geoapi.mmdb, 0, sizeof(geoapi.mmdb));
}
~ModuleGeoMaxMind() override
{
MMDB_close(&geoapi.mmdb);
}
void init() override
{
ServerInstance->Logs.Normal(MODNAME, "{} is running against libmaxminddb version {}",
MODNAME, MMDB_lib_version());
}
void ReadConfig(ConfigStatus& status) override
{
const auto& tag = ServerInstance->Config->ConfValue("maxmind");
const std::string file = ServerInstance->Config->Paths.PrependConfig(tag->getString("file", "GeoLite2-Country.mmdb", 1));
// Try to read the new database.
MMDB_s mmdb;
int result = MMDB_open(file.c_str(), MMDB_MODE_MMAP, &mmdb);
if (result != MMDB_SUCCESS)
throw ModuleException(weak_from_this(), "Unable to load the MaxMind database ({}): {}",
file, MMDB_strerror(result));
// Swap the new database with the old database.
std::swap(mmdb, geoapi.mmdb);
// Free the old database.
MMDB_close(&mmdb);
}
void OnGarbageCollect() override
{
for (LocationMap::iterator iter = geoapi.locations.begin(); iter != geoapi.locations.end(); )
{
auto location = iter->second.lock();
if (!location)
{
iter = geoapi.locations.erase(iter);
continue; // Entry expired.
}
ServerInstance->Logs.Debug(MODNAME, "Preserving geolocation data for {} ({}) with use count {}... ",
location->GetName(), location->GetCode(), location.use_count());
iter++;
}
geoapi.locations.shrink_to_fit();
}
void OnChangeRemoteAddress(LocalUser* user) override
{
// Unset the extension so that the location of this user is looked
// up again next time it is requested.
geoapi.ext.Unset(user);
}
void OnModuleRehash(User* user, const std::string& param) override
{
if (!insp::casemapped_equals(param, "geolocation"))
return;
try
{
ConfigStatus status;
this->ReadConfig(status);
ServerInstance->SNO.WriteToSnoMask('r', "MaxMind database has been reloaded.");
}
catch (const ModuleException& ex)
{
ServerInstance->SNO.WriteToSnoMask('r', "Failed to reload the MaxMind database. " + ex.GetReason());
}
}
};
MODULE_INIT(ModuleGeoMaxMind)