LCOV - code coverage report
Current view: top level - src/jamidht - namedirectory.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 217 317 68.5 %
Date: 2024-12-21 08:56:24 Functions: 31 67 46.3 %

          Line data    Source code
       1             : /*
       2             :  *  Copyright (C) 2004-2024 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/json.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             : const std::string HEX_PREFIX = "0x";
      56             : constexpr std::chrono::seconds SAVE_INTERVAL {5};
      57             : 
      58             : /** Parser for URIs.         ( protocol        )    ( username         ) ( hostname ) */
      59             : const std::regex URI_VALIDATOR {
      60             :     "^([a-zA-Z]+:(?://)?)?(?:([a-z0-9-_]{1,64})@)?([a-zA-Z0-9\\-._~%!$&'()*+,;=:\\[\\]]+)"};
      61             : const std::regex NAME_VALIDATOR {"^[a-zA-Z0-9-_]{3,32}$"};
      62             : 
      63             : constexpr size_t MAX_RESPONSE_SIZE {1024ul * 1024};
      64             : 
      65             : using Request = dht::http::Request;
      66             : 
      67             : void
      68           3 : toLower(std::string& string)
      69             : {
      70           3 :     std::transform(string.begin(), string.end(), string.begin(), ::tolower);
      71           3 : }
      72             : 
      73             : NameDirectory&
      74           0 : NameDirectory::instance()
      75             : {
      76           0 :     return instance(DEFAULT_SERVER_HOST);
      77             : }
      78             : 
      79             : void
      80           4 : NameDirectory::lookupUri(std::string_view uri, const std::string& default_server, LookupCallback cb)
      81             : {
      82           4 :     const std::string& default_ns = default_server.empty() ? DEFAULT_SERVER_HOST : default_server;
      83           4 :     std::svmatch pieces_match;
      84           4 :     if (std::regex_match(uri, pieces_match, URI_VALIDATOR)) {
      85           4 :         if (pieces_match.size() == 4) {
      86           4 :             if (pieces_match[2].length() == 0)
      87           4 :                 instance(default_ns).lookupName(pieces_match[3], std::move(cb));
      88             :             else
      89           0 :                 instance(pieces_match[3].str()).lookupName(pieces_match[2], std::move(cb));
      90           4 :             return;
      91             :         }
      92             :     }
      93           0 :     JAMI_ERROR("Unable to parse URI: {}", uri);
      94           0 :     cb("", Response::invalidResponse);
      95           8 : }
      96             : 
      97          25 : NameDirectory::NameDirectory(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
      98          25 :     : serverUrl_(serverUrl)
      99          25 :     , logger_(std::move(l))
     100          50 :     , httpContext_(Manager::instance().ioContext())
     101             : {
     102          25 :     if (!serverUrl_.empty() && serverUrl_.back() == '/')
     103           0 :         serverUrl_.pop_back();
     104          25 :     resolver_ = std::make_shared<dht::http::Resolver>(*httpContext_, serverUrl, logger_);
     105          25 :     cachePath_ = fileutils::get_cache_dir() / CACHE_DIRECTORY / resolver_->get_url().host;
     106          25 : }
     107             : 
     108          25 : NameDirectory::~NameDirectory()
     109             : {
     110          25 :     decltype(requests_) requests;
     111             :     {
     112          25 :         std::lock_guard lk(requestsMtx_);
     113          25 :         requests = std::move(requests_);
     114          25 :     }
     115          25 :     for (auto& req : requests)
     116           0 :         req->cancel();
     117          25 : }
     118             : 
     119             : void
     120          25 : NameDirectory::load()
     121             : {
     122          25 :     loadCache();
     123          25 : }
     124             : 
     125             : NameDirectory&
     126         803 : NameDirectory::instance(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
     127             : {
     128         803 :     const std::string& s = serverUrl.empty() ? DEFAULT_SERVER_HOST : serverUrl;
     129             :     static std::mutex instanceMtx {};
     130             : 
     131         803 :     std::lock_guard lock(instanceMtx);
     132         803 :     static std::map<std::string, NameDirectory> instances {};
     133         803 :     auto it = instances.find(s);
     134         803 :     if (it != instances.end())
     135         778 :         return it->second;
     136          25 :     auto r = instances.emplace(std::piecewise_construct,
     137          25 :                                std::forward_as_tuple(s),
     138          25 :                                std::forward_as_tuple(s, l));
     139          25 :     if (r.second)
     140          25 :         r.first->second.load();
     141          25 :     return r.first->second;
     142         803 : }
     143             : 
     144             : void
     145         791 : NameDirectory::setHeaderFields(Request& request)
     146             : {
     147         791 :     request.set_header_field(restinio::http_field_t::user_agent, fmt::format("Jami ({}/{})",
     148         791 :         jami::platform(), jami::arch()));
     149         791 :     request.set_header_field(restinio::http_field_t::accept, "*/*");
     150         791 :     request.set_header_field(restinio::http_field_t::content_type, "application/json");
     151         791 : }
     152             : 
     153             : void
     154         789 : NameDirectory::lookupAddress(const std::string& addr, LookupCallback cb)
     155             : {
     156         789 :     auto cacheResult = nameCache(addr);
     157         789 :     if (not cacheResult.empty()) {
     158           1 :         cb(cacheResult, Response::found);
     159           1 :         return;
     160             :     }
     161         788 :     auto request = std::make_shared<Request>(*httpContext_,
     162         788 :                                              resolver_,
     163        2364 :                                              serverUrl_ + QUERY_ADDR + addr);
     164             :     try {
     165         788 :         request->set_method(restinio::http_method_get());
     166         788 :         setHeaderFields(*request);
     167        1576 :         request->add_on_done_callback(
     168         788 :             [this, cb = std::move(cb), addr](const dht::http::Response& response) {
     169         788 :                 if (response.status_code >= 400 && response.status_code < 500) {
     170         787 :                     auto cacheResult = nameCache(addr);
     171         787 :                     if (not cacheResult.empty())
     172           1 :                         cb(cacheResult, Response::found);
     173             :                     else
     174         786 :                         cb("", Response::notFound);
     175         788 :                 } else if (response.status_code != 200) {
     176           0 :                     JAMI_ERROR("Address lookup for {} on {} failed with code={}",
     177             :                                addr, serverUrl_, response.status_code);
     178           0 :                     cb("", Response::error);
     179             :                 } else {
     180             :                     try {
     181           1 :                         Json::Value json;
     182           1 :                         std::string err;
     183           1 :                         Json::CharReaderBuilder rbuilder;
     184           1 :                         auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
     185           2 :                         if (!reader->parse(response.body.data(),
     186           1 :                                            response.body.data() + response.body.size(),
     187             :                                            &json,
     188             :                                            &err)) {
     189           0 :                             JAMI_DBG("Address lookup for %s: Unable to parse server response: %s",
     190             :                                      addr.c_str(),
     191             :                                      response.body.c_str());
     192           0 :                             cb("", Response::error);
     193           0 :                             return;
     194             :                         }
     195           1 :                         auto name = json["name"].asString();
     196           1 :                         if (name.empty()) {
     197           0 :                             cb(name, Response::notFound);
     198           0 :                             return;
     199             :                         }
     200           3 :                         JAMI_DEBUG("Found name for {}: {}", addr, name);
     201             :                         {
     202           1 :                             std::lock_guard l(cacheLock_);
     203           1 :                             addrCache_.emplace(name, addr);
     204           1 :                             nameCache_.emplace(addr, name);
     205           1 :                         }
     206           1 :                         cb(name, Response::found);
     207           1 :                         scheduleCacheSave();
     208           1 :                     } catch (const std::exception& e) {
     209           0 :                         JAMI_ERROR("Error when performing address lookup: {}", e.what());
     210           0 :                         cb("", Response::error);
     211           0 :                     }
     212             :                 }
     213         788 :                 std::lock_guard lk(requestsMtx_);
     214         788 :                 if (auto req = response.request.lock())
     215         788 :                     requests_.erase(req);
     216         788 :             });
     217             :         {
     218         788 :             std::lock_guard lk(requestsMtx_);
     219         788 :             requests_.emplace(request);
     220         788 :         }
     221         788 :         request->send();
     222           0 :     } catch (const std::exception& e) {
     223           0 :         JAMI_ERROR("Error when performing address lookup: {}", e.what());
     224           0 :         std::lock_guard lk(requestsMtx_);
     225           0 :         if (request)
     226           0 :             requests_.erase(request);
     227           0 :     }
     228         789 : }
     229             : 
     230             : bool
     231           0 : NameDirectory::verify(const std::string& name,
     232             :                       const dht::crypto::PublicKey& pk,
     233             :                       const std::string& signature)
     234             : {
     235           0 :     return pk.checkSignature(std::vector<uint8_t>(name.begin(), name.end()),
     236           0 :                              base64::decode(signature));
     237             : }
     238             : 
     239             : void
     240           4 : NameDirectory::lookupName(const std::string& n, LookupCallback cb)
     241             : {
     242           4 :     std::string name {n};
     243           4 :     if (not validateName(name)) {
     244           2 :         cb("", Response::invalidResponse);
     245           2 :         return;
     246             :     }
     247           2 :     toLower(name);
     248           2 :     std::string cacheResult = addrCache(name);
     249           2 :     if (not cacheResult.empty()) {
     250           0 :         cb(cacheResult, Response::found);
     251           0 :         return;
     252             :     }
     253           2 :     auto request = std::make_shared<Request>(*httpContext_,
     254           2 :                                              resolver_,
     255           6 :                                              serverUrl_ + QUERY_NAME + name);
     256             :     try {
     257           2 :         request->set_method(restinio::http_method_get());
     258           2 :         setHeaderFields(*request);
     259           2 :         request->add_on_done_callback([this, name, cb = std::move(cb)](
     260           6 :                                           const dht::http::Response& response) {
     261           2 :             if (response.status_code >= 400 && response.status_code < 500)
     262           1 :                 cb("", Response::notFound);
     263           1 :             else if (response.status_code < 200 || response.status_code > 299) {
     264           0 :                 JAMI_ERROR("Name lookup for {} on {} failed with code={}",
     265             :                            name, serverUrl_, response.status_code);
     266           0 :                 cb("", Response::error);
     267           0 :             } else {
     268             :                 try {
     269           1 :                     Json::Value json;
     270           1 :                     std::string err;
     271           1 :                     Json::CharReaderBuilder rbuilder;
     272           1 :                     auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
     273           2 :                     if (!reader->parse(response.body.data(),
     274           1 :                                        response.body.data() + response.body.size(),
     275             :                                        &json,
     276             :                                        &err)) {
     277           0 :                         JAMI_ERROR("Name lookup for {}: Unable to parse server response: {}",
     278             :                                  name, response.body);
     279           0 :                         cb("", Response::error);
     280           0 :                         return;
     281             :                     }
     282           1 :                     auto addr = json["addr"].asString();
     283           1 :                     auto publickey = json["publickey"].asString();
     284           1 :                     auto signature = json["signature"].asString();
     285             : 
     286           1 :                     if (!addr.compare(0, HEX_PREFIX.size(), HEX_PREFIX))
     287           0 :                         addr = addr.substr(HEX_PREFIX.size());
     288           1 :                     if (addr.empty()) {
     289           0 :                         cb("", Response::notFound);
     290           0 :                         return;
     291             :                     }
     292           1 :                     if (not publickey.empty() and not signature.empty()) {
     293             :                         try {
     294           0 :                             auto pk = dht::crypto::PublicKey(base64::decode(publickey));
     295           0 :                             if (pk.getId().toString() != addr or not verify(name, pk, signature)) {
     296           0 :                                 cb("", Response::invalidResponse);
     297           0 :                                 return;
     298             :                             }
     299           0 :                         } catch (const std::exception& e) {
     300           0 :                             cb("", Response::invalidResponse);
     301           0 :                             return;
     302           0 :                         }
     303             :                     }
     304           3 :                     JAMI_DEBUG("Found address for {}: {}", name, addr);
     305             :                     {
     306           1 :                         std::lock_guard l(cacheLock_);
     307           1 :                         addrCache_.emplace(name, addr);
     308           1 :                         nameCache_.emplace(addr, name);
     309           1 :                     }
     310           1 :                     cb(addr, Response::found);
     311           1 :                     scheduleCacheSave();
     312           1 :                 } catch (const std::exception& e) {
     313           0 :                     JAMI_ERROR("Error when performing name lookup: {}", e.what());
     314           0 :                     cb("", Response::error);
     315           0 :                 }
     316             :             }
     317           2 :             if (auto req = response.request.lock())
     318           2 :                 requests_.erase(req);
     319             :         });
     320             :         {
     321           2 :             std::lock_guard lk(requestsMtx_);
     322           2 :             requests_.emplace(request);
     323           2 :         }
     324           2 :         request->send();
     325           0 :     } catch (const std::exception& e) {
     326           0 :         JAMI_ERROR("Name lookup for {} failed: {}", name, e.what());
     327           0 :         std::lock_guard lk(requestsMtx_);
     328           0 :         if (request)
     329           0 :             requests_.erase(request);
     330           0 :     }
     331           4 : }
     332             : 
     333             : bool
     334           5 : NameDirectory::validateName(const std::string& name) const
     335             : {
     336           5 :     return std::regex_match(name, NAME_VALIDATOR);
     337             : }
     338             : 
     339             : using Blob = std::vector<uint8_t>;
     340             : void
     341           1 : NameDirectory::registerName(const std::string& addr,
     342             :                             const std::string& n,
     343             :                             const std::string& owner,
     344             :                             RegistrationCallback cb,
     345             :                             const std::string& signedname,
     346             :                             const std::string& publickey)
     347             : {
     348           1 :     std::string name {n};
     349           1 :     if (not validateName(name)) {
     350           0 :         cb(RegistrationResponse::invalidName, name);
     351           0 :         return;
     352             :     }
     353           1 :     toLower(name);
     354           1 :     auto cacheResult = addrCache(name);
     355           1 :     if (not cacheResult.empty()) {
     356           0 :         if (cacheResult == addr)
     357           0 :             cb(RegistrationResponse::success, name);
     358             :         else
     359           0 :             cb(RegistrationResponse::alreadyTaken, name);
     360           0 :         return;
     361             :     }
     362             :     {
     363           1 :         std::lock_guard l(cacheLock_);
     364           1 :         if (not pendingRegistrations_.emplace(addr, name).second) {
     365           0 :             JAMI_WARNING("RegisterName: already registering name {} {}", addr, name);
     366           0 :             cb(RegistrationResponse::error, name);
     367           0 :             return;
     368             :         }
     369           1 :     }
     370             :     std::string body = fmt::format("{{\"addr\":\"{}\",\"owner\":\"{}\",\"signature\":\"{}\",\"publickey\":\"{}\"}}",
     371             :         addr,
     372             :         owner,
     373             :         signedname,
     374           2 :         base64::encode(publickey));
     375           1 :     auto request = std::make_shared<Request>(*httpContext_,
     376           1 :                                              resolver_,
     377           3 :                                              serverUrl_ + QUERY_NAME + name);
     378             :     try {
     379           1 :         request->set_method(restinio::http_method_post());
     380           1 :         setHeaderFields(*request);
     381           1 :         request->set_body(body);
     382             : 
     383           3 :         JAMI_WARNING("RegisterName: sending request {} {}", addr, name);
     384             : 
     385           2 :         request->add_on_done_callback(
     386           1 :             [this, name, addr, cb = std::move(cb)](const dht::http::Response& response) {
     387             :                 {
     388           1 :                     std::lock_guard l(cacheLock_);
     389           1 :                     pendingRegistrations_.erase(name);
     390           1 :                 }
     391           1 :                 if (response.status_code == 400) {
     392           0 :                     cb(RegistrationResponse::incompleteRequest, name);
     393           0 :                     JAMI_ERROR("RegistrationResponse::incompleteRequest");
     394           1 :                 } else if (response.status_code == 401) {
     395           0 :                     cb(RegistrationResponse::signatureVerificationFailed, name);
     396           0 :                     JAMI_ERROR("RegistrationResponse::signatureVerificationFailed");
     397           1 :                 } else if (response.status_code == 403) {
     398           0 :                     cb(RegistrationResponse::alreadyTaken, name);
     399           0 :                     JAMI_ERROR("RegistrationResponse::alreadyTaken");
     400           1 :                 } else if (response.status_code == 409) {
     401           0 :                     cb(RegistrationResponse::alreadyTaken, name);
     402           0 :                     JAMI_ERROR("RegistrationResponse::alreadyTaken");
     403           1 :                 } else if (response.status_code > 400 && response.status_code < 500) {
     404           0 :                     cb(RegistrationResponse::alreadyTaken, name);
     405           0 :                     JAMI_ERROR("RegistrationResponse::alreadyTaken");
     406           1 :                 } else if (response.status_code < 200 || response.status_code > 299) {
     407           0 :                     cb(RegistrationResponse::error, name);
     408           0 :                     JAMI_ERROR("RegistrationResponse::error");
     409           0 :                 } else {
     410           1 :                     Json::Value json;
     411           1 :                     std::string err;
     412           1 :                     Json::CharReaderBuilder rbuilder;
     413             : 
     414           1 :                     auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
     415           2 :                     if (!reader->parse(response.body.data(),
     416           1 :                                        response.body.data() + response.body.size(),
     417             :                                        &json,
     418             :                                        &err)) {
     419           0 :                         cb(RegistrationResponse::error, name);
     420           0 :                         return;
     421             :                     }
     422           1 :                     auto success = json["success"].asBool();
     423           3 :                     JAMI_DEBUG("Got reply for registration of {} {}: {}",
     424             :                              name, addr, success ? "success" : "failure");
     425           1 :                     if (success) {
     426           1 :                         std::lock_guard l(cacheLock_);
     427           1 :                         addrCache_.emplace(name, addr);
     428           1 :                         nameCache_.emplace(addr, name);
     429           1 :                     }
     430           1 :                     cb(success ? RegistrationResponse::success : RegistrationResponse::error, name);
     431           1 :                 }
     432           1 :                 std::lock_guard lk(requestsMtx_);
     433           1 :                 if (auto req = response.request.lock())
     434           1 :                     requests_.erase(req);
     435           1 :             });
     436             :         {
     437           1 :             std::lock_guard lk(requestsMtx_);
     438           1 :             requests_.emplace(request);
     439           1 :         }
     440           1 :         request->send();
     441           0 :     } catch (const std::exception& e) {
     442           0 :         JAMI_ERROR("Error when performing name registration: {}", e.what());
     443           0 :         cb(RegistrationResponse::error, name);
     444             :         {
     445           0 :             std::lock_guard l(cacheLock_);
     446           0 :             pendingRegistrations_.erase(name);
     447           0 :         }
     448           0 :         std::lock_guard lk(requestsMtx_);
     449           0 :         if (request)
     450           0 :             requests_.erase(request);
     451           0 :     }
     452           1 : }
     453             : 
     454             : void
     455           2 : NameDirectory::scheduleCacheSave()
     456             : {
     457             :     // JAMI_DBG("Scheduling cache save to %s", cachePath_.c_str());
     458           4 :     std::weak_ptr<Task> task = Manager::instance().scheduler().scheduleIn(
     459           4 :         [this] { dht::ThreadPool::io().run([this] { saveCache(); }); }, SAVE_INTERVAL);
     460           2 :     std::swap(saveTask_, task);
     461           2 :     if (auto old = task.lock())
     462           2 :         old->cancel();
     463           2 : }
     464             : 
     465             : void
     466           1 : NameDirectory::saveCache()
     467             : {
     468           1 :     dhtnet::fileutils::recursive_mkdir(fileutils::get_cache_dir() / CACHE_DIRECTORY);
     469           1 :     std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
     470           1 :     std::ofstream file(cachePath_, std::ios::trunc | std::ios::binary);
     471             :     {
     472           1 :         std::lock_guard l(cacheLock_);
     473           1 :         msgpack::pack(file, nameCache_);
     474           1 :     }
     475           3 :     JAMI_DEBUG("Saved {:d} name-address mappings to {}",
     476             :              nameCache_.size(), cachePath_);
     477           1 : }
     478             : 
     479             : void
     480          25 : NameDirectory::loadCache()
     481             : {
     482          25 :     msgpack::unpacker pac;
     483             : 
     484             :     // read file
     485             :     {
     486          25 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
     487          25 :         std::ifstream file(cachePath_);
     488          25 :         if (!file.is_open()) {
     489          75 :             JAMI_DEBUG("Unable to load {}", cachePath_);
     490          25 :             return;
     491             :         }
     492           0 :         std::string line;
     493           0 :         while (std::getline(file, line)) {
     494           0 :             pac.reserve_buffer(line.size());
     495           0 :             memcpy(pac.buffer(), line.data(), line.size());
     496           0 :             pac.buffer_consumed(line.size());
     497             :         }
     498          50 :     }
     499             : 
     500             :     // load values
     501           0 :     std::lock_guard l(cacheLock_);
     502           0 :     msgpack::object_handle oh;
     503           0 :     if (pac.next(oh))
     504           0 :         oh.get().convert(nameCache_);
     505           0 :     for (const auto& m : nameCache_)
     506           0 :         addrCache_.emplace(m.second, m.first);
     507           0 :     JAMI_DEBUG("Loaded {:d} name-address mappings", nameCache_.size());
     508          25 : }
     509             : 
     510             : } // namespace jami

Generated by: LCOV version 1.14