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