Ring Daemon 16.0.0
Loading...
Searching...
No Matches
namedirectory.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2025 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
18#ifdef HAVE_CONFIG_H
19#include "config.h"
20#endif
21#include "namedirectory.h"
22
23#include "logger.h"
24#include "string_utils.h"
25#include "fileutils.h"
26#include "base64.h"
27#include "scheduled_executor.h"
28
29#include <asio.hpp>
30
31#include "manager.h"
32#include <opendht/crypto.h>
33#include <opendht/utils.h>
34#include <opendht/http.h>
35#include <opendht/logger.h>
36#include <opendht/thread_pool.h>
37
38#include <cstddef>
39#include <msgpack.hpp>
40#include "json_utils.h"
41
42/* for Visual Studio */
43#include <ciso646>
44#include <sstream>
45#include <regex>
46#include <fstream>
47
48namespace jami {
49
50constexpr const char* const QUERY_NAME {"/name/"};
51constexpr const char* const QUERY_ADDR {"/addr/"};
52constexpr auto CACHE_DIRECTORY {"namecache"sv};
53constexpr const char DEFAULT_SERVER_HOST[] = "https://ns.jami.net";
54
55constexpr std::string_view HEX_PREFIX = "0x"sv;
56constexpr std::chrono::seconds SAVE_INTERVAL {5};
57
58/*
59 * Parser for URIs. ( protocol ) ( username ) ( hostname )
60 * - Requires "@" if a username is present (e.g., "user@domain.com").
61 * - Allows common URL-safe special characters in usernames and domains.
62 *
63 * Regex breakdown:
64 * 1. `([a-zA-Z]+:(?://)?)?` → Optional scheme ("http://", "ftp://").
65 * 2. `(?:([^\s@]{1,64})@)?` → Optional username (max 64 chars, Unicode allowed).
66 * 3. `([^\s@]+)` → Domain or standalone name (Unicode allowed, no spaces or "@").
67 */
68const std::regex URI_VALIDATOR {
69 R"(^([a-zA-Z]+:(?://)?)?(?:([\w\-.~%!$&'()*+,;=]{1,64}|[^\s@]{1,64})@)?([^\s@]+)$)"
70};
71
72constexpr size_t MAX_RESPONSE_SIZE {1024ul * 1024};
73
74using Request = dht::http::Request;
75
76void
77toLower(std::string& string)
78{
79 std::transform(string.begin(), string.end(), string.begin(), ::tolower);
80}
81
82NameDirectory&
87
88void
89NameDirectory::lookupUri(std::string_view uri, const std::string& default_server, LookupCallback cb)
90{
91 const std::string& default_ns = default_server.empty() ? DEFAULT_SERVER_HOST : default_server;
94 if (pieces_match.size() == 4) {
95 if (pieces_match[2].length() == 0)
97 else
98 instance(pieces_match[3].str()).lookupName(pieces_match[2], std::move(cb));
99 return;
100 }
101 }
102 JAMI_ERROR("Unable to parse URI: {}", uri);
104}
105
106NameDirectory::NameDirectory(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
107 : serverUrl_(serverUrl)
108 , logger_(std::move(l))
109 , httpContext_(Manager::instance().ioContext())
110{
111 if (!serverUrl_.empty() && serverUrl_.back() == '/')
112 serverUrl_.pop_back();
113 resolver_ = std::make_shared<dht::http::Resolver>(*httpContext_, serverUrl, logger_);
114 cachePath_ = fileutils::get_cache_dir() / CACHE_DIRECTORY / resolver_->get_url().host;
115}
116
118{
119 decltype(requests_) requests;
120 {
121 std::lock_guard lk(requestsMtx_);
122 requests = std::move(requests_);
123 }
124 for (auto& req : requests)
125 req->cancel();
126}
127
128void
130{
131 loadCache();
132}
133
134std::string
135canonicalName(const std::string& url) {
136 std::string name = url;
137 std::transform(name.begin(), name.end(), name.begin(), ::tolower);
138 if (name.find("://") == std::string::npos)
139 name = "https://" + name;
140 return name;
141}
142
143NameDirectory&
144NameDirectory::instance(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
145{
146 const std::string& s = serverUrl.empty() ? DEFAULT_SERVER_HOST : canonicalName(serverUrl);
147 static std::mutex instanceMtx {};
148
149 std::lock_guard lock(instanceMtx);
150 static std::map<std::string, NameDirectory> instances {};
151 auto it = instances.find(s);
152 if (it != instances.end())
153 return it->second;
154 auto r = instances.emplace(std::piecewise_construct,
155 std::forward_as_tuple(s),
156 std::forward_as_tuple(s, l));
157 if (r.second)
158 r.first->second.load();
159 return r.first->second;
160}
161
162void
163NameDirectory::setHeaderFields(Request& request)
164{
165 request.set_header_field(restinio::http_field_t::user_agent, fmt::format("Jami ({}/{})",
167 request.set_header_field(restinio::http_field_t::accept, "*/*");
168 request.set_header_field(restinio::http_field_t::content_type, "application/json");
169}
170
171void
173{
174 auto cacheResult = nameCache(addr);
175 if (not cacheResult.first.empty()) {
177 return;
178 }
179 auto request = std::make_shared<Request>(*httpContext_,
180 resolver_,
181 serverUrl_ + QUERY_ADDR + addr);
182 try {
183 request->set_method(restinio::http_method_get());
184 setHeaderFields(*request);
185 request->add_on_done_callback(
186 [this, cb = std::move(cb), addr](const dht::http::Response& response) {
187 if (response.status_code > 400 && response.status_code < 500) {
188 auto cacheResult = nameCache(addr);
189 if (not cacheResult.first.empty())
190 cb(cacheResult.first, cacheResult.second, Response::found);
191 else
192 cb("", "", Response::notFound);
193 } else if (response.status_code == 400)
195 else if (response.status_code != 200) {
196 JAMI_ERROR("Address lookup for {} on {} failed with code={}",
197 addr, serverUrl_, response.status_code);
198 cb("", "", Response::error);
199 } else {
200 try {
201 Json::Value json;
202 if (!json::parse(response.body, json)) {
203 cb("", "", Response::error);
204 return;
205 }
206 auto name = json["name"].asString();
207 if (name.empty()) {
208 cb(name, addr, Response::notFound);
209 return;
210 }
211 JAMI_DEBUG("Found name for {}: {}", addr, name);
212 {
213 std::lock_guard l(cacheLock_);
214 addrCache_.emplace(name, std::pair(name, addr));
215 nameCache_.emplace(addr, std::pair(name, addr));
216 }
217 cb(name, addr, Response::found);
218 scheduleCacheSave();
219 } catch (const std::exception& e) {
220 JAMI_ERROR("Error when performing address lookup: {}", e.what());
221 cb("", "", Response::error);
222 }
223 }
224 std::lock_guard lk(requestsMtx_);
225 if (auto req = response.request.lock())
226 requests_.erase(req);
227 });
228 {
229 std::lock_guard lk(requestsMtx_);
230 requests_.emplace(request);
231 }
232 request->send();
233 } catch (const std::exception& e) {
234 JAMI_ERROR("Error when performing address lookup: {}", e.what());
235 std::lock_guard lk(requestsMtx_);
236 if (request)
237 requests_.erase(request);
238 }
239}
240
241bool
242NameDirectory::verify(const std::string& name,
243 const dht::crypto::PublicKey& pk,
244 const std::string& signature)
245{
246 return pk.checkSignature(std::vector<uint8_t>(name.begin(), name.end()),
247 base64::decode(signature));
248}
249
250void
251NameDirectory::lookupName(const std::string& name, LookupCallback cb)
252{
253 auto cacheResult = addrCache(name);
254 if (not cacheResult.first.empty()) {
255 cb(cacheResult.first, cacheResult.second, Response::found);
256 return;
257 }
258 auto encodedName = urlEncode(name);
259 auto request = std::make_shared<Request>(*httpContext_,
260 resolver_,
261 serverUrl_ + QUERY_NAME + encodedName);
262 try {
263 request->set_method(restinio::http_method_get());
264 setHeaderFields(*request);
265 request->add_on_done_callback([this, name, cb = std::move(cb)](
266 const dht::http::Response& response) {
267 if (response.status_code > 400 && response.status_code < 500)
268 cb("", "", Response::notFound);
269 else if (response.status_code == 400)
270 cb("", "", Response::invalidResponse);
271 else if (response.status_code < 200 || response.status_code > 299) {
272 JAMI_ERROR("Name lookup for {} on {} failed with code={}",
273 name, serverUrl_, response.status_code);
274 cb("", "", Response::error);
275 } else {
276 try {
277 Json::Value json;
278 if (!json::parse(response.body, json)) {
279 cb("", "", Response::error);
280 return;
281 }
282 auto nameResult = json["name"].asString();
283 auto addr = json["addr"].asString();
284 auto publickey = json["publickey"].asString();
285 auto signature = json["signature"].asString();
286
287 if (starts_with(addr, HEX_PREFIX))
288 addr = addr.substr(HEX_PREFIX.size());
289 if (addr.empty()) {
290 cb("", "", Response::notFound);
291 return;
292 }
293 if (not publickey.empty() and not signature.empty()) {
294 try {
295 auto pk = dht::crypto::PublicKey(base64::decode(publickey));
296 if (pk.getId().toString() != addr or not verify(nameResult, pk, signature)) {
297 cb("", "", Response::invalidResponse);
298 return;
299 }
300 } catch (const std::exception& e) {
301 cb("", "", Response::invalidResponse);
302 return;
303 }
304 }
305 JAMI_DEBUG("Found address for {}: {}", name, addr);
306 {
307 std::lock_guard l(cacheLock_);
308 addrCache_.emplace(name, std::pair(nameResult, addr));
309 addrCache_.emplace(nameResult, std::pair(nameResult, addr));
310 nameCache_.emplace(addr, std::pair(nameResult, addr));
311 }
312 cb(nameResult, addr, Response::found);
313 scheduleCacheSave();
314 } catch (const std::exception& e) {
315 JAMI_ERROR("Error when performing name lookup: {}", e.what());
316 cb("", "", Response::error);
317 }
318 }
319 if (auto req = response.request.lock())
320 requests_.erase(req);
321 });
322 {
323 std::lock_guard lk(requestsMtx_);
324 requests_.emplace(request);
325 }
326 request->send();
327 } catch (const std::exception& e) {
328 JAMI_ERROR("Name lookup for {} failed: {}", name, e.what());
329 std::lock_guard lk(requestsMtx_);
330 if (request)
331 requests_.erase(request);
332 }
333}
334
335using Blob = std::vector<uint8_t>;
336void
337NameDirectory::registerName(const std::string& addr,
338 const std::string& n,
339 const std::string& owner,
341 const std::string& signedname,
342 const std::string& publickey)
343{
344 std::string name {n};
345 toLower(name);
346 auto cacheResult = addrCache(name);
347 if (not cacheResult.first.empty()) {
348 if (cacheResult.second == addr)
349 cb(RegistrationResponse::success, name);
350 else
351 cb(RegistrationResponse::alreadyTaken, name);
352 return;
353 }
354 {
355 std::lock_guard l(cacheLock_);
356 if (not pendingRegistrations_.emplace(addr, name).second) {
357 JAMI_WARNING("RegisterName: already registering name {} {}", addr, name);
358 cb(RegistrationResponse::error, name);
359 return;
360 }
361 }
362 std::string body = fmt::format("{{\"addr\":\"{}\",\"owner\":\"{}\",\"signature\":\"{}\",\"publickey\":\"{}\"}}",
363 addr,
364 owner,
365 signedname,
366 base64::encode(publickey));
367
368 auto encodedName = urlEncode(name);
369 auto request = std::make_shared<Request>(*httpContext_,
370 resolver_,
371 serverUrl_ + QUERY_NAME + encodedName);
372 try {
373 request->set_method(restinio::http_method_post());
374 setHeaderFields(*request);
375 request->set_body(body);
376
377 JAMI_WARNING("RegisterName: sending request {} {}", addr, name);
378
379 request->add_on_done_callback(
380 [this, name, addr, cb = std::move(cb)](const dht::http::Response& response) {
381 {
382 std::lock_guard l(cacheLock_);
383 pendingRegistrations_.erase(name);
384 }
385 if (response.status_code == 400) {
386 cb(RegistrationResponse::incompleteRequest, name);
387 JAMI_ERROR("RegistrationResponse::incompleteRequest");
388 } else if (response.status_code == 401) {
389 cb(RegistrationResponse::signatureVerificationFailed, name);
390 JAMI_ERROR("RegistrationResponse::signatureVerificationFailed");
391 } else if (response.status_code == 403) {
392 cb(RegistrationResponse::alreadyTaken, name);
393 JAMI_ERROR("RegistrationResponse::alreadyTaken");
394 } else if (response.status_code == 409) {
395 cb(RegistrationResponse::alreadyTaken, name);
396 JAMI_ERROR("RegistrationResponse::alreadyTaken");
397 } else if (response.status_code > 400 && response.status_code < 500) {
398 cb(RegistrationResponse::alreadyTaken, name);
399 JAMI_ERROR("RegistrationResponse::alreadyTaken");
400 } else if (response.status_code < 200 || response.status_code > 299) {
401 cb(RegistrationResponse::error, name);
402 JAMI_ERROR("RegistrationResponse::error");
403 } else {
404 Json::Value json;
405 std::string err;
406 Json::CharReaderBuilder rbuilder;
407
408 auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
409 if (!reader->parse(response.body.data(),
410 response.body.data() + response.body.size(),
411 &json,
412 &err)) {
413 cb(RegistrationResponse::error, name);
414 return;
415 }
416 auto success = json["success"].asBool();
417 JAMI_DEBUG("Got reply for registration of {} {}: {}",
418 name, addr, success ? "success" : "failure");
419 if (success) {
420 std::lock_guard l(cacheLock_);
421 addrCache_.emplace(name, std::pair(name, addr));
422 nameCache_.emplace(addr, std::pair(name, addr));
423 }
424 cb(success ? RegistrationResponse::success : RegistrationResponse::error, name);
425 }
426 std::lock_guard lk(requestsMtx_);
427 if (auto req = response.request.lock())
428 requests_.erase(req);
429 });
430 {
431 std::lock_guard lk(requestsMtx_);
432 requests_.emplace(request);
433 }
434 request->send();
435 } catch (const std::exception& e) {
436 JAMI_ERROR("Error when performing name registration: {}", e.what());
437 cb(RegistrationResponse::error, name);
438 {
439 std::lock_guard l(cacheLock_);
440 pendingRegistrations_.erase(name);
441 }
442 std::lock_guard lk(requestsMtx_);
443 if (request)
444 requests_.erase(request);
445 }
446}
447
448void
449NameDirectory::scheduleCacheSave()
450{
451 // JAMI_DBG("Scheduling cache save to %s", cachePath_.c_str());
452 std::weak_ptr<Task> task = Manager::instance().scheduler().scheduleIn(
453 [this] { dht::ThreadPool::io().run([this] { saveCache(); }); }, SAVE_INTERVAL);
454 std::swap(saveTask_, task);
455 if (auto old = task.lock())
456 old->cancel();
457}
458
459void
460NameDirectory::saveCache()
461{
462 dhtnet::fileutils::recursive_mkdir(fileutils::get_cache_dir() / CACHE_DIRECTORY);
463 std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
464 std::ofstream file(cachePath_, std::ios::trunc | std::ios::binary);
465 if (!file.is_open()) {
466 JAMI_ERROR("Unable to save cache to {}", cachePath_);
467 return;
468 }
469 {
470 std::lock_guard l(cacheLock_);
471 msgpack::pack(file, nameCache_);
472 }
473 JAMI_DEBUG("Saved {:d} name-address mapping(s) to {}",
474 nameCache_.size(), cachePath_);
475}
476
477void
478NameDirectory::loadCache()
479{
480 msgpack::unpacker pac;
481
482 // read file
483 {
484 std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
485 std::ifstream file(cachePath_);
486 if (!file.is_open()) {
487 JAMI_DEBUG("Unable to load {}", cachePath_);
488 return;
489 }
490 std::string line;
491 while (std::getline(file, line)) {
492 pac.reserve_buffer(line.size());
493 memcpy(pac.buffer(), line.data(), line.size());
494 pac.buffer_consumed(line.size());
495 }
496 }
497
498 try {
499 // load values
500 std::lock_guard l(cacheLock_);
501 msgpack::object_handle oh;
502 if (pac.next(oh))
503 oh.get().convert(nameCache_);
504 for (const auto& m : nameCache_)
505 addrCache_.emplace(m.second.second, m.second);
506 } catch (const msgpack::parse_error& e) {
507 JAMI_ERROR("Error when parsing msgpack object: {}", e.what());
508 } catch (const std::bad_cast& e) {
509 JAMI_ERROR("Error when loading cache: {}", e.what());
510 }
511
512 JAMI_DEBUG("Loaded {:d} name-address mapping(s) from cache", nameCache_.size());
513}
514
515} // namespace jami
Manager (controller) of daemon.
Definition manager.h:67
std::function< void(RegistrationResponse response, const std::string &name)> RegistrationCallback
void lookupName(const std::string &name, LookupCallback cb)
void lookupAddress(const std::string &addr, LookupCallback cb)
static void lookupUri(std::string_view uri, const std::string &default_server, LookupCallback cb)
std::function< void(const std::string &name, const std::string &address, Response response)> LookupCallback
static NameDirectory & instance()
NameDirectory(const std::string &serverUrl, std::shared_ptr< dht::Logger > l={})
#define JAMI_ERROR(formatstr,...)
Definition logger.h:228
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:226
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
const std::filesystem::path & get_cache_dir()
constexpr const char *const QUERY_ADDR
std::string canonicalName(const std::string &url)
constexpr std::string_view platform()
void emitSignal(Args... args)
Definition ring_signal.h:64
const std::regex URI_VALIDATOR
constexpr size_t MAX_RESPONSE_SIZE
constexpr auto CACHE_DIRECTORY
constexpr std::string_view HEX_PREFIX
constexpr const char DEFAULT_SERVER_HOST[]
void toLower(std::string &string)
std::string urlEncode(std::string_view input)
Percent-encode a string according to RFC 3986 unreserved characters.
std::vector< uint8_t > Blob
constexpr const char *const QUERY_NAME
constexpr std::chrono::seconds SAVE_INTERVAL
constexpr std::string_view arch()
bool regex_match(string_view sv, svmatch &m, const regex &e, regex_constants::match_flag_type flags=regex_constants::match_default)
match_results< string_view::const_iterator > svmatch