diff --git a/src/data_transfer.cpp b/src/data_transfer.cpp index a67cf40a258e1479015c84a5407bedded41e0a50..49773e45a72303176d993eaf2f7e058fb832e0cc 100644 --- a/src/data_transfer.cpp +++ b/src/data_transfer.cpp @@ -20,14 +20,15 @@ #include "data_transfer.h" -#include "manager.h" +#include "base64.h" +#include "client/ring_signal.h" +#include "fileutils.h" #include "jamidht/jamiaccount.h" +#include "jamidht/p2p.h" +#include "manager.h" +#include "map_utils.h" #include "peer_connection.h" -#include "fileutils.h" #include "string_utils.h" -#include "map_utils.h" -#include "client/ring_signal.h" -#include "jamidht/p2p.h" #include <thread> #include <stdexcept> @@ -354,13 +355,14 @@ private: onRecvCb_(std::string_view(buf.data(), buf.size())); } JAMI_DBG() << "FTP#" << getId() << ": sent " << info_.bytesProgress << " bytes"; - if (internalCompletionCb_) - internalCompletionCb_(info_.path); if (info_.bytesProgress != info_.totalSize) emit(DRing::DataTransferEventCode::closed_by_peer); - else + else { + if (internalCompletionCb_) + internalCompletionCb_(info_.path); emit(DRing::DataTransferEventCode::finished); + } }); } @@ -709,7 +711,7 @@ FileInfo::emit(DRing::DataTransferEventCode code) { if (finishedCb_ && code >= DRing::DataTransferEventCode::finished) finishedCb_(uint32_t(code)); - if (fileId_ != "profile.vcf") { + if (interactionId_ != "") { // Else it's an internal transfer runOnMainThread([info = info_, iid = interactionId_, fid = fileId_, code]() { emitSignal<DRing::DataTransferSignal::DataTransferEvent>(info.accountId, @@ -882,6 +884,8 @@ public: fileutils::check_dir(conversationDataPath_.c_str()); waitingPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + "waiting"; } + profilesPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_STR + accountId_ + + DIR_SEPARATOR_STR + "profiles"; loadWaiting(); } @@ -918,6 +922,7 @@ public: std::string accountId_ {}; std::string to_ {}; std::string waitingPath_ {}; + std::string profilesPath_ {}; std::string conversationDataPath_ {}; // Pre swarm @@ -928,7 +933,7 @@ public: std::map<std::string, WaitingRequest> waitingIds_ {}; std::map<std::shared_ptr<ChannelSocket>, std::shared_ptr<OutgoingFile>> outgoings_ {}; std::map<std::string, std::shared_ptr<IncomingFile>> incomings_ {}; - std::map<std::string, std::shared_ptr<IncomingFile>> vcards_ {}; + std::map<std::pair<std::string, std::string>, std::shared_ptr<IncomingFile>> vcards_ {}; }; TransferManager::TransferManager(const std::string& accountId, const std::string& to) @@ -1272,34 +1277,48 @@ TransferManager::onIncomingProfile(const std::shared_ptr<ChannelSocket>& channel { if (!channel) return; + + auto name = channel->name(); + auto lastSep = name.find_last_of('/'); + auto fileId = name.substr(lastSep + 1); + auto deviceId = channel->deviceId().toString(); auto cert = channel->peerCertificate(); - if (!cert || !cert->issuer) + if (!cert || !cert->issuer || fileId.find(".vcf") == std::string::npos) return; + + auto uri = fileId == "profile.vcf" ? cert->issuer->getId().toString() + : fileId.substr(0, fileId.size() - 4 /*.vcf*/); + std::lock_guard<std::mutex> lk(pimpl_->mapMutex_); + auto idx = std::pair<std::string, std::string> {deviceId, uri}; // Check if not already an incoming file for this id and that we are waiting this file - auto itV = pimpl_->vcards_.find(deviceId); + auto itV = pimpl_->vcards_.find(idx); if (itV != pimpl_->vcards_.end()) { channel->shutdown(); return; } + auto tid = generateUID(); DRing::DataTransferInfo info; info.accountId = pimpl_->accountId_; info.conversationId = pimpl_->to_; info.path = fileutils::get_cache_dir() + DIR_SEPARATOR_STR + pimpl_->accountId_ - + DIR_SEPARATOR_STR + "vcard" + DIR_SEPARATOR_STR + deviceId; + + DIR_SEPARATOR_STR + "vcard" + DIR_SEPARATOR_STR + deviceId + "_" + uri + "_" + + std::to_string(tid); auto ifile = std::make_shared<IncomingFile>(std::move(channel), info, "profile.vcf", ""); - auto res = pimpl_->vcards_.emplace(deviceId, std::move(ifile)); + auto res = pimpl_->vcards_.emplace(idx, std::move(ifile)); if (res.second) { res.first->second->onFinished([w = weak(), + uri = std::move(uri), deviceId = std::move(deviceId), accountId = pimpl_->accountId_, cert = std::move(cert), path = info.path](uint32_t code) { // schedule destroy transfer as not needed dht::ThreadPool().computation().run([w, + uri = std::move(uri), deviceId = std::move(deviceId), accountId = std::move(accountId), cert = std::move(cert), @@ -1308,13 +1327,12 @@ TransferManager::onIncomingProfile(const std::shared_ptr<ChannelSocket>& channel if (auto sthis_ = w.lock()) { auto& pimpl = sthis_->pimpl_; std::lock_guard<std::mutex> lk {pimpl->mapMutex_}; - auto itO = pimpl->vcards_.find(deviceId); + auto itO = pimpl->vcards_.find({deviceId, uri}); if (itO != pimpl->vcards_.end()) pimpl->vcards_.erase(itO); if (code == uint32_t(DRing::DataTransferEventCode::finished)) emitSignal<DRing::ConfigurationSignal::ProfileReceived>(accountId, - cert->issuer->getId() - .toString(), + uri, path); } }); @@ -1323,6 +1341,13 @@ TransferManager::onIncomingProfile(const std::shared_ptr<ChannelSocket>& channel } } +std::string +TransferManager::profilePath(const std::string& contactId) const +{ + // TODO Android? iOS? + return pimpl_->profilesPath_ + DIR_SEPARATOR_STR + base64::encode(contactId) + ".vcf"; +} + std::vector<WaitingRequest> TransferManager::waitingRequests() const { diff --git a/src/data_transfer.h b/src/data_transfer.h index b41b2d7951271103a83ec35eef4325bb0b113771..24041aac6e21d08710132edddfde7ef19e17f47e 100644 --- a/src/data_transfer.h +++ b/src/data_transfer.h @@ -235,6 +235,12 @@ public: bool isWaiting(const std::string& fileId) const; void onIncomingProfile(const std::shared_ptr<ChannelSocket>& channel); + /** + * @param contactId contact's id + * @return where profile.vcf is stored + */ + std::string profilePath(const std::string& contactId) const; + private: std::weak_ptr<TransferManager> weak() { diff --git a/src/jamidht/CMakeLists.txt b/src/jamidht/CMakeLists.txt index eb2c0acca0ac7f8f04d26ea80e350cb1beb8c2a2..b2b7812db0cffacfb38781ec45d83060b3fdb074 100644 --- a/src/jamidht/CMakeLists.txt +++ b/src/jamidht/CMakeLists.txt @@ -35,6 +35,8 @@ list (APPEND Source_Files__jamidht "${CMAKE_CURRENT_SOURCE_DIR}/channel_handler.h" "${CMAKE_CURRENT_SOURCE_DIR}/conversation_channel_handler.h" "${CMAKE_CURRENT_SOURCE_DIR}/conversation_channel_handler.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/transfer_channel_handler.h" + "${CMAKE_CURRENT_SOURCE_DIR}/transfer_channel_handler.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/conversation_module.h" "${CMAKE_CURRENT_SOURCE_DIR}/conversation_module.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/namedirectory.cpp" diff --git a/src/jamidht/Makefile.am b/src/jamidht/Makefile.am index 67f74ceb46d83ac2311b7105e3985d114edc4e33..4ab94e574a909e4f0abdfa8e3a6eae8af10ec127 100644 --- a/src/jamidht/Makefile.am +++ b/src/jamidht/Makefile.am @@ -43,7 +43,9 @@ libringacc_la_SOURCES = \ ./jamidht/sync_channel_handler.h \ ./jamidht/sync_channel_handler.cpp \ ./jamidht/sync_module.h \ - ./jamidht/sync_module.cpp + ./jamidht/sync_module.cpp \ + ./jamidht/transfer_channel_handler.h \ + ./jamidht/transfer_channel_handler.cpp if RINGNS libringacc_la_SOURCES += \ diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index 63db5e8cbf0a46c7abe3c618c06fdad583f61935..c6686fbd60dbd0d8dca227480e7cdf826c8ec794 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -1091,7 +1091,7 @@ Conversation::downloadFile(const std::string& interactionId, sha3sum, path, totalSize); - acc->askForFileChannel(shared->id(), deviceId, fileId, start, end); + acc->askForFileChannel(shared->id(), deviceId, interactionId, fileId, start, end); } }); return true; diff --git a/src/jamidht/conversation_channel_handler.cpp b/src/jamidht/conversation_channel_handler.cpp index 4d7edbd081ed61fcbfc2eab6aff4ccfb9bc3cd17..ef8c37e09772c027bba64dc028340a9299a38aae 100644 --- a/src/jamidht/conversation_channel_handler.cpp +++ b/src/jamidht/conversation_channel_handler.cpp @@ -22,7 +22,7 @@ namespace jami { -ConversationChannelHandler::ConversationChannelHandler(std::weak_ptr<JamiAccount>&& acc, +ConversationChannelHandler::ConversationChannelHandler(const std::shared_ptr<JamiAccount>& acc, ConnectionManager& cm) : ChannelHandlerInterface() , account_(acc) diff --git a/src/jamidht/conversation_channel_handler.h b/src/jamidht/conversation_channel_handler.h index 455bc0790a8d760b8786ec00fe0f6dbca5459104..b9418fb8e82d610d6aa8dfab87af3b309e2b5885 100644 --- a/src/jamidht/conversation_channel_handler.h +++ b/src/jamidht/conversation_channel_handler.h @@ -32,7 +32,7 @@ namespace jami { class ConversationChannelHandler : public ChannelHandlerInterface { public: - ConversationChannelHandler(std::weak_ptr<JamiAccount>&& acc, ConnectionManager& cm); + ConversationChannelHandler(const std::shared_ptr<JamiAccount>& acc, ConnectionManager& cm); ~ConversationChannelHandler(); /** diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp index e6884c56a213c79d535daa177958cea921686b85..131dfe762e777d098a0c85ff5d507a9ab88232fe 100644 --- a/src/jamidht/conversation_module.cpp +++ b/src/jamidht/conversation_module.cpp @@ -485,6 +485,20 @@ ConversationModule::Impl::handlePendingConversations() *commits.rbegin(), true); }); + // Download members profile on first sync + if (auto cert = tls::CertificateStore::instance().getCertificate(deviceId)) { + if (cert->issuer + && cert->issuer->getId().toString() == sthis->username_) { + if (auto acc = sthis->account_.lock()) { + for (const auto& member : conversation->getMembers()) { + if (member.at("uri") != sthis->username_) + acc->askForProfile(conversationId, + deviceId, + member.at("uri")); + } + } + } + } } } catch (const std::exception& e) { emitSignal<DRing::ConversationSignal::OnConversationError>(sthis->accountId_, diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index f1930379e7257aff0148b5dede02553952a137c8..32a53d2b7436fe47a94d7ec1ddc6a713d6b74852 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -40,6 +40,7 @@ #include "multiplexed_socket.h" #include "conversation_channel_handler.h" #include "sync_channel_handler.h" +#include "transfer_channel_handler.h" #include "sip/sdp.h" #include "sip/sipvoiplink.h" @@ -2192,38 +2193,6 @@ JamiAccount::doRegister_() std::lock_guard<std::mutex> lk(transfersMtx_); incomingFileTransfers_.emplace(tid); return true; - } else if (isDataTransfer) { - // Check if sync request is from same account - std::promise<bool> accept; - std::future<bool> fut = accept.get_future(); - - auto idstr = name.substr(16); - auto sep = idstr.find('/'); - auto lastSep = idstr.find_last_of('/'); - auto conversationId = idstr.substr(0, sep); - auto fileHost = idstr.substr(sep + 1, lastSep - sep - 1); - auto fileId = idstr.substr(lastSep + 1); - if (fileHost == currentDeviceId()) - return false; - - sep = fileId.find_last_of('?'); - if (sep != std::string::npos) { - fileId = fileId.substr(0, sep); - } - - // Check if peer is member of the conversation - if (fileId == "profile.vcf") { - auto members = convModule()->getConversationMembers(conversationId); - return std::find_if(members.begin(), - members.end(), - [&](auto m) { return m["uri"] == issuer; }) - != members.end(); - } - - return convModule()->onFileChannelRequest(conversationId, - issuer, - fileId, - !noSha3sumVerification_); } return false; }); @@ -2248,7 +2217,7 @@ JamiAccount::doRegister_() return; incomingFileTransfers_.erase(it); lk.unlock(); - std::function<void(const std::string&)> cb; + InternalCompletionCb cb; if (isVCard) cb = [peerId, accountId = getAccountID()](const std::string& path) { emitSignal<DRing::ConfigurationSignal::ProfileReceived>(accountId, @@ -2323,53 +2292,6 @@ JamiAccount::doRegister_() shared->gitServers_.erase(serverId); }); }); - } else if (name.find(DATA_TRANSFER_URI) == 0) { - auto idstr = name.substr(16); - auto sep = idstr.find('/'); - auto lastSep = idstr.find_last_of('/'); - auto conversationId = idstr.substr(0, sep); - auto fileId = idstr.substr(lastSep + 1); - if (channel->isInitiator()) - return; - - sep = fileId.find_last_of('?'); - std::string arguments; - if (sep != std::string::npos) { - arguments = fileId.substr(sep + 1); - fileId = fileId.substr(0, sep); - } - - if (fileId == "profile.vcf") { - std::string path = fileutils::sha3File(idPath_ + DIR_SEPARATOR_STR - + "profile.vcf"); - dataTransfer()->transferFile(channel, fileId, "", path); - return; - } - auto dt = dataTransfer(conversationId); - sep = fileId.find('_'); - if (!dt or sep == std::string::npos) { - channel->shutdown(); - return; - } - auto interactionId = fileId.substr(0, sep); - std::string path = dt->path(fileId); - auto start = 0u, end = 0u; - for (const auto arg : jami::split_string(arguments, '&')) { - auto keyVal = jami::split_string(arg, '='); - if (keyVal.size() == 2) { - if (keyVal[0] == "start") { - std::from_chars(keyVal[1].data(), - keyVal[1].data() + keyVal[1].size(), - start); - } else if (keyVal[0] == "end") { - std::from_chars(keyVal[1].data(), - keyVal[1].data() + keyVal[1].size(), - end); - } - } - } - - dt->transferFile(channel, fileId, interactionId, path, start, end); } else { // TODO move git:// auto uri = Uri(name); @@ -4172,12 +4094,16 @@ JamiAccount::sendProfile(const std::string& deviceId) sendFile(deviceId, idPath_ + DIR_SEPARATOR_STR + "profile.vcf", - [deviceId, accId = getAccountID()](const std::string&) { + [deviceId, this](const std::string&) { // Mark the VCard as sent - auto path = fileutils::get_cache_dir() + DIR_SEPARATOR_STR + accId + auto path = fileutils::get_cache_dir() + DIR_SEPARATOR_STR + getAccountID() + DIR_SEPARATOR_STR + "vcard" + DIR_SEPARATOR_STR + deviceId; + std::lock_guard<std::mutex> lock(fileutils::getFileLock(path)); + if (fileutils::isFile(path)) + return; fileutils::ofstream(path); }); + } catch (const std::exception& e) { JAMI_ERR() << e.what(); } @@ -4413,6 +4339,7 @@ JamiAccount::transferFile(const std::string& conversationId, void JamiAccount::askForFileChannel(const std::string& conversationId, const std::string& deviceId, + const std::string& interactionId, const std::string& fileId, size_t start, size_t end) @@ -4432,21 +4359,23 @@ JamiAccount::askForFileChannel(const std::string& conversationId, connectionManager_->connectDevice( did, channelName, - [this, conversationId, fileId](std::shared_ptr<ChannelSocket> channel, const DeviceId&) { + [this, conversationId, fileId, interactionId](std::shared_ptr<ChannelSocket> channel, + const DeviceId&) { if (!channel) return; - dht::ThreadPool::io().run([w = weak(), conversationId, channel, fileId] { - auto shared = w.lock(); - if (!shared) - return; - auto dt = shared->dataTransfer(conversationId); - if (!dt) - return; - if (fileId == "profile.vcf") - dt->onIncomingProfile(channel); - else - dt->onIncomingFileTransfer(fileId, channel); - }); + dht::ThreadPool::io().run( + [w = weak(), conversationId, channel, fileId, interactionId] { + auto shared = w.lock(); + if (!shared) + return; + auto dt = shared->dataTransfer(conversationId); + if (!dt) + return; + if (interactionId.empty()) + dt->onIncomingProfile(channel); + else + dt->onIncomingFileTransfer(fileId, channel); + }); }, false); }; @@ -4467,15 +4396,44 @@ JamiAccount::askForFileChannel(const std::string& conversationId, } } +void +JamiAccount::askForProfile(const std::string& conversationId, + const std::string& deviceId, + const std::string& memberUri) +{ + std::lock_guard<std::mutex> lkCM(connManagerMtx_); + if (!connectionManager_) + return; + + auto channelName = DATA_TRANSFER_URI + conversationId + "/profile/" + memberUri + ".vcf"; + // We can avoid to negotiate new sessions, as the file notif + // probably come from an online device or last connected device. + connectionManager_->connectDevice( + DeviceId(deviceId), + channelName, + [this, conversationId](std::shared_ptr<ChannelSocket> channel, const DeviceId&) { + if (!channel) + return; + dht::ThreadPool::io().run([w = weak(), conversationId, channel] { + if (auto shared = w.lock()) + if (auto dt = shared->dataTransfer(conversationId)) + dt->onIncomingProfile(channel); + }); + }, + false); +} + void JamiAccount::initConnectionManager() { if (!connectionManager_) { connectionManager_ = std::make_unique<ConnectionManager>(*this); channelHandlers_[Uri::Scheme::GIT] - = std::make_unique<ConversationChannelHandler>(weak(), *connectionManager_.get()); + = std::make_unique<ConversationChannelHandler>(shared(), *connectionManager_.get()); channelHandlers_[Uri::Scheme::SYNC] - = std::make_unique<SyncChannelHandler>(weak(), *connectionManager_.get()); + = std::make_unique<SyncChannelHandler>(shared(), *connectionManager_.get()); + channelHandlers_[Uri::Scheme::DATA_TRANSFER] + = std::make_unique<TransferChannelHandler>(shared(), *connectionManager_.get()); } } diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 0587a2a37afad5a6f5a54b0d1ed50b3896c958d3..3c7ebf391e927c030b082fd19bb4e70ed708bb84 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -560,10 +560,15 @@ public: void askForFileChannel(const std::string& conversationId, const std::string& deviceId, + const std::string& interactionId, const std::string& fileId, size_t start = 0, size_t end = 0); + void askForProfile(const std::string& conversationId, + const std::string& deviceId, + const std::string& memberUri); + /** * Retrieve linked transfer manager * @param id conversationId or empty for fallback @@ -581,7 +586,7 @@ public: // Note: when swarm will be merged, this can be moved in transferManager bool needToSendProfile(const std::string& deviceId); /** - * Send Profile via cached SIP connection + * Send profile via cached SIP connection * @param deviceId Device that will receive the profile */ void sendProfile(const std::string& deviceId); @@ -590,6 +595,8 @@ public: AccountManager* accountManager() { return accountManager_.get(); } + bool sha3SumVerify() const { return !noSha3sumVerification_; } + private: NON_COPYABLE(JamiAccount); diff --git a/src/jamidht/multiplexed_socket.cpp b/src/jamidht/multiplexed_socket.cpp index 5930a1561460a258283711afbb665ec456643f74..3f6121416803cb290931fe4d881a910bf649f03c 100644 --- a/src/jamidht/multiplexed_socket.cpp +++ b/src/jamidht/multiplexed_socket.cpp @@ -257,7 +257,9 @@ void MultiplexedSocket::Impl::onAccept(const std::string& name, uint16_t channel) { std::lock_guard<std::mutex> lkSockets(socketsMutex); - auto socket = makeSocket(name, channel); + auto& socket = sockets[channel]; + if (!socket) + socket = makeSocket(name, channel); onChannelReady_(deviceId, socket); socket->ready(); } diff --git a/src/jamidht/sync_channel_handler.cpp b/src/jamidht/sync_channel_handler.cpp index 6adb88073a21d45682d03f4f8211d8b54b78cfa0..c75123a09af4dcad73aea5e0a4045559e674c2ae 100644 --- a/src/jamidht/sync_channel_handler.cpp +++ b/src/jamidht/sync_channel_handler.cpp @@ -24,7 +24,8 @@ static constexpr const char SYNC_URI[] {"sync://"}; namespace jami { -SyncChannelHandler::SyncChannelHandler(std::weak_ptr<JamiAccount>&& acc, ConnectionManager& cm) +SyncChannelHandler::SyncChannelHandler(const std::shared_ptr<JamiAccount>& acc, + ConnectionManager& cm) : ChannelHandlerInterface() , account_(acc) , connectionManager_(cm) @@ -69,7 +70,6 @@ SyncChannelHandler::onReady(const DeviceId& deviceId, if (!cert || !acc || !acc->syncModule()) return; acc->syncModule()->cacheSyncConnection(std::move(channel), cert->getIssuerUID(), deviceId); - acc->sendProfile(deviceId.toString()); } } // namespace jami \ No newline at end of file diff --git a/src/jamidht/sync_channel_handler.h b/src/jamidht/sync_channel_handler.h index 0cce9403570a99d22e78b244f6ec8612f30dbc63..5e6f72e03af9c8eb2ebe5b513227df1a35ae1dc3 100644 --- a/src/jamidht/sync_channel_handler.h +++ b/src/jamidht/sync_channel_handler.h @@ -32,7 +32,7 @@ namespace jami { class SyncChannelHandler : public ChannelHandlerInterface { public: - SyncChannelHandler(std::weak_ptr<JamiAccount>&& acc, ConnectionManager& cm); + SyncChannelHandler(const std::shared_ptr<JamiAccount>& acc, ConnectionManager& cm); ~SyncChannelHandler(); /** diff --git a/src/jamidht/transfer_channel_handler.cpp b/src/jamidht/transfer_channel_handler.cpp new file mode 100644 index 0000000000000000000000000000000000000000..824f308ef43364876a3137730a673daf075f2f80 --- /dev/null +++ b/src/jamidht/transfer_channel_handler.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2021 Savoir-faire Linux Inc. + * + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "jamidht/transfer_channel_handler.h" + +#include <charconv> + +#include "fileutils.h" + +namespace jami { + +TransferChannelHandler::TransferChannelHandler(const std::shared_ptr<JamiAccount>& account, + ConnectionManager& cm) + : ChannelHandlerInterface() + , account_(account) + , connectionManager_(cm) +{ + auto acc = account_.lock(); + idPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_STR + acc->getAccountID(); +} + +TransferChannelHandler::~TransferChannelHandler() {} + +void +TransferChannelHandler::connect(const DeviceId& deviceId, + const std::string& channelName, + ConnectCb&& cb) +{} + +bool +TransferChannelHandler::onRequest(const DeviceId& deviceId, const std::string& name) +{ + auto acc = account_.lock(); + auto cert = tls::CertificateStore::instance().getCertificate(deviceId.toString()); + if (!acc || !cert || !cert->issuer) + return false; + auto uri = cert->issuer->getId().toString(); + auto idstr = name.substr(16); + auto sep = idstr.find('/'); + auto lastSep = idstr.find_last_of('/'); + auto conversationId = idstr.substr(0, sep); + auto fileHost = idstr.substr(sep + 1, lastSep - sep - 1); + auto fileId = idstr.substr(lastSep + 1); + if (fileHost == acc->currentDeviceId()) + return false; + + sep = fileId.find_last_of('?'); + if (sep != std::string::npos) { + fileId = fileId.substr(0, sep); + } + + // Check if peer is member of the conversation + if (fileId == "profile.vcf") { + auto members = acc->convModule()->getConversationMembers(conversationId); + return std::find_if(members.begin(), members.end(), [&](auto m) { return m["uri"] == uri; }) + != members.end(); + } else if (fileHost == "profile") { + // If a profile is sent, check if it's from another device + return uri == acc->getUsername(); + } + + return acc->convModule()->onFileChannelRequest(conversationId, + uri, + fileId, + acc->sha3SumVerify()); +} + +void +TransferChannelHandler::onReady(const DeviceId&, + const std::string& name, + std::shared_ptr<ChannelSocket> channel) +{ + auto acc = account_.lock(); + if (!acc) + return; + + auto idstr = name.substr(16); + auto splitted_id = split_string(idstr, '/'); + if (splitted_id.size() < 3) { + JAMI_ERR() << "Unsupported ID detected " << name; + channel->shutdown(); + return; + } + + // convId/fileHost/fileId or convId/profile/fileId + auto conversationId = std::string(splitted_id[0]); + auto fileHost = std::string(splitted_id[1]); + auto isContactProfile = splitted_id[1] == "profile"; + auto fileId = std::string(splitted_id[splitted_id.size() - 1]); + if (channel->isInitiator()) + return; + + auto sep = fileId.find_last_of('?'); + std::string arguments; + if (sep != std::string::npos) { + arguments = fileId.substr(sep + 1); + fileId = fileId.substr(0, sep); + } + + if (fileId == "profile.vcf") { + std::string path = fileutils::sha3File(idPath_ + DIR_SEPARATOR_STR + "profile.vcf"); + acc->dataTransfer()->transferFile(channel, fileId, "", path); + return; + } else if (isContactProfile && fileId.find(".vcf") != std::string::npos) { + auto path = acc->dataTransfer()->profilePath(fileId.substr(0, fileId.size() - 4)); + acc->dataTransfer()->transferFile(channel, fileId, "", path); + return; + } + auto dt = acc->dataTransfer(conversationId); + sep = fileId.find('_'); + if (!dt or sep == std::string::npos) { + channel->shutdown(); + return; + } + auto interactionId = fileId.substr(0, sep); + std::string path = dt->path(fileId); + auto start = 0u, end = 0u; + for (const auto arg : split_string(arguments, '&')) { + auto keyVal = split_string(arg, '='); + if (keyVal.size() == 2) { + if (keyVal[0] == "start") { + std::from_chars(keyVal[1].data(), keyVal[1].data() + keyVal[1].size(), start); + } else if (keyVal[0] == "end") { + std::from_chars(keyVal[1].data(), keyVal[1].data() + keyVal[1].size(), end); + } + } + } + + dt->transferFile(channel, fileId, interactionId, path, start, end); +} + +} // namespace jami \ No newline at end of file diff --git a/src/jamidht/transfer_channel_handler.h b/src/jamidht/transfer_channel_handler.h new file mode 100644 index 0000000000000000000000000000000000000000..62c777ef896c902d4aeb55fe6ef024825098d5a9 --- /dev/null +++ b/src/jamidht/transfer_channel_handler.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 Savoir-faire Linux Inc. + * + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "jamidht/channel_handler.h" +#include "jamidht/connectionmanager.h" +#include "jamidht/jamiaccount.h" + +namespace jami { + +/** + * Manages Conversation's channels + */ +class TransferChannelHandler : public ChannelHandlerInterface +{ +public: + TransferChannelHandler(const std::shared_ptr<JamiAccount>& acc, ConnectionManager& cm); + ~TransferChannelHandler(); + + /** + * Ask for a new channel + * This replaces the connectDevice() in jamiaccount + * @param deviceId The device to connect + * @param channelName The name of the channel + * @param cb The callback to call when connected (can be immediate if already connected) + */ + void connect(const DeviceId& deviceId, const std::string& channelName, ConnectCb&& cb) override; + + /** + * Determine if we accept or not the request + * @param deviceId device who asked + * @param name name asked + * @return if we accept or not + */ + bool onRequest(const DeviceId& deviceId, const std::string& name) override; + + /** + * Handle socket ready + * @param deviceId Related device + * @param name Name of the handler + * @param channel Channel to handle + */ + void onReady(const DeviceId& deviceId, + const std::string& name, + std::shared_ptr<ChannelSocket> channel) override; + +private: + std::weak_ptr<JamiAccount> account_; + ConnectionManager& connectionManager_; + + std::string idPath_; +}; + +} // namespace jami \ No newline at end of file diff --git a/src/uri.cpp b/src/uri.cpp index c6499e7c49265bbe7a689bebb8376e787fe74bef..4b5024759cab31630bf1eddde1bb402a9022a397 100644 --- a/src/uri.cpp +++ b/src/uri.cpp @@ -35,6 +35,8 @@ Uri::Uri(const std::string_view& uri) scheme_ = Uri::Scheme::SWARM; else if (scheme_str == "jami") scheme_ = Uri::Scheme::JAMI; + else if (scheme_str == "data-transfer") + scheme_ = Uri::Scheme::DATA_TRANSFER; else if (scheme_str == "git") scheme_ = Uri::Scheme::GIT; else if (scheme_str == "sync") diff --git a/src/uri.h b/src/uri.h index 9dfcf945778c43dceb8f0271437b5c4288375362..4db5d9ba30b061dcb299215e1be702bf6ff880dc 100644 --- a/src/uri.h +++ b/src/uri.h @@ -28,12 +28,13 @@ class Uri { public: enum class Scheme { - JAMI, // Start with "jami:" and 45 ASCII chars OR 40 ASCII chars - SIP, // Start with "sip:" - SWARM, // Start with "swarm:" and 40 ASCII chars - GIT, // Start with "git:" - SYNC, // Start with "sync:" - UNRECOGNIZED // Anything that doesn't fit in other categories + JAMI, // Start with "jami:" and 45 ASCII chars OR 40 ASCII chars + SIP, // Start with "sip:" + SWARM, // Start with "swarm:" and 40 ASCII chars + GIT, // Start with "git:" + DATA_TRANSFER, // Start with "data-transfer://" + SYNC, // Start with "sync:" + UNRECOGNIZED // Anything that doesn't fit in other categories }; Uri(const std::string_view& uri); diff --git a/test/unitTest/fileTransfer/fileTransfer.cpp b/test/unitTest/fileTransfer/fileTransfer.cpp index 25bbca243fe2faf8560156f1ecc2c7fec51f0eff..0e15cebaabaad3105de6b4c4511695654ec12e7f 100644 --- a/test/unitTest/fileTransfer/fileTransfer.cpp +++ b/test/unitTest/fileTransfer/fileTransfer.cpp @@ -70,6 +70,7 @@ private: void testMultipleFileTransfer(); void testConversationFileTransfer(); void testFileTransferInConversation(); + void testVcfFileTransferInConversation(); void testBadSha3sumOut(); void testBadSha3sumIn(); void testAskToMultipleParticipants(); @@ -83,6 +84,7 @@ private: CPPUNIT_TEST(testMultipleFileTransfer); CPPUNIT_TEST(testConversationFileTransfer); CPPUNIT_TEST(testFileTransferInConversation); + CPPUNIT_TEST(testVcfFileTransferInConversation); CPPUNIT_TEST(testBadSha3sumOut); CPPUNIT_TEST(testBadSha3sumIn); CPPUNIT_TEST(testAskToMultipleParticipants); @@ -162,7 +164,9 @@ FileTransferTest::testFileTransfer() DRing::registerSignalHandlers(confHandlers); // Create file to send - std::ofstream sendFile(sendPath); + auto sendVcfPath = sendPath + ".vcf"; + auto recvVcfPath = recvPath + ".vcf"; + std::ofstream sendFile(sendVcfPath); CPPUNIT_ASSERT(sendFile.is_open()); sendFile << std::string(64000, 'A'); sendFile.close(); @@ -172,15 +176,15 @@ FileTransferTest::testFileTransfer() uint64_t id; info.accountId = aliceAccount->getAccountID(); info.peer = bobUri; - info.path = sendPath; - info.displayName = "SEND"; + info.path = sendVcfPath; + info.displayName = "SEND.vcf"; info.bytesProgress = 0; CPPUNIT_ASSERT(DRing::sendFileLegacy(info, id) == DRing::DataTransferError::success); cv.wait_for(lk, std::chrono::seconds(30)); CPPUNIT_ASSERT(transferWaiting); - CPPUNIT_ASSERT(DRing::acceptFileTransfer(bobId, finalId, recvPath) + CPPUNIT_ASSERT(DRing::acceptFileTransfer(bobId, finalId, recvVcfPath) == DRing::DataTransferError::success); // Wait 2 times, both sides will got a finished status @@ -188,14 +192,12 @@ FileTransferTest::testFileTransfer() cv.wait_for(lk, std::chrono::seconds(30)); CPPUNIT_ASSERT(transferFinished); - CPPUNIT_ASSERT(compare(info.path, recvPath)); + CPPUNIT_ASSERT(compare(info.path, recvVcfPath)); // TODO FIX ME. The ICE take some time to stop and it doesn't seems to like // when stopping the daemon and removing the accounts to soon. - std::remove(sendPath.c_str()); - std::remove(recvPath.c_str()); - JAMI_INFO("Waiting...."); - std::this_thread::sleep_for(std::chrono::seconds(3)); + std::remove(sendVcfPath.c_str()); + std::remove(recvVcfPath.c_str()); } void @@ -288,8 +290,6 @@ FileTransferTest::testDataTransferInfo() // when stopping the daemon and removing the accounts to soon. std::remove(sendPath.c_str()); std::remove(recvPath.c_str()); - JAMI_INFO("Waiting...."); - std::this_thread::sleep_for(std::chrono::seconds(3)); } void @@ -391,8 +391,6 @@ FileTransferTest::testMultipleFileTransfer() std::remove(sendPath2.c_str()); std::remove(recvPath.c_str()); std::remove(recv2Path.c_str()); - JAMI_INFO("Waiting...."); - std::this_thread::sleep_for(std::chrono::seconds(3)); } void @@ -611,6 +609,103 @@ FileTransferTest::testFileTransferInConversation() std::this_thread::sleep_for(std::chrono::seconds(5)); } +void +FileTransferTest::testVcfFileTransferInConversation() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto aliceUri = aliceAccount->getUsername(); + auto convId = DRing::startConversation(aliceId); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool requestReceived = false; + bool conversationReady = false; + bool bobJoined = false; + std::string tidBob, iidBob; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> message) { + if (message["type"] == "application/data-transfer+json") { + if (accountId == bobId) { + tidBob = message["fileId"]; + iidBob = message["id"]; + } + } + if (accountId == aliceId && message["type"] == "member" && message["action"] == "join") { + bobJoined = true; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + bool transferAFinished = false, transferBFinished = false; + // Watch signals + confHandlers.insert(DRing::exportable_callback<DRing::DataTransferSignal::DataTransferEvent>( + [&](const std::string& accountId, + const std::string& conversationId, + const std::string&, + const std::string&, + int code) { + if (code == static_cast<int>(DRing::DataTransferEventCode::finished) + && conversationId == convId) { + if (accountId == aliceId) + transferAFinished = true; + else if (accountId == bobId) + transferBFinished = true; + } + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + DRing::addConversationMember(aliceId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + + DRing::acceptConversationRequest(bobId, convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && bobJoined; + })); + + // Create file to send + std::ofstream sendFile(sendPath); + CPPUNIT_ASSERT(sendFile.is_open()); + sendFile << std::string(64000, 'A'); + sendFile.close(); + + DRing::sendFile(aliceId, convId, sendPath, "SEND", ""); + + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return !tidBob.empty(); })); + + transferAFinished = false; + transferBFinished = false; + DRing::downloadFile(bobId, convId, iidBob, tidBob, recvPath); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return transferAFinished && transferBFinished; + })); + + std::remove(sendPath.c_str()); + std::remove(recvPath.c_str()); + DRing::unregisterSignalHandlers(); + std::this_thread::sleep_for(std::chrono::seconds(5)); +} + void FileTransferTest::testBadSha3sumOut() { diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp index 2cb28faa55009b64f325cb32d4de3ecc19cad1fa..7b07f4019ddd61f85bb6b7898a6d98b9f7ac0b74 100644 --- a/test/unitTest/syncHistory/syncHistory.cpp +++ b/test/unitTest/syncHistory/syncHistory.cpp @@ -69,6 +69,7 @@ private: void testSyncCreateAccountExportDeleteReimportWithConvReq(); void testSyncOneToOne(); void testConversationRequestRemoved(); + void testProfileReceivedMultiDevice(); CPPUNIT_TEST_SUITE(SyncHistoryTest); CPPUNIT_TEST(testCreateConversationThenSync); @@ -82,6 +83,7 @@ private: CPPUNIT_TEST(testSyncCreateAccountExportDeleteReimportWithConvReq); CPPUNIT_TEST(testSyncOneToOne); CPPUNIT_TEST(testConversationRequestRemoved); + CPPUNIT_TEST(testProfileReceivedMultiDevice); CPPUNIT_TEST_SUITE_END(); }; @@ -952,6 +954,118 @@ SyncHistoryTest::testConversationRequestRemoved() std::remove(aliceArchive.c_str()); } +void +SyncHistoryTest::testProfileReceivedMultiDevice() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + + // Export alice + auto aliceArchive = std::filesystem::current_path().string() + "/alice.gz"; + std::remove(aliceArchive.c_str()); + aliceAccount->exportArchive(aliceArchive); + + // Set VCards + std::string vcard = "BEGIN:VCARD\n\ +VERSION:2.1\n\ +FN:TITLE\n\ +DESCRIPTION:DESC\n\ +END:VCARD"; + auto alicePath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "profile.vcf"; + auto bobPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() + + DIR_SEPARATOR_STR + "profile.vcf"; + // Save VCard + auto p = std::filesystem::path(alicePath); + fileutils::recursive_mkdir(p.parent_path()); + std::ofstream aliceFile(alicePath); + if (aliceFile.is_open()) { + aliceFile << vcard; + aliceFile.close(); + } + p = std::filesystem::path(bobPath); + fileutils::recursive_mkdir(p.parent_path()); + std::ofstream bobFile(bobPath); + if (bobFile.is_open()) { + bobFile << vcard; + bobFile.close(); + } + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool conversationReady = false, requestReceived = false, bobProfileReceived = false, + aliceProfileReceived = false; + std::string convId = ""; + std::string bobDest = aliceAccount->dataTransfer()->profilePath(bobUri); + confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>( + [&](const std::string& account_id, + const std::string& /*from*/, + const std::string& /*conversationId*/, + const std::vector<uint8_t>& /*payload*/, + time_t /*received*/) { + if (account_id == bobId) + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == aliceId) { + convId = conversationId; + } else if (accountId == bobId) { + conversationReady = true; + } + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::ProfileReceived>( + [&](const std::string& accountId, const std::string& peerId, const std::string& path) { + if (accountId == aliceId && peerId == bobUri) { + bobProfileReceived = true; + auto p = std::filesystem::path(bobDest); + fileutils::recursive_mkdir(p.parent_path()); + std::rename(path.c_str(), bobDest.c_str()); + } else if (accountId == bobId && peerId == aliceUri) { + aliceProfileReceived = true; + } else if (accountId == alice2Id && peerId == bobUri) { + bobProfileReceived = true; + } else if (accountId == alice2Id && peerId == aliceUri) { + aliceProfileReceived = true; + } + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + aliceAccount->addContact(bobUri); + aliceAccount->sendTrustRequest(bobUri, {}); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + + CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && bobProfileReceived && aliceProfileReceived; + })); + CPPUNIT_ASSERT(fileutils::isFile(bobDest)); + + // Now create alice2 + std::map<std::string, std::string> details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "ALICE2"; + details[ConfProperties::ALIAS] = "ALICE2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = aliceArchive; + bobProfileReceived = false, aliceProfileReceived = false; + alice2Id = Manager::instance().addAccount(details); + std::remove(aliceArchive.c_str()); + + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { + return aliceProfileReceived && bobProfileReceived; + })); + DRing::unregisterSignalHandlers(); +} + } // namespace test } // namespace jami