Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 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 :
22 : #include "jamiaccount.h"
23 : #include "presence_manager.h"
24 :
25 : #include "logger.h"
26 :
27 : #include "accountarchive.h"
28 : #include "jami_contact.h"
29 : #include "configkeys.h"
30 : #include "contact_list.h"
31 : #include "archive_account_manager.h"
32 : #include "server_account_manager.h"
33 : #include "jamidht/commit_message.h"
34 : #include "jamidht/channeled_transport.h"
35 : #include "jamidht/collaborative_editing.h"
36 : #include "conversation_channel_handler.h"
37 : #include "sync_channel_handler.h"
38 : #include "ydoc_channel_handler.h"
39 : #include "message_channel_handler.h"
40 : #include "auth_channel_handler.h"
41 : #include "transfer_channel_handler.h"
42 : #include "swarm/swarm_channel_handler.h"
43 : #include "service_manager.h"
44 : #include "svc_discovery_channel_handler.h"
45 : #include "svc_tunnel_channel_handler.h"
46 : #include "jami/media_const.h"
47 :
48 : #include "sip/sdp.h"
49 : #include "sip/sipvoiplink.h"
50 : #include "sip/sipcall.h"
51 : #include "sip/siptransport.h"
52 : #include "connectivity/sip_utils.h"
53 :
54 : #include "uri.h"
55 :
56 : #include "client/jami_signal.h"
57 : #include "jami/call_const.h"
58 : #include "jami/account_const.h"
59 :
60 : #include "system_codec_container.h"
61 :
62 : #include "account_schema.h"
63 : #include "manager.h"
64 : #include "connectivity/utf8_utils.h"
65 : #include "connectivity/ip_utils.h"
66 :
67 : #ifdef ENABLE_PLUGIN
68 : #include "plugin/jamipluginmanager.h"
69 : #include "plugin/chatservicesmanager.h"
70 : #endif
71 :
72 : #ifdef ENABLE_VIDEO
73 : #include "libav_utils.h"
74 : #endif
75 : #include "fileutils.h"
76 : #include "string_utils.h"
77 : #include "archiver.h"
78 : #include "data_transfer.h"
79 : #include "json_utils.h"
80 :
81 : #include "libdevcrypto/Common.h"
82 : #include "base64.h"
83 : #include "vcard.h"
84 : #include "im/instant_messaging.h"
85 :
86 : #include <dhtnet/ice_transport.h>
87 : #include <dhtnet/ice_transport_factory.h>
88 : #include <dhtnet/upnp/upnp_control.h>
89 : #include <dhtnet/multiplexed_socket.h>
90 : #include <dhtnet/certstore.h>
91 :
92 : #include <opendht/thread_pool.h>
93 : #include <opendht/peer_discovery.h>
94 : #include <opendht/http.h>
95 :
96 : #include <yaml-cpp/yaml.h>
97 : #include <fmt/format.h>
98 :
99 : #include <unistd.h>
100 :
101 : #include <algorithm>
102 : #include <array>
103 : #include <cctype>
104 : #include <charconv>
105 : #include <cinttypes>
106 : #include <cstdarg>
107 : #include <fstream>
108 : #include <initializer_list>
109 : #include <memory>
110 : #include <regex>
111 : #include <sstream>
112 : #include <string>
113 : #include <system_error>
114 : #include <utility>
115 :
116 : using namespace std::placeholders;
117 :
118 : namespace jami {
119 :
120 : static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"};
121 : static constexpr const char MIME_TYPE_PIDF[] {"application/pidf+xml"};
122 : static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"};
123 : static constexpr const char DEVICE_ID_PATH[] {"ring_device"};
124 : static constexpr auto TREATED_PATH = "treatedImMessages"sv;
125 :
126 : struct VCardMessageCtx
127 : {
128 : std::shared_ptr<std::atomic_int> success;
129 : int total;
130 : std::string path;
131 : };
132 :
133 : namespace Migration {
134 :
135 : enum class State { // Contains all the Migration states
136 : SUCCESS,
137 : INVALID
138 : };
139 :
140 : std::string
141 4 : mapStateNumberToString(const State migrationState)
142 : {
143 : #define CASE_STATE(X) \
144 : case Migration::State::X: \
145 : return #X
146 :
147 4 : switch (migrationState) {
148 0 : CASE_STATE(INVALID);
149 12 : CASE_STATE(SUCCESS);
150 : }
151 0 : return {};
152 : }
153 :
154 : void
155 4 : setState(const std::string& accountID, const State migrationState)
156 : {
157 4 : emitSignal<libjami::ConfigurationSignal::MigrationEnded>(accountID, mapStateNumberToString(migrationState));
158 4 : }
159 :
160 : } // namespace Migration
161 :
162 : struct JamiAccount::PendingCall
163 : {
164 : std::chrono::steady_clock::time_point start;
165 : std::shared_ptr<IceTransport> ice_sp;
166 : std::shared_ptr<IceTransport> ice_tcp_sp;
167 : std::weak_ptr<SIPCall> call;
168 : std::future<size_t> listen_key;
169 : dht::InfoHash call_key;
170 : dht::InfoHash from;
171 : dht::InfoHash from_account;
172 : std::shared_ptr<dht::crypto::Certificate> from_cert;
173 : };
174 :
175 : struct JamiAccount::PendingMessage
176 : {
177 : std::set<DeviceId> to;
178 : };
179 :
180 : struct AccountPeerInfo
181 : {
182 : dht::InfoHash accountId;
183 : std::string displayName;
184 0 : MSGPACK_DEFINE(accountId, displayName)
185 : };
186 :
187 : struct JamiAccount::DiscoveredPeer
188 : {
189 : std::string displayName;
190 : std::unique_ptr<asio::steady_timer> cleanupTimer;
191 : };
192 :
193 : /**
194 : * Track sending state for a single message to one or more devices.
195 : */
196 : class JamiAccount::SendMessageContext
197 : {
198 : public:
199 : using OnComplete = std::function<void(bool, bool)>;
200 14873 : SendMessageContext(OnComplete onComplete)
201 14873 : : onComplete(std::move(onComplete))
202 14873 : {}
203 : /** Track new pending message for device */
204 13524 : bool add(const DeviceId& device)
205 : {
206 13524 : std::lock_guard lk(mtx);
207 27048 : return devices.insert(device).second;
208 13524 : }
209 : /** Call after all messages are sent */
210 14873 : void start()
211 : {
212 14873 : std::unique_lock lk(mtx);
213 14873 : started = true;
214 14873 : checkComplete(lk);
215 14873 : }
216 : /** Complete pending message for device */
217 12583 : bool complete(const DeviceId& device, bool success)
218 : {
219 12583 : std::unique_lock lk(mtx);
220 12583 : if (devices.erase(device) == 0)
221 0 : return false;
222 12583 : ++completeCount;
223 12583 : if (success)
224 12583 : ++successCount;
225 12583 : checkComplete(lk);
226 12583 : return true;
227 12583 : }
228 : bool empty() const
229 : {
230 : std::lock_guard lk(mtx);
231 : return devices.empty();
232 : }
233 2329 : bool pending(const DeviceId& device) const
234 : {
235 2329 : std::lock_guard lk(mtx);
236 4658 : return devices.find(device) != devices.end();
237 2329 : }
238 :
239 : private:
240 : mutable std::mutex mtx;
241 : OnComplete onComplete;
242 : std::set<DeviceId> devices;
243 : unsigned completeCount = 0;
244 : unsigned successCount = 0;
245 : bool started {false};
246 :
247 27456 : void checkComplete(std::unique_lock<std::mutex>& lk)
248 : {
249 27456 : if (started && (devices.empty() || successCount)) {
250 14880 : if (onComplete) {
251 14873 : auto cb = std::move(onComplete);
252 14873 : auto success = successCount != 0;
253 14873 : auto complete = completeCount != 0;
254 14873 : onComplete = {};
255 14873 : lk.unlock();
256 14873 : cb(success, complete);
257 14873 : }
258 : }
259 27456 : }
260 : };
261 :
262 : static const constexpr std::string_view RING_URI_PREFIX = "ring:";
263 : static const constexpr std::string_view JAMI_URI_PREFIX = "jami:";
264 : static const auto PROXY_REGEX = std::regex("(https?://)?([\\w\\.\\-_\\~]+)(:(\\d+)|:\\[(.+)-(.+)\\])?");
265 : static const constexpr std::string_view PEER_DISCOVERY_JAMI_SERVICE = "jami";
266 : const constexpr auto PEER_DISCOVERY_EXPIRATION = std::chrono::minutes(1);
267 :
268 : using ValueIdDist = std::uniform_int_distribution<dht::Value::Id>;
269 :
270 : std::string_view
271 41514 : stripPrefix(std::string_view toUrl)
272 : {
273 41514 : auto dhtf = toUrl.find(RING_URI_PREFIX);
274 41514 : if (dhtf != std::string_view::npos) {
275 0 : dhtf += RING_URI_PREFIX.size();
276 : } else {
277 41514 : dhtf = toUrl.find(JAMI_URI_PREFIX);
278 41514 : if (dhtf != std::string_view::npos) {
279 0 : dhtf += JAMI_URI_PREFIX.size();
280 : } else {
281 41514 : dhtf = toUrl.find("sips:");
282 41514 : dhtf = (dhtf == std::string_view::npos) ? 0 : dhtf + 5;
283 : }
284 : }
285 41514 : while (dhtf < toUrl.length() && toUrl[dhtf] == '/')
286 0 : dhtf++;
287 41514 : return toUrl.substr(dhtf);
288 : }
289 :
290 : std::string_view
291 41514 : parseJamiUri(std::string_view toUrl)
292 : {
293 41514 : auto sufix = stripPrefix(toUrl);
294 41514 : if (sufix.length() < 40)
295 0 : throw std::invalid_argument("Not a valid Jami URI: " + toUrl);
296 :
297 41514 : const std::string_view toUri = sufix.substr(0, 40);
298 41514 : if (std::find_if_not(toUri.cbegin(), toUri.cend(), ::isxdigit) != toUri.cend())
299 0 : throw std::invalid_argument("Not a valid Jami URI: " + toUrl);
300 41514 : return toUri;
301 : }
302 :
303 : static constexpr std::string_view
304 5378 : dhtStatusStr(dht::NodeStatus status)
305 : {
306 : return status == dht::NodeStatus::Connected
307 5378 : ? "connected"sv
308 5378 : : (status == dht::NodeStatus::Connecting ? "connecting"sv : "disconnected"sv);
309 : }
310 :
311 1016 : JamiAccount::JamiAccount(const std::string& accountId)
312 : : SIPAccountBase(accountId)
313 1016 : , cachePath_(fileutils::get_cache_dir() / accountId)
314 1016 : , dataPath_(cachePath_ / "values")
315 2032 : , logger_(Logger::dhtLogger(fmt::format("Account {}", accountId)))
316 1016 : , certStore_ {std::make_shared<dhtnet::tls::CertificateStore>(idPath_, logger_)}
317 1016 : , dht_(std::make_shared<dht::DhtRunner>())
318 1016 : , treatedMessages_(cachePath_ / TREATED_PATH)
319 1016 : , presenceManager_(std::make_unique<PresenceManager>(dht_))
320 1016 : , connectionManager_ {}
321 8128 : , nonSwarmTransferManager_()
322 : {
323 1016 : presenceListenerToken_ = presenceManager_->addListener([this](const std::string& uri, bool online) {
324 1524 : runOnMainThread([w = weak(), uri, online] {
325 1524 : if (auto sthis = w.lock()) {
326 1524 : if (online) {
327 1396 : sthis->onTrackedBuddyOnline(uri);
328 1396 : sthis->messageEngine_.onPeerOnline(uri);
329 : } else {
330 128 : sthis->onTrackedBuddyOffline(uri);
331 : }
332 1524 : }
333 1524 : });
334 1524 : });
335 : // When a device of a contact that advertises services changes presence,
336 : // re-publish that peer's service list with refreshed availability so an
337 : // already-open services menu greys out / re-enables entries live.
338 1016 : svcPresenceListenerToken_ = presenceManager_->addDeviceListener([this](const std::string& uri,
339 : const dht::PkId&,
340 : bool) {
341 1610 : runOnMainThread([w = weak(), uri] {
342 1610 : auto sthis = w.lock();
343 1610 : if (!sthis)
344 0 : return;
345 1610 : auto servicesJson = sthis->buildPeerServicesJson(uri);
346 1610 : if (servicesJson.empty())
347 1610 : return;
348 0 : emitSignal<libjami::ServiceSignal::PeerServicesReceived>(0u,
349 0 : sthis->getAccountID(),
350 0 : uri,
351 : static_cast<int>(
352 : libjami::ServiceSignal::PeerServicesStatus::OK),
353 : servicesJson);
354 3220 : });
355 1610 : });
356 1016 : }
357 :
358 2032 : JamiAccount::~JamiAccount() noexcept
359 : {
360 1016 : if (dht_)
361 1016 : dht_->join();
362 1016 : }
363 :
364 : void
365 1057 : JamiAccount::shutdownConnections()
366 : {
367 1057 : JAMI_LOG("[Account {}] Shutdown connections", getAccountID());
368 :
369 1057 : decltype(gitServers_) gservers;
370 : {
371 1057 : std::lock_guard lk(gitServersMtx_);
372 1057 : gservers = std::move(gitServers_);
373 1057 : }
374 1781 : for (auto& [_id, gs] : gservers)
375 1448 : dht::ThreadPool::io().run([gs = std::shared_ptr(std::move(gs))] { gs->stop(); });
376 1057 : gservers.clear();
377 :
378 : {
379 1057 : std::lock_guard lk(connManagerMtx_);
380 : // Just move destruction on another thread.
381 1057 : dht::ThreadPool::io().run([conMgr = std::shared_ptr(std::move(connectionManager_))] {});
382 1057 : connectionManager_.reset();
383 1057 : channelHandlers_.clear();
384 1057 : }
385 1057 : if (convModule_) {
386 917 : convModule_->shutdownConnections();
387 : }
388 :
389 1057 : std::lock_guard lk(sipConnsMtx_);
390 1057 : sipConns_.clear();
391 1057 : }
392 :
393 : void
394 1011 : JamiAccount::flush()
395 : {
396 : // Class base method
397 1011 : SIPAccountBase::flush();
398 :
399 1011 : dhtnet::fileutils::removeAll(cachePath_);
400 1011 : dhtnet::fileutils::removeAll(dataPath_);
401 1011 : dhtnet::fileutils::removeAll(idPath_, true);
402 1011 : }
403 :
404 : std::shared_ptr<SIPCall>
405 103 : JamiAccount::newIncomingCall(const std::string& from,
406 : const std::vector<libjami::MediaMap>& mediaList,
407 : const std::shared_ptr<SipTransport>& sipTransp)
408 : {
409 103 : JAMI_DEBUG("New incoming call from {:s} with {:d} media", from, mediaList.size());
410 :
411 103 : if (sipTransp) {
412 103 : auto call = Manager::instance().callFactory.newSipCall(shared(), Call::CallType::INCOMING, mediaList);
413 103 : call->setPeerUri(JAMI_URI_PREFIX + from);
414 103 : call->setPeerNumber(from);
415 :
416 103 : call->setSipTransport(sipTransp, getContactHeader(sipTransp));
417 :
418 103 : return call;
419 103 : }
420 :
421 0 : JAMI_ERROR("newIncomingCall: unable to find matching call for {}", from);
422 0 : return nullptr;
423 : }
424 :
425 : std::shared_ptr<Call>
426 117 : JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami::MediaMap>& mediaList)
427 : {
428 117 : auto uri = Uri(toUrl);
429 117 : if (uri.scheme() == Uri::Scheme::SWARM || uri.scheme() == Uri::Scheme::RENDEZVOUS) {
430 : // NOTE: In this case newOutgoingCall can act as "resumeConference" and just attach the
431 : // host to the current hosted conference. So, no call will be returned in that case.
432 22 : return newSwarmOutgoingCallHelper(uri, mediaList);
433 : }
434 :
435 95 : auto& manager = Manager::instance();
436 95 : std::shared_ptr<SIPCall> call;
437 :
438 : // SIP allows sending empty invites, this use case is not used with Jami accounts.
439 95 : if (not mediaList.empty()) {
440 33 : call = manager.callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
441 : } else {
442 62 : JAMI_WARNING("Media list is empty, setting a default list");
443 124 : call = manager.callFactory.newSipCall(shared(),
444 : Call::CallType::OUTGOING,
445 124 : MediaAttribute::mediaAttributesToMediaMaps(
446 186 : createDefaultMediaList(isVideoEnabled())));
447 : }
448 :
449 95 : if (not call)
450 0 : return {};
451 :
452 95 : std::shared_lock lkCM(connManagerMtx_);
453 95 : if (!connectionManager_)
454 0 : return {};
455 :
456 95 : connectionManager_->getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) {
457 95 : if (call->isIceEnabled()) {
458 95 : if (not call->createIceMediaTransport(false)
459 190 : or not call->initIceMediaTransport(true, std::forward<dhtnet::IceTransportOptions>(opts))) {
460 0 : return;
461 : }
462 : }
463 95 : auto shared = w.lock();
464 95 : if (!shared)
465 0 : return;
466 95 : JAMI_LOG("New outgoing call with {}", uri.toString());
467 95 : call->setPeerNumber(uri.authority());
468 95 : call->setPeerUri(uri.toString());
469 :
470 95 : shared->newOutgoingCallHelper(call, uri);
471 95 : });
472 :
473 95 : return call;
474 117 : }
475 :
476 : void
477 95 : JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri)
478 : {
479 95 : JAMI_LOG("[Account {}] Calling peer {}", getAccountID(), uri.authority());
480 : try {
481 95 : startOutgoingCall(call, uri.authority());
482 0 : } catch (const std::invalid_argument&) {
483 0 : auto suffix = stripPrefix(uri.toString());
484 0 : NameDirectory::lookupUri(suffix,
485 0 : config().nameServer,
486 0 : [wthis_ = weak(), call](const std::string& regName,
487 : const std::string& address,
488 : NameDirectory::Response response) {
489 : // we may run inside an unknown thread, but following code must
490 : // be called in main thread
491 0 : runOnMainThread([wthis_, regName, address, response, call]() {
492 0 : if (response != NameDirectory::Response::found) {
493 0 : call->onFailure(PJSIP_SC_NOT_FOUND);
494 0 : return;
495 : }
496 0 : if (auto sthis = wthis_.lock()) {
497 : try {
498 0 : sthis->startOutgoingCall(call, address);
499 0 : } catch (const std::invalid_argument&) {
500 0 : call->onFailure(PJSIP_SC_NOT_FOUND);
501 0 : }
502 : } else {
503 0 : call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
504 0 : }
505 : });
506 0 : });
507 0 : }
508 95 : }
509 :
510 : std::shared_ptr<SIPCall>
511 22 : JamiAccount::newSwarmOutgoingCallHelper(const Uri& uri, const std::vector<libjami::MediaMap>& mediaList)
512 : {
513 22 : JAMI_DEBUG("[Account {}] Calling conversation {}", getAccountID(), uri.authority());
514 : return convModule()
515 22 : ->call(uri.authority(), mediaList, [this, uri](const auto& accountUri, const auto& deviceId, const auto& call) {
516 11 : if (!call)
517 0 : return;
518 :
519 11 : std::string peerId = accountUri;
520 11 : if (uri.scheme() == Uri::Scheme::RENDEZVOUS) {
521 9 : auto parts = jami::split_string(accountUri, '/');
522 9 : if (parts.size() == 4)
523 18 : peerId = std::string(parts[1]);
524 9 : }
525 :
526 11 : std::unique_lock lkSipConn(sipConnsMtx_);
527 12 : for (auto& [key, value] : sipConns_) {
528 1 : if (key.first != peerId || key.second != deviceId)
529 0 : continue;
530 1 : if (value.empty())
531 0 : continue;
532 1 : auto& sipConn = value.back();
533 :
534 1 : if (!sipConn.channel) {
535 0 : JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
536 0 : continue;
537 : }
538 :
539 1 : auto transport = sipConn.transport;
540 1 : if (!transport or !sipConn.channel)
541 0 : continue;
542 1 : call->setState(Call::ConnectionState::PROGRESSING);
543 1 : call->setSipTransport(transport, getContactHeader(transport));
544 :
545 1 : auto remoted_address = sipConn.channel->getRemoteAddress();
546 : try {
547 1 : onConnectedOutgoingCall(call, uri.authority(), remoted_address);
548 1 : return;
549 0 : } catch (const VoipLinkException&) {
550 : // In this case, the main scenario is that SIPStartCall failed because
551 : // the ICE is dead and the TLS session didn't send any packet on that dead
552 : // link (connectivity change, killed by the operating system, etc)
553 : // Here, we don't need to do anything, the TLS will fail and will delete
554 : // the cached transport
555 0 : continue;
556 : }
557 : }
558 10 : lkSipConn.unlock();
559 : {
560 10 : std::lock_guard lkP(pendingCallsMutex_);
561 10 : pendingCalls_[deviceId].emplace_back(call);
562 10 : }
563 :
564 : // Else, ask for a channel (for future calls/text messages)
565 10 : auto type = call->hasVideo() ? "videoCall" : "audioCall";
566 10 : JAMI_WARNING("[call {}] No channeled socket with this peer. Send request", call->getCallId());
567 20 : requestSIPConnection(peerId, deviceId, type, true, call);
568 56 : });
569 : }
570 :
571 : void
572 10 : JamiAccount::handleIncomingConversationCall(const std::string& callId, const std::string& destination)
573 : {
574 10 : auto split = jami::split_string(destination, '/');
575 10 : if (split.size() != 4)
576 0 : return;
577 20 : auto conversationId = std::string(split[0]);
578 20 : auto accountUri = std::string(split[1]);
579 20 : auto deviceId = std::string(split[2]);
580 10 : auto confId = std::string(split[3]);
581 :
582 10 : if (getUsername() != accountUri || currentDeviceId() != deviceId)
583 0 : return;
584 :
585 : // Avoid concurrent checks in this part
586 10 : std::lock_guard lk(rdvMtx_);
587 10 : auto isNotHosting = !convModule()->isHosting(conversationId, confId);
588 10 : if (confId == "0") {
589 1 : auto currentCalls = convModule()->getActiveCalls(conversationId);
590 1 : if (!currentCalls.empty()) {
591 0 : confId = currentCalls[0]["id"];
592 0 : isNotHosting = false;
593 : } else {
594 1 : confId = callId;
595 1 : JAMI_DEBUG("No active call to join, create conference");
596 : }
597 1 : }
598 10 : auto preferences = convModule()->getConversationPreferences(conversationId);
599 10 : auto canHost = true;
600 : #if defined(__ANDROID__) || defined(__APPLE__)
601 : // By default, mobile devices SHOULD NOT host conferences.
602 : canHost = false;
603 : #endif
604 10 : auto itPref = preferences.find(ConversationPreferences::HOST_CONFERENCES);
605 10 : if (itPref != preferences.end()) {
606 0 : canHost = itPref->second == TRUE_STR;
607 : }
608 :
609 10 : auto call = getCall(callId);
610 10 : if (!call) {
611 0 : JAMI_ERROR("Call {} not found", callId);
612 0 : return;
613 : }
614 :
615 10 : if (isNotHosting && !canHost) {
616 0 : JAMI_DEBUG("Request for hosting a conference declined");
617 0 : Manager::instance().hangupCall(getAccountID(), callId);
618 0 : return;
619 : }
620 : // Due to the fact that in a conference, the host is not the one who
621 : // provides the initial sdp offer, the following block of code is responsible
622 : // for handling the medialist that the host will form his response with.
623 : // We always want the hosts response to be the same length as that of the
624 : // peer who is asking to join (providing the offer). A priori though the peer
625 : // doesn't know what active media streams the host will have so we deal with the
626 : // possible cases here.
627 10 : std::shared_ptr<Conference> conf;
628 10 : std::vector<libjami::MediaMap> currentMediaList;
629 10 : if (!isNotHosting) {
630 7 : conf = getConference(confId);
631 7 : if (!conf) {
632 0 : JAMI_ERROR("[conf:{}] Conference not found", confId);
633 0 : return;
634 : }
635 7 : auto hostMedias = conf->currentMediaList();
636 7 : auto sipCall = std::dynamic_pointer_cast<SIPCall>(call);
637 7 : if (hostMedias.empty()) {
638 0 : currentMediaList = MediaAttribute::mediaAttributesToMediaMaps(
639 0 : createDefaultMediaList(call->hasVideo(), true));
640 7 : } else if (hostMedias.size() < sipCall->getRtpSessionList().size()) {
641 : // First case: host has less media streams than the other person is joining
642 : // with. We need to add video media to the host before accepting the offer
643 : // This can happen if we host an audio call and someone joins with video
644 0 : currentMediaList = hostMedias;
645 0 : currentMediaList.push_back(
646 : {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO},
647 : {libjami::Media::MediaAttributeKey::ENABLED, TRUE_STR},
648 : {libjami::Media::MediaAttributeKey::MUTED, TRUE_STR},
649 : {libjami::Media::MediaAttributeKey::SOURCE, ""},
650 : {libjami::Media::MediaAttributeKey::LABEL, "video_0"}});
651 : } else {
652 7 : bool hasVideo = false;
653 7 : if (sipCall) {
654 7 : const auto rtpSessions = sipCall->getRtpSessionList();
655 7 : hasVideo = std::any_of(rtpSessions.begin(), rtpSessions.end(), [](const auto& session) {
656 13 : return session && session->getMediaType() == MediaType::MEDIA_VIDEO;
657 : });
658 7 : }
659 : // The second case is that the host has the same or more media
660 : // streams than the person joining. In this case we match all their
661 : // medias to form our offer. They will then potentially join the call without seeing
662 : // seeing all of our medias. For now we deal with this by calling a
663 : // requestmediachange once they've joined.
664 14 : for (const auto& m : conf->currentMediaList()) {
665 : // We only expect to have 1 audio stream, add it.
666 26 : if (m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE) == libjami::Media::MediaAttributeValue::AUDIO) {
667 7 : currentMediaList.emplace_back(m);
668 6 : } else if (hasVideo
669 24 : && m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE)
670 6 : == libjami::Media::MediaAttributeValue::VIDEO) {
671 6 : currentMediaList.emplace_back(m);
672 6 : break;
673 : }
674 7 : }
675 : }
676 7 : }
677 10 : Manager::instance().acceptCall(*call, currentMediaList);
678 :
679 10 : if (isNotHosting) {
680 3 : JAMI_DEBUG("Creating conference for swarm {} with ID {}", conversationId, confId);
681 : // Create conference and host it.
682 3 : convModule()->hostConference(conversationId, confId, callId);
683 : } else {
684 7 : JAMI_DEBUG("Adding participant {} for swarm {} with ID {}", callId, conversationId, confId);
685 7 : Manager::instance().addAudio(*call);
686 7 : conf->addSubCall(callId);
687 7 : emitSignal<libjami::CallSignal::ConferenceChanged>(getAccountID(), conf->getConfId(), conf->getStateStr());
688 : }
689 10 : }
690 :
691 : std::shared_ptr<SIPCall>
692 190 : JamiAccount::createSubCall(const std::shared_ptr<SIPCall>& mainCall)
693 : {
694 190 : auto mediaList = MediaAttribute::mediaAttributesToMediaMaps(mainCall->getMediaAttributeList());
695 380 : return Manager::instance().callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
696 190 : }
697 :
698 : void
699 95 : JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std::string& toUri)
700 : {
701 95 : if (not accountManager_ or not dht_) {
702 0 : call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
703 0 : return;
704 : }
705 :
706 : // TODO: for now, we automatically trust all explicitly called peers
707 95 : setCertificateStatus(toUri, dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
708 :
709 95 : call->setState(Call::ConnectionState::TRYING);
710 95 : std::weak_ptr<SIPCall> wCall = call;
711 :
712 190 : accountManager_->lookupAddress(toUri,
713 190 : [wCall](const std::string& regName,
714 : const std::string& /*address*/,
715 : const NameDirectory::Response& response) {
716 95 : if (response == NameDirectory::Response::found)
717 1 : if (auto call = wCall.lock()) {
718 1 : call->setPeerRegisteredName(regName);
719 1 : }
720 95 : });
721 :
722 95 : dht::InfoHash peer_account(toUri);
723 95 : if (!peer_account) {
724 0 : throw std::invalid_argument("Invalid peer account: " + toUri);
725 : }
726 :
727 : // Call connected devices
728 95 : std::set<DeviceId> devices;
729 95 : std::unique_lock lkSipConn(sipConnsMtx_);
730 : // NOTE: dummyCall is a call used to avoid to mark the call as failed if the
731 : // cached connection is failing with ICE (close event still not detected).
732 95 : auto dummyCall = createSubCall(call);
733 :
734 95 : if (!dummyCall) {
735 0 : call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
736 0 : return;
737 : }
738 :
739 95 : call->addSubCall(*dummyCall);
740 95 : dummyCall->setIceMedia(call->getIceMedia());
741 281 : auto sendRequest = [this, wCall, toUri, dummyCall = std::move(dummyCall)](const DeviceId& deviceId,
742 : bool eraseDummy) {
743 186 : if (eraseDummy) {
744 : // Mark the temp call as failed to stop the main call if necessary
745 95 : if (dummyCall)
746 95 : dummyCall->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
747 95 : return;
748 : }
749 91 : auto call = wCall.lock();
750 91 : if (not call)
751 0 : return;
752 91 : auto state = call->getConnectionState();
753 91 : if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING)
754 0 : return;
755 :
756 91 : auto dev_call = createSubCall(call);
757 91 : dev_call->setPeerNumber(call->getPeerNumber());
758 91 : dev_call->setState(Call::ConnectionState::TRYING);
759 91 : call->addStateListener([w = weak(), deviceId](Call::CallState, Call::ConnectionState state, int) {
760 269 : if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING) {
761 91 : if (auto shared = w.lock())
762 91 : shared->callConnectionClosed(deviceId, true);
763 91 : return false;
764 : }
765 178 : return true;
766 : });
767 91 : call->addSubCall(*dev_call);
768 91 : dev_call->setIceMedia(call->getIceMedia());
769 : {
770 91 : std::lock_guard lk(pendingCallsMutex_);
771 91 : pendingCalls_[deviceId].emplace_back(dev_call);
772 91 : }
773 :
774 91 : JAMI_WARNING("[call {}] No channeled socket with this peer. Send request", call->getCallId());
775 : // Else, ask for a channel (for future calls/text messages)
776 91 : const auto* type = call->hasVideo() ? "videoCall" : "audioCall";
777 182 : requestSIPConnection(toUri, deviceId, type, true, dev_call);
778 186 : };
779 :
780 95 : std::vector<std::shared_ptr<dhtnet::ChannelSocket>> channels;
781 150 : for (auto& [key, value] : sipConns_) {
782 55 : if (key.first != toUri)
783 51 : continue;
784 4 : if (value.empty())
785 0 : continue;
786 4 : auto& sipConn = value.back();
787 :
788 4 : if (!sipConn.channel) {
789 0 : JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
790 0 : continue;
791 : }
792 :
793 4 : auto transport = sipConn.transport;
794 4 : auto remote_address = sipConn.channel->getRemoteAddress();
795 4 : if (!transport or !remote_address)
796 0 : continue;
797 :
798 4 : channels.emplace_back(sipConn.channel);
799 :
800 4 : JAMI_WARNING("[call {}] A channeled socket is detected with this peer.", call->getCallId());
801 :
802 4 : auto dev_call = createSubCall(call);
803 4 : dev_call->setPeerNumber(call->getPeerNumber());
804 4 : dev_call->setSipTransport(transport, getContactHeader(transport));
805 4 : call->addSubCall(*dev_call);
806 4 : dev_call->setIceMedia(call->getIceMedia());
807 :
808 : // Set the call in PROGRESSING State because the ICE session
809 : // is already ready. Note that this line should be after
810 : // addSubcall() to change the state of the main call
811 : // and avoid to get an active call in a TRYING state.
812 4 : dev_call->setState(Call::ConnectionState::PROGRESSING);
813 :
814 : {
815 4 : std::lock_guard lk(onConnectionClosedMtx_);
816 4 : onConnectionClosed_[key.second] = sendRequest;
817 4 : }
818 :
819 4 : call->addStateListener([w = weak(), deviceId = key.second](Call::CallState, Call::ConnectionState state, int) {
820 10 : if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING) {
821 4 : if (auto shared = w.lock())
822 4 : shared->callConnectionClosed(deviceId, true);
823 4 : return false;
824 : }
825 6 : return true;
826 : });
827 :
828 : try {
829 4 : onConnectedOutgoingCall(dev_call, toUri, remote_address);
830 0 : } catch (const VoipLinkException&) {
831 : // In this case, the main scenario is that SIPStartCall failed because
832 : // the ICE is dead and the TLS session didn't send any packet on that dead
833 : // link (connectivity change, killed by the os, etc)
834 : // Here, we don't need to do anything, the TLS will fail and will delete
835 : // the cached transport
836 0 : continue;
837 0 : }
838 4 : devices.emplace(key.second);
839 4 : }
840 :
841 95 : lkSipConn.unlock();
842 : // Note: Send beacon can destroy the socket (if storing last occurence of shared_ptr)
843 : // causing sipConn to be destroyed. So, do it while sipConns_ not locked.
844 99 : for (const auto& channel : channels)
845 4 : channel->sendBeacon();
846 :
847 : // Find listening devices for this account
848 285 : accountManager_->forEachDevice(
849 : peer_account,
850 190 : [this, devices = std::move(devices), sendRequest](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
851 : // Test if already sent via a SIP transport
852 95 : auto deviceId = dev->getLongId();
853 95 : if (devices.find(deviceId) != devices.end())
854 4 : return;
855 : {
856 91 : std::lock_guard lk(onConnectionClosedMtx_);
857 91 : onConnectionClosed_[deviceId] = sendRequest;
858 91 : }
859 91 : sendRequest(deviceId, false);
860 : },
861 190 : [wCall](bool ok) {
862 95 : if (not ok) {
863 1 : if (auto call = wCall.lock()) {
864 1 : JAMI_WARNING("[call:{}] No devices found", call->getCallId());
865 : // Note: if a P2P connection exists, the call will be at least in CONNECTING
866 1 : if (call->getConnectionState() == Call::ConnectionState::TRYING)
867 1 : call->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
868 1 : }
869 : }
870 95 : });
871 95 : }
872 :
873 : void
874 103 : JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call,
875 : const std::string& to_id,
876 : dhtnet::IpAddr target)
877 : {
878 103 : if (!call)
879 0 : return;
880 103 : JAMI_LOG("[call:{}] Outgoing call connected to {}", call->getCallId(), to_id);
881 :
882 103 : const auto localAddress = dhtnet::ip_utils::getInterfaceAddr(getLocalInterface(), target.getFamily());
883 :
884 103 : dhtnet::IpAddr addrSdp = getPublishedSameasLocal() ? localAddress
885 103 : : connectionManager_->getPublishedIpAddress(target.getFamily());
886 :
887 : // fallback on local address
888 103 : if (not addrSdp)
889 0 : addrSdp = localAddress;
890 :
891 : // Building the local SDP offer
892 103 : auto& sdp = call->getSDP();
893 :
894 103 : sdp.setPublishedIP(addrSdp);
895 :
896 103 : auto mediaAttrList = call->getMediaAttributeList();
897 103 : if (mediaAttrList.empty()) {
898 0 : JAMI_ERROR("[call:{}] No media. Abort!", call->getCallId());
899 0 : return;
900 : }
901 :
902 103 : if (not sdp.createOffer(mediaAttrList)) {
903 0 : JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call", call->getCallId());
904 0 : return;
905 : }
906 :
907 : // Note: pj_ice_strans_create can call onComplete in the same thread
908 : // This means that iceMutex_ in IceTransport can be locked when onInitDone is called
909 : // So, we need to run the call creation in the main thread
910 : // Also, we do not directly call SIPStartCall before receiving onInitDone, because
911 : // there is an inside waitForInitialization that can block the thread.
912 : // Note: avoid runMainThread as SIPStartCall use transportMutex
913 103 : dht::ThreadPool::io().run([w = weak(), call = std::move(call), target] {
914 103 : auto account = w.lock();
915 103 : if (not account)
916 0 : return;
917 :
918 103 : if (not account->SIPStartCall(*call, target)) {
919 0 : JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call", call->getCallId());
920 : }
921 103 : });
922 103 : }
923 :
924 : bool
925 103 : JamiAccount::SIPStartCall(SIPCall& call, const dhtnet::IpAddr& target)
926 : {
927 103 : JAMI_LOG("[call:{}] Start SIP call", call.getCallId());
928 :
929 103 : if (call.isIceEnabled())
930 103 : call.addLocalIceAttributes();
931 :
932 : std::string toUri(
933 103 : getToUri(call.getPeerNumber() + "@" + target.toString(true))); // expecting a fully well formed sip uri
934 :
935 103 : pj_str_t pjTo = sip_utils::CONST_PJ_STR(toUri);
936 :
937 : // Create the from header
938 103 : std::string from(getFromUri());
939 103 : pj_str_t pjFrom = sip_utils::CONST_PJ_STR(from);
940 :
941 103 : std::string targetStr = getToUri(target.toString(true));
942 103 : pj_str_t pjTarget = sip_utils::CONST_PJ_STR(targetStr);
943 :
944 103 : auto contact = call.getContactHeader();
945 103 : auto pjContact = sip_utils::CONST_PJ_STR(contact);
946 :
947 103 : JAMI_LOG("[call:{}] Contact header: {} / {} -> {} / {}", call.getCallId(), contact, from, toUri, targetStr);
948 :
949 103 : auto* local_sdp = call.getSDP().getLocalSdpSession();
950 103 : pjsip_dialog* dialog {nullptr};
951 103 : pjsip_inv_session* inv {nullptr};
952 103 : if (!CreateClientDialogAndInvite(&pjFrom, &pjContact, &pjTo, &pjTarget, local_sdp, &dialog, &inv))
953 0 : return false;
954 :
955 103 : inv->mod_data[link_.getModId()] = &call;
956 103 : call.setInviteSession(inv);
957 :
958 : pjsip_tx_data* tdata;
959 :
960 103 : if (pjsip_inv_invite(call.inviteSession_.get(), &tdata) != PJ_SUCCESS) {
961 0 : JAMI_ERROR("[call:{}] Unable to initialize invite", call.getCallId());
962 0 : return false;
963 : }
964 :
965 : pjsip_tpselector tp_sel;
966 103 : tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
967 103 : if (!call.getTransport()) {
968 0 : JAMI_ERROR("[call:{}] Unable to get transport", call.getCallId());
969 0 : return false;
970 : }
971 103 : tp_sel.u.transport = call.getTransport()->get();
972 103 : if (pjsip_dlg_set_transport(dialog, &tp_sel) != PJ_SUCCESS) {
973 0 : JAMI_ERROR("[call:{}] Unable to associate transport for invite session dialog", call.getCallId());
974 0 : return false;
975 : }
976 :
977 103 : JAMI_LOG("[call:{}] Sending SIP invite", call.getCallId());
978 :
979 : // Add user-agent header
980 103 : sip_utils::addUserAgentHeader(getUserAgentName(), tdata);
981 103 : sip_utils::addCallHandoverHeader(tdata);
982 :
983 103 : if (pjsip_inv_send_msg(call.inviteSession_.get(), tdata) != PJ_SUCCESS) {
984 0 : JAMI_ERROR("[call:{}] Unable to send invite message", call.getCallId());
985 0 : return false;
986 : }
987 :
988 103 : call.setState(Call::CallState::ACTIVE, Call::ConnectionState::PROGRESSING);
989 103 : return true;
990 103 : }
991 :
992 : void
993 3358 : JamiAccount::saveConfig() const
994 : {
995 : try {
996 3358 : auto accountConfig = config().path / "config.yml";
997 3358 : std::lock_guard lock(dhtnet::fileutils::getFileLock(accountConfig));
998 3358 : std::ofstream fout(accountConfig);
999 3358 : YAML::Emitter accountOut(fout);
1000 3358 : config().serialize(accountOut);
1001 3358 : JAMI_LOG("Saved account config to {}", accountConfig);
1002 3358 : } catch (const std::exception& e) {
1003 0 : JAMI_ERROR("Error saving account config: {}", e.what());
1004 0 : }
1005 3358 : }
1006 :
1007 : void
1008 1033 : JamiAccount::loadConfig()
1009 : {
1010 1033 : SIPAccountBase::loadConfig();
1011 1033 : registeredName_ = config().registeredName;
1012 1033 : if (accountManager_)
1013 22 : accountManager_->setAccountDeviceName(config().deviceName);
1014 1033 : if (connectionManager_) {
1015 18 : if (auto c = connectionManager_->getConfig()) {
1016 : // Update connectionManager's config
1017 18 : c->upnpEnabled = config().upnpEnabled;
1018 18 : c->turnEnabled = config().turnEnabled;
1019 18 : c->turnServer = config().turnServer;
1020 18 : c->turnServerUserName = config().turnServerUserName;
1021 18 : c->turnServerPwd = config().turnServerPwd;
1022 18 : c->turnServerRealm = config().turnServerRealm;
1023 18 : }
1024 : }
1025 1033 : if (config().proxyEnabled) {
1026 : try {
1027 0 : auto str = fileutils::loadCacheTextFile(cachePath_ / "dhtproxy", std::chrono::hours(24 * 14));
1028 0 : Json::Value root;
1029 0 : if (json::parse(str, root)) {
1030 0 : proxyServerCached_ = root[getProxyConfigKey()].asString();
1031 : }
1032 0 : } catch (const std::exception& e) {
1033 0 : JAMI_LOG("[Account {}] Unable to load proxy URL from cache: {}", getAccountID(), e.what());
1034 0 : proxyServerCached_.clear();
1035 0 : }
1036 : } else {
1037 1033 : proxyServerCached_.clear();
1038 1033 : std::error_code ec;
1039 1033 : std::filesystem::remove(cachePath_ / "dhtproxy", ec);
1040 : }
1041 1033 : if (not config().dhtProxyServerEnabled) {
1042 1033 : dhtProxyServer_.reset();
1043 : }
1044 1033 : auto credentials = consumeConfigCredentials();
1045 1033 : loadAccount(credentials.archive_password_scheme, credentials.archive_password, credentials.archive_path);
1046 1033 : }
1047 :
1048 : bool
1049 7 : JamiAccount::changeArchivePassword(const std::string& password_old, const std::string& password_new)
1050 : {
1051 : try {
1052 7 : if (!accountManager_->changePassword(password_old, password_new)) {
1053 2 : JAMI_ERROR("[Account {}] Unable to change archive password", getAccountID());
1054 2 : return false;
1055 : }
1056 10 : editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = not password_new.empty(); });
1057 0 : } catch (const std::exception& ex) {
1058 0 : JAMI_ERROR("[Account {}] Unable to change archive password: {}", getAccountID(), ex.what());
1059 0 : if (password_old.empty()) {
1060 0 : editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = true; });
1061 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
1062 : }
1063 0 : return false;
1064 0 : }
1065 5 : if (password_old != password_new)
1066 5 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
1067 5 : return true;
1068 : }
1069 :
1070 : bool
1071 3 : JamiAccount::isPasswordValid(const std::string& password)
1072 : {
1073 3 : return accountManager_ and accountManager_->isPasswordValid(password);
1074 : }
1075 :
1076 : std::vector<uint8_t>
1077 0 : JamiAccount::getPasswordKey(const std::string& password)
1078 : {
1079 0 : return accountManager_ ? accountManager_->getPasswordKey(password) : std::vector<uint8_t>();
1080 : }
1081 :
1082 : bool
1083 7 : JamiAccount::provideAccountAuthentication(const std::string& credentialsFromUser, const std::string& scheme)
1084 : {
1085 7 : if (auto manager = std::dynamic_pointer_cast<ArchiveAccountManager>(accountManager_)) {
1086 7 : return manager->provideAccountAuthentication(credentialsFromUser, scheme);
1087 7 : }
1088 0 : JAMI_ERROR("[LinkDevice] Invalid AccountManager instance while providing current account authentication.");
1089 0 : return false;
1090 : }
1091 :
1092 : int32_t
1093 6 : JamiAccount::addDevice(const std::string& uriProvided)
1094 : {
1095 6 : JAMI_LOG("[LinkDevice] JamiAccount::addDevice({})", getAccountID());
1096 6 : if (not accountManager_) {
1097 0 : JAMI_ERROR("[LinkDevice] Invalid AccountManager instance while adding a device.");
1098 0 : return static_cast<int32_t>(AccountManager::AddDeviceError::GENERIC);
1099 : }
1100 6 : auto authHandler = channelHandlers_.find(Uri::Scheme::AUTH);
1101 6 : if (authHandler == channelHandlers_.end())
1102 0 : return static_cast<int32_t>(AccountManager::AddDeviceError::GENERIC);
1103 12 : return accountManager_->addDevice(uriProvided,
1104 6 : config().archiveHasPassword ? fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD
1105 : : fileutils::ARCHIVE_AUTH_SCHEME_NONE,
1106 12 : (AuthChannelHandler*) authHandler->second.get());
1107 : }
1108 :
1109 : bool
1110 0 : JamiAccount::cancelAddDevice(uint32_t op_token)
1111 : {
1112 0 : if (!accountManager_)
1113 0 : return false;
1114 0 : return accountManager_->cancelAddDevice(op_token);
1115 : }
1116 :
1117 : bool
1118 4 : JamiAccount::confirmAddDevice(uint32_t op_token)
1119 : {
1120 4 : if (!accountManager_)
1121 0 : return false;
1122 4 : return accountManager_->confirmAddDevice(op_token);
1123 : }
1124 :
1125 : bool
1126 40 : JamiAccount::exportArchive(const std::string& destinationPath, std::string_view scheme, const std::string& password)
1127 : {
1128 40 : if (auto* manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
1129 40 : return manager->exportArchive(destinationPath, scheme, password);
1130 : }
1131 0 : return false;
1132 : }
1133 :
1134 : bool
1135 2 : JamiAccount::setValidity(std::string_view scheme, const std::string& pwd, const dht::InfoHash& id, int64_t validity)
1136 : {
1137 2 : if (auto* manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
1138 2 : if (manager->setValidity(scheme, pwd, id_, id, validity)) {
1139 2 : saveIdentity(id_, idPath_, DEVICE_ID_PATH);
1140 2 : return true;
1141 : }
1142 : }
1143 0 : return false;
1144 : }
1145 :
1146 : void
1147 4 : JamiAccount::forceReloadAccount()
1148 : {
1149 4 : editConfig([&](JamiAccountConfig& conf) {
1150 4 : conf.receipt.clear();
1151 4 : conf.receiptSignature.clear();
1152 4 : });
1153 4 : loadAccount();
1154 4 : }
1155 :
1156 : void
1157 3 : JamiAccount::unlinkConversations(const std::set<std::string>& removed)
1158 : {
1159 3 : std::lock_guard lock(configurationMutex_);
1160 3 : if (const auto* info = accountManager_->getInfo()) {
1161 3 : auto contacts = info->contacts->getContacts();
1162 6 : for (auto& [id, c] : contacts) {
1163 3 : if (removed.find(c.conversationId) != removed.end()) {
1164 2 : info->contacts->updateConversation(id, "");
1165 1 : JAMI_WARNING("[Account {}] Detected removed conversation ({}) in contact details for {}",
1166 : getAccountID(),
1167 : c.conversationId,
1168 : id.toString());
1169 : }
1170 : }
1171 3 : }
1172 3 : }
1173 :
1174 : bool
1175 1992 : JamiAccount::isValidAccountDevice(const dht::crypto::Certificate& cert) const
1176 : {
1177 1992 : if (accountManager_) {
1178 1992 : if (const auto* info = accountManager_->getInfo()) {
1179 1992 : if (info->contacts)
1180 1992 : return info->contacts->isValidAccountDevice(cert).isValid();
1181 : }
1182 : }
1183 0 : return false;
1184 : }
1185 :
1186 : bool
1187 3 : JamiAccount::revokeDevice(const std::string& device, std::string_view scheme, const std::string& password)
1188 : {
1189 3 : if (not accountManager_)
1190 0 : return false;
1191 3 : return accountManager_
1192 6 : ->revokeDevice(device, scheme, password, [this, device](AccountManager::RevokeDeviceResult result) {
1193 3 : emitSignal<libjami::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(),
1194 3 : device,
1195 : static_cast<int>(result));
1196 6 : });
1197 : return true;
1198 : }
1199 :
1200 : std::pair<std::string, std::string>
1201 1015 : JamiAccount::saveIdentity(const dht::crypto::Identity& id, const std::filesystem::path& path, const std::string& name)
1202 : {
1203 1015 : auto names = std::make_pair(name + ".key", name + ".crt");
1204 1015 : if (id.first)
1205 1015 : fileutils::saveFile(path / names.first, id.first->serialize(), 0600);
1206 1015 : if (id.second)
1207 1015 : fileutils::saveFile(path / names.second, id.second->getPacked(), 0600);
1208 1015 : return names;
1209 0 : }
1210 :
1211 : void
1212 1013 : JamiAccount::scheduleAccountReady() const
1213 : {
1214 1013 : const auto accountId = getAccountID();
1215 2026 : runOnMainThread([accountId] { Manager::instance().markAccountReady(accountId); });
1216 1013 : }
1217 :
1218 : AccountManager::OnChangeCallback
1219 1033 : JamiAccount::setupAccountCallbacks()
1220 : {
1221 1033 : return AccountManager::OnChangeCallback {[this](const std::string& uri, bool confirmed) {
1222 771 : onContactAdded(uri, confirmed);
1223 771 : },
1224 1033 : [this](const std::string& uri, bool banned) {
1225 22 : onContactRemoved(uri, banned);
1226 22 : },
1227 1033 : [this](const std::string& uri,
1228 : const std::string& conversationId,
1229 : const std::vector<uint8_t>& payload,
1230 : TimePoint received,
1231 : TimePoint invited) {
1232 447 : onIncomingTrustRequest(uri, conversationId, payload, received, invited);
1233 447 : },
1234 1033 : [this](const std::map<DeviceId, KnownDevice>& devices) {
1235 3400 : onKnownDevicesChanged(devices);
1236 3400 : },
1237 1033 : [this](const std::string& conversationId, const std::string& deviceId) {
1238 396 : onConversationRequestAccepted(conversationId, deviceId);
1239 396 : },
1240 2066 : [this](const std::string& uri, const std::string& convFromReq) {
1241 411 : onContactConfirmed(uri, convFromReq);
1242 1033 : }};
1243 : }
1244 :
1245 : void
1246 771 : JamiAccount::onContactAdded(const std::string& uri, bool confirmed)
1247 : {
1248 771 : if (!id_.first)
1249 15 : return;
1250 756 : if (jami::Manager::instance().syncOnRegister) {
1251 756 : dht::ThreadPool::io().run([w = weak(), uri, confirmed] {
1252 756 : if (auto shared = w.lock()) {
1253 756 : if (auto* cm = shared->convModule(true)) {
1254 756 : auto activeConv = cm->getOneToOneConversation(uri);
1255 756 : if (!activeConv.empty())
1256 756 : cm->bootstrap(activeConv);
1257 756 : }
1258 : // Propagate the new contact to our other devices.
1259 756 : shared->onSyncListChanged();
1260 756 : emitSignal<libjami::ConfigurationSignal::ContactAdded>(shared->getAccountID(), uri, confirmed);
1261 756 : }
1262 756 : });
1263 : }
1264 : }
1265 :
1266 : void
1267 22 : JamiAccount::onContactRemoved(const std::string& uri, bool banned)
1268 : {
1269 22 : if (!id_.first)
1270 0 : return;
1271 22 : dht::ThreadPool::io().run([w = weak(), uri, banned] {
1272 22 : if (auto shared = w.lock()) {
1273 : // Erase linked conversation's requests
1274 22 : if (auto* convModule = shared->convModule(true))
1275 22 : convModule->removeContact(uri, banned);
1276 : // Remove current connections with contact
1277 : // Note: if contact is ourself, we don't close the connection
1278 : // because it's used for syncing other conversations.
1279 22 : if (shared->connectionManager_ && uri != shared->getUsername()) {
1280 : // Hang up first: calls losing their SIP channel would
1281 : // otherwise try to recover as after a network change.
1282 19 : shared->hangupCallsWith(uri);
1283 19 : shared->connectionManager_->closeConnectionsWith(uri);
1284 : }
1285 : // Propagate the removal to our other devices.
1286 22 : shared->onSyncListChanged();
1287 : // Update client.
1288 22 : emitSignal<libjami::ConfigurationSignal::ContactRemoved>(shared->getAccountID(), uri, banned);
1289 22 : }
1290 22 : });
1291 : }
1292 :
1293 : void
1294 19 : JamiAccount::hangupCallsWith(const std::string& uri)
1295 : {
1296 19 : for (const auto& callId : getCallList()) {
1297 0 : auto call = std::dynamic_pointer_cast<SIPCall>(getCall(callId));
1298 0 : if (not call)
1299 0 : continue;
1300 : // Calls without an authenticated SIP channel yet only have the URI
1301 : // they were placed to or received from, which may carry a scheme.
1302 0 : auto peer = call->getPeerAccountId();
1303 0 : if (peer.empty())
1304 0 : peer = sip_utils::stripSipUriPrefix(Uri(call->getPeerNumber()).authority());
1305 0 : if (peer == uri)
1306 0 : Manager::instance().hangupCall(getAccountID(), callId);
1307 19 : }
1308 19 : }
1309 :
1310 : void
1311 447 : JamiAccount::onIncomingTrustRequest(const std::string& uri,
1312 : const std::string& conversationId,
1313 : const std::vector<uint8_t>& payload,
1314 : TimePoint received,
1315 : TimePoint invited)
1316 : {
1317 447 : if (!id_.first)
1318 0 : return;
1319 447 : dht::ThreadPool::io().run([w = weak(), uri, conversationId, payload, received, invited] {
1320 447 : if (auto shared = w.lock()) {
1321 447 : shared->clearProfileCache(uri);
1322 447 : if (conversationId.empty()) {
1323 : // Old path
1324 0 : emitSignal<libjami::ConfigurationSignal::IncomingTrustRequest>(shared->getAccountID(),
1325 0 : conversationId,
1326 0 : uri,
1327 0 : payload,
1328 0 : toSecondsSinceEpoch(received));
1329 0 : return;
1330 : }
1331 : // Here account can be initializing
1332 447 : if (auto* cm = shared->convModule(true)) {
1333 447 : auto activeConv = cm->getOneToOneConversation(uri);
1334 447 : if (activeConv != conversationId)
1335 296 : cm->onTrustRequest(uri, conversationId, payload, received, invited);
1336 447 : }
1337 447 : }
1338 : });
1339 : }
1340 :
1341 : void
1342 3400 : JamiAccount::onKnownDevicesChanged(const std::map<DeviceId, KnownDevice>& devices)
1343 : {
1344 3400 : std::map<std::string, std::string> ids;
1345 1016439 : for (auto& d : devices) {
1346 1013039 : auto id = d.first.toString();
1347 1013039 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
1348 1013039 : ids.emplace(std::move(id), std::move(label));
1349 1013039 : }
1350 3400 : runOnMainThread([id = getAccountID(), devices = std::move(ids)] {
1351 3400 : emitSignal<libjami::ConfigurationSignal::KnownDevicesChanged>(id, devices);
1352 3400 : });
1353 3400 : }
1354 :
1355 : void
1356 396 : JamiAccount::onConversationRequestAccepted(const std::string& conversationId, const std::string& deviceId)
1357 : {
1358 : // Note: Do not retrigger on another thread. This has to be done
1359 : // at the same time of acceptTrustRequest a synced state between TrustRequest
1360 : // and convRequests.
1361 396 : if (auto* cm = convModule(true))
1362 396 : cm->acceptConversationRequest(conversationId, deviceId);
1363 396 : }
1364 :
1365 : void
1366 411 : JamiAccount::onContactConfirmed(const std::string& uri, const std::string& convFromReq)
1367 : {
1368 411 : dht::ThreadPool::io().run([w = weak(), convFromReq, uri] {
1369 411 : if (auto shared = w.lock()) {
1370 411 : shared->convModule(true);
1371 : // Remove cached payload if there is one
1372 411 : auto requestPath = shared->cachePath_ / "requests" / uri;
1373 411 : dhtnet::fileutils::remove(requestPath);
1374 411 : dhtnet::fileutils::remove(shared->cachePath_ / "requests" / (uri + ".ts"));
1375 822 : }
1376 411 : });
1377 411 : }
1378 :
1379 : std::unique_ptr<AccountManager::AccountCredentials>
1380 1015 : JamiAccount::buildAccountCredentials(const JamiAccountConfig& conf,
1381 : const dht::crypto::Identity& id,
1382 : const std::string& archive_password_scheme,
1383 : const std::string& archive_password,
1384 : const std::string& archive_path,
1385 : bool& migrating,
1386 : bool& hasPassword)
1387 : {
1388 1015 : std::unique_ptr<AccountManager::AccountCredentials> creds;
1389 :
1390 1015 : if (conf.managerUri.empty()) {
1391 1015 : auto acreds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
1392 1015 : auto archivePath = fileutils::getFullPath(idPath_, conf.archivePath);
1393 :
1394 1015 : if (!archive_path.empty()) {
1395 41 : acreds->scheme = "file";
1396 41 : acreds->uri = archive_path;
1397 974 : } else if (!conf.archive_url.empty() && conf.archive_url == "jami-auth") {
1398 5 : JAMI_DEBUG("[Account {}] [LinkDevice] scheme p2p & uri {}", getAccountID(), conf.archive_url);
1399 5 : acreds->scheme = "p2p";
1400 5 : acreds->uri = conf.archive_url;
1401 969 : } else if (std::filesystem::is_regular_file(archivePath)) {
1402 4 : acreds->scheme = "local";
1403 4 : acreds->uri = archivePath.string();
1404 4 : acreds->updateIdentity = id;
1405 4 : migrating = true;
1406 : }
1407 :
1408 1015 : creds = std::move(acreds);
1409 1015 : } else {
1410 0 : auto screds = std::make_unique<ServerAccountManager::ServerAccountCredentials>();
1411 0 : screds->username = conf.managerUsername;
1412 0 : screds->identity = id;
1413 0 : creds = std::move(screds);
1414 0 : }
1415 :
1416 1015 : creds->password = archive_password;
1417 1015 : hasPassword = !archive_password.empty();
1418 2020 : creds->password_scheme = (hasPassword && archive_password_scheme.empty()) ? fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD
1419 2020 : : archive_password_scheme;
1420 :
1421 1015 : return creds;
1422 0 : }
1423 :
1424 : void
1425 1013 : JamiAccount::onAuthenticationSuccess(bool migrating,
1426 : bool hasPassword,
1427 : const AccountInfo& info,
1428 : const std::map<std::string, std::string>& configMap,
1429 : std::string&& receipt,
1430 : std::vector<uint8_t>&& receiptSignature)
1431 : {
1432 1013 : JAMI_LOG("[Account {}] Auth success! Device: {}", getAccountID(), info.deviceId);
1433 :
1434 1013 : dhtnet::fileutils::check_dir(idPath_, 0700);
1435 :
1436 1013 : auto id = info.identity;
1437 2026 : editConfig([&](JamiAccountConfig& conf) {
1438 1013 : std::tie(conf.tlsPrivateKeyFile, conf.tlsCertificateFile) = saveIdentity(id, idPath_, DEVICE_ID_PATH);
1439 1013 : conf.tlsPassword = {};
1440 :
1441 2026 : auto passwordIt = configMap.find(libjami::Account::ConfProperties::ARCHIVE_HAS_PASSWORD);
1442 2026 : conf.archiveHasPassword = (passwordIt != configMap.end() && !passwordIt->second.empty())
1443 2026 : ? passwordIt->second == "true"
1444 0 : : hasPassword;
1445 :
1446 1013 : if (not conf.managerUri.empty()) {
1447 0 : conf.registeredName = conf.managerUsername;
1448 0 : registeredName_ = conf.managerUsername;
1449 : }
1450 :
1451 1013 : conf.username = info.accountId;
1452 1013 : conf.deviceName = accountManager_->getAccountDeviceName();
1453 :
1454 2026 : auto nameServerIt = configMap.find(libjami::Account::ConfProperties::Nameserver::URI);
1455 1013 : if (nameServerIt != configMap.end() && !nameServerIt->second.empty())
1456 0 : conf.nameServer = nameServerIt->second;
1457 :
1458 2026 : auto displayNameIt = configMap.find(libjami::Account::ConfProperties::DISPLAYNAME);
1459 1013 : if (displayNameIt != configMap.end() && !displayNameIt->second.empty())
1460 44 : conf.displayName = displayNameIt->second;
1461 :
1462 1013 : conf.receipt = std::move(receipt);
1463 1013 : conf.receiptSignature = std::move(receiptSignature);
1464 1013 : conf.fromMap(configMap);
1465 1013 : });
1466 :
1467 1013 : id_ = std::move(id);
1468 : {
1469 1013 : std::lock_guard lk(moduleMtx_);
1470 1013 : convModule_.reset();
1471 1013 : }
1472 :
1473 1013 : if (migrating)
1474 4 : Migration::setState(getAccountID(), Migration::State::SUCCESS);
1475 :
1476 1013 : setRegistrationState(RegistrationState::UNREGISTERED);
1477 :
1478 1013 : if (!info.photo.empty() || !info.displayName.empty()) {
1479 : try {
1480 0 : auto newProfile = vCard::utils::initVcard();
1481 0 : newProfile[std::string(vCard::Property::FORMATTED_NAME)] = info.displayName;
1482 0 : newProfile[std::string(vCard::Property::PHOTO)] = info.photo;
1483 :
1484 0 : const auto& profiles = idPath_ / "profiles";
1485 0 : const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(info.accountId));
1486 0 : vCard::utils::save(newProfile, vCardPath, profilePath());
1487 :
1488 0 : runOnMainThread([w = weak(), id = info.accountId, vCardPath] {
1489 0 : if (auto shared = w.lock()) {
1490 0 : emitSignal<libjami::ConfigurationSignal::ProfileReceived>(shared->getAccountID(),
1491 0 : id,
1492 0 : vCardPath.string());
1493 0 : }
1494 0 : });
1495 0 : } catch (const std::exception& e) {
1496 0 : JAMI_WARNING("[Account {}] Unable to save profile after authentication: {}", getAccountID(), e.what());
1497 0 : }
1498 : }
1499 :
1500 1013 : updateTrustedCa();
1501 1013 : doRegister();
1502 1013 : scheduleAccountReady();
1503 1013 : }
1504 :
1505 : void
1506 0 : JamiAccount::onAuthenticationError(const std::weak_ptr<JamiAccount>& w,
1507 : bool hadIdentity,
1508 : bool migrating,
1509 : std::string accountId,
1510 : AccountManager::AuthError error,
1511 : const std::string& message)
1512 : {
1513 0 : JAMI_WARNING("[Account {}] Auth error: {} {}", accountId, (int) error, message);
1514 :
1515 0 : if ((hadIdentity || migrating) && error == AccountManager::AuthError::INVALID_ARGUMENTS) {
1516 0 : Migration::setState(accountId, Migration::State::INVALID);
1517 0 : if (auto acc = w.lock())
1518 0 : acc->setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
1519 0 : return;
1520 : }
1521 :
1522 0 : if (auto acc = w.lock())
1523 0 : acc->setRegistrationState(RegistrationState::ERROR_GENERIC);
1524 :
1525 0 : runOnMainThread([accountId = std::move(accountId)] { Manager::instance().removeAccount(accountId, true); });
1526 : }
1527 :
1528 : // must be called while configurationMutex_ is locked
1529 : void
1530 1037 : JamiAccount::loadAccount(const std::string& archive_password_scheme,
1531 : const std::string& archive_password,
1532 : const std::string& archive_path)
1533 : {
1534 1037 : if (registrationState_ == RegistrationState::INITIALIZING)
1535 22 : return;
1536 :
1537 1033 : JAMI_DEBUG("[Account {:s}] Loading account", getAccountID());
1538 :
1539 1033 : const auto scheduleAccountReady = [accountId = getAccountID()] {
1540 18 : runOnMainThread([accountId] {
1541 18 : auto& manager = Manager::instance();
1542 18 : manager.markAccountReady(accountId);
1543 18 : });
1544 1051 : };
1545 :
1546 1033 : const auto& conf = config();
1547 1033 : auto callbacks = setupAccountCallbacks();
1548 :
1549 : try {
1550 1033 : auto oldIdentity = id_.first ? id_.first->getPublicKey().getLongId() : DeviceId();
1551 :
1552 1033 : if (conf.managerUri.empty()) {
1553 2066 : accountManager_ = std::make_shared<ArchiveAccountManager>(
1554 1033 : getAccountID(),
1555 : getPath(),
1556 46 : [this]() { return getAccountDetails(); },
1557 1033 : [this](DeviceSync&& syncData) {
1558 1436 : if (auto* sm = syncModule()) {
1559 1436 : auto syncDataPtr = std::make_shared<SyncMsg>();
1560 1436 : syncDataPtr->ds = std::move(syncData);
1561 1436 : sm->syncWithConnected(syncDataPtr);
1562 1436 : }
1563 1436 : },
1564 2066 : conf.archivePath.empty() ? "archive.gz" : conf.archivePath,
1565 2066 : conf.nameServer);
1566 : } else {
1567 0 : accountManager_ = std::make_shared<ServerAccountManager>(getAccountID(),
1568 : getPath(),
1569 0 : conf.managerUri,
1570 0 : conf.nameServer);
1571 : }
1572 :
1573 1033 : auto id = accountManager_->loadIdentity(conf.tlsCertificateFile, conf.tlsPrivateKeyFile, conf.tlsPassword);
1574 :
1575 1033 : if (const auto* info
1576 1033 : = accountManager_->useIdentity(id, conf.receipt, conf.receiptSignature, conf.managerUsername, callbacks)) {
1577 18 : id_ = std::move(id);
1578 18 : config_->username = info->accountId;
1579 18 : JAMI_WARNING("[Account {:s}] Loaded account identity", getAccountID());
1580 :
1581 18 : if (info->identity.first->getPublicKey().getLongId() != oldIdentity) {
1582 0 : JAMI_WARNING("[Account {:s}] Identity changed", getAccountID());
1583 : {
1584 0 : std::lock_guard lk(moduleMtx_);
1585 0 : convModule_.reset();
1586 0 : }
1587 0 : convModule();
1588 : } else {
1589 18 : convModule()->setAccountManager(accountManager_);
1590 : }
1591 :
1592 18 : convModule()->initPresence();
1593 18 : if (not isEnabled())
1594 0 : setRegistrationState(RegistrationState::UNREGISTERED);
1595 :
1596 18 : updateTrustedCa();
1597 18 : scheduleAccountReady();
1598 18 : return;
1599 : }
1600 :
1601 1015 : if (!isEnabled())
1602 0 : return;
1603 :
1604 1015 : JAMI_WARNING("[Account {}] useIdentity failed!", getAccountID());
1605 :
1606 1015 : if (not conf.managerUri.empty() && archive_password.empty()) {
1607 0 : Migration::setState(accountID_, Migration::State::INVALID);
1608 0 : setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
1609 0 : return;
1610 : }
1611 :
1612 1015 : bool migrating = registrationState_ == RegistrationState::ERROR_NEED_MIGRATION;
1613 1015 : setRegistrationState(RegistrationState::INITIALIZING);
1614 :
1615 1015 : bool hasPassword = false;
1616 : auto creds = buildAccountCredentials(conf,
1617 : id,
1618 : archive_password_scheme,
1619 : archive_password,
1620 : archive_path,
1621 : migrating,
1622 1015 : hasPassword);
1623 :
1624 1015 : JAMI_WARNING("[Account {}] initAuthentication {}", getAccountID(), fmt::ptr(this));
1625 :
1626 1015 : const bool hadIdentity = static_cast<bool>(id.first);
1627 4060 : accountManager_->initAuthentication(
1628 2030 : ip_utils::getDeviceName(),
1629 1015 : std::move(creds),
1630 2030 : [w = weak(), migrating, hasPassword](const AccountInfo& info,
1631 : const std::map<std::string, std::string>& configMap,
1632 : std::string&& receipt,
1633 : std::vector<uint8_t>&& receiptSignature) {
1634 1013 : if (auto self = w.lock())
1635 2026 : self->onAuthenticationSuccess(migrating,
1636 : hasPassword,
1637 : info,
1638 : configMap,
1639 1013 : std::move(receipt),
1640 2026 : std::move(receiptSignature));
1641 1013 : },
1642 2030 : [w = weak(), hadIdentity, accountId = getAccountID(), migrating](AccountManager::AuthError error,
1643 : const std::string& message) {
1644 0 : JamiAccount::onAuthenticationError(w, hadIdentity, migrating, accountId, error, message);
1645 0 : },
1646 : callbacks);
1647 1033 : } catch (const std::exception& e) {
1648 0 : JAMI_WARNING("[Account {}] Error loading account: {}", getAccountID(), e.what());
1649 0 : accountManager_.reset();
1650 0 : setRegistrationState(RegistrationState::ERROR_GENERIC);
1651 0 : }
1652 1051 : }
1653 :
1654 : std::map<std::string, std::string>
1655 6423 : JamiAccount::getVolatileAccountDetails() const
1656 : {
1657 6423 : auto a = SIPAccountBase::getVolatileAccountDetails();
1658 6423 : a.emplace(libjami::Account::VolatileProperties::InstantMessaging::OFF_CALL, TRUE_STR);
1659 6423 : auto registeredName = getRegisteredName();
1660 6423 : if (not registeredName.empty())
1661 3 : a.emplace(libjami::Account::VolatileProperties::REGISTERED_NAME, registeredName);
1662 6423 : a.emplace(libjami::Account::ConfProperties::PROXY_SERVER, proxyServerCached_);
1663 6423 : a.emplace(libjami::Account::VolatileProperties::DHT_BOUND_PORT, std::to_string(dhtBoundPort_));
1664 6423 : a.emplace(libjami::Account::VolatileProperties::DEVICE_ANNOUNCED, deviceAnnounced_ ? TRUE_STR : FALSE_STR);
1665 6423 : if (accountManager_) {
1666 6423 : if (const auto* info = accountManager_->getInfo()) {
1667 5375 : a.emplace(libjami::Account::ConfProperties::DEVICE_ID, info->deviceId);
1668 : }
1669 : }
1670 12846 : return a;
1671 6423 : }
1672 :
1673 : bool
1674 3 : JamiAccount::lookupName(const std::string& name)
1675 : {
1676 3 : std::lock_guard lock(configurationMutex_);
1677 3 : auto acc = getAccountID();
1678 3 : if (!accountManager_) {
1679 0 : JAMI_WARNING("[Account {}] Unable to look up {}: account is not loaded", acc, name);
1680 0 : return false;
1681 : }
1682 6 : accountManager_->lookupUri(name,
1683 3 : config().nameServer,
1684 6 : [acc, name](const std::string& regName,
1685 : const std::string& address,
1686 : NameDirectory::Response response) {
1687 6 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
1688 3 : name,
1689 : (int) response,
1690 : address,
1691 : regName);
1692 3 : });
1693 3 : return true;
1694 3 : }
1695 :
1696 : bool
1697 3 : JamiAccount::lookupAddress(const std::string& addr)
1698 : {
1699 3 : std::lock_guard lock(configurationMutex_);
1700 3 : auto acc = getAccountID();
1701 3 : if (!accountManager_) {
1702 0 : JAMI_WARNING("[Account {}] Unable to look up {}: account is not loaded", acc, addr);
1703 0 : return false;
1704 : }
1705 6 : accountManager_->lookupAddress(
1706 6 : addr, [acc, addr](const std::string& regName, const std::string& address, NameDirectory::Response response) {
1707 3 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc, addr, (int) response, address, regName);
1708 3 : });
1709 3 : return true;
1710 3 : }
1711 :
1712 : void
1713 1 : JamiAccount::registerName(const std::string& name, const std::string& scheme, const std::string& password)
1714 : {
1715 1 : std::lock_guard lock(configurationMutex_);
1716 1 : if (accountManager_)
1717 1 : accountManager_
1718 2 : ->registerName(name,
1719 : scheme,
1720 : password,
1721 2 : [acc = getAccountID(), name, w = weak()](NameDirectory::RegistrationResponse response,
1722 : const std::string& regName) {
1723 1 : auto res = (int) std::min(response, NameDirectory::RegistrationResponse::error);
1724 1 : if (response == NameDirectory::RegistrationResponse::success) {
1725 1 : if (auto this_ = w.lock()) {
1726 1 : if (this_->setRegisteredName(regName)) {
1727 2 : this_->editConfig(
1728 2 : [&](JamiAccountConfig& config) { config.registeredName = regName; });
1729 1 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
1730 2 : this_->accountID_, this_->getVolatileAccountDetails());
1731 : }
1732 1 : }
1733 : }
1734 1 : emitSignal<libjami::ConfigurationSignal::NameRegistrationEnded>(acc, res, name);
1735 1 : });
1736 1 : }
1737 :
1738 : bool
1739 0 : JamiAccount::searchUser(const std::string& query)
1740 : {
1741 0 : if (accountManager_)
1742 0 : return accountManager_
1743 0 : ->searchUser(query,
1744 0 : [acc = getAccountID(), query](const jami::NameDirectory::SearchResult& result,
1745 : jami::NameDirectory::Response response) {
1746 0 : jami::emitSignal<libjami::ConfigurationSignal::UserSearchEnded>(acc,
1747 : (int) response,
1748 0 : query,
1749 : result);
1750 0 : });
1751 0 : return false;
1752 : }
1753 :
1754 : void
1755 216 : JamiAccount::forEachPendingCall(const DeviceId& deviceId, const std::function<void(const std::shared_ptr<SIPCall>&)>& cb)
1756 : {
1757 216 : std::vector<std::shared_ptr<SIPCall>> pc;
1758 : {
1759 216 : std::lock_guard lk(pendingCallsMutex_);
1760 216 : pc = std::move(pendingCalls_[deviceId]);
1761 216 : }
1762 314 : for (const auto& pendingCall : pc) {
1763 98 : cb(pendingCall);
1764 : }
1765 216 : }
1766 :
1767 : void
1768 916 : JamiAccount::registerAsyncOps()
1769 : {
1770 916 : loadCachedProxyServer([w = weak()](const std::string&) {
1771 916 : runOnMainThread([w] {
1772 916 : if (auto s = w.lock()) {
1773 916 : std::lock_guard lock(s->configurationMutex_);
1774 916 : s->doRegister_();
1775 1832 : }
1776 916 : });
1777 916 : });
1778 916 : }
1779 :
1780 : void
1781 2106 : JamiAccount::doRegister()
1782 : {
1783 2106 : std::lock_guard lock(configurationMutex_);
1784 2106 : if (not isUsable()) {
1785 179 : JAMI_WARNING("[Account {:s}] Account must be enabled and active to register, ignoring", getAccountID());
1786 179 : return;
1787 : }
1788 :
1789 1927 : JAMI_LOG("[Account {:s}] Starting account…", getAccountID());
1790 :
1791 : // invalid state transitions:
1792 : // INITIALIZING: generating/loading certificates, unable to register
1793 : // NEED_MIGRATION: old account detected, user needs to migrate
1794 1927 : if (registrationState_ == RegistrationState::INITIALIZING
1795 916 : || registrationState_ == RegistrationState::ERROR_NEED_MIGRATION)
1796 1011 : return;
1797 :
1798 916 : convModule(); // Init conv module before passing in trying
1799 916 : setRegistrationState(RegistrationState::TRYING);
1800 916 : if (proxyServerCached_.empty()) {
1801 916 : registerAsyncOps();
1802 : } else {
1803 0 : doRegister_();
1804 : }
1805 2106 : }
1806 :
1807 : std::vector<std::string>
1808 916 : JamiAccount::loadBootstrap() const
1809 : {
1810 916 : std::vector<std::string> bootstrap;
1811 916 : std::string_view stream(config().hostname), node_addr;
1812 1832 : while (jami::getline(stream, node_addr, ';'))
1813 916 : bootstrap.emplace_back(node_addr);
1814 1832 : for (const auto& b : bootstrap)
1815 916 : JAMI_LOG("[Account {}] Bootstrap node: {}", getAccountID(), b);
1816 1832 : return bootstrap;
1817 0 : }
1818 :
1819 : void
1820 34 : JamiAccount::trackBuddyPresence(const std::string& buddy_id, bool track)
1821 : {
1822 34 : std::string buddyUri;
1823 : try {
1824 34 : buddyUri = parseJamiUri(buddy_id);
1825 0 : } catch (...) {
1826 0 : JAMI_ERROR("[Account {:s}] Failed to track presence: invalid URI {:s}", getAccountID(), buddy_id);
1827 0 : return;
1828 0 : }
1829 34 : JAMI_LOG("[Account {:s}] {:s} presence for {:s}", getAccountID(), track ? "Track" : "Untrack", buddy_id);
1830 :
1831 34 : if (!presenceManager_)
1832 0 : return;
1833 :
1834 34 : if (track) {
1835 34 : presenceManager_->trackBuddy(buddyUri);
1836 34 : std::lock_guard lock(presenceStateMtx_);
1837 34 : auto it = presenceState_.find(buddyUri);
1838 34 : if (it != presenceState_.end() && it->second != PresenceState::DISCONNECTED) {
1839 3 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1840 : buddyUri,
1841 3 : static_cast<int>(it->second),
1842 : "");
1843 : }
1844 34 : } else {
1845 0 : presenceManager_->untrackBuddy(buddyUri);
1846 : }
1847 34 : }
1848 :
1849 : std::map<std::string, bool>
1850 2 : JamiAccount::getTrackedBuddyPresence() const
1851 : {
1852 2 : if (!presenceManager_)
1853 0 : return {};
1854 2 : return presenceManager_->getTrackedBuddyPresence();
1855 : }
1856 :
1857 : void
1858 1396 : JamiAccount::onTrackedBuddyOnline(const std::string& contactId)
1859 : {
1860 1396 : JAMI_DEBUG("[Account {:s}] Buddy {} online", getAccountID(), contactId);
1861 1396 : std::lock_guard lock(presenceStateMtx_);
1862 1396 : auto& state = presenceState_[contactId];
1863 1396 : if (state < PresenceState::AVAILABLE) {
1864 766 : state = PresenceState::AVAILABLE;
1865 766 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1866 : contactId,
1867 : static_cast<int>(PresenceState::AVAILABLE),
1868 : "");
1869 : }
1870 :
1871 1396 : if (auto details = getContactInfo(contactId)) {
1872 586 : if (!details->confirmed) {
1873 168 : auto convId = convModule()->getOneToOneConversation(contactId);
1874 168 : if (convId.empty())
1875 2 : return;
1876 : // In this case, the TrustRequest was sent but never confirmed (cause the contact was
1877 : // offline maybe) To avoid the contact to never receive the conv request, retry there
1878 166 : std::lock_guard lock(configurationMutex_);
1879 166 : if (accountManager_) {
1880 : // Retrieve cached payload for trust request.
1881 166 : auto requestPath = cachePath_ / "requests" / contactId;
1882 166 : std::vector<uint8_t> payload;
1883 : try {
1884 176 : payload = fileutils::loadFile(requestPath);
1885 10 : } catch (...) {
1886 10 : }
1887 166 : if (payload.size() >= 64000) {
1888 1 : JAMI_WARNING("[Account {:s}] Trust request for contact {:s} is too big, reset payload",
1889 : getAccountID(),
1890 : contactId);
1891 1 : payload.clear();
1892 : }
1893 : // Reuse the original invite timestamp so this passive retry isn't treated as a
1894 : // fresh invitation; fall back to now() if no cached timestamp is found (e.g.
1895 : // cache predates this mechanism, or the sidecar failed to write previously).
1896 166 : auto tsPath = cachePath_ / "requests" / (contactId + ".ts");
1897 166 : TimePoint invited {};
1898 : try {
1899 176 : auto tsData = fileutils::loadFile(tsPath);
1900 158 : auto invitedMs = std::stoll(std::string(tsData.begin(), tsData.end()));
1901 154 : if (invitedMs > 0)
1902 154 : invited = timePointFromMilliseconds(invitedMs);
1903 168 : } catch (const std::exception& e) {
1904 12 : JAMI_WARNING("[Account {:s}] Unable to load invite timestamp for {:s}: {}",
1905 : getAccountID(),
1906 : contactId,
1907 : e.what());
1908 12 : }
1909 166 : if (invited == TimePoint {}) {
1910 12 : invited = nowMs();
1911 : // Persist the fallback so subsequent retries stay stable instead of minting
1912 : // a new timestamp (and thus looking like a fresh re-invitation) every time.
1913 12 : std::ofstream tsFile(tsPath, std::ios::trunc | std::ios::binary);
1914 12 : if (tsFile.is_open())
1915 2 : tsFile << toMillisecondsSinceEpoch(invited);
1916 : else
1917 10 : JAMI_WARNING("[Account {:s}] Unable to write invite timestamp for {:s}",
1918 : getAccountID(),
1919 : contactId);
1920 12 : }
1921 166 : accountManager_->sendTrustRequest(contactId, convId, payload, invited);
1922 166 : }
1923 168 : }
1924 1396 : }
1925 1396 : }
1926 :
1927 : void
1928 128 : JamiAccount::onTrackedBuddyOffline(const std::string& contactId)
1929 : {
1930 128 : JAMI_DEBUG("[Account {:s}] Buddy {} offline", getAccountID(), contactId);
1931 128 : std::lock_guard lock(presenceStateMtx_);
1932 128 : auto& state = presenceState_[contactId];
1933 128 : if (state > PresenceState::DISCONNECTED) {
1934 128 : if (state == PresenceState::CONNECTED) {
1935 0 : JAMI_WARNING("[Account {:s}] Buddy {} is not present on the DHT, but P2P connected",
1936 : getAccountID(),
1937 : contactId);
1938 0 : return;
1939 : }
1940 128 : state = PresenceState::DISCONNECTED;
1941 128 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1942 : contactId,
1943 : static_cast<int>(PresenceState::DISCONNECTED),
1944 : "");
1945 : }
1946 128 : }
1947 :
1948 : void
1949 916 : JamiAccount::doRegister_()
1950 : {
1951 916 : if (registrationState_ != RegistrationState::TRYING) {
1952 0 : JAMI_ERROR("[Account {}] Already registered", getAccountID());
1953 0 : return;
1954 : }
1955 :
1956 916 : JAMI_DEBUG("[Account {}] Starting account…", getAccountID());
1957 916 : const auto& conf = config();
1958 :
1959 : try {
1960 916 : if (not accountManager_ or not accountManager_->getInfo())
1961 0 : throw std::runtime_error("No identity configured for this account.");
1962 :
1963 916 : if (dht_->isRunning()) {
1964 2 : JAMI_ERROR("[Account {}] DHT already running (stopping it first).", getAccountID());
1965 2 : dht_->join();
1966 : }
1967 :
1968 916 : convModule()->clearPendingFetch();
1969 :
1970 : // Look for registered name
1971 1832 : accountManager_->lookupAddress(accountManager_->getInfo()->accountId,
1972 1832 : [w = weak()](const std::string& regName,
1973 : const std::string& /*address*/,
1974 : const NameDirectory::Response& response) {
1975 916 : if (auto this_ = w.lock())
1976 916 : this_->lookupRegisteredName(regName, response);
1977 916 : });
1978 :
1979 916 : dht::DhtRunner::Config config = initDhtConfig(conf);
1980 :
1981 : // check if dht peer service is enabled
1982 916 : if (conf.accountPeerDiscovery or conf.accountPublish) {
1983 0 : peerDiscovery_ = std::make_shared<dht::PeerDiscovery>();
1984 0 : if (conf.accountPeerDiscovery) {
1985 0 : JAMI_LOG("[Account {}] Starting Jami account discovery…", getAccountID());
1986 0 : startAccountDiscovery();
1987 : }
1988 0 : if (conf.accountPublish)
1989 0 : startAccountPublish();
1990 : }
1991 :
1992 916 : dht::DhtRunner::Context context = initDhtContext();
1993 :
1994 916 : accountManager_->setDht(dht_);
1995 916 : dht_->run(conf.dhtPort, config, std::move(context));
1996 :
1997 916 : dhtBoundPort_ = dht_->getBoundPort();
1998 :
1999 : // Now that the DHT is running and we know the actual bound port,
2000 : // request a UPnP mapping for it. A bound port of 0 means the DHT has no
2001 : // socket to forward (proxy-only mode): requesting a mapping would make the
2002 : // UPnP context allocate an arbitrary port that nothing listens on.
2003 916 : if (upnpCtrl_ and dhtUpnpMapping_.getInternalPort() != dhtBoundPort_) {
2004 850 : releaseDhtUpnpMapping();
2005 :
2006 850 : if (dhtBoundPort_ != 0) {
2007 850 : JAMI_LOG("[Account {:s}] UPnP: requesting mapping for DHT port {}", getAccountID(), dhtBoundPort_);
2008 :
2009 850 : dhtUpnpMapping_.enableAutoUpdate(true);
2010 :
2011 850 : dhtnet::upnp::Mapping desired(dhtnet::upnp::PortType::UDP, dhtBoundPort_, dhtBoundPort_);
2012 850 : dhtUpnpMapping_.updateFrom(desired);
2013 :
2014 850 : dhtUpnpMapping_.setNotifyCallback([w = weak()](const dhtnet::upnp::Mapping::sharedPtr_t& mapRes) {
2015 932 : if (auto accPtr = w.lock()) {
2016 932 : auto& dhtMap = accPtr->dhtUpnpMapping_;
2017 932 : const auto& accId = accPtr->getAccountID();
2018 :
2019 932 : JAMI_LOG("[Account {:s}] DHT UPnP mapping changed to {:s}", accId, mapRes->toString(true));
2020 :
2021 932 : if (dhtMap.getMapKey() != mapRes->getMapKey() or dhtMap.getState() != mapRes->getState()) {
2022 824 : dhtMap.updateFrom(mapRes);
2023 824 : if (mapRes->getState() == dhtnet::upnp::MappingState::OPEN) {
2024 0 : JAMI_LOG("[Account {:s}] Mapping {:s} successfully allocated", accId, dhtMap.toString());
2025 0 : accPtr->dht_->connectivityChanged();
2026 824 : } else if (mapRes->getState() == dhtnet::upnp::MappingState::FAILED) {
2027 824 : JAMI_WARNING("[Account {:s}] UPnP mapping failed", accId);
2028 : }
2029 : } else {
2030 108 : dhtMap.updateFrom(mapRes);
2031 : }
2032 932 : }
2033 932 : });
2034 :
2035 850 : upnpCtrl_->reserveMapping(dhtUpnpMapping_);
2036 850 : }
2037 : }
2038 :
2039 1832 : for (const auto& bootstrap : loadBootstrap())
2040 1832 : dht_->bootstrap(bootstrap);
2041 :
2042 916 : if (conf.dhtProxyServerEnabled) {
2043 0 : dht::ProxyServerConfig proxyConfig;
2044 0 : proxyConfig.port = conf.dhtProxyServerPort;
2045 0 : proxyConfig.identity = id_;
2046 0 : dhtProxyServer_ = std::make_shared<dht::DhtProxyServer>(dht_, proxyConfig);
2047 0 : } else {
2048 916 : dhtProxyServer_.reset();
2049 : }
2050 :
2051 916 : std::unique_lock lkCM(connManagerMtx_);
2052 916 : initConnectionManager();
2053 916 : connectionManager_->dhtStarted();
2054 1815 : connectionManager_->onICERequest([this](const DeviceId& deviceId) { return onICERequest(deviceId); });
2055 916 : connectionManager_->onChannelRequest([this](const std::shared_ptr<dht::crypto::Certificate>& cert,
2056 5658 : const std::string& name) { return onChannelRequest(cert, name); });
2057 1832 : connectionManager_->onNewDeviceConnection(
2058 2282 : [this](const std::shared_ptr<dht::crypto::Certificate>& cert) { onNewDeviceConnection(cert); });
2059 1832 : connectionManager_->onConnectionReady(
2060 916 : [this](const DeviceId& deviceId, const std::string& name, std::shared_ptr<dhtnet::ChannelSocket> channel) {
2061 11020 : onConnectionReady(deviceId, name, std::move(channel));
2062 11020 : });
2063 916 : lkCM.unlock();
2064 :
2065 916 : if (!conf.managerUri.empty() && accountManager_) {
2066 0 : dynamic_cast<ServerAccountManager*>(accountManager_.get())->onDeviceRevoked([this]() {
2067 0 : JAMI_WARNING("[Account {}] Device revoked by server, deleting identity", getAccountID());
2068 0 : editConfig([&](JamiAccountConfig& conf) {
2069 : // Delete the revoked device's key and certificate files
2070 0 : std::error_code ec;
2071 0 : if (!conf.tlsPrivateKeyFile.empty())
2072 0 : std::filesystem::remove(idPath_ / conf.tlsPrivateKeyFile, ec);
2073 0 : if (!conf.tlsCertificateFile.empty())
2074 0 : std::filesystem::remove(idPath_ / conf.tlsCertificateFile, ec);
2075 0 : conf.tlsPrivateKeyFile.clear();
2076 0 : conf.tlsCertificateFile.clear();
2077 0 : conf.receipt.clear();
2078 0 : conf.receiptSignature.clear();
2079 0 : });
2080 0 : Migration::setState(accountID_, Migration::State::INVALID);
2081 0 : setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
2082 0 : });
2083 0 : dynamic_cast<ServerAccountManager*>(accountManager_.get())
2084 0 : ->syncBlueprintConfig([this](const std::map<std::string, std::string>& config) {
2085 0 : editConfig([&](JamiAccountConfig& conf) { conf.fromMap(config); });
2086 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
2087 0 : });
2088 : }
2089 :
2090 916 : if (presenceManager_)
2091 916 : presenceManager_->refresh();
2092 916 : } catch (const std::exception& e) {
2093 0 : JAMI_ERROR("Error registering DHT account: {}", e.what());
2094 0 : setRegistrationState(RegistrationState::ERROR_GENERIC);
2095 0 : }
2096 : }
2097 :
2098 : void
2099 910 : JamiAccount::lookupRegisteredName(const std::string& regName, const NameDirectory::Response& response)
2100 : {
2101 910 : if (response == NameDirectory::Response::found or response == NameDirectory::Response::notFound) {
2102 1819 : const auto& nameResult = response == NameDirectory::Response::found ? regName : "";
2103 910 : if (setRegisteredName(nameResult)) {
2104 0 : editConfig([&](JamiAccountConfig& config) { config.registeredName = nameResult; });
2105 0 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID_, getVolatileAccountDetails());
2106 : }
2107 910 : }
2108 910 : }
2109 :
2110 : dht::DhtRunner::Config
2111 916 : JamiAccount::initDhtConfig(const JamiAccountConfig& conf)
2112 : {
2113 916 : dht::DhtRunner::Config config {};
2114 916 : config.dht_config.node_config.network = 0;
2115 916 : config.dht_config.node_config.maintain_storage = false;
2116 916 : config.dht_config.node_config.persist_path = (cachePath_ / "dhtstate").string();
2117 916 : config.dht_config.id = id_;
2118 916 : config.dht_config.cert_cache_all = true;
2119 916 : config.push_node_id = getAccountID();
2120 916 : config.push_token = conf.deviceKey;
2121 916 : config.push_topic = conf.notificationTopic;
2122 916 : config.push_platform = conf.platform;
2123 916 : config.proxy_user_agent = jami::userAgent();
2124 916 : config.threaded = true;
2125 916 : config.peer_discovery = conf.dhtPeerDiscovery;
2126 916 : config.peer_publish = conf.dhtPeerDiscovery;
2127 916 : if (conf.proxyEnabled)
2128 0 : config.proxy_server = proxyServerCached_;
2129 :
2130 916 : if (not config.proxy_server.empty()) {
2131 0 : JAMI_LOG("[Account {}] Using proxy server {}", getAccountID(), config.proxy_server);
2132 0 : if (not config.push_token.empty()) {
2133 0 : JAMI_LOG("[Account {}] using push notifications with platform: {}, topic: {}, token: {}",
2134 : getAccountID(),
2135 : config.push_platform,
2136 : config.push_topic,
2137 : config.push_token);
2138 : }
2139 : }
2140 916 : return config;
2141 0 : }
2142 :
2143 : dht::DhtRunner::Context
2144 916 : JamiAccount::initDhtContext()
2145 : {
2146 916 : dht::DhtRunner::Context context {};
2147 916 : context.peerDiscovery = peerDiscovery_;
2148 916 : context.rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
2149 :
2150 916 : auto dht_log_level = Manager::instance().dhtLogLevel;
2151 916 : if (dht_log_level > 0) {
2152 0 : context.logger = logger_;
2153 : }
2154 :
2155 4228 : context.certificateStore = [&](const DeviceId& pk_id) {
2156 2396 : std::vector<std::shared_ptr<dht::crypto::Certificate>> ret;
2157 2396 : if (auto cert = certStore().getCertificate(pk_id.toString()))
2158 2396 : ret.emplace_back(std::move(cert));
2159 2396 : JAMI_LOG("[Account {}] Query for local certificate store: {}: {} found.",
2160 : getAccountID(),
2161 : pk_id.toString(),
2162 : ret.size());
2163 2396 : return ret;
2164 916 : };
2165 :
2166 4521 : context.statusChangedCallback = [this](dht::NodeStatus s4, dht::NodeStatus s6) {
2167 2689 : JAMI_LOG("[Account {}] DHT status: IPv4 {}; IPv6 {}", getAccountID(), dhtStatusStr(s4), dhtStatusStr(s6));
2168 : RegistrationState state;
2169 2689 : auto newStatus = std::max(s4, s6);
2170 2689 : switch (newStatus) {
2171 875 : case dht::NodeStatus::Connecting:
2172 875 : state = RegistrationState::TRYING;
2173 875 : break;
2174 1814 : case dht::NodeStatus::Connected:
2175 1814 : state = RegistrationState::REGISTERED;
2176 1814 : break;
2177 0 : case dht::NodeStatus::Disconnected:
2178 0 : state = RegistrationState::UNREGISTERED;
2179 0 : break;
2180 0 : default:
2181 0 : state = RegistrationState::ERROR_GENERIC;
2182 0 : break;
2183 : }
2184 :
2185 2689 : setRegistrationState(state);
2186 3605 : };
2187 :
2188 2741 : context.identityAnnouncedCb = [this](bool ok) {
2189 909 : if (!ok) {
2190 15 : JAMI_ERROR("[Account {}] Identity announcement failed", getAccountID());
2191 15 : return;
2192 : }
2193 894 : JAMI_WARNING("[Account {}] Identity announcement succeeded", getAccountID());
2194 894 : accountManager_
2195 2768 : ->startSync([this](const std::shared_ptr<dht::crypto::Certificate>& crt) { onAccountDeviceFound(crt); },
2196 1787 : [this] { onAccountDeviceAnnounced(); },
2197 894 : publishPresence_);
2198 916 : };
2199 :
2200 916 : return context;
2201 0 : }
2202 :
2203 : void
2204 980 : JamiAccount::onAccountDeviceFound(const std::shared_ptr<dht::crypto::Certificate>& crt)
2205 : {
2206 980 : if (jami::Manager::instance().syncOnRegister) {
2207 980 : if (!crt)
2208 0 : return;
2209 980 : auto deviceId = crt->getLongId();
2210 980 : auto accountManager = accountManager_;
2211 980 : auto info = accountManager ? accountManager->getInfo() : nullptr;
2212 980 : if (!info) {
2213 0 : JAMI_DEBUG("[Account {}] Ignoring discovered account device without account info", getAccountID());
2214 0 : return;
2215 : }
2216 980 : if (info->deviceId == deviceId.toString())
2217 902 : return;
2218 :
2219 78 : dht::ThreadPool::io().run([w = weak(), deviceId] {
2220 78 : auto shared = w.lock();
2221 78 : if (!shared)
2222 0 : return;
2223 : // Only establish a sync connection if this device may be missing a
2224 : // local contact/conversation-list change. This avoids waking up
2225 : // devices (especially mobiles) when there is nothing new to sync.
2226 78 : if (auto* sm = shared->syncModule()) {
2227 78 : if (!sm->needsSync(deviceId)) {
2228 3 : JAMI_DEBUG("[Account {}] [device {}] up to date, skipping sync connection",
2229 : shared->getAccountID(),
2230 : deviceId);
2231 3 : return;
2232 : }
2233 : }
2234 : // Initiate a message connection to create the first TCP link.
2235 : // Once established, onNewDeviceConnection will set up sync and
2236 : // swarm channels.
2237 75 : shared->connectSyncDevice(deviceId);
2238 78 : });
2239 980 : }
2240 : }
2241 :
2242 : void
2243 1131 : JamiAccount::connectSyncDevice(const DeviceId& deviceId)
2244 : {
2245 1131 : requestMessageConnection(getUsername(), deviceId, "sync");
2246 1131 : }
2247 :
2248 : void
2249 1959 : JamiAccount::onSyncListChanged()
2250 : {
2251 1959 : if (!jami::Manager::instance().syncOnRegister)
2252 0 : return;
2253 : // Coalesce bursts of changes (e.g. initial sync delivering many contacts
2254 : // and conversations) into a single propagation pass. A single version bump
2255 : // already marks every device out of date, and sync is full-state, so
2256 : // collapsing many changes into one pass is also semantically correct.
2257 1959 : std::lock_guard lk(syncListChangedMtx_);
2258 1959 : if (!syncListChangedTimer_)
2259 511 : syncListChangedTimer_ = std::make_shared<asio::steady_timer>(*jami::Manager::instance().ioContext());
2260 1959 : syncListChangedTimer_->expires_after(std::chrono::seconds(1));
2261 1959 : syncListChangedTimer_->async_wait([w = weak()](const std::error_code& ec) {
2262 1959 : if (ec) // cancelled by a more recent change (debounce) or shutting down
2263 1022 : return;
2264 937 : dht::ThreadPool::io().run([w] {
2265 937 : auto shared = w.lock();
2266 937 : if (!shared)
2267 0 : return;
2268 937 : auto* sm = shared->syncModule();
2269 937 : if (!sm)
2270 0 : return;
2271 : // A list change makes every device potentially out of date.
2272 937 : sm->bumpVersion();
2273 : // (Re)connect to the account's other devices that are not up to
2274 : // date so the change is pushed. Offline ones are reached on their
2275 : // next presence announcement (onAccountDeviceFound).
2276 937 : auto am = shared->accountManager();
2277 937 : if (am && am->getInfo()) {
2278 937 : auto currentDevice = shared->currentDeviceId();
2279 2930 : for (const auto& [deviceId, device] : am->getKnownDevices()) {
2280 1993 : if (deviceId.toString() == currentDevice)
2281 937 : continue;
2282 1056 : if (sm->needsSync(deviceId))
2283 1056 : shared->connectSyncDevice(deviceId);
2284 937 : }
2285 : }
2286 : // Push immediately to already-connected devices.
2287 937 : sm->syncWithConnected();
2288 937 : });
2289 : });
2290 1959 : }
2291 :
2292 : void
2293 893 : JamiAccount::onAccountDeviceAnnounced()
2294 : {
2295 893 : if (jami::Manager::instance().syncOnRegister) {
2296 893 : deviceAnnounced_ = true;
2297 :
2298 : // Bootstrap at the end to avoid to be long to load.
2299 893 : dht::ThreadPool::io().run([w = weak()] {
2300 893 : if (auto shared = w.lock())
2301 2679 : shared->convModule()->bootstrap();
2302 893 : });
2303 893 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID_, getVolatileAccountDetails());
2304 : }
2305 893 : }
2306 :
2307 : void
2308 1366 : JamiAccount::onNewDeviceConnection(const std::shared_ptr<dht::crypto::Certificate>& cert)
2309 : {
2310 1366 : if (!cert || !cert->issuer)
2311 0 : return;
2312 :
2313 1366 : dht::ThreadPool::io().run([w = weak(), cert] {
2314 1366 : auto shared = w.lock();
2315 1366 : if (!shared)
2316 0 : return;
2317 :
2318 1366 : JAMI_WARNING("[Account {}] New device connection: {}", shared->getAccountID(), cert->getLongId());
2319 :
2320 1366 : const auto peerId = cert->issuer->getId().toString();
2321 1366 : const auto deviceId = cert->getLongId();
2322 1366 : auto am = shared->accountManager();
2323 1366 : if (!am || am->getCertificateStatus(peerId) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2324 0 : return;
2325 : }
2326 :
2327 1366 : const auto isSyncDevice = jami::Manager::instance().syncOnRegister && peerId == shared->getUsername();
2328 2732 : shared->requestMessageConnection(peerId, deviceId, isSyncDevice ? "sync" : "");
2329 :
2330 1366 : if (isSyncDevice) {
2331 74 : auto* sm = shared->syncModule();
2332 74 : if (sm && !sm->isConnected(deviceId)) {
2333 74 : std::shared_lock lk(shared->connManagerMtx_);
2334 74 : if (!shared->connectionManager_)
2335 0 : return;
2336 :
2337 74 : auto it = shared->channelHandlers_.find(Uri::Scheme::SYNC);
2338 74 : if (it != shared->channelHandlers_.end() && it->second)
2339 370 : it->second->connect(deviceId,
2340 : "",
2341 74 : [](const std::shared_ptr<dhtnet::ChannelSocket>& /*socket*/,
2342 74 : const DeviceId& /*deviceId*/) {});
2343 74 : }
2344 : }
2345 :
2346 : // Notify the DRT in all conversations where this peer is a member,
2347 : // so it can decide whether to open a swarm channel over the new connection.
2348 1366 : if (auto* cm = shared->convModule())
2349 1366 : cm->addKnownDevice(peerId, deviceId);
2350 :
2351 : // Proactively refresh the service cache for this device.
2352 : {
2353 1366 : std::shared_lock lk(shared->connManagerMtx_);
2354 1366 : auto it = shared->channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
2355 1366 : if (it != shared->channelHandlers_.end() && it->second) {
2356 1366 : static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->refreshDevice(peerId, deviceId);
2357 : }
2358 1366 : }
2359 1366 : });
2360 : }
2361 :
2362 : void
2363 1031 : JamiAccount::updateTrustedCa()
2364 : {
2365 1031 : if (!accountManager_)
2366 0 : return;
2367 1031 : const auto* info = accountManager_->getInfo();
2368 1031 : if (!info || !info->identity.second)
2369 0 : return;
2370 :
2371 1031 : auto accountCert = info->identity.second->issuer;
2372 1031 : if (!accountCert)
2373 0 : return;
2374 1031 : auto caCert = accountCert->issuer;
2375 1031 : if (!caCert)
2376 0 : return;
2377 :
2378 1031 : auto status = config().allowPeersFromTrusted ? dhtnet::tls::TrustStore::PermissionStatus::ALLOWED
2379 1031 : : dhtnet::tls::TrustStore::PermissionStatus::UNDEFINED;
2380 1031 : JAMI_LOG("[Account {}] {} organization CA {}",
2381 : getAccountID(),
2382 : config().allowPeersFromTrusted ? "Trusting" : "Untrusting",
2383 : caCert->getLongId());
2384 1031 : setCertificateStatus(caCert, status, false);
2385 1031 : }
2386 :
2387 : bool
2388 899 : JamiAccount::onICERequest(const DeviceId& deviceId)
2389 : {
2390 899 : std::promise<bool> accept;
2391 899 : std::future<bool> fut = accept.get_future();
2392 899 : accountManager_->findCertificate(deviceId, [this, &accept](const std::shared_ptr<dht::crypto::Certificate>& cert) {
2393 899 : if (!cert) {
2394 0 : accept.set_value(false);
2395 0 : return;
2396 : }
2397 899 : dht::InfoHash peer_account_id;
2398 899 : auto res = AccountManager::foundPeerDevice(getAccountID(), cert, peer_account_id);
2399 899 : if (res) {
2400 899 : res = accountManager_->isAllowed(*cert, this->config().allowPublicIncoming);
2401 1798 : if (!res
2402 899 : && accountManager_->getCertificateStatus(cert->getLongId().toString())
2403 : != dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2404 455 : auto* conversationModule = convModule(true);
2405 455 : res = conversationModule && conversationModule->isPeerAuthorizedByConversation(*cert);
2406 : }
2407 899 : if (!res) {
2408 39 : JAMI_WARNING("[Account {}] [Auth] Discarding ICE request from unauthorized peer {}.",
2409 : getAccountID(),
2410 : peer_account_id);
2411 : }
2412 : }
2413 899 : JAMI_LOG("[Account {}] [device {}] {} ICE request from {}",
2414 : getAccountID(),
2415 : cert->getLongId(),
2416 : res ? "Accepting" : "Discarding",
2417 : peer_account_id);
2418 899 : accept.set_value(res);
2419 : });
2420 899 : fut.wait();
2421 899 : auto result = fut.get();
2422 899 : return result;
2423 899 : }
2424 :
2425 : bool
2426 5658 : JamiAccount::onChannelRequest(const std::shared_ptr<dht::crypto::Certificate>& cert, const std::string& name)
2427 : {
2428 5658 : JAMI_LOG("[Account {}] [device {}] New channel requested: '{}'", getAccountID(), cert->getLongId(), name);
2429 :
2430 5658 : if (this->config().turnEnabled && turnCache_) {
2431 5658 : auto addr = turnCache_->getResolvedTurn();
2432 5658 : if (addr == std::nullopt) {
2433 : // If TURN is enabled, but no TURN cached, there can be a temporary
2434 : // resolution error to solve. Sometimes, a connectivity change is not
2435 : // enough, so even if this case is really rare, it should be easy to avoid.
2436 2 : turnCache_->refresh();
2437 : }
2438 : }
2439 :
2440 5658 : auto uri = Uri(name);
2441 5658 : std::shared_lock lk(connManagerMtx_);
2442 5658 : auto itHandler = channelHandlers_.find(uri.scheme());
2443 5658 : if (itHandler != channelHandlers_.end() && itHandler->second)
2444 5517 : return itHandler->second->onRequest(cert, name);
2445 141 : return name == "sip";
2446 5658 : }
2447 :
2448 : void
2449 22 : JamiAccount::connectYdocDevice(const DeviceId& deviceId, const std::string& documentId)
2450 : {
2451 22 : std::shared_lock lk(connManagerMtx_);
2452 22 : auto itHandler = channelHandlers_.find(Uri::Scheme::YDOC);
2453 22 : if (itHandler == channelHandlers_.end() || !itHandler->second)
2454 0 : return;
2455 44 : itHandler->second->connect(deviceId, documentId, [](std::shared_ptr<dhtnet::ChannelSocket>, const DeviceId&) {});
2456 22 : }
2457 :
2458 : void
2459 11020 : JamiAccount::onConnectionReady(const DeviceId& deviceId,
2460 : const std::string& name,
2461 : std::shared_ptr<dhtnet::ChannelSocket> channel)
2462 : {
2463 11020 : if (channel) {
2464 11020 : auto cert = channel->peerCertificate();
2465 11020 : if (!cert || !cert->issuer)
2466 0 : return;
2467 11020 : auto peerId = cert->issuer->getId().toString();
2468 : // A connection request can be sent just before member is banned and this must be ignored.
2469 11020 : if (accountManager()->getCertificateStatus(peerId) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2470 0 : channel->shutdown();
2471 0 : return;
2472 : }
2473 11020 : if (name == "sip") {
2474 216 : cacheSIPConnection(std::move(channel), peerId, deviceId);
2475 10804 : } else if (name.find("git://") == 0) {
2476 3045 : auto sep = name.find_last_of('/');
2477 3045 : auto conversationId = name.substr(sep + 1);
2478 3045 : auto targetDevice = name.substr(6, sep - 6);
2479 3045 : auto remoteDevice = deviceId.toString();
2480 :
2481 3045 : if (channel->isInitiator()) {
2482 : // Check if wanted remote is our side (git://targetDevice/conversationId)
2483 1521 : return;
2484 : }
2485 :
2486 1524 : if (targetDevice != currentDeviceId()) {
2487 0 : JAMI_WARNING(
2488 : "[Account {:s}] [Conversation {}] Git server requested for device {}, but this is not ours.",
2489 : getAccountID(),
2490 : conversationId,
2491 : targetDevice);
2492 0 : channel->shutdown();
2493 0 : return;
2494 : }
2495 :
2496 1524 : auto sock = convModule()->gitSocket(remoteDevice, conversationId);
2497 1524 : if (sock == channel) {
2498 : // The onConnectionReady is already used as client (for retrieving messages)
2499 : // So it's not the server socket
2500 0 : return;
2501 : }
2502 :
2503 1524 : auto startGitServer = [w = weak(), conversationId, remoteDevice, channel]() {
2504 1524 : auto shared = w.lock();
2505 1524 : if (!shared)
2506 0 : return;
2507 : /*JAMI_LOG("[Account {:s}] [Conversation {}] [device {}] Git server requested",
2508 : shared->accountID_,
2509 : conversationId,
2510 : remoteDevice);*/
2511 1524 : auto gs = std::make_unique<GitServer>(shared->accountID_, conversationId, channel);
2512 1524 : shared->syncCnt_.fetch_add(1);
2513 : // Balances the increment above exactly once, whether the peer ends up
2514 : // fetching or the channel simply dies.
2515 5290 : auto endSync = [w, done = std::make_shared<std::once_flag>()] {
2516 3766 : std::call_once(*done, [&] {
2517 1524 : if (auto shared = w.lock())
2518 3048 : if (shared->syncCnt_.fetch_sub(1) == 1)
2519 1470 : emitSignal<libjami::ConversationSignal::ConversationCloned>(
2520 2994 : shared->getAccountID().c_str());
2521 1524 : });
2522 5290 : };
2523 1524 : gs->setOnFetched([w, conversationId, remoteDevice, endSync](const std::string& commit) {
2524 2242 : dht::ThreadPool::computation().run([w, conversationId, remoteDevice, commit, endSync]() {
2525 2242 : if (auto shared = w.lock()) {
2526 2242 : shared->convModule()->setFetched(conversationId, remoteDevice, commit);
2527 2242 : endSync();
2528 2242 : }
2529 2242 : });
2530 2242 : });
2531 1524 : const dht::Value::Id serverId = ValueIdDist()(shared->rand);
2532 : {
2533 1524 : std::lock_guard lk(shared->gitServersMtx_);
2534 1524 : shared->gitServers_[serverId] = std::move(gs);
2535 1524 : }
2536 1524 : channel->onShutdown([w, serverId, endSync](const std::error_code&) {
2537 1524 : endSync();
2538 : // Run on main thread to avoid to be in mxSock's eventLoop
2539 1524 : runOnMainThread([serverId, w]() {
2540 1524 : if (auto sthis = w.lock()) {
2541 1524 : std::lock_guard lk(sthis->gitServersMtx_);
2542 1524 : sthis->gitServers_.erase(serverId);
2543 3048 : }
2544 1524 : });
2545 1524 : });
2546 3048 : };
2547 :
2548 1524 : if (!convModule()->isPeerAuthorized(conversationId, peerId, remoteDevice, true)) {
2549 : // Not a member — but if this is a collaborative document held
2550 : // here, a member of its parent conversation may still open it:
2551 : // the serving holder writes the add commit, then serves.
2552 : convModule()
2553 44 : ->authorizeDocumentPeer(conversationId,
2554 : peerId,
2555 : remoteDevice,
2556 44 : [w = weak(), conversationId, peerId, remoteDevice, channel, startGitServer](
2557 : bool authorized) {
2558 22 : if (authorized) {
2559 22 : startGitServer();
2560 22 : return;
2561 : }
2562 0 : if (auto shared = w.lock())
2563 0 : JAMI_WARNING("[Account {:s}] [Conversation {}] Git "
2564 : "server requested, but peer {}/{} is "
2565 : "not authorized",
2566 : shared->getAccountID(),
2567 : conversationId,
2568 : peerId,
2569 0 : remoteDevice);
2570 0 : channel->shutdown();
2571 : });
2572 22 : return;
2573 : }
2574 :
2575 1502 : startGitServer();
2576 6175 : } else {
2577 : // TODO move git://
2578 7759 : std::shared_lock lk(connManagerMtx_);
2579 7759 : auto uri = Uri(name);
2580 7759 : auto itHandler = channelHandlers_.find(uri.scheme());
2581 7759 : if (itHandler != channelHandlers_.end() && itHandler->second)
2582 7753 : itHandler->second->onReady(cert, name, std::move(channel));
2583 7759 : }
2584 12563 : }
2585 : }
2586 :
2587 : void
2588 3040 : JamiAccount::conversationNeedsSyncing(std::shared_ptr<SyncMsg>&& syncMsg)
2589 : {
2590 : // Decide from the message *content* whether it can require (re)opening sync
2591 : // connections. A change to the contact/conversation list (or requests) must
2592 : // reach devices that are not currently connected; a metadata-only update
2593 : // (read status, preferences) only rides the existing connections. Checking
2594 : // the content rather than merely "is syncMsg null" keeps this correct if
2595 : // list data is ever attached to a syncMsg in the future.
2596 3040 : if (syncMsg && !syncMsg->affectsList()) {
2597 : // Metadata-only update: ride the existing sync connections, never open
2598 : // new ones.
2599 1859 : dht::ThreadPool::computation().run([w = weak(), syncMsg = std::move(syncMsg)] {
2600 1859 : if (auto shared = w.lock())
2601 1859 : if (auto* sm = shared->syncModule())
2602 1859 : sm->syncWithConnected(syncMsg);
2603 1859 : });
2604 1859 : return;
2605 : }
2606 : // Contact/conversation-list change: for JAMS accounts, update the server;
2607 : // then bump the local sync version and (re)connect/push to other devices.
2608 : // A non-null syncMsg carrying list state is covered by the full-state push
2609 : // performed by onSyncListChanged().
2610 1181 : dht::ThreadPool::computation().run([w = weak()] {
2611 1181 : auto shared = w.lock();
2612 1181 : if (!shared)
2613 0 : return;
2614 1181 : const auto& config = shared->config();
2615 1181 : if (!config.managerUri.empty())
2616 0 : if (auto am = shared->accountManager())
2617 0 : am->syncDevices();
2618 1181 : shared->onSyncListChanged();
2619 1181 : });
2620 : }
2621 :
2622 : uint64_t
2623 13980 : JamiAccount::conversationSendMessage(const std::string& uri,
2624 : const DeviceId& device,
2625 : const std::map<std::string, std::string>& msg,
2626 : uint64_t token)
2627 : {
2628 : // No need to retrigger, sendTextMessage will call
2629 : // messageEngine_.sendMessage, already retriggering on
2630 : // main thread.
2631 16519 : auto deviceId = device ? device.toString() : "";
2632 27960 : return sendTextMessage(uri, deviceId, msg, token);
2633 13980 : }
2634 :
2635 : void
2636 2514 : JamiAccount::onConversationNeedSocket(const std::string& convId,
2637 : const std::string& deviceId,
2638 : ChannelCb&& cb,
2639 : const std::string& type,
2640 : bool /*noNewSocket*/)
2641 : {
2642 2514 : dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::move(cb), type] {
2643 2514 : auto shared = w.lock();
2644 2514 : if (!shared)
2645 0 : return;
2646 2514 : if (auto socket = shared->convModule()->gitSocket(deviceId, convId)) {
2647 806 : auto remoteCert = socket->peerCertificate();
2648 806 : if (!remoteCert || !remoteCert->issuer
2649 2418 : || !shared->convModule()->isPeerAuthorized(convId,
2650 1612 : remoteCert->issuer->getId().toString(),
2651 1612 : socket->deviceId().toString(),
2652 : true)) {
2653 0 : socket->shutdown();
2654 0 : shared->convModule()->removeGitSocket(socket->deviceId().toString(), convId, socket);
2655 0 : cb({});
2656 0 : return;
2657 : }
2658 806 : if (!cb(socket))
2659 0 : socket->shutdown();
2660 806 : return;
2661 3320 : }
2662 1708 : std::shared_lock lkCM(shared->connManagerMtx_);
2663 1708 : if (!shared->connectionManager_) {
2664 10 : lkCM.unlock();
2665 10 : cb({});
2666 10 : return;
2667 : }
2668 :
2669 3396 : shared->connectionManager_->connectDevice(
2670 1698 : DeviceId(deviceId),
2671 5094 : fmt::format("git://{}/{}", deviceId, convId),
2672 3396 : [w, cb = std::move(cb), convId, requestedDeviceId = deviceId](std::shared_ptr<dhtnet::ChannelSocket> socket,
2673 : const DeviceId&) {
2674 3396 : dht::ThreadPool::io().run(
2675 3396 : [w, cb = std::move(cb), socket = std::move(socket), convId, requestedDeviceId] {
2676 1698 : if (socket) {
2677 1519 : auto shared = w.lock();
2678 1519 : auto remoteCert = socket->peerCertificate();
2679 1519 : auto remoteDeviceId = socket->deviceId().toString();
2680 1519 : if (!shared || !remoteCert || !remoteCert->issuer || remoteDeviceId != requestedDeviceId
2681 4557 : || !shared->convModule()->isPeerAuthorized(convId,
2682 3038 : remoteCert->issuer->getId().toString(),
2683 : remoteDeviceId,
2684 : true)) {
2685 1 : socket->shutdown();
2686 1 : cb({});
2687 1 : return;
2688 : }
2689 1518 : socket->onShutdown([w, deviceId = socket->deviceId(), convId, ws = std::weak_ptr(socket)](
2690 : const std::error_code&) {
2691 1518 : dht::ThreadPool::io().run([w, deviceId, convId, ws] {
2692 1518 : auto shared = w.lock();
2693 : // Identify the channel, so that one dying after it has already
2694 : // been replaced doesn't unregister its replacement.
2695 1518 : auto socket = ws.lock();
2696 1518 : if (shared && socket)
2697 1509 : shared->convModule()->removeGitSocket(deviceId.toString(), convId, socket);
2698 1518 : });
2699 1518 : });
2700 1518 : if (!cb(socket))
2701 6 : socket->shutdown();
2702 1521 : } else
2703 179 : cb({});
2704 : });
2705 1698 : },
2706 : false,
2707 : false,
2708 1698 : type);
2709 2524 : });
2710 2514 : }
2711 :
2712 : void
2713 1894 : JamiAccount::onConversationNeedSwarmSocket(const std::string& convId,
2714 : const std::string& deviceId,
2715 : ChannelCb&& cb,
2716 : const std::string& /*type*/,
2717 : bool noNewSocket)
2718 : {
2719 1894 : dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::forward<ChannelCb&&>(cb), noNewSocket] {
2720 1894 : auto shared = w.lock();
2721 1894 : if (!shared)
2722 0 : return;
2723 1894 : auto* cm = shared->convModule();
2724 1894 : std::shared_lock lkCM(shared->connManagerMtx_);
2725 1894 : if (!shared->connectionManager_ || !cm || cm->isDeviceBanned(convId, deviceId)) {
2726 218 : asio::post(*Manager::instance().ioContext(), [cb = std::move(cb)] { cb({}); });
2727 109 : return;
2728 : }
2729 1785 : DeviceId device(deviceId);
2730 1785 : auto swarmUri = fmt::format("swarm://{}", convId);
2731 1785 : dhtnet::ConnectDeviceOptions opts;
2732 1785 : opts.connType = "";
2733 1785 : opts.noNewSocket = noNewSocket;
2734 1785 : opts.uniqueName = true;
2735 3570 : shared->connectionManager_->connectDevice(
2736 : device,
2737 : swarmUri,
2738 5355 : [w,
2739 1785 : cb = std::move(cb),
2740 : wam = std::weak_ptr(shared->accountManager())](std::shared_ptr<dhtnet::ChannelSocket> socket,
2741 : const DeviceId&) {
2742 1784 : dht::ThreadPool::io().run([w, wam, cb = std::move(cb), socket = std::move(socket)] {
2743 1784 : if (socket) {
2744 1189 : auto shared = w.lock();
2745 1189 : auto am = wam.lock();
2746 1189 : auto remoteCert = socket->peerCertificate();
2747 1189 : if (!remoteCert || !remoteCert->issuer) {
2748 0 : cb(nullptr);
2749 0 : return;
2750 : }
2751 1189 : auto uri = remoteCert->issuer->getId().toString();
2752 2378 : if (!shared || !am
2753 2378 : || am->getCertificateStatus(uri) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2754 0 : cb(nullptr);
2755 0 : return;
2756 : }
2757 1189 : }
2758 1784 : cb(socket);
2759 : });
2760 1784 : },
2761 : opts);
2762 2003 : });
2763 1894 : }
2764 :
2765 : void
2766 1 : JamiAccount::conversationOneToOneReceive(const std::string& convId, const std::string& from)
2767 : {
2768 2 : accountManager_->findCertificate(dht::InfoHash(from),
2769 2 : [this, from, convId](const std::shared_ptr<dht::crypto::Certificate>& cert) {
2770 1 : const auto* info = accountManager_->getInfo();
2771 1 : if (!cert || !info)
2772 0 : return;
2773 2 : info->contacts->onTrustRequest(dht::InfoHash(from),
2774 : cert->getSharedPublicKey(),
2775 : nowMs(),
2776 : false,
2777 1 : convId,
2778 : {});
2779 : });
2780 1 : }
2781 :
2782 : ConversationModule*
2783 52957 : JamiAccount::convModule(bool noCreation)
2784 : {
2785 52957 : if (noCreation)
2786 11228 : return convModule_.get();
2787 41729 : if (!accountManager() || currentDeviceId() == "") {
2788 1 : JAMI_ERROR("[Account {}] Calling convModule() with an uninitialized account", getAccountID());
2789 1 : return nullptr;
2790 : }
2791 41728 : std::unique_lock lock(configurationMutex_);
2792 41728 : std::lock_guard lk(moduleMtx_);
2793 41728 : if (!convModule_) {
2794 1746 : convModule_ = std::make_unique<ConversationModule>(
2795 873 : shared(),
2796 873 : accountManager_,
2797 3040 : [this](auto&& syncMsg) { conversationNeedsSyncing(std::forward<std::shared_ptr<SyncMsg>>(syncMsg)); },
2798 0 : [this](auto&& uri, auto&& device, auto&& msg, auto token = 0) {
2799 13980 : return conversationSendMessage(uri, device, msg, token);
2800 : },
2801 0 : [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType, bool noNewSocket) {
2802 2514 : onConversationNeedSocket(convId, deviceId, std::forward<decltype(cb)>(cb), connectionType, noNewSocket);
2803 2514 : },
2804 0 : [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType, bool noNewSocket) {
2805 1894 : onConversationNeedSwarmSocket(convId,
2806 : deviceId,
2807 1894 : std::forward<decltype(cb)>(cb),
2808 : connectionType,
2809 : noNewSocket);
2810 1894 : },
2811 874 : [this](const auto& convId, const auto& from) { conversationOneToOneReceive(convId, from); },
2812 1746 : autoLoadConversations_);
2813 : }
2814 41728 : return convModule_.get();
2815 41728 : }
2816 :
2817 : SyncModule*
2818 4532 : JamiAccount::syncModule()
2819 : {
2820 4532 : if (!accountManager() || currentDeviceId() == "") {
2821 0 : JAMI_ERROR("Calling syncModule() with an uninitialized account.");
2822 0 : return nullptr;
2823 : }
2824 4532 : std::lock_guard lk(moduleMtx_);
2825 4532 : if (!syncModule_)
2826 859 : syncModule_ = std::make_unique<SyncModule>(shared());
2827 4532 : return syncModule_.get();
2828 4532 : }
2829 :
2830 : std::shared_ptr<CollaborativeEditing>
2831 1616 : JamiAccount::collaborativeEditing()
2832 : {
2833 1616 : std::lock_guard lk(moduleMtx_);
2834 1616 : if (!collaborativeEditing_)
2835 452 : collaborativeEditing_ = std::make_shared<CollaborativeEditing>(shared());
2836 3232 : return collaborativeEditing_;
2837 1616 : }
2838 :
2839 : void
2840 12581 : JamiAccount::onTextMessage(const std::string& id,
2841 : const std::string& from,
2842 : const std::shared_ptr<dht::crypto::Certificate>& peerCert,
2843 : const std::map<std::string, std::string>& payloads)
2844 : {
2845 : try {
2846 25162 : const std::string fromUri {parseJamiUri(from)};
2847 12581 : SIPAccountBase::onTextMessage(id, fromUri, peerCert, payloads);
2848 12581 : } catch (...) {
2849 0 : }
2850 12581 : }
2851 :
2852 : void
2853 0 : JamiAccount::loadConversation(const std::string& convId)
2854 : {
2855 0 : if (auto* cm = convModule(true))
2856 0 : cm->loadSingleConversation(convId);
2857 0 : }
2858 :
2859 : void
2860 1257 : JamiAccount::doUnregister(bool forceShutdownConnections)
2861 : {
2862 1257 : std::unique_lock lock(configurationMutex_);
2863 1257 : if (registrationState_ >= RegistrationState::ERROR_GENERIC) {
2864 181 : return;
2865 : }
2866 :
2867 1076 : std::mutex mtx;
2868 1076 : std::condition_variable cv;
2869 1076 : bool shutdown_complete {false};
2870 :
2871 1076 : if (peerDiscovery_) {
2872 0 : peerDiscovery_->stopPublish(PEER_DISCOVERY_JAMI_SERVICE);
2873 0 : peerDiscovery_->stopDiscovery(PEER_DISCOVERY_JAMI_SERVICE);
2874 : }
2875 :
2876 : // Edits made here since the last checkpoint are pending for persistence only
2877 : // on this device. Online receivers may have merged them into live replicas,
2878 : // but they do not checkpoint remote updates, so those copies are volatile.
2879 : // Write the pending updates out before anything is torn down. Copy the
2880 : // pointer under its own lock: the manager is created lazily, and a channel
2881 : // request or a client call can still be assigning it on another thread.
2882 1076 : std::shared_ptr<CollaborativeEditing> collab;
2883 : {
2884 1076 : std::lock_guard lk(moduleMtx_);
2885 1076 : collab = collaborativeEditing_;
2886 1076 : }
2887 1076 : if (collab) {
2888 : // flush() reaches convModule() and the CRDT document lock; both would
2889 : // deadlock against configurationMutex_, which we hold here.
2890 492 : lock.unlock();
2891 492 : collab->flush();
2892 492 : lock.lock();
2893 : // Another unregistration may have completed while the lock was down.
2894 492 : if (registrationState_ >= RegistrationState::ERROR_GENERIC)
2895 0 : return;
2896 : }
2897 :
2898 1076 : JAMI_WARNING("[Account {}] Unregistering account {}", getAccountID(), fmt::ptr(this));
2899 2152 : dht_->shutdown(
2900 1076 : [&] {
2901 1076 : JAMI_WARNING("[Account {}] DHT shutdown complete", getAccountID());
2902 1076 : std::lock_guard lock(mtx);
2903 1076 : shutdown_complete = true;
2904 1076 : cv.notify_all();
2905 1076 : },
2906 : true);
2907 :
2908 : {
2909 1076 : std::lock_guard lk(pendingCallsMutex_);
2910 1076 : pendingCalls_.clear();
2911 1076 : }
2912 :
2913 : // Stop all current P2P connections if account is disabled
2914 : // or if explicitly requested by the caller.
2915 : // NOTE: Leaving the connections open is useful when changing an account's config.
2916 1076 : if (not isEnabled() || forceShutdownConnections)
2917 1057 : shutdownConnections();
2918 :
2919 : // Keep the UPnP mapping across a plain config reload: the DHT will rebind the
2920 : // same port and doRegister_() then reuses the mapping instead of allocating a new one.
2921 1076 : if (not isEnabled() || forceShutdownConnections)
2922 1057 : releaseDhtUpnpMapping();
2923 :
2924 : {
2925 1076 : std::unique_lock lock(mtx);
2926 3057 : cv.wait(lock, [&] { return shutdown_complete; });
2927 1076 : }
2928 1076 : dht_->join();
2929 1076 : setRegistrationState(RegistrationState::UNREGISTERED);
2930 :
2931 1076 : lock.unlock();
2932 :
2933 : #ifdef ENABLE_PLUGIN
2934 2152 : jami::Manager::instance().getJamiPluginManager().getChatServicesManager().cleanChatSubjects(getAccountID());
2935 : #endif
2936 1257 : }
2937 :
2938 : void
2939 2135 : JamiAccount::releaseDhtUpnpMapping()
2940 : {
2941 2135 : if (dhtUpnpMapping_.getInternalPort() == 0)
2942 1285 : return;
2943 :
2944 850 : if (upnpCtrl_)
2945 850 : upnpCtrl_->releaseMapping(dhtUpnpMapping_);
2946 850 : dhtUpnpMapping_.setNotifyCallback(nullptr);
2947 850 : dhtUpnpMapping_.updateFrom(dhtnet::upnp::Mapping(dhtnet::upnp::PortType::UDP));
2948 : }
2949 :
2950 : void
2951 6709 : JamiAccount::setRegistrationState(RegistrationState state, int detail_code, const std::string& detail_str)
2952 : {
2953 6709 : if (registrationState_ != state) {
2954 4764 : if (state == RegistrationState::REGISTERED) {
2955 910 : JAMI_WARNING("[Account {}] Connected", getAccountID());
2956 910 : turnCache_->refresh();
2957 910 : if (connectionManager_)
2958 902 : connectionManager_->storeActiveIpAddress();
2959 3854 : } else if (state == RegistrationState::TRYING) {
2960 916 : JAMI_WARNING("[Account {}] Connecting…", getAccountID());
2961 : } else {
2962 2938 : deviceAnnounced_ = false;
2963 2938 : JAMI_WARNING("[Account {}] Disconnected", getAccountID());
2964 : }
2965 : }
2966 : // Update registrationState_ & emit signals
2967 6709 : SIPAccountBase::setRegistrationState(state, detail_code, detail_str);
2968 6709 : }
2969 :
2970 : void
2971 0 : JamiAccount::reloadContacts()
2972 : {
2973 0 : accountManager_->reloadContacts();
2974 0 : }
2975 :
2976 : void
2977 0 : JamiAccount::connectivityChanged()
2978 : {
2979 0 : if (not isUsable()) {
2980 : // nothing to do
2981 0 : return;
2982 : }
2983 0 : JAMI_WARNING("[{}] connectivityChanged", getAccountID());
2984 :
2985 0 : if (auto* cm = convModule())
2986 0 : cm->connectivityChanged();
2987 0 : dht_->connectivityChanged();
2988 : {
2989 0 : std::shared_lock lkCM(connManagerMtx_);
2990 0 : if (connectionManager_) {
2991 0 : connectionManager_->connectivityChanged();
2992 : // reset cache
2993 0 : connectionManager_->setPublishedAddress({});
2994 : }
2995 0 : }
2996 : }
2997 :
2998 : void
2999 2 : JamiAccount::networkInterfaceChanged()
3000 : {
3001 2 : if (not isUsable())
3002 0 : return;
3003 2 : runOnMainThread([w = weak()] {
3004 2 : if (auto account = w.lock()) {
3005 5 : for (const auto& callId : account->getCallList()) {
3006 3 : if (auto call = std::dynamic_pointer_cast<SIPCall>(account->getCall(callId)))
3007 3 : call->beginCallRecovery(true);
3008 2 : }
3009 2 : }
3010 2 : });
3011 : }
3012 :
3013 : bool
3014 12 : JamiAccount::requestCallRecovery(const std::shared_ptr<SIPCall>& call, bool newNetwork)
3015 : {
3016 : // Channels already requested on a previous network may never complete:
3017 : // a network change always starts a new attempt.
3018 12 : auto attempt = call->startRecoveryAttempt(newNetwork);
3019 12 : if (not attempt)
3020 3 : return call->isRecovering();
3021 :
3022 9 : auto* transport = call->getTransport();
3023 9 : if (not transport or transport->deviceId().empty() or transport->peerAccountId().empty()) {
3024 0 : JAMI_ERROR("[call:{}] No authenticated peer device for SIP reconnection", call->getCallId());
3025 0 : call->finishRecoveryAttempt(*attempt);
3026 0 : return false;
3027 : }
3028 :
3029 9 : auto deviceId = DeviceId(std::string(transport->deviceId()));
3030 9 : auto peerId = std::string(transport->peerAccountId());
3031 20 : auto retry = [w = weak(), wcall = std::weak_ptr(call), attempt = *attempt] {
3032 2 : auto timer = std::make_shared<asio::steady_timer>(*Manager::instance().ioContext(), 1s);
3033 2 : timer->async_wait([w, wcall, timer, attempt](const asio::error_code& ec) {
3034 2 : if (ec)
3035 0 : return;
3036 2 : runOnMainThread([w, wcall, attempt] {
3037 2 : auto account = w.lock();
3038 2 : auto call = wcall.lock();
3039 2 : if (account and call and call->finishRecoveryAttempt(attempt) and call->needsNewSipChannel())
3040 0 : account->requestCallRecovery(call);
3041 2 : });
3042 : });
3043 11 : };
3044 18 : requestRecoverySIPConnection(deviceId,
3045 9 : call->hasVideo() ? "videoCall" : "audioCall",
3046 18 : [w = weak(), wcall = std::weak_ptr(call), peerId, deviceId, attempt = *attempt,
3047 : retry](const std::shared_ptr<dhtnet::ChannelSocket>& socket) {
3048 9 : if (not socket) {
3049 0 : retry();
3050 0 : return;
3051 : }
3052 9 : auto account = w.lock();
3053 9 : if (not account)
3054 0 : return;
3055 9 : std::shared_ptr<SipTransport> sipTransport;
3056 : {
3057 9 : std::lock_guard lk(account->sipConnsMtx_);
3058 9 : auto it = account->sipConns_.find({peerId, deviceId});
3059 9 : if (it != account->sipConns_.end()) {
3060 9 : auto connection = std::find_if(it->second.begin(),
3061 9 : it->second.end(),
3062 15 : [&](const auto& entry) {
3063 15 : return entry.channel == socket;
3064 : });
3065 9 : if (connection != it->second.end())
3066 9 : sipTransport = connection->transport;
3067 : }
3068 9 : }
3069 9 : if (not sipTransport) {
3070 0 : JAMI_ERROR("[Account {}] Recovered SIP channel was not cached",
3071 : account->getAccountID());
3072 0 : retry();
3073 0 : return;
3074 : }
3075 9 : runOnMainThread([w, wcall, sipTransport, attempt, retry] {
3076 9 : auto account = w.lock();
3077 9 : auto call = wcall.lock();
3078 9 : if (account and call
3079 27 : and not call->useRecoveredTransport(sipTransport,
3080 18 : account->getContactHeader(sipTransport),
3081 9 : attempt))
3082 2 : retry();
3083 9 : });
3084 9 : });
3085 9 : return true;
3086 9 : }
3087 :
3088 : bool
3089 0 : JamiAccount::findCertificate(const dht::InfoHash& h,
3090 : std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
3091 : {
3092 0 : if (accountManager_)
3093 0 : return accountManager_->findCertificate(h, std::move(cb));
3094 0 : return false;
3095 : }
3096 :
3097 : bool
3098 0 : JamiAccount::findCertificate(const dht::PkId& id,
3099 : std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
3100 : {
3101 0 : if (accountManager_)
3102 0 : return accountManager_->findCertificate(id, std::move(cb));
3103 0 : return false;
3104 : }
3105 :
3106 : bool
3107 1125 : JamiAccount::findCertificate(const std::string& crt_id)
3108 : {
3109 1125 : if (accountManager_)
3110 1125 : return accountManager_->findCertificate(dht::InfoHash(crt_id));
3111 0 : return false;
3112 : }
3113 :
3114 : bool
3115 97 : JamiAccount::setCertificateStatus(const std::string& cert_id, dhtnet::tls::TrustStore::PermissionStatus status)
3116 : {
3117 97 : bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false;
3118 97 : if (done) {
3119 94 : findCertificate(cert_id);
3120 94 : emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
3121 : cert_id,
3122 : dhtnet::tls::TrustStore::statusToStr(status));
3123 : }
3124 97 : return done;
3125 : }
3126 :
3127 : bool
3128 1031 : JamiAccount::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
3129 : dhtnet::tls::TrustStore::PermissionStatus status,
3130 : bool local)
3131 : {
3132 1031 : bool done = accountManager_ ? accountManager_->setCertificateStatus(cert, status, local) : false;
3133 1031 : if (done) {
3134 1031 : findCertificate(cert->getLongId().toString());
3135 2062 : emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
3136 2062 : cert->getLongId().toString(),
3137 : dhtnet::tls::TrustStore::statusToStr(status));
3138 : }
3139 1031 : return done;
3140 : }
3141 :
3142 : std::vector<std::string>
3143 0 : JamiAccount::getCertificatesByStatus(dhtnet::tls::TrustStore::PermissionStatus status)
3144 : {
3145 0 : if (accountManager_)
3146 0 : return accountManager_->getCertificatesByStatus(status);
3147 0 : return {};
3148 : }
3149 :
3150 : bool
3151 0 : JamiAccount::isMessageTreated(dht::Value::Id id)
3152 : {
3153 0 : std::lock_guard lock(messageMutex_);
3154 0 : return !treatedMessages_.add(id);
3155 0 : }
3156 :
3157 : bool
3158 16 : JamiAccount::sha3SumVerify() const
3159 : {
3160 16 : return !noSha3sumVerification_;
3161 : }
3162 :
3163 : #ifdef LIBJAMI_TEST
3164 : void
3165 1 : JamiAccount::noSha3sumVerification(bool newValue)
3166 : {
3167 1 : noSha3sumVerification_ = newValue;
3168 1 : }
3169 : #endif
3170 :
3171 : std::map<std::string, std::string>
3172 0 : JamiAccount::getKnownDevices() const
3173 : {
3174 0 : std::lock_guard lock(configurationMutex_);
3175 0 : if (not accountManager_ or not accountManager_->getInfo())
3176 0 : return {};
3177 0 : std::map<std::string, std::string> ids;
3178 0 : for (const auto& d : accountManager_->getKnownDevices()) {
3179 0 : auto id = d.first.toString();
3180 0 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
3181 0 : ids.emplace(std::move(id), std::move(label));
3182 0 : }
3183 0 : return ids;
3184 0 : }
3185 :
3186 : void
3187 0 : JamiAccount::loadCachedUrl(const std::string& url,
3188 : const std::filesystem::path& cachePath,
3189 : const std::chrono::seconds& cacheDuration,
3190 : const std::function<void(const dht::http::Response& response)>& cb)
3191 : {
3192 0 : dht::ThreadPool::io().run([cb, url, cachePath, cacheDuration, w = weak()]() {
3193 : try {
3194 0 : std::string data;
3195 : {
3196 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
3197 0 : data = fileutils::loadCacheTextFile(cachePath, cacheDuration);
3198 0 : }
3199 0 : dht::http::Response ret;
3200 0 : ret.body = std::move(data);
3201 0 : ret.status_code = 200;
3202 0 : cb(ret);
3203 0 : } catch (const std::exception& e) {
3204 0 : JAMI_LOG("Failed to load '{}' from '{}': {}", url, cachePath, e.what());
3205 :
3206 0 : if (auto sthis = w.lock()) {
3207 : auto req = std::make_shared<dht::http::Request>(
3208 0 : *Manager::instance().ioContext(), url, [cb, cachePath, w](const dht::http::Response& response) {
3209 0 : if (response.status_code == 200) {
3210 : try {
3211 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
3212 0 : fileutils::saveFile(cachePath,
3213 0 : (const uint8_t*) response.body.data(),
3214 : response.body.size(),
3215 : 0600);
3216 0 : JAMI_LOG("Cached result to '{}'", cachePath);
3217 0 : } catch (const std::exception& ex) {
3218 0 : JAMI_WARNING("Failed to save result to '{}': {}", cachePath, ex.what());
3219 0 : }
3220 0 : cb(response);
3221 : } else {
3222 : try {
3223 0 : if (std::filesystem::exists(cachePath)) {
3224 0 : JAMI_WARNING("Failed to download URL, using cached data");
3225 0 : std::string data;
3226 : {
3227 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
3228 0 : data = fileutils::loadTextFile(cachePath);
3229 0 : }
3230 0 : dht::http::Response ret;
3231 0 : ret.body = std::move(data);
3232 0 : ret.status_code = 200;
3233 0 : cb(ret);
3234 0 : } else
3235 0 : throw std::runtime_error("No cached data");
3236 0 : } catch (...) {
3237 0 : cb(response);
3238 0 : }
3239 : }
3240 0 : if (auto req = response.request.lock())
3241 0 : if (auto sthis = w.lock())
3242 0 : sthis->requests_.erase(req);
3243 0 : });
3244 0 : sthis->requests_.emplace(req);
3245 0 : req->send();
3246 0 : }
3247 0 : }
3248 0 : });
3249 0 : }
3250 :
3251 : void
3252 916 : JamiAccount::loadCachedProxyServer(std::function<void(const std::string& proxy)> cb)
3253 : {
3254 916 : const auto& conf = config();
3255 916 : if (conf.proxyEnabled and proxyServerCached_.empty()) {
3256 0 : JAMI_DEBUG("[Account {:s}] Loading DHT proxy URL: {:s}", getAccountID(), conf.proxyListUrl);
3257 0 : if (conf.proxyListUrl.empty() or not conf.proxyListEnabled) {
3258 0 : cb(getDhtProxyServer(conf.proxyServer));
3259 : } else {
3260 0 : loadCachedUrl(conf.proxyListUrl,
3261 0 : cachePath_ / "dhtproxylist",
3262 0 : std::chrono::hours(24 * 3),
3263 0 : [w = weak(), cb = std::move(cb)](const dht::http::Response& response) {
3264 0 : if (auto sthis = w.lock()) {
3265 0 : if (response.status_code == 200) {
3266 0 : cb(sthis->getDhtProxyServer(response.body));
3267 : } else {
3268 0 : cb(sthis->getDhtProxyServer(sthis->config().proxyServer));
3269 : }
3270 0 : }
3271 0 : });
3272 : }
3273 : } else {
3274 916 : cb(proxyServerCached_);
3275 : }
3276 916 : }
3277 :
3278 : std::string
3279 0 : JamiAccount::getDhtProxyServer(const std::string& serverList)
3280 : {
3281 0 : if (proxyServerCached_.empty()) {
3282 0 : std::vector<std::string> proxys;
3283 : // Split the list of servers
3284 0 : std::sregex_iterator begin = {serverList.begin(), serverList.end(), PROXY_REGEX}, end;
3285 0 : for (auto it = begin; it != end; ++it) {
3286 0 : auto& match = *it;
3287 0 : if (match[5].matched and match[6].matched) {
3288 : try {
3289 0 : auto start = std::stoi(match[5]), end = std::stoi(match[6]);
3290 0 : for (auto p = start; p <= end; p++)
3291 0 : proxys.emplace_back(match[1].str() + match[2].str() + ":" + std::to_string(p));
3292 0 : } catch (...) {
3293 0 : JAMI_WARNING("Malformed proxy, ignore it");
3294 0 : continue;
3295 0 : }
3296 : } else {
3297 0 : proxys.emplace_back(match[0].str());
3298 : }
3299 0 : }
3300 0 : if (proxys.empty())
3301 0 : return {};
3302 : // Select one of the list as the current proxy.
3303 0 : auto randIt = proxys.begin();
3304 0 : std::advance(randIt, std::uniform_int_distribution<unsigned long>(0, proxys.size() - 1)(rand));
3305 0 : proxyServerCached_ = *randIt;
3306 : // Cache it!
3307 0 : dhtnet::fileutils::check_dir(cachePath_, 0700);
3308 0 : auto proxyCachePath = cachePath_ / "dhtproxy";
3309 0 : std::ofstream file(proxyCachePath);
3310 0 : JAMI_DEBUG("Cache DHT proxy server: {}", proxyServerCached_);
3311 0 : Json::Value node(Json::objectValue);
3312 0 : node[getProxyConfigKey()] = proxyServerCached_;
3313 0 : if (file.is_open())
3314 0 : file << node;
3315 : else
3316 0 : JAMI_WARNING("Unable to write into {}", proxyCachePath);
3317 0 : }
3318 0 : return proxyServerCached_;
3319 : }
3320 :
3321 : MatchRank
3322 0 : JamiAccount::matches(std::string_view userName, std::string_view server) const
3323 : {
3324 0 : if (not accountManager_ or not accountManager_->getInfo())
3325 0 : return MatchRank::NONE;
3326 :
3327 0 : if (userName == accountManager_->getInfo()->accountId || server == accountManager_->getInfo()->accountId
3328 0 : || userName == accountManager_->getInfo()->deviceId) {
3329 0 : JAMI_LOG("Matching account ID in request with username {}", userName);
3330 0 : return MatchRank::FULL;
3331 : } else {
3332 0 : return MatchRank::NONE;
3333 : }
3334 : }
3335 :
3336 : std::string
3337 372 : JamiAccount::getFromUri() const
3338 : {
3339 372 : std::string uri = "<sip:" + accountManager_->getInfo()->accountId + "@ring.dht>";
3340 372 : if (not config().displayName.empty())
3341 372 : return sip_utils::quoteDisplayName(config().displayName) + " " + uri;
3342 0 : return uri;
3343 372 : }
3344 :
3345 : std::string
3346 310 : JamiAccount::getToUri(const std::string& to) const
3347 : {
3348 310 : auto username = to;
3349 930 : string_replace(username, "sip:", "");
3350 620 : return fmt::format("<sips:{};transport=tls>", username);
3351 310 : }
3352 :
3353 : std::string
3354 7 : getDisplayed(const std::string& conversationId, const std::string& messageId)
3355 : {
3356 : // implementing https://tools.ietf.org/rfc/rfc5438.txt
3357 : return fmt::format("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
3358 : "<imdn><message-id>{}</message-id>\n"
3359 : "{}"
3360 : "<display-notification><status><displayed/></status></display-notification>\n"
3361 : "</imdn>",
3362 : messageId,
3363 14 : conversationId.empty() ? "" : "<conversation>" + conversationId + "</conversation>");
3364 : }
3365 :
3366 : std::string
3367 6 : getPIDF(const std::string& note)
3368 : {
3369 : // implementing https://datatracker.ietf.org/doc/html/rfc3863
3370 : return fmt::format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
3371 : "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\">\n"
3372 : " <tuple>\n"
3373 : " <status>\n"
3374 : " <basic>{}</basic>\n"
3375 : " </status>\n"
3376 : " </tuple>\n"
3377 : "</presence>",
3378 12 : note);
3379 : }
3380 :
3381 : void
3382 5 : JamiAccount::setIsComposing(const std::string& conversationUri, bool isWriting)
3383 : {
3384 5 : Uri uri(conversationUri);
3385 5 : std::string conversationId = {};
3386 5 : if (uri.scheme() == Uri::Scheme::SWARM) {
3387 5 : conversationId = uri.authority();
3388 : } else {
3389 0 : return;
3390 : }
3391 :
3392 5 : if (auto* cm = convModule(true)) {
3393 5 : if (auto typer = cm->getTypers(conversationId)) {
3394 5 : if (isWriting)
3395 4 : typer->addTyper(getUsername(), true);
3396 : else
3397 1 : typer->removeTyper(getUsername(), true);
3398 5 : }
3399 : }
3400 5 : }
3401 :
3402 : bool
3403 8 : JamiAccount::setMessageDisplayed(const std::string& conversationUri, const std::string& messageId, int status)
3404 : {
3405 8 : Uri uri(conversationUri);
3406 8 : std::string conversationId = {};
3407 8 : if (uri.scheme() == Uri::Scheme::SWARM)
3408 8 : conversationId = uri.authority();
3409 8 : auto sendMessage = status == (int) libjami::Account::MessageStates::DISPLAYED && isReadReceiptEnabled();
3410 8 : if (!conversationId.empty())
3411 8 : sendMessage &= convModule()->onMessageDisplayed(getUsername(), conversationId, messageId);
3412 8 : if (sendMessage)
3413 21 : sendInstantMessage(uri.authority(), {{MIME_TYPE_IMDN, getDisplayed(conversationId, messageId)}});
3414 8 : return true;
3415 15 : }
3416 :
3417 : std::string
3418 231 : JamiAccount::getContactHeader(const std::shared_ptr<SipTransport>& sipTransport)
3419 : {
3420 231 : if (sipTransport and sipTransport->get() != nullptr) {
3421 231 : auto* transport = sipTransport->get();
3422 231 : auto* td = reinterpret_cast<tls::AbstractSIPTransport::TransportData*>(transport);
3423 231 : auto address = td->self->getLocalAddress().toString(true);
3424 231 : bool reliable = transport->flag & PJSIP_TRANSPORT_RELIABLE;
3425 : return fmt::format("{} <sips:{}{}{};transport={}>",
3426 231 : sip_utils::quoteDisplayName(config().displayName),
3427 462 : id_.second->getId().toString(),
3428 231 : address.empty() ? "" : "@",
3429 : address,
3430 693 : reliable ? "tls" : "dtls");
3431 231 : } else {
3432 0 : JAMI_ERROR("getContactHeader: no SIP transport provided");
3433 : return fmt::format("{} <sips:{}@ring.dht>",
3434 0 : sip_utils::quoteDisplayName(config().displayName),
3435 0 : id_.second->getId().toString());
3436 : }
3437 : }
3438 :
3439 : void
3440 264 : JamiAccount::addContact(const std::string& uri, bool confirmed)
3441 : {
3442 264 : dht::InfoHash h(uri);
3443 264 : if (not h) {
3444 1 : JAMI_ERROR("addContact: invalid contact URI");
3445 1 : return;
3446 : }
3447 263 : auto conversation = convModule()->getOneToOneConversation(uri);
3448 263 : if (!confirmed && conversation.empty())
3449 263 : conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
3450 263 : std::unique_lock lock(configurationMutex_);
3451 263 : if (accountManager_)
3452 263 : accountManager_->addContact(h, confirmed, conversation);
3453 : else
3454 0 : JAMI_WARNING("[Account {}] addContact: account not loaded", getAccountID());
3455 263 : }
3456 :
3457 : void
3458 19 : JamiAccount::removeContact(const std::string& uri, bool ban)
3459 : {
3460 19 : std::lock_guard lock(configurationMutex_);
3461 19 : if (accountManager_)
3462 19 : accountManager_->removeContact(uri, ban);
3463 : else
3464 0 : JAMI_WARNING("[Account {}] removeContact: account not loaded", getAccountID());
3465 19 : }
3466 :
3467 : std::map<std::string, std::string>
3468 8 : JamiAccount::getContactDetails(const std::string& uri) const
3469 : {
3470 8 : std::lock_guard lock(configurationMutex_);
3471 16 : return accountManager_ ? accountManager_->getContactDetails(uri) : std::map<std::string, std::string> {};
3472 8 : }
3473 :
3474 : std::optional<Contact>
3475 5317 : JamiAccount::getContactInfo(const std::string& uri) const
3476 : {
3477 5317 : std::lock_guard lock(configurationMutex_);
3478 10634 : return accountManager_ ? accountManager_->getContactInfo(uri) : std::nullopt;
3479 5317 : }
3480 :
3481 : bool
3482 0 : JamiAccount::isContact(const std::string& peerAccountUri) const
3483 : {
3484 0 : auto info = getContactInfo(peerAccountUri);
3485 0 : return info && info->isActive();
3486 0 : }
3487 :
3488 : // ----------------------------------------------------------------------------
3489 : // Service-exposure: peer discovery & tunnel orchestration.
3490 : // ----------------------------------------------------------------------------
3491 :
3492 : struct JamiAccount::PendingSvcQuery
3493 : {
3494 : uint32_t requestId {0};
3495 : std::string peerUri;
3496 : };
3497 :
3498 : void
3499 1 : JamiAccount::finalizeSvcQuery(uint32_t requestId, int status, const std::string& servicesJson)
3500 : {
3501 1 : JAMI_LOG("[Account {}] finalizeSvcQuery req={} status={}", getAccountID(), requestId, status);
3502 1 : std::shared_ptr<PendingSvcQuery> q;
3503 : {
3504 1 : std::lock_guard lk(pendingSvcQueriesMtx_);
3505 1 : auto it = pendingSvcQueries_.find(requestId);
3506 1 : if (it == pendingSvcQueries_.end())
3507 0 : return;
3508 1 : q = std::move(it->second);
3509 1 : pendingSvcQueries_.erase(it);
3510 1 : }
3511 1 : emitSignal<libjami::ServiceSignal::PeerServicesReceived>(requestId, getAccountID(), q->peerUri, status, servicesJson);
3512 1 : }
3513 :
3514 : std::string
3515 2979 : JamiAccount::buildPeerServicesJson(const std::string& peerUri, const DeviceId* forceAvailableDevice)
3516 : {
3517 2979 : std::vector<SvcDiscoveryChannelHandler::CachedSvcInfo> services;
3518 : {
3519 2979 : std::shared_lock lk(connManagerMtx_);
3520 2979 : auto it = channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
3521 2979 : if (it == channelHandlers_.end() || !it->second)
3522 142 : return {};
3523 2837 : services = static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->getCachedServices(peerUri);
3524 2979 : }
3525 2837 : if (services.empty())
3526 2827 : return {};
3527 :
3528 10 : std::vector<DeviceId> onlineDevices;
3529 10 : if (presenceManager_)
3530 10 : onlineDevices = presenceManager_->getDevices(peerUri);
3531 :
3532 10 : Json::Value arr(Json::arrayValue);
3533 20 : for (const auto& s : services) {
3534 10 : Json::Value v(Json::objectValue);
3535 10 : v["id"] = s.info.id;
3536 10 : v["name"] = s.info.name;
3537 10 : v["description"] = s.info.description;
3538 10 : v["proto"] = s.info.proto;
3539 10 : v["scheme"] = s.info.scheme;
3540 10 : v["preferredPort"] = s.info.preferred_port;
3541 10 : v["device"] = s.deviceId.toString();
3542 19 : v["available"] = (forceAvailableDevice && *forceAvailableDevice == s.deviceId)
3543 29 : || std::find(onlineDevices.begin(), onlineDevices.end(), s.deviceId) != onlineDevices.end();
3544 10 : arr.append(std::move(v));
3545 10 : }
3546 10 : return json::toString(arr);
3547 2979 : }
3548 :
3549 : uint32_t
3550 1 : JamiAccount::queryPeerServices(const std::string& peerUri)
3551 : {
3552 : using PSStatus = libjami::ServiceSignal::PeerServicesStatus;
3553 :
3554 : static std::atomic<uint32_t> sQueryCounter {0};
3555 1 : const auto requestId = ++sQueryCounter;
3556 :
3557 1 : JAMI_LOG("[Account {}] queryPeerServices req={} peer={}", getAccountID(), requestId, peerUri);
3558 :
3559 1 : auto state = std::make_shared<PendingSvcQuery>();
3560 1 : state->requestId = requestId;
3561 1 : state->peerUri = peerUri;
3562 : {
3563 1 : std::lock_guard lk(pendingSvcQueriesMtx_);
3564 1 : pendingSvcQueries_.emplace(requestId, state);
3565 1 : }
3566 :
3567 1 : std::shared_lock lk(connManagerMtx_);
3568 1 : auto* handler = static_cast<SvcDiscoveryChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_DISCOVERY].get());
3569 1 : if (!handler) {
3570 0 : return 0;
3571 : }
3572 :
3573 1 : runOnMainThread([w = weak(), requestId, peerUri] {
3574 1 : auto sthis = w.lock();
3575 1 : if (!sthis)
3576 0 : return;
3577 1 : auto servicesJson = sthis->buildPeerServicesJson(peerUri);
3578 2 : sthis->finalizeSvcQuery(requestId,
3579 1 : static_cast<int>(servicesJson.empty() ? PSStatus::NoDevices : PSStatus::OK),
3580 : servicesJson);
3581 1 : });
3582 1 : return requestId;
3583 1 : }
3584 :
3585 : std::string
3586 1 : JamiAccount::openServiceTunnel(const std::string& peerUri,
3587 : const std::string& deviceId,
3588 : const std::string& serviceId,
3589 : const std::string& serviceName,
3590 : uint16_t localPort)
3591 : {
3592 1 : auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
3593 1 : if (!handler)
3594 0 : return {};
3595 1 : DeviceId dev;
3596 : try {
3597 1 : dev = DeviceId(deviceId);
3598 0 : } catch (...) {
3599 0 : return {};
3600 0 : }
3601 1 : auto accId = getAccountID();
3602 : return handler->openTunnel(
3603 : peerUri,
3604 : dev,
3605 : serviceId,
3606 : serviceName,
3607 : localPort,
3608 2 : [accId](const std::string& tunnelId, uint16_t port) {
3609 1 : emitSignal<libjami::ServiceSignal::TunnelOpened>(accId, tunnelId, port);
3610 1 : },
3611 1 : [accId](const std::string& tunnelId, const std::string& reason) {
3612 1 : emitSignal<libjami::ServiceSignal::TunnelClosed>(accId, tunnelId, reason);
3613 4 : });
3614 1 : }
3615 :
3616 : bool
3617 1 : JamiAccount::closeServiceTunnel(const std::string& tunnelId)
3618 : {
3619 1 : auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
3620 1 : if (!handler)
3621 0 : return false;
3622 1 : return handler->closeTunnel(tunnelId);
3623 : }
3624 :
3625 : void
3626 0 : JamiAccount::closeServerTunnelsForService(const std::string& serviceId)
3627 : {
3628 0 : auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
3629 0 : if (!handler)
3630 0 : return;
3631 0 : handler->closeServerChannelsForService(serviceId);
3632 : }
3633 :
3634 : std::vector<std::map<std::string, std::string>>
3635 0 : JamiAccount::getActiveServiceTunnels() const
3636 : {
3637 0 : auto it = channelHandlers_.find(Uri::Scheme::SVC_TUNNEL);
3638 0 : if (it == channelHandlers_.end())
3639 0 : return {};
3640 0 : auto* handler = static_cast<SvcTunnelChannelHandler*>(it->second.get());
3641 0 : if (!handler)
3642 0 : return {};
3643 0 : auto tunnels = handler->activeTunnels();
3644 0 : std::vector<std::map<std::string, std::string>> out;
3645 0 : out.reserve(tunnels.size());
3646 0 : for (auto& t : tunnels) {
3647 0 : out.push_back({{"id", t.id},
3648 0 : {"peerUri", t.peerUri},
3649 0 : {"deviceId", t.peerDevice},
3650 0 : {"serviceId", t.serviceId},
3651 0 : {"serviceName", t.serviceName},
3652 0 : {"localPort", std::to_string(t.localPort)}});
3653 : }
3654 0 : return out;
3655 0 : }
3656 :
3657 : std::vector<std::map<std::string, std::string>>
3658 5 : JamiAccount::getContacts(bool includeRemoved) const
3659 : {
3660 5 : std::lock_guard lock(configurationMutex_);
3661 5 : if (not accountManager_)
3662 0 : return {};
3663 5 : const auto& contacts = accountManager_->getContacts(includeRemoved);
3664 5 : std::vector<std::map<std::string, std::string>> ret;
3665 5 : ret.reserve(contacts.size());
3666 12 : for (const auto& c : contacts) {
3667 7 : auto details = c.second.toMap();
3668 7 : if (not details.empty()) {
3669 21 : details["id"] = c.first.toString();
3670 7 : ret.emplace_back(std::move(details));
3671 : }
3672 7 : }
3673 5 : return ret;
3674 5 : }
3675 :
3676 : std::vector<std::map<std::string, std::string>>
3677 112 : JamiAccount::getDeviceList() const
3678 : {
3679 112 : std::lock_guard lock(configurationMutex_);
3680 112 : if (not accountManager_ or not accountManager_->getInfo())
3681 12 : return {};
3682 100 : std::vector<std::map<std::string, std::string>> ret;
3683 100 : auto devices = accountManager_->getKnownDevices();
3684 100 : ret.reserve(devices.size());
3685 298 : for (const auto& d : devices) {
3686 198 : auto id = d.first.toString();
3687 198 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
3688 792 : std::map<std::string, std::string> device {{"id", std::move(id)}, {"label", std::move(label)}};
3689 198 : if (d.second.last_sync != KnownDevice::time_point::min()) {
3690 632 : device["lastSync"] = std::to_string(
3691 474 : std::chrono::duration_cast<std::chrono::seconds>(d.second.last_sync.time_since_epoch()).count());
3692 : }
3693 198 : ret.emplace_back(std::move(device));
3694 198 : }
3695 100 : return ret;
3696 310 : }
3697 :
3698 : /* trust requests */
3699 :
3700 : std::vector<std::map<std::string, std::string>>
3701 919 : JamiAccount::getTrustRequests() const
3702 : {
3703 919 : std::lock_guard lock(configurationMutex_);
3704 1838 : return accountManager_ ? accountManager_->getTrustRequests() : std::vector<std::map<std::string, std::string>> {};
3705 919 : }
3706 :
3707 : bool
3708 226 : JamiAccount::acceptTrustRequest(const std::string& from, bool includeConversation)
3709 : {
3710 226 : dht::InfoHash h(from);
3711 226 : if (not h) {
3712 0 : JAMI_ERROR("addContact: invalid contact URI");
3713 0 : return false;
3714 : }
3715 226 : std::unique_lock lock(configurationMutex_);
3716 226 : if (accountManager_) {
3717 226 : if (!accountManager_->acceptTrustRequest(from, includeConversation)) {
3718 : // Note: unused for swarm
3719 : // Typically the case where the trust request doesn't exists, only incoming DHT messages
3720 0 : return accountManager_->addContact(h, true);
3721 : }
3722 226 : return true;
3723 : }
3724 0 : JAMI_WARNING("[Account {}] acceptTrustRequest: account not loaded", getAccountID());
3725 0 : return false;
3726 226 : }
3727 :
3728 : bool
3729 2 : JamiAccount::discardTrustRequest(const std::string& from)
3730 : {
3731 : // Remove 1:1 generated conv requests
3732 2 : auto requests = getTrustRequests();
3733 4 : for (const auto& req : requests) {
3734 4 : if (req.at(libjami::Account::TrustRequest::FROM) == from) {
3735 6 : convModule()->declineConversationRequest(req.at(libjami::Account::TrustRequest::CONVERSATIONID));
3736 : }
3737 : }
3738 :
3739 : // Remove trust request
3740 2 : std::lock_guard lock(configurationMutex_);
3741 2 : if (accountManager_)
3742 2 : return accountManager_->discardTrustRequest(from);
3743 0 : JAMI_WARNING("[Account {:s}] discardTrustRequest: account not loaded", getAccountID());
3744 0 : return false;
3745 2 : }
3746 :
3747 : void
3748 3 : JamiAccount::declineConversationRequest(const std::string& conversationId)
3749 : {
3750 3 : auto peerId = convModule()->peerFromConversationRequest(conversationId);
3751 3 : convModule()->declineConversationRequest(conversationId);
3752 3 : if (!peerId.empty()) {
3753 3 : std::lock_guard lock(configurationMutex_);
3754 3 : if (const auto* info = accountManager_->getInfo()) {
3755 : // Verify if we have a trust request with this peer + convId
3756 3 : auto req = info->contacts->getTrustRequest(dht::InfoHash(peerId));
3757 9 : if (req.find(libjami::Account::TrustRequest::CONVERSATIONID) != req.end()
3758 8 : && req.at(libjami::Account::TrustRequest::CONVERSATIONID) == conversationId) {
3759 1 : accountManager_->discardTrustRequest(peerId);
3760 1 : JAMI_DEBUG("[Account {:s}] Declined trust request with {:s}", getAccountID(), peerId);
3761 : }
3762 3 : }
3763 3 : }
3764 3 : }
3765 :
3766 : void
3767 252 : JamiAccount::sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload)
3768 : {
3769 252 : dht::InfoHash h(to);
3770 252 : if (not h) {
3771 0 : JAMI_ERROR("addContact: invalid contact URI");
3772 0 : return;
3773 : }
3774 :
3775 : // The invite timestamp is now a native field on the trust request itself (see
3776 : // jami::TrustRequestMsg), no need to stamp it into the vCard payload anymore.
3777 252 : auto invited = nowMs();
3778 :
3779 : // Cache the payload (and the invite timestamp alongside it) so retries reuse the same
3780 : // timestamp instead of being treated as a new re-invite.
3781 252 : auto requestPath = cachePath_ / "requests";
3782 252 : dhtnet::fileutils::recursive_mkdir(requestPath, 0700);
3783 252 : auto cachedFile = requestPath / to;
3784 252 : std::ofstream req(cachedFile, std::ios::trunc | std::ios::binary);
3785 252 : if (!req.is_open()) {
3786 0 : JAMI_ERROR("Unable to write data to {}", cachedFile);
3787 0 : return;
3788 : }
3789 :
3790 252 : if (not payload.empty()) {
3791 3 : req.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
3792 : }
3793 252 : req.close();
3794 :
3795 252 : auto cachedTsFile = requestPath / (to + ".ts");
3796 252 : std::ofstream tsFile(cachedTsFile, std::ios::trunc | std::ios::binary);
3797 252 : if (tsFile.is_open())
3798 252 : tsFile << toMillisecondsSinceEpoch(invited);
3799 : else
3800 0 : JAMI_WARNING("Unable to write invite timestamp cache to {}", cachedTsFile);
3801 :
3802 252 : if (payload.size() >= 64000) {
3803 1 : JAMI_WARNING("Trust request is too big. Remove payload");
3804 : }
3805 :
3806 252 : auto conversation = convModule()->getOneToOneConversation(to);
3807 252 : if (conversation.empty())
3808 0 : conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
3809 252 : if (not conversation.empty()) {
3810 252 : std::lock_guard lock(configurationMutex_);
3811 252 : if (accountManager_)
3812 504 : accountManager_->sendTrustRequest(to,
3813 : conversation,
3814 504 : payload.size() >= 64000 ? std::vector<uint8_t> {} : payload,
3815 : invited);
3816 : else
3817 0 : JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
3818 252 : } else
3819 0 : JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
3820 252 : }
3821 :
3822 : void
3823 0 : JamiAccount::forEachDevice(const dht::InfoHash& to,
3824 : std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)>&& op,
3825 : std::function<void(bool)>&& end)
3826 : {
3827 0 : accountManager_->forEachDevice(to, std::move(op), std::move(end));
3828 0 : }
3829 :
3830 : uint64_t
3831 13981 : JamiAccount::sendTextMessage(const std::string& to,
3832 : const std::string& deviceId,
3833 : const std::map<std::string, std::string>& payloads,
3834 : uint64_t refreshToken,
3835 : bool onlyConnected)
3836 : {
3837 13981 : Uri uri(to);
3838 13981 : if (uri.scheme() == Uri::Scheme::SWARM) {
3839 0 : sendInstantMessage(uri.authority(), payloads);
3840 0 : return 0;
3841 : }
3842 :
3843 13981 : std::string toUri;
3844 : try {
3845 13981 : toUri = parseJamiUri(to);
3846 0 : } catch (...) {
3847 0 : JAMI_ERROR("Failed to send a text message due to an invalid URI {}", to);
3848 0 : return 0;
3849 0 : }
3850 13981 : if (payloads.size() != 1) {
3851 0 : JAMI_ERROR("Multi-part im is not supported yet by JamiAccount");
3852 0 : return 0;
3853 : }
3854 13981 : return SIPAccountBase::sendTextMessage(toUri, deviceId, payloads, refreshToken, onlyConnected);
3855 13981 : }
3856 :
3857 : void
3858 14918 : JamiAccount::sendMessage(const std::string& to,
3859 : const std::string& deviceId,
3860 : const std::map<std::string, std::string>& payloads,
3861 : uint64_t token,
3862 : bool retryOnTimeout,
3863 : bool onlyConnected)
3864 : {
3865 14918 : std::string toUri;
3866 : try {
3867 14918 : toUri = parseJamiUri(to);
3868 0 : } catch (...) {
3869 0 : JAMI_ERROR("[Account {}] Failed to send a text message due to an invalid URI {}", getAccountID(), to);
3870 0 : if (!onlyConnected)
3871 0 : messageEngine_.onMessageSent(to, token, false, deviceId);
3872 0 : return;
3873 0 : }
3874 14918 : if (payloads.size() != 1) {
3875 0 : JAMI_ERROR("Multi-part im is not supported");
3876 0 : if (!onlyConnected)
3877 0 : messageEngine_.onMessageSent(toUri, token, false, deviceId);
3878 0 : return;
3879 : }
3880 :
3881 : // Use the Message channel if available
3882 14918 : std::shared_lock clk(connManagerMtx_);
3883 14918 : auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
3884 14918 : if (!handler) {
3885 45 : clk.unlock();
3886 45 : if (!onlyConnected)
3887 45 : messageEngine_.onMessageSent(to, token, false, deviceId);
3888 45 : return;
3889 : }
3890 :
3891 : auto devices = std::make_shared<SendMessageContext>(
3892 29746 : [w = weak(), to, token, deviceId, onlyConnected, retryOnTimeout](bool success, bool sent) {
3893 14873 : if (auto acc = w.lock())
3894 14873 : acc->onMessageSent(to, token, deviceId, success, onlyConnected, sent && retryOnTimeout);
3895 29746 : });
3896 :
3897 12583 : auto completed = [w = weak(), to, devices](const DeviceId& device,
3898 : const std::shared_ptr<dhtnet::ChannelSocket>& conn,
3899 : bool success) {
3900 12583 : if (!success)
3901 0 : if (auto acc = w.lock()) {
3902 0 : std::shared_lock clk(acc->connManagerMtx_);
3903 0 : if (auto* handler = static_cast<MessageChannelHandler*>(
3904 0 : acc->channelHandlers_[Uri::Scheme::MESSAGE].get())) {
3905 0 : handler->closeChannel(to, device, conn);
3906 : }
3907 0 : }
3908 12583 : devices->complete(device, success);
3909 27456 : };
3910 :
3911 14873 : const auto& payload = *payloads.begin();
3912 14873 : auto msg = std::make_shared<MessageChannelHandler::Message>();
3913 14873 : msg->id = token;
3914 14873 : msg->t = payload.first;
3915 14873 : msg->c = payload.second;
3916 14873 : auto device = deviceId.empty() ? DeviceId() : DeviceId(deviceId);
3917 14873 : if (deviceId.empty()) {
3918 3432 : auto conns = handler->getChannels(toUri);
3919 3432 : clk.unlock();
3920 5522 : for (const auto& conn : conns) {
3921 2090 : auto connDevice = conn->deviceId();
3922 2090 : if (!devices->add(connDevice))
3923 941 : continue;
3924 1149 : dht::ThreadPool::io().run([completed, connDevice, conn, msg] {
3925 1149 : completed(connDevice, conn, MessageChannelHandler::sendMessage(conn, *msg));
3926 1149 : });
3927 : }
3928 3432 : } else {
3929 11441 : if (auto conn = handler->getChannel(toUri, device)) {
3930 11434 : clk.unlock();
3931 11434 : devices->add(device);
3932 11434 : dht::ThreadPool::io().run([completed, device, conn, msg] {
3933 11434 : completed(device, conn, MessageChannelHandler::sendMessage(conn, *msg));
3934 11434 : });
3935 11434 : devices->start();
3936 11434 : return;
3937 11441 : }
3938 : }
3939 3439 : if (clk)
3940 7 : clk.unlock();
3941 :
3942 3439 : devices->start();
3943 :
3944 3439 : if (onlyConnected)
3945 22 : return;
3946 : // We are unable to send the message directly, try connecting
3947 :
3948 : // Get conversation id, which will be used by the iOS notification extension
3949 : // to load the conversation.
3950 2834 : auto extractIdFromJson = [](const std::string& jsonData) -> std::string {
3951 2834 : Json::Value parsed;
3952 2834 : if (json::parse(jsonData, parsed)) {
3953 2834 : auto value = parsed.get("id", Json::nullValue);
3954 2834 : if (value && value.isString()) {
3955 2834 : return value.asString();
3956 : }
3957 2834 : } else {
3958 0 : JAMI_WARNING("Unable to parse jsonData to get conversation ID");
3959 : }
3960 0 : return "";
3961 2834 : };
3962 :
3963 : // get request type
3964 3417 : auto payload_type = msg->t;
3965 3417 : if (payload_type == MIME_TYPE_GIT) {
3966 2834 : std::string id = extractIdFromJson(msg->c);
3967 2834 : if (!id.empty()) {
3968 2834 : payload_type += "/" + id;
3969 : }
3970 2834 : }
3971 :
3972 3417 : if (deviceId.empty()) {
3973 3410 : auto toH = dht::InfoHash(toUri);
3974 : // Find listening devices for this account
3975 10230 : accountManager_->forEachDevice(toH,
3976 6820 : [this, to, devices, payload_type, currentDevice = DeviceId(currentDeviceId())](
3977 : const std::shared_ptr<dht::crypto::PublicKey>& dev) {
3978 : // Test if already sent
3979 3413 : auto deviceId = dev->getLongId();
3980 3413 : if (deviceId == currentDevice || devices->pending(deviceId)) {
3981 1084 : return;
3982 : }
3983 :
3984 : // Else, ask for a channel to send the message
3985 2329 : dht::ThreadPool::io().run([this, to, deviceId, payload_type]() {
3986 2329 : requestMessageConnection(to, deviceId, payload_type);
3987 2329 : });
3988 : });
3989 : } else {
3990 7 : requestMessageConnection(to, device, payload_type);
3991 : }
3992 60787 : }
3993 :
3994 : void
3995 14873 : JamiAccount::onMessageSent(
3996 : const std::string& to, uint64_t id, const std::string& deviceId, bool success, bool onlyConnected, bool retry)
3997 : {
3998 14873 : if (!onlyConnected)
3999 14844 : messageEngine_.onMessageSent(to, id, success, deviceId);
4000 :
4001 14873 : if (!success) {
4002 2297 : if (retry)
4003 0 : messageEngine_.onPeerOnline(to, deviceId);
4004 : }
4005 14873 : }
4006 :
4007 : dhtnet::IceTransportOptions
4008 142 : JamiAccount::getIceOptions() const
4009 : {
4010 142 : return connectionManager_->getIceOptions();
4011 : }
4012 :
4013 : void
4014 20 : JamiAccount::getIceOptions(std::function<void(dhtnet::IceTransportOptions&&)> cb) const
4015 : {
4016 20 : return connectionManager_->getIceOptions(std::move(cb));
4017 : }
4018 :
4019 : dhtnet::IpAddr
4020 120 : JamiAccount::getPublishedIpAddress(uint16_t family) const
4021 : {
4022 120 : return connectionManager_->getPublishedIpAddress(family);
4023 : }
4024 :
4025 : bool
4026 0 : JamiAccount::setPushNotificationToken(const std::string& token)
4027 : {
4028 0 : if (SIPAccountBase::setPushNotificationToken(token)) {
4029 0 : JAMI_WARNING("[Account {:s}] setPushNotificationToken: {:s}", getAccountID(), token);
4030 0 : if (dht_)
4031 0 : dht_->setPushNotificationToken(token);
4032 0 : return true;
4033 : }
4034 0 : return false;
4035 : }
4036 :
4037 : bool
4038 0 : JamiAccount::setPushNotificationTopic(const std::string& topic)
4039 : {
4040 0 : if (SIPAccountBase::setPushNotificationTopic(topic)) {
4041 0 : if (dht_)
4042 0 : dht_->setPushNotificationTopic(topic);
4043 0 : return true;
4044 : }
4045 0 : return false;
4046 : }
4047 :
4048 : bool
4049 0 : JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data)
4050 : {
4051 0 : if (SIPAccountBase::setPushNotificationConfig(data)) {
4052 0 : if (dht_) {
4053 0 : dht_->setPushNotificationPlatform(config_->platform);
4054 0 : dht_->setPushNotificationTopic(config_->notificationTopic);
4055 0 : dht_->setPushNotificationToken(config_->deviceKey);
4056 : }
4057 0 : return true;
4058 : }
4059 0 : return false;
4060 : }
4061 :
4062 : /**
4063 : * To be called by clients with relevant data when a push notification is received.
4064 : */
4065 : void
4066 0 : JamiAccount::pushNotificationReceived(const std::string& /*from*/, const std::map<std::string, std::string>& data)
4067 : {
4068 0 : auto ret_future = dht_->pushNotificationReceived(data);
4069 0 : dht::ThreadPool::computation().run([id = getAccountID(), ret_future = ret_future.share()] {
4070 0 : JAMI_WARNING("[Account {:s}] pushNotificationReceived: {}", id, (uint8_t) ret_future.get());
4071 0 : });
4072 0 : }
4073 :
4074 : std::string
4075 9 : JamiAccount::getUserUri() const
4076 : {
4077 9 : if (not registeredName_.empty())
4078 0 : return JAMI_URI_PREFIX + registeredName_;
4079 9 : return JAMI_URI_PREFIX + config().username;
4080 : }
4081 :
4082 : std::vector<libjami::Message>
4083 0 : JamiAccount::getLastMessages(const uint64_t& base_timestamp)
4084 : {
4085 0 : return SIPAccountBase::getLastMessages(base_timestamp);
4086 : }
4087 :
4088 : void
4089 0 : JamiAccount::startAccountPublish()
4090 : {
4091 0 : AccountPeerInfo info_pub;
4092 0 : info_pub.accountId = dht::InfoHash(accountManager_->getInfo()->accountId);
4093 0 : info_pub.displayName = config().displayName;
4094 0 : peerDiscovery_->startPublish<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, info_pub);
4095 0 : }
4096 :
4097 : void
4098 0 : JamiAccount::startAccountDiscovery()
4099 : {
4100 0 : auto id = dht::InfoHash(accountManager_->getInfo()->accountId);
4101 0 : peerDiscovery_
4102 0 : ->startDiscovery<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, [this, id](AccountPeerInfo&& v, dht::SockAddr&&) {
4103 0 : std::lock_guard lc(discoveryMapMtx_);
4104 : // Make sure that account itself will not be recorded
4105 0 : if (v.accountId != id) {
4106 : // Create or find the old one
4107 0 : auto& dp = discoveredPeers_[v.accountId];
4108 0 : dp.displayName = v.displayName;
4109 0 : discoveredPeerMap_[v.accountId.toString()] = v.displayName;
4110 0 : if (!dp.cleanupTimer) {
4111 : // Avoid repeat reception of same peer
4112 0 : JAMI_LOG("Account discovered: {}: {}", v.displayName, v.accountId.to_c_str());
4113 : // Send Added Peer and corrsponding accoundID
4114 0 : emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(getAccountID(),
4115 0 : v.accountId.toString(),
4116 : 0,
4117 0 : v.displayName);
4118 0 : dp.cleanupTimer = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext(),
4119 0 : PEER_DISCOVERY_EXPIRATION);
4120 : }
4121 0 : dp.cleanupTimer->expires_after(PEER_DISCOVERY_EXPIRATION);
4122 0 : dp.cleanupTimer->async_wait(
4123 0 : [w = weak(), p = v.accountId, a = v.displayName](const asio::error_code& ec) {
4124 0 : if (ec)
4125 0 : return;
4126 0 : if (auto this_ = w.lock()) {
4127 : {
4128 0 : std::lock_guard lc(this_->discoveryMapMtx_);
4129 0 : this_->discoveredPeers_.erase(p);
4130 0 : this_->discoveredPeerMap_.erase(p.toString());
4131 0 : }
4132 : // Send deleted peer
4133 0 : emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(this_->getAccountID(),
4134 0 : p.toString(),
4135 : 1,
4136 0 : a);
4137 0 : }
4138 0 : JAMI_LOG("Account removed from discovery list: {}", a);
4139 : });
4140 : }
4141 0 : });
4142 0 : }
4143 :
4144 : std::map<std::string, std::string>
4145 0 : JamiAccount::getNearbyPeers() const
4146 : {
4147 0 : return discoveredPeerMap_;
4148 : }
4149 :
4150 : void
4151 0 : JamiAccount::sendProfileToPeers()
4152 : {
4153 0 : if (!connectionManager_)
4154 0 : return;
4155 0 : std::set<std::string> peers;
4156 0 : const auto& accountUri = accountManager_->getInfo()->accountId;
4157 : // TODO: avoid using getConnectionList
4158 0 : for (const auto& connection : connectionManager_->getConnectionList()) {
4159 0 : const auto& device = connection.at("device");
4160 0 : const auto& peer = connection.at("peer");
4161 0 : if (!peers.emplace(peer).second)
4162 0 : continue;
4163 0 : if (peer == accountUri) {
4164 0 : sendProfile("", accountUri, device);
4165 0 : continue;
4166 : }
4167 0 : const auto& conversationId = convModule()->getOneToOneConversation(peer);
4168 0 : if (!conversationId.empty()) {
4169 0 : sendProfile(conversationId, peer, device);
4170 : }
4171 0 : }
4172 0 : }
4173 :
4174 : void
4175 0 : JamiAccount::updateProfile(const std::string& displayName,
4176 : const std::string& avatar,
4177 : const std::string& fileType,
4178 : const std::string& botOwner,
4179 : int32_t flag)
4180 : {
4181 : // if the fileType is empty then only the display name will be upated
4182 :
4183 0 : const auto& accountUri = accountManager_->getInfo()->accountId;
4184 0 : const auto& path = profilePath();
4185 0 : const auto& profiles = idPath_ / "profiles";
4186 :
4187 : try {
4188 0 : if (!std::filesystem::exists(profiles)) {
4189 0 : std::filesystem::create_directories(profiles);
4190 : }
4191 0 : } catch (const std::exception& e) {
4192 0 : JAMI_ERROR("Failed to create profiles directory: {}", e.what());
4193 0 : return;
4194 0 : }
4195 :
4196 0 : const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(accountUri));
4197 :
4198 0 : auto profile = getProfileVcard();
4199 0 : if (profile.empty()) {
4200 0 : profile = vCard::utils::initVcard();
4201 : }
4202 :
4203 0 : profile[std::string(vCard::Property::FORMATTED_NAME)] = displayName;
4204 0 : editConfig([&](JamiAccountConfig& config) { config.displayName = displayName; });
4205 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
4206 :
4207 0 : if (!fileType.empty()) {
4208 0 : const std::string& key = "PHOTO;ENCODING=BASE64;TYPE=" + fileType;
4209 0 : if (flag == 0) {
4210 0 : vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
4211 0 : const auto& avatarPath = std::filesystem::path(avatar);
4212 0 : if (std::filesystem::exists(avatarPath)) {
4213 : try {
4214 0 : profile[key] = base64::encode(fileutils::loadFile(avatarPath));
4215 0 : } catch (const std::exception& e) {
4216 0 : JAMI_ERROR("Failed to load avatar: {}", e.what());
4217 0 : }
4218 : }
4219 0 : } else if (flag == 1) {
4220 0 : vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
4221 0 : profile[key] = avatar;
4222 : }
4223 0 : }
4224 0 : if (flag == 2) {
4225 0 : vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
4226 : }
4227 0 : if (!botOwner.empty()) {
4228 : // See RFC 6473
4229 0 : profile[std::string(vCard::Property::KIND)] = "application";
4230 : // See RFC 6350
4231 0 : profile[std::string(vCard::Property::RELATED_OWNER)] = botOwner;
4232 : }
4233 : try {
4234 0 : vCard::utils::save(profile, vCardPath, path);
4235 0 : emitSignal<libjami::ConfigurationSignal::ProfileReceived>(getAccountID(), accountUri, path.string());
4236 :
4237 : // Delete all profile sent markers:
4238 0 : std::error_code ec;
4239 0 : std::filesystem::remove_all(cachePath_ / "vcard", ec);
4240 0 : sendProfileToPeers();
4241 0 : } catch (const std::exception& e) {
4242 0 : JAMI_ERROR("Error writing profile: {}", e.what());
4243 0 : }
4244 0 : }
4245 :
4246 : void
4247 1033 : JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
4248 : {
4249 1033 : Account::setActiveCodecs(list);
4250 1033 : if (!hasActiveCodec(MEDIA_AUDIO))
4251 1011 : setCodecActive(AV_CODEC_ID_OPUS);
4252 1033 : if (!hasActiveCodec(MEDIA_VIDEO)) {
4253 1011 : setCodecActive(AV_CODEC_ID_HEVC);
4254 1011 : setCodecActive(AV_CODEC_ID_H264);
4255 1011 : setCodecActive(AV_CODEC_ID_VP8);
4256 : }
4257 1033 : config_->activeCodecs = getActiveCodecs(MEDIA_ALL);
4258 1033 : }
4259 :
4260 : void
4261 11 : JamiAccount::sendInstantMessage(const std::string& convId, const std::map<std::string, std::string>& msg)
4262 : {
4263 11 : auto members = convModule()->getConversationMembers(convId);
4264 11 : if (convId.empty() && members.empty()) {
4265 : // TODO remove, it's for old API for contacts
4266 0 : sendTextMessage(convId, "", msg);
4267 0 : return;
4268 : }
4269 33 : for (const auto& m : members) {
4270 22 : const auto& uri = m.at("uri");
4271 22 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
4272 : // Announce to all members that a new message is sent
4273 66 : sendMessage(uri, "", msg, token, false, true);
4274 : }
4275 11 : }
4276 :
4277 : bool
4278 12581 : JamiAccount::handleMessage(const std::shared_ptr<dht::crypto::Certificate>& cert,
4279 : const std::string& from,
4280 : const std::pair<std::string, std::string>& m)
4281 : {
4282 12581 : if (not cert or not cert->issuer)
4283 0 : return true; // stop processing message
4284 :
4285 12581 : if (cert->issuer->getId().to_view() != from) {
4286 0 : JAMI_WARNING("[Account {}] [device {}] handleMessage: invalid author {}",
4287 : getAccountID(),
4288 : cert->issuer->getId().to_view(),
4289 : from);
4290 0 : return true;
4291 : }
4292 12581 : if (m.first == MIME_TYPE_GIT) {
4293 12076 : Json::Value json;
4294 12076 : if (!json::parse(m.second, json)) {
4295 0 : return true;
4296 : }
4297 :
4298 : // fetchNewCommits will do heavy stuff like fetching, avoid to block SIP socket
4299 24152 : dht::ThreadPool::io().run([w = weak(),
4300 : from,
4301 12076 : deviceId = json["deviceId"].asString(),
4302 12076 : id = json["id"].asString(),
4303 12076 : commit = json["commit"].asString()] {
4304 12076 : if (auto shared = w.lock()) {
4305 12076 : if (auto* cm = shared->convModule())
4306 12076 : cm->fetchNewCommits(from, deviceId, id, commit);
4307 12076 : }
4308 12076 : });
4309 12076 : return true;
4310 12581 : } else if (m.first == MIME_TYPE_INVITE) {
4311 159 : convModule()->onNeedConversationRequest(from, m.second);
4312 159 : return true;
4313 346 : } else if (m.first == MIME_TYPE_INVITE_JSON) {
4314 327 : Json::Value json;
4315 327 : if (!json::parse(m.second, json)) {
4316 0 : return true;
4317 : }
4318 327 : convModule()->onConversationRequest(from, json);
4319 327 : return true;
4320 346 : } else if (m.first == MIME_TYPE_IM_COMPOSING) {
4321 : try {
4322 4 : static const std::regex COMPOSING_REGEX("<state>\\s*(\\w+)\\s*<\\/state>");
4323 4 : std::smatch matched_pattern;
4324 4 : std::regex_search(m.second, matched_pattern, COMPOSING_REGEX);
4325 4 : bool isComposing {false};
4326 4 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4327 4 : isComposing = matched_pattern[1] == "active";
4328 : }
4329 4 : static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
4330 4 : std::regex_search(m.second, matched_pattern, CONVID_REGEX);
4331 4 : std::string conversationId = "";
4332 4 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4333 4 : conversationId = matched_pattern[1];
4334 : }
4335 4 : if (!conversationId.empty()) {
4336 4 : if (auto* cm = convModule(true)) {
4337 4 : if (auto typer = cm->getTypers(conversationId)) {
4338 4 : if (isComposing)
4339 3 : typer->addTyper(from);
4340 : else
4341 1 : typer->removeTyper(from);
4342 4 : }
4343 : }
4344 : }
4345 4 : return true;
4346 4 : } catch (const std::exception& e) {
4347 0 : JAMI_WARNING("Error parsing composing state: {}", e.what());
4348 0 : }
4349 15 : } else if (m.first == MIME_TYPE_IMDN) {
4350 : try {
4351 8 : static const std::regex IMDN_MSG_ID_REGEX("<message-id>\\s*(\\w+)\\s*<\\/message-id>");
4352 8 : std::smatch matched_pattern;
4353 :
4354 8 : std::regex_search(m.second, matched_pattern, IMDN_MSG_ID_REGEX);
4355 8 : std::string messageId;
4356 8 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4357 8 : messageId = matched_pattern[1];
4358 : } else {
4359 0 : JAMI_WARNING("Message displayed: unable to parse message ID");
4360 0 : return true;
4361 : }
4362 :
4363 8 : static const std::regex STATUS_REGEX("<status>\\s*<(\\w+)\\/>\\s*<\\/status>");
4364 8 : std::regex_search(m.second, matched_pattern, STATUS_REGEX);
4365 8 : bool isDisplayed {false};
4366 8 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4367 8 : isDisplayed = matched_pattern[1] == "displayed";
4368 : } else {
4369 0 : JAMI_WARNING("Message displayed: unable to parse status");
4370 0 : return true;
4371 : }
4372 :
4373 8 : static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
4374 8 : std::regex_search(m.second, matched_pattern, CONVID_REGEX);
4375 8 : std::string conversationId = "";
4376 8 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4377 8 : conversationId = matched_pattern[1];
4378 : }
4379 :
4380 8 : if (!isReadReceiptEnabled())
4381 0 : return true;
4382 8 : if (isDisplayed) {
4383 8 : if (convModule()->onMessageDisplayed(from, conversationId, messageId)) {
4384 7 : JAMI_DEBUG("[message {}] Displayed by peer", messageId);
4385 14 : emitSignal<libjami::ConfigurationSignal::AccountMessageStatusChanged>(
4386 7 : accountID_,
4387 : conversationId,
4388 : from,
4389 : messageId,
4390 : static_cast<int>(libjami::Account::MessageStates::DISPLAYED));
4391 : }
4392 : }
4393 8 : return true;
4394 8 : } catch (const std::exception& e) {
4395 0 : JAMI_ERROR("Error parsing display notification: {}", e.what());
4396 0 : }
4397 7 : } else if (m.first == MIME_TYPE_PIDF) {
4398 7 : std::smatch matched_pattern;
4399 7 : static const std::regex BASIC_REGEX("<basic>([\\w\\s]+)<\\/basic>");
4400 7 : std::regex_search(m.second, matched_pattern, BASIC_REGEX);
4401 7 : std::string customStatus {};
4402 7 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4403 7 : customStatus = matched_pattern[1];
4404 7 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
4405 : from,
4406 : static_cast<int>(PresenceState::CONNECTED),
4407 : customStatus);
4408 : } else {
4409 0 : JAMI_WARNING("Presence: unable to parse status");
4410 : }
4411 7 : return true;
4412 7 : }
4413 :
4414 0 : return false;
4415 : }
4416 :
4417 : void
4418 314 : JamiAccount::callConnectionClosed(const DeviceId& deviceId, bool eraseDummy)
4419 : {
4420 314 : std::function<void(const DeviceId&, bool)> cb;
4421 : {
4422 314 : std::lock_guard lk(onConnectionClosedMtx_);
4423 314 : auto it = onConnectionClosed_.find(deviceId);
4424 314 : if (it != onConnectionClosed_.end()) {
4425 95 : if (eraseDummy) {
4426 95 : cb = std::move(it->second);
4427 95 : onConnectionClosed_.erase(it);
4428 : } else {
4429 : // In this case a new subcall is created and the callback
4430 : // will be re-called once with eraseDummy = true
4431 0 : cb = it->second;
4432 : }
4433 : }
4434 314 : }
4435 314 : dht::ThreadPool::io().run([w = weak(), cb = std::move(cb), id = deviceId, erase = std::move(eraseDummy)] {
4436 314 : if (auto acc = w.lock()) {
4437 314 : if (cb)
4438 95 : cb(id, erase);
4439 314 : }
4440 314 : });
4441 314 : }
4442 :
4443 : void
4444 4833 : JamiAccount::requestMessageConnection(const std::string& peerId,
4445 : const DeviceId& deviceId,
4446 : const std::string& connectionType)
4447 : {
4448 4833 : std::shared_lock lk(connManagerMtx_);
4449 4833 : auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
4450 4833 : if (!handler)
4451 1 : return;
4452 4832 : if (deviceId) {
4453 4832 : if (auto connected = handler->getChannel(peerId, deviceId)) {
4454 1194 : return;
4455 4832 : }
4456 : } else {
4457 0 : auto connected = handler->getChannels(peerId);
4458 0 : if (!connected.empty()) {
4459 0 : return;
4460 : }
4461 0 : }
4462 : // Nothing to do on completion: the work that has to happen once the device is
4463 : // reachable is driven by onMessageChannelReady().
4464 10914 : handler->connect(deviceId, "", [](const std::shared_ptr<dhtnet::ChannelSocket>&, const DeviceId&) {}, connectionType);
4465 4833 : }
4466 :
4467 : void
4468 1362 : JamiAccount::onMessageChannelReady(const std::string& peerId, const DeviceId& deviceId)
4469 : {
4470 1362 : messageEngine_.onPeerOnline(peerId);
4471 1362 : messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
4472 1362 : if (!presenceNote_.empty()) {
4473 : // If a presence note is set, send it to this device.
4474 3 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
4475 9 : std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
4476 3 : sendMessage(peerId, deviceId.toString(), msg, token, false, true);
4477 3 : }
4478 1362 : convModule()->syncConversations(peerId, deviceId.toString());
4479 1365 : }
4480 :
4481 : void
4482 101 : JamiAccount::requestSIPConnection(const std::string& peerId,
4483 : const DeviceId& deviceId,
4484 : const std::string& connectionType,
4485 : bool forceNewConnection,
4486 : const std::shared_ptr<SIPCall>& pc)
4487 : {
4488 101 : JAMI_LOG("[Account {}] Request SIP connection to peer {} on device {}", getAccountID(), peerId, deviceId);
4489 :
4490 : // If a connection already exists or is in progress, no need to do this
4491 101 : std::lock_guard lk(sipConnsMtx_);
4492 101 : auto id = std::make_pair(peerId, deviceId);
4493 :
4494 101 : if (sipConns_.find(id) != sipConns_.end()) {
4495 0 : JAMI_LOG("[Account {}] A SIP connection with {} already exists", getAccountID(), deviceId);
4496 0 : return;
4497 : }
4498 : // If not present, create it
4499 101 : std::shared_lock lkCM(connManagerMtx_);
4500 101 : if (!connectionManager_)
4501 0 : return;
4502 : // Note, Even if we send 50 "sip" request, the connectionManager_ will only use one socket.
4503 : // however, this will still ask for multiple channels, so only ask
4504 : // if there is no pending request
4505 101 : if (!forceNewConnection && connectionManager_->isConnecting(deviceId, "sip")) {
4506 0 : JAMI_LOG("[Account {}] Already connecting to {}", getAccountID(), deviceId);
4507 0 : return;
4508 : }
4509 101 : JAMI_LOG("[Account {}] Ask {} for a new SIP channel", getAccountID(), deviceId);
4510 101 : dhtnet::ConnectDeviceOptions options;
4511 101 : options.noNewSocket = false;
4512 101 : options.forceNewSocket = forceNewConnection;
4513 101 : options.connType = connectionType;
4514 101 : options.channelTimeout = 3s;
4515 101 : options.uniqueName = true;
4516 303 : connectionManager_->connectDevice(
4517 : deviceId,
4518 : "sip",
4519 202 : [w = weak(), id = std::move(id), pc = std::move(pc)](const std::shared_ptr<dhtnet::ChannelSocket>& socket,
4520 : const DeviceId&) {
4521 101 : if (socket)
4522 98 : return;
4523 3 : auto shared = w.lock();
4524 3 : if (!shared)
4525 0 : return;
4526 : // If this is triggered, this means that the
4527 : // connectDevice didn't get any response from the DHT.
4528 : // Stop searching pending call.
4529 3 : shared->callConnectionClosed(id.second, true);
4530 3 : if (pc)
4531 3 : pc->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
4532 3 : },
4533 : options);
4534 101 : }
4535 :
4536 : void
4537 9 : JamiAccount::requestRecoverySIPConnection(const DeviceId& deviceId,
4538 : const std::string& connectionType,
4539 : std::function<void(const std::shared_ptr<dhtnet::ChannelSocket>&)>&& onResult)
4540 : {
4541 9 : std::shared_lock lkCM(connManagerMtx_);
4542 9 : if (!connectionManager_) {
4543 0 : JAMI_WARNING("[Account {}] No connection manager to recover the SIP channel", getAccountID());
4544 0 : onResult(nullptr);
4545 0 : return;
4546 : }
4547 9 : JAMI_LOG("[Account {}] Ask {} for a SIP channel on a new connection", getAccountID(), deviceId);
4548 9 : dhtnet::ConnectDeviceOptions options;
4549 9 : options.forceNewSocket = true;
4550 9 : options.ignoreConnectedSockets = true;
4551 9 : options.upnpMappingTimeout = std::chrono::milliseconds::zero();
4552 9 : options.connType = connectionType;
4553 9 : options.channelTimeout = 3s;
4554 36 : connectionManager_->connectDevice(
4555 : deviceId,
4556 : "sip",
4557 18 : [onResult = std::move(onResult)](const std::shared_ptr<dhtnet::ChannelSocket>& socket, const DeviceId&) {
4558 9 : onResult(socket);
4559 9 : },
4560 : options);
4561 9 : }
4562 :
4563 : bool
4564 613 : JamiAccount::isConnectedWith(const DeviceId& deviceId) const
4565 : {
4566 613 : std::shared_lock lkCM(connManagerMtx_);
4567 613 : if (connectionManager_)
4568 609 : return connectionManager_->isConnected(deviceId);
4569 4 : return false;
4570 613 : }
4571 :
4572 : void
4573 3 : JamiAccount::sendPresenceNote(const std::string& note)
4574 : {
4575 3 : if (const auto* info = accountManager_->getInfo()) {
4576 3 : if (!info || !info->contacts)
4577 0 : return;
4578 3 : presenceNote_ = note;
4579 3 : auto contacts = info->contacts->getContacts();
4580 3 : std::vector<std::pair<std::string, DeviceId>> keys;
4581 : {
4582 3 : std::shared_lock lkCM(connManagerMtx_);
4583 3 : auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
4584 3 : if (!handler)
4585 0 : return;
4586 5 : for (const auto& contact : contacts) {
4587 2 : auto peerId = contact.first.toString();
4588 2 : auto channels = handler->getChannels(peerId);
4589 6 : for (const auto& channel : channels) {
4590 4 : keys.emplace_back(peerId, channel->deviceId());
4591 : }
4592 2 : }
4593 3 : }
4594 3 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
4595 9 : std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
4596 7 : for (auto& key : keys) {
4597 4 : sendMessage(key.first, key.second.toString(), msg, token, false, true);
4598 : }
4599 3 : }
4600 3 : }
4601 :
4602 : void
4603 1762 : JamiAccount::sendProfile(const std::string& convId, const std::string& peerUri, const std::string& deviceId)
4604 : {
4605 1762 : auto accProfilePath = profilePath();
4606 1762 : if (not std::filesystem::is_regular_file(accProfilePath))
4607 1756 : return;
4608 6 : auto currentSha3 = fileutils::sha3File(accProfilePath);
4609 : // VCard sync for peerUri
4610 6 : if (not needToSendProfile(peerUri, deviceId, currentSha3)) {
4611 : /*JAMI_DEBUG("[Account {}] [device {}] Peer {} already got an up-to-date vCard",
4612 : getAccountID(),
4613 : deviceId,
4614 : peerUri);*/
4615 0 : return;
4616 : }
4617 : // We need a new channel
4618 36 : transferFile(convId,
4619 12 : accProfilePath.string(),
4620 : deviceId,
4621 : "profile.vcf",
4622 : "",
4623 : 0,
4624 : 0,
4625 : currentSha3,
4626 : fileutils::lastWriteTimeInSeconds(accProfilePath),
4627 12 : [accId = getAccountID(), peerUri, deviceId]() {
4628 : // Mark the VCard as sent
4629 4 : auto sendDir = fileutils::get_cache_dir() / accId / "vcard" / peerUri;
4630 4 : auto path = sendDir / deviceId;
4631 4 : dhtnet::fileutils::recursive_mkdir(sendDir);
4632 4 : std::lock_guard lock(dhtnet::fileutils::getFileLock(path));
4633 4 : if (std::filesystem::is_regular_file(path))
4634 0 : return;
4635 4 : std::ofstream p(path);
4636 4 : });
4637 1762 : }
4638 :
4639 : bool
4640 6 : JamiAccount::needToSendProfile(const std::string& peerUri, const std::string& deviceId, const std::string& sha3Sum)
4641 : {
4642 6 : std::string previousSha3 {};
4643 6 : auto vCardPath = cachePath_ / "vcard";
4644 6 : auto sha3Path = vCardPath / "sha3";
4645 6 : dhtnet::fileutils::check_dir(vCardPath, 0700);
4646 : try {
4647 9 : previousSha3 = fileutils::loadTextFile(sha3Path);
4648 3 : } catch (...) {
4649 3 : fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
4650 3 : return true;
4651 3 : }
4652 3 : if (sha3Sum != previousSha3) {
4653 : // Incorrect sha3 stored. Update it
4654 0 : dhtnet::fileutils::removeAll(vCardPath, true);
4655 0 : dhtnet::fileutils::check_dir(vCardPath, 0700);
4656 0 : fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
4657 0 : return true;
4658 : }
4659 3 : auto peerPath = vCardPath / peerUri;
4660 3 : dhtnet::fileutils::recursive_mkdir(peerPath);
4661 3 : return not std::filesystem::is_regular_file(peerPath / deviceId);
4662 6 : }
4663 :
4664 : void
4665 447 : JamiAccount::clearProfileCache(const std::string& peerUri)
4666 : {
4667 447 : std::error_code ec;
4668 447 : std::filesystem::remove_all(cachePath_ / "vcard" / peerUri, ec);
4669 447 : }
4670 :
4671 : std::filesystem::path
4672 1764 : JamiAccount::profilePath() const
4673 : {
4674 1764 : return idPath_ / "profile.vcf";
4675 : }
4676 :
4677 : void
4678 216 : JamiAccount::cacheSIPConnection(std::shared_ptr<dhtnet::ChannelSocket>&& socket,
4679 : const std::string& peerId,
4680 : const DeviceId& deviceId)
4681 : {
4682 216 : std::unique_lock lk(sipConnsMtx_);
4683 : // Verify that the connection is not already cached
4684 216 : SipConnectionKey key(peerId, deviceId);
4685 216 : auto& connections = sipConns_[key];
4686 228 : auto conn = std::find_if(connections.begin(), connections.end(), [&](const auto& v) { return v.channel == socket; });
4687 216 : if (conn != connections.end()) {
4688 0 : JAMI_WARNING("[Account {}] Channel socket already cached with this peer", getAccountID());
4689 0 : return;
4690 : }
4691 :
4692 : // Convert to SIP transport
4693 216 : auto onShutdown = [w = weak(), peerId, key, socket]() {
4694 216 : dht::ThreadPool::io().run([w = std::move(w), peerId, key, socket] {
4695 216 : auto shared = w.lock();
4696 216 : if (!shared)
4697 0 : return;
4698 216 : shared->shutdownSIPConnection(socket, key.first, key.second);
4699 : // The connection can be closed during the SIP initialization, so
4700 : // if this happens, the request should be re-sent to ask for a new
4701 : // SIP channel to make the call pass through
4702 216 : shared->callConnectionClosed(key.second, false);
4703 216 : });
4704 432 : };
4705 216 : auto sip_tr = link_.sipTransportBroker->getChanneledTransport(shared(), socket, std::move(onShutdown));
4706 216 : if (!sip_tr) {
4707 0 : JAMI_ERROR("No channeled transport found");
4708 0 : return;
4709 : }
4710 216 : sip_tr->setPeerAccountId(peerId);
4711 : // Store the connection
4712 216 : connections.emplace_back(SipConnection {sip_tr, socket});
4713 216 : JAMI_WARNING("[Account {:s}] [device {}] New SIP channel opened", getAccountID(), deviceId);
4714 216 : lk.unlock();
4715 :
4716 : // Retry messages
4717 216 : messageEngine_.onPeerOnline(peerId);
4718 216 : messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
4719 :
4720 : // Connect pending calls
4721 216 : forEachPendingCall(deviceId, [&](const auto& pc) {
4722 98 : if (pc->getConnectionState() != Call::ConnectionState::TRYING
4723 98 : and pc->getConnectionState() != Call::ConnectionState::PROGRESSING)
4724 0 : return;
4725 98 : pc->setSipTransport(sip_tr, getContactHeader(sip_tr));
4726 98 : pc->setState(Call::ConnectionState::PROGRESSING);
4727 98 : if (auto remote_address = socket->getRemoteAddress()) {
4728 : try {
4729 98 : onConnectedOutgoingCall(pc, peerId, remote_address);
4730 0 : } catch (const VoipLinkException&) {
4731 : // In this case, the main scenario is that SIPStartCall failed because
4732 : // the ICE is dead and the TLS session didn't send any packet on that dead
4733 : // link (connectivity change, killed by the os, etc)
4734 : // Here, we don't need to do anything, the TLS will fail and will delete
4735 : // the cached transport
4736 : }
4737 : }
4738 : });
4739 :
4740 : // Locally opened channels are adopted only by the matching recovery request.
4741 216 : if (socket->isInitiator())
4742 108 : return;
4743 108 : runOnMainThread([w = weak(), sip_tr, peerId, deviceId] {
4744 108 : auto account = w.lock();
4745 108 : if (not account)
4746 0 : return;
4747 126 : for (const auto& callId : account->getCallList()) {
4748 18 : auto call = std::dynamic_pointer_cast<SIPCall>(account->getCall(callId));
4749 18 : if (not call or not call->needsNewSipChannel())
4750 14 : continue;
4751 4 : auto* oldTransport = call->getTransport();
4752 4 : if (not oldTransport or oldTransport->deviceId() != deviceId.toString())
4753 0 : continue;
4754 4 : if (oldTransport->peerAccountId() != peerId)
4755 0 : continue;
4756 4 : call->useRecoveredTransport(sip_tr, account->getContactHeader(sip_tr), std::nullopt);
4757 126 : }
4758 108 : });
4759 540 : }
4760 :
4761 : void
4762 216 : JamiAccount::shutdownSIPConnection(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
4763 : const std::string& peerId,
4764 : const DeviceId& deviceId)
4765 : {
4766 216 : std::unique_lock lk(sipConnsMtx_);
4767 216 : SipConnectionKey key(peerId, deviceId);
4768 216 : auto it = sipConns_.find(key);
4769 216 : if (it != sipConns_.end()) {
4770 112 : auto& conns = it->second;
4771 230 : conns.erase(std::remove_if(conns.begin(), conns.end(), [&](auto v) { return v.channel == channel; }),
4772 112 : conns.end());
4773 112 : if (conns.empty()) {
4774 106 : sipConns_.erase(it);
4775 : }
4776 : }
4777 216 : lk.unlock();
4778 : // Shutdown after removal to let the callbacks do stuff if needed
4779 216 : if (channel)
4780 216 : channel->shutdown();
4781 216 : }
4782 :
4783 : std::string_view
4784 55958 : JamiAccount::currentDeviceId() const
4785 : {
4786 55958 : if (!accountManager_ or not accountManager_->getInfo())
4787 1 : return {};
4788 55957 : return accountManager_->getInfo()->deviceId;
4789 : }
4790 :
4791 : std::shared_ptr<TransferManager>
4792 538 : JamiAccount::dataTransfer(const std::string& id)
4793 : {
4794 538 : if (id.empty())
4795 251 : return nonSwarmTransferManager_;
4796 287 : if (auto* cm = convModule())
4797 287 : return cm->dataTransfer(id);
4798 0 : return {};
4799 : }
4800 :
4801 : void
4802 0 : JamiAccount::monitor()
4803 : {
4804 0 : JAMI_DEBUG("[Account {:s}] Monitor connections", getAccountID());
4805 0 : JAMI_DEBUG("[Account {:s}] Using proxy: {:s}", getAccountID(), proxyServerCached_);
4806 :
4807 0 : if (auto* cm = convModule())
4808 0 : cm->monitor();
4809 0 : std::shared_lock lkCM(connManagerMtx_);
4810 0 : if (connectionManager_)
4811 0 : connectionManager_->monitor();
4812 0 : }
4813 :
4814 : std::vector<std::map<std::string, std::string>>
4815 0 : JamiAccount::getConnectionList(const std::string& conversationId)
4816 : {
4817 0 : std::shared_lock lkCM(connManagerMtx_);
4818 0 : if (connectionManager_ && conversationId.empty()) {
4819 0 : return connectionManager_->getConnectionList();
4820 0 : } else if (connectionManager_ && convModule_) {
4821 0 : std::vector<std::map<std::string, std::string>> connectionList;
4822 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4823 0 : for (const auto& deviceId : conv->getDeviceIdList()) {
4824 0 : auto connections = connectionManager_->getConnectionList(deviceId);
4825 0 : connectionList.reserve(connectionList.size() + connections.size());
4826 0 : std::move(connections.begin(), connections.end(), std::back_inserter(connectionList));
4827 0 : }
4828 0 : }
4829 0 : return connectionList;
4830 0 : } else {
4831 0 : return {};
4832 : }
4833 0 : }
4834 :
4835 : std::vector<std::map<std::string, std::string>>
4836 0 : JamiAccount::getConversationConnectivity(const std::string& conversationId)
4837 : {
4838 0 : std::shared_lock lkCM(connManagerMtx_);
4839 0 : if (convModule_) {
4840 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4841 0 : return conv->getConnectivity();
4842 0 : }
4843 : }
4844 0 : return {};
4845 0 : }
4846 :
4847 : std::vector<std::map<std::string, std::string>>
4848 0 : JamiAccount::getConversationTrackedMembers(const std::string& conversationId)
4849 : {
4850 0 : std::shared_lock lkCM(connManagerMtx_);
4851 0 : if (convModule_) {
4852 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4853 0 : return conv->getTrackedMembers();
4854 0 : }
4855 : }
4856 0 : return {};
4857 0 : }
4858 :
4859 : std::vector<std::map<std::string, std::string>>
4860 0 : JamiAccount::getChannelList(const std::string& connectionId)
4861 : {
4862 0 : std::shared_lock lkCM(connManagerMtx_);
4863 0 : if (!connectionManager_)
4864 0 : return {};
4865 0 : return connectionManager_->getChannelList(connectionId);
4866 0 : }
4867 :
4868 : void
4869 15 : JamiAccount::sendFile(const std::string& conversationId,
4870 : const std::filesystem::path& path,
4871 : const std::string& name,
4872 : const std::string& replyTo)
4873 : {
4874 15 : std::error_code ec;
4875 15 : if (!std::filesystem::is_regular_file(path, ec)) {
4876 0 : JAMI_ERROR("Invalid filename '{}'", path);
4877 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(getAccountID(),
4878 : conversationId,
4879 : EVALIDFETCH,
4880 : "Invalid filename.");
4881 0 : return;
4882 : }
4883 :
4884 15 : auto fileSize = std::filesystem::file_size(path, ec);
4885 15 : if (ec || fileSize == static_cast<decltype(fileSize)>(-1)) {
4886 0 : JAMI_ERROR("Negative file size, user probably doesn't have the appropriate permissions for '{}'", path);
4887 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
4888 0 : getAccountID(),
4889 : conversationId,
4890 : EVALIDFETCH,
4891 : "Negative file size, could be due to insufficient file permissions.");
4892 0 : return;
4893 : }
4894 :
4895 : // NOTE: this sendMessage is in a computation thread because
4896 : // sha3sum can take quite some time to computer if the user decide
4897 : // to send a big file
4898 15 : dht::ThreadPool::computation().run([w = weak(), conversationId, path, name, fileSize, replyTo]() {
4899 15 : if (auto shared = w.lock()) {
4900 15 : auto tid = jami::generateUID(shared->rand);
4901 15 : auto displayName = name.empty() ? path.filename().string() : name;
4902 15 : auto sha3sum = fileutils::sha3File(path);
4903 15 : auto commitMessage = CommitMessage::fileSent(displayName, sha3sum, tid, fileSize, replyTo);
4904 :
4905 30 : shared->convModule()->createCommit(
4906 15 : conversationId,
4907 15 : std::move(commitMessage),
4908 : true,
4909 30 : [w, accId = shared->getAccountID(), conversationId, tid, displayName, path, sha3sum, fileSize](
4910 : const std::string& commitId) {
4911 : // Index the sent file so that it can be served to peers even if the
4912 : // original is later removed
4913 15 : auto fileId = getFileId(commitId, std::to_string(tid), displayName);
4914 15 : auto account = w.lock();
4915 15 : auto transferManager = account ? account->dataTransfer(conversationId) : nullptr;
4916 15 : auto indexed = transferManager && transferManager->indexFile(fileId, path, sha3sum, fileSize, true);
4917 45 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4918 15 : accId,
4919 15 : conversationId,
4920 : commitId,
4921 30 : path.string(),
4922 : uint32_t(indexed ? libjami::DataTransferEventCode::created
4923 : : libjami::DataTransferEventCode::invalid));
4924 15 : });
4925 30 : }
4926 15 : });
4927 : }
4928 :
4929 : void
4930 6 : JamiAccount::transferFile(const std::string& conversationId,
4931 : const std::string& path,
4932 : const std::string& deviceId,
4933 : const std::string& fileId,
4934 : const std::string& interactionId,
4935 : size_t start,
4936 : size_t end,
4937 : const std::string& sha3Sum,
4938 : uint64_t lastWriteTime,
4939 : std::function<void()> onFinished)
4940 : {
4941 6 : std::string modified;
4942 6 : if (lastWriteTime != 0) {
4943 12 : modified = fmt::format("&modified={}", lastWriteTime);
4944 : }
4945 12 : auto fid = fileId == "profile.vcf" ? fmt::format("profile.vcf?sha3={}{}", sha3Sum, modified) : fileId;
4946 6 : auto channelName = conversationId.empty()
4947 6 : ? fmt::format("{}profile.vcf?sha3={}{}", DATA_TRANSFER_SCHEME, sha3Sum, modified)
4948 12 : : fmt::format("{}{}/{}/{}", DATA_TRANSFER_SCHEME, conversationId, currentDeviceId(), fid);
4949 6 : std::shared_lock lkCM(connManagerMtx_);
4950 6 : if (!connectionManager_)
4951 0 : return;
4952 12 : connectionManager_->connectDevice(
4953 12 : DeviceId(deviceId),
4954 : channelName,
4955 18 : [this,
4956 : conversationId,
4957 6 : path = std::move(path),
4958 : fileId,
4959 : interactionId,
4960 : start,
4961 : end,
4962 6 : onFinished = std::move(onFinished)](std::shared_ptr<dhtnet::ChannelSocket> socket, const DeviceId&) {
4963 6 : if (!socket)
4964 0 : return;
4965 30 : dht::ThreadPool::io().run([w = weak(),
4966 6 : path = std::move(path),
4967 6 : socket = std::move(socket),
4968 6 : conversationId = std::move(conversationId),
4969 6 : fileId,
4970 6 : interactionId,
4971 : start,
4972 : end,
4973 6 : onFinished = std::move(onFinished)] {
4974 6 : if (auto shared = w.lock())
4975 6 : if (auto dt = shared->dataTransfer(conversationId))
4976 12 : dt->transferFile(socket, fileId, interactionId, path, start, end, std::move(onFinished));
4977 6 : });
4978 : });
4979 12 : }
4980 :
4981 : void
4982 14 : JamiAccount::askForFileChannel(const std::string& conversationId,
4983 : const std::string& deviceId,
4984 : const std::string& interactionId,
4985 : const std::string& fileId,
4986 : size_t start,
4987 : size_t end)
4988 : {
4989 31 : auto tryDevice = [=](const auto& did) {
4990 31 : std::shared_lock lkCM(connManagerMtx_);
4991 31 : if (!connectionManager_)
4992 0 : return;
4993 :
4994 31 : auto channelName = fmt::format("{}{}/{}/{}", DATA_TRANSFER_SCHEME, conversationId, currentDeviceId(), fileId);
4995 31 : if (start != 0 || end != 0) {
4996 10 : channelName += fmt::format("?start={}&end={}", start, end);
4997 : }
4998 : // We can avoid to negotiate new sessions, as the file notif
4999 : // probably came from an online device or last connected device.
5000 93 : connectionManager_->connectDevice(
5001 : did,
5002 : channelName,
5003 93 : [w = weak(),
5004 31 : conversationId,
5005 31 : fileId,
5006 31 : interactionId,
5007 : start](const std::shared_ptr<dhtnet::ChannelSocket>& channel, const DeviceId&) {
5008 31 : if (!channel)
5009 18 : return;
5010 13 : dht::ThreadPool::io().run([w, conversationId, channel, fileId, interactionId, start] {
5011 13 : auto shared = w.lock();
5012 13 : if (!shared)
5013 0 : return;
5014 13 : auto dt = shared->dataTransfer(conversationId);
5015 13 : if (!dt)
5016 0 : return;
5017 13 : if (interactionId.empty())
5018 0 : dt->onIncomingProfile(channel);
5019 : else
5020 13 : dt->onIncomingFileTransfer(fileId, channel, start);
5021 13 : });
5022 : },
5023 : false);
5024 45 : };
5025 :
5026 14 : if (!deviceId.empty()) {
5027 : // Only ask for device
5028 1 : tryDevice(DeviceId(deviceId));
5029 : } else {
5030 : // Only ask for connected devices. For others we will attempt
5031 : // with new peer online
5032 43 : for (const auto& m : convModule()->getConversationMembers(conversationId)) {
5033 150 : accountManager_->forEachDevice(dht::InfoHash(m.at("uri")),
5034 60 : [tryDevice](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
5035 30 : tryDevice(dev->getLongId());
5036 30 : });
5037 13 : }
5038 : }
5039 14 : }
5040 :
5041 : void
5042 237 : JamiAccount::askForProfile(const std::string& conversationId, const std::string& deviceId, const std::string& memberUri)
5043 : {
5044 237 : std::shared_lock lkCM(connManagerMtx_);
5045 237 : if (!connectionManager_)
5046 4 : return;
5047 :
5048 233 : auto channelName = fmt::format("{}{}/profile/{}.vcf", DATA_TRANSFER_SCHEME, conversationId, memberUri);
5049 : // We can avoid to negotiate new sessions, as the file notif
5050 : // probably came from an online device or last connected device.
5051 932 : connectionManager_->connectDevice(
5052 466 : DeviceId(deviceId),
5053 : channelName,
5054 466 : [this, conversationId](const std::shared_ptr<dhtnet::ChannelSocket>& channel, const DeviceId&) {
5055 233 : if (!channel)
5056 5 : return;
5057 228 : dht::ThreadPool::io().run([w = weak(), conversationId, channel] {
5058 228 : if (auto shared = w.lock())
5059 228 : if (auto dt = shared->dataTransfer(conversationId))
5060 908 : dt->onIncomingProfile(channel);
5061 228 : });
5062 : },
5063 : false);
5064 237 : }
5065 :
5066 : void
5067 2701 : JamiAccount::onPeerConnected(const std::string& peerId, bool connected)
5068 : {
5069 2701 : auto isOnline = presenceManager_ && presenceManager_->isOnline(peerId);
5070 4051 : auto newState = connected ? PresenceState::CONNECTED
5071 1350 : : (isOnline ? PresenceState::AVAILABLE : PresenceState::DISCONNECTED);
5072 :
5073 2701 : runOnMainThread([w = weak(), peerId, newState] {
5074 2701 : if (auto sthis = w.lock()) {
5075 2701 : std::lock_guard lock(sthis->presenceStateMtx_);
5076 2701 : auto& state = sthis->presenceState_[peerId];
5077 2701 : if (state != newState) {
5078 2701 : state = newState;
5079 2701 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(sthis->getAccountID(),
5080 2701 : peerId,
5081 : static_cast<int>(newState),
5082 : "");
5083 : }
5084 5402 : }
5085 2701 : });
5086 2701 : }
5087 :
5088 : void
5089 916 : JamiAccount::initConnectionManager()
5090 : {
5091 916 : if (!nonSwarmTransferManager_)
5092 862 : nonSwarmTransferManager_ = std::make_shared<TransferManager>(accountID_,
5093 862 : config().username,
5094 : "",
5095 2586 : dht::crypto::getDerivedRandomEngine(rand));
5096 916 : if (!connectionManager_) {
5097 891 : auto connectionManagerConfig = std::make_shared<dhtnet::ConnectionManager::Config>();
5098 891 : connectionManagerConfig->ioContext = Manager::instance().ioContext();
5099 891 : connectionManagerConfig->dht = dht();
5100 891 : connectionManagerConfig->certStore = certStore_;
5101 891 : connectionManagerConfig->id = identity();
5102 891 : connectionManagerConfig->upnpCtrl = upnpCtrl_;
5103 891 : connectionManagerConfig->turnServer = config().turnServer;
5104 891 : connectionManagerConfig->upnpEnabled = config().upnpEnabled;
5105 891 : connectionManagerConfig->turnServerUserName = config().turnServerUserName;
5106 891 : connectionManagerConfig->turnServerPwd = config().turnServerPwd;
5107 891 : connectionManagerConfig->turnServerRealm = config().turnServerRealm;
5108 891 : connectionManagerConfig->turnEnabled = config().turnEnabled;
5109 891 : connectionManagerConfig->cachePath = cachePath_;
5110 891 : if (Manager::instance().dhtnetLogLevel > 0) {
5111 0 : connectionManagerConfig->logger = logger_;
5112 : }
5113 891 : connectionManagerConfig->factory = Manager::instance().getIceTransportFactory();
5114 891 : connectionManagerConfig->turnCache = turnCache_;
5115 891 : connectionManagerConfig->rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
5116 891 : connectionManagerConfig->legacyMode = dhtnet::LegacyMode::Disabled;
5117 891 : connectionManager_ = std::make_unique<dhtnet::ConnectionManager>(connectionManagerConfig);
5118 1782 : channelHandlers_[Uri::Scheme::SWARM] = std::make_unique<SwarmChannelHandler>(shared(),
5119 1782 : *connectionManager_.get());
5120 1782 : channelHandlers_[Uri::Scheme::GIT] = std::make_unique<ConversationChannelHandler>(shared(),
5121 1782 : *connectionManager_.get());
5122 891 : channelHandlers_[Uri::Scheme::YDOC] = std::make_unique<YdocChannelHandler>(shared(), *connectionManager_.get());
5123 891 : if (jami::Manager::instance().syncOnRegister) {
5124 1782 : channelHandlers_[Uri::Scheme::SYNC] = std::make_unique<SyncChannelHandler>(shared(),
5125 1782 : *connectionManager_.get());
5126 : }
5127 891 : channelHandlers_[Uri::Scheme::DATA_TRANSFER]
5128 1782 : = std::make_unique<TransferChannelHandler>(shared(), *connectionManager_.get());
5129 1782 : channelHandlers_[Uri::Scheme::MESSAGE] = std::make_unique<MessageChannelHandler>(
5130 891 : *connectionManager_.get(),
5131 891 : [this](const auto& cert, std::string& type, const std::string& content) {
5132 62905 : onTextMessage("", cert->issuer->getId().toString(), cert, {{type, content}});
5133 25162 : },
5134 1782 : [w = weak()](const std::string& peer, bool connected) {
5135 2702 : asio::post(*Manager::instance().ioContext(), [w, peer, connected] {
5136 2702 : if (auto acc = w.lock())
5137 2702 : acc->onPeerConnected(peer, connected);
5138 2702 : });
5139 2702 : },
5140 1782 : [w = weak()](const std::string& peer, const DeviceId& device) {
5141 1362 : dht::ThreadPool::io().run([w, peer, device] {
5142 1362 : if (auto acc = w.lock())
5143 1362 : acc->onMessageChannelReady(peer, device);
5144 1362 : });
5145 2253 : });
5146 891 : channelHandlers_[Uri::Scheme::AUTH] = std::make_unique<AuthChannelHandler>(shared(), *connectionManager_.get());
5147 :
5148 891 : if (!serviceManager_)
5149 862 : serviceManager_ = std::make_unique<ServiceManager>(idPath_);
5150 891 : channelHandlers_[Uri::Scheme::SVC_DISCOVERY]
5151 1782 : = std::make_unique<SvcDiscoveryChannelHandler>(shared(), *connectionManager_.get(), cachePath_);
5152 891 : static_cast<SvcDiscoveryChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_DISCOVERY].get())
5153 891 : ->onCacheUpdated([w = weak()](const std::string& peerUri,
5154 : const DeviceId& deviceId,
5155 : const std::vector<svc_protocol::SvcInfo>&) {
5156 1368 : auto self = w.lock();
5157 1368 : if (!self)
5158 0 : return;
5159 : // Re-publish the peer's full service list. The device that just
5160 : // answered is flagged available immediately, even if its DHT
5161 : // presence announcement has not yet been observed. A cache
5162 : // update is always published, even when the peer's last service
5163 : // was removed (empty list), so listeners can clear it.
5164 1368 : auto servicesJson = self->buildPeerServicesJson(peerUri, &deviceId);
5165 2736 : emitSignal<libjami::ServiceSignal::PeerServicesReceived>(
5166 : 0u,
5167 1368 : self->getAccountID(),
5168 : peerUri,
5169 : static_cast<int>(libjami::ServiceSignal::PeerServicesStatus::OK),
5170 4095 : servicesJson.empty() ? "[]" : servicesJson);
5171 1368 : });
5172 891 : serviceManager_->setOnChanged([w = weak()]() {
5173 8 : runOnMainThread([w]() {
5174 8 : auto self = w.lock();
5175 8 : if (!self)
5176 0 : return;
5177 8 : std::shared_lock lk(self->connManagerMtx_);
5178 8 : auto it = self->channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
5179 8 : if (it != self->channelHandlers_.end() && it->second)
5180 8 : static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->broadcastServiceUpdate();
5181 8 : });
5182 8 : });
5183 891 : channelHandlers_[Uri::Scheme::SVC_TUNNEL]
5184 1782 : = std::make_unique<SvcTunnelChannelHandler>(shared(),
5185 891 : *connectionManager_.get(),
5186 2673 : Manager::instance().ioContext());
5187 :
5188 : #if TARGET_OS_IOS
5189 : connectionManager_->oniOSConnected([&](const std::string& connType, dht::InfoHash peer_h) {
5190 : if ((connType == "videoCall" || connType == "audioCall") && jami::Manager::instance().isIOSExtension) {
5191 : bool hasVideo = connType == "videoCall";
5192 : emitSignal<libjami::ConversationSignal::CallConnectionRequest>("", peer_h.toString(), hasVideo);
5193 : return true;
5194 : }
5195 : return false;
5196 : });
5197 : #endif
5198 891 : }
5199 916 : }
5200 :
5201 : void
5202 1323 : JamiAccount::updateUpnpController()
5203 : {
5204 1323 : if (upnpCtrl_ and (not config().upnpEnabled or not isUsable()))
5205 228 : releaseDhtUpnpMapping();
5206 1323 : Account::updateUpnpController();
5207 1323 : if (connectionManager_) {
5208 68 : auto config = connectionManager_->getConfig();
5209 68 : if (config)
5210 68 : config->upnpCtrl = upnpCtrl_;
5211 68 : }
5212 1323 : }
5213 :
5214 : } // namespace jami
|