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
|