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