Line data Source code
1 : /*
2 : * Copyright (C) 2004-2024 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #ifdef HAVE_CONFIG_H
19 : #include "config.h"
20 : #endif
21 :
22 : #include "jamiaccount.h"
23 :
24 : #include "logger.h"
25 :
26 : #include "accountarchive.h"
27 : #include "jami_contact.h"
28 : #include "configkeys.h"
29 : #include "contact_list.h"
30 : #include "archive_account_manager.h"
31 : #include "server_account_manager.h"
32 : #include "jamidht/channeled_transport.h"
33 : #include "conversation_channel_handler.h"
34 : #include "sync_channel_handler.h"
35 : #include "message_channel_handler.h"
36 : #include "transfer_channel_handler.h"
37 : #include "swarm/swarm_channel_handler.h"
38 : #include "jami/media_const.h"
39 :
40 : #include "sip/sdp.h"
41 : #include "sip/sipvoiplink.h"
42 : #include "sip/sipcall.h"
43 : #include "sip/siptransport.h"
44 : #include "connectivity/sip_utils.h"
45 :
46 : #include "uri.h"
47 :
48 : #include "client/ring_signal.h"
49 : #include "jami/call_const.h"
50 : #include "jami/account_const.h"
51 :
52 : #include "system_codec_container.h"
53 :
54 : #include "account_schema.h"
55 : #include "manager.h"
56 : #include "connectivity/utf8_utils.h"
57 : #include "connectivity/ip_utils.h"
58 :
59 : #ifdef ENABLE_PLUGIN
60 : #include "plugin/jamipluginmanager.h"
61 : #include "plugin/chatservicesmanager.h"
62 : #endif
63 :
64 : #ifdef ENABLE_VIDEO
65 : #include "libav_utils.h"
66 : #endif
67 : #include "fileutils.h"
68 : #include "string_utils.h"
69 : #include "archiver.h"
70 : #include "data_transfer.h"
71 :
72 : #include "libdevcrypto/Common.h"
73 : #include "base64.h"
74 : #include "vcard.h"
75 : #include "im/instant_messaging.h"
76 :
77 : #include <dhtnet/ice_transport.h>
78 : #include <dhtnet/ice_transport_factory.h>
79 : #include <dhtnet/upnp/upnp_control.h>
80 : #include <dhtnet/multiplexed_socket.h>
81 : #include <dhtnet/certstore.h>
82 :
83 : #include <opendht/thread_pool.h>
84 : #include <opendht/peer_discovery.h>
85 : #include <opendht/http.h>
86 :
87 : #include <yaml-cpp/yaml.h>
88 :
89 : #include <unistd.h>
90 :
91 : #include <algorithm>
92 : #include <array>
93 : #include <cctype>
94 : #include <charconv>
95 : #include <cinttypes>
96 : #include <cstdarg>
97 : #include <initializer_list>
98 : #include <memory>
99 : #include <regex>
100 : #include <sstream>
101 : #include <string>
102 : #include <system_error>
103 :
104 : using namespace std::placeholders;
105 :
106 : namespace jami {
107 :
108 : constexpr pj_str_t STR_MESSAGE_ID = jami::sip_utils::CONST_PJ_STR("Message-ID");
109 : static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"};
110 : static constexpr const char MIME_TYPE_PIDF[] {"application/pidf+xml"};
111 : static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"};
112 : static constexpr const char DEVICE_ID_PATH[] {"ring_device"};
113 : static constexpr auto TREATED_PATH = "treatedImMessages"sv;
114 :
115 : // Used to pass info to a pjsip callback (pjsip_endpt_send_request)
116 : struct TextMessageCtx
117 : {
118 : std::weak_ptr<JamiAccount> acc;
119 : std::string to;
120 : DeviceId deviceId;
121 : uint64_t id;
122 : bool retryOnTimeout;
123 : std::shared_ptr<dhtnet::ChannelSocket> channel;
124 : bool onlyConnected;
125 : };
126 :
127 : struct VCardMessageCtx
128 : {
129 : std::shared_ptr<std::atomic_int> success;
130 : int total;
131 : std::string path;
132 : };
133 :
134 : namespace Migration {
135 :
136 : enum class State { // Contains all the Migration states
137 : SUCCESS,
138 : INVALID
139 : };
140 :
141 : std::string
142 4 : mapStateNumberToString(const State migrationState)
143 : {
144 : #define CASE_STATE(X) \
145 : case Migration::State::X: \
146 : return #X
147 :
148 4 : switch (migrationState) {
149 0 : CASE_STATE(INVALID);
150 4 : CASE_STATE(SUCCESS);
151 : }
152 0 : return {};
153 : }
154 :
155 : void
156 4 : setState(const std::string& accountID, const State migrationState)
157 : {
158 4 : emitSignal<libjami::ConfigurationSignal::MigrationEnded>(accountID,
159 8 : mapStateNumberToString(migrationState));
160 4 : }
161 :
162 : } // namespace Migration
163 :
164 : struct JamiAccount::BuddyInfo
165 : {
166 : /* the buddy id */
167 : dht::InfoHash id;
168 :
169 : /* number of devices connected on the DHT */
170 : uint32_t devices_cnt {};
171 :
172 : /* The disposable object to update buddy info */
173 : std::future<size_t> listenToken;
174 :
175 34 : BuddyInfo(dht::InfoHash id)
176 34 : : id(id)
177 34 : {}
178 : };
179 :
180 : struct JamiAccount::PendingCall
181 : {
182 : std::chrono::steady_clock::time_point start;
183 : std::shared_ptr<IceTransport> ice_sp;
184 : std::shared_ptr<IceTransport> ice_tcp_sp;
185 : std::weak_ptr<SIPCall> call;
186 : std::future<size_t> listen_key;
187 : dht::InfoHash call_key;
188 : dht::InfoHash from;
189 : dht::InfoHash from_account;
190 : std::shared_ptr<dht::crypto::Certificate> from_cert;
191 : };
192 :
193 : struct JamiAccount::PendingMessage
194 : {
195 : std::set<DeviceId> to;
196 : };
197 :
198 : struct AccountPeerInfo
199 : {
200 : dht::InfoHash accountId;
201 : std::string displayName;
202 0 : MSGPACK_DEFINE(accountId, displayName)
203 : };
204 :
205 : struct JamiAccount::DiscoveredPeer
206 : {
207 : std::string displayName;
208 : std::shared_ptr<Task> cleanupTask;
209 : };
210 :
211 : static constexpr const char* const RING_URI_PREFIX = "ring:";
212 : static constexpr const char* const JAMI_URI_PREFIX = "jami:";
213 : static const auto PROXY_REGEX = std::regex(
214 : "(https?://)?([\\w\\.\\-_\\~]+)(:(\\d+)|:\\[(.+)-(.+)\\])?");
215 : static const std::string PEER_DISCOVERY_JAMI_SERVICE = "jami";
216 : const constexpr auto PEER_DISCOVERY_EXPIRATION = std::chrono::minutes(1);
217 :
218 : using ValueIdDist = std::uniform_int_distribution<dht::Value::Id>;
219 :
220 : static std::string_view
221 36645 : stripPrefix(std::string_view toUrl)
222 : {
223 36645 : auto dhtf = toUrl.find(RING_URI_PREFIX);
224 36633 : if (dhtf != std::string_view::npos) {
225 0 : dhtf = dhtf + 5;
226 : } else {
227 36633 : dhtf = toUrl.find(JAMI_URI_PREFIX);
228 36634 : if (dhtf != std::string_view::npos) {
229 0 : dhtf = dhtf + 5;
230 : } else {
231 36634 : dhtf = toUrl.find("sips:");
232 36633 : dhtf = (dhtf == std::string_view::npos) ? 0 : dhtf + 5;
233 : }
234 : }
235 36633 : while (dhtf < toUrl.length() && toUrl[dhtf] == '/')
236 0 : dhtf++;
237 36628 : return toUrl.substr(dhtf);
238 : }
239 :
240 : static std::string_view
241 36647 : parseJamiUri(std::string_view toUrl)
242 : {
243 36647 : auto sufix = stripPrefix(toUrl);
244 36625 : if (sufix.length() < 40)
245 0 : throw std::invalid_argument("ID must be a Jami infohash");
246 :
247 36625 : const std::string_view toUri = sufix.substr(0, 40);
248 36636 : if (std::find_if_not(toUri.cbegin(), toUri.cend(), ::isxdigit) != toUri.cend())
249 1 : throw std::invalid_argument("ID must be a Jami infohash");
250 36639 : return toUri;
251 : }
252 :
253 : static constexpr const char*
254 4014 : dhtStatusStr(dht::NodeStatus status)
255 : {
256 : return status == dht::NodeStatus::Connected
257 6003 : ? "connected"
258 6003 : : (status == dht::NodeStatus::Connecting ? "connecting" : "disconnected");
259 : }
260 :
261 784 : JamiAccount::JamiAccount(const std::string& accountId)
262 : : SIPAccountBase(accountId)
263 784 : , cachePath_(fileutils::get_cache_dir() / accountId)
264 784 : , dataPath_(cachePath_ / "values")
265 784 : , certStore_ {std::make_unique<dhtnet::tls::CertificateStore>(idPath_, Logger::dhtLogger())}
266 784 : , dht_(new dht::DhtRunner)
267 784 : , treatedMessages_(cachePath_ / TREATED_PATH)
268 784 : , connectionManager_ {}
269 3920 : , nonSwarmTransferManager_()
270 784 : {}
271 :
272 1568 : JamiAccount::~JamiAccount() noexcept
273 : {
274 784 : if (dht_)
275 784 : dht_->join();
276 784 : }
277 :
278 : void
279 936 : JamiAccount::shutdownConnections()
280 : {
281 936 : JAMI_DBG("[Account %s] Shutdown connections", getAccountID().c_str());
282 :
283 936 : decltype(gitServers_) gservers;
284 : {
285 936 : std::lock_guard lk(gitServersMtx_);
286 936 : gservers = std::move(gitServers_);
287 936 : }
288 1374 : for (auto& [_id, gs] : gservers)
289 438 : gs->stop();
290 : {
291 936 : std::lock_guard lk(connManagerMtx_);
292 : // Just move destruction on another thread.
293 1872 : dht::ThreadPool::io().run([conMgr = std::make_shared<decltype(connectionManager_)>(
294 936 : std::move(connectionManager_))] {});
295 936 : connectionManager_.reset();
296 936 : channelHandlers_.clear();
297 936 : }
298 936 : if (convModule_) {
299 724 : convModule_->shutdownConnections();
300 : }
301 :
302 936 : std::lock_guard lk(sipConnsMtx_);
303 936 : sipConns_.clear();
304 936 : }
305 :
306 : void
307 779 : JamiAccount::flush()
308 : {
309 : // Class base method
310 779 : SIPAccountBase::flush();
311 :
312 779 : dhtnet::fileutils::removeAll(cachePath_);
313 779 : dhtnet::fileutils::removeAll(dataPath_);
314 779 : dhtnet::fileutils::removeAll(idPath_, true);
315 779 : }
316 :
317 : std::shared_ptr<SIPCall>
318 97 : JamiAccount::newIncomingCall(const std::string& from,
319 : const std::vector<libjami::MediaMap>& mediaList,
320 : const std::shared_ptr<SipTransport>& sipTransp)
321 : {
322 291 : JAMI_DEBUG("New incoming call from {:s} with {:d} media", from, mediaList.size());
323 :
324 97 : if (sipTransp) {
325 97 : auto call = Manager::instance().callFactory.newSipCall(shared(),
326 : Call::CallType::INCOMING,
327 194 : mediaList);
328 97 : call->setPeerUri(JAMI_URI_PREFIX + from);
329 97 : call->setPeerNumber(from);
330 :
331 97 : call->setSipTransport(sipTransp, getContactHeader(sipTransp));
332 :
333 97 : return call;
334 97 : }
335 :
336 0 : JAMI_ERR("newIncomingCall: unable to find matching call for %s", from.c_str());
337 0 : return nullptr;
338 : }
339 :
340 : std::shared_ptr<Call>
341 111 : JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami::MediaMap>& mediaList)
342 : {
343 111 : auto uri = Uri(toUrl);
344 111 : if (uri.scheme() == Uri::Scheme::SWARM || uri.scheme() == Uri::Scheme::RENDEZVOUS) {
345 : // NOTE: In this case newOutgoingCall can act as "unholdConference" and just attach the
346 : // host to the current hosted conference. So, no call will be returned in that case.
347 22 : return newSwarmOutgoingCallHelper(uri, mediaList);
348 : }
349 :
350 89 : auto& manager = Manager::instance();
351 89 : std::shared_ptr<SIPCall> call;
352 :
353 : // SIP allows sending empty invites, this use case is not used with Jami accounts.
354 89 : if (not mediaList.empty()) {
355 33 : call = manager.callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
356 : } else {
357 56 : JAMI_WARN("Media list is empty, setting a default list");
358 112 : call = manager.callFactory.newSipCall(shared(),
359 : Call::CallType::OUTGOING,
360 112 : MediaAttribute::mediaAttributesToMediaMaps(
361 168 : createDefaultMediaList(isVideoEnabled())));
362 : }
363 :
364 89 : if (not call)
365 0 : return {};
366 :
367 89 : connectionManager_->getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) {
368 89 : if (call->isIceEnabled()) {
369 89 : if (not call->createIceMediaTransport(false)
370 178 : or not call->initIceMediaTransport(true,
371 : std::forward<dhtnet::IceTransportOptions>(opts))) {
372 0 : return;
373 : }
374 : }
375 89 : auto shared = w.lock();
376 89 : if (!shared)
377 0 : return;
378 89 : JAMI_DBG() << "New outgoing call with " << uri.toString();
379 89 : call->setPeerNumber(uri.authority());
380 89 : call->setPeerUri(uri.toString());
381 :
382 89 : shared->newOutgoingCallHelper(call, uri);
383 89 : });
384 :
385 89 : return call;
386 111 : }
387 :
388 : void
389 89 : JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri)
390 : {
391 89 : JAMI_DBG() << this << "Calling peer " << uri.authority();
392 : try {
393 89 : startOutgoingCall(call, uri.authority());
394 0 : } catch (...) {
395 : #if HAVE_RINGNS
396 0 : auto suffix = stripPrefix(uri.toString());
397 0 : NameDirectory::lookupUri(suffix,
398 0 : config().nameServer,
399 0 : [wthis_ = weak(), call](const std::string& result,
400 : NameDirectory::Response response) {
401 : // we may run inside an unknown thread, but following code must
402 : // be called in main thread
403 0 : runOnMainThread([wthis_, result, response, call]() {
404 0 : if (response != NameDirectory::Response::found) {
405 0 : call->onFailure(EINVAL);
406 0 : return;
407 : }
408 0 : if (auto sthis = wthis_.lock()) {
409 : try {
410 0 : sthis->startOutgoingCall(call, result);
411 0 : } catch (...) {
412 0 : call->onFailure(ENOENT);
413 0 : }
414 : } else {
415 0 : call->onFailure();
416 0 : }
417 : });
418 0 : });
419 : #else
420 : call->onFailure(ENOENT);
421 : #endif
422 0 : }
423 89 : }
424 :
425 : std::shared_ptr<SIPCall>
426 22 : JamiAccount::newSwarmOutgoingCallHelper(const Uri& uri,
427 : const std::vector<libjami::MediaMap>& mediaList)
428 : {
429 66 : JAMI_DEBUG("[Account {}] Calling conversation {}", getAccountID(), uri.authority());
430 : return convModule()->call(
431 : uri.authority(),
432 : mediaList,
433 66 : [this, uri](const auto& accountUri, const auto& deviceId, const auto& call) {
434 11 : if (!call)
435 0 : return;
436 11 : std::unique_lock lkSipConn(sipConnsMtx_);
437 21 : for (auto& [key, value] : sipConns_) {
438 10 : if (key.first != accountUri || key.second != deviceId)
439 10 : continue;
440 0 : if (value.empty())
441 0 : continue;
442 0 : auto& sipConn = value.back();
443 :
444 0 : if (!sipConn.channel) {
445 0 : JAMI_WARN(
446 : "A SIP transport exists without Channel, this is a bug. Please report");
447 0 : continue;
448 : }
449 :
450 0 : auto transport = sipConn.transport;
451 0 : if (!transport or !sipConn.channel)
452 0 : continue;
453 0 : call->setState(Call::ConnectionState::PROGRESSING);
454 :
455 0 : auto remoted_address = sipConn.channel->getRemoteAddress();
456 : try {
457 0 : onConnectedOutgoingCall(call, uri.authority(), remoted_address);
458 0 : return;
459 0 : } catch (const VoipLinkException&) {
460 : // In this case, the main scenario is that SIPStartCall failed because
461 : // the ICE is dead and the TLS session didn't send any packet on that dead
462 : // link (connectivity change, killed by the operating system, etc)
463 : // Here, we don't need to do anything, the TLS will fail and will delete
464 : // the cached transport
465 0 : continue;
466 : }
467 : }
468 11 : lkSipConn.unlock();
469 : {
470 11 : std::lock_guard lkP(pendingCallsMutex_);
471 11 : pendingCalls_[deviceId].emplace_back(call);
472 11 : }
473 :
474 : // Else, ask for a channel (for future calls/text messages)
475 11 : auto type = call->hasVideo() ? "videoCall" : "audioCall";
476 33 : JAMI_WARNING("[call {}] No channeled socket with this peer. Send request",
477 : call->getCallId());
478 11 : requestSIPConnection(accountUri, deviceId, type, true, call);
479 33 : });
480 : }
481 :
482 : void
483 10 : JamiAccount::handleIncomingConversationCall(const std::string& callId,
484 : const std::string& destination)
485 : {
486 10 : auto split = jami::split_string(destination, '/');
487 10 : if (split.size() != 4)
488 0 : return;
489 10 : auto conversationId = std::string(split[0]);
490 10 : auto accountUri = std::string(split[1]);
491 10 : auto deviceId = std::string(split[2]);
492 10 : auto confId = std::string(split[3]);
493 :
494 10 : if (getUsername() != accountUri || currentDeviceId() != deviceId)
495 0 : return;
496 :
497 : // Avoid concurrent checks in this part
498 10 : std::lock_guard lk(rdvMtx_);
499 10 : auto isNotHosting = !convModule()->isHosting(conversationId, confId);
500 10 : if (confId == "0") {
501 1 : auto currentCalls = convModule()->getActiveCalls(conversationId);
502 1 : if (!currentCalls.empty()) {
503 0 : confId = currentCalls[0]["id"];
504 0 : isNotHosting = false;
505 : } else {
506 1 : confId = callId;
507 3 : JAMI_DEBUG("No active call to join, create conference");
508 : }
509 1 : }
510 10 : auto preferences = convModule()->getConversationPreferences(conversationId);
511 10 : auto canHost = true;
512 : #if defined(__ANDROID__) || defined(__APPLE__)
513 : // By default, mobile devices SHOULD NOT host conferences.
514 : canHost = false;
515 : #endif
516 10 : auto itPref = preferences.find(ConversationPreferences::HOST_CONFERENCES);
517 10 : if (itPref != preferences.end()) {
518 0 : canHost = itPref->second == TRUE_STR;
519 : }
520 :
521 10 : auto call = getCall(callId);
522 10 : if (!call) {
523 0 : JAMI_ERROR("Call {} not found", callId);
524 0 : return;
525 : }
526 :
527 10 : if (isNotHosting && !canHost) {
528 0 : JAMI_DEBUG("Request for hosting a conference declined");
529 0 : Manager::instance().hangupCall(getAccountID(), callId);
530 0 : return;
531 : }
532 :
533 10 : std::shared_ptr<Conference> conf;
534 10 : std::vector<libjami::MediaMap> currentMediaList;
535 10 : if (!isNotHosting) {
536 7 : conf = getConference(confId);
537 7 : if (!conf) {
538 0 : JAMI_ERROR("Conference {} not found", confId);
539 0 : return;
540 : }
541 20 : for (const auto& m : conf->currentMediaList()) {
542 26 : if (m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE)
543 13 : == libjami::Media::MediaAttributeValue::VIDEO
544 39 : && !call->hasVideo()) {
545 0 : continue;
546 : }
547 13 : currentMediaList.emplace_back(m);
548 7 : }
549 : }
550 10 : if (currentMediaList.empty()) {
551 6 : currentMediaList = MediaAttribute::mediaAttributesToMediaMaps(
552 9 : createDefaultMediaList(call->hasVideo(), true));
553 : }
554 10 : Manager::instance().answerCall(*call, currentMediaList);
555 :
556 10 : if (isNotHosting) {
557 9 : JAMI_DEBUG("Creating conference for swarm {} with ID {}", conversationId, confId);
558 : // Create conference and host it.
559 3 : convModule()->hostConference(conversationId, confId, callId);
560 : } else {
561 21 : JAMI_DEBUG("Adding participant {} for swarm {} with ID {}", callId, conversationId, confId);
562 7 : Manager::instance().addAudio(*call);
563 7 : conf->addSubCall(callId);
564 14 : emitSignal<libjami::CallSignal::ConferenceChanged>(getAccountID(),
565 7 : conf->getConfId(),
566 : conf->getStateStr());
567 : }
568 10 : }
569 :
570 : std::shared_ptr<SIPCall>
571 178 : JamiAccount::createSubCall(const std::shared_ptr<SIPCall>& mainCall)
572 : {
573 178 : auto mediaList = MediaAttribute::mediaAttributesToMediaMaps(mainCall->getMediaAttributeList());
574 356 : return Manager::instance().callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
575 178 : }
576 :
577 : void
578 89 : JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std::string& toUri)
579 : {
580 89 : if (not accountManager_ or not dht_) {
581 0 : call->onFailure(ENETDOWN);
582 0 : return;
583 : }
584 :
585 : // TODO: for now, we automatically trust all explicitly called peers
586 89 : setCertificateStatus(toUri, dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
587 :
588 89 : call->setState(Call::ConnectionState::TRYING);
589 89 : std::weak_ptr<SIPCall> wCall = call;
590 :
591 : #if HAVE_RINGNS
592 89 : accountManager_->lookupAddress(toUri,
593 89 : [wCall](const std::string& result,
594 : const NameDirectory::Response& response) {
595 89 : if (response == NameDirectory::Response::found)
596 1 : if (auto call = wCall.lock()) {
597 1 : call->setPeerRegisteredName(result);
598 1 : }
599 89 : });
600 : #endif
601 :
602 89 : dht::InfoHash peer_account(toUri);
603 :
604 : // Call connected devices
605 89 : std::set<DeviceId> devices;
606 89 : std::unique_lock lkSipConn(sipConnsMtx_);
607 : // NOTE: dummyCall is a call used to avoid to mark the call as failed if the
608 : // cached connection is failing with ICE (close event still not detected).
609 89 : auto dummyCall = createSubCall(call);
610 :
611 89 : call->addSubCall(*dummyCall);
612 89 : dummyCall->setIceMedia(call->getIceMedia());
613 : auto sendRequest =
614 89 : [this, wCall, toUri, dummyCall = std::move(dummyCall)](const DeviceId& deviceId,
615 415 : bool eraseDummy) {
616 172 : if (eraseDummy) {
617 : // Mark the temp call as failed to stop the main call if necessary
618 89 : if (dummyCall)
619 89 : dummyCall->onFailure(static_cast<int>(std::errc::no_such_device_or_address));
620 89 : return;
621 : }
622 83 : auto call = wCall.lock();
623 83 : if (not call)
624 0 : return;
625 83 : auto state = call->getConnectionState();
626 83 : if (state != Call::ConnectionState::PROGRESSING
627 83 : and state != Call::ConnectionState::TRYING)
628 0 : return;
629 :
630 83 : auto dev_call = createSubCall(call);
631 83 : dev_call->setPeerNumber(call->getPeerNumber());
632 83 : dev_call->setState(Call::ConnectionState::TRYING);
633 83 : call->addStateListener(
634 245 : [w = weak(), deviceId](Call::CallState, Call::ConnectionState state, int) {
635 245 : if (state != Call::ConnectionState::PROGRESSING
636 83 : and state != Call::ConnectionState::TRYING) {
637 83 : if (auto shared = w.lock())
638 83 : shared->callConnectionClosed(deviceId, true);
639 83 : return false;
640 : }
641 162 : return true;
642 : });
643 83 : call->addSubCall(*dev_call);
644 83 : dev_call->setIceMedia(call->getIceMedia());
645 : {
646 83 : std::lock_guard lk(pendingCallsMutex_);
647 83 : pendingCalls_[deviceId].emplace_back(dev_call);
648 83 : }
649 :
650 249 : JAMI_WARNING("[call {}] No channeled socket with this peer. Send request",
651 : call->getCallId());
652 : // Else, ask for a channel (for future calls/text messages)
653 83 : auto type = call->hasVideo() ? "videoCall" : "audioCall";
654 83 : requestSIPConnection(toUri, deviceId, type, true, dev_call);
655 261 : };
656 :
657 89 : std::vector<std::shared_ptr<dhtnet::ChannelSocket>> channels;
658 143 : for (auto& [key, value] : sipConns_) {
659 54 : if (key.first != toUri)
660 48 : continue;
661 6 : if (value.empty())
662 0 : continue;
663 6 : auto& sipConn = value.back();
664 :
665 6 : if (!sipConn.channel) {
666 0 : JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
667 0 : continue;
668 0 : }
669 :
670 6 : auto transport = sipConn.transport;
671 6 : auto remote_address = sipConn.channel->getRemoteAddress();
672 6 : if (!transport or !remote_address)
673 0 : continue;
674 :
675 6 : channels.emplace_back(sipConn.channel);
676 :
677 18 : JAMI_WARNING("[call {}] A channeled socket is detected with this peer.", call->getCallId());
678 :
679 6 : auto dev_call = createSubCall(call);
680 6 : dev_call->setPeerNumber(call->getPeerNumber());
681 6 : dev_call->setSipTransport(transport, getContactHeader(transport));
682 6 : call->addSubCall(*dev_call);
683 6 : dev_call->setIceMedia(call->getIceMedia());
684 :
685 : // Set the call in PROGRESSING State because the ICE session
686 : // is already ready. Note that this line should be after
687 : // addSubcall() to change the state of the main call
688 : // and avoid to get an active call in a TRYING state.
689 6 : dev_call->setState(Call::ConnectionState::PROGRESSING);
690 :
691 : {
692 6 : std::lock_guard lk(onConnectionClosedMtx_);
693 6 : onConnectionClosed_[key.second] = sendRequest;
694 6 : }
695 :
696 6 : call->addStateListener(
697 18 : [w = weak(), deviceId = key.second](Call::CallState, Call::ConnectionState state, int) {
698 18 : if (state != Call::ConnectionState::PROGRESSING
699 6 : and state != Call::ConnectionState::TRYING) {
700 6 : if (auto shared = w.lock())
701 6 : shared->callConnectionClosed(deviceId, true);
702 6 : return false;
703 : }
704 12 : return true;
705 : });
706 :
707 : try {
708 6 : onConnectedOutgoingCall(dev_call, toUri, remote_address);
709 0 : } catch (const VoipLinkException&) {
710 : // In this case, the main scenario is that SIPStartCall failed because
711 : // the ICE is dead and the TLS session didn't send any packet on that dead
712 : // link (connectivity change, killed by the os, etc)
713 : // Here, we don't need to do anything, the TLS will fail and will delete
714 : // the cached transport
715 0 : continue;
716 0 : }
717 6 : devices.emplace(key.second);
718 6 : }
719 :
720 89 : lkSipConn.unlock();
721 : // Note: Send beacon can destroy the socket (if storing last occurence of shared_ptr)
722 : // causing sipConn to be destroyed. So, do it while sipConns_ not locked.
723 95 : for (const auto& channel : channels)
724 6 : channel->sendBeacon();
725 :
726 : // Find listening devices for this account
727 178 : accountManager_->forEachDevice(
728 : peer_account,
729 89 : [this, devices = std::move(devices), sendRequest](
730 166 : const std::shared_ptr<dht::crypto::PublicKey>& dev) {
731 : // Test if already sent via a SIP transport
732 89 : auto deviceId = dev->getLongId();
733 89 : if (devices.find(deviceId) != devices.end())
734 6 : return;
735 : {
736 83 : std::lock_guard lk(onConnectionClosedMtx_);
737 83 : onConnectionClosed_[deviceId] = sendRequest;
738 83 : }
739 83 : sendRequest(deviceId, false);
740 : },
741 89 : [wCall](bool ok) {
742 89 : if (not ok) {
743 1 : if (auto call = wCall.lock()) {
744 3 : JAMI_WARNING("[call:{}] No devices found", call->getCallId());
745 : // Note: if a P2P connection exists, the call will be at least in CONNECTING
746 1 : if (call->getConnectionState() == Call::ConnectionState::TRYING)
747 1 : call->onFailure(static_cast<int>(std::errc::no_such_device_or_address));
748 1 : }
749 : }
750 89 : });
751 89 : }
752 :
753 : void
754 97 : JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call,
755 : const std::string& to_id,
756 : dhtnet::IpAddr target)
757 : {
758 97 : if (!call)
759 0 : return;
760 291 : JAMI_LOG("[call:{}] Outgoing call connected to {}", call->getCallId(), to_id);
761 :
762 97 : const auto localAddress = dhtnet::ip_utils::getInterfaceAddr(getLocalInterface(),
763 97 : target.getFamily());
764 :
765 97 : dhtnet::IpAddr addrSdp = getPublishedSameasLocal()
766 : ? localAddress
767 97 : : connectionManager_->getPublishedIpAddress(target.getFamily());
768 :
769 : // fallback on local address
770 97 : if (not addrSdp)
771 0 : addrSdp = localAddress;
772 :
773 : // Initialize the session using ULAW as default codec in case of early media
774 : // The session should be ready to receive media once the first INVITE is sent, before
775 : // the session initialization is completed
776 97 : if (!getSystemCodecContainer()->searchCodecByName("PCMA", jami::MEDIA_AUDIO))
777 0 : JAMI_WARNING("[call:{}] Unable to instantiate codec for early media", call->getCallId());
778 :
779 : // Building the local SDP offer
780 97 : auto& sdp = call->getSDP();
781 :
782 97 : sdp.setPublishedIP(addrSdp);
783 :
784 97 : auto mediaAttrList = call->getMediaAttributeList();
785 :
786 97 : if (mediaAttrList.empty()) {
787 0 : JAMI_ERROR("[call:{}] No media. Abort!", call->getCallId());
788 0 : return;
789 : }
790 :
791 97 : if (not sdp.createOffer(mediaAttrList)) {
792 0 : JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call",
793 : call->getCallId());
794 0 : return;
795 : }
796 :
797 : // Note: pj_ice_strans_create can call onComplete in the same thread
798 : // This means that iceMutex_ in IceTransport can be locked when onInitDone is called
799 : // So, we need to run the call creation in the main thread
800 : // Also, we do not directly call SIPStartCall before receiving onInitDone, because
801 : // there is an inside waitForInitialization that can block the thread.
802 : // Note: avoid runMainThread as SIPStartCall use transportMutex
803 97 : dht::ThreadPool::io().run([w = weak(), call = std::move(call), target] {
804 97 : auto account = w.lock();
805 97 : if (not account)
806 0 : return;
807 :
808 97 : if (not account->SIPStartCall(*call, target)) {
809 0 : JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call",
810 : call->getCallId());
811 : }
812 97 : });
813 97 : }
814 :
815 : bool
816 97 : JamiAccount::SIPStartCall(SIPCall& call, const dhtnet::IpAddr& target)
817 : {
818 291 : JAMI_LOG("[call:{}] Start SIP call", call.getCallId());
819 :
820 97 : if (call.isIceEnabled())
821 97 : call.addLocalIceAttributes();
822 :
823 194 : std::string toUri(getToUri(call.getPeerNumber() + "@"
824 291 : + target.toString(true))); // expecting a fully well formed sip uri
825 :
826 97 : pj_str_t pjTo = sip_utils::CONST_PJ_STR(toUri);
827 :
828 : // Create the from header
829 97 : std::string from(getFromUri());
830 97 : pj_str_t pjFrom = sip_utils::CONST_PJ_STR(from);
831 :
832 97 : std::string targetStr = getToUri(target.toString(true));
833 97 : pj_str_t pjTarget = sip_utils::CONST_PJ_STR(targetStr);
834 :
835 97 : auto contact = call.getContactHeader();
836 97 : auto pjContact = sip_utils::CONST_PJ_STR(contact);
837 :
838 291 : JAMI_LOG("[call:{}] Contact header: {} / {} -> {} / {}",
839 : call.getCallId(),
840 : contact,
841 : from,
842 : toUri,
843 : targetStr);
844 :
845 97 : auto local_sdp = call.getSDP().getLocalSdpSession();
846 97 : pjsip_dialog* dialog {nullptr};
847 97 : pjsip_inv_session* inv {nullptr};
848 97 : if (!CreateClientDialogAndInvite(&pjFrom, &pjContact, &pjTo, &pjTarget, local_sdp, &dialog, &inv))
849 0 : return false;
850 :
851 97 : inv->mod_data[link_.getModId()] = &call;
852 97 : call.setInviteSession(inv);
853 :
854 : pjsip_tx_data* tdata;
855 :
856 97 : if (pjsip_inv_invite(call.inviteSession_.get(), &tdata) != PJ_SUCCESS) {
857 0 : JAMI_ERROR("[call:{}] Unable to initialize invite", call.getCallId());
858 0 : return false;
859 : }
860 :
861 : pjsip_tpselector tp_sel;
862 97 : tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
863 97 : if (!call.getTransport()) {
864 0 : JAMI_ERROR("[call:{}] Unable to get transport", call.getCallId());
865 0 : return false;
866 : }
867 97 : tp_sel.u.transport = call.getTransport()->get();
868 97 : if (pjsip_dlg_set_transport(dialog, &tp_sel) != PJ_SUCCESS) {
869 0 : JAMI_ERROR("[call:{}] Unable to associate transport for invite session dialog",
870 : call.getCallId());
871 0 : return false;
872 : }
873 :
874 291 : JAMI_LOG("[call:{}] Sending SIP invite", call.getCallId());
875 :
876 : // Add user-agent header
877 97 : sip_utils::addUserAgentHeader(getUserAgentName(), tdata);
878 :
879 97 : if (pjsip_inv_send_msg(call.inviteSession_.get(), tdata) != PJ_SUCCESS) {
880 0 : JAMI_ERROR("[call:{}] Unable to send invite message", call.getCallId());
881 0 : return false;
882 : }
883 :
884 97 : call.setState(Call::CallState::ACTIVE, Call::ConnectionState::PROGRESSING);
885 97 : return true;
886 97 : }
887 :
888 : void
889 2579 : JamiAccount::saveConfig() const
890 : {
891 : try {
892 2579 : YAML::Emitter accountOut;
893 2579 : config().serialize(accountOut);
894 2579 : auto accountConfig = config().path / "config.yml";
895 2579 : std::lock_guard lock(dhtnet::fileutils::getFileLock(accountConfig));
896 2579 : std::ofstream fout(accountConfig);
897 2579 : fout.write(accountOut.c_str(), accountOut.size());
898 7737 : JAMI_LOG("Saved account config to {}", accountConfig);
899 2579 : } catch (const std::exception& e) {
900 0 : JAMI_ERROR("Error saving account config: {}", e.what());
901 0 : }
902 2579 : }
903 :
904 : void
905 799 : JamiAccount::loadConfig()
906 : {
907 799 : SIPAccountBase::loadConfig();
908 799 : registeredName_ = config().registeredName;
909 799 : if (accountManager_)
910 20 : accountManager_->setAccountDeviceName(config().deviceName);
911 799 : if (connectionManager_) {
912 16 : if (auto c = connectionManager_->getConfig()) {
913 : // Update connectionManager's config
914 16 : c->upnpEnabled = config().upnpEnabled;
915 16 : c->turnEnabled = config().turnEnabled;
916 16 : c->turnServer = config().turnServer;
917 16 : c->turnServerUserName = config().turnServerUserName;
918 16 : c->turnServerPwd = config().turnServerPwd;
919 16 : c->turnServerRealm = config().turnServerRealm;
920 16 : }
921 : }
922 799 : if (config().proxyEnabled) {
923 : try {
924 0 : auto str = fileutils::loadCacheTextFile(cachePath_ / "dhtproxy",
925 0 : std::chrono::hours(24 * 7));
926 0 : std::string err;
927 0 : Json::Value root;
928 0 : Json::CharReaderBuilder rbuilder;
929 0 : auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
930 0 : if (reader->parse(str.data(), str.data() + str.size(), &root, &err)) {
931 0 : proxyServerCached_ = root[getProxyConfigKey()].asString();
932 : }
933 0 : } catch (const std::exception& e) {
934 0 : JAMI_LOG("[Account {}] Unable to load proxy URL from cache: {}",
935 : getAccountID(),
936 : e.what());
937 0 : proxyServerCached_.clear();
938 0 : }
939 : } else {
940 799 : proxyServerCached_.clear();
941 799 : std::error_code ec;
942 799 : std::filesystem::remove(cachePath_ / "dhtproxy", ec);
943 : }
944 799 : auto credentials = consumeConfigCredentials();
945 799 : loadAccount(credentials.archive_password_scheme,
946 : credentials.archive_password,
947 : credentials.archive_pin,
948 : credentials.archive_path);
949 799 : }
950 :
951 : bool
952 4 : JamiAccount::changeArchivePassword(const std::string& password_old, const std::string& password_new)
953 : {
954 : try {
955 4 : if (!accountManager_->changePassword(password_old, password_new)) {
956 6 : JAMI_ERROR("[Account {}] Unable to change archive password", getAccountID());
957 2 : return false;
958 : }
959 2 : editConfig([&](JamiAccountConfig& config) {
960 2 : config.archiveHasPassword = not password_new.empty();
961 2 : });
962 0 : } catch (const std::exception& ex) {
963 0 : JAMI_ERROR("[Account {}] Unable to change archive password: {}", getAccountID(), ex.what());
964 0 : if (password_old.empty()) {
965 0 : editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = true; });
966 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(),
967 0 : getAccountDetails());
968 : }
969 0 : return false;
970 0 : }
971 2 : if (password_old != password_new)
972 2 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(),
973 4 : getAccountDetails());
974 2 : return true;
975 : }
976 :
977 : bool
978 0 : JamiAccount::isPasswordValid(const std::string& password)
979 : {
980 0 : return accountManager_ and accountManager_->isPasswordValid(password);
981 : }
982 :
983 : std::vector<uint8_t>
984 0 : JamiAccount::getPasswordKey(const std::string& password)
985 : {
986 0 : return accountManager_ ? accountManager_->getPasswordKey(password) : std::vector<uint8_t>();
987 : }
988 :
989 : void
990 2 : JamiAccount::addDevice(const std::string& password)
991 : {
992 2 : if (not accountManager_) {
993 0 : emitSignal<libjami::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 2, "");
994 0 : return;
995 : }
996 2 : accountManager_
997 2 : ->addDevice(password, [this](AccountManager::AddDeviceResult result, std::string pin) {
998 2 : switch (result) {
999 1 : case AccountManager::AddDeviceResult::SUCCESS_SHOW_PIN:
1000 1 : emitSignal<libjami::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 0, pin);
1001 1 : break;
1002 1 : case AccountManager::AddDeviceResult::ERROR_CREDENTIALS:
1003 1 : emitSignal<libjami::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 1, "");
1004 1 : break;
1005 0 : case AccountManager::AddDeviceResult::ERROR_NETWORK:
1006 0 : emitSignal<libjami::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 2, "");
1007 0 : break;
1008 : }
1009 2 : });
1010 : }
1011 :
1012 : bool
1013 38 : JamiAccount::exportArchive(const std::string& destinationPath,
1014 : std::string_view scheme,
1015 : const std::string& password)
1016 : {
1017 38 : if (auto manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
1018 38 : return manager->exportArchive(destinationPath, scheme, password);
1019 : }
1020 0 : return false;
1021 : }
1022 :
1023 : bool
1024 2 : JamiAccount::setValidity(std::string_view scheme,
1025 : const std::string& pwd,
1026 : const dht::InfoHash& id,
1027 : int64_t validity)
1028 : {
1029 2 : if (auto manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
1030 2 : if (manager->setValidity(scheme, pwd, id_, id, validity)) {
1031 2 : saveIdentity(id_, idPath_, DEVICE_ID_PATH);
1032 2 : return true;
1033 : }
1034 : }
1035 0 : return false;
1036 : }
1037 :
1038 : void
1039 4 : JamiAccount::forceReloadAccount()
1040 : {
1041 4 : editConfig([&](JamiAccountConfig& conf) {
1042 4 : conf.receipt.clear();
1043 4 : conf.receiptSignature.clear();
1044 4 : });
1045 4 : loadAccount();
1046 4 : }
1047 :
1048 : void
1049 2 : JamiAccount::unlinkConversations(const std::set<std::string>& removed)
1050 : {
1051 2 : std::lock_guard lock(configurationMutex_);
1052 2 : if (auto info = accountManager_->getInfo()) {
1053 2 : auto contacts = info->contacts->getContacts();
1054 4 : for (auto& [id, c] : contacts) {
1055 2 : if (removed.find(c.conversationId) != removed.end()) {
1056 1 : info->contacts->updateConversation(id, "");
1057 3 : JAMI_WARNING(
1058 : "[Account {}] Detected removed conversation ({}) in contact details for {}",
1059 : getAccountID(),
1060 : c.conversationId,
1061 : id.toString());
1062 : }
1063 : }
1064 2 : }
1065 2 : }
1066 :
1067 : bool
1068 1028 : JamiAccount::isValidAccountDevice(const dht::crypto::Certificate& cert) const
1069 : {
1070 1028 : if (accountManager_) {
1071 1028 : if (auto info = accountManager_->getInfo()) {
1072 1028 : if (info->contacts)
1073 1028 : return info->contacts->isValidAccountDevice(cert).isValid();
1074 : }
1075 : }
1076 0 : return false;
1077 : }
1078 :
1079 : bool
1080 3 : JamiAccount::revokeDevice(const std::string& device,
1081 : std::string_view scheme,
1082 : const std::string& password)
1083 : {
1084 3 : if (not accountManager_)
1085 0 : return false;
1086 6 : return accountManager_->revokeDevice(
1087 6 : device, scheme, password, [this, device](AccountManager::RevokeDeviceResult result) {
1088 3 : emitSignal<libjami::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(),
1089 3 : device,
1090 : static_cast<int>(
1091 : result));
1092 6 : });
1093 : return true;
1094 : }
1095 :
1096 : std::pair<std::string, std::string>
1097 785 : JamiAccount::saveIdentity(const dht::crypto::Identity id,
1098 : const std::filesystem::path& path,
1099 : const std::string& name)
1100 : {
1101 1570 : auto names = std::make_pair(name + ".key", name + ".crt");
1102 785 : if (id.first)
1103 785 : fileutils::saveFile(path / names.first, id.first->serialize(), 0600);
1104 785 : if (id.second)
1105 785 : fileutils::saveFile(path / names.second, id.second->getPacked(), 0600);
1106 785 : return names;
1107 0 : }
1108 :
1109 : // must be called while configurationMutex_ is locked
1110 : void
1111 803 : JamiAccount::loadAccount(const std::string& archive_password_scheme,
1112 : const std::string& archive_password,
1113 : const std::string& archive_pin,
1114 : const std::string& archive_path)
1115 : {
1116 803 : if (registrationState_ == RegistrationState::INITIALIZING)
1117 4 : return;
1118 :
1119 2397 : JAMI_DEBUG("[Account {:s}] Loading account", getAccountID());
1120 : AccountManager::OnChangeCallback callbacks {
1121 337 : [this](const std::string& uri, bool confirmed) {
1122 170 : if (!id_.first)
1123 3 : return;
1124 167 : if (jami::Manager::instance().syncOnRegister) {
1125 167 : dht::ThreadPool::io().run([w = weak(), uri, confirmed] {
1126 167 : if (auto shared = w.lock()) {
1127 167 : if (auto cm = shared->convModule(true)) {
1128 167 : auto activeConv = cm->getOneToOneConversation(uri);
1129 167 : if (!activeConv.empty())
1130 166 : cm->bootstrap(activeConv);
1131 167 : }
1132 167 : emitSignal<libjami::ConfigurationSignal::ContactAdded>(shared->getAccountID(),
1133 167 : uri,
1134 : confirmed);
1135 167 : }
1136 167 : });
1137 : }
1138 : },
1139 52 : [this](const std::string& uri, bool banned) {
1140 26 : if (!id_.first)
1141 0 : return;
1142 26 : dht::ThreadPool::io().run([w = weak(), uri, banned] {
1143 26 : if (auto shared = w.lock()) {
1144 : // Erase linked conversation's requests
1145 26 : if (auto convModule = shared->convModule(true))
1146 26 : convModule->removeContact(uri, banned);
1147 : // Remove current connections with contact
1148 : // Note: if contact is ourself, we don't close the connection
1149 : // because it's used for syncing other conversations.
1150 26 : if (shared->connectionManager_ && uri != shared->getUsername()) {
1151 25 : shared->connectionManager_->closeConnectionsWith(uri);
1152 : }
1153 : // Update client.
1154 26 : emitSignal<libjami::ConfigurationSignal::ContactRemoved>(shared->getAccountID(),
1155 26 : uri,
1156 : banned);
1157 26 : }
1158 26 : });
1159 : },
1160 83 : [this](const std::string& uri,
1161 : const std::string& conversationId,
1162 : const std::vector<uint8_t>& payload,
1163 166 : time_t received) {
1164 83 : if (!id_.first)
1165 0 : return;
1166 83 : dht::ThreadPool::io().run([w = weak(), uri, conversationId, payload, received] {
1167 83 : if (auto shared = w.lock()) {
1168 83 : shared->clearProfileCache(uri);
1169 83 : if (conversationId.empty()) {
1170 : // Old path
1171 0 : emitSignal<libjami::ConfigurationSignal::IncomingTrustRequest>(
1172 0 : shared->getAccountID(), conversationId, uri, payload, received);
1173 0 : return;
1174 : }
1175 : // Here account can be initializing
1176 83 : if (auto cm = shared->convModule(true)) {
1177 83 : auto activeConv = cm->getOneToOneConversation(uri);
1178 83 : if (activeConv != conversationId)
1179 77 : cm->onTrustRequest(uri, conversationId, payload, received);
1180 83 : }
1181 83 : }
1182 : });
1183 : },
1184 5888 : [this](const std::map<DeviceId, KnownDevice>& devices) {
1185 2944 : std::map<std::string, std::string> ids;
1186 1010973 : for (auto& d : devices) {
1187 1008020 : auto id = d.first.toString();
1188 1008083 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
1189 1008067 : ids.emplace(std::move(id), std::move(label));
1190 1008031 : }
1191 2944 : runOnMainThread([id = getAccountID(), devices = std::move(ids)] {
1192 2944 : emitSignal<libjami::ConfigurationSignal::KnownDevicesChanged>(id, devices);
1193 2944 : });
1194 2944 : },
1195 56 : [this](const std::string& conversationId, const std::string& deviceId) {
1196 : // Note: Do not retrigger on another thread. This has to be done
1197 : // at the same time of acceptTrustRequest a synced state between TrustRequest
1198 : // and convRequests.
1199 56 : if (auto cm = convModule(true))
1200 56 : cm->acceptConversationRequest(conversationId, deviceId);
1201 56 : },
1202 90 : [this](const std::string& uri, const std::string& convFromReq) {
1203 45 : dht::ThreadPool::io().run([w = weak(), convFromReq, uri] {
1204 45 : if (auto shared = w.lock()) {
1205 45 : auto cm = shared->convModule(true);
1206 : // Remove cached payload if there is one
1207 90 : auto requestPath = shared->cachePath_ / "requests" / uri;
1208 45 : dhtnet::fileutils::remove(requestPath);
1209 45 : if (!convFromReq.empty()) {
1210 45 : auto oldConv = cm->getOneToOneConversation(uri);
1211 : // If we previously removed the contact, and re-add it, we may
1212 : // receive a convId different from the request. In that case,
1213 : // we need to remove the current conversation and clone the old
1214 : // one (given by convFromReq).
1215 : // TODO: In the future, we may want to re-commit the messages we
1216 : // may have send in the request we sent.
1217 45 : if (oldConv != convFromReq
1218 45 : && cm->updateConvForContact(uri, oldConv, convFromReq)) {
1219 7 : cm->initReplay(oldConv, convFromReq);
1220 7 : cm->cloneConversationFrom(convFromReq, uri, oldConv);
1221 : }
1222 45 : }
1223 90 : }
1224 45 : });
1225 844 : }};
1226 :
1227 799 : const auto& conf = config();
1228 : try {
1229 799 : auto oldIdentity = id_.first ? id_.first->getPublicKey().getLongId() : DeviceId();
1230 799 : if (conf.managerUri.empty()) {
1231 1598 : accountManager_ = std::make_shared<ArchiveAccountManager>(
1232 : getPath(),
1233 43 : [this]() { return getAccountDetails(); },
1234 1598 : conf.archivePath.empty() ? "archive.gz" : conf.archivePath,
1235 1598 : conf.nameServer);
1236 : } else {
1237 0 : accountManager_ = std::make_shared<ServerAccountManager>(getPath(),
1238 0 : conf.managerUri,
1239 0 : conf.nameServer);
1240 : }
1241 :
1242 799 : auto id = accountManager_->loadIdentity(getAccountID(),
1243 799 : conf.tlsCertificateFile,
1244 799 : conf.tlsPrivateKeyFile,
1245 799 : conf.tlsPassword);
1246 :
1247 1598 : if (auto info = accountManager_->useIdentity(getAccountID(),
1248 : id,
1249 799 : conf.receipt,
1250 799 : conf.receiptSignature,
1251 799 : conf.managerUsername,
1252 : callbacks)) {
1253 : // normal loading path
1254 16 : id_ = std::move(id);
1255 16 : config_->username = info->accountId;
1256 48 : JAMI_WARNING("[Account {:s}] Loaded account identity", getAccountID());
1257 16 : if (info->identity.first->getPublicKey().getLongId() != oldIdentity) {
1258 0 : JAMI_WARNING("[Account {:s}] Identity changed", getAccountID());
1259 : {
1260 0 : std::lock_guard lk(moduleMtx_);
1261 0 : convModule_.reset();
1262 0 : }
1263 0 : convModule(); // convModule must absolutely be initialized in
1264 : // both branches of the if statement here in order
1265 : // for it to exist for subsequent use.
1266 : } else {
1267 16 : convModule()->setAccountManager(accountManager_);
1268 : }
1269 16 : if (not isEnabled()) {
1270 0 : setRegistrationState(RegistrationState::UNREGISTERED);
1271 : }
1272 783 : } else if (isEnabled()) {
1273 2349 : JAMI_WARNING("[Account {}] useIdentity failed!", getAccountID());
1274 783 : if (not conf.managerUri.empty() and archive_password.empty()) {
1275 0 : Migration::setState(accountID_, Migration::State::INVALID);
1276 0 : setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
1277 0 : return;
1278 : }
1279 :
1280 783 : bool migrating = registrationState_ == RegistrationState::ERROR_NEED_MIGRATION;
1281 783 : setRegistrationState(RegistrationState::INITIALIZING);
1282 783 : auto fDeviceKey = dht::ThreadPool::computation()
1283 783 : .getShared<std::shared_ptr<dht::crypto::PrivateKey>>([]() {
1284 : return std::make_shared<dht::crypto::PrivateKey>(
1285 1566 : dht::crypto::PrivateKey::generate());
1286 783 : });
1287 :
1288 783 : std::unique_ptr<AccountManager::AccountCredentials> creds;
1289 783 : if (conf.managerUri.empty()) {
1290 783 : auto acreds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
1291 783 : auto archivePath = fileutils::getFullPath(idPath_, conf.archivePath);
1292 783 : bool hasArchive = std::filesystem::is_regular_file(archivePath);
1293 :
1294 783 : if (not archive_path.empty()) {
1295 : // Importing external archive
1296 39 : acreds->scheme = "file";
1297 39 : acreds->uri = archive_path;
1298 744 : } else if (not archive_pin.empty()) {
1299 : // Importing from DHT
1300 1 : acreds->scheme = "dht";
1301 1 : acreds->uri = archive_pin;
1302 1 : acreds->dhtBootstrap = loadBootstrap();
1303 1 : acreds->dhtPort = dhtPortUsed();
1304 743 : } else if (hasArchive) {
1305 : // Migrating local account
1306 4 : acreds->scheme = "local";
1307 4 : acreds->uri = std::move(archivePath).string();
1308 4 : acreds->updateIdentity = id;
1309 4 : migrating = true;
1310 : }
1311 783 : creds = std::move(acreds);
1312 783 : } else {
1313 0 : auto screds = std::make_unique<ServerAccountManager::ServerAccountCredentials>();
1314 0 : screds->username = conf.managerUsername;
1315 0 : creds = std::move(screds);
1316 0 : }
1317 783 : creds->password = archive_password;
1318 783 : bool hasPassword = !archive_password.empty();
1319 783 : if (hasPassword && archive_password_scheme.empty())
1320 13 : creds->password_scheme = fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD;
1321 : else
1322 770 : creds->password_scheme = archive_password_scheme;
1323 :
1324 3915 : accountManager_->initAuthentication(
1325 : getAccountID(),
1326 : fDeviceKey,
1327 1566 : ip_utils::getDeviceName(),
1328 783 : std::move(creds),
1329 783 : [w = weak(),
1330 : this,
1331 : migrating,
1332 : hasPassword](const AccountInfo& info,
1333 : const std::map<std::string, std::string>& config,
1334 : std::string&& receipt,
1335 9398 : std::vector<uint8_t>&& receipt_signature) {
1336 783 : auto sthis = w.lock();
1337 783 : if (not sthis)
1338 0 : return;
1339 2349 : JAMI_LOG("[Account {}] Auth success!", getAccountID());
1340 :
1341 783 : dhtnet::fileutils::check_dir(idPath_, 0700);
1342 :
1343 783 : auto id = info.identity;
1344 783 : editConfig([&](JamiAccountConfig& conf) {
1345 783 : std::tie(conf.tlsPrivateKeyFile, conf.tlsCertificateFile)
1346 1566 : = saveIdentity(id, idPath_, DEVICE_ID_PATH);
1347 783 : conf.tlsPassword = {};
1348 783 : conf.archiveHasPassword = hasPassword;
1349 783 : if (not conf.managerUri.empty()) {
1350 0 : conf.registeredName = conf.managerUsername;
1351 0 : registeredName_ = conf.managerUsername;
1352 : }
1353 783 : conf.username = info.accountId;
1354 783 : conf.deviceName = accountManager_->getAccountDeviceName();
1355 :
1356 783 : auto nameServerIt = config.find(
1357 : libjami::Account::ConfProperties::RingNS::URI);
1358 783 : if (nameServerIt != config.end() && !nameServerIt->second.empty()) {
1359 0 : conf.nameServer = nameServerIt->second;
1360 : }
1361 783 : auto displayNameIt = config.find(
1362 : libjami::Account::ConfProperties::DISPLAYNAME);
1363 783 : if (displayNameIt != config.end() && !displayNameIt->second.empty()) {
1364 42 : conf.displayName = displayNameIt->second;
1365 : }
1366 783 : conf.receipt = std::move(receipt);
1367 783 : conf.receiptSignature = std::move(receipt_signature);
1368 783 : conf.fromMap(config);
1369 783 : });
1370 783 : id_ = std::move(id);
1371 : {
1372 783 : std::lock_guard lk(moduleMtx_);
1373 783 : convModule_.reset();
1374 783 : }
1375 783 : if (migrating) {
1376 4 : Migration::setState(getAccountID(), Migration::State::SUCCESS);
1377 : }
1378 783 : if (not info.photo.empty() or not config_->displayName.empty())
1379 782 : emitSignal<libjami::ConfigurationSignal::AccountProfileReceived>(
1380 782 : getAccountID(), config_->displayName, info.photo);
1381 783 : setRegistrationState(RegistrationState::UNREGISTERED);
1382 783 : doRegister();
1383 783 : },
1384 0 : [w = weak(),
1385 : id,
1386 783 : accountId = getAccountID(),
1387 0 : migrating](AccountManager::AuthError error, const std::string& message) {
1388 0 : JAMI_WARNING("[Account {}] Auth error: {} {}", accountId, (int) error, message);
1389 0 : if ((id.first || migrating)
1390 0 : && error == AccountManager::AuthError::INVALID_ARGUMENTS) {
1391 : // In cast of a migration or manager connexion failure stop the migration
1392 : // and block the account
1393 0 : Migration::setState(accountId, Migration::State::INVALID);
1394 0 : if (auto acc = w.lock())
1395 0 : acc->setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
1396 : } else {
1397 : // In case of a DHT or backup import failure, just remove the account
1398 0 : if (auto acc = w.lock())
1399 0 : acc->setRegistrationState(RegistrationState::ERROR_GENERIC);
1400 0 : runOnMainThread([accountId = std::move(accountId)] {
1401 0 : Manager::instance().removeAccount(accountId, true);
1402 0 : });
1403 : }
1404 0 : },
1405 : callbacks);
1406 783 : }
1407 799 : } catch (const std::exception& e) {
1408 0 : JAMI_WARNING("[Account {}] Error loading account: {}", getAccountID(), e.what());
1409 0 : accountManager_.reset();
1410 0 : setRegistrationState(RegistrationState::ERROR_GENERIC);
1411 0 : }
1412 799 : }
1413 :
1414 : std::map<std::string, std::string>
1415 4605 : JamiAccount::getVolatileAccountDetails() const
1416 : {
1417 4605 : auto a = SIPAccountBase::getVolatileAccountDetails();
1418 4605 : a.emplace(libjami::Account::VolatileProperties::InstantMessaging::OFF_CALL, TRUE_STR);
1419 : #if HAVE_RINGNS
1420 4605 : auto registeredName = getRegisteredName();
1421 4605 : if (not registeredName.empty())
1422 3 : a.emplace(libjami::Account::VolatileProperties::REGISTERED_NAME, registeredName);
1423 : #endif
1424 4605 : a.emplace(libjami::Account::ConfProperties::PROXY_SERVER, proxyServerCached_);
1425 4605 : a.emplace(libjami::Account::VolatileProperties::DHT_BOUND_PORT, std::to_string(dhtBoundPort_));
1426 4605 : a.emplace(libjami::Account::VolatileProperties::DEVICE_ANNOUNCED,
1427 4605 : deviceAnnounced_ ? TRUE_STR : FALSE_STR);
1428 4605 : if (accountManager_) {
1429 4605 : if (auto info = accountManager_->getInfo()) {
1430 3820 : a.emplace(libjami::Account::ConfProperties::DEVICE_ID, info->deviceId);
1431 : }
1432 : }
1433 9210 : return a;
1434 4605 : }
1435 :
1436 : #if HAVE_RINGNS
1437 : void
1438 4 : JamiAccount::lookupName(const std::string& name)
1439 : {
1440 4 : std::lock_guard lock(configurationMutex_);
1441 4 : if (accountManager_)
1442 12 : accountManager_->lookupUri(name,
1443 4 : config().nameServer,
1444 4 : [acc = getAccountID(), name](const std::string& result,
1445 : NameDirectory::Response response) {
1446 8 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(
1447 8 : acc, (int) response, result, name);
1448 4 : });
1449 4 : }
1450 :
1451 : void
1452 2 : JamiAccount::lookupAddress(const std::string& addr)
1453 : {
1454 2 : std::lock_guard lock(configurationMutex_);
1455 2 : auto acc = getAccountID();
1456 2 : if (accountManager_)
1457 2 : accountManager_->lookupAddress(
1458 2 : addr, [acc, addr](const std::string& result, NameDirectory::Response response) {
1459 4 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
1460 : (int) response,
1461 2 : addr,
1462 : result);
1463 2 : });
1464 2 : }
1465 :
1466 : void
1467 1 : JamiAccount::registerName(const std::string& name,
1468 : const std::string& scheme,
1469 : const std::string& password)
1470 : {
1471 1 : std::lock_guard lock(configurationMutex_);
1472 1 : if (accountManager_)
1473 2 : accountManager_->registerName(
1474 : name,
1475 : scheme,
1476 : password,
1477 1 : [acc = getAccountID(), name, w = weak()](NameDirectory::RegistrationResponse response,
1478 : const std::string& regName) {
1479 1 : auto res = (int) std::min(response, NameDirectory::RegistrationResponse::error);
1480 1 : if (response == NameDirectory::RegistrationResponse::success) {
1481 1 : if (auto this_ = w.lock()) {
1482 1 : if (this_->setRegisteredName(regName)) {
1483 1 : this_->editConfig([&](JamiAccountConfig& config) {
1484 1 : config.registeredName = regName;
1485 1 : });
1486 1 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
1487 2 : this_->accountID_, this_->getVolatileAccountDetails());
1488 : }
1489 1 : }
1490 : }
1491 1 : emitSignal<libjami::ConfigurationSignal::NameRegistrationEnded>(acc, res, name);
1492 1 : });
1493 1 : }
1494 : #endif
1495 :
1496 : bool
1497 0 : JamiAccount::searchUser(const std::string& query)
1498 : {
1499 0 : if (accountManager_)
1500 0 : return accountManager_->searchUser(
1501 : query,
1502 0 : [acc = getAccountID(), query](const jami::NameDirectory::SearchResult& result,
1503 : jami::NameDirectory::Response response) {
1504 0 : jami::emitSignal<libjami::ConfigurationSignal::UserSearchEnded>(acc,
1505 : (int) response,
1506 0 : query,
1507 : result);
1508 0 : });
1509 0 : return false;
1510 : }
1511 :
1512 : void
1513 1368 : JamiAccount::forEachPendingCall(const DeviceId& deviceId,
1514 : const std::function<void(const std::shared_ptr<SIPCall>&)>& cb)
1515 : {
1516 1368 : std::vector<std::shared_ptr<SIPCall>> pc;
1517 : {
1518 1368 : std::lock_guard lk(pendingCallsMutex_);
1519 1368 : pc = std::move(pendingCalls_[deviceId]);
1520 1368 : }
1521 1458 : for (const auto& pendingCall : pc) {
1522 91 : cb(pendingCall);
1523 : }
1524 1367 : }
1525 :
1526 : void
1527 699 : JamiAccount::registerAsyncOps()
1528 : {
1529 2716 : auto onLoad = [this, loaded = std::make_shared<std::atomic_uint>()] {
1530 2017 : if (++(*loaded) == 2u) {
1531 699 : runOnMainThread([w = weak()] {
1532 699 : if (auto s = w.lock()) {
1533 699 : std::lock_guard lock(s->configurationMutex_);
1534 699 : s->doRegister_();
1535 1398 : }
1536 699 : });
1537 : }
1538 2716 : };
1539 :
1540 1398 : loadCachedProxyServer([onLoad](const std::string&) { onLoad(); });
1541 :
1542 699 : if (upnpCtrl_) {
1543 1965 : JAMI_LOG("[Account {:s}] UPnP: attempting to map ports", getAccountID());
1544 :
1545 : // Release current mapping if any.
1546 655 : if (dhtUpnpMapping_.isValid()) {
1547 0 : upnpCtrl_->releaseMapping(dhtUpnpMapping_);
1548 : }
1549 :
1550 655 : dhtUpnpMapping_.enableAutoUpdate(true);
1551 :
1552 : // Set the notify callback.
1553 1310 : dhtUpnpMapping_.setNotifyCallback([w = weak(),
1554 : onLoad,
1555 655 : update = std::make_shared<bool>(false)](
1556 : dhtnet::upnp::Mapping::sharedPtr_t mapRes) {
1557 630 : if (auto accPtr = w.lock()) {
1558 630 : auto& dhtMap = accPtr->dhtUpnpMapping_;
1559 630 : const auto& accId = accPtr->getAccountID();
1560 :
1561 1890 : JAMI_LOG("[Account {:s}] DHT UPnP mapping changed to {:s}",
1562 : accId,
1563 : mapRes->toString(true));
1564 :
1565 630 : if (*update) {
1566 : // Check if we need to update the mapping and the registration.
1567 5 : if (dhtMap.getMapKey() != mapRes->getMapKey()
1568 5 : or dhtMap.getState() != mapRes->getState()) {
1569 : // The connectivity must be restarted, if either:
1570 : // - the state changed to "OPEN",
1571 : // - the state changed to "FAILED" and the mapping was in use.
1572 5 : if (mapRes->getState() == dhtnet::upnp::MappingState::OPEN
1573 10 : or (mapRes->getState() == dhtnet::upnp::MappingState::FAILED
1574 5 : and dhtMap.getState() == dhtnet::upnp::MappingState::OPEN)) {
1575 : // Update the mapping and restart the registration.
1576 0 : dhtMap.updateFrom(mapRes);
1577 :
1578 0 : JAMI_WARNING(
1579 : "[Account {:s}] Allocated port changed to {}. Restarting the "
1580 : "registration",
1581 : accId,
1582 : accPtr->dhtPortUsed());
1583 :
1584 0 : accPtr->dht_->connectivityChanged();
1585 :
1586 : } else {
1587 : // Only update the mapping.
1588 5 : dhtMap.updateFrom(mapRes);
1589 : }
1590 : }
1591 : } else {
1592 625 : *update = true;
1593 : // Set connection info and load the account.
1594 625 : if (mapRes->getState() == dhtnet::upnp::MappingState::OPEN) {
1595 0 : dhtMap.updateFrom(mapRes);
1596 0 : JAMI_LOG(
1597 : "[Account {:s}] Mapping {:s} successfully allocated: starting the DHT",
1598 : accId,
1599 : dhtMap.toString());
1600 : } else {
1601 1875 : JAMI_WARNING("[Account {:s}] Mapping request is in {:s} state: starting "
1602 : "the DHT anyway",
1603 : accId,
1604 : mapRes->getStateStr());
1605 : }
1606 :
1607 : // Load the account and start the DHT.
1608 625 : onLoad();
1609 : }
1610 630 : }
1611 630 : });
1612 :
1613 : // Request the mapping.
1614 655 : auto map = upnpCtrl_->reserveMapping(dhtUpnpMapping_);
1615 : // The returned mapping is invalid. Load the account now since
1616 : // we may never receive the callback.
1617 655 : if (not map) {
1618 649 : onLoad();
1619 : }
1620 655 : } else {
1621 : // No UPNP. Load the account and start the DHT. The local DHT
1622 : // might not be reachable for peers if we are behind a NAT.
1623 44 : onLoad();
1624 : }
1625 699 : }
1626 :
1627 : void
1628 1619 : JamiAccount::doRegister()
1629 : {
1630 1619 : std::lock_guard lock(configurationMutex_);
1631 1619 : if (not isUsable()) {
1632 423 : JAMI_WARNING("[Account {:s}] Account must be enabled and active to register, ignoring",
1633 : getAccountID());
1634 141 : return;
1635 : }
1636 :
1637 4434 : JAMI_LOG("[Account {:s}] Starting account…", getAccountID());
1638 :
1639 : // invalid state transitions:
1640 : // INITIALIZING: generating/loading certificates, unable to register
1641 : // NEED_MIGRATION: old account detected, user needs to migrate
1642 1478 : if (registrationState_ == RegistrationState::INITIALIZING
1643 699 : || registrationState_ == RegistrationState::ERROR_NEED_MIGRATION)
1644 779 : return;
1645 :
1646 699 : convModule(); // Init conv module before passing in trying
1647 699 : setRegistrationState(RegistrationState::TRYING);
1648 : /* if UPnP is enabled, then wait for IGD to complete registration */
1649 699 : if (upnpCtrl_ or proxyServerCached_.empty()) {
1650 699 : registerAsyncOps();
1651 : } else {
1652 0 : doRegister_();
1653 : }
1654 1619 : }
1655 :
1656 : std::vector<std::string>
1657 699 : JamiAccount::loadBootstrap() const
1658 : {
1659 699 : std::vector<std::string> bootstrap;
1660 699 : std::string_view stream(config().hostname), node_addr;
1661 1398 : while (jami::getline(stream, node_addr, ';'))
1662 699 : bootstrap.emplace_back(node_addr);
1663 1398 : for (const auto& b : bootstrap)
1664 699 : JAMI_DBG("[Account %s] Bootstrap node: %s", getAccountID().c_str(), b.c_str());
1665 1398 : return bootstrap;
1666 0 : }
1667 :
1668 : void
1669 34 : JamiAccount::trackBuddyPresence(const std::string& buddy_id, bool track)
1670 : {
1671 34 : std::string buddyUri;
1672 : try {
1673 34 : buddyUri = parseJamiUri(buddy_id);
1674 0 : } catch (...) {
1675 0 : JAMI_ERR("[Account %s] Failed to track presence: invalid URI %s",
1676 : getAccountID().c_str(),
1677 : buddy_id.c_str());
1678 0 : return;
1679 0 : }
1680 34 : JAMI_DBG("[Account %s] %s presence for %s",
1681 : getAccountID().c_str(),
1682 : track ? "Track" : "Untrack",
1683 : buddy_id.c_str());
1684 :
1685 34 : auto h = dht::InfoHash(buddyUri);
1686 34 : std::unique_lock lock(buddyInfoMtx);
1687 34 : if (track) {
1688 34 : auto buddy = trackedBuddies_.emplace(h, BuddyInfo {h});
1689 34 : if (buddy.second) {
1690 34 : trackPresence(buddy.first->first, buddy.first->second);
1691 : }
1692 34 : auto it = presenceState_.find(buddyUri);
1693 34 : if (it != presenceState_.end() && it->second != PresenceState::DISCONNECTED) {
1694 1 : lock.unlock();
1695 1 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1696 : buddyUri,
1697 1 : static_cast<int>(it->second),
1698 : "");
1699 : }
1700 : } else {
1701 0 : auto buddy = trackedBuddies_.find(h);
1702 0 : if (buddy != trackedBuddies_.end()) {
1703 0 : if (auto dht = dht_)
1704 0 : if (dht->isRunning())
1705 0 : dht->cancelListen(h, std::move(buddy->second.listenToken));
1706 0 : trackedBuddies_.erase(buddy);
1707 : }
1708 : }
1709 34 : }
1710 :
1711 : void
1712 35 : JamiAccount::trackPresence(const dht::InfoHash& h, BuddyInfo& buddy)
1713 : {
1714 35 : auto dht = dht_;
1715 35 : if (not dht or not dht->isRunning()) {
1716 1 : return;
1717 : }
1718 : buddy.listenToken
1719 68 : = dht->listen<DeviceAnnouncement>(h, [this, h](DeviceAnnouncement&& dev, bool expired) {
1720 : bool wasConnected, isConnected;
1721 : {
1722 66 : std::lock_guard lock(buddyInfoMtx);
1723 66 : auto buddy = trackedBuddies_.find(h);
1724 66 : if (buddy == trackedBuddies_.end())
1725 0 : return true;
1726 66 : wasConnected = buddy->second.devices_cnt > 0;
1727 66 : if (expired)
1728 33 : --buddy->second.devices_cnt;
1729 : else
1730 33 : ++buddy->second.devices_cnt;
1731 66 : isConnected = buddy->second.devices_cnt > 0;
1732 66 : }
1733 : // NOTE: the rest can use configurationMtx_, that can be locked during unregister so
1734 : // do not retrigger on dht
1735 66 : runOnMainThread([w = weak(), h, dev, expired, isConnected, wasConnected]() {
1736 66 : auto sthis = w.lock();
1737 66 : if (!sthis)
1738 0 : return;
1739 66 : if (not expired) {
1740 : // Retry messages every time a new device announce its presence
1741 33 : sthis->messageEngine_.onPeerOnline(h.toString());
1742 : }
1743 66 : if (isConnected and not wasConnected) {
1744 32 : sthis->onTrackedBuddyOnline(h);
1745 34 : } else if (not isConnected and wasConnected) {
1746 32 : sthis->onTrackedBuddyOffline(h);
1747 : }
1748 66 : });
1749 :
1750 66 : return true;
1751 34 : });
1752 35 : }
1753 :
1754 : std::map<std::string, bool>
1755 2 : JamiAccount::getTrackedBuddyPresence() const
1756 : {
1757 2 : std::lock_guard lock(buddyInfoMtx);
1758 2 : std::map<std::string, bool> presence_info;
1759 4 : for (const auto& buddy_info_p : trackedBuddies_)
1760 2 : presence_info.emplace(buddy_info_p.first.toString(), buddy_info_p.second.devices_cnt > 0);
1761 4 : return presence_info;
1762 2 : }
1763 :
1764 : void
1765 32 : JamiAccount::onTrackedBuddyOnline(const dht::InfoHash& contactId)
1766 : {
1767 32 : std::string id(contactId.toString());
1768 96 : JAMI_DEBUG("Buddy {} online", id);
1769 32 : auto& state = presenceState_[id];
1770 32 : if (state < PresenceState::AVAILABLE) {
1771 32 : state = PresenceState::AVAILABLE;
1772 32 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1773 : id,
1774 : static_cast<int>(
1775 : PresenceState::AVAILABLE),
1776 : "");
1777 : }
1778 :
1779 32 : auto details = getContactDetails(id);
1780 32 : auto it = details.find("confirmed");
1781 32 : if (it == details.end() or it->second == "false") {
1782 32 : auto convId = convModule()->getOneToOneConversation(id);
1783 32 : if (convId.empty())
1784 30 : return;
1785 : // In this case, the TrustRequest was sent but never confirmed (cause the contact was
1786 : // offline maybe) To avoid the contact to never receive the conv request, retry there
1787 2 : std::lock_guard lock(configurationMutex_);
1788 2 : if (accountManager_) {
1789 : // Retrieve cached payload for trust request.
1790 4 : auto requestPath = cachePath_ / "requests" / id;
1791 2 : std::vector<uint8_t> payload;
1792 : try {
1793 3 : payload = fileutils::loadFile(requestPath);
1794 1 : } catch (...) {
1795 1 : }
1796 :
1797 2 : if (payload.size() >= 64000) {
1798 0 : JAMI_WARN() << "Trust request is too big, reset payload";
1799 0 : payload.clear();
1800 : }
1801 :
1802 2 : accountManager_->sendTrustRequest(id, convId, payload);
1803 2 : }
1804 32 : }
1805 62 : }
1806 :
1807 : void
1808 32 : JamiAccount::onTrackedBuddyOffline(const dht::InfoHash& contactId)
1809 : {
1810 32 : auto id = contactId.toString();
1811 96 : JAMI_DEBUG("Buddy {} offline", id);
1812 32 : auto& state = presenceState_[id];
1813 32 : if (state > PresenceState::DISCONNECTED) {
1814 32 : if (state == PresenceState::CONNECTED) {
1815 63 : JAMI_WARNING("Buddy {} is not present on the DHT, but P2P connected", id);
1816 : }
1817 32 : state = PresenceState::DISCONNECTED;
1818 32 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1819 : id,
1820 : static_cast<int>(
1821 : PresenceState::DISCONNECTED),
1822 : "");
1823 : }
1824 32 : }
1825 :
1826 : void
1827 699 : JamiAccount::doRegister_()
1828 : {
1829 699 : if (registrationState_ != RegistrationState::TRYING) {
1830 3 : JAMI_ERROR("[Account {}] Already registered", getAccountID());
1831 1 : return;
1832 : }
1833 :
1834 2094 : JAMI_DEBUG("[Account {}] Starting account…", getAccountID());
1835 698 : const auto& conf = config();
1836 :
1837 : try {
1838 698 : if (not accountManager_ or not accountManager_->getInfo())
1839 0 : throw std::runtime_error("No identity configured for this account.");
1840 :
1841 698 : if (dht_->isRunning()) {
1842 3 : JAMI_ERROR("[Account {}] DHT already running (stopping it first).", getAccountID());
1843 1 : dht_->join();
1844 : }
1845 :
1846 698 : convModule()->clearPendingFetch();
1847 :
1848 : #if HAVE_RINGNS
1849 : // Look for registered name
1850 1396 : accountManager_->lookupAddress(
1851 698 : accountManager_->getInfo()->accountId,
1852 698 : [w = weak()](const std::string& result, const NameDirectory::Response& response) {
1853 698 : if (auto this_ = w.lock()) {
1854 694 : if (response == NameDirectory::Response::found
1855 693 : or response == NameDirectory::Response::notFound) {
1856 694 : const auto& nameResult = response == NameDirectory::Response::found ? result
1857 694 : : "";
1858 694 : if (this_->setRegisteredName(nameResult)) {
1859 0 : this_->editConfig([&](JamiAccountConfig& config) {
1860 0 : config.registeredName = nameResult;
1861 0 : });
1862 0 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
1863 0 : this_->accountID_, this_->getVolatileAccountDetails());
1864 : }
1865 694 : }
1866 698 : }
1867 698 : });
1868 : #endif
1869 :
1870 698 : dht::DhtRunner::Config config {};
1871 698 : config.dht_config.node_config.network = 0;
1872 698 : config.dht_config.node_config.maintain_storage = false;
1873 698 : config.dht_config.node_config.persist_path = (cachePath_ / "dhtstate").string();
1874 698 : config.dht_config.id = id_;
1875 698 : config.dht_config.cert_cache_all = true;
1876 698 : config.push_node_id = getAccountID();
1877 698 : config.push_token = conf.deviceKey;
1878 698 : config.push_topic = conf.notificationTopic;
1879 698 : config.push_platform = conf.platform;
1880 698 : config.proxy_user_agent = jami::userAgent();
1881 698 : config.threaded = true;
1882 698 : config.peer_discovery = conf.dhtPeerDiscovery;
1883 698 : config.peer_publish = conf.dhtPeerDiscovery;
1884 698 : if (conf.proxyEnabled)
1885 0 : config.proxy_server = proxyServerCached_;
1886 :
1887 698 : if (not config.proxy_server.empty()) {
1888 0 : JAMI_LOG("[Account {}] Using proxy server {}", getAccountID(), config.proxy_server);
1889 0 : if (not config.push_token.empty()) {
1890 0 : JAMI_LOG(
1891 : "[Account {}] using push notifications with platform: {}, topic: {}, token: {}",
1892 : getAccountID(),
1893 : config.push_platform,
1894 : config.push_topic,
1895 : config.push_token);
1896 : }
1897 : }
1898 :
1899 : // check if dht peer service is enabled
1900 698 : if (conf.accountPeerDiscovery or conf.accountPublish) {
1901 0 : peerDiscovery_ = std::make_shared<dht::PeerDiscovery>();
1902 0 : if (conf.accountPeerDiscovery) {
1903 0 : JAMI_LOG("[Account {}] Starting Jami account discovery…", getAccountID());
1904 0 : startAccountDiscovery();
1905 : }
1906 0 : if (conf.accountPublish)
1907 0 : startAccountPublish();
1908 : }
1909 698 : dht::DhtRunner::Context context {};
1910 698 : context.peerDiscovery = peerDiscovery_;
1911 698 : context.rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
1912 :
1913 698 : auto dht_log_level = Manager::instance().dhtLogLevel.load();
1914 698 : if (dht_log_level > 0) {
1915 0 : context.logger = Logger::dhtLogger();
1916 : }
1917 1819 : context.certificateStore = [&](const dht::InfoHash& pk_id) {
1918 1819 : std::vector<std::shared_ptr<dht::crypto::Certificate>> ret;
1919 3638 : if (auto cert = certStore().getCertificate(pk_id.toString()))
1920 1819 : ret.emplace_back(std::move(cert));
1921 5457 : JAMI_LOG("Query for local certificate store: {}: {} found.",
1922 : pk_id.toString(),
1923 : ret.size());
1924 1819 : return ret;
1925 698 : };
1926 :
1927 6021 : context.statusChangedCallback = [this](dht::NodeStatus s4, dht::NodeStatus s6) {
1928 2007 : JAMI_DBG("[Account %s] DHT status: IPv4 %s; IPv6 %s",
1929 : getAccountID().c_str(),
1930 : dhtStatusStr(s4),
1931 : dhtStatusStr(s6));
1932 : RegistrationState state;
1933 2007 : auto newStatus = std::max(s4, s6);
1934 2007 : switch (newStatus) {
1935 679 : case dht::NodeStatus::Connecting:
1936 679 : state = RegistrationState::TRYING;
1937 679 : break;
1938 1328 : case dht::NodeStatus::Connected:
1939 1328 : state = RegistrationState::REGISTERED;
1940 1328 : break;
1941 0 : case dht::NodeStatus::Disconnected:
1942 0 : state = RegistrationState::UNREGISTERED;
1943 0 : break;
1944 0 : default:
1945 0 : state = RegistrationState::ERROR_GENERIC;
1946 0 : break;
1947 : }
1948 :
1949 2007 : setRegistrationState(state);
1950 2705 : };
1951 2739 : context.identityAnnouncedCb = [this](bool ok) {
1952 696 : if (!ok)
1953 15 : return;
1954 1362 : accountManager_->startSync(
1955 1816 : [this](const std::shared_ptr<dht::crypto::Certificate>& crt) {
1956 760 : if (jami::Manager::instance().syncOnRegister) {
1957 760 : if (!crt)
1958 686 : return;
1959 760 : auto deviceId = crt->getLongId().toString();
1960 760 : if (accountManager_->getInfo()->deviceId == deviceId)
1961 686 : return;
1962 :
1963 74 : std::unique_lock lk(connManagerMtx_);
1964 74 : initConnectionManager();
1965 74 : lk.unlock();
1966 148 : requestSIPConnection(
1967 : getUsername(),
1968 74 : crt->getLongId(),
1969 : "sync"); // For git notifications, will use the same socket as sync
1970 760 : }
1971 : },
1972 3405 : [this] {
1973 681 : if (jami::Manager::instance().syncOnRegister) {
1974 681 : deviceAnnounced_ = true;
1975 :
1976 : // Bootstrap at the end to avoid to be long to load.
1977 681 : dht::ThreadPool::io().run([w = weak()] {
1978 681 : if (auto shared = w.lock())
1979 681 : shared->convModule()->bootstrap();
1980 681 : });
1981 681 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
1982 1362 : accountID_, getVolatileAccountDetails());
1983 : }
1984 681 : },
1985 681 : publishPresence_);
1986 698 : };
1987 :
1988 698 : dht_->run(dhtPortUsed(), config, std::move(context));
1989 :
1990 1396 : for (const auto& bootstrap : loadBootstrap())
1991 1396 : dht_->bootstrap(bootstrap);
1992 :
1993 698 : dhtBoundPort_ = dht_->getBoundPort();
1994 :
1995 698 : accountManager_->setDht(dht_);
1996 :
1997 698 : std::unique_lock lkCM(connManagerMtx_);
1998 698 : initConnectionManager();
1999 698 : connectionManager_->onDhtConnected(*accountManager_->getInfo()->devicePk);
2000 698 : connectionManager_->onICERequest([this](const DeviceId& deviceId) {
2001 760 : std::promise<bool> accept;
2002 760 : std::future<bool> fut = accept.get_future();
2003 760 : accountManager_->findCertificate(
2004 3040 : deviceId, [this, &accept](const std::shared_ptr<dht::crypto::Certificate>& cert) {
2005 760 : dht::InfoHash peer_account_id;
2006 760 : auto res = accountManager_->onPeerCertificate(cert,
2007 760 : this->config().dhtPublicInCalls,
2008 760 : peer_account_id);
2009 2280 : JAMI_LOG("{} ICE request from {}",
2010 : res ? "Accepting" : "Discarding",
2011 : peer_account_id);
2012 760 : accept.set_value(res);
2013 760 : });
2014 760 : fut.wait();
2015 760 : auto result = fut.get();
2016 760 : return result;
2017 760 : });
2018 698 : connectionManager_->onChannelRequest(
2019 23868 : [this](const std::shared_ptr<dht::crypto::Certificate>& cert, const std::string& name) {
2020 8934 : JAMI_WARNING("[Account {}] New channel asked with name {} from {}",
2021 : getAccountID(),
2022 : name,
2023 : cert->issuer->getId());
2024 :
2025 2981 : if (this->config().turnEnabled && turnCache_) {
2026 2981 : auto addr = turnCache_->getResolvedTurn();
2027 2981 : if (addr == std::nullopt) {
2028 : // If TURN is enabled, but no TURN cached, there can be a temporary
2029 : // resolution error to solve. Sometimes, a connectivity change is not
2030 : // enough, so even if this case is really rare, it should be easy to avoid.
2031 27 : turnCache_->refresh();
2032 : }
2033 : }
2034 :
2035 2981 : auto uri = Uri(name);
2036 2981 : std::lock_guard lk(connManagerMtx_);
2037 2981 : auto itHandler = channelHandlers_.find(uri.scheme());
2038 2981 : if (itHandler != channelHandlers_.end() && itHandler->second)
2039 2252 : return itHandler->second->onRequest(cert, name);
2040 729 : return name == "sip";
2041 2981 : });
2042 698 : connectionManager_->onConnectionReady([this](const DeviceId& deviceId,
2043 : const std::string& name,
2044 23098 : std::shared_ptr<dhtnet::ChannelSocket> channel) {
2045 5739 : if (channel) {
2046 5708 : auto cert = channel->peerCertificate();
2047 5708 : if (!cert || !cert->issuer)
2048 0 : return;
2049 5705 : auto peerId = cert->issuer->getId().toString();
2050 : // A connection request can be sent just before member is banned and this must be ignored.
2051 5708 : if (accountManager()->getCertificateStatus(peerId)
2052 5708 : == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2053 82 : channel->shutdown();
2054 82 : return;
2055 : }
2056 5626 : if (name == "sip") {
2057 1368 : cacheSIPConnection(std::move(channel), peerId, deviceId);
2058 4257 : } else if (name.find("git://") == 0) {
2059 1624 : auto sep = name.find_last_of('/');
2060 1624 : auto conversationId = name.substr(sep + 1);
2061 1624 : auto remoteDevice = name.substr(6, sep - 6);
2062 :
2063 1624 : if (channel->isInitiator()) {
2064 : // Check if wanted remote is our side (git://remoteDevice/conversationId)
2065 812 : return;
2066 : }
2067 :
2068 : // Check if pull from banned device
2069 812 : if (convModule()->isBanned(conversationId, remoteDevice)) {
2070 0 : JAMI_WARNING(
2071 : "[Account {:s}] Git server requested for conversation {:s}, but the "
2072 : "device is "
2073 : "unauthorized ({:s}) ",
2074 : getAccountID(),
2075 : conversationId,
2076 : remoteDevice);
2077 0 : channel->shutdown();
2078 0 : return;
2079 : }
2080 :
2081 812 : auto sock = convModule()->gitSocket(deviceId.toString(), conversationId);
2082 812 : if (sock == channel) {
2083 : // The onConnectionReady is already used as client (for retrieving messages)
2084 : // So it's not the server socket
2085 0 : return;
2086 : }
2087 2436 : JAMI_WARNING(
2088 : "[Account {:s}] Git server requested for conversation {:s}, device {:s}, "
2089 : "channel {}",
2090 : accountID_,
2091 : conversationId,
2092 : deviceId.toString(),
2093 : channel->channel());
2094 812 : auto gs = std::make_unique<GitServer>(accountID_, conversationId, channel);
2095 812 : syncCnt_.fetch_add(1);
2096 812 : gs->setOnFetched(
2097 1109 : [w = weak(), conversationId, deviceId](const std::string& commit) {
2098 1109 : if (auto shared = w.lock()) {
2099 2218 : shared->convModule()->setFetched(conversationId,
2100 2218 : deviceId.toString(),
2101 : commit);
2102 1109 : shared->syncCnt_.fetch_sub(1);
2103 2218 : if (shared->syncCnt_.load() == 0) {
2104 303 : emitSignal<libjami::ConversationSignal::ConversationCloned>(
2105 303 : shared->getAccountID().c_str());
2106 : }
2107 1109 : }
2108 1109 : });
2109 812 : const dht::Value::Id serverId = ValueIdDist()(rand);
2110 : {
2111 812 : std::lock_guard lk(gitServersMtx_);
2112 812 : gitServers_[serverId] = std::move(gs);
2113 812 : }
2114 812 : channel->onShutdown([w = weak(), serverId]() {
2115 : // Run on main thread to avoid to be in mxSock's eventLoop
2116 812 : runOnMainThread([serverId, w]() {
2117 812 : auto shared = w.lock();
2118 812 : if (!shared)
2119 0 : return;
2120 812 : std::lock_guard lk(shared->gitServersMtx_);
2121 812 : shared->gitServers_.erase(serverId);
2122 812 : });
2123 812 : });
2124 2436 : } else {
2125 : // TODO move git://
2126 2634 : std::lock_guard lk(connManagerMtx_);
2127 2634 : auto uri = Uri(name);
2128 2634 : auto itHandler = channelHandlers_.find(uri.scheme());
2129 2634 : if (itHandler != channelHandlers_.end() && itHandler->second)
2130 2635 : itHandler->second->onReady(cert, name, std::move(channel));
2131 2637 : }
2132 6604 : }
2133 : });
2134 698 : lkCM.unlock();
2135 :
2136 698 : if (!conf.managerUri.empty() && accountManager_) {
2137 0 : dynamic_cast<ServerAccountManager*>(accountManager_.get())->onNeedsMigration([this]() {
2138 0 : editConfig([&](JamiAccountConfig& conf) {
2139 0 : conf.receipt.clear();
2140 0 : conf.receiptSignature.clear();
2141 0 : });
2142 0 : Migration::setState(accountID_, Migration::State::INVALID);
2143 0 : setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
2144 0 : });
2145 0 : dynamic_cast<ServerAccountManager*>(accountManager_.get())
2146 0 : ->syncBlueprintConfig([this](const std::map<std::string, std::string>& config) {
2147 0 : editConfig([&](JamiAccountConfig& conf) { conf.fromMap(config); });
2148 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(
2149 0 : getAccountID(), getAccountDetails());
2150 0 : });
2151 : }
2152 :
2153 698 : std::lock_guard lock(buddyInfoMtx);
2154 699 : for (auto& buddy : trackedBuddies_) {
2155 1 : buddy.second.devices_cnt = 0;
2156 1 : trackPresence(buddy.first, buddy.second);
2157 : }
2158 698 : } catch (const std::exception& e) {
2159 0 : JAMI_ERR("Error registering DHT account: %s", e.what());
2160 0 : setRegistrationState(RegistrationState::ERROR_GENERIC);
2161 0 : }
2162 : }
2163 :
2164 : ConversationModule*
2165 33456 : JamiAccount::convModule(bool noCreation)
2166 : {
2167 33456 : if (noCreation)
2168 6464 : return convModule_.get();
2169 26992 : if (!accountManager() || currentDeviceId() == "") {
2170 0 : JAMI_ERROR("[Account {}] Calling convModule() with an uninitialized account",
2171 : getAccountID());
2172 0 : return nullptr;
2173 : }
2174 26995 : std::unique_lock<std::recursive_mutex> lock(configurationMutex_);
2175 27002 : std::lock_guard lk(moduleMtx_);
2176 27002 : if (!convModule_) {
2177 1354 : convModule_ = std::make_unique<ConversationModule>(
2178 677 : shared(),
2179 677 : accountManager_,
2180 3110 : [this](auto&& syncMsg) {
2181 3110 : dht::ThreadPool::io().run([w = weak(), syncMsg] {
2182 3110 : if (auto shared = w.lock()) {
2183 1555 : auto& config = shared->config();
2184 : // For JAMS account, we must update the server
2185 1555 : if (!config.managerUri.empty())
2186 0 : if (auto am = shared->accountManager())
2187 0 : am->syncDevices();
2188 1555 : if (auto sm = shared->syncModule())
2189 1555 : sm->syncWithConnected(syncMsg);
2190 : }
2191 : });
2192 1555 : },
2193 37176 : [this](auto&& uri, auto&& device, auto&& msg, auto token = 0) {
2194 : // No need to retrigger, sendTextMessage will call
2195 : // messageEngine_.sendMessage, already retriggering on
2196 : // main thread.
2197 18588 : auto deviceId = device ? device.toString() : "";
2198 37178 : return sendTextMessage(uri, deviceId, msg, token);
2199 18590 : },
2200 5396 : [this](const auto& convId, const auto& deviceId, auto cb, const auto& type) {
2201 2698 : dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::move(cb), type] {
2202 2698 : auto shared = w.lock();
2203 2698 : if (!shared)
2204 0 : return;
2205 4499 : if (auto socket = shared->convModule()->gitSocket(deviceId, convId)) {
2206 1801 : if (!cb(socket))
2207 0 : socket->shutdown();
2208 : else
2209 1801 : cb({});
2210 1801 : return;
2211 : }
2212 897 : std::unique_lock lkCM(shared->connManagerMtx_);
2213 897 : if (!shared->connectionManager_) {
2214 6 : lkCM.unlock();
2215 6 : cb({});
2216 6 : return;
2217 : }
2218 :
2219 4455 : shared->connectionManager_->connectDevice(
2220 1782 : DeviceId(deviceId),
2221 891 : fmt::format("git://{}/{}", deviceId, convId),
2222 2673 : [w,
2223 891 : cb = std::move(cb),
2224 891 : convId](std::shared_ptr<dhtnet::ChannelSocket> socket, const DeviceId&) {
2225 4455 : dht::ThreadPool::io().run([w,
2226 891 : cb = std::move(cb),
2227 891 : socket = std::move(socket),
2228 891 : convId] {
2229 891 : if (socket) {
2230 812 : socket->onShutdown([w, deviceId = socket->deviceId(), convId] {
2231 1624 : dht::ThreadPool::io().run([w, deviceId, convId] {
2232 1624 : if (auto shared = w.lock())
2233 : shared->convModule()
2234 812 : ->removeGitSocket(deviceId.toString(), convId);
2235 : });
2236 : });
2237 812 : if (!cb(socket))
2238 1 : socket->shutdown();
2239 : } else
2240 79 : cb({});
2241 : });
2242 : },
2243 : false,
2244 : false,
2245 891 : type);
2246 2704 : });
2247 2698 : },
2248 1664 : [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType) {
2249 1664 : dht::ThreadPool::io().run([w = weak(),
2250 : convId,
2251 : deviceId,
2252 832 : cb = std::move(cb),
2253 : connectionType] {
2254 829 : auto shared = w.lock();
2255 831 : if (!shared)
2256 0 : return;
2257 830 : auto cm = shared->convModule();
2258 832 : std::lock_guard lkCM(shared->connManagerMtx_);
2259 832 : if (!shared->connectionManager_ || !cm || cm->isBanned(convId, deviceId)) {
2260 20 : Manager::instance().ioContext()->post([cb] { cb({}); });
2261 10 : return;
2262 : }
2263 1644 : if (!shared->connectionManager_->isConnecting(DeviceId(deviceId),
2264 : fmt::format("swarm://{}",
2265 822 : convId))) {
2266 3160 : shared->connectionManager_->connectDevice(
2267 1580 : DeviceId(deviceId),
2268 790 : fmt::format("swarm://{}", convId),
2269 790 : [w, cb = std::move(cb)](std::shared_ptr<dhtnet::ChannelSocket> socket,
2270 : const DeviceId& deviceId) {
2271 2367 : dht::ThreadPool::io().run([w,
2272 789 : cb = std::move(cb),
2273 789 : socket = std::move(socket),
2274 : deviceId] {
2275 788 : if (socket) {
2276 502 : auto shared = w.lock();
2277 502 : if (!shared)
2278 0 : return;
2279 502 : auto remoteCert = socket->peerCertificate();
2280 502 : auto uri = remoteCert->issuer->getId().toString();
2281 502 : if (shared->accountManager()->getCertificateStatus(uri)
2282 502 : == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2283 0 : cb(nullptr);
2284 0 : return;
2285 : }
2286 :
2287 502 : shared->requestSIPConnection(uri, deviceId, "");
2288 502 : }
2289 786 : cb(socket);
2290 : });
2291 : });
2292 : }
2293 842 : });
2294 832 : },
2295 2 : [this](auto&& convId, auto&& from) {
2296 1 : accountManager_
2297 1 : ->findCertificate(dht::InfoHash(from),
2298 1 : [this, from, convId](
2299 : const std::shared_ptr<dht::crypto::Certificate>& cert) {
2300 1 : auto info = accountManager_->getInfo();
2301 1 : if (!cert || !info)
2302 0 : return;
2303 2 : info->contacts->onTrustRequest(dht::InfoHash(from),
2304 : cert->getSharedPublicKey(),
2305 : time(nullptr),
2306 : false,
2307 1 : convId,
2308 : {});
2309 : });
2310 1 : },
2311 1354 : autoLoadConversations_);
2312 : }
2313 27000 : return convModule_.get();
2314 26998 : }
2315 :
2316 : SyncModule*
2317 1817 : JamiAccount::syncModule()
2318 : {
2319 1817 : if (!accountManager() || currentDeviceId() == "") {
2320 0 : JAMI_ERR() << "Calling syncModule() with an uninitialized account.";
2321 0 : return nullptr;
2322 : }
2323 1817 : std::lock_guard lk(moduleMtx_);
2324 1817 : if (!syncModule_)
2325 382 : syncModule_ = std::make_unique<SyncModule>(weak());
2326 1817 : return syncModule_.get();
2327 1817 : }
2328 :
2329 : void
2330 289 : JamiAccount::onTextMessage(const std::string& id,
2331 : const std::string& from,
2332 : const std::string& deviceId,
2333 : const std::map<std::string, std::string>& payloads)
2334 : {
2335 : try {
2336 289 : const std::string fromUri {parseJamiUri(from)};
2337 289 : SIPAccountBase::onTextMessage(id, fromUri, deviceId, payloads);
2338 289 : } catch (...) {
2339 0 : }
2340 289 : }
2341 :
2342 : void
2343 0 : JamiAccount::loadConversation(const std::string& convId)
2344 : {
2345 0 : if (auto cm = convModule(true))
2346 0 : cm->loadSingleConversation(convId);
2347 0 : }
2348 :
2349 : void
2350 964 : JamiAccount::doUnregister(std::function<void(bool)> released_cb)
2351 : {
2352 964 : std::unique_lock<std::recursive_mutex> lock(configurationMutex_);
2353 964 : if (registrationState_ >= RegistrationState::ERROR_GENERIC) {
2354 141 : lock.unlock();
2355 141 : if (released_cb)
2356 0 : released_cb(false);
2357 141 : return;
2358 : }
2359 :
2360 823 : std::mutex mtx;
2361 823 : std::condition_variable cv;
2362 823 : bool shutdown_complete {false};
2363 :
2364 823 : if (peerDiscovery_) {
2365 0 : peerDiscovery_->stopPublish(PEER_DISCOVERY_JAMI_SERVICE);
2366 0 : peerDiscovery_->stopDiscovery(PEER_DISCOVERY_JAMI_SERVICE);
2367 : }
2368 :
2369 823 : JAMI_WARN("[Account %s] Unregistering account %p", getAccountID().c_str(), this);
2370 823 : dht_->shutdown(
2371 823 : [&] {
2372 823 : JAMI_WARN("[Account %s] DHT shutdown complete", getAccountID().c_str());
2373 823 : std::lock_guard lock(mtx);
2374 823 : shutdown_complete = true;
2375 823 : cv.notify_all();
2376 823 : },
2377 : true);
2378 :
2379 : {
2380 823 : std::lock_guard lk(pendingCallsMutex_);
2381 823 : pendingCalls_.clear();
2382 823 : }
2383 :
2384 : // Stop all current P2P connections if account is disabled
2385 : // Else, we let the system managing if the co is down or not
2386 : // NOTE: this is used for changing account's config.
2387 823 : if (not isEnabled())
2388 157 : shutdownConnections();
2389 :
2390 : // Release current UPnP mapping if any.
2391 823 : if (upnpCtrl_ and dhtUpnpMapping_.isValid()) {
2392 0 : upnpCtrl_->releaseMapping(dhtUpnpMapping_);
2393 : }
2394 :
2395 : {
2396 823 : std::unique_lock lock(mtx);
2397 2339 : cv.wait(lock, [&] { return shutdown_complete; });
2398 823 : }
2399 823 : dht_->join();
2400 823 : setRegistrationState(RegistrationState::UNREGISTERED);
2401 :
2402 823 : lock.unlock();
2403 :
2404 823 : if (released_cb)
2405 16 : released_cb(false);
2406 : #ifdef ENABLE_PLUGIN
2407 823 : jami::Manager::instance().getJamiPluginManager().getChatServicesManager().cleanChatSubjects(
2408 : getAccountID());
2409 : #endif
2410 964 : }
2411 :
2412 : void
2413 5095 : JamiAccount::setRegistrationState(RegistrationState state,
2414 : int detail_code,
2415 : const std::string& detail_str)
2416 : {
2417 5095 : if (registrationState_ != state) {
2418 3656 : if (state == RegistrationState::REGISTERED) {
2419 2091 : JAMI_WARNING("[Account {}] Connected", getAccountID());
2420 697 : turnCache_->refresh();
2421 697 : if (connectionManager_)
2422 690 : connectionManager_->storeActiveIpAddress();
2423 2959 : } else if (state == RegistrationState::TRYING) {
2424 2097 : JAMI_WARNING("[Account {}] Connecting…", getAccountID());
2425 : } else {
2426 2260 : deviceAnnounced_ = false;
2427 6780 : JAMI_WARNING("[Account {}] Disconnected", getAccountID());
2428 : }
2429 : }
2430 : // Update registrationState_ & emit signals
2431 5095 : Account::setRegistrationState(state, detail_code, detail_str);
2432 5095 : }
2433 :
2434 : void
2435 0 : JamiAccount::reloadContacts()
2436 : {
2437 0 : accountManager_->reloadContacts();
2438 0 : }
2439 :
2440 : void
2441 0 : JamiAccount::connectivityChanged()
2442 : {
2443 0 : JAMI_WARN("connectivityChanged");
2444 0 : if (not isUsable()) {
2445 : // nothing to do
2446 0 : return;
2447 : }
2448 :
2449 0 : if (auto cm = convModule())
2450 0 : cm->connectivityChanged();
2451 0 : dht_->connectivityChanged();
2452 : {
2453 0 : std::lock_guard lkCM(connManagerMtx_);
2454 0 : if (connectionManager_) {
2455 0 : connectionManager_->connectivityChanged();
2456 : // reset cache
2457 0 : connectionManager_->setPublishedAddress({});
2458 : }
2459 0 : }
2460 : }
2461 :
2462 : bool
2463 0 : JamiAccount::findCertificate(
2464 : const dht::InfoHash& h,
2465 : std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
2466 : {
2467 0 : if (accountManager_)
2468 0 : return accountManager_->findCertificate(h, std::move(cb));
2469 0 : return false;
2470 : }
2471 :
2472 : bool
2473 0 : JamiAccount::findCertificate(
2474 : const dht::PkId& id, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
2475 : {
2476 0 : if (accountManager_)
2477 0 : return accountManager_->findCertificate(id, std::move(cb));
2478 0 : return false;
2479 : }
2480 :
2481 : bool
2482 86 : JamiAccount::findCertificate(const std::string& crt_id)
2483 : {
2484 86 : if (accountManager_)
2485 86 : return accountManager_->findCertificate(dht::InfoHash(crt_id));
2486 0 : return false;
2487 : }
2488 :
2489 : bool
2490 89 : JamiAccount::setCertificateStatus(const std::string& cert_id,
2491 : dhtnet::tls::TrustStore::PermissionStatus status)
2492 : {
2493 89 : bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false;
2494 89 : if (done) {
2495 86 : findCertificate(cert_id);
2496 172 : emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(
2497 86 : getAccountID(), cert_id, dhtnet::tls::TrustStore::statusToStr(status));
2498 : }
2499 89 : return done;
2500 : }
2501 :
2502 : bool
2503 0 : JamiAccount::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
2504 : dhtnet::tls::TrustStore::PermissionStatus status,
2505 : bool local)
2506 : {
2507 0 : bool done = accountManager_ ? accountManager_->setCertificateStatus(cert, status, local)
2508 0 : : false;
2509 0 : if (done) {
2510 0 : findCertificate(cert->getId().toString());
2511 0 : emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(
2512 0 : getAccountID(), cert->getId().toString(), dhtnet::tls::TrustStore::statusToStr(status));
2513 : }
2514 0 : return done;
2515 : }
2516 :
2517 : std::vector<std::string>
2518 0 : JamiAccount::getCertificatesByStatus(dhtnet::tls::TrustStore::PermissionStatus status)
2519 : {
2520 0 : if (accountManager_)
2521 0 : return accountManager_->getCertificatesByStatus(status);
2522 0 : return {};
2523 : }
2524 :
2525 : bool
2526 297 : JamiAccount::isMessageTreated(dht::Value::Id id)
2527 : {
2528 297 : std::lock_guard lock(messageMutex_);
2529 594 : return !treatedMessages_.add(id);
2530 297 : }
2531 :
2532 : bool
2533 13 : JamiAccount::sha3SumVerify() const
2534 : {
2535 13 : return !noSha3sumVerification_;
2536 : }
2537 :
2538 : #ifdef LIBJAMI_TESTABLE
2539 : void
2540 1 : JamiAccount::noSha3sumVerification(bool newValue)
2541 : {
2542 1 : noSha3sumVerification_ = newValue;
2543 1 : }
2544 : #endif
2545 :
2546 : std::map<std::string, std::string>
2547 181 : JamiAccount::getKnownDevices() const
2548 : {
2549 181 : std::lock_guard lock(configurationMutex_);
2550 181 : if (not accountManager_ or not accountManager_->getInfo())
2551 0 : return {};
2552 181 : std::map<std::string, std::string> ids;
2553 1364 : for (const auto& d : accountManager_->getKnownDevices()) {
2554 1183 : auto id = d.first.toString();
2555 1183 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
2556 1183 : ids.emplace(std::move(id), std::move(label));
2557 1183 : }
2558 181 : return ids;
2559 181 : }
2560 :
2561 : void
2562 0 : JamiAccount::loadCachedUrl(const std::string& url,
2563 : const std::filesystem::path& cachePath,
2564 : const std::chrono::seconds& cacheDuration,
2565 : std::function<void(const dht::http::Response& response)> cb)
2566 : {
2567 0 : dht::ThreadPool::io().run([cb, url, cachePath, cacheDuration, w = weak()]() {
2568 : try {
2569 0 : std::string data;
2570 : {
2571 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
2572 0 : data = fileutils::loadCacheTextFile(cachePath, cacheDuration);
2573 0 : }
2574 0 : dht::http::Response ret;
2575 0 : ret.body = std::move(data);
2576 0 : ret.status_code = 200;
2577 0 : cb(ret);
2578 0 : } catch (const std::exception& e) {
2579 0 : JAMI_LOG("Failed to load '{}' from '{}': {}", url, cachePath, e.what());
2580 :
2581 0 : if (auto sthis = w.lock()) {
2582 : auto req = std::make_shared<dht::http::Request>(
2583 0 : *Manager::instance().ioContext(),
2584 0 : url,
2585 0 : [cb, cachePath, w](const dht::http::Response& response) {
2586 0 : if (response.status_code == 200) {
2587 : try {
2588 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
2589 0 : fileutils::saveFile(cachePath,
2590 0 : (const uint8_t*) response.body.data(),
2591 : response.body.size(),
2592 : 0600);
2593 0 : JAMI_LOG("Cached result to '{}'", cachePath);
2594 0 : } catch (const std::exception& ex) {
2595 0 : JAMI_WARNING("Failed to save result to '{}': {}",
2596 : cachePath,
2597 : ex.what());
2598 0 : }
2599 0 : cb(response);
2600 : } else {
2601 : try {
2602 0 : if (std::filesystem::exists(cachePath)) {
2603 0 : JAMI_WARNING("Failed to download URL, using cached data");
2604 0 : std::string data;
2605 : {
2606 : std::lock_guard lk(
2607 0 : dhtnet::fileutils::getFileLock(cachePath));
2608 0 : data = fileutils::loadTextFile(cachePath);
2609 0 : }
2610 0 : dht::http::Response ret;
2611 0 : ret.body = std::move(data);
2612 0 : ret.status_code = 200;
2613 0 : cb(ret);
2614 0 : } else
2615 0 : throw std::runtime_error("No cached data");
2616 0 : } catch (...) {
2617 0 : cb(response);
2618 0 : }
2619 : }
2620 0 : if (auto req = response.request.lock())
2621 0 : if (auto sthis = w.lock())
2622 0 : sthis->requests_.erase(req);
2623 0 : });
2624 0 : sthis->requests_.emplace(req);
2625 0 : req->send();
2626 0 : }
2627 0 : }
2628 0 : });
2629 0 : }
2630 :
2631 : void
2632 699 : JamiAccount::loadCachedProxyServer(std::function<void(const std::string& proxy)> cb)
2633 : {
2634 699 : const auto& conf = config();
2635 699 : if (conf.proxyEnabled and proxyServerCached_.empty()) {
2636 0 : JAMI_DEBUG("[Account {:s}] Loading DHT proxy URL: {:s}", getAccountID(), conf.proxyListUrl);
2637 0 : if (conf.proxyListUrl.empty() or not conf.proxyListEnabled) {
2638 0 : cb(getDhtProxyServer(conf.proxyServer));
2639 : } else {
2640 0 : loadCachedUrl(conf.proxyListUrl,
2641 0 : cachePath_ / "dhtproxylist",
2642 0 : std::chrono::hours(24 * 3),
2643 0 : [w = weak(), cb = std::move(cb)](const dht::http::Response& response) {
2644 0 : if (auto sthis = w.lock()) {
2645 0 : if (response.status_code == 200) {
2646 0 : cb(sthis->getDhtProxyServer(response.body));
2647 : } else {
2648 0 : cb(sthis->getDhtProxyServer(sthis->config().proxyServer));
2649 : }
2650 0 : }
2651 0 : });
2652 : }
2653 : } else {
2654 699 : cb(proxyServerCached_);
2655 : }
2656 699 : }
2657 :
2658 : std::string
2659 0 : JamiAccount::getDhtProxyServer(const std::string& serverList)
2660 : {
2661 0 : if (proxyServerCached_.empty()) {
2662 0 : std::vector<std::string> proxys;
2663 : // Split the list of servers
2664 0 : std::sregex_iterator begin = {serverList.begin(), serverList.end(), PROXY_REGEX}, end;
2665 0 : for (auto it = begin; it != end; ++it) {
2666 0 : auto& match = *it;
2667 0 : if (match[5].matched and match[6].matched) {
2668 : try {
2669 0 : auto start = std::stoi(match[5]), end = std::stoi(match[6]);
2670 0 : for (auto p = start; p <= end; p++)
2671 0 : proxys.emplace_back(match[1].str() + match[2].str() + ":"
2672 0 : + std::to_string(p));
2673 0 : } catch (...) {
2674 0 : JAMI_WARN("Malformed proxy, ignore it");
2675 0 : continue;
2676 0 : }
2677 : } else {
2678 0 : proxys.emplace_back(match[0].str());
2679 : }
2680 0 : }
2681 0 : if (proxys.empty())
2682 0 : return {};
2683 : // Select one of the list as the current proxy.
2684 0 : auto randIt = proxys.begin();
2685 0 : std::advance(randIt,
2686 0 : std::uniform_int_distribution<unsigned long>(0, proxys.size() - 1)(rand));
2687 0 : proxyServerCached_ = *randIt;
2688 : // Cache it!
2689 0 : dhtnet::fileutils::check_dir(cachePath_, 0700);
2690 0 : auto proxyCachePath = cachePath_ / "dhtproxy";
2691 0 : std::ofstream file(proxyCachePath);
2692 0 : JAMI_DEBUG("Cache DHT proxy server: {}", proxyServerCached_);
2693 0 : Json::Value node(Json::objectValue);
2694 0 : node[getProxyConfigKey()] = proxyServerCached_;
2695 0 : if (file.is_open())
2696 0 : file << node;
2697 : else
2698 0 : JAMI_WARNING("Unable to write into {}", proxyCachePath);
2699 0 : }
2700 0 : return proxyServerCached_;
2701 : }
2702 :
2703 : MatchRank
2704 0 : JamiAccount::matches(std::string_view userName, std::string_view server) const
2705 : {
2706 0 : if (not accountManager_ or not accountManager_->getInfo())
2707 0 : return MatchRank::NONE;
2708 :
2709 0 : if (userName == accountManager_->getInfo()->accountId
2710 0 : || server == accountManager_->getInfo()->accountId
2711 0 : || userName == accountManager_->getInfo()->deviceId) {
2712 0 : JAMI_LOG("Matching account ID in request with username {}", userName);
2713 0 : return MatchRank::FULL;
2714 : } else {
2715 0 : return MatchRank::NONE;
2716 : }
2717 : }
2718 :
2719 : std::string
2720 595 : JamiAccount::getFromUri() const
2721 : {
2722 595 : const std::string uri = "<sip:" + accountManager_->getInfo()->accountId + "@ring.dht>";
2723 596 : if (not config().displayName.empty())
2724 1192 : return "\"" + config().displayName + "\" " + uri;
2725 0 : return uri;
2726 596 : }
2727 :
2728 : std::string
2729 590 : JamiAccount::getToUri(const std::string& to) const
2730 : {
2731 590 : auto username = to;
2732 590 : string_replace(username, "sip:", "");
2733 590 : return fmt::format("<sips:{};transport=tls>", username);
2734 590 : }
2735 :
2736 : std::string
2737 7 : getDisplayed(const std::string& conversationId, const std::string& messageId)
2738 : {
2739 : // implementing https://tools.ietf.org/rfc/rfc5438.txt
2740 : return fmt::format(
2741 : "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
2742 : "<imdn><message-id>{}</message-id>\n"
2743 : "{}"
2744 : "<display-notification><status><displayed/></status></display-notification>\n"
2745 : "</imdn>",
2746 : messageId,
2747 14 : conversationId.empty() ? "" : "<conversation>" + conversationId + "</conversation>");
2748 : }
2749 :
2750 : std::string
2751 5 : getPIDF(const std::string& note)
2752 : {
2753 : // implementing https://datatracker.ietf.org/doc/html/rfc3863
2754 : return fmt::format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
2755 : "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\">\n"
2756 : " <tuple>\n"
2757 : " <status>\n"
2758 : " <basic>{}</basic>\n"
2759 : " </status>\n"
2760 : " </tuple>\n"
2761 : "</presence>",
2762 5 : note);
2763 : }
2764 :
2765 : void
2766 5 : JamiAccount::setIsComposing(const std::string& conversationUri, bool isWriting)
2767 : {
2768 5 : Uri uri(conversationUri);
2769 5 : std::string conversationId = {};
2770 5 : if (uri.scheme() == Uri::Scheme::SWARM) {
2771 5 : conversationId = uri.authority();
2772 : } else {
2773 0 : return;
2774 : }
2775 :
2776 5 : if (auto cm = convModule(true)) {
2777 5 : if (auto typer = cm->getTypers(conversationId)) {
2778 5 : if (isWriting)
2779 4 : typer->addTyper(getUsername(), true);
2780 : else
2781 1 : typer->removeTyper(getUsername(), true);
2782 5 : }
2783 : }
2784 5 : }
2785 :
2786 : bool
2787 9 : JamiAccount::setMessageDisplayed(const std::string& conversationUri,
2788 : const std::string& messageId,
2789 : int status)
2790 : {
2791 9 : Uri uri(conversationUri);
2792 9 : std::string conversationId = {};
2793 9 : if (uri.scheme() == Uri::Scheme::SWARM)
2794 9 : conversationId = uri.authority();
2795 : auto sendMessage = status == (int) libjami::Account::MessageStates::DISPLAYED
2796 9 : && isReadReceiptEnabled();
2797 9 : if (!conversationId.empty())
2798 9 : sendMessage &= convModule()->onMessageDisplayed(getUsername(), conversationId, messageId);
2799 9 : if (sendMessage)
2800 14 : sendInstantMessage(uri.authority(),
2801 14 : {{MIME_TYPE_IMDN, getDisplayed(conversationId, messageId)}});
2802 9 : return true;
2803 9 : }
2804 :
2805 : std::string
2806 194 : JamiAccount::getContactHeader(const std::shared_ptr<SipTransport>& sipTransport)
2807 : {
2808 194 : if (sipTransport and sipTransport->get() != nullptr) {
2809 194 : auto transport = sipTransport->get();
2810 194 : auto* td = reinterpret_cast<tls::AbstractSIPTransport::TransportData*>(transport);
2811 194 : auto address = td->self->getLocalAddress().toString(true);
2812 194 : bool reliable = transport->flag & PJSIP_TRANSPORT_RELIABLE;
2813 : return fmt::format("\"{}\" <sips:{}{}{};transport={}>",
2814 194 : config().displayName,
2815 194 : id_.second->getId().toString(),
2816 194 : address.empty() ? "" : "@",
2817 : address,
2818 388 : reliable ? "tls" : "dtls");
2819 194 : } else {
2820 0 : JAMI_ERR("getContactHeader: no SIP transport provided");
2821 : return fmt::format("\"{}\" <sips:{}@ring.dht>",
2822 0 : config().displayName,
2823 0 : id_.second->getId().toString());
2824 : }
2825 : }
2826 :
2827 : void
2828 67 : JamiAccount::addContact(const std::string& uri, bool confirmed)
2829 : {
2830 67 : dht::InfoHash h(uri);
2831 67 : if (not h) {
2832 3 : JAMI_ERROR("addContact: invalid contact URI");
2833 1 : return;
2834 : }
2835 66 : auto conversation = convModule()->getOneToOneConversation(uri);
2836 66 : if (!confirmed && conversation.empty())
2837 65 : conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
2838 66 : std::unique_lock<std::recursive_mutex> lock(configurationMutex_);
2839 66 : if (accountManager_)
2840 66 : accountManager_->addContact(h, confirmed, conversation);
2841 : else
2842 0 : JAMI_WARNING("[Account {}] addContact: account not loaded", getAccountID());
2843 66 : }
2844 :
2845 : void
2846 20 : JamiAccount::removeContact(const std::string& uri, bool ban)
2847 : {
2848 20 : std::lock_guard lock(configurationMutex_);
2849 20 : if (accountManager_)
2850 20 : accountManager_->removeContact(uri, ban);
2851 : else
2852 0 : JAMI_WARNING("[Account {}] removeContact: account not loaded", getAccountID());
2853 20 : }
2854 :
2855 : std::map<std::string, std::string>
2856 40 : JamiAccount::getContactDetails(const std::string& uri) const
2857 : {
2858 40 : std::lock_guard lock(configurationMutex_);
2859 40 : return accountManager_ ? accountManager_->getContactDetails(uri)
2860 80 : : std::map<std::string, std::string> {};
2861 40 : }
2862 :
2863 : std::vector<std::map<std::string, std::string>>
2864 1 : JamiAccount::getContacts(bool includeRemoved) const
2865 : {
2866 1 : std::lock_guard lock(configurationMutex_);
2867 1 : if (not accountManager_)
2868 0 : return {};
2869 1 : return accountManager_->getContacts(includeRemoved);
2870 1 : }
2871 :
2872 : /* trust requests */
2873 :
2874 : std::vector<std::map<std::string, std::string>>
2875 710 : JamiAccount::getTrustRequests() const
2876 : {
2877 710 : std::lock_guard lock(configurationMutex_);
2878 710 : return accountManager_ ? accountManager_->getTrustRequests()
2879 1420 : : std::vector<std::map<std::string, std::string>> {};
2880 710 : }
2881 :
2882 : bool
2883 29 : JamiAccount::acceptTrustRequest(const std::string& from, bool includeConversation)
2884 : {
2885 29 : dht::InfoHash h(from);
2886 29 : if (not h) {
2887 0 : JAMI_ERROR("addContact: invalid contact URI");
2888 0 : return false;
2889 : }
2890 29 : std::unique_lock<std::recursive_mutex> lock(configurationMutex_);
2891 29 : if (accountManager_) {
2892 29 : if (!accountManager_->acceptTrustRequest(from, includeConversation)) {
2893 : // Note: unused for swarm
2894 : // Typically the case where the trust request doesn't exists, only incoming DHT messages
2895 0 : return accountManager_->addContact(h, true);
2896 : }
2897 29 : return true;
2898 : }
2899 0 : JAMI_WARNING("[Account {}] acceptTrustRequest: account not loaded", getAccountID());
2900 0 : return false;
2901 29 : }
2902 :
2903 : bool
2904 2 : JamiAccount::discardTrustRequest(const std::string& from)
2905 : {
2906 : // Remove 1:1 generated conv requests
2907 2 : auto requests = getTrustRequests();
2908 4 : for (const auto& req : requests) {
2909 2 : if (req.at(libjami::Account::TrustRequest::FROM) == from) {
2910 2 : convModule()->declineConversationRequest(
2911 2 : req.at(libjami::Account::TrustRequest::CONVERSATIONID));
2912 : }
2913 : }
2914 :
2915 : // Remove trust request
2916 2 : std::lock_guard lock(configurationMutex_);
2917 2 : if (accountManager_)
2918 2 : return accountManager_->discardTrustRequest(from);
2919 0 : JAMI_WARNING("[Account {:s}] discardTrustRequest: account not loaded", getAccountID());
2920 0 : return false;
2921 2 : }
2922 :
2923 : void
2924 3 : JamiAccount::declineConversationRequest(const std::string& conversationId)
2925 : {
2926 3 : auto peerId = convModule()->peerFromConversationRequest(conversationId);
2927 3 : convModule()->declineConversationRequest(conversationId);
2928 3 : if (!peerId.empty()) {
2929 3 : std::lock_guard lock(configurationMutex_);
2930 3 : if (auto info = accountManager_->getInfo()) {
2931 : // Verify if we have a trust request with this peer + convId
2932 3 : auto req = info->contacts->getTrustRequest(dht::InfoHash(peerId));
2933 6 : if (req.find(libjami::Account::TrustRequest::CONVERSATIONID) != req.end()
2934 6 : && req.at(libjami::Account::TrustRequest::CONVERSATIONID) == conversationId) {
2935 1 : accountManager_->discardTrustRequest(peerId);
2936 3 : JAMI_DEBUG("[Account {:s}] Declined trust request with {:s}",
2937 : getAccountID(),
2938 : peerId);
2939 : }
2940 3 : }
2941 3 : }
2942 3 : }
2943 :
2944 : void
2945 59 : JamiAccount::sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload)
2946 : {
2947 59 : dht::InfoHash h(to);
2948 59 : if (not h) {
2949 0 : JAMI_ERROR("addContact: invalid contact URI");
2950 0 : return;
2951 : }
2952 : // Here we cache payload sent by the client
2953 59 : auto requestPath = cachePath_ / "requests";
2954 59 : dhtnet::fileutils::recursive_mkdir(requestPath, 0700);
2955 59 : auto cachedFile = requestPath / to;
2956 59 : std::ofstream req(cachedFile, std::ios::trunc | std::ios::binary);
2957 59 : if (!req.is_open()) {
2958 0 : JAMI_ERROR("Unable to write data to {}", cachedFile);
2959 0 : return;
2960 : }
2961 :
2962 59 : if (not payload.empty()) {
2963 3 : req.write(reinterpret_cast<const char*>(payload.data()), payload.size());
2964 : }
2965 :
2966 59 : if (payload.size() >= 64000) {
2967 1 : JAMI_WARN() << "Trust request is too big. Remove payload";
2968 : }
2969 :
2970 59 : auto conversation = convModule()->getOneToOneConversation(to);
2971 59 : if (conversation.empty())
2972 0 : conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
2973 59 : if (not conversation.empty()) {
2974 59 : std::lock_guard lock(configurationMutex_);
2975 59 : if (accountManager_)
2976 118 : accountManager_->sendTrustRequest(to,
2977 : conversation,
2978 118 : payload.size() >= 64000 ? std::vector<uint8_t> {}
2979 : : payload);
2980 : else
2981 0 : JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
2982 59 : } else
2983 0 : JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
2984 59 : }
2985 :
2986 : void
2987 289 : JamiAccount::forEachDevice(const dht::InfoHash& to,
2988 : std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)>&& op,
2989 : std::function<void(bool)>&& end)
2990 : {
2991 289 : accountManager_->forEachDevice(to, std::move(op), std::move(end));
2992 289 : }
2993 :
2994 : uint64_t
2995 18588 : JamiAccount::sendTextMessage(const std::string& to,
2996 : const std::string& deviceId,
2997 : const std::map<std::string, std::string>& payloads,
2998 : uint64_t refreshToken,
2999 : bool onlyConnected)
3000 : {
3001 18588 : Uri uri(to);
3002 18591 : if (uri.scheme() == Uri::Scheme::SWARM) {
3003 0 : sendInstantMessage(uri.authority(), payloads);
3004 0 : return 0;
3005 : }
3006 :
3007 18591 : std::string toUri;
3008 : try {
3009 18591 : toUri = parseJamiUri(to);
3010 0 : } catch (...) {
3011 0 : JAMI_ERROR("Failed to send a text message due to an invalid URI {}", to);
3012 0 : return 0;
3013 0 : }
3014 18589 : if (payloads.size() != 1) {
3015 0 : JAMI_ERROR("Multi-part im is not supported yet by JamiAccount");
3016 0 : return 0;
3017 : }
3018 18588 : return SIPAccountBase::sendTextMessage(toUri, deviceId, payloads, refreshToken, onlyConnected);
3019 18591 : }
3020 :
3021 : void
3022 17734 : JamiAccount::sendMessage(const std::string& to,
3023 : const std::string& deviceId,
3024 : const std::map<std::string, std::string>& payloads,
3025 : uint64_t token,
3026 : bool retryOnTimeout,
3027 : bool onlyConnected)
3028 : {
3029 17734 : std::string toUri;
3030 : try {
3031 17734 : toUri = parseJamiUri(to);
3032 0 : } catch (...) {
3033 0 : JAMI_ERROR("[Account {}] Failed to send a text message due to an invalid URI {}",
3034 : getAccountID(),
3035 : to);
3036 0 : if (!onlyConnected)
3037 0 : messageEngine_.onMessageSent(to, token, false, deviceId);
3038 0 : return;
3039 0 : }
3040 17734 : if (payloads.size() != 1) {
3041 0 : JAMI_ERROR("Multi-part im is not supported");
3042 0 : if (!onlyConnected)
3043 0 : messageEngine_.onMessageSent(toUri, token, false, deviceId);
3044 0 : return;
3045 : }
3046 :
3047 17734 : auto devices = std::make_shared<std::set<DeviceId>>();
3048 :
3049 : // Use the Message channel if available
3050 17734 : std::unique_lock clk(connManagerMtx_);
3051 : auto* handler = static_cast<MessageChannelHandler*>(
3052 17734 : channelHandlers_[Uri::Scheme::MESSAGE].get());
3053 17734 : if (!handler) {
3054 16 : clk.unlock();
3055 16 : if (!onlyConnected)
3056 16 : messageEngine_.onMessageSent(to, token, false, deviceId);
3057 16 : return;
3058 : }
3059 17718 : const auto& payload = *payloads.begin();
3060 17718 : MessageChannelHandler::Message msg;
3061 17716 : msg.t = payload.first;
3062 17718 : msg.c = payload.second;
3063 17718 : auto device = deviceId.empty() ? DeviceId() : DeviceId(deviceId);
3064 17717 : if (deviceId.empty()) {
3065 3103 : bool sent = false;
3066 3103 : auto conns = handler->getChannels(toUri);
3067 3104 : clk.unlock();
3068 4293 : for (const auto& conn : conns) {
3069 1189 : if (MessageChannelHandler::sendMessage(conn, msg)) {
3070 1189 : devices->emplace(conn->deviceId());
3071 1189 : sent = true;
3072 : }
3073 : }
3074 3104 : if (sent) {
3075 1122 : if (!onlyConnected)
3076 1110 : runOnMainThread([w = weak(), to, token, deviceId]() {
3077 1110 : if (auto acc = w.lock())
3078 1110 : acc->messageEngine_.onMessageSent(to, token, true, deviceId);
3079 1110 : });
3080 1122 : return;
3081 : }
3082 3104 : } else {
3083 14614 : if (auto conn = handler->getChannel(toUri, device)) {
3084 14566 : clk.unlock();
3085 14566 : if (MessageChannelHandler::sendMessage(conn, msg)) {
3086 14566 : if (!onlyConnected)
3087 14561 : runOnMainThread([w = weak(), to, token, deviceId]() {
3088 14561 : if (auto acc = w.lock())
3089 14561 : acc->messageEngine_.onMessageSent(to, token, true, deviceId);
3090 14561 : });
3091 14566 : return;
3092 : }
3093 14614 : }
3094 : }
3095 2030 : if (clk)
3096 48 : clk.unlock();
3097 :
3098 2030 : std::unique_lock lk(sipConnsMtx_);
3099 14536 : for (auto& [key, value] : sipConns_) {
3100 12508 : if (key.first != to or value.empty())
3101 12210 : continue;
3102 298 : if (!deviceId.empty() && key.second != device)
3103 0 : continue;
3104 298 : if (!devices->emplace(key.second).second)
3105 0 : continue;
3106 :
3107 298 : auto& conn = value.back();
3108 298 : auto& channel = conn.channel;
3109 :
3110 : // Set input token into callback
3111 298 : auto ctx = std::make_unique<TextMessageCtx>();
3112 298 : ctx->acc = weak();
3113 298 : ctx->to = to;
3114 298 : ctx->deviceId = device;
3115 298 : ctx->id = token;
3116 298 : ctx->onlyConnected = onlyConnected;
3117 298 : ctx->retryOnTimeout = retryOnTimeout;
3118 298 : ctx->channel = channel;
3119 :
3120 : try {
3121 596 : auto res = sendSIPMessage(conn,
3122 : to,
3123 298 : ctx.release(),
3124 : token,
3125 : payloads,
3126 298 : [](void* token, pjsip_event* event) {
3127 : std::shared_ptr<TextMessageCtx> c {
3128 298 : (TextMessageCtx*) token};
3129 298 : auto code = event->body.tsx_state.tsx->status_code;
3130 298 : runOnMainThread([c = std::move(c), code]() {
3131 298 : if (c) {
3132 298 : if (auto acc = c->acc.lock())
3133 298 : acc->onSIPMessageSent(std::move(c), code);
3134 : }
3135 298 : });
3136 298 : });
3137 298 : if (!res) {
3138 0 : if (!onlyConnected)
3139 0 : messageEngine_.onMessageSent(to, token, false, deviceId);
3140 0 : devices->erase(key.second);
3141 0 : continue;
3142 : }
3143 0 : } catch (const std::runtime_error& ex) {
3144 0 : JAMI_WARNING("{}", ex.what());
3145 0 : if (!onlyConnected)
3146 0 : messageEngine_.onMessageSent(to, token, false, deviceId);
3147 0 : devices->erase(key.second);
3148 : // Remove connection in incorrect state
3149 0 : shutdownSIPConnection(channel, to, key.second);
3150 0 : continue;
3151 0 : }
3152 :
3153 298 : if (key.second == device) {
3154 2 : return;
3155 : }
3156 298 : }
3157 2028 : lk.unlock();
3158 :
3159 2028 : if (onlyConnected)
3160 10 : return;
3161 : // We are unable to send the message directly, try connecting
3162 2018 : messageEngine_.onMessageSent(to, token, false, deviceId);
3163 :
3164 : // Get conversation id, which will be used by the iOS notification extension
3165 : // to load the conversation.
3166 1695 : auto extractIdFromJson = [](const std::string& jsonData) -> std::string {
3167 1695 : Json::Value parsed;
3168 1695 : Json::CharReaderBuilder readerBuilder;
3169 1695 : auto reader = std::unique_ptr<Json::CharReader>(readerBuilder.newCharReader());
3170 1695 : std::string errors;
3171 :
3172 1695 : if (reader->parse(jsonData.c_str(), jsonData.c_str() + jsonData.size(), &parsed, &errors)) {
3173 1695 : auto value = parsed.get("id", Json::nullValue);
3174 1695 : if (value && value.isString()) {
3175 1695 : return value.asString();
3176 : }
3177 1695 : } else {
3178 0 : JAMI_WARNING("Unable to parse jsonData to get conversation ID");
3179 : }
3180 0 : return "";
3181 1695 : };
3182 :
3183 : // get request type
3184 2018 : auto payload_type = msg.t;
3185 2018 : if (payload_type == MIME_TYPE_GIT) {
3186 1695 : std::string id = extractIdFromJson(msg.c);
3187 1695 : if (!id.empty()) {
3188 1695 : payload_type += "/" + id;
3189 : }
3190 1695 : }
3191 :
3192 2018 : if (deviceId.empty()) {
3193 1972 : auto toH = dht::InfoHash(toUri);
3194 : // Find listening devices for this account
3195 1972 : accountManager_->forEachDevice(toH,
3196 1944 : [this,
3197 : to,
3198 : devices,
3199 : payload_type,
3200 : currentDevice = DeviceId(currentDeviceId())](
3201 1010 : const std::shared_ptr<dht::crypto::PublicKey>& dev) {
3202 : // Test if already sent
3203 1944 : auto deviceId = dev->getLongId();
3204 1944 : if (!devices->emplace(deviceId).second
3205 1944 : || deviceId == currentDevice) {
3206 934 : return;
3207 : }
3208 :
3209 : // Else, ask for a channel to send the message
3210 1010 : requestSIPConnection(to, deviceId, payload_type);
3211 : });
3212 : } else {
3213 46 : requestSIPConnection(to, device, payload_type);
3214 : }
3215 64878 : }
3216 :
3217 : void
3218 297 : JamiAccount::onSIPMessageSent(const std::shared_ptr<TextMessageCtx>& ctx, int code)
3219 : {
3220 297 : if (code == PJSIP_SC_OK) {
3221 297 : if (!ctx->onlyConnected)
3222 296 : messageEngine_.onMessageSent(ctx->to,
3223 296 : ctx->id,
3224 : true,
3225 592 : ctx->deviceId ? ctx->deviceId.toString() : "");
3226 : } else {
3227 : // Note: This can be called from PJSIP's eventloop while
3228 : // sipConnsMtx_ is locked. So we should retrigger the shutdown.
3229 0 : auto acc = ctx->acc.lock();
3230 0 : if (not acc)
3231 0 : return;
3232 0 : JAMI_WARN("Timeout when send a message, close current connection");
3233 0 : shutdownSIPConnection(ctx->channel, ctx->to, ctx->deviceId);
3234 : // This MUST be done after closing the connection to avoid race condition
3235 : // with messageEngine_
3236 0 : if (!ctx->onlyConnected)
3237 0 : messageEngine_.onMessageSent(ctx->to,
3238 0 : ctx->id,
3239 : false,
3240 0 : ctx->deviceId ? ctx->deviceId.toString() : "");
3241 :
3242 : // In that case, the peer typically changed its connectivity.
3243 : // After closing sockets with that peer, we try to re-connect to
3244 : // that peer one time.
3245 0 : if (ctx->retryOnTimeout)
3246 0 : messageEngine_.onPeerOnline(ctx->to, ctx->deviceId ? ctx->deviceId.toString() : "");
3247 0 : }
3248 : }
3249 :
3250 : dhtnet::IceTransportOptions
3251 115 : JamiAccount::getIceOptions() const noexcept
3252 : {
3253 115 : return connectionManager_->getIceOptions();
3254 : }
3255 :
3256 : void
3257 11 : JamiAccount::getIceOptions(std::function<void(dhtnet::IceTransportOptions&&)> cb) const noexcept
3258 : {
3259 11 : return connectionManager_->getIceOptions(std::move(cb));
3260 : }
3261 :
3262 : dhtnet::IpAddr
3263 101 : JamiAccount::getPublishedIpAddress(uint16_t family) const
3264 : {
3265 101 : return connectionManager_->getPublishedIpAddress(family);
3266 : }
3267 :
3268 : bool
3269 0 : JamiAccount::setPushNotificationToken(const std::string& token)
3270 : {
3271 0 : if (SIPAccountBase::setPushNotificationToken(token)) {
3272 0 : JAMI_WARNING("[Account {:s}] setPushNotificationToken: {:s}", getAccountID(), token);
3273 0 : if (dht_)
3274 0 : dht_->setPushNotificationToken(token);
3275 0 : return true;
3276 : }
3277 0 : return false;
3278 : }
3279 :
3280 : bool
3281 0 : JamiAccount::setPushNotificationTopic(const std::string& topic)
3282 : {
3283 0 : if (SIPAccountBase::setPushNotificationTopic(topic)) {
3284 0 : if (dht_)
3285 0 : dht_->setPushNotificationTopic(topic);
3286 0 : return true;
3287 : }
3288 0 : return false;
3289 : }
3290 :
3291 : bool
3292 0 : JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data)
3293 : {
3294 0 : if (SIPAccountBase::setPushNotificationConfig(data)) {
3295 0 : if (dht_) {
3296 0 : dht_->setPushNotificationPlatform(config_->platform);
3297 0 : dht_->setPushNotificationTopic(config_->notificationTopic);
3298 0 : dht_->setPushNotificationToken(config_->deviceKey);
3299 : }
3300 0 : return true;
3301 : }
3302 0 : return false;
3303 : }
3304 :
3305 : /**
3306 : * To be called by clients with relevant data when a push notification is received.
3307 : */
3308 : void
3309 0 : JamiAccount::pushNotificationReceived(const std::string& from,
3310 : const std::map<std::string, std::string>& data)
3311 : {
3312 0 : JAMI_WARNING("[Account {:s}] pushNotificationReceived: {:s}", getAccountID(), from);
3313 0 : dht_->pushNotificationReceived(data);
3314 0 : }
3315 :
3316 : std::string
3317 10 : JamiAccount::getUserUri() const
3318 : {
3319 : #ifdef HAVE_RINGNS
3320 10 : if (not registeredName_.empty())
3321 0 : return JAMI_URI_PREFIX + registeredName_;
3322 : #endif
3323 10 : return JAMI_URI_PREFIX + config().username;
3324 : }
3325 :
3326 : std::vector<libjami::Message>
3327 0 : JamiAccount::getLastMessages(const uint64_t& base_timestamp)
3328 : {
3329 0 : return SIPAccountBase::getLastMessages(base_timestamp);
3330 : }
3331 :
3332 : void
3333 0 : JamiAccount::startAccountPublish()
3334 : {
3335 0 : AccountPeerInfo info_pub;
3336 0 : info_pub.accountId = dht::InfoHash(accountManager_->getInfo()->accountId);
3337 0 : info_pub.displayName = config().displayName;
3338 0 : peerDiscovery_->startPublish<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, info_pub);
3339 0 : }
3340 :
3341 : void
3342 0 : JamiAccount::startAccountDiscovery()
3343 : {
3344 0 : auto id = dht::InfoHash(accountManager_->getInfo()->accountId);
3345 0 : peerDiscovery_->startDiscovery<AccountPeerInfo>(
3346 0 : PEER_DISCOVERY_JAMI_SERVICE, [this, id](AccountPeerInfo&& v, dht::SockAddr&&) {
3347 0 : std::lock_guard lc(discoveryMapMtx_);
3348 : // Make sure that account itself will not be recorded
3349 0 : if (v.accountId != id) {
3350 : // Create or find the old one
3351 0 : auto& dp = discoveredPeers_[v.accountId];
3352 0 : dp.displayName = v.displayName;
3353 0 : discoveredPeerMap_[v.accountId.toString()] = v.displayName;
3354 0 : if (dp.cleanupTask) {
3355 0 : dp.cleanupTask->cancel();
3356 : } else {
3357 : // Avoid repeat reception of same peer
3358 0 : JAMI_LOG("Account discovered: {}: {}", v.displayName, v.accountId.to_c_str());
3359 : // Send Added Peer and corrsponding accoundID
3360 0 : emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(getAccountID(),
3361 : v.accountId
3362 0 : .toString(),
3363 : 0,
3364 0 : v.displayName);
3365 : }
3366 0 : dp.cleanupTask = Manager::instance().scheduler().scheduleIn(
3367 0 : [w = weak(), p = v.accountId, a = v.displayName] {
3368 0 : if (auto this_ = w.lock()) {
3369 : {
3370 0 : std::lock_guard lc(this_->discoveryMapMtx_);
3371 0 : this_->discoveredPeers_.erase(p);
3372 0 : this_->discoveredPeerMap_.erase(p.toString());
3373 0 : }
3374 : // Send deleted peer
3375 0 : emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(
3376 0 : this_->getAccountID(), p.toString(), 1, a);
3377 0 : }
3378 0 : JAMI_INFO("Account removed from discovery list: %s", a.c_str());
3379 0 : },
3380 0 : PEER_DISCOVERY_EXPIRATION);
3381 : }
3382 0 : });
3383 0 : }
3384 :
3385 : std::map<std::string, std::string>
3386 0 : JamiAccount::getNearbyPeers() const
3387 : {
3388 0 : return discoveredPeerMap_;
3389 : }
3390 :
3391 : void
3392 0 : JamiAccount::sendProfileToPeers()
3393 : {
3394 0 : if (!connectionManager_)
3395 0 : return;
3396 0 : std::set<std::string> peers;
3397 0 : const auto& accountUri = accountManager_->getInfo()->accountId;
3398 : // TODO: avoid using getConnectionList
3399 0 : for (const auto& connection : connectionManager_->getConnectionList()) {
3400 0 : const auto& device = connection.at("device");
3401 0 : const auto& peer = connection.at("peer");
3402 0 : if (!peers.emplace(peer).second)
3403 0 : continue;
3404 0 : if (peer == accountUri) {
3405 0 : sendProfile("", accountUri, device);
3406 0 : continue;
3407 : }
3408 0 : const auto& conversationId = convModule()->getOneToOneConversation(peer);
3409 0 : if (!conversationId.empty()) {
3410 0 : sendProfile(conversationId, peer, device);
3411 : }
3412 0 : }
3413 0 : }
3414 :
3415 : void
3416 0 : JamiAccount::updateProfile(const std::string& displayName,
3417 : const std::string& avatar,
3418 : const std::string& fileType,
3419 : int32_t flag)
3420 : {
3421 : // if the fileType is empty then only the display name will be upated
3422 :
3423 0 : const auto& accountUri = accountManager_->getInfo()->accountId;
3424 0 : const auto& path = profilePath();
3425 0 : const auto& profiles = idPath_ / "profiles";
3426 :
3427 : try {
3428 0 : if (!std::filesystem::exists(profiles)) {
3429 0 : std::filesystem::create_directories(profiles);
3430 : }
3431 0 : } catch (const std::exception& e) {
3432 0 : JAMI_ERROR("Failed to create profiles directory: {}", e.what());
3433 0 : return;
3434 0 : }
3435 :
3436 0 : const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(accountUri));
3437 :
3438 0 : auto profile = getProfileVcard();
3439 0 : if (profile.empty()) {
3440 0 : profile = vCard::utils::initVcard();
3441 : }
3442 :
3443 0 : profile["FN"] = displayName;
3444 0 : editConfig([&](JamiAccountConfig& config) { config.displayName = displayName; });
3445 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(),
3446 0 : getAccountDetails());
3447 :
3448 0 : if (!fileType.empty()) {
3449 0 : const std::string& key = "PHOTO;ENCODING=BASE64;TYPE=" + fileType;
3450 0 : if (flag == 0) {
3451 0 : vCard::utils::removeByKey(profile, "PHOTO");
3452 0 : const auto& avatarPath = std::filesystem::path(avatar);
3453 0 : if (std::filesystem::exists(avatarPath)) {
3454 : try {
3455 0 : profile[key] = base64::encode(fileutils::loadFile(avatarPath));
3456 0 : } catch (const std::exception& e) {
3457 0 : JAMI_ERROR("Failed to load avatar: {}", e.what());
3458 0 : }
3459 : }
3460 0 : } else if (flag == 1) {
3461 0 : vCard::utils::removeByKey(profile, "PHOTO");
3462 0 : profile[key] = avatar;
3463 : }
3464 0 : }
3465 0 : if (flag == 2) {
3466 0 : vCard::utils::removeByKey(profile, "PHOTO");
3467 : }
3468 : try {
3469 0 : std::filesystem::path tmpPath = vCardPath.string() + ".tmp";
3470 0 : std::ofstream file(tmpPath);
3471 0 : if (file.is_open()) {
3472 0 : file << vCard::utils::toString(profile);
3473 0 : file.close();
3474 0 : std::filesystem::rename(tmpPath, vCardPath);
3475 0 : fileutils::createFileLink(path, vCardPath);
3476 0 : emitSignal<libjami::ConfigurationSignal::ProfileReceived>(getAccountID(),
3477 : accountUri,
3478 0 : path.string());
3479 0 : sendProfileToPeers();
3480 : } else {
3481 0 : JAMI_ERROR("Unable to open file for writing: {}", tmpPath.string());
3482 : }
3483 0 : } catch (const std::exception& e) {
3484 0 : JAMI_ERROR("Error writing profile: {}", e.what());
3485 0 : }
3486 0 : }
3487 :
3488 : void
3489 799 : JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
3490 : {
3491 799 : Account::setActiveCodecs(list);
3492 799 : if (!hasActiveCodec(MEDIA_AUDIO))
3493 779 : setCodecActive(AV_CODEC_ID_OPUS);
3494 799 : if (!hasActiveCodec(MEDIA_VIDEO)) {
3495 779 : setCodecActive(AV_CODEC_ID_HEVC);
3496 779 : setCodecActive(AV_CODEC_ID_H264);
3497 779 : setCodecActive(AV_CODEC_ID_VP8);
3498 : }
3499 799 : config_->activeCodecs = getActiveCodecs(MEDIA_ALL);
3500 799 : }
3501 :
3502 : void
3503 11 : JamiAccount::sendInstantMessage(const std::string& convId,
3504 : const std::map<std::string, std::string>& msg)
3505 : {
3506 11 : auto members = convModule()->getConversationMembers(convId);
3507 11 : if (convId.empty() && members.empty()) {
3508 : // TODO remove, it's for old API for contacts
3509 0 : sendTextMessage(convId, "", msg);
3510 0 : return;
3511 : }
3512 33 : for (const auto& m : members) {
3513 22 : const auto& uri = m.at("uri");
3514 22 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
3515 : // Announce to all members that a new message is sent
3516 22 : sendMessage(uri, "", msg, token, false, true);
3517 : }
3518 11 : }
3519 :
3520 : bool
3521 16029 : JamiAccount::handleMessage(const std::string& from, const std::pair<std::string, std::string>& m)
3522 : {
3523 16029 : if (m.first == MIME_TYPE_GIT) {
3524 15518 : Json::Value json;
3525 15518 : std::string err;
3526 15519 : Json::CharReaderBuilder rbuilder;
3527 15522 : auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
3528 15518 : if (!reader->parse(m.second.data(), m.second.data() + m.second.size(), &json, &err)) {
3529 0 : JAMI_ERROR("Unable to parse server response: {}", err);
3530 0 : return false;
3531 : }
3532 :
3533 15522 : std::string deviceId = json["deviceId"].asString();
3534 15522 : std::string id = json["id"].asString();
3535 15521 : std::string commit = json["commit"].asString();
3536 : // fetchNewCommits will do heavy stuff like fetching, avoid to block SIP socket
3537 15521 : dht::ThreadPool::io().run([w = weak(), from, deviceId, id, commit] {
3538 15521 : if (auto shared = w.lock()) {
3539 15521 : if (auto cm = shared->convModule())
3540 15521 : cm->fetchNewCommits(from, deviceId, id, commit);
3541 15521 : }
3542 15521 : });
3543 15522 : return true;
3544 16033 : } else if (m.first == MIME_TYPE_INVITE) {
3545 150 : convModule()->onNeedConversationRequest(from, m.second);
3546 150 : return true;
3547 362 : } else if (m.first == MIME_TYPE_INVITE_JSON) {
3548 342 : Json::Value json;
3549 342 : std::string err;
3550 342 : Json::CharReaderBuilder rbuilder;
3551 342 : auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
3552 342 : if (!reader->parse(m.second.data(), m.second.data() + m.second.size(), &json, &err)) {
3553 0 : JAMI_ERROR("Unable to parse server response: {}", err);
3554 0 : return false;
3555 : }
3556 342 : convModule()->onConversationRequest(from, json);
3557 342 : return true;
3558 362 : } else if (m.first == MIME_TYPE_IM_COMPOSING) {
3559 : try {
3560 4 : static const std::regex COMPOSING_REGEX("<state>\\s*(\\w+)\\s*<\\/state>");
3561 4 : std::smatch matched_pattern;
3562 4 : std::regex_search(m.second, matched_pattern, COMPOSING_REGEX);
3563 4 : bool isComposing {false};
3564 4 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
3565 4 : isComposing = matched_pattern[1] == "active";
3566 : }
3567 4 : static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
3568 4 : std::regex_search(m.second, matched_pattern, CONVID_REGEX);
3569 4 : std::string conversationId = "";
3570 4 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
3571 4 : conversationId = matched_pattern[1];
3572 : }
3573 4 : if (!conversationId.empty()) {
3574 4 : if (auto cm = convModule(true)) {
3575 4 : if (auto typer = cm->getTypers(conversationId)) {
3576 4 : if (isComposing)
3577 3 : typer->addTyper(from);
3578 : else
3579 1 : typer->removeTyper(from);
3580 4 : }
3581 : }
3582 : }
3583 4 : return true;
3584 4 : } catch (const std::exception& e) {
3585 0 : JAMI_WARNING("Error parsing composing state: {}", e.what());
3586 0 : }
3587 16 : } else if (m.first == MIME_TYPE_IMDN) {
3588 : try {
3589 10 : static const std::regex IMDN_MSG_ID_REGEX("<message-id>\\s*(\\w+)\\s*<\\/message-id>");
3590 10 : std::smatch matched_pattern;
3591 :
3592 10 : std::regex_search(m.second, matched_pattern, IMDN_MSG_ID_REGEX);
3593 10 : std::string messageId;
3594 10 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
3595 10 : messageId = matched_pattern[1];
3596 : } else {
3597 0 : JAMI_WARNING("Message displayed: unable to parse message ID");
3598 0 : return false;
3599 : }
3600 :
3601 10 : static const std::regex STATUS_REGEX("<status>\\s*<(\\w+)\\/>\\s*<\\/status>");
3602 10 : std::regex_search(m.second, matched_pattern, STATUS_REGEX);
3603 10 : bool isDisplayed {false};
3604 10 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
3605 10 : isDisplayed = matched_pattern[1] == "displayed";
3606 : } else {
3607 0 : JAMI_WARNING("Message displayed: unable to parse status");
3608 0 : return false;
3609 : }
3610 :
3611 10 : static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
3612 10 : std::regex_search(m.second, matched_pattern, CONVID_REGEX);
3613 10 : std::string conversationId = "";
3614 10 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
3615 10 : conversationId = matched_pattern[1];
3616 : }
3617 :
3618 10 : if (!isReadReceiptEnabled())
3619 0 : return true;
3620 10 : if (isDisplayed) {
3621 10 : if (convModule()->onMessageDisplayed(from, conversationId, messageId)) {
3622 27 : JAMI_DEBUG("[message {}] Displayed by peer", messageId);
3623 18 : emitSignal<libjami::ConfigurationSignal::AccountMessageStatusChanged>(
3624 9 : accountID_,
3625 : conversationId,
3626 : from,
3627 : messageId,
3628 : static_cast<int>(libjami::Account::MessageStates::DISPLAYED));
3629 : }
3630 : }
3631 10 : return true;
3632 10 : } catch (const std::exception& e) {
3633 0 : JAMI_ERROR("Error parsing display notification: {}", e.what());
3634 0 : }
3635 6 : } else if (m.first == MIME_TYPE_PIDF) {
3636 6 : std::smatch matched_pattern;
3637 6 : static const std::regex BASIC_REGEX("<basic>([\\w\\s]+)<\\/basic>");
3638 6 : std::regex_search(m.second, matched_pattern, BASIC_REGEX);
3639 6 : std::string customStatus {};
3640 6 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
3641 4 : customStatus = matched_pattern[1];
3642 4 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
3643 : from,
3644 : static_cast<int>(
3645 : PresenceState::CONNECTED),
3646 : customStatus);
3647 4 : return true;
3648 : } else {
3649 6 : JAMI_WARNING("Presence: unable to parse status");
3650 : }
3651 10 : }
3652 :
3653 2 : return false;
3654 : }
3655 :
3656 : void
3657 1507 : JamiAccount::callConnectionClosed(const DeviceId& deviceId, bool eraseDummy)
3658 : {
3659 1507 : std::function<void(const DeviceId&, bool)> cb;
3660 : {
3661 1507 : std::lock_guard lk(onConnectionClosedMtx_);
3662 1507 : auto it = onConnectionClosed_.find(deviceId);
3663 1507 : if (it != onConnectionClosed_.end()) {
3664 89 : if (eraseDummy) {
3665 89 : cb = std::move(it->second);
3666 89 : onConnectionClosed_.erase(it);
3667 : } else {
3668 : // In this case a new subcall is created and the callback
3669 : // will be re-called once with eraseDummy = true
3670 0 : cb = it->second;
3671 : }
3672 : }
3673 1507 : }
3674 4521 : dht::ThreadPool::io().run(
3675 3014 : [w = weak(), cb = std::move(cb), id = deviceId, erase = std::move(eraseDummy)] {
3676 1506 : if (auto acc = w.lock()) {
3677 1507 : if (cb)
3678 89 : cb(id, erase);
3679 1507 : }
3680 1505 : });
3681 1507 : }
3682 :
3683 : void
3684 1726 : JamiAccount::requestMessageConnection(const std::string& peerId,
3685 : const DeviceId& deviceId,
3686 : const std::string& connectionType)
3687 : {
3688 : auto* handler = static_cast<MessageChannelHandler*>(
3689 1726 : channelHandlers_[Uri::Scheme::MESSAGE].get());
3690 1725 : if (deviceId) {
3691 1726 : if (auto connected = handler->getChannel(peerId, deviceId)) {
3692 501 : return;
3693 1725 : }
3694 : } else {
3695 0 : auto connected = handler->getChannels(peerId);
3696 0 : if (!connected.empty()) {
3697 0 : return;
3698 : }
3699 0 : }
3700 1225 : handler->connect(
3701 : deviceId,
3702 : "",
3703 751 : [w = weak(), peerId](std::shared_ptr<dhtnet::ChannelSocket> socket,
3704 : const DeviceId& deviceId) {
3705 751 : if (socket)
3706 700 : if (auto acc = w.lock()) {
3707 700 : acc->messageEngine_.onPeerOnline(peerId);
3708 700 : acc->messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
3709 700 : }
3710 751 : },
3711 : connectionType);
3712 : }
3713 :
3714 : void
3715 1726 : JamiAccount::requestSIPConnection(const std::string& peerId,
3716 : const DeviceId& deviceId,
3717 : const std::string& connectionType,
3718 : bool forceNewConnection,
3719 : const std::shared_ptr<SIPCall>& pc)
3720 : {
3721 1726 : requestMessageConnection(peerId, deviceId, connectionType);
3722 1726 : if (peerId == getUsername()) {
3723 118 : if (!syncModule()->isConnected(deviceId))
3724 86 : channelHandlers_[Uri::Scheme::SYNC]
3725 86 : ->connect(deviceId,
3726 : "",
3727 78 : [](std::shared_ptr<dhtnet::ChannelSocket> socket,
3728 78 : const DeviceId& deviceId) {});
3729 : }
3730 :
3731 5178 : JAMI_LOG("[Account {}] Request SIP connection to peer {} on device {}",
3732 : getAccountID(),
3733 : peerId,
3734 : deviceId);
3735 :
3736 : // If a connection already exists or is in progress, no need to do this
3737 1726 : std::lock_guard lk(sipConnsMtx_);
3738 1726 : auto id = std::make_pair(peerId, deviceId);
3739 :
3740 1726 : if (sipConns_.find(id) != sipConns_.end()) {
3741 1494 : JAMI_LOG("[Account {}] A SIP connection with {} already exists", getAccountID(), deviceId);
3742 498 : return;
3743 : }
3744 : // If not present, create it
3745 1228 : std::lock_guard lkCM(connManagerMtx_);
3746 1228 : if (!connectionManager_)
3747 1 : return;
3748 : // Note, Even if we send 50 "sip" request, the connectionManager_ will only use one socket.
3749 : // however, this will still ask for multiple channels, so only ask
3750 : // if there is no pending request
3751 1227 : if (!forceNewConnection && connectionManager_->isConnecting(deviceId, "sip")) {
3752 1419 : JAMI_LOG("[Account {}] Already connecting to {}", getAccountID(), deviceId);
3753 473 : return;
3754 : }
3755 2262 : JAMI_LOG("[Account {}] Ask {} for a new SIP channel", getAccountID(), deviceId);
3756 2262 : connectionManager_->connectDevice(
3757 : deviceId,
3758 : "sip",
3759 754 : [w = weak(),
3760 754 : id = std::move(id),
3761 754 : pc = std::move(pc)](std::shared_ptr<dhtnet::ChannelSocket> socket, const DeviceId&) {
3762 754 : if (socket)
3763 704 : return;
3764 51 : auto shared = w.lock();
3765 51 : if (!shared)
3766 1 : return;
3767 : // If this is triggered, this means that the
3768 : // connectDevice didn't get any response from the DHT.
3769 : // Stop searching pending call.
3770 50 : shared->callConnectionClosed(id.second, true);
3771 50 : if (pc)
3772 3 : pc->onFailure();
3773 51 : },
3774 : false,
3775 : forceNewConnection,
3776 : connectionType);
3777 3172 : }
3778 :
3779 : bool
3780 2572 : JamiAccount::isConnectedWith(const DeviceId& deviceId) const
3781 : {
3782 2572 : std::lock_guard lkCM(connManagerMtx_);
3783 2572 : if (connectionManager_)
3784 2572 : return connectionManager_->isConnected(deviceId);
3785 0 : return false;
3786 2572 : }
3787 :
3788 : void
3789 3 : JamiAccount::sendPresenceNote(const std::string& note)
3790 : {
3791 3 : if (auto info = accountManager_->getInfo()) {
3792 3 : if (!info || !info->contacts)
3793 0 : return;
3794 3 : presenceNote_ = note;
3795 3 : auto contacts = info->contacts->getContacts();
3796 3 : std::vector<SipConnectionKey> keys;
3797 : {
3798 3 : std::lock_guard lk(sipConnsMtx_);
3799 7 : for (auto& [key, conns] : sipConns_) {
3800 4 : keys.push_back(key);
3801 : }
3802 3 : }
3803 3 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
3804 12 : std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
3805 7 : for (auto& key : keys) {
3806 4 : sendMessage(key.first, key.second.toString(), msg, token, false, true);
3807 : }
3808 3 : }
3809 : }
3810 :
3811 : void
3812 1142 : JamiAccount::sendProfile(const std::string& convId,
3813 : const std::string& peerUri,
3814 : const std::string& deviceId)
3815 : {
3816 1142 : auto accProfilePath = profilePath();
3817 1142 : if (not std::filesystem::is_regular_file(accProfilePath))
3818 1134 : return;
3819 7 : auto currentSha3 = fileutils::sha3File(accProfilePath);
3820 : // VCard sync for peerUri
3821 7 : if (not needToSendProfile(peerUri, deviceId, currentSha3)) {
3822 0 : JAMI_DEBUG("Peer {} already got an up-to-date vCard", peerUri);
3823 0 : return;
3824 : }
3825 : // We need a new channel
3826 21 : transferFile(convId,
3827 14 : accProfilePath.string(),
3828 : deviceId,
3829 : "profile.vcf",
3830 : "",
3831 : 0,
3832 : 0,
3833 : currentSha3,
3834 : fileutils::lastWriteTimeInSeconds(accProfilePath),
3835 7 : [accId = getAccountID(), peerUri, deviceId]() {
3836 : // Mark the VCard as sent
3837 10 : auto sendDir = fileutils::get_cache_dir() / accId / "vcard" / peerUri;
3838 5 : auto path = sendDir / deviceId;
3839 5 : dhtnet::fileutils::recursive_mkdir(sendDir);
3840 5 : std::lock_guard lock(dhtnet::fileutils::getFileLock(path));
3841 5 : if (std::filesystem::is_regular_file(path))
3842 0 : return;
3843 5 : std::ofstream p(path);
3844 5 : });
3845 1141 : }
3846 :
3847 : bool
3848 7 : JamiAccount::needToSendProfile(const std::string& peerUri,
3849 : const std::string& deviceId,
3850 : const std::string& sha3Sum)
3851 : {
3852 7 : std::string previousSha3 {};
3853 7 : auto vCardPath = cachePath_ / "vcard";
3854 7 : auto sha3Path = vCardPath / "sha3";
3855 7 : dhtnet::fileutils::check_dir(vCardPath, 0700);
3856 : try {
3857 10 : previousSha3 = fileutils::loadTextFile(sha3Path);
3858 3 : } catch (...) {
3859 3 : fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
3860 3 : return true;
3861 3 : }
3862 4 : if (sha3Sum != previousSha3) {
3863 : // Incorrect sha3 stored. Update it
3864 0 : dhtnet::fileutils::removeAll(vCardPath, true);
3865 0 : dhtnet::fileutils::check_dir(vCardPath, 0700);
3866 0 : fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
3867 0 : return true;
3868 : }
3869 4 : auto peerPath = vCardPath / peerUri;
3870 4 : dhtnet::fileutils::recursive_mkdir(peerPath);
3871 4 : return not std::filesystem::is_regular_file(peerPath / deviceId);
3872 7 : }
3873 :
3874 : bool
3875 298 : JamiAccount::sendSIPMessage(SipConnection& conn,
3876 : const std::string& to,
3877 : void* ctx,
3878 : uint64_t token,
3879 : const std::map<std::string, std::string>& data,
3880 : pjsip_endpt_send_callback cb)
3881 : {
3882 298 : auto transport = conn.transport;
3883 298 : auto channel = conn.channel;
3884 298 : if (!channel)
3885 0 : throw std::runtime_error(
3886 0 : "A SIP transport exists without Channel, this is a bug. Please report");
3887 298 : auto remote_address = channel->getRemoteAddress();
3888 298 : if (!remote_address)
3889 0 : return false;
3890 :
3891 : // Build SIP Message
3892 : // "deviceID@IP"
3893 894 : auto toURI = getToUri(fmt::format("{}@{}", to, remote_address.toString(true)));
3894 298 : std::string from = getFromUri();
3895 :
3896 : // Build SIP message
3897 298 : constexpr pjsip_method msg_method = {PJSIP_OTHER_METHOD,
3898 : sip_utils::CONST_PJ_STR(sip_utils::SIP_METHODS::MESSAGE)};
3899 298 : pj_str_t pjFrom = sip_utils::CONST_PJ_STR(from);
3900 298 : pj_str_t pjTo = sip_utils::CONST_PJ_STR(toURI);
3901 :
3902 : // Create request.
3903 298 : pjsip_tx_data* tdata = nullptr;
3904 298 : pj_status_t status = pjsip_endpt_create_request(link_.getEndpoint(),
3905 : &msg_method,
3906 : &pjTo,
3907 : &pjFrom,
3908 : &pjTo,
3909 : nullptr,
3910 : nullptr,
3911 : -1,
3912 : nullptr,
3913 : &tdata);
3914 298 : if (status != PJ_SUCCESS) {
3915 0 : JAMI_ERROR("Unable to create request: {}", sip_utils::sip_strerror(status));
3916 0 : return false;
3917 : }
3918 :
3919 : // Add Date Header.
3920 : pj_str_t date_str;
3921 298 : constexpr auto key = sip_utils::CONST_PJ_STR("Date");
3922 : pjsip_hdr* hdr;
3923 298 : auto time = std::time(nullptr);
3924 298 : auto date = std::ctime(&time);
3925 : // the erase-remove idiom for a cstring, removes _all_ new lines with in date
3926 298 : *std::remove(date, date + strlen(date), '\n') = '\0';
3927 :
3928 : // Add Header
3929 : hdr = reinterpret_cast<pjsip_hdr*>(
3930 298 : pjsip_date_hdr_create(tdata->pool, &key, pj_cstr(&date_str, date)));
3931 298 : pjsip_msg_add_hdr(tdata->msg, hdr);
3932 :
3933 : // https://tools.ietf.org/html/rfc5438#section-6.3
3934 298 : auto token_str = to_hex_string(token);
3935 298 : auto pjMessageId = sip_utils::CONST_PJ_STR(token_str);
3936 : hdr = reinterpret_cast<pjsip_hdr*>(
3937 298 : pjsip_generic_string_hdr_create(tdata->pool, &STR_MESSAGE_ID, &pjMessageId));
3938 298 : pjsip_msg_add_hdr(tdata->msg, hdr);
3939 :
3940 : // Add user-agent header
3941 298 : sip_utils::addUserAgentHeader(getUserAgentName(), tdata);
3942 :
3943 : // Init tdata
3944 298 : const pjsip_tpselector tp_sel = SIPVoIPLink::getTransportSelector(transport->get());
3945 298 : status = pjsip_tx_data_set_transport(tdata, &tp_sel);
3946 298 : if (status != PJ_SUCCESS) {
3947 0 : JAMI_ERROR("Unable to create request: {}", sip_utils::sip_strerror(status));
3948 0 : return false;
3949 : }
3950 298 : im::fillPJSIPMessageBody(*tdata, data);
3951 :
3952 : // Because pjsip_endpt_send_request can take quite some time, move it in a io thread to avoid to block
3953 298 : dht::ThreadPool::io().run([w = weak(), tdata, ctx, cb = std::move(cb)] {
3954 298 : auto shared = w.lock();
3955 298 : if (!shared)
3956 0 : return;
3957 298 : auto status = pjsip_endpt_send_request(shared->link_.getEndpoint(), tdata, -1, ctx, cb);
3958 298 : if (status != PJ_SUCCESS)
3959 0 : JAMI_ERROR("Unable to send request: {}", sip_utils::sip_strerror(status));
3960 298 : });
3961 298 : return true;
3962 298 : }
3963 :
3964 : void
3965 83 : JamiAccount::clearProfileCache(const std::string& peerUri)
3966 : {
3967 83 : std::error_code ec;
3968 83 : std::filesystem::remove_all(cachePath_ / "vcard" / peerUri, ec);
3969 83 : }
3970 :
3971 : std::filesystem::path
3972 1144 : JamiAccount::profilePath() const
3973 : {
3974 1144 : return idPath_ / "profile.vcf";
3975 : }
3976 :
3977 : void
3978 1368 : JamiAccount::cacheSIPConnection(std::shared_ptr<dhtnet::ChannelSocket>&& socket,
3979 : const std::string& peerId,
3980 : const DeviceId& deviceId)
3981 : {
3982 1368 : std::unique_lock lk(sipConnsMtx_);
3983 : // Verify that the connection is not already cached
3984 1368 : SipConnectionKey key(peerId, deviceId);
3985 1368 : auto& connections = sipConns_[key];
3986 1368 : auto conn = std::find_if(connections.begin(), connections.end(), [&](const auto& v) {
3987 112 : return v.channel == socket;
3988 : });
3989 1368 : if (conn != connections.end()) {
3990 0 : JAMI_WARNING("[Account {}] Channel socket already cached with this peer", getAccountID());
3991 0 : return;
3992 : }
3993 :
3994 : // Convert to SIP transport
3995 1368 : auto onShutdown = [w = weak(), peerId, key, socket]() {
3996 1368 : dht::ThreadPool::io().run([w = std::move(w), peerId, key, socket] {
3997 1368 : auto shared = w.lock();
3998 1368 : if (!shared)
3999 0 : return;
4000 1368 : shared->shutdownSIPConnection(socket, key.first, key.second);
4001 : // The connection can be closed during the SIP initialization, so
4002 : // if this happens, the request should be re-sent to ask for a new
4003 : // SIP channel to make the call pass through
4004 1368 : shared->callConnectionClosed(key.second, false);
4005 1368 : });
4006 2736 : };
4007 2736 : auto sip_tr = link_.sipTransportBroker->getChanneledTransport(shared(),
4008 : socket,
4009 4104 : std::move(onShutdown));
4010 1368 : if (!sip_tr) {
4011 0 : JAMI_ERROR("No channeled transport found");
4012 0 : return;
4013 : }
4014 : // Store the connection
4015 1368 : connections.emplace_back(SipConnection {sip_tr, socket});
4016 4104 : JAMI_WARNING("[Account {:s}] New SIP channel opened with {:s}", getAccountID(), deviceId);
4017 1368 : lk.unlock();
4018 :
4019 1368 : dht::ThreadPool::io().run([w = weak(), peerId, deviceId] {
4020 1368 : if (auto shared = w.lock()) {
4021 1368 : if (shared->presenceNote_ != "") {
4022 : // If a presence note is set, send it to this device.
4023 2 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(
4024 2 : shared->rand);
4025 : std::map<std::string, std::string> msg = {
4026 8 : {MIME_TYPE_PIDF, getPIDF(shared->presenceNote_)}};
4027 2 : shared->sendMessage(peerId, deviceId.toString(), msg, token, false, true);
4028 2 : }
4029 1368 : shared->convModule()->syncConversations(peerId, deviceId.toString());
4030 1368 : }
4031 1368 : });
4032 :
4033 : // Retry messages
4034 1368 : messageEngine_.onPeerOnline(peerId);
4035 1368 : messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
4036 :
4037 : // Connect pending calls
4038 1368 : forEachPendingCall(deviceId, [&](const auto& pc) {
4039 91 : if (pc->getConnectionState() != Call::ConnectionState::TRYING
4040 91 : and pc->getConnectionState() != Call::ConnectionState::PROGRESSING)
4041 0 : return;
4042 91 : pc->setSipTransport(sip_tr, getContactHeader(sip_tr));
4043 91 : pc->setState(Call::ConnectionState::PROGRESSING);
4044 91 : if (auto remote_address = socket->getRemoteAddress()) {
4045 : try {
4046 91 : onConnectedOutgoingCall(pc, peerId, remote_address);
4047 0 : } catch (const VoipLinkException&) {
4048 : // In this case, the main scenario is that SIPStartCall failed because
4049 : // the ICE is dead and the TLS session didn't send any packet on that dead
4050 : // link (connectivity change, killed by the os, etc)
4051 : // Here, we don't need to do anything, the TLS will fail and will delete
4052 : // the cached transport
4053 : }
4054 : }
4055 : });
4056 :
4057 1367 : auto& state = presenceState_[peerId];
4058 1367 : if (state != PresenceState::CONNECTED) {
4059 1248 : state = PresenceState::CONNECTED;
4060 1248 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
4061 : peerId,
4062 : static_cast<int>(
4063 : PresenceState::CONNECTED),
4064 : "");
4065 : }
4066 1367 : }
4067 :
4068 : void
4069 1368 : JamiAccount::shutdownSIPConnection(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
4070 : const std::string& peerId,
4071 : const DeviceId& deviceId)
4072 : {
4073 1368 : std::unique_lock lk(sipConnsMtx_);
4074 1368 : SipConnectionKey key(peerId, deviceId);
4075 1368 : auto it = sipConns_.find(key);
4076 1368 : if (it != sipConns_.end()) {
4077 777 : auto& conns = it->second;
4078 777 : conns.erase(std::remove_if(conns.begin(),
4079 : conns.end(),
4080 833 : [&](auto v) { return v.channel == channel; }),
4081 777 : conns.end());
4082 777 : if (conns.empty()) {
4083 723 : sipConns_.erase(it);
4084 : // If all devices of an account are disconnected, we need to update the presence state
4085 723 : auto it = std::find_if(sipConns_.begin(), sipConns_.end(), [&](const auto& v) {
4086 6113 : return v.first.first == peerId;
4087 : });
4088 723 : if (it == sipConns_.end()) {
4089 720 : auto& state = presenceState_[peerId];
4090 720 : if (state == PresenceState::CONNECTED) {
4091 719 : std::lock_guard lock(buddyInfoMtx);
4092 719 : auto buddy = trackedBuddies_.find(dht::InfoHash(peerId));
4093 719 : if (buddy == trackedBuddies_.end())
4094 709 : state = PresenceState::DISCONNECTED;
4095 : else
4096 10 : state = buddy->second.devices_cnt > 0 ? PresenceState::AVAILABLE
4097 : : PresenceState::DISCONNECTED;
4098 719 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
4099 : peerId,
4100 : static_cast<int>(
4101 : state),
4102 : "");
4103 719 : }
4104 : }
4105 : }
4106 : }
4107 1368 : lk.unlock();
4108 : // Shutdown after removal to let the callbacks do stuff if needed
4109 1368 : if (channel)
4110 1368 : channel->shutdown();
4111 1368 : }
4112 :
4113 : std::string_view
4114 32564 : JamiAccount::currentDeviceId() const
4115 : {
4116 32564 : if (!accountManager_ or not accountManager_->getInfo())
4117 0 : return {};
4118 32564 : return accountManager_->getInfo()->deviceId;
4119 : }
4120 :
4121 : std::shared_ptr<TransferManager>
4122 139 : JamiAccount::dataTransfer(const std::string& id)
4123 : {
4124 139 : if (id.empty())
4125 63 : return nonSwarmTransferManager_;
4126 76 : return convModule()->dataTransfer(id);
4127 : }
4128 :
4129 : void
4130 0 : JamiAccount::monitor()
4131 : {
4132 0 : JAMI_DEBUG("[Account {:s}] Monitor connections", getAccountID());
4133 0 : JAMI_DEBUG("[Account {:s}] Using proxy: {:s}", getAccountID(), proxyServerCached_);
4134 :
4135 0 : if (auto cm = convModule())
4136 0 : cm->monitor();
4137 0 : std::lock_guard lkCM(connManagerMtx_);
4138 0 : if (connectionManager_)
4139 0 : connectionManager_->monitor();
4140 0 : }
4141 :
4142 : std::vector<std::map<std::string, std::string>>
4143 0 : JamiAccount::getConnectionList(const std::string& conversationId)
4144 : {
4145 0 : std::lock_guard lkCM(connManagerMtx_);
4146 0 : if (connectionManager_ && conversationId.empty()) {
4147 0 : return connectionManager_->getConnectionList();
4148 0 : } else if (connectionManager_ && convModule_) {
4149 0 : std::vector<std::map<std::string, std::string>> connectionList;
4150 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4151 0 : for (const auto& deviceId : conv->getDeviceIdList()) {
4152 0 : auto connections = connectionManager_->getConnectionList(deviceId);
4153 0 : connectionList.reserve(connectionList.size() + connections.size());
4154 0 : std::move(connections.begin(),
4155 : connections.end(),
4156 : std::back_inserter(connectionList));
4157 0 : }
4158 0 : }
4159 0 : return connectionList;
4160 0 : } else {
4161 0 : return {};
4162 : }
4163 0 : }
4164 :
4165 : std::vector<std::map<std::string, std::string>>
4166 0 : JamiAccount::getChannelList(const std::string& connectionId)
4167 : {
4168 0 : std::lock_guard lkCM(connManagerMtx_);
4169 0 : if (!connectionManager_)
4170 0 : return {};
4171 0 : return connectionManager_->getChannelList(connectionId);
4172 0 : }
4173 :
4174 : void
4175 11 : JamiAccount::sendFile(const std::string& conversationId,
4176 : const std::filesystem::path& path,
4177 : const std::string& name,
4178 : const std::string& replyTo)
4179 : {
4180 11 : if (!std::filesystem::is_regular_file(path)) {
4181 0 : JAMI_ERROR("Invalid filename '{}'", path);
4182 0 : return;
4183 : }
4184 : // NOTE: this sendMessage is in a computation thread because
4185 : // sha3sum can take quite some time to computer if the user decide
4186 : // to send a big file
4187 11 : dht::ThreadPool::computation().run([w = weak(), conversationId, path, name, replyTo]() {
4188 11 : if (auto shared = w.lock()) {
4189 11 : Json::Value value;
4190 11 : auto tid = jami::generateUID(shared->rand);
4191 11 : value["tid"] = std::to_string(tid);
4192 11 : value["displayName"] = name.empty() ? path.filename().string() : name;
4193 11 : value["totalSize"] = std::to_string(fileutils::size(path));
4194 11 : value["sha3sum"] = fileutils::sha3File(path);
4195 11 : value["type"] = "application/data-transfer+json";
4196 :
4197 44 : shared->convModule()->sendMessage(
4198 11 : conversationId,
4199 11 : std::move(value),
4200 11 : replyTo,
4201 : true,
4202 22 : [accId = shared->getAccountID(), conversationId, tid, path](
4203 11 : const std::string& commitId) {
4204 : // Create a symlink to answer to re-ask
4205 22 : auto filelinkPath = fileutils::get_data_dir() / accId / "conversation_data"
4206 44 : / conversationId / (commitId + "_" + std::to_string(tid));
4207 11 : filelinkPath += path.extension();
4208 11 : if (path != filelinkPath && !std::filesystem::is_symlink(filelinkPath)) {
4209 11 : if (!fileutils::createFileLink(filelinkPath, path, true)) {
4210 0 : JAMI_WARNING(
4211 : "Unable to create symlink for file transfer {} - {}. Copy file",
4212 : filelinkPath,
4213 : path);
4214 0 : if (!std::filesystem::copy_file(path, filelinkPath)) {
4215 0 : JAMI_ERROR("Unable to copy file for file transfer {} - {}",
4216 : filelinkPath,
4217 : path);
4218 : // Signal to notify clients that the operation failed.
4219 : // The fileId field sends the filePath.
4220 : // libjami::DataTransferEventCode::unsupported (2) is unused elsewhere.
4221 0 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4222 0 : accId,
4223 0 : conversationId,
4224 : commitId,
4225 0 : path.u8string(),
4226 : uint32_t(libjami::DataTransferEventCode::invalid));
4227 : } else {
4228 : // Signal to notify clients that the file is copied and can be
4229 : // safely deleted. The fileId field sends the filePath.
4230 : // libjami::DataTransferEventCode::created (1) is unused elsewhere.
4231 0 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4232 0 : accId,
4233 0 : conversationId,
4234 : commitId,
4235 0 : path.u8string(),
4236 : uint32_t(libjami::DataTransferEventCode::created));
4237 : }
4238 : } else {
4239 22 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4240 11 : accId,
4241 11 : conversationId,
4242 : commitId,
4243 22 : path.u8string(),
4244 : uint32_t(libjami::DataTransferEventCode::created));
4245 : }
4246 : }
4247 11 : });
4248 22 : }
4249 11 : });
4250 : }
4251 :
4252 : void
4253 7 : JamiAccount::transferFile(const std::string& conversationId,
4254 : const std::string& path,
4255 : const std::string& deviceId,
4256 : const std::string& fileId,
4257 : const std::string& interactionId,
4258 : size_t start,
4259 : size_t end,
4260 : const std::string& sha3Sum,
4261 : uint64_t lastWriteTime,
4262 : std::function<void()> onFinished)
4263 : {
4264 7 : std::string modified;
4265 7 : if (lastWriteTime != 0) {
4266 7 : modified = fmt::format("&modified={}", lastWriteTime);
4267 : }
4268 7 : auto fid = fileId == "profile.vcf" ? fmt::format("profile.vcf?sha3={}{}", sha3Sum, modified)
4269 21 : : fileId;
4270 7 : auto channelName = conversationId.empty() ? fmt::format("{}profile.vcf?sha3={}{}",
4271 : DATA_TRANSFER_SCHEME,
4272 : sha3Sum,
4273 : modified)
4274 : : fmt::format("{}{}/{}/{}",
4275 : DATA_TRANSFER_SCHEME,
4276 : conversationId,
4277 12 : currentDeviceId(),
4278 26 : fid);
4279 7 : std::lock_guard lkCM(connManagerMtx_);
4280 7 : if (!connectionManager_)
4281 0 : return;
4282 : connectionManager_
4283 21 : ->connectDevice(DeviceId(deviceId),
4284 : channelName,
4285 7 : [this,
4286 : conversationId,
4287 7 : path = std::move(path),
4288 : fileId,
4289 : interactionId,
4290 : start,
4291 : end,
4292 7 : onFinished = std::move(
4293 : onFinished)](std::shared_ptr<dhtnet::ChannelSocket> socket,
4294 14 : const DeviceId&) {
4295 7 : if (!socket)
4296 0 : return;
4297 35 : dht::ThreadPool::io().run([w = weak(),
4298 7 : path = std::move(path),
4299 7 : socket = std::move(socket),
4300 7 : conversationId = std::move(conversationId),
4301 7 : fileId,
4302 7 : interactionId,
4303 : start,
4304 : end,
4305 7 : onFinished = std::move(onFinished)] {
4306 7 : if (auto shared = w.lock())
4307 7 : if (auto dt = shared->dataTransfer(conversationId))
4308 21 : dt->transferFile(socket,
4309 7 : fileId,
4310 7 : interactionId,
4311 7 : path,
4312 : start,
4313 : end,
4314 21 : std::move(onFinished));
4315 7 : });
4316 : });
4317 7 : }
4318 :
4319 : void
4320 11 : JamiAccount::askForFileChannel(const std::string& conversationId,
4321 : const std::string& deviceId,
4322 : const std::string& interactionId,
4323 : const std::string& fileId,
4324 : size_t start,
4325 : size_t end)
4326 : {
4327 26 : auto tryDevice = [=](const auto& did) {
4328 26 : std::lock_guard lkCM(connManagerMtx_);
4329 26 : if (!connectionManager_)
4330 0 : return;
4331 :
4332 0 : auto channelName = fmt::format("{}{}/{}/{}",
4333 : DATA_TRANSFER_SCHEME,
4334 26 : conversationId,
4335 0 : currentDeviceId(),
4336 26 : fileId);
4337 26 : if (start != 0 || end != 0) {
4338 0 : channelName += fmt::format("?start={}&end={}", start, end);
4339 : }
4340 : // We can avoid to negotiate new sessions, as the file notif
4341 : // probably came from an online device or last connected device.
4342 52 : connectionManager_->connectDevice(
4343 : did,
4344 : channelName,
4345 26 : [w = weak(),
4346 26 : conversationId,
4347 26 : fileId,
4348 26 : interactionId](std::shared_ptr<dhtnet::ChannelSocket> channel, const DeviceId&) {
4349 26 : if (!channel)
4350 15 : return;
4351 11 : dht::ThreadPool::io().run([w, conversationId, channel, fileId, interactionId] {
4352 11 : auto shared = w.lock();
4353 11 : if (!shared)
4354 0 : return;
4355 11 : auto dt = shared->dataTransfer(conversationId);
4356 11 : if (!dt)
4357 0 : return;
4358 11 : if (interactionId.empty())
4359 0 : dt->onIncomingProfile(channel);
4360 : else
4361 11 : dt->onIncomingFileTransfer(fileId, channel);
4362 11 : });
4363 : },
4364 : false);
4365 37 : };
4366 :
4367 11 : if (!deviceId.empty()) {
4368 : // Only ask for device
4369 0 : tryDevice(DeviceId(deviceId));
4370 : } else {
4371 : // Only ask for connected devices. For others we will attempt
4372 : // with new peer online
4373 37 : for (const auto& m : convModule()->getConversationMembers(conversationId)) {
4374 52 : accountManager_->forEachDevice(dht::InfoHash(m.at("uri")),
4375 26 : [tryDevice = std::move(tryDevice)](
4376 : const std::shared_ptr<dht::crypto::PublicKey>& dev) {
4377 26 : tryDevice(dev->getLongId());
4378 26 : });
4379 11 : }
4380 : }
4381 11 : }
4382 :
4383 : void
4384 58 : JamiAccount::askForProfile(const std::string& conversationId,
4385 : const std::string& deviceId,
4386 : const std::string& memberUri)
4387 : {
4388 58 : std::lock_guard lkCM(connManagerMtx_);
4389 58 : if (!connectionManager_)
4390 0 : return;
4391 :
4392 : auto channelName = fmt::format("{}{}/profile/{}.vcf",
4393 : DATA_TRANSFER_SCHEME,
4394 : conversationId,
4395 0 : memberUri);
4396 : // We can avoid to negotiate new sessions, as the file notif
4397 : // probably came from an online device or last connected device.
4398 174 : connectionManager_->connectDevice(
4399 116 : DeviceId(deviceId),
4400 : channelName,
4401 103 : [this, conversationId](std::shared_ptr<dhtnet::ChannelSocket> channel, const DeviceId&) {
4402 58 : if (!channel)
4403 13 : return;
4404 45 : dht::ThreadPool::io().run([w = weak(), conversationId, channel] {
4405 45 : if (auto shared = w.lock())
4406 45 : if (auto dt = shared->dataTransfer(conversationId))
4407 90 : dt->onIncomingProfile(channel);
4408 45 : });
4409 : },
4410 : false);
4411 58 : }
4412 :
4413 : void
4414 772 : JamiAccount::initConnectionManager()
4415 : {
4416 772 : if (!nonSwarmTransferManager_)
4417 : nonSwarmTransferManager_
4418 668 : = std::make_shared<TransferManager>(accountID_,
4419 668 : config().username,
4420 : "",
4421 2004 : dht::crypto::getDerivedRandomEngine(rand));
4422 772 : if (!connectionManager_) {
4423 678 : auto connectionManagerConfig = std::make_shared<dhtnet::ConnectionManager::Config>();
4424 678 : connectionManagerConfig->ioContext = Manager::instance().ioContext();
4425 678 : connectionManagerConfig->dht = dht();
4426 678 : connectionManagerConfig->certStore = certStore_;
4427 678 : connectionManagerConfig->id = identity();
4428 678 : connectionManagerConfig->upnpCtrl = upnpCtrl_;
4429 678 : connectionManagerConfig->turnServer = config().turnServer;
4430 678 : connectionManagerConfig->upnpEnabled = config().upnpEnabled;
4431 678 : connectionManagerConfig->turnServerUserName = config().turnServerUserName;
4432 678 : connectionManagerConfig->turnServerPwd = config().turnServerPwd;
4433 678 : connectionManagerConfig->turnServerRealm = config().turnServerRealm;
4434 678 : connectionManagerConfig->turnEnabled = config().turnEnabled;
4435 678 : connectionManagerConfig->cachePath = cachePath_;
4436 678 : connectionManagerConfig->logger = Logger::dhtLogger();
4437 678 : connectionManagerConfig->factory = Manager::instance().getIceTransportFactory();
4438 678 : connectionManagerConfig->turnCache = turnCache_;
4439 1356 : connectionManagerConfig->rng = std::make_unique<std::mt19937_64>(
4440 1356 : dht::crypto::getDerivedRandomEngine(rand));
4441 678 : connectionManager_ = std::make_unique<dhtnet::ConnectionManager>(connectionManagerConfig);
4442 678 : channelHandlers_[Uri::Scheme::SWARM]
4443 1356 : = std::make_unique<SwarmChannelHandler>(shared(), *connectionManager_.get());
4444 678 : channelHandlers_[Uri::Scheme::GIT]
4445 1356 : = std::make_unique<ConversationChannelHandler>(shared(), *connectionManager_.get());
4446 678 : channelHandlers_[Uri::Scheme::SYNC]
4447 1356 : = std::make_unique<SyncChannelHandler>(shared(), *connectionManager_.get());
4448 678 : channelHandlers_[Uri::Scheme::DATA_TRANSFER]
4449 1356 : = std::make_unique<TransferChannelHandler>(shared(), *connectionManager_.get());
4450 678 : channelHandlers_[Uri::Scheme::MESSAGE]
4451 1356 : = std::make_unique<MessageChannelHandler>(shared(), *connectionManager_.get());
4452 :
4453 : #if TARGET_OS_IOS
4454 : connectionManager_->oniOSConnected([&](const std::string& connType, dht::InfoHash peer_h) {
4455 : if ((connType == "videoCall" || connType == "audioCall")
4456 : && jami::Manager::instance().isIOSExtension) {
4457 : bool hasVideo = connType == "videoCall";
4458 : emitSignal<libjami::ConversationSignal::CallConnectionRequest>("",
4459 : peer_h.toString(),
4460 : hasVideo);
4461 : return true;
4462 : }
4463 : return false;
4464 : });
4465 : #endif
4466 678 : }
4467 772 : }
4468 :
4469 : void
4470 1009 : JamiAccount::updateUpnpController()
4471 : {
4472 1009 : Account::updateUpnpController();
4473 1009 : if (connectionManager_) {
4474 45 : auto config = connectionManager_->getConfig();
4475 45 : if (config)
4476 45 : config->upnpCtrl = upnpCtrl_;
4477 45 : }
4478 1009 : }
4479 :
4480 : } // namespace jami
|