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