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