LCOV - code coverage report
Current view: top level - foo/src/jamidht - namedirectory.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 202 307 65.8 %
Date: 2025-12-18 10:07:43 Functions: 31 71 43.7 %

          Line data    Source code
       1             : /*
       2             :  *  Copyright (C) 2004-2025 Savoir-faire Linux Inc.
       3             :  *
       4             :  *  This program is free software: you can redistribute it and/or modify
       5             :  *  it under the terms of the GNU General Public License as published by
       6             :  *  the Free Software Foundation, either version 3 of the License, or
       7             :  *  (at your option) any later version.
       8             :  *
       9             :  *  This program is distributed in the hope that it will be useful,
      10             :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12             :  *  GNU General Public License for more details.
      13             :  *
      14             :  *  You should have received a copy of the GNU General Public License
      15             :  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #ifdef HAVE_CONFIG_H
      19             : #include "config.h"
      20             : #endif
      21             : #include "namedirectory.h"
      22             : 
      23             : #include "logger.h"
      24             : #include "string_utils.h"
      25             : #include "fileutils.h"
      26             : #include "base64.h"
      27             : #include "scheduled_executor.h"
      28             : 
      29             : #include <asio.hpp>
      30             : 
      31             : #include "manager.h"
      32             : #include <opendht/crypto.h>
      33             : #include <opendht/utils.h>
      34             : #include <opendht/http.h>
      35             : #include <opendht/logger.h>
      36             : #include <opendht/thread_pool.h>
      37             : 
      38             : #include <cstddef>
      39             : #include <msgpack.hpp>
      40             : #include "json_utils.h"
      41             : 
      42             : /* for Visual Studio */
      43             : #include <ciso646>
      44             : #include <sstream>
      45             : #include <regex>
      46             : #include <fstream>
      47             : 
      48             : namespace jami {
      49             : 
      50             : constexpr const char* const QUERY_NAME {"/name/"};
      51             : constexpr const char* const QUERY_ADDR {"/addr/"};
      52             : constexpr auto CACHE_DIRECTORY {"namecache"sv};
      53             : constexpr const char DEFAULT_SERVER_HOST[] = "https://ns.jami.net";
      54             : 
      55             : constexpr std::string_view HEX_PREFIX = "0x"sv;
      56             : constexpr std::chrono::seconds SAVE_INTERVAL {5};
      57             : 
      58             : /*
      59             :  *  Parser for URIs.         ( protocol        )    ( username         ) ( hostname )
      60             :  *  - Requires "@" if a username is present (e.g., "user@domain.com").
      61             :  *  - Allows common URL-safe special characters in usernames and domains.
      62             :  *
      63             :  *  Regex breakdown:
      64             :  *  1. `([a-zA-Z]+:(?://)?)?` → Optional scheme ("http://", "ftp://").
      65             :  *  2. `(?:([^\s@]{1,64})@)?` → Optional username (max 64 chars, Unicode allowed).
      66             :  *  3. `([^\s@]+)` → Domain or standalone name (Unicode allowed, no spaces or "@").
      67             :  */
      68             : const std::regex URI_VALIDATOR {R"(^([a-zA-Z]+:(?://)?)?(?:([\w\-.~%!$&'()*+,;=]{1,64}|[^\s@]{1,64})@)?([^\s@]+)$)"};
      69             : 
      70             : constexpr size_t MAX_RESPONSE_SIZE {1024ul * 1024};
      71             : 
      72             : using Request = dht::http::Request;
      73             : 
      74             : void
      75           1 : toLower(std::string& string)
      76             : {
      77           1 :     std::transform(string.begin(), string.end(), string.begin(), ::tolower);
      78           1 : }
      79             : 
      80             : NameDirectory&
      81           0 : NameDirectory::instance()
      82             : {
      83           0 :     return instance(DEFAULT_SERVER_HOST);
      84             : }
      85             : 
      86             : void
      87           3 : NameDirectory::lookupUri(std::string_view uri, const std::string& default_server, LookupCallback cb)
      88             : {
      89           3 :     const std::string& default_ns = default_server.empty() ? DEFAULT_SERVER_HOST : default_server;
      90           3 :     std::svmatch pieces_match;
      91           3 :     if (std::regex_match(uri, pieces_match, URI_VALIDATOR)) {
      92           3 :         if (pieces_match.size() == 4) {
      93           3 :             if (pieces_match[2].length() == 0)
      94           3 :                 instance(default_ns).lookupName(pieces_match[3], std::move(cb));
      95             :             else
      96           0 :                 instance(pieces_match[3].str()).lookupName(pieces_match[2], std::move(cb));
      97           3 :             return;
      98             :         }
      99             :     }
     100           0 :     JAMI_ERROR("Unable to parse URI: {}", uri);
     101           0 :     cb("", "", Response::invalidResponse);
     102           6 : }
     103             : 
     104          24 : NameDirectory::NameDirectory(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
     105          24 :     : serverUrl_(serverUrl)
     106          24 :     , logger_(std::move(l))
     107          48 :     , httpContext_(Manager::instance().ioContext())
     108             : {
     109          24 :     if (!serverUrl_.empty() && serverUrl_.back() == '/')
     110           0 :         serverUrl_.pop_back();
     111          24 :     resolver_ = std::make_shared<dht::http::Resolver>(*httpContext_, serverUrl, logger_);
     112          24 :     cachePath_ = fileutils::get_cache_dir() / CACHE_DIRECTORY / resolver_->get_url().host;
     113          24 : }
     114             : 
     115          24 : NameDirectory::~NameDirectory()
     116             : {
     117          24 :     decltype(requests_) requests;
     118             :     {
     119          24 :         std::lock_guard lk(requestsMtx_);
     120          24 :         requests = std::move(requests_);
     121          24 :     }
     122          24 :     for (auto& req : requests)
     123           0 :         req->cancel();
     124          24 : }
     125             : 
     126             : void
     127          24 : NameDirectory::load()
     128             : {
     129          24 :     loadCache();
     130          24 : }
     131             : 
     132             : std::string
     133          10 : canonicalName(const std::string& url)
     134             : {
     135          10 :     std::string name = url;
     136          10 :     std::transform(name.begin(), name.end(), name.begin(), ::tolower);
     137          10 :     if (name.find("://") == std::string::npos)
     138           0 :         name = "https://" + name;
     139          10 :     return name;
     140           0 : }
     141             : 
     142             : NameDirectory&
     143         795 : NameDirectory::instance(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
     144             : {
     145         795 :     const std::string& s = serverUrl.empty() ? DEFAULT_SERVER_HOST : canonicalName(serverUrl);
     146             :     static std::mutex instanceMtx {};
     147             : 
     148         795 :     std::lock_guard lock(instanceMtx);
     149         795 :     static std::map<std::string, NameDirectory> instances {};
     150         795 :     auto it = instances.find(s);
     151         795 :     if (it != instances.end())
     152         771 :         return it->second;
     153          24 :     auto r = instances.emplace(std::piecewise_construct, std::forward_as_tuple(s), std::forward_as_tuple(s, l));
     154          24 :     if (r.second)
     155          24 :         r.first->second.load();
     156          24 :     return r.first->second;
     157         795 : }
     158             : 
     159             : void
     160         780 : NameDirectory::setHeaderFields(Request& request)
     161             : {
     162         780 :     request.set_header_field(restinio::http_field_t::user_agent,
     163        1560 :                              fmt::format("Jami ({}/{})", jami::platform(), jami::arch()));
     164         780 :     request.set_header_field(restinio::http_field_t::accept, "*/*");
     165         780 :     request.set_header_field(restinio::http_field_t::content_type, "application/json");
     166         780 : }
     167             : 
     168             : void
     169         777 : NameDirectory::lookupAddress(const std::string& addr, LookupCallback cb)
     170             : {
     171         777 :     auto cacheResult = nameCache(addr);
     172         777 :     if (not cacheResult.first.empty()) {
     173           1 :         cb(cacheResult.first, cacheResult.second, Response::found);
     174           1 :         return;
     175             :     }
     176        1552 :     auto request = std::make_shared<Request>(*httpContext_, resolver_, serverUrl_ + QUERY_ADDR + addr);
     177             :     try {
     178         776 :         request->set_method(restinio::http_method_get());
     179         776 :         setHeaderFields(*request);
     180         776 :         request->add_on_done_callback([this, cb = std::move(cb), addr](const dht::http::Response& response) {
     181         776 :             if (response.status_code > 400 && response.status_code < 500) {
     182         774 :                 auto cacheResult = nameCache(addr);
     183         774 :                 if (not cacheResult.first.empty())
     184           0 :                     cb(cacheResult.first, cacheResult.second, Response::found);
     185             :                 else
     186         774 :                     cb("", "", Response::notFound);
     187         776 :             } else if (response.status_code == 400)
     188           1 :                 cb("", "", Response::invalidResponse);
     189           1 :             else if (response.status_code != 200) {
     190           0 :                 JAMI_ERROR("Address lookup for {} on {} failed with code={}", addr, serverUrl_, response.status_code);
     191           0 :                 cb("", "", Response::error);
     192             :             } else {
     193             :                 try {
     194           1 :                     Json::Value json;
     195           1 :                     if (!json::parse(response.body, json)) {
     196           0 :                         cb("", "", Response::error);
     197           0 :                         return;
     198             :                     }
     199           1 :                     auto name = json["name"].asString();
     200           1 :                     if (name.empty()) {
     201           0 :                         cb(name, addr, Response::notFound);
     202           0 :                         return;
     203             :                     }
     204           4 :                     JAMI_DEBUG("Found name for {}: {}", addr, name);
     205             :                     {
     206           1 :                         std::lock_guard l(cacheLock_);
     207           1 :                         addrCache_.emplace(name, std::pair(name, addr));
     208           1 :                         nameCache_.emplace(addr, std::pair(name, addr));
     209           1 :                     }
     210           1 :                     cb(name, addr, Response::found);
     211           1 :                     scheduleCacheSave();
     212           1 :                 } catch (const std::exception& e) {
     213           0 :                     JAMI_ERROR("Error when performing address lookup: {}", e.what());
     214           0 :                     cb("", "", Response::error);
     215           0 :                 }
     216             :             }
     217         776 :             std::lock_guard lk(requestsMtx_);
     218         776 :             if (auto req = response.request.lock())
     219         776 :                 requests_.erase(req);
     220         776 :         });
     221             :         {
     222         776 :             std::lock_guard lk(requestsMtx_);
     223         776 :             requests_.emplace(request);
     224         776 :         }
     225         776 :         request->send();
     226           0 :     } catch (const std::exception& e) {
     227           0 :         JAMI_ERROR("Error when performing address lookup: {}", e.what());
     228           0 :         std::lock_guard lk(requestsMtx_);
     229           0 :         if (request)
     230           0 :             requests_.erase(request);
     231           0 :     }
     232         777 : }
     233             : 
     234             : bool
     235           0 : NameDirectory::verify(const std::string& name, const dht::crypto::PublicKey& pk, const std::string& signature)
     236             : {
     237           0 :     return pk.checkSignature(std::vector<uint8_t>(name.begin(), name.end()), base64::decode(signature));
     238             : }
     239             : 
     240             : void
     241           3 : NameDirectory::lookupName(const std::string& name, LookupCallback cb)
     242             : {
     243           3 :     auto cacheResult = addrCache(name);
     244           3 :     if (not cacheResult.first.empty()) {
     245           0 :         cb(cacheResult.first, cacheResult.second, Response::found);
     246           0 :         return;
     247             :     }
     248           3 :     auto encodedName = urlEncode(name);
     249           6 :     auto request = std::make_shared<Request>(*httpContext_, resolver_, serverUrl_ + QUERY_NAME + encodedName);
     250             :     try {
     251           3 :         request->set_method(restinio::http_method_get());
     252           3 :         setHeaderFields(*request);
     253           3 :         request->add_on_done_callback([this, name, cb = std::move(cb)](const dht::http::Response& response) {
     254           3 :             if (response.status_code > 400 && response.status_code < 500)
     255           1 :                 cb("", "", Response::notFound);
     256           2 :             else if (response.status_code == 400)
     257           1 :                 cb("", "", Response::invalidResponse);
     258           1 :             else if (response.status_code < 200 || response.status_code > 299) {
     259           0 :                 JAMI_ERROR("Name lookup for {} on {} failed with code={}", name, serverUrl_, response.status_code);
     260           0 :                 cb("", "", Response::error);
     261           0 :             } else {
     262             :                 try {
     263           1 :                     Json::Value json;
     264           1 :                     if (!json::parse(response.body, json)) {
     265           0 :                         cb("", "", Response::error);
     266           0 :                         return;
     267             :                     }
     268           1 :                     auto nameResult = json["name"].asString();
     269           1 :                     auto addr = json["addr"].asString();
     270           1 :                     auto publickey = json["publickey"].asString();
     271           1 :                     auto signature = json["signature"].asString();
     272             : 
     273           1 :                     if (starts_with(addr, HEX_PREFIX))
     274           0 :                         addr = addr.substr(HEX_PREFIX.size());
     275           1 :                     if (addr.empty()) {
     276           0 :                         cb("", "", Response::notFound);
     277           0 :                         return;
     278             :                     }
     279           1 :                     if (not publickey.empty() and not signature.empty()) {
     280             :                         try {
     281           0 :                             auto pk = dht::crypto::PublicKey(base64::decode(publickey));
     282           0 :                             if (pk.getId().toString() != addr or not verify(nameResult, pk, signature)) {
     283           0 :                                 cb("", "", Response::invalidResponse);
     284           0 :                                 return;
     285             :                             }
     286           0 :                         } catch (const std::exception& e) {
     287           0 :                             cb("", "", Response::invalidResponse);
     288           0 :                             return;
     289           0 :                         }
     290             :                     }
     291           4 :                     JAMI_DEBUG("Found address for {}: {}", name, addr);
     292             :                     {
     293           1 :                         std::lock_guard l(cacheLock_);
     294           1 :                         addrCache_.emplace(name, std::pair(nameResult, addr));
     295           1 :                         addrCache_.emplace(nameResult, std::pair(nameResult, addr));
     296           1 :                         nameCache_.emplace(addr, std::pair(nameResult, addr));
     297           1 :                     }
     298           1 :                     cb(nameResult, addr, Response::found);
     299           1 :                     scheduleCacheSave();
     300           1 :                 } catch (const std::exception& e) {
     301           0 :                     JAMI_ERROR("Error when performing name lookup: {}", e.what());
     302           0 :                     cb("", "", Response::error);
     303           0 :                 }
     304             :             }
     305           3 :             if (auto req = response.request.lock())
     306           3 :                 requests_.erase(req);
     307             :         });
     308             :         {
     309           3 :             std::lock_guard lk(requestsMtx_);
     310           3 :             requests_.emplace(request);
     311           3 :         }
     312           3 :         request->send();
     313           0 :     } catch (const std::exception& e) {
     314           0 :         JAMI_ERROR("Name lookup for {} failed: {}", name, e.what());
     315           0 :         std::lock_guard lk(requestsMtx_);
     316           0 :         if (request)
     317           0 :             requests_.erase(request);
     318           0 :     }
     319           3 : }
     320             : 
     321             : using Blob = std::vector<uint8_t>;
     322             : void
     323           1 : NameDirectory::registerName(const std::string& addr,
     324             :                             const std::string& n,
     325             :                             const std::string& owner,
     326             :                             RegistrationCallback cb,
     327             :                             const std::string& signedname,
     328             :                             const std::string& publickey)
     329             : {
     330           1 :     std::string name {n};
     331           1 :     toLower(name);
     332           1 :     auto cacheResult = addrCache(name);
     333           1 :     if (not cacheResult.first.empty()) {
     334           0 :         if (cacheResult.second == addr)
     335           0 :             cb(RegistrationResponse::success, name);
     336             :         else
     337           0 :             cb(RegistrationResponse::alreadyTaken, name);
     338           0 :         return;
     339             :     }
     340             :     {
     341           1 :         std::lock_guard l(cacheLock_);
     342           1 :         if (not pendingRegistrations_.emplace(addr, name).second) {
     343           0 :             JAMI_WARNING("RegisterName: already registering name {} {}", addr, name);
     344           0 :             cb(RegistrationResponse::error, name);
     345           0 :             return;
     346             :         }
     347           1 :     }
     348             :     std::string body = fmt::format("{{\"addr\":\"{}\",\"owner\":\"{}\",\"signature\":\"{}\",\"publickey\":\"{}\"}}",
     349             :                                    addr,
     350             :                                    owner,
     351             :                                    signedname,
     352           2 :                                    base64::encode(publickey));
     353             : 
     354           1 :     auto encodedName = urlEncode(name);
     355           2 :     auto request = std::make_shared<Request>(*httpContext_, resolver_, serverUrl_ + QUERY_NAME + encodedName);
     356             :     try {
     357           1 :         request->set_method(restinio::http_method_post());
     358           1 :         setHeaderFields(*request);
     359           1 :         request->set_body(body);
     360             : 
     361           4 :         JAMI_WARNING("RegisterName: sending request {} {}", addr, name);
     362             : 
     363           1 :         request->add_on_done_callback([this, name, addr, cb = std::move(cb)](const dht::http::Response& response) {
     364             :             {
     365           1 :                 std::lock_guard l(cacheLock_);
     366           1 :                 pendingRegistrations_.erase(name);
     367           1 :             }
     368           1 :             if (response.status_code == 400) {
     369           0 :                 cb(RegistrationResponse::incompleteRequest, name);
     370           0 :                 JAMI_ERROR("RegistrationResponse::incompleteRequest");
     371           1 :             } else if (response.status_code == 401) {
     372           0 :                 cb(RegistrationResponse::signatureVerificationFailed, name);
     373           0 :                 JAMI_ERROR("RegistrationResponse::signatureVerificationFailed");
     374           1 :             } else if (response.status_code == 403) {
     375           0 :                 cb(RegistrationResponse::alreadyTaken, name);
     376           0 :                 JAMI_ERROR("RegistrationResponse::alreadyTaken");
     377           1 :             } else if (response.status_code == 409) {
     378           0 :                 cb(RegistrationResponse::alreadyTaken, name);
     379           0 :                 JAMI_ERROR("RegistrationResponse::alreadyTaken");
     380           1 :             } else if (response.status_code > 400 && response.status_code < 500) {
     381           0 :                 cb(RegistrationResponse::alreadyTaken, name);
     382           0 :                 JAMI_ERROR("RegistrationResponse::alreadyTaken");
     383           1 :             } else if (response.status_code < 200 || response.status_code > 299) {
     384           0 :                 cb(RegistrationResponse::error, name);
     385           0 :                 JAMI_ERROR("RegistrationResponse::error");
     386           0 :             } else {
     387           1 :                 Json::Value json;
     388           1 :                 std::string err;
     389           1 :                 Json::CharReaderBuilder rbuilder;
     390             : 
     391           1 :                 auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
     392           1 :                 if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)) {
     393           0 :                     cb(RegistrationResponse::error, name);
     394           0 :                     return;
     395             :                 }
     396           1 :                 auto success = json["success"].asBool();
     397           4 :                 JAMI_DEBUG("Got reply for registration of {} {}: {}", name, addr, success ? "success" : "failure");
     398           1 :                 if (success) {
     399           1 :                     std::lock_guard l(cacheLock_);
     400           1 :                     addrCache_.emplace(name, std::pair(name, addr));
     401           1 :                     nameCache_.emplace(addr, std::pair(name, addr));
     402           1 :                 }
     403           1 :                 cb(success ? RegistrationResponse::success : RegistrationResponse::error, name);
     404           1 :             }
     405           1 :             std::lock_guard lk(requestsMtx_);
     406           1 :             if (auto req = response.request.lock())
     407           1 :                 requests_.erase(req);
     408           1 :         });
     409             :         {
     410           1 :             std::lock_guard lk(requestsMtx_);
     411           1 :             requests_.emplace(request);
     412           1 :         }
     413           1 :         request->send();
     414           0 :     } catch (const std::exception& e) {
     415           0 :         JAMI_ERROR("Error when performing name registration: {}", e.what());
     416           0 :         cb(RegistrationResponse::error, name);
     417             :         {
     418           0 :             std::lock_guard l(cacheLock_);
     419           0 :             pendingRegistrations_.erase(name);
     420           0 :         }
     421           0 :         std::lock_guard lk(requestsMtx_);
     422           0 :         if (request)
     423           0 :             requests_.erase(request);
     424           0 :     }
     425           1 : }
     426             : 
     427             : void
     428           2 : NameDirectory::scheduleCacheSave()
     429             : {
     430             :     // JAMI_DBG("Scheduling cache save to %s", cachePath_.c_str());
     431             :     std::weak_ptr<Task> task
     432           6 :         = Manager::instance().scheduler().scheduleIn([this] { dht::ThreadPool::io().run([this] { saveCache(); }); },
     433           2 :                                                      SAVE_INTERVAL);
     434           2 :     std::swap(saveTask_, task);
     435           2 :     if (auto old = task.lock())
     436           2 :         old->cancel();
     437           2 : }
     438             : 
     439             : void
     440           1 : NameDirectory::saveCache()
     441             : {
     442           1 :     dhtnet::fileutils::recursive_mkdir(fileutils::get_cache_dir() / CACHE_DIRECTORY);
     443           1 :     std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
     444           1 :     std::ofstream file(cachePath_, std::ios::trunc | std::ios::binary);
     445           1 :     if (!file.is_open()) {
     446           0 :         JAMI_ERROR("Unable to save cache to {}", cachePath_);
     447           0 :         return;
     448             :     }
     449             :     {
     450           1 :         std::lock_guard l(cacheLock_);
     451           1 :         msgpack::pack(file, nameCache_);
     452           1 :     }
     453           4 :     JAMI_DEBUG("Saved {:d} name-address mapping(s) to {}", nameCache_.size(), cachePath_);
     454           1 : }
     455             : 
     456             : void
     457          24 : NameDirectory::loadCache()
     458             : {
     459          24 :     msgpack::unpacker pac;
     460             : 
     461             :     // read file
     462             :     {
     463          24 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
     464          24 :         std::ifstream file(cachePath_);
     465          24 :         if (!file.is_open()) {
     466          96 :             JAMI_DEBUG("Unable to load {}", cachePath_);
     467          24 :             return;
     468             :         }
     469           0 :         std::string line;
     470           0 :         while (std::getline(file, line)) {
     471           0 :             pac.reserve_buffer(line.size());
     472           0 :             memcpy(pac.buffer(), line.data(), line.size());
     473           0 :             pac.buffer_consumed(line.size());
     474             :         }
     475          48 :     }
     476             : 
     477             :     try {
     478             :         // load values
     479           0 :         std::lock_guard l(cacheLock_);
     480           0 :         msgpack::object_handle oh;
     481           0 :         if (pac.next(oh))
     482           0 :             oh.get().convert(nameCache_);
     483           0 :         for (const auto& m : nameCache_)
     484           0 :             addrCache_.emplace(m.second.second, m.second);
     485           0 :     } catch (const msgpack::parse_error& e) {
     486           0 :         JAMI_ERROR("Error when parsing msgpack object: {}", e.what());
     487           0 :     } catch (const std::bad_cast& e) {
     488           0 :         JAMI_ERROR("Error when loading cache: {}", e.what());
     489           0 :     }
     490             : 
     491           0 :     JAMI_DEBUG("Loaded {:d} name-address mapping(s) from cache", nameCache_.size());
     492          24 : }
     493             : 
     494             : } // namespace jami

Generated by: LCOV version 1.14