LCOV - code coverage report
Current view: top level - src/jamidht - service_manager.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 92.1 % 202 186
Test Date: 2026-08-23 08:52:56 Functions: 94.7 % 19 18

            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/service_manager.h"
      18              : 
      19              : #include "fileutils.h"
      20              : #include "json_utils.h"
      21              : #include "logger.h"
      22              : 
      23              : #include <algorithm>
      24              : #include <fstream>
      25              : #include <random>
      26              : #include <system_error>
      27              : #include <string_view>
      28              : 
      29              : namespace jami {
      30              : 
      31              : namespace {
      32              : 
      33              : using namespace std::literals;
      34              : 
      35              : constexpr std::string_view SERVICES_FILENAME = "exposed_services.json";
      36              : 
      37              : std::string
      38           32 : policyToString(AccessPolicy p)
      39              : {
      40           32 :     switch (p) {
      41           16 :     case AccessPolicy::CONTACTS_ONLY:
      42           16 :         return "contacts"s;
      43            4 :     case AccessPolicy::SPECIFIC_CONTACTS:
      44            4 :         return "specific"s;
      45           12 :     case AccessPolicy::PUBLIC:
      46           12 :         return "public"s;
      47              :     }
      48            0 :     return "contacts"s;
      49              : }
      50              : 
      51              : constexpr AccessPolicy
      52            2 : policyFromString(std::string_view s)
      53              : {
      54            2 :     if (s == "specific"sv)
      55            1 :         return AccessPolicy::SPECIFIC_CONTACTS;
      56            1 :     if (s == "public"sv)
      57            0 :         return AccessPolicy::PUBLIC;
      58            1 :     return AccessPolicy::CONTACTS_ONLY;
      59              : }
      60              : 
      61              : Json::Value
      62           32 : toJson(const ServiceRecord& r)
      63              : {
      64           32 :     Json::Value v(Json::objectValue);
      65           32 :     v["id"] = r.id;
      66           32 :     v["type"] = r.type;
      67           32 :     v["name"] = r.name;
      68           32 :     v["description"] = r.description;
      69           32 :     v["scheme"] = r.scheme;
      70           32 :     v["localHost"] = r.localHost;
      71           32 :     v["localPort"] = static_cast<Json::UInt>(r.localPort);
      72           32 :     v["preferredPort"] = static_cast<Json::UInt>(r.preferredPort);
      73           32 :     v["directory"] = r.directory;
      74           32 :     v["policy"] = policyToString(r.policy);
      75           32 :     Json::Value allowed(Json::arrayValue);
      76           38 :     for (const auto& a : r.allowedContacts)
      77            6 :         allowed.append(a);
      78           32 :     v["allowedContacts"] = std::move(allowed);
      79           32 :     v["enabled"] = r.enabled;
      80           64 :     return v;
      81           32 : }
      82              : 
      83              : bool
      84            2 : fromJson(const Json::Value& v, ServiceRecord& r)
      85              : {
      86            2 :     if (!v.isObject())
      87            0 :         return false;
      88            2 :     r.id = v.get("id", "").asString();
      89            2 :     r.type = v.get("type", "custom").asString();
      90            2 :     if (r.type.empty())
      91            0 :         r.type = "custom";
      92            2 :     r.name = v.get("name", "").asString();
      93            2 :     r.description = v.get("description", "").asString();
      94            2 :     r.scheme = v.get("scheme", "").asString();
      95            2 :     r.localHost = v.get("localHost", "localhost").asString();
      96            2 :     r.localPort = static_cast<uint16_t>(v.get("localPort", 0).asUInt());
      97            2 :     r.preferredPort = static_cast<uint16_t>(v.get("preferredPort", 0).asUInt());
      98            2 :     r.directory = v.get("directory", "").asString();
      99            2 :     r.policy = policyFromString(v.get("policy", "contacts").asString());
     100            2 :     r.allowedContacts.clear();
     101            2 :     if (v.isMember("allowedContacts") && v["allowedContacts"].isArray()) {
     102            4 :         for (const auto& a : v["allowedContacts"])
     103            2 :             r.allowedContacts.push_back(a.asString());
     104              :     }
     105            2 :     r.enabled = v.get("enabled", true).asBool();
     106            2 :     return !r.id.empty();
     107              : }
     108              : 
     109              : } // namespace
     110              : 
     111              : std::string
     112         1024 : generateServiceUuid(std::mt19937_64& rng)
     113              : {
     114              :     // RFC 4122 v4 UUID.
     115         1024 :     std::uniform_int_distribution<uint64_t> dist;
     116         1024 :     uint64_t a = dist(rng);
     117         1024 :     uint64_t b = dist(rng);
     118              :     // Set version (0100) and variant (10).
     119         1024 :     a = (a & 0xffffffffffff0fffULL) | 0x0000000000004000ULL;
     120         1024 :     b = (b & 0x3fffffffffffffffULL) | 0x8000000000000000ULL;
     121              :     return fmt::format("{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
     122            0 :                        static_cast<uint32_t>((a >> 32) & 0xffffffffULL),
     123            0 :                        static_cast<uint32_t>((a >> 16) & 0xffffULL),
     124            0 :                        static_cast<uint32_t>(a & 0xffffULL),
     125            0 :                        static_cast<uint32_t>((b >> 48) & 0xffffULL),
     126         2048 :                        static_cast<uint64_t>(b & 0xffffffffffffULL));
     127              : }
     128              : 
     129          729 : ServiceManager::ServiceManager(std::filesystem::path storagePath)
     130          729 :     : storagePath_(std::move(storagePath))
     131              : {
     132          729 :     std::unique_lock lk(mutex_);
     133          729 :     loadLocked();
     134          729 : }
     135              : 
     136              : std::filesystem::path
     137            0 : ServiceManager::filePath() const
     138              : {
     139            0 :     return storagePath_ / SERVICES_FILENAME;
     140              : }
     141              : 
     142              : void
     143          729 : ServiceManager::loadLocked()
     144              : {
     145          729 :     services_.clear();
     146          729 :     auto path = storagePath_ / SERVICES_FILENAME;
     147          729 :     std::error_code ec;
     148          729 :     if (!std::filesystem::exists(path, ec))
     149          727 :         return;
     150            2 :     std::ifstream in(path);
     151            2 :     if (!in)
     152            0 :         return;
     153            2 :     std::string content((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
     154            2 :     Json::Value root;
     155            2 :     if (!json::parse(content, root) || !root.isArray())
     156            0 :         return;
     157            4 :     for (const auto& v : root) {
     158            2 :         ServiceRecord r;
     159            2 :         if (fromJson(v, r))
     160            2 :             services_.emplace(r.id, std::move(r));
     161            2 :     }
     162          729 : }
     163              : 
     164              : void
     165           30 : ServiceManager::saveLocked() const
     166              : {
     167           30 :     std::error_code ec;
     168           30 :     std::filesystem::create_directories(storagePath_, ec);
     169           30 :     Json::Value root(Json::arrayValue);
     170           62 :     for (const auto& [_id, r] : services_)
     171           32 :         root.append(toJson(r));
     172           30 :     auto path = storagePath_ / SERVICES_FILENAME;
     173           30 :     std::ofstream out(path, std::ios::trunc);
     174           30 :     if (!out) {
     175            0 :         JAMI_WARNING("[ServiceManager] Unable to write {}", path.string());
     176            0 :         return;
     177              :     }
     178           30 :     out << json::toString(root);
     179           30 : }
     180              : 
     181              : std::string
     182           25 : ServiceManager::addService(ServiceRecord rec, std::mt19937_64& rng)
     183              : {
     184           25 :     if (rec.name.empty() || rec.localPort == 0)
     185            4 :         return {};
     186           21 :     if (rec.id.empty())
     187           21 :         rec.id = generateServiceUuid(rng);
     188           21 :     std::unique_lock lk(mutex_);
     189           21 :     auto id = rec.id;
     190           21 :     services_[id] = std::move(rec);
     191           21 :     saveLocked();
     192           21 :     const auto& stored = services_[id];
     193           21 :     JAMI_LOG("[ServiceManager] added service id={} name=\"{}\" target={}:{} enabled={}",
     194              :              id,
     195              :              stored.name,
     196              :              stored.localHost,
     197              :              stored.localPort,
     198              :              stored.enabled);
     199           21 :     lk.unlock();
     200           21 :     notifyChanged();
     201           21 :     return id;
     202           21 : }
     203              : 
     204              : bool
     205            5 : ServiceManager::updateService(const ServiceRecord& rec)
     206              : {
     207            5 :     if (rec.id.empty() || rec.name.empty() || rec.localPort == 0)
     208            0 :         return false;
     209            5 :     std::unique_lock lk(mutex_);
     210            5 :     auto it = services_.find(rec.id);
     211            5 :     if (it == services_.end())
     212            1 :         return false;
     213            4 :     bool wasEnabled = it->second.enabled;
     214            4 :     it->second = rec;
     215            4 :     saveLocked();
     216            4 :     if (wasEnabled != rec.enabled)
     217            1 :         JAMI_LOG("[ServiceManager] service id={} name=\"{}\" {}",
     218              :                  rec.id,
     219              :                  rec.name,
     220              :                  rec.enabled ? "enabled" : "disabled");
     221              :     else
     222            3 :         JAMI_LOG("[ServiceManager] updated service id={} name=\"{}\" target={}:{}",
     223              :                  rec.id,
     224              :                  rec.name,
     225              :                  rec.localHost,
     226              :                  rec.localPort);
     227            4 :     lk.unlock();
     228            4 :     notifyChanged();
     229            4 :     return true;
     230            5 : }
     231              : 
     232              : bool
     233            7 : ServiceManager::removeService(const std::string& id)
     234              : {
     235            7 :     std::unique_lock lk(mutex_);
     236            7 :     auto erased = services_.erase(id) > 0;
     237            7 :     if (erased) {
     238            5 :         saveLocked();
     239            5 :         JAMI_LOG("[ServiceManager] removed service id={}", id);
     240            5 :         lk.unlock();
     241            5 :         notifyChanged();
     242              :     }
     243            7 :     return erased;
     244            7 : }
     245              : 
     246              : std::vector<ServiceRecord>
     247            7 : ServiceManager::getServices() const
     248              : {
     249            7 :     std::shared_lock lk(mutex_);
     250            7 :     std::vector<ServiceRecord> out;
     251            7 :     out.reserve(services_.size());
     252            9 :     for (const auto& [_id, r] : services_)
     253            2 :         out.push_back(r);
     254           14 :     return out;
     255            7 : }
     256              : 
     257              : std::optional<ServiceRecord>
     258           10 : ServiceManager::getService(const std::string& id) const
     259              : {
     260           10 :     std::shared_lock lk(mutex_);
     261           10 :     auto it = services_.find(id);
     262           10 :     if (it == services_.end())
     263            1 :         return std::nullopt;
     264            9 :     return it->second;
     265           10 : }
     266              : 
     267              : bool
     268           27 : ServiceManager::isAuthorizedNoLock(const ServiceRecord& rec,
     269              :                                    const std::string& peerAccountUri,
     270              :                                    const ContactChecker& isContact)
     271              : {
     272           27 :     if (!rec.enabled)
     273            4 :         return false;
     274           23 :     switch (rec.policy) {
     275           11 :     case AccessPolicy::PUBLIC:
     276           11 :         return true;
     277            6 :     case AccessPolicy::CONTACTS_ONLY:
     278            6 :         return isContact && isContact(peerAccountUri);
     279            6 :     case AccessPolicy::SPECIFIC_CONTACTS:
     280            6 :         return std::find(rec.allowedContacts.begin(), rec.allowedContacts.end(), peerAccountUri)
     281           12 :                != rec.allowedContacts.end();
     282              :     }
     283            0 :     return false;
     284              : }
     285              : 
     286              : bool
     287           11 : ServiceManager::isAuthorized(const std::string& serviceId,
     288              :                              const std::string& peerAccountUri,
     289              :                              const ContactChecker& isContact) const
     290              : {
     291           11 :     std::shared_lock lk(mutex_);
     292           11 :     auto it = services_.find(serviceId);
     293           11 :     if (it == services_.end())
     294            1 :         return false;
     295           10 :     return isAuthorizedNoLock(it->second, peerAccountUri, isContact);
     296           11 : }
     297              : 
     298              : std::vector<ServiceRecord>
     299         1259 : ServiceManager::getVisibleServices(const std::string& peerAccountUri, const ContactChecker& isContact) const
     300              : {
     301         1259 :     std::shared_lock lk(mutex_);
     302         1259 :     std::vector<ServiceRecord> out;
     303         1259 :     out.reserve(services_.size());
     304         1276 :     for (const auto& [_id, r] : services_) {
     305           17 :         if (isAuthorizedNoLock(r, peerAccountUri, isContact))
     306           10 :             out.push_back(r);
     307              :     }
     308         2518 :     return out;
     309         1259 : }
     310              : 
     311              : void
     312          726 : ServiceManager::setOnChanged(OnChangeCb cb)
     313              : {
     314          726 :     std::unique_lock lk(mutex_);
     315          726 :     onChangeCb_ = std::move(cb);
     316          726 : }
     317              : 
     318              : void
     319           30 : ServiceManager::notifyChanged()
     320              : {
     321           30 :     OnChangeCb cb;
     322              :     {
     323           30 :         std::shared_lock lk(mutex_);
     324           30 :         cb = onChangeCb_;
     325           30 :     }
     326           30 :     if (cb)
     327           12 :         cb();
     328           30 : }
     329              : 
     330              : } // namespace jami
        

Generated by: LCOV version 2.0-1