LCOV - code coverage report
Current view: top level - src/jamidht - namedirectory.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 207 298 69.5 %
Date: 2024-04-25 08:05:53 Functions: 19 21 90.5 %

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

Generated by: LCOV version 1.14