LCOV - code coverage report
Current view: top level - src/jamidht - svc_discovery_channel_handler.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 75.7 % 288 218
Test Date: 2026-08-23 08:52:56 Functions: 81.5 % 27 22

            Line data    Source code
       1              : /*
       2              :  *  Copyright (C) 2004-2026 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              : #include "jamidht/svc_discovery_channel_handler.h"
      18              : 
      19              : #include "jamidht/account_manager.h"
      20              : #include "jamidht/contact_list.h"
      21              : #include "jamidht/service_manager.h"
      22              : #include "logger.h"
      23              : 
      24              : #include <algorithm>
      25              : #include <cstring>
      26              : #include <fstream>
      27              : 
      28              : namespace jami {
      29              : 
      30              : namespace {
      31              : 
      32              : template<typename T>
      33              : bool
      34         2516 : sendMsg(const std::shared_ptr<dhtnet::ChannelSocket>& s, const T& msg)
      35              : {
      36         2516 :     if (!s)
      37            0 :         return false;
      38         2516 :     msgpack::sbuffer buf;
      39         2516 :     msgpack::pack(buf, msg);
      40         2516 :     std::error_code ec;
      41         2516 :     auto sent = s->write(reinterpret_cast<const uint8_t*>(buf.data()), buf.size(), ec);
      42         2516 :     if (ec) {
      43            0 :         JAMI_WARNING("[SvcDiscovery] write error: {}", ec.message());
      44            0 :         return false;
      45              :     }
      46         2516 :     return sent == buf.size();
      47         2516 : }
      48              : 
      49              : constexpr const char* SVC_CACHE_FILENAME = "svc_discovery_cache.msgpack";
      50              : 
      51              : } // namespace
      52              : 
      53          721 : SvcDiscoveryChannelHandler::SvcDiscoveryChannelHandler(const std::shared_ptr<JamiAccount>& acc,
      54              :                                                        dhtnet::ConnectionManager& cm,
      55          721 :                                                        std::filesystem::path cachePath)
      56              :     : ChannelHandlerInterface()
      57          721 :     , account_(acc)
      58          721 :     , connectionManager_(cm)
      59          721 :     , state_(std::make_shared<State>())
      60         1442 :     , cachePath_(std::move(cachePath))
      61              : {
      62          721 :     loadCache();
      63              : 
      64              :     // Install the default response callback that populates the cache.
      65          721 :     auto cachePathCopy = cachePath_;
      66         1442 :     state_->responseCb = [state = state_, cachePathCopy](const std::string& peerAccountUri,
      67              :                                                          const std::string& peerDeviceId,
      68              :                                                          const std::vector<svc_protocol::SvcInfo>& services) {
      69         1258 :         if (peerDeviceId.empty())
      70            0 :             return;
      71         1257 :         DeviceId dev;
      72              :         try {
      73         1257 :             dev = DeviceId(peerDeviceId);
      74            0 :         } catch (...) {
      75            0 :             return;
      76            0 :         }
      77         1258 :         CacheUpdateCb updateCb;
      78         1258 :         msgpack::sbuffer buf;
      79              :         {
      80         1258 :             std::lock_guard lk(state->mtx);
      81         1258 :             state->cache[dev] = State::CachedDeviceServices {peerAccountUri, services};
      82         1258 :             updateCb = state->cacheUpdateCb;
      83         1258 :             msgpack::pack(buf, state->cache);
      84         1258 :         }
      85              :         // Write to disk outside the lock
      86         1258 :         if (!cachePathCopy.empty()) {
      87         1258 :             std::error_code ec;
      88         1258 :             std::filesystem::create_directories(cachePathCopy, ec);
      89         1258 :             std::ofstream out(cachePathCopy / SVC_CACHE_FILENAME, std::ios::binary | std::ios::trunc);
      90         1258 :             if (out)
      91         1258 :                 out.write(buf.data(), buf.size());
      92         1258 :         }
      93         1258 :         if (updateCb)
      94         1258 :             updateCb(peerAccountUri, dev, services);
      95         1979 :     };
      96          721 : }
      97              : 
      98         1442 : SvcDiscoveryChannelHandler::~SvcDiscoveryChannelHandler() = default;
      99              : 
     100              : void
     101          721 : SvcDiscoveryChannelHandler::onCacheUpdated(CacheUpdateCb cb)
     102              : {
     103          721 :     std::lock_guard lk(state_->mtx);
     104          721 :     state_->cacheUpdateCb = std::move(cb);
     105          721 : }
     106              : 
     107              : void
     108            0 : SvcDiscoveryChannelHandler::setOnResponse(ResponseCb cb)
     109              : {
     110            0 :     std::lock_guard lk(state_->mtx);
     111            0 :     state_->responseCb = std::move(cb);
     112            0 : }
     113              : 
     114              : svc_protocol::SvcDiscResponse
     115         1256 : SvcDiscoveryChannelHandler::buildResponse(JamiAccount& account, const std::string& peerAccountUri)
     116              : {
     117         1256 :     svc_protocol::SvcDiscResponse out;
     118         1256 :     if (const auto& id = account.identity().first)
     119         1256 :         out.device = id->getPublicKey().getLongId().toString();
     120            0 :     auto checker = [&account](const std::string& uri) {
     121            0 :         return account.isContact(uri);
     122         1256 :     };
     123         1256 :     auto visible = account.serviceManager().getVisibleServices(peerAccountUri, checker);
     124         1256 :     out.services.reserve(visible.size());
     125         1261 :     for (auto& r : visible) {
     126            5 :         svc_protocol::SvcInfo info;
     127            5 :         info.id = std::move(r.id);
     128            5 :         info.name = std::move(r.name);
     129            5 :         info.description = std::move(r.description);
     130            5 :         info.proto = "tcp";
     131            5 :         info.scheme = std::move(r.scheme);
     132            5 :         info.preferred_port = r.preferredPort;
     133            5 :         out.services.push_back(std::move(info));
     134            5 :     }
     135         2512 :     return out;
     136         1256 : }
     137              : 
     138              : void
     139         1259 : SvcDiscoveryChannelHandler::connect(const DeviceId& deviceId,
     140              :                                     const std::string& /*name*/,
     141              :                                     ConnectCb&& cb,
     142              :                                     const std::string& /*connectionType*/,
     143              :                                     bool /*forceNewConnection*/)
     144              : {
     145         1259 :     auto userCb = std::make_shared<ConnectCb>(std::move(cb));
     146         1259 :     auto state = state_;
     147         1259 :     auto wacc = account_;
     148         1259 :     connectionManager_.connectDevice(deviceId,
     149         2518 :                                      std::string(svc_protocol::DiscoveryChannelName),
     150         2518 :                                      [userCb, state, wacc, this](std::shared_ptr<dhtnet::ChannelSocket> socket,
     151              :                                                                  const DeviceId& dev) {
     152         1259 :                                          if (socket) {
     153              :                                              // Retain the channel for its full lifetime so the response
     154              :                                              // can come back even if no one else holds it.
     155         1253 :                                              auto cert = socket->peerCertificate();
     156         1253 :                                              std::string peerAccountUri;
     157         1253 :                                              if (cert && cert->issuer)
     158         1253 :                                                  peerAccountUri = cert->issuer->getId().toString();
     159              :                                              {
     160         1253 :                                                  std::lock_guard lk(state->mtx);
     161         1253 :                                                  state->channels[peerAccountUri].push_back(socket);
     162         1253 :                                              }
     163         1253 :                                              socket->onShutdown([state, ws = std::weak_ptr(socket), peerAccountUri](
     164              :                                                                     const std::error_code&) {
     165         1253 :                                                  auto s = ws.lock();
     166         1253 :                                                  if (!s)
     167            0 :                                                      return;
     168         1253 :                                                  std::lock_guard lk(state->mtx);
     169         1252 :                                                  auto it = state->channels.find(peerAccountUri);
     170         1253 :                                                  if (it != state->channels.end()) {
     171         1253 :                                                      auto& vec = it->second;
     172         1253 :                                                      vec.erase(std::remove(vec.begin(), vec.end(), s), vec.end());
     173         1253 :                                                      if (vec.empty())
     174          622 :                                                          state->channels.erase(it);
     175              :                                                  }
     176         1253 :                                              });
     177              :                                              // The initiating side immediately writes a Query so the server
     178              :                                              // can respond. We need to install a reader to handle the
     179              :                                              // response too.
     180         1253 :                                              installReader(socket, peerAccountUri);
     181         3759 :                                              if (!sendMsg(socket, svc_protocol::SvcDiscQuery {}))
     182            0 :                                                  JAMI_WARNING("[SvcDiscovery] failed to send SvcDiscQuery to {}",
     183              :                                                               peerAccountUri);
     184         1253 :                                          }
     185         1259 :                                          if (*userCb)
     186         1259 :                                              (*userCb)(socket, dev);
     187         1259 :                                      });
     188         2518 : }
     189              : 
     190              : bool
     191         1254 : SvcDiscoveryChannelHandler::onRequest(const std::shared_ptr<dht::crypto::Certificate>& peer, const std::string& /*name*/)
     192              : {
     193         1254 :     return peer && peer->issuer;
     194              : }
     195              : 
     196              : void
     197         2505 : SvcDiscoveryChannelHandler::onReady(const std::shared_ptr<dht::crypto::Certificate>& peer,
     198              :                                     const std::string& /*name*/,
     199              :                                     std::shared_ptr<dhtnet::ChannelSocket> channel)
     200              : {
     201         2505 :     if (!channel)
     202            0 :         return;
     203              :     // The initiator already retains the socket and installs its reader from
     204              :     // connect(); no need to do the work twice.
     205         2505 :     if (channel->isInitiator())
     206         1253 :         return;
     207         1252 :     if (!peer || !peer->issuer) {
     208            0 :         channel->shutdown();
     209            0 :         return;
     210              :     }
     211         1252 :     auto peerUri = peer->issuer->getId().toString();
     212              :     {
     213         1252 :         std::lock_guard lk(state_->mtx);
     214         1252 :         state_->channels[peerUri].push_back(channel);
     215         1252 :     }
     216         1252 :     auto state = state_;
     217         1252 :     channel->onShutdown([state, ws = std::weak_ptr(channel), peerUri](const std::error_code&) {
     218         1252 :         auto s = ws.lock();
     219         1252 :         if (!s)
     220            0 :             return;
     221         1252 :         std::lock_guard lk(state->mtx);
     222         1252 :         auto it = state->channels.find(peerUri);
     223         1252 :         if (it != state->channels.end()) {
     224         1251 :             auto& vec = it->second;
     225         1252 :             vec.erase(std::remove(vec.begin(), vec.end(), s), vec.end());
     226         1252 :             if (vec.empty())
     227          620 :                 state->channels.erase(it);
     228              :         }
     229         1251 :     });
     230         1252 :     installReader(channel, peer->issuer->getId().toString());
     231         1252 : }
     232              : 
     233              : void
     234         2504 : SvcDiscoveryChannelHandler::installReader(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
     235              :                                           std::string peerAccountUri)
     236              : {
     237         2504 :     auto reader = std::make_shared<msgpack::unpacker>();
     238         2504 :     reader->reserve_buffer(4096);
     239         2505 :     auto wacc = account_;
     240         2505 :     auto state = state_;
     241         2503 :     std::weak_ptr<dhtnet::ChannelSocket> wsock = channel;
     242              : 
     243         2502 :     channel->setOnRecv([reader, wacc, state, wsock, peerAccountUri = std::move(peerAccountUri)](const uint8_t* data,
     244              :                                                                                                 size_t size) -> ssize_t {
     245         2506 :         if (size == 0)
     246            0 :             return 0;
     247         2506 :         if (reader->buffer_capacity() < size)
     248            0 :             reader->reserve_buffer(size);
     249         2505 :         std::memcpy(reader->buffer(), data, size);
     250         2505 :         reader->buffer_consumed(size);
     251              : 
     252         2505 :         msgpack::object_handle oh;
     253         5012 :         while (reader->next(oh)) {
     254         2506 :             const auto& obj = oh.get();
     255         2507 :             const auto type = svc_protocol::peekType(obj);
     256         2507 :             const auto v = svc_protocol::peekVersion(obj);
     257         2507 :             auto sock = wsock.lock();
     258         2507 :             if (!sock)
     259            0 :                 return static_cast<ssize_t>(size);
     260              : 
     261         2507 :             if (type == svc_protocol::MsgType::Query) {
     262         1249 :                 auto acc = wacc.lock();
     263         1249 :                 if (!acc) {
     264            0 :                     sock->shutdown();
     265            0 :                     continue;
     266              :                 }
     267         1249 :                 if (v > svc_protocol::MaxVersion) {
     268            0 :                     svc_protocol::SvcDiscVersionMismatch vm;
     269            0 :                     vm.max_supported = svc_protocol::MaxVersion;
     270            0 :                     sendMsg(sock, vm);
     271            0 :                     continue;
     272            0 :                 }
     273         1249 :                 auto resp = SvcDiscoveryChannelHandler::buildResponse(*acc, peerAccountUri);
     274         1249 :                 JAMI_LOG("[SvcDiscovery] returning {} service(s) to peer={}", resp.services.size(), peerAccountUri);
     275         1249 :                 if (!sendMsg(sock, resp))
     276            0 :                     JAMI_WARNING("[SvcDiscovery] failed to send SvcDiscResponse to {}", peerAccountUri);
     277         2507 :             } else if (type == svc_protocol::MsgType::ServiceList) {
     278         1244 :                 svc_protocol::SvcDiscResponse resp;
     279              :                 try {
     280         1244 :                     obj.convert(resp);
     281            0 :                 } catch (const std::exception& e) {
     282            0 :                     JAMI_WARNING("[SvcDiscovery] bad service_list: {}", e.what());
     283            0 :                     continue;
     284            0 :                 }
     285         1244 :                 ResponseCb cb;
     286              :                 {
     287         1243 :                     std::lock_guard lk(state->mtx);
     288         1244 :                     cb = state->responseCb;
     289         1244 :                 }
     290         1244 :                 if (cb)
     291         1244 :                     cb(peerAccountUri, resp.device, resp.services);
     292              :                 else
     293            0 :                     JAMI_WARNING("[SvcDiscovery] no responseCb set; dropping {} services from {}",
     294              :                                  resp.services.size(),
     295              :                                  peerAccountUri);
     296         1258 :             } else if (type == svc_protocol::MsgType::ServiceUpdate) {
     297           14 :                 svc_protocol::SvcDiscServiceUpdate update;
     298              :                 try {
     299           14 :                     obj.convert(update);
     300            0 :                 } catch (const std::exception& e) {
     301            0 :                     JAMI_WARNING("[SvcDiscovery] bad service_update: {}", e.what());
     302            0 :                     continue;
     303            0 :                 }
     304           14 :                 ResponseCb cb;
     305              :                 {
     306           14 :                     std::lock_guard lk(state->mtx);
     307           14 :                     cb = state->responseCb;
     308           14 :                 }
     309           14 :                 if (cb)
     310           14 :                     cb(peerAccountUri, update.device, update.services);
     311           14 :             } else if (type == svc_protocol::MsgType::VersionMismatch || type == svc_protocol::MsgType::Error) {
     312            0 :                 ResponseCb cb;
     313              :                 {
     314            0 :                     std::lock_guard lk(state->mtx);
     315            0 :                     cb = state->responseCb;
     316            0 :                 }
     317            0 :                 if (cb)
     318            0 :                     cb(peerAccountUri, std::string {}, {});
     319            0 :             } else {
     320            0 :                 JAMI_WARNING("[SvcDiscovery] unknown message type '{}'", type);
     321              :             }
     322         2507 :         }
     323         2507 :         return static_cast<ssize_t>(size);
     324         2507 :     });
     325         2504 : }
     326              : 
     327              : void
     328            8 : SvcDiscoveryChannelHandler::broadcastServiceUpdate()
     329              : {
     330            8 :     auto acc = account_.lock();
     331            8 :     if (!acc)
     332            0 :         return;
     333              : 
     334              :     // Snapshot connected peers and their channels under the lock.
     335            8 :     std::map<std::string, std::vector<std::shared_ptr<dhtnet::ChannelSocket>>> snapshot;
     336              :     {
     337            8 :         std::lock_guard lk(state_->mtx);
     338            8 :         snapshot = state_->channels;
     339            8 :     }
     340              : 
     341            8 :     if (snapshot.empty())
     342            1 :         return;
     343              : 
     344            7 :     std::size_t peerCount = 0;
     345           14 :     for (const auto& [peerUri, sockets] : snapshot) {
     346              :         // Build a per-peer filtered service list.
     347            7 :         auto resp = buildResponse(*acc, peerUri);
     348            7 :         svc_protocol::SvcDiscServiceUpdate update;
     349            7 :         update.device = resp.device;
     350            7 :         update.services = std::move(resp.services);
     351              : 
     352           21 :         for (const auto& sock : sockets)
     353           14 :             sendMsg(sock, update);
     354            7 :         ++peerCount;
     355            7 :     }
     356            7 :     JAMI_LOG("[SvcDiscovery] broadcast service_update to {} peer(s)", peerCount);
     357            9 : }
     358              : 
     359              : void
     360         1259 : SvcDiscoveryChannelHandler::refreshDevice(const std::string& /*peerUri*/, const DeviceId& deviceId)
     361              : {
     362         3777 :     connect(deviceId,
     363         2518 :             std::string(svc_protocol::DiscoveryChannelName),
     364         1258 :             [](std::shared_ptr<dhtnet::ChannelSocket>, const DeviceId&) {
     365              :                 // Connection callback — nothing needed here; the cache is
     366              :                 // updated when the response is read via installReader/responseCb.
     367         1259 :             });
     368         1258 : }
     369              : 
     370              : std::vector<SvcDiscoveryChannelHandler::CachedSvcInfo>
     371         1928 : SvcDiscoveryChannelHandler::getCachedServices(const std::string& peerUri) const
     372              : {
     373         1928 :     std::vector<CachedSvcInfo> result;
     374         1928 :     std::lock_guard lk(state_->mtx);
     375        11596 :     for (const auto& [dev, entry] : state_->cache) {
     376         9672 :         if (entry.peerUri == peerUri) {
     377         1513 :             for (const auto& svc : entry.services)
     378           10 :                 result.push_back(CachedSvcInfo {dev, svc});
     379              :         }
     380              :     }
     381         3856 :     return result;
     382         1928 : }
     383              : 
     384              : void
     385            0 : SvcDiscoveryChannelHandler::removeDevice(const DeviceId& deviceId)
     386              : {
     387              :     {
     388            0 :         std::lock_guard lk(state_->mtx);
     389            0 :         state_->cache.erase(deviceId);
     390            0 :     }
     391            0 :     saveCache();
     392            0 : }
     393              : 
     394              : void
     395            0 : SvcDiscoveryChannelHandler::saveCache() const
     396              : {
     397            0 :     if (cachePath_.empty())
     398            0 :         return;
     399            0 :     std::error_code ec;
     400            0 :     std::filesystem::create_directories(cachePath_, ec);
     401            0 :     std::ofstream out(cachePath_ / SVC_CACHE_FILENAME, std::ios::binary | std::ios::trunc);
     402            0 :     if (out) {
     403            0 :         std::lock_guard lk(state_->mtx);
     404            0 :         msgpack::pack(out, state_->cache);
     405            0 :     } else {
     406            0 :         JAMI_WARNING("[SvcDiscovery] failed to save cache to disk");
     407              :     }
     408            0 : }
     409              : 
     410              : void
     411          721 : SvcDiscoveryChannelHandler::loadCache()
     412              : {
     413          721 :     if (cachePath_.empty())
     414            0 :         return;
     415          721 :     auto path = cachePath_ / SVC_CACHE_FILENAME;
     416          721 :     std::ifstream in(path, std::ios::binary);
     417          721 :     if (!in)
     418          715 :         return;
     419            6 :     std::string content((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
     420              :     try {
     421            6 :         auto oh = msgpack::unpack(content.data(), content.size());
     422            6 :         std::lock_guard lk(state_->mtx);
     423            6 :         oh.get().convert(state_->cache);
     424            6 :     } catch (const std::exception& e) {
     425            0 :         JAMI_WARNING("[SvcDiscovery] failed to load cache: {}", e.what());
     426            0 :         return;
     427            0 :     }
     428            6 :     JAMI_LOG("[SvcDiscovery] loaded {} cached device entries from disk", state_->cache.size());
     429         1436 : }
     430              : 
     431              : } // namespace jami
        

Generated by: LCOV version 2.0-1