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