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