Ring Daemon 16.0.0
Loading...
Searching...
No Matches
contact_list.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#include "contact_list.h"
18#include "logger.h"
19#include "jamiaccount.h"
20#include "fileutils.h"
21
22#include "manager.h"
23#ifdef ENABLE_PLUGIN
25#endif
26
27#include "account_const.h"
28
29#include <fstream>
30#include <gnutls/ocsp.h>
31
32namespace jami {
33
34ContactList::ContactList(const std::string& accountId,
35 const std::shared_ptr<crypto::Certificate>& cert,
36 const std::filesystem::path& path,
38 : accountId_(accountId)
39 , path_(path)
40 , callbacks_(std::move(cb))
41{
42 if (cert) {
43 trust_ = std::make_unique<dhtnet::tls::TrustStore>(jami::Manager::instance().certStore(accountId_));
44 accountTrust_.add(*cert);
45 }
46}
47
49
50void
52{
53 loadContacts();
54 loadTrustRequests();
55 loadKnownDevices();
56}
57
58void
60{
62 saveTrustRequests();
63 saveKnownDevices();
64}
65
66bool
68 const dhtnet::tls::TrustStore::PermissionStatus status)
69{
70 if (contacts_.find(dht::InfoHash(cert_id)) != contacts_.end()) {
71 JAMI_LOG("[Account {}] [Contacts] Unable to set certificate status for existing contacts {}", accountId_, cert_id);
72 return false;
73 }
74 return trust_->setCertificateStatus(cert_id, status);
75}
76
77bool
78ContactList::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
79 dhtnet::tls::TrustStore::PermissionStatus status,
80 bool local)
81{
82 return trust_->setCertificateStatus(cert, status, local);
83}
84
85bool
86ContactList::addContact(const dht::InfoHash& h, bool confirmed, const std::string& conversationId)
87{
88 JAMI_WARNING("[Account {}] [Contacts] addContact: {}, conversation: {}", accountId_, h, conversationId);
89 auto c = contacts_.find(h);
90 if (c == contacts_.end())
91 c = contacts_.emplace(h, Contact {}).first;
92 else if (c->second.isActive() and c->second.confirmed == confirmed && c->second.conversationId == conversationId)
93 return false;
94 c->second.added = std::time(nullptr);
95 // NOTE: because we can re-add a contact after removing it
96 // we should reset removed (as not removed anymore). This fix isActive()
97 // if addContact is called just after removeContact during the same second
98 c->second.removed = 0;
99 c->second.conversationId = conversationId;
100 c->second.confirmed |= confirmed;
101 auto hStr = h.toString();
102 trust_->setCertificateStatus(hStr, dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
103 saveContacts();
104 callbacks_.contactAdded(hStr, c->second.confirmed);
105 return true;
106}
107
108void
109ContactList::updateConversation(const dht::InfoHash& h, const std::string& conversationId)
110{
111 auto c = contacts_.find(h);
112 if (c != contacts_.end() && c->second.conversationId != conversationId) {
113 c->second.conversationId = conversationId;
114 saveContacts();
115 }
116}
117
118bool
119ContactList::removeContact(const dht::InfoHash& h, bool ban)
120{
121 std::unique_lock lk(mutex_);
122 JAMI_WARNING("[Account {}] [Contacts] removeContact: {} (banned: {})", accountId_, h, ban);
123 auto c = contacts_.find(h);
124 if (c == contacts_.end())
125 c = contacts_.emplace(h, Contact {}).first;
126 c->second.removed = std::time(nullptr);
127 c->second.confirmed = false;
128 c->second.banned = ban;
129 auto uri = h.toString();
130 trust_->setCertificateStatus(uri,
131 ban ? dhtnet::tls::TrustStore::PermissionStatus::BANNED
132 : dhtnet::tls::TrustStore::PermissionStatus::UNDEFINED);
133 if (trustRequests_.erase(h) > 0)
134 saveTrustRequests();
135 saveContacts();
136 lk.unlock();
137#ifdef ENABLE_PLUGIN
138 auto filename = path_.filename().string();
140 .getJamiPluginManager()
141 .getChatServicesManager()
142 .cleanChatSubjects(filename, uri);
143#endif
144 callbacks_.contactRemoved(uri, ban);
145 return true;
146}
147
148bool
150{
151 auto c = contacts_.find(h);
152 if (c == contacts_.end())
153 return false;
154 c->second.conversationId = "";
155 saveContacts();
156 return true;
157}
158
159std::map<std::string, std::string>
160ContactList::getContactDetails(const dht::InfoHash& h) const
161{
162 const auto c = contacts_.find(h);
163 if (c == std::end(contacts_)) {
164 JAMI_WARNING("[Account {}] [Contacts] Contact '{}' not found", accountId_, h.to_view());
165 return {};
166 }
167
168 auto details = c->second.toMap();
169 if (not details.empty())
170 details["id"] = c->first.toString();
171
172 return details;
173}
174
175std::optional<Contact>
176ContactList::getContactInfo(const dht::InfoHash& h) const
177{
178 const auto c = contacts_.find(h);
179 if (c == std::end(contacts_)) {
180 JAMI_WARNING("[Account {}] [Contacts] Contact '{}' not found", accountId_, h.to_view());
181 return {};
182 }
183 return c->second;
184}
185
186const std::map<dht::InfoHash, Contact>&
188{
189 return contacts_;
190}
191
192void
193ContactList::setContacts(const std::map<dht::InfoHash, Contact>& contacts)
194{
195 JAMI_LOG("[Account {}] [Contacts] replacing contact list (old: {} new: {})", accountId_, contacts_.size(), contacts.size());
196 contacts_ = contacts;
197 saveContacts();
198 // Set contacts is used when creating a new device, so just announce new contacts
199 for (auto& peer : contacts)
200 if (peer.second.isActive())
201 callbacks_.contactAdded(peer.first.toString(), peer.second.confirmed);
202}
203
204void
205ContactList::updateContact(const dht::InfoHash& id, const Contact& contact, bool emit)
206{
207 if (not id) {
208 JAMI_ERROR("[Account {}] [Contacts] updateContact: invalid contact ID", accountId_);
209 return;
210 }
211 bool stateChanged {false};
212 auto c = contacts_.find(id);
213 if (c == contacts_.end()) {
214 // JAMI_DBG("[Contacts] New contact: %s", id.toString().c_str());
215 c = contacts_.emplace(id, contact).first;
216 stateChanged = c->second.isActive() or c->second.isBanned();
217 } else {
218 // JAMI_DBG("[Contacts] Updated contact: %s", id.toString().c_str());
219 stateChanged = c->second.update(contact);
220 }
221 if (stateChanged) {
222 {
223 std::lock_guard lk(mutex_);
224 if (trustRequests_.erase(id) > 0)
225 saveTrustRequests();
226 }
227 if (c->second.isActive()) {
228 trust_->setCertificateStatus(id.toString(), dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
229 if (emit)
230 callbacks_.contactAdded(id.toString(), c->second.confirmed);
231 } else {
232 if (c->second.banned)
233 trust_->setCertificateStatus(id.toString(),
234 dhtnet::tls::TrustStore::PermissionStatus::BANNED);
235 if (emit)
236 callbacks_.contactRemoved(id.toString(), c->second.banned);
237 }
238 }
239}
240
241void
242ContactList::loadContacts()
243{
244 decltype(contacts_) contacts;
245 try {
246 // read file
247 auto file = fileutils::loadFile("contacts", path_);
248 // load values
249 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
250 oh.get().convert(contacts);
251 } catch (const std::exception& e) {
252 JAMI_WARNING("[Account {}] [Contacts] Error loading contacts: {}", accountId_, e.what());
253 return;
254 }
255
256 JAMI_WARNING("[Account {}] [Contacts] Loaded {} contacts", accountId_, contacts.size());
257 for (auto& peer : contacts)
258 updateContact(peer.first, peer.second, false);
259}
260
261void
263{
264 JAMI_LOG("[Account {}] [Contacts] saving {} contacts", accountId_, contacts_.size());
265 std::ofstream file(path_ / "contacts", std::ios::trunc | std::ios::binary);
266 msgpack::pack(file, contacts_);
267}
268
269void
270ContactList::saveTrustRequests() const
271{
272 // mutex_ MUST BE locked
273 std::ofstream file(path_ / "incomingTrustRequests",
274 std::ios::trunc | std::ios::binary);
275 msgpack::pack(file, trustRequests_);
276}
277
278void
279ContactList::loadTrustRequests()
280{
281 if (!std::filesystem::is_regular_file(fileutils::getFullPath(path_, "incomingTrustRequests")))
282 return;
283 std::map<dht::InfoHash, TrustRequest> requests;
284 try {
285 // read file
286 auto file = fileutils::loadFile("incomingTrustRequests", path_);
287 // load values
288 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
289 oh.get().convert(requests);
290 } catch (const std::exception& e) {
291 JAMI_WARNING("[Account {}] [Contacts] Error loading trust requests: {}", accountId_, e.what());
292 return;
293 }
294
295 JAMI_WARNING("[Account {}] [Contacts] Loaded {} contact requests", accountId_, requests.size());
296 for (auto& tr : requests)
298 tr.second.device,
299 tr.second.received,
300 false,
301 tr.second.conversationId,
302 std::move(tr.second.payload));
303}
304
305bool
307 const std::shared_ptr<dht::crypto::PublicKey>& peer_device,
308 time_t received,
309 bool confirm,
310 const std::string& conversationId,
311 std::vector<uint8_t>&& payload)
312{
313 bool accept = false;
314 // Check existing contact
315 std::unique_lock lk(mutex_);
316 auto contact = contacts_.find(peer_account);
317 bool active = false;
318 if (contact != contacts_.end()) {
319 // Banned contact: discard request
320 if (contact->second.isBanned())
321 return false;
322
323 if (contact->second.isActive()) {
324 active = true;
325 // Send confirmation
326 if (not confirm)
327 accept = true;
328 if (not contact->second.confirmed) {
329 contact->second.confirmed = true;
330 callbacks_.contactAdded(peer_account.toString(), true);
331 }
332 }
333 }
334 if (not active) {
335 auto req = trustRequests_.find(peer_account);
336 if (req == trustRequests_.end()) {
337 // Add trust request
338 req = trustRequests_
339 .emplace(peer_account,
340 TrustRequest {peer_device, conversationId, received, payload})
341 .first;
342 } else {
343 // Update trust request
344 if (received > req->second.received) {
345 req->second.device = peer_device;
346 req->second.conversationId = conversationId;
347 req->second.received = received;
348 req->second.payload = payload;
349 } else {
350 JAMI_LOG("[Account {}] [Contacts] Ignoring outdated trust request from {}",
351 accountId_,
353 }
354 }
355 saveTrustRequests();
356 }
357 lk.unlock();
358 // Note: call JamiAccount's callback to build ConversationRequest anyway
359 if (!confirm)
360 callbacks_.trustRequest(peer_account.toString(),
361 conversationId,
362 std::move(payload),
363 received);
364 else if (active) {
365 // Only notify if confirmed + not removed
366 callbacks_.onConfirmation(peer_account.toString(), conversationId);
367 }
368 return accept;
369}
370
371/* trust requests */
372
373std::vector<std::map<std::string, std::string>>
375{
376 using Map = std::map<std::string, std::string>;
377 std::vector<Map> ret;
378 std::lock_guard lk(mutex_);
379 ret.reserve(trustRequests_.size());
380 for (const auto& r : trustRequests_) {
381 ret.emplace_back(
382 Map {{libjami::Account::TrustRequest::FROM, r.first.toString()},
383 {libjami::Account::TrustRequest::RECEIVED, std::to_string(r.second.received)},
384 {libjami::Account::TrustRequest::CONVERSATIONID, r.second.conversationId},
386 std::string(r.second.payload.begin(), r.second.payload.end())}});
387 }
388 return ret;
389}
390
391std::map<std::string, std::string>
392ContactList::getTrustRequest(const dht::InfoHash& from) const
393{
394 using Map = std::map<std::string, std::string>;
395 std::lock_guard lk(mutex_);
396 auto r = trustRequests_.find(from);
397 if (r == trustRequests_.end())
398 return {};
399 return Map {{libjami::Account::TrustRequest::FROM, r->first.toString()},
400 {libjami::Account::TrustRequest::RECEIVED, std::to_string(r->second.received)},
401 {libjami::Account::TrustRequest::CONVERSATIONID, r->second.conversationId},
403 std::string(r->second.payload.begin(), r->second.payload.end())}};
404}
405
406bool
407ContactList::acceptTrustRequest(const dht::InfoHash& from)
408{
409 // The contact sent us a TR so we are in its contact list
410 std::unique_lock lk(mutex_);
411 auto i = trustRequests_.find(from);
412 if (i == trustRequests_.end())
413 return false;
414 auto convId = i->second.conversationId;
415 // Clear trust request
416 trustRequests_.erase(i);
417 saveTrustRequests();
418 lk.unlock();
419 addContact(from, true, convId);
420 return true;
421}
422
423void
424ContactList::acceptConversation(const std::string& convId, const std::string& deviceId)
425{
426 if (callbacks_.acceptConversation)
427 callbacks_.acceptConversation(convId, deviceId);
428}
429
430bool
431ContactList::discardTrustRequest(const dht::InfoHash& from)
432{
433 std::lock_guard lk(mutex_);
434 if (trustRequests_.erase(from) > 0) {
435 saveTrustRequests();
436 return true;
437 }
438 return false;
439}
440
441void
442ContactList::loadKnownDevices()
443{
444 auto& certStore = jami::Manager::instance().certStore(accountId_);
445 try {
446 // read file
447 auto file = fileutils::loadFile("knownDevices", path_);
448 // load values
449 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
450
451 std::map<dht::PkId, std::pair<std::string, uint64_t>> knownDevices;
452 oh.get().convert(knownDevices);
453 for (const auto& d : knownDevices) {
454 if (auto crt = certStore.getCertificate(d.first.toString())) {
455 if (not foundAccountDevice(crt, d.second.first, clock::from_time_t(d.second.second), false))
456 JAMI_WARNING("[Account {}] [Contacts] Unable to add device {}", accountId_, d.first);
457 } else {
458 JAMI_WARNING("[Account {}] [Contacts] Unable to find certificate for device {}", accountId_,
459 d.first);
460 }
461 }
462 if (not knownDevices.empty()) {
463 callbacks_.devicesChanged(knownDevices_);
464 }
465 } catch (const std::exception& e) {
466 JAMI_WARNING("[Account {}] [Contacts] Error loading devices: {}", accountId_, e.what());
467 return;
468 }
469}
470
471void
472ContactList::saveKnownDevices() const
473{
474 std::ofstream file(path_ / "knownDevices", std::ios::trunc | std::ios::binary);
475
476 std::map<dht::PkId, std::pair<std::string, uint64_t>> devices;
477 for (const auto& id : knownDevices_) {
478 devices.emplace(id.first,
479 std::make_pair(id.second.name, clock::to_time_t(id.second.last_sync)));
480 }
481
482 msgpack::pack(file, devices);
483}
484
485void
486ContactList::foundAccountDevice(const dht::PkId& device,
487 const std::string& name,
488 const time_point& updated)
489{
490 // insert device
491 auto it = knownDevices_.emplace(device, KnownDevice {{}, name, updated});
492 if (it.second) {
493 JAMI_LOG("[Account {}] [Contacts] Found account device: {} {}", accountId_, name, device);
494 saveKnownDevices();
495 callbacks_.devicesChanged(knownDevices_);
496 } else {
497 // update device name
498 if (not name.empty() and it.first->second.name != name) {
499 JAMI_LOG("[Account {}] [Contacts] Updating device name: {} {}", accountId_,
500 name, device);
501 it.first->second.name = name;
502 saveKnownDevices();
503 callbacks_.devicesChanged(knownDevices_);
504 }
505 }
506}
507
508bool
509ContactList::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& crt,
510 const std::string& name,
511 const time_point& updated,
512 bool notify)
513{
514 if (not crt)
515 return false;
516
517 auto id = crt->getLongId();
518
519 // match certificate chain
520 auto verifyResult = accountTrust_.verify(*crt);
521 if (not verifyResult) {
522 JAMI_WARNING("[Account {}] [Contacts] Found invalid account device: {:s}: {:s}",
523 accountId_, id, verifyResult.toString());
524 return false;
525 }
526
527 // insert device
528 auto it = knownDevices_.emplace(id, KnownDevice {crt, name, updated});
529 if (it.second) {
530 JAMI_LOG("[Account {}] [Contacts] Found account device: {} {}", accountId_, name, id);
531 jami::Manager::instance().certStore(accountId_).pinCertificate(crt);
532 if (crt->ocspResponse) {
533 unsigned int status = crt->ocspResponse->getCertificateStatus();
534 if (status == GNUTLS_OCSP_CERT_REVOKED) {
535 JAMI_ERROR("[Account {}] Certificate {} has revoked OCSP status", accountId_, id);
536 trust_->setCertificateStatus(crt, dhtnet::tls::TrustStore::PermissionStatus::BANNED, false);
537 }
538 }
539 if (notify) {
540 saveKnownDevices();
541 callbacks_.devicesChanged(knownDevices_);
542 }
543 } else {
544 // update device name
545 if (not name.empty() and it.first->second.name != name) {
546 JAMI_LOG("[Account {}] [Contacts] updating device name: {} {}", accountId_, name, id);
547 it.first->second.name = name;
548 if (notify) {
549 saveKnownDevices();
550 callbacks_.devicesChanged(knownDevices_);
551 }
552 }
553 }
554 return true;
555}
556
557bool
558ContactList::removeAccountDevice(const dht::PkId& device)
559{
560 if (knownDevices_.erase(device) > 0) {
561 saveKnownDevices();
562 return true;
563 }
564 return false;
565}
566
567void
568ContactList::setAccountDeviceName(const dht::PkId& device, const std::string& name)
569{
570 auto dev = knownDevices_.find(device);
571 if (dev != knownDevices_.end()) {
572 if (dev->second.name != name) {
573 dev->second.name = name;
574 saveKnownDevices();
575 callbacks_.devicesChanged(knownDevices_);
576 }
577 }
578}
579
580std::string
581ContactList::getAccountDeviceName(const dht::PkId& device) const
582{
583 auto dev = knownDevices_.find(device);
584 if (dev != knownDevices_.end()) {
585 return dev->second.name;
586 }
587 return {};
588}
589
592{
594 sync_data.date = clock::now().time_since_epoch().count();
595 // sync_data.device_name = deviceName_;
596 sync_data.peers = getContacts();
597
598 static constexpr size_t MAX_TRUST_REQUESTS = 20;
599 std::lock_guard lk(mutex_);
600 if (trustRequests_.size() <= MAX_TRUST_REQUESTS)
601 for (const auto& req : trustRequests_)
602 sync_data.trust_requests.emplace(req.first,
603 TrustRequest {req.second.device,
604 req.second.conversationId,
605 req.second.received,
606 {}});
607 else {
608 size_t inserted = 0;
609 auto req = trustRequests_.lower_bound(dht::InfoHash::getRandom());
610 while (inserted++ < MAX_TRUST_REQUESTS) {
611 if (req == trustRequests_.end())
612 req = trustRequests_.begin();
613 sync_data.trust_requests.emplace(req->first,
614 TrustRequest {req->second.device,
615 req->second.conversationId,
616 req->second.received,
617 {}});
618 ++req;
619 }
620 }
621
622 for (const auto& dev : knownDevices_) {
623 if (!dev.second.certificate) {
624 JAMI_WARNING("[Account {}] [Contacts] No certificate found for {}", accountId_, dev.first);
625 continue;
626 }
627 sync_data.devices.emplace(dev.second.certificate->getLongId(),
628 KnownDeviceSync {dev.second.name,
629 dev.second.certificate->getId()});
630 }
631 return sync_data;
632}
633
634bool
635ContactList::syncDevice(const dht::PkId& device, const time_point& syncDate)
636{
637 auto it = knownDevices_.find(device);
638 if (it == knownDevices_.end()) {
639 JAMI_WARNING("[Account {}] [Contacts] Dropping sync data from unknown device", accountId_);
640 return false;
641 }
642 if (it->second.last_sync >= syncDate) {
643 JAMI_LOG("[Account {}] [Contacts] Dropping outdated sync data", accountId_);
644 return false;
645 }
646 it->second.last_sync = syncDate;
647 return true;
648}
649
650} // namespace jami
bool removeAccountDevice(const dht::PkId &device)
std::vector< std::map< std::string, std::string > > getTrustRequests() const
ContactList(const std::string &accountId, const std::shared_ptr< crypto::Certificate > &cert, const std::filesystem::path &path, OnChangeCallback cb)
void foundAccountDevice(const dht::PkId &device, const std::string &name={}, const time_point &last_sync=time_point::min())
bool removeContactConversation(const dht::InfoHash &)
bool addContact(const dht::InfoHash &, bool confirmed=false, const std::string &conversationId="")
clock::time_point time_point
void setContacts(const std::map< dht::InfoHash, Contact > &)
void saveContacts() const
Should be called only after updateContact.
bool onTrustRequest(const dht::InfoHash &peer_account, const std::shared_ptr< dht::crypto::PublicKey > &peer_device, time_t received, bool confirm, const std::string &conversationId, std::vector< uint8_t > &&payload)
Inform of a new contact request.
std::string getAccountDeviceName(const dht::PkId &device) const
DeviceSync getSyncData() const
void setAccountDeviceName(const dht::PkId &device, const std::string &name)
bool acceptTrustRequest(const dht::InfoHash &from)
bool discardTrustRequest(const dht::InfoHash &from)
std::map< std::string, std::string > getContactDetails(const dht::InfoHash &) const
void updateConversation(const dht::InfoHash &h, const std::string &conversationId)
void updateContact(const dht::InfoHash &, const Contact &, bool emit=true)
bool setCertificateStatus(const std::string &cert_id, const dhtnet::tls::TrustStore::PermissionStatus status)
std::optional< Contact > getContactInfo(const dht::InfoHash &) const
const std::map< dht::InfoHash, Contact > & getContacts() const
void acceptConversation(const std::string &convId, const std::string &deviceId="")
std::map< std::string, std::string > getTrustRequest(const dht::InfoHash &from) const
bool removeContact(const dht::InfoHash &, bool ban)
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:676
dhtnet::tls::CertificateStore & certStore(const std::string &accountId) const
Definition manager.cpp:3302
#define JAMI_ERROR(formatstr,...)
Definition logger.h:228
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
#define JAMI_LOG(formatstr,...)
Definition logger.h:225
Definition Address.h:25
std::vector< uint8_t > loadFile(const std::filesystem::path &path, const std::filesystem::path &default_dir)
Read the full content of a file at path.
std::filesystem::path getFullPath(const std::filesystem::path &base, const std::filesystem::path &path)
If path is relative, it is appended to base.
static constexpr std::string_view toString(AuthDecodingState state)
void emitSignal(Args... args)
Definition ring_signal.h:64
static constexpr const char PAYLOAD[]
static constexpr const char RECEIVED[]
static constexpr const char CONVERSATIONID[]
static constexpr const char FROM[]
OnIncomingTrustRequest trustRequest
OnAcceptConversation acceptConversation
std::shared_ptr< dht::crypto::PublicKey > device