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