Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : * Author: Adrien BĂ©raud <adrien.beraud@savoirfairelinux.com>
4 : * Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
5 : *
6 : * This program is free software; you can redistribute it and/or modify
7 : * it under the terms of the GNU General Public License as published by
8 : * the Free Software Foundation; either version 3 of the License, or
9 : * (at your option) any later version.
10 : *
11 : * This library is distributed in the hope that it will be useful,
12 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : * Lesser General Public License for more details.
15 : *
16 : * You should have received a copy of the GNU General Public License
17 : * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 : */
19 :
20 : #ifdef HAVE_CONFIG_H
21 : #include "config.h"
22 : #endif
23 : #include "namedirectory.h"
24 :
25 : #include "logger.h"
26 : #include "string_utils.h"
27 : #include "fileutils.h"
28 : #include "base64.h"
29 : #include "scheduled_executor.h"
30 :
31 : #include <asio.hpp>
32 :
33 : #include "manager.h"
34 : #include <opendht/crypto.h>
35 : #include <opendht/utils.h>
36 : #include <opendht/http.h>
37 : #include <opendht/logger.h>
38 : #include <opendht/thread_pool.h>
39 :
40 : #include <cstddef>
41 : #include <msgpack.hpp>
42 : #include <json/json.h>
43 :
44 : /* for visual studio */
45 : #include <ciso646>
46 : #include <sstream>
47 : #include <regex>
48 : #include <fstream>
49 :
50 : namespace jami {
51 :
52 : constexpr const char* const QUERY_NAME {"/name/"};
53 : constexpr const char* const QUERY_ADDR {"/addr/"};
54 : constexpr auto CACHE_DIRECTORY {"namecache"sv};
55 : constexpr const char DEFAULT_SERVER_HOST[] = "https://ns.jami.net";
56 :
57 : const std::string HEX_PREFIX = "0x";
58 : constexpr std::chrono::seconds SAVE_INTERVAL {5};
59 :
60 : /** Parser for URIs. ( protocol ) ( username ) ( hostname ) */
61 : const std::regex URI_VALIDATOR {
62 : "^([a-zA-Z]+:(?://)?)?(?:([a-z0-9-_]{1,64})@)?([a-zA-Z0-9\\-._~%!$&'()*+,;=:\\[\\]]+)"};
63 : const std::regex NAME_VALIDATOR {"^[a-zA-Z0-9-_]{3,32}$"};
64 :
65 : constexpr size_t MAX_RESPONSE_SIZE {1024ul * 1024};
66 :
67 : using Request = dht::http::Request;
68 :
69 : void
70 3 : toLower(std::string& string)
71 : {
72 3 : std::transform(string.begin(), string.end(), string.begin(), ::tolower);
73 3 : }
74 :
75 : NameDirectory&
76 0 : NameDirectory::instance()
77 : {
78 0 : return instance(DEFAULT_SERVER_HOST);
79 : }
80 :
81 : void
82 4 : NameDirectory::lookupUri(std::string_view uri, const std::string& default_server, LookupCallback cb)
83 : {
84 4 : const std::string& default_ns = default_server.empty() ? DEFAULT_SERVER_HOST : default_server;
85 4 : std::svmatch pieces_match;
86 4 : if (std::regex_match(uri, pieces_match, URI_VALIDATOR)) {
87 4 : if (pieces_match.size() == 4) {
88 4 : if (pieces_match[2].length() == 0)
89 4 : instance(default_ns).lookupName(pieces_match[3], std::move(cb));
90 : else
91 0 : instance(pieces_match[3].str()).lookupName(pieces_match[2], std::move(cb));
92 4 : return;
93 : }
94 : }
95 0 : JAMI_ERR("Can't parse URI: %.*s", (int) uri.size(), uri.data());
96 0 : cb("", Response::invalidResponse);
97 8 : }
98 :
99 25 : NameDirectory::NameDirectory(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
100 25 : : serverUrl_(serverUrl)
101 25 : , logger_(std::move(l))
102 50 : , httpContext_(Manager::instance().ioContext())
103 : {
104 25 : if (!serverUrl_.empty() && serverUrl_.back() == '/')
105 0 : serverUrl_.pop_back();
106 25 : resolver_ = std::make_shared<dht::http::Resolver>(*httpContext_, serverUrl, logger_);
107 25 : cachePath_ = fileutils::get_cache_dir() / CACHE_DIRECTORY / resolver_->get_url().host;
108 25 : }
109 :
110 25 : NameDirectory::~NameDirectory()
111 : {
112 25 : decltype(requests_) requests;
113 : {
114 25 : std::lock_guard lk(requestsMtx_);
115 25 : requests = std::move(requests_);
116 25 : }
117 25 : for (auto& req : requests)
118 0 : req->cancel();
119 25 : }
120 :
121 : void
122 25 : NameDirectory::load()
123 : {
124 25 : loadCache();
125 25 : }
126 :
127 : NameDirectory&
128 794 : NameDirectory::instance(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
129 : {
130 794 : const std::string& s = serverUrl.empty() ? DEFAULT_SERVER_HOST : serverUrl;
131 : static std::mutex instanceMtx {};
132 :
133 794 : std::lock_guard lock(instanceMtx);
134 794 : static std::map<std::string, NameDirectory> instances {};
135 794 : auto it = instances.find(s);
136 794 : if (it != instances.end())
137 769 : return it->second;
138 25 : auto r = instances.emplace(std::piecewise_construct,
139 25 : std::forward_as_tuple(s),
140 25 : std::forward_as_tuple(s, l));
141 25 : if (r.second)
142 25 : r.first->second.load();
143 25 : return r.first->second;
144 794 : }
145 :
146 : void
147 784 : NameDirectory::setHeaderFields(Request& request)
148 : {
149 784 : request.set_header_field(restinio::http_field_t::user_agent, "JamiDHT");
150 784 : request.set_header_field(restinio::http_field_t::accept, "*/*");
151 784 : request.set_header_field(restinio::http_field_t::content_type, "application/json");
152 784 : }
153 :
154 : void
155 782 : NameDirectory::lookupAddress(const std::string& addr, LookupCallback cb)
156 : {
157 782 : std::string cacheResult = nameCache(addr);
158 782 : if (not cacheResult.empty()) {
159 1 : cb(cacheResult, Response::found);
160 1 : return;
161 : }
162 781 : auto request = std::make_shared<Request>(*httpContext_,
163 781 : resolver_,
164 2343 : serverUrl_ + QUERY_ADDR + addr);
165 : try {
166 781 : request->set_method(restinio::http_method_get());
167 781 : setHeaderFields(*request);
168 1562 : request->add_on_done_callback(
169 781 : [this, cb = std::move(cb), addr](const dht::http::Response& response) {
170 781 : if (response.status_code >= 400 && response.status_code < 500) {
171 780 : cb("", Response::notFound);
172 1 : } else if (response.status_code != 200) {
173 0 : JAMI_ERR("Address lookup for %s failed with code=%i",
174 : addr.c_str(),
175 : response.status_code);
176 0 : cb("", Response::error);
177 : } else {
178 : try {
179 1 : Json::Value json;
180 1 : std::string err;
181 1 : Json::CharReaderBuilder rbuilder;
182 1 : auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
183 2 : if (!reader->parse(response.body.data(),
184 1 : response.body.data() + response.body.size(),
185 : &json,
186 : &err)) {
187 0 : JAMI_DBG("Address lookup for %s: can't parse server response: %s",
188 : addr.c_str(),
189 : response.body.c_str());
190 0 : cb("", Response::error);
191 0 : return;
192 : }
193 1 : auto name = json["name"].asString();
194 1 : if (name.empty()) {
195 0 : cb(name, Response::notFound);
196 0 : return;
197 : }
198 1 : JAMI_DBG("Found name for %s: %s", addr.c_str(), name.c_str());
199 : {
200 1 : std::lock_guard l(cacheLock_);
201 1 : addrCache_.emplace(name, addr);
202 1 : nameCache_.emplace(addr, name);
203 1 : }
204 1 : cb(name, Response::found);
205 1 : scheduleCacheSave();
206 1 : } catch (const std::exception& e) {
207 0 : JAMI_ERR("Error when performing address lookup: %s", e.what());
208 0 : cb("", Response::error);
209 0 : }
210 : }
211 781 : std::lock_guard lk(requestsMtx_);
212 781 : if (auto req = response.request.lock())
213 781 : requests_.erase(req);
214 781 : });
215 : {
216 781 : std::lock_guard lk(requestsMtx_);
217 781 : requests_.emplace(request);
218 781 : }
219 781 : request->send();
220 0 : } catch (const std::exception& e) {
221 0 : JAMI_ERR("Error when performing address lookup: %s", e.what());
222 0 : std::lock_guard lk(requestsMtx_);
223 0 : if (request)
224 0 : requests_.erase(request);
225 0 : }
226 782 : }
227 :
228 : bool
229 0 : NameDirectory::verify(const std::string& name,
230 : const dht::crypto::PublicKey& pk,
231 : const std::string& signature)
232 : {
233 0 : return pk.checkSignature(std::vector<uint8_t>(name.begin(), name.end()),
234 0 : base64::decode(signature));
235 : }
236 :
237 : void
238 4 : NameDirectory::lookupName(const std::string& n, LookupCallback cb)
239 : {
240 4 : std::string name {n};
241 4 : if (not validateName(name)) {
242 2 : cb("", Response::invalidResponse);
243 2 : return;
244 : }
245 2 : toLower(name);
246 2 : std::string cacheResult = addrCache(name);
247 2 : if (not cacheResult.empty()) {
248 0 : cb(cacheResult, Response::found);
249 0 : return;
250 : }
251 2 : auto request = std::make_shared<Request>(*httpContext_,
252 2 : resolver_,
253 6 : serverUrl_ + QUERY_NAME + name);
254 : try {
255 2 : request->set_method(restinio::http_method_get());
256 2 : setHeaderFields(*request);
257 2 : request->add_on_done_callback([this, name, cb = std::move(cb)](
258 6 : const dht::http::Response& response) {
259 2 : if (response.status_code >= 400 && response.status_code < 500)
260 1 : cb("", Response::notFound);
261 1 : else if (response.status_code < 200 || response.status_code > 299)
262 0 : cb("", Response::error);
263 : else {
264 : try {
265 1 : Json::Value json;
266 1 : std::string err;
267 1 : Json::CharReaderBuilder rbuilder;
268 1 : auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
269 2 : if (!reader->parse(response.body.data(),
270 1 : response.body.data() + response.body.size(),
271 : &json,
272 : &err)) {
273 0 : JAMI_ERR("Name lookup for %s: can't parse server response: %s",
274 : name.c_str(),
275 : response.body.c_str());
276 0 : cb("", Response::error);
277 0 : return;
278 : }
279 1 : auto addr = json["addr"].asString();
280 1 : auto publickey = json["publickey"].asString();
281 1 : auto signature = json["signature"].asString();
282 :
283 1 : if (!addr.compare(0, HEX_PREFIX.size(), HEX_PREFIX))
284 0 : addr = addr.substr(HEX_PREFIX.size());
285 1 : if (addr.empty()) {
286 0 : cb("", Response::notFound);
287 0 : return;
288 : }
289 1 : if (not publickey.empty() and not signature.empty()) {
290 : try {
291 0 : auto pk = dht::crypto::PublicKey(base64::decode(publickey));
292 0 : if (pk.getId().toString() != addr or not verify(name, pk, signature)) {
293 0 : cb("", Response::invalidResponse);
294 0 : return;
295 : }
296 0 : } catch (const std::exception& e) {
297 0 : cb("", Response::invalidResponse);
298 0 : return;
299 0 : }
300 : }
301 1 : JAMI_DBG("Found address for %s: %s", name.c_str(), addr.c_str());
302 : {
303 1 : std::lock_guard l(cacheLock_);
304 1 : addrCache_.emplace(name, addr);
305 1 : nameCache_.emplace(addr, name);
306 1 : }
307 1 : cb(addr, Response::found);
308 1 : scheduleCacheSave();
309 1 : } catch (const std::exception& e) {
310 0 : JAMI_ERR("Error when performing name lookup: %s", e.what());
311 0 : cb("", Response::error);
312 0 : }
313 : }
314 2 : if (auto req = response.request.lock())
315 2 : requests_.erase(req);
316 : });
317 : {
318 2 : std::lock_guard lk(requestsMtx_);
319 2 : requests_.emplace(request);
320 2 : }
321 2 : request->send();
322 0 : } catch (const std::exception& e) {
323 0 : JAMI_ERR("Name lookup for %s failed: %s", name.c_str(), e.what());
324 0 : std::lock_guard lk(requestsMtx_);
325 0 : if (request)
326 0 : requests_.erase(request);
327 0 : }
328 4 : }
329 :
330 : bool
331 5 : NameDirectory::validateName(const std::string& name) const
332 : {
333 5 : return std::regex_match(name, NAME_VALIDATOR);
334 : }
335 :
336 : using Blob = std::vector<uint8_t>;
337 : void
338 1 : NameDirectory::registerName(const std::string& addr,
339 : const std::string& n,
340 : const std::string& owner,
341 : RegistrationCallback cb,
342 : const std::string& signedname,
343 : const std::string& publickey)
344 : {
345 1 : std::string name {n};
346 1 : if (not validateName(name)) {
347 0 : cb(RegistrationResponse::invalidName);
348 0 : return;
349 : }
350 1 : toLower(name);
351 1 : auto cacheResult = addrCache(name);
352 1 : if (not cacheResult.empty()) {
353 0 : if (cacheResult == addr)
354 0 : cb(RegistrationResponse::success);
355 : else
356 0 : cb(RegistrationResponse::alreadyTaken);
357 0 : return;
358 : }
359 : std::string body = fmt::format("{{\"addr\":\"{}\",\"owner\":\"{}\",\"signature\":\"{}\",\"publickey\":\"{}\"}}",
360 : addr,
361 : owner,
362 : signedname,
363 2 : base64::encode(publickey));
364 1 : auto request = std::make_shared<Request>(*httpContext_,
365 1 : resolver_,
366 3 : serverUrl_ + QUERY_NAME + name);
367 : try {
368 1 : request->set_method(restinio::http_method_post());
369 1 : setHeaderFields(*request);
370 1 : request->set_body(body);
371 :
372 1 : JAMI_WARN("RegisterName: sending request %s %s", addr.c_str(), name.c_str());
373 :
374 2 : request->add_on_done_callback(
375 1 : [this, name, addr, cb = std::move(cb)](const dht::http::Response& response) {
376 1 : if (response.status_code == 400) {
377 0 : cb(RegistrationResponse::incompleteRequest);
378 0 : JAMI_ERR("RegistrationResponse::incompleteRequest");
379 1 : } else if (response.status_code == 401) {
380 0 : cb(RegistrationResponse::signatureVerificationFailed);
381 0 : JAMI_ERR("RegistrationResponse::signatureVerificationFailed");
382 1 : } else if (response.status_code == 403) {
383 0 : cb(RegistrationResponse::alreadyTaken);
384 0 : JAMI_ERR("RegistrationResponse::alreadyTaken");
385 1 : } else if (response.status_code == 409) {
386 0 : cb(RegistrationResponse::alreadyTaken);
387 0 : JAMI_ERR("RegistrationResponse::alreadyTaken");
388 1 : } else if (response.status_code > 400 && response.status_code < 500) {
389 0 : cb(RegistrationResponse::alreadyTaken);
390 0 : JAMI_ERR("RegistrationResponse::alreadyTaken");
391 1 : } else if (response.status_code < 200 || response.status_code > 299) {
392 0 : cb(RegistrationResponse::error);
393 0 : JAMI_ERR("RegistrationResponse::error");
394 : } else {
395 1 : Json::Value json;
396 1 : std::string err;
397 1 : Json::CharReaderBuilder rbuilder;
398 :
399 1 : auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
400 2 : if (!reader->parse(response.body.data(),
401 1 : response.body.data() + response.body.size(),
402 : &json,
403 : &err)) {
404 0 : cb(RegistrationResponse::error);
405 0 : return;
406 : }
407 1 : auto success = json["success"].asBool();
408 1 : JAMI_DBG("Got reply for registration of %s %s: %s",
409 : name.c_str(),
410 : addr.c_str(),
411 : success ? "success" : "failure");
412 1 : if (success) {
413 1 : std::lock_guard l(cacheLock_);
414 1 : addrCache_.emplace(name, addr);
415 1 : nameCache_.emplace(addr, name);
416 1 : }
417 1 : cb(success ? RegistrationResponse::success : RegistrationResponse::error);
418 1 : }
419 1 : std::lock_guard lk(requestsMtx_);
420 1 : if (auto req = response.request.lock())
421 1 : requests_.erase(req);
422 1 : });
423 : {
424 1 : std::lock_guard lk(requestsMtx_);
425 1 : requests_.emplace(request);
426 1 : }
427 1 : request->send();
428 0 : } catch (const std::exception& e) {
429 0 : JAMI_ERR("Error when performing name registration: %s", e.what());
430 0 : cb(RegistrationResponse::error);
431 0 : std::lock_guard lk(requestsMtx_);
432 0 : if (request)
433 0 : requests_.erase(request);
434 0 : }
435 1 : }
436 :
437 : void
438 2 : NameDirectory::scheduleCacheSave()
439 : {
440 : // JAMI_DBG("Scheduling cache save to %s", cachePath_.c_str());
441 4 : std::weak_ptr<Task> task = Manager::instance().scheduler().scheduleIn(
442 4 : [this] { dht::ThreadPool::io().run([this] { saveCache(); }); }, SAVE_INTERVAL);
443 2 : std::swap(saveTask_, task);
444 2 : if (auto old = task.lock())
445 2 : old->cancel();
446 2 : }
447 :
448 : void
449 1 : NameDirectory::saveCache()
450 : {
451 1 : dhtnet::fileutils::recursive_mkdir(fileutils::get_cache_dir() / CACHE_DIRECTORY);
452 1 : std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
453 1 : std::ofstream file(cachePath_, std::ios::trunc | std::ios::binary);
454 : {
455 1 : std::lock_guard l(cacheLock_);
456 1 : msgpack::pack(file, nameCache_);
457 1 : }
458 1 : JAMI_DBG("Saved %lu name-address mappings to %s",
459 : (long unsigned) nameCache_.size(),
460 : cachePath_.c_str());
461 1 : }
462 :
463 : void
464 25 : NameDirectory::loadCache()
465 : {
466 25 : msgpack::unpacker pac;
467 :
468 : // read file
469 : {
470 25 : std::lock_guard lock(dhtnet::fileutils::getFileLock(cachePath_));
471 25 : std::ifstream file(cachePath_);
472 25 : if (!file.is_open()) {
473 25 : JAMI_DBG("Could not load %s", cachePath_.c_str());
474 25 : return;
475 : }
476 0 : std::string line;
477 0 : while (std::getline(file, line)) {
478 0 : pac.reserve_buffer(line.size());
479 0 : memcpy(pac.buffer(), line.data(), line.size());
480 0 : pac.buffer_consumed(line.size());
481 : }
482 50 : }
483 :
484 : // load values
485 0 : std::lock_guard l(cacheLock_);
486 0 : msgpack::object_handle oh;
487 0 : if (pac.next(oh))
488 0 : oh.get().convert(nameCache_);
489 0 : for (const auto& m : nameCache_)
490 0 : addrCache_.emplace(m.second, m.first);
491 0 : JAMI_DBG("Loaded %lu name-address mappings", (long unsigned) nameCache_.size());
492 25 : }
493 :
494 : } // namespace jami
|