diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 5e3c6f3e38d636260b08f225d57e41bf69504fbd..a978d063915faccefbf77ef92022bb8e7c81524e 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -1956,6 +1956,33 @@ </arg> </signal> + <signal name="onConversationError" tp:name-for-bindings="onConversationError"> + <tp:added version="10.0.0"/> + <tp:docstring> + Notify clients when an error occurs in a conversation + </tp:docstring> + <arg type="s" name="account_id"> + <tp:docstring> + Account id related + </tp:docstring> + </arg> + <arg type="s" name="conversation_id"> + <tp:docstring> + Conversation id + </tp:docstring> + </arg> + <arg type="u" name="code"> + <tp:docstring> + TODO + </tp:docstring> + </arg> + <arg type="s" name="what"> + <tp:docstring> + The error's description + </tp:docstring> + </arg> + </signal> + <signal name="debugMessageReceived" tp:name-for-bindings="debugMessageReceived"> <tp:added version="5.2.0"/> <tp:docstring> diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp index 0881232c84e1a5baeb873b9079fb7112302f56b9..6724539a512173a3b370a76c602d4dc21eed0bdd 100644 --- a/bin/dbus/dbusclient.cpp +++ b/bin/dbus/dbusclient.cpp @@ -305,6 +305,8 @@ DBusClient::initLibrary(int flags) bind(&DBusConfigurationManager::conversationRemoved, confM, _1, _2)), exportable_callback<ConversationSignal::ConversationMemberEvent>( bind(&DBusConfigurationManager::conversationMemberEvent, confM, _1, _2, _3, _4)), + exportable_callback<ConversationSignal::OnConversationError>( + bind(&DBusConfigurationManager::onConversationError, confM, _1, _2, _3, _4)), }; #ifdef ENABLE_VIDEO diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index 40b249a44dca21da1946c5c761f0c418c1ce99d8..1427bd08ad8f5be8def108299ab046f6873da1d3 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -31,6 +31,7 @@ public: virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} + virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} }; %} @@ -66,4 +67,5 @@ public: virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} + virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} }; \ No newline at end of file diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i index 34030f34251f3ade5f8e20cb36beefdf91d6eefd..da3e951268cf47bd4933c5f87269043fe7aec770 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -320,7 +320,8 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&ConversationCallback::conversationRequestReceived, convM, _1, _2, _3)), exportable_callback<ConversationSignal::ConversationReady>(bind(&ConversationCallback::conversationReady, convM, _1, _2)), exportable_callback<ConversationSignal::ConversationRemoved>(bind(&ConversationCallback::conversationRemoved, convM, _1, _2)), - exportable_callback<ConversationSignal::ConversationMemberEvent>(bind(&ConversationCallback::conversationMemberEvent, convM, _1, _2, _3, _4)) + exportable_callback<ConversationSignal::ConversationMemberEvent>(bind(&ConversationCallback::conversationMemberEvent, convM, _1, _2, _3, _4)), + exportable_callback<ConversationSignal::OnConversationError>(bind(&ConversationCallback::onConversationError, convM, _1, _2, _3, _4)) }; if (!DRing::init(static_cast<DRing::InitFlag>(DRing::DRING_FLAG_DEBUG))) diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index 04377ee7fe0adfc48d41d4781e4eb9066b44638e..af662e05ec08345d53971cf08e7f2a5666e5e66f 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -31,6 +31,7 @@ public: virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} + virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} }; %} @@ -79,4 +80,5 @@ public: virtual void conversationReady(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} + virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} }; \ No newline at end of file diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index fe6d35cd5ae83c9ac166649e1eff219423c2ce91..4397e6d48280365677073f69d16f6c3b1bf7092a 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -132,6 +132,7 @@ getSignalHandlers() exported_callback<DRing::ConversationSignal::ConversationReady>(), exported_callback<DRing::ConversationSignal::ConversationRemoved>(), exported_callback<DRing::ConversationSignal::ConversationMemberEvent>(), + exported_callback<DRing::ConversationSignal::OnConversationError>(), }; return handlers; diff --git a/src/dring/conversation_interface.h b/src/dring/conversation_interface.h index 5bda2bed21ddac94219da4205f658fcf114ed1b7..9e33a649c2f8570cf421c2d8ac1a805e9815eb32 100644 --- a/src/dring/conversation_interface.h +++ b/src/dring/conversation_interface.h @@ -107,6 +107,14 @@ struct DRING_PUBLIC ConversationSignal const std::string& /* memberUri */, int /* event 0 = add, 1 = joins, 2 = leave, 3 = banned */); }; + struct DRING_PUBLIC OnConversationError + { + constexpr static const char* name = "OnConversationError"; + using cb_type = void(const std::string& /*accountId*/, + const std::string& /* conversationId */, + int code, + const std::string& what); + }; }; } // namespace DRing diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index dae2f8a50f66784ac9ae907dc18c406c53256296..2acce647948195c49b6b37175ebff7cb4b658e87 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -63,6 +63,10 @@ public: remoteDevice, conversationId); if (!repository_) { + if (auto shared = account.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), conversationId, EFETCH, "Couldn't clone repository"); + } throw std::logic_error("Couldn't clone repository"); } } @@ -97,12 +101,7 @@ Conversation::Impl::isAdmin() const auto adminsPath = repoPath() + DIR_SEPARATOR_STR + "admins"; auto cert = shared->identity().second; - auto parentCert = cert->issuer; - if (!parentCert) { - JAMI_ERR("Parent cert is null!"); - return false; - } - auto uri = parentCert->getId().toString(); + auto uri = cert->getIssuerUID(); return fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt")); } @@ -123,10 +122,24 @@ Conversation::Impl::convCommitToMap(const std::vector<ConversationCommit>& commi for (const auto& commit : commits) { auto authorDevice = commit.author.email; auto cert = tls::CertificateStore::instance().getCertificate(authorDevice); - if (!cert && cert->issuer) { - JAMI_WARN("No author found for commit %s", commit.id.c_str()); + if (!cert) { + JAMI_WARN("No author found for commit %s, reload certificates", commit.id.c_str()); + if (repository_) + repository_->pinCertificates(); + // Get certificate from repo + try { + auto certPath = fileutils::getFullPath(repoPath(), std::string("devices") + DIR_SEPARATOR_STR + authorDevice + ".crt"); + auto deviceCert = fileutils::loadTextFile(certPath); + cert = std::make_shared<crypto::Certificate>(deviceCert); + if (!cert) { + JAMI_ERR("No author found for commit %s", commit.id.c_str()); + continue; + } + } catch (...) { + continue; + } } - auto authorId = cert->issuer->getId().toString(); + auto authorId = cert->getIssuerUID(); std::string parents; auto parentsSize = commit.parents.size(); for (std::size_t i = 0; i < parentsSize; ++i) { @@ -522,15 +535,6 @@ Conversation::generateInvitation() const std::map<std::string, std::string> invite; Json::Value root; root["conversationId"] = id(); - // TODO remove, cause the peer cannot trust? - // Or add signatures? - for (const auto& member : getMembers()) { - Json::Value jsonMember; - for (const auto& [key, value] : member) { - jsonMember[key] = value; - } - root["members"].append(jsonMember); - } // TODO metadatas Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 94abeebc22a9de180d6203b0bc9c6d70bda49de1..693dd27aa06b07e4abd74aff9b11dd8a0d7a9b7c 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -23,6 +23,7 @@ #include "fileutils.h" #include "gittransport.h" #include "string_utils.h" +#include "client/ring_signal.h" using random_device = dht::crypto::random_device; @@ -123,7 +124,8 @@ public: mutable std::mutex membersMtx_ {}; std::vector<ConversationMember> members_ {}; - std::vector<ConversationMember> members() const { + std::vector<ConversationMember> members() const + { std::lock_guard<std::mutex> lk(membersMtx_); return members_; } @@ -427,7 +429,8 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str return false; } GitAnnotatedCommit annotated {annotated_ptr, git_annotated_commit_free}; - if (git_commit_lookup(&parent, repository_.get(), git_annotated_commit_id(annotated.get())) < 0) { + if (git_commit_lookup(&parent, repository_.get(), git_annotated_commit_id(annotated.get())) + < 0) { JAMI_ERR("Couldn't lookup commit %s", wanted_ref.c_str()); return false; } @@ -485,7 +488,12 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str JAMI_INFO("New merge commit added with id: %s", commit_str); // Move commit to main branch git_reference* ref_ptr = nullptr; - if (git_reference_create(&ref_ptr, repository_.get(), "refs/heads/main", &commit_oid, true, nullptr) + if (git_reference_create(&ref_ptr, + repository_.get(), + "refs/heads/main", + &commit_oid, + true, + nullptr) < 0) { JAMI_WARN("Could not move commit to main"); } @@ -517,7 +525,12 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is const auto* symbolic_ref = git_reference_symbolic_target(head_ref.get()); // Create our main reference on the target OID - if (git_reference_create(&target_ref_ptr, repository_.get(), symbolic_ref, target_oid, 0, nullptr) + if (git_reference_create(&target_ref_ptr, + repository_.get(), + symbolic_ref, + target_oid, + 0, + nullptr) < 0) { JAMI_ERR("failed to create main reference"); return false; @@ -655,9 +668,9 @@ ConversationRepository::Impl::checkVote(const std::string& userDevice, } auto cert = tls::CertificateStore::instance().getCertificate(userDevice); - if (!cert && cert->issuer) + if (!cert) return false; - auto userUri = cert->issuer->getId().toString(); + auto userUri = cert->getIssuerUID(); // Check that voter is admin auto adminFile = std::string("admins") + DIR_SEPARATOR_STR + userUri + ".crt"; @@ -740,9 +753,9 @@ ConversationRepository::Impl::checkValidAdd(const std::string& userDevice, const std::string& parentId) const { auto cert = tls::CertificateStore::instance().getCertificate(userDevice); - if (!cert && cert->issuer) + if (!cert) return false; - auto userUri = cert->issuer->getId().toString(); + auto userUri = cert->getIssuerUID(); auto repo = repository(); std::string repoPath = git_repository_workdir(repository_.get()); @@ -825,9 +838,9 @@ ConversationRepository::Impl::checkValidJoins(const std::string& userDevice, const std::string& parentId) const { auto cert = tls::CertificateStore::instance().getCertificate(userDevice); - if (!cert && cert->issuer) + if (!cert) return false; - auto userUri = cert->issuer->getId().toString(); + auto userUri = cert->getIssuerUID(); // Check no other files changed auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId)); auto oneone = mode() == ConversationMode::ONE_TO_ONE; @@ -888,9 +901,9 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice, const std::string& parentId) const { auto cert = tls::CertificateStore::instance().getCertificate(userDevice); - if (!cert && cert->issuer) + if (!cert) return false; - auto userUri = cert->issuer->getId().toString(); + auto userUri = cert->getIssuerUID(); auto removeSelf = userUri == uriMember; // Retrieve tree for recent commit @@ -918,7 +931,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice, // Ignore } else if (std::regex_match(f, base_match, regex_votes)) { if (base_match.size() != 4 or base_match[2] != uriMember) { - JAMI_ERR("Invalid vote file detected :%s", f.c_str()); + JAMI_ERR("Invalid vote file detected: %s", f.c_str()); return false; } voters.emplace_back(base_match[3]); @@ -950,9 +963,9 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice, return false; } cert = tls::CertificateStore::instance().getCertificate(deviceUri); - if (!cert && cert->issuer) + if (!cert) return false; - if (uriMember != cert->issuer->getId().toString() + if (uriMember != cert->getIssuerUID() and uriMember != deviceUri /* If device is removed */) { JAMI_ERR("device removed but not for removed user (%s)", deviceFile.c_str()); return false; @@ -1001,9 +1014,9 @@ ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice, const std::string& commitId) const { auto cert = tls::CertificateStore::instance().getCertificate(userDevice); - if (!cert && cert->issuer) + if (!cert || !cert->issuer) return false; - auto userUri = cert->issuer->getId().toString(); + auto userUri = cert->getIssuerUID(); // Retrieve tree for commit auto tree = treeAtCommit(commitId); @@ -1045,9 +1058,11 @@ ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice, const std::string& commitId) const { auto cert = tls::CertificateStore::instance().getCertificate(userDevice); - if (!cert && cert->issuer) + if (!cert) { + JAMI_ERR("Cannot find certificate for %s", userDevice.c_str()); return false; - auto userUri = cert->issuer->getId().toString(); + } + auto userUri = cert->getIssuerUID(); auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, "")); try { @@ -1191,6 +1206,12 @@ ConversationRepository::Impl::mode() const auto lastMsg = log(id_, "", 1); if (lastMsg.size() == 0) { throw std::logic_error("Can't retrieve first commit"); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(), + id_, + EINVALIDMODE, + "No initial commit"); + } } auto commitMsg = lastMsg[0].commit_msg; @@ -1200,9 +1221,21 @@ ConversationRepository::Impl::mode() const auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); if (!reader->parse(commitMsg.data(), commitMsg.data() + commitMsg.size(), &root, &err)) { throw std::logic_error("Can't retrieve first commit"); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(), + id_, + EINVALIDMODE, + "No initial commit"); + } } if (!root.isMember("mode")) { throw std::logic_error("No mode detected for initial commit"); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(), + id_, + EINVALIDMODE, + "No mode detected"); + } } int mode = root["mode"].asInt(); @@ -1220,6 +1253,12 @@ ConversationRepository::Impl::mode() const mode_ = ConversationMode::PUBLIC; break; default: + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(), + id_, + EINVALIDMODE, + "Incorrect mode detected"); + } throw std::logic_error("Incorrect mode detected"); break; } @@ -1394,7 +1433,11 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to cc->author = std::move(author); cc->parents = std::move(parents); git_buf signature = {}, signed_data = {}; - if (git_commit_extract_signature(&signature, &signed_data, repository_.get(), &oid, "signature") + if (git_commit_extract_signature(&signature, + &signed_data, + repository_.get(), + &oid, + "signature") < 0) { JAMI_WARN("Could not extract signature for commit %s", id.c_str()); } else { @@ -1467,12 +1510,12 @@ ConversationRepository::Impl::getInitialMembers() const return {}; } auto commit = firstCommit[0]; + auto authorDevice = commit.author.email; auto cert = tls::CertificateStore::instance().getCertificate(authorDevice); - if (!cert && cert->issuer) { + if (!cert) return {}; - } - auto authorId = cert->issuer->getId().toString(); + auto authorId = cert->getIssuerUID(); if (mode() == ConversationMode::ONE_TO_ONE) { std::string err; Json::Value root; @@ -1500,12 +1543,11 @@ ConversationRepository::Impl::initMembers() std::lock_guard<std::mutex> lk(membersMtx_); members_.clear(); std::string repoPath = git_repository_workdir(repository_.get()); - std::vector<std::string> paths = { - repoPath + DIR_SEPARATOR_STR + "invited", - repoPath + DIR_SEPARATOR_STR + "admins", - repoPath + DIR_SEPARATOR_STR + "members", - repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "members" - }; + std::vector<std::string> paths = {repoPath + DIR_SEPARATOR_STR + "invited", + repoPath + DIR_SEPARATOR_STR + "admins", + repoPath + DIR_SEPARATOR_STR + "members", + repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + + "members"}; std::vector<MemberRole> roles = { MemberRole::INVITED, MemberRole::ADMIN, @@ -1514,7 +1556,7 @@ ConversationRepository::Impl::initMembers() }; auto i = 0; - for (const auto& p: paths) { + for (const auto& p : paths) { for (const auto& f : fileutils::readDirectory(p)) { auto pos = f.find(".crt"); auto uri = f.substr(0, pos); @@ -1650,6 +1692,7 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco } git_repository_free(rep); auto repo = std::make_unique<ConversationRepository>(account, conversationId); + repo->pinCertificates(); // need to load certificates to validate non known members if (!repo->validClone()) { JAMI_ERR("Error when validating remote conversation"); return nullptr; @@ -1670,6 +1713,10 @@ ConversationRepository::Impl::validCommits( JAMI_WARN("Malformed initial commit %s. Please check you use the latest " "version of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed initial commit"); + } return false; } } else if (commit.parents.size() == 1) { @@ -1680,6 +1727,10 @@ ConversationRepository::Impl::validCommits( JAMI_WARN("Malformed vote commit %s. Please check you use the latest version " "of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed vote"); + } return false; } } else if (type == "member") { @@ -1692,6 +1743,10 @@ ConversationRepository::Impl::validCommits( &root, &err)) { JAMI_ERR() << "Failed to parse " << err; + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed member commit"); + } return false; } std::string action = root["action"].asString(); @@ -1702,6 +1757,13 @@ ConversationRepository::Impl::validCommits( "Malformed add commit %s. Please check you use the latest version " "of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), + id_, + EVALIDFETCH, + "Malformed add member commit"); + } return false; } } else if (action == "join") { @@ -1710,6 +1772,13 @@ ConversationRepository::Impl::validCommits( "Malformed joins commit %s. Please check you use the latest version " "of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), + id_, + EVALIDFETCH, + "Malformed join member commit"); + } return false; } } else if (action == "remove") { @@ -1721,6 +1790,13 @@ ConversationRepository::Impl::validCommits( "Malformed removes commit %s. Please check you use the latest version " "of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), + id_, + EVALIDFETCH, + "Malformed remove member commit"); + } return false; } } else if (action == "ban") { @@ -1730,6 +1806,13 @@ ConversationRepository::Impl::validCommits( "Malformed removes commit %s. Please check you use the latest version " "of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), + id_, + EVALIDFETCH, + "Malformed ban member commit"); + } return false; } } else { @@ -1738,6 +1821,10 @@ ConversationRepository::Impl::validCommits( "version of Jami, or that your contact is not doing unwanted stuff.", commit.id.c_str(), action.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed member commit"); + } return false; } } else { @@ -1749,6 +1836,10 @@ ConversationRepository::Impl::validCommits( "version of Jami, or that your contact is not doing unwanted stuff.", type.c_str(), commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed commit"); + } return false; } } @@ -1762,6 +1853,12 @@ ConversationRepository::Impl::validCommits( "that your contact is not doing unwanted stuff. %s", validUserAtCommit.c_str(), commit.commit_msg.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(), + id_, + EVALIDFETCH, + "Malformed commit"); + } return false; } } else { @@ -1874,7 +1971,12 @@ ConversationRepository::amend(const std::string& id, const std::string& msg) // Move commit to main branch git_reference* ref_ptr = nullptr; - if (git_reference_create(&ref_ptr, pimpl_->repository_.get(), "refs/heads/main", &commit_id, true, nullptr) + if (git_reference_create(&ref_ptr, + pimpl_->repository_.get(), + "refs/heads/main", + &commit_id, + true, + nullptr) < 0) { JAMI_WARN("Could not move commit to main"); } @@ -1910,7 +2012,10 @@ ConversationRepository::fetch(const std::string& remoteDeviceId) JAMI_ERR("Couldn't lookup for remote %s", remoteDeviceId.c_str()); return false; } - if (git_remote_create(&remote_ptr, pimpl_->repository_.get(), remoteDeviceId.c_str(), channelName.c_str()) + if (git_remote_create(&remote_ptr, + pimpl_->repository_.get(), + remoteDeviceId.c_str(), + channelName.c_str()) < 0) { JAMI_ERR("Could not create remote for repository for conversation %s", pimpl_->id_.c_str()); @@ -1936,6 +2041,13 @@ ConversationRepository::fetch(const std::string& remoteDeviceId) JAMI_ERR("Could not fetch remote repository for conversation %s: %s", pimpl_->id_.c_str(), err->message); + + if (auto shared = pimpl_->account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>(shared->getAccountID(), + pimpl_->id_, + EFETCH, + err->message); + } } return false; } @@ -2054,7 +2166,8 @@ ConversationRepository::merge(const std::string& merge_id) git_merge_analysis_t analysis; git_merge_preference_t preference; const git_annotated_commit* const_annotated = annotated.get(); - if (git_merge_analysis(&analysis, &preference, pimpl_->repository_.get(), &const_annotated, 1) < 0) { + if (git_merge_analysis(&analysis, &preference, pimpl_->repository_.get(), &const_annotated, 1) + < 0) { JAMI_ERR("Merge operation aborted: repository analysis failed"); return false; } @@ -2091,7 +2204,8 @@ ConversationRepository::merge(const std::string& merge_id) return false; } - if (git_merge(pimpl_->repository_.get(), &const_annotated, 1, &merge_opts, &checkout_opts) < 0) { + if (git_merge(pimpl_->repository_.get(), &const_annotated, 1, &merge_opts, &checkout_opts) + < 0) { const git_error* err = giterr_last(); if (err) JAMI_ERR("Git merge failed: %s", err->message); @@ -2199,7 +2313,6 @@ ConversationRepository::join() pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::MEMBER}); } - return commitMessage(Json::writeString(wbuilder, json)); } @@ -2274,7 +2387,9 @@ ConversationRepository::leave() { std::lock_guard<std::mutex> lk(pimpl_->membersMtx_); - std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](auto& member) { return member.uri == account->getUsername(); }); + std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](auto& member) { + return member.uri == account->getUsername(); + }); } return commitMessage(Json::writeString(wbuilder, json)); @@ -2306,12 +2421,7 @@ ConversationRepository::voteKick(const std::string& uri, bool isDevice) if (!account) return {}; auto cert = account->identity().second; - auto parentCert = cert->issuer; - if (!parentCert) { - JAMI_ERR("Parent cert is null!"); - return {}; - } - auto adminUri = parentCert->getId().toString(); + auto adminUri = cert->getIssuerUID(); if (adminUri == uri) { JAMI_WARN("Admin tried to ban theirself"); return {}; @@ -2491,4 +2601,24 @@ ConversationRepository::refreshMembers() const return pimpl_->initMembers(); } +void +ConversationRepository::pinCertificates() +{ + auto repo = pimpl_->repository(); + if (!repo) + return; + + std::string repoPath = git_repository_workdir(repo.get()); + std::vector<std::string> paths = {repoPath + "admins", + repoPath + "members", + repoPath + "devices"}; + + for (const auto& path : paths) { + tls::CertificateStore::instance().pinCertificatePath(path, [](auto& ids) { + for (const auto& id : ids) + JAMI_ERR("@@@ LOADED %s", id.c_str()); + }); + } +} + } // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index b4a104f199a625f170cf04037da3b64b9a5be8eb..f6c2752c5a60d9f8f80e36d5ce308ab485e22f67 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -45,6 +45,10 @@ using GitIndexConflictIterator namespace jami { +constexpr auto EFETCH = 1; +constexpr auto EINVALIDMODE = 2; +constexpr auto EVALIDFETCH = 3; + class JamiAccount; class ChannelSocket; @@ -255,6 +259,13 @@ public: */ void refreshMembers() const; + + /** + * Because conversations can contains non contacts certificates, this methods + * loads certificates in conversations into the cert store + */ + void pinCertificates(); + private: ConversationRepository() = delete; class Impl; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 9b56f404345c9a456af4fc17c6226c5d52a453f4..abf36690e9349cc25764804c5019a3a582b2f378 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -1215,34 +1215,29 @@ JamiAccount::loadAccount(const std::string& archive_password, const std::string& conversationId, const std::vector<uint8_t>& payload, time_t received) { - dht::ThreadPool::computation().run([w = weak(), - uri, - payload = std::move(payload), - received, - conversationId] { - if (auto acc = w.lock()) { - if (!conversationId.empty()) { - std::lock_guard<std::mutex> lk(acc->conversationsRequestsMtx_); - auto it = acc->conversationsRequests_.find(conversationId); - if (it != acc->conversationsRequests_.end()) { - JAMI_INFO( - "[Account %s] Received a request for a conversation already existing. " - "Ignore", - acc->getAccountID().c_str()); - return; + dht::ThreadPool::computation().run( + [w = weak(), uri, payload = std::move(payload), received, conversationId] { + if (auto acc = w.lock()) { + if (!conversationId.empty()) { + std::lock_guard<std::mutex> lk(acc->conversationsRequestsMtx_); + auto it = acc->conversationsRequests_.find(conversationId); + if (it != acc->conversationsRequests_.end()) { + JAMI_INFO("[Account %s] Received a request for a conversation " + "already existing. " + "Ignore", + acc->getAccountID().c_str()); + return; + } + ConversationRequest req; + req.from = uri; + req.conversationId = conversationId; + req.received = std::time(nullptr); + acc->conversationsRequests_[conversationId] = std::move(req); } - ConversationRequest req; - req.from = uri; - req.conversationId = conversationId; - req.received = std::time(nullptr); - acc->conversationsRequests_[conversationId] = std::move(req); + emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>( + acc->getAccountID(), uri, payload, received); } - emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>(acc->getAccountID(), - uri, - payload, - received); - } - }); + }); }, [this](const std::map<dht::InfoHash, KnownDevice>& devices) { std::map<std::string, std::string> ids; @@ -2209,11 +2204,15 @@ JamiAccount::onTrackedBuddyOnline(const dht::InfoHash& contactId) auto convId = getOneToOneConversation(id); if (convId.empty()) return; - // In this case, the TrustRequest was sent but never confirmed (cause the contact was offline maybe) - // To avoid the contact to never receive the conv request, retry there + // In this case, the TrustRequest was sent but never confirmed (cause the contact was + // offline maybe) To avoid the contact to never receive the conv request, retry there std::lock_guard<std::mutex> lock(configurationMutex_); if (accountManager_) - accountManager_->sendTrustRequest(id, convId, {}); /* TODO payload?, MessageEngine not generic and will be able to move to conversation's requests */ + accountManager_ + ->sendTrustRequest(id, + convId, + {}); /* TODO payload?, MessageEngine not generic and will be able + to move to conversation's requests */ } } @@ -2469,11 +2468,11 @@ JamiAccount::doRegister_() ->findCertificate(deviceId, [this, &accept]( const std::shared_ptr<dht::crypto::Certificate>& cert) { - if (not cert or not cert->issuer) { + if (not cert) { accept.set_value(false); return; } - accept.set_value(cert->issuer->getId().toString() + accept.set_value(cert->getIssuerUID() == accountManager_->getInfo()->accountId); }); fut.wait(); @@ -2549,21 +2548,20 @@ JamiAccount::doRegister_() auto conversation = conversations_.find(conversationId); if (conversation == conversations_.end()) { JAMI_WARN("[Account %s] Git server requested, but for a non existing " - "conversation (%s)", - getAccountID().c_str(), - conversationId.c_str()); + "conversation (%s)", + getAccountID().c_str(), + conversationId.c_str()); return; } if (conversation->second->isBanned(remoteDevice)) { JAMI_WARN("[Account %s] %s is a banned device in conversation %s", - getAccountID().c_str(), - remoteDevice.c_str(), - conversationId.c_str()); + getAccountID().c_str(), + remoteDevice.c_str(), + conversationId.c_str()); return; } } - if (gitSocket(deviceId.toString(), conversationId) == channel) { // The onConnectionReady is already used as client (for retrieving messages) // So it's not the server socket @@ -3312,7 +3310,8 @@ JamiAccount::removeContact(const std::string& uri, bool ban) if (conv->mode() == ConversationMode::ONE_TO_ONE) { auto initMembers = conv->getInitialMembers(); if ((isSelf && initMembers.size() == 1) - || std::find(initMembers.begin(), initMembers.end(), uri) != initMembers.end()) + || std::find(initMembers.begin(), initMembers.end(), uri) + != initMembers.end()) toRm.emplace_back(key); } } catch (const std::exception& e) { @@ -3738,8 +3737,8 @@ JamiAccount::requestPeerConnection( isVCard, channeledConnectedCb, onChanneledCancelled); - //auto convId = getOneToOneConversation(info.peer); - //if (!convId.empty()) { + // auto convId = getOneToOneConversation(info.peer); + // if (!convId.empty()) { // Json::Value value; // value["tid"] = std::to_string(tid); // value["type"] = "application/data-transfer+json"; @@ -3938,7 +3937,6 @@ JamiAccount::acceptConversationRequest(const std::string& conversationId) lk.unlock(); // Save the git socket addGitSocket(dev.toString(), request.conversationId, socket); - // TODO when do we remove the gitSocket? } else { lk.unlock(); socket->shutdown(); @@ -3979,8 +3977,8 @@ JamiAccount::handlePendingConversations() for (auto it = pendingConversationsFetch_.begin(); it != pendingConversationsFetch_.end();) { if (it->second.ready) { // Clone and store conversation + auto conversationId = it->first; try { - auto conversationId = it->first; auto conversation = std::make_shared<Conversation>(weak(), it->second.deviceId, conversationId); @@ -4011,6 +4009,10 @@ JamiAccount::handlePendingConversations() conversationId); } } catch (const std::exception& e) { + emitSignal<DRing::ConversationSignal::OnConversationError>(getAccountID(), + conversationId, + EFETCH, + e.what()); JAMI_WARN("Something went wrong when cloning conversation: %s", e.what()); } it = pendingConversationsFetch_.erase(it); @@ -4134,7 +4136,9 @@ JamiAccount::addConversationMember(const std::string& conversationId, } if (it->second->isMember(contactUri, true)) { - JAMI_DBG("%s is already a member of %s, resend invite", contactUri.c_str(), conversationId.c_str()); + JAMI_DBG("%s is already a member of %s, resend invite", + contactUri.c_str(), + conversationId.c_str()); // Note: This should not be necessary, but if for whatever reason the other side didn't join // we should not forbid new invites sendTextMessage(contactUri, it->second->generateInvitation()); @@ -4146,24 +4150,29 @@ JamiAccount::addConversationMember(const std::string& conversationId, JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str()); return false; } - it->second->loadMessages([w=weak(), conversationId, sendRequest, contactUri, commitId](auto&& messages) { - auto shared = w.lock(); - if (not shared or messages.empty()) - return; // should not happen - std::lock_guard<std::mutex> lk(shared->conversationsMtx_); - // Add a new member in the conversation - auto it = shared->conversations_.find(conversationId); - if (it == shared->conversations_.end()) { - return; - } - auto message = messages.front(); - if (message.at("type") == "member") - shared->announceMemberMessage(conversationId, message); - emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), conversationId, message); - if (sendRequest) - shared->sendTextMessage(contactUri, it->second->generateInvitation()); - shared->sendMessageNotification(*it->second, commitId, true); - }, commitId, 1); + it->second->loadMessages( + [w = weak(), conversationId, sendRequest, contactUri, commitId](auto&& messages) { + auto shared = w.lock(); + if (not shared or messages.empty()) + return; // should not happen + std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + // Add a new member in the conversation + auto it = shared->conversations_.find(conversationId); + if (it == shared->conversations_.end()) { + return; + } + auto message = messages.front(); + if (message.at("type") == "member") + shared->announceMemberMessage(conversationId, message); + emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), + conversationId, + message); + if (sendRequest) + shared->sendTextMessage(contactUri, it->second->generateInvitation()); + shared->sendMessageNotification(*it->second, commitId, true); + }, + commitId, + 1); return true; } @@ -4177,24 +4186,30 @@ JamiAccount::removeConversationMember(const std::string& conversationId, if (conversation != conversations_.end() && conversation->second) { auto lastCommit = conversation->second->lastCommitId(); if (conversation->second->removeMember(contactUri, isDevice)) { - conversation->second->loadMessages([w=weak(), lastCommit, conversationId] (auto&& messages) { - if (auto shared = w.lock()) { - std::reverse(messages.begin(), messages.end()); - // Announce new commits - for (const auto& msg : messages) { - if (msg.at("type") == "member") - shared->announceMemberMessage(conversationId, msg); - emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), - conversationId, - msg); + conversation->second->loadMessages( + [w = weak(), conversationId](auto&& messages) { + if (auto shared = w.lock()) { + std::reverse(messages.begin(), messages.end()); + // Announce new commits + auto lastId = std::string(); + for (const auto& msg : messages) { + if (msg.at("type") == "member") + shared->announceMemberMessage(conversationId, msg); + emitSignal<DRing::ConversationSignal::MessageReceived>( + shared->getAccountID(), conversationId, msg); + } + // Get last id + if (messages.size() > 0) + lastId = messages.rbegin()->at("id"); + std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + auto conversation = shared->conversations_.find(conversationId); + // Send notification for others + if (conversation != shared->conversations_.end() && conversation->second) + shared->sendMessageNotification(*conversation->second, lastId, true); } - std::lock_guard<std::mutex> lk(shared->conversationsMtx_); - auto conversation = shared->conversations_.find(conversationId); - // Send notification for others - if (conversation != shared->conversations_.end() && conversation->second) - shared->sendMessageNotification(*conversation->second, lastCommit, true); - } - }, "", lastCommit); + }, + "", + lastCommit); return true; } } @@ -4238,13 +4253,16 @@ JamiAccount::sendMessage(const std::string& conversationId, if (!announce) return; if (!commitId.empty()) { - conversation->second->loadMessages([w=weak(), conversationId] (auto&& messages) { - auto shared = w.lock(); - if (shared && !messages.empty()) - emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), - conversationId, - messages.front()); - }, commitId, 1); + conversation->second->loadMessages( + [w = weak(), conversationId](auto&& messages) { + auto shared = w.lock(); + if (shared && !messages.empty()) + emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), + conversationId, + messages.front()); + }, + commitId, + 1); sendMessageNotification(*conversation->second, commitId, true); } else { JAMI_ERR("Failed to send message to conversation %s", conversationId.c_str()); @@ -4263,14 +4281,17 @@ JamiAccount::loadConversationMessages(const std::string& conversationId, std::lock_guard<std::mutex> lk(conversationsMtx_); auto conversation = conversations_.find(conversationId); if (conversation != conversations_.end() && conversation->second) { - conversation->second->loadMessages([w=weak(), conversationId, id](auto&& messages) { - if (auto shared = w.lock()) { - emitSignal<DRing::ConversationSignal::ConversationLoaded>(id, - shared->getAccountID(), - conversationId, - messages); - } - }, fromMessage, n); + conversation->second->loadMessages( + [w = weak(), conversationId, id](auto&& messages) { + if (auto shared = w.lock()) { + emitSignal<DRing::ConversationSignal::ConversationLoaded>(id, + shared->getAccountID(), + conversationId, + messages); + } + }, + fromMessage, + n); } return id; } @@ -4352,25 +4373,26 @@ JamiAccount::fetchNewCommits(const std::string& peer, }; if (gitSocket(deviceId, conversationId)) { - conversation->second - ->pull(deviceId, - [deviceId, - conversationId, - w = weak(), - announceMessages = std::move(announceMessages)](bool ok, auto messages) { - auto shared = w.lock(); - if (!shared) - return; - if (!ok) { - JAMI_WARN("[Account %s] Could not fetch new commit from %s for %s", - shared->getAccountID().c_str(), - deviceId.c_str(), - conversationId.c_str()); - shared->removeGitSocket(deviceId, conversationId); - } - if (!messages.empty()) - announceMessages(messages); - }, commitId); + conversation->second->pull( + deviceId, + [deviceId, + conversationId, + w = weak(), + announceMessages = std::move(announceMessages)](bool ok, auto messages) { + auto shared = w.lock(); + if (!shared) + return; + if (!ok) { + JAMI_WARN("[Account %s] Could not fetch new commit from %s for %s", + shared->getAccountID().c_str(), + deviceId.c_str(), + conversationId.c_str()); + shared->removeGitSocket(deviceId, conversationId); + } + if (!messages.empty()) + announceMessages(messages); + }, + commitId); } else { lk.unlock(); // Else we need to add a new gitSocket @@ -4384,12 +4406,8 @@ JamiAccount::fetchNewCommits(const std::string& peer, connectionManager_->connectDevice( DeviceId(deviceId), "git://" + deviceId + "/" + conversationId, - [this, - conversationId, - commitId, - announceMessages = std::move( - announceMessages)](std::shared_ptr<ChannelSocket> socket, - const DeviceId& deviceId) { + [this, conversationId, commitId, announceMessages = std::move(announceMessages)]( + std::shared_ptr<ChannelSocket> socket, const DeviceId& deviceId) { dht::ThreadPool::io().run([w = weak(), conversationId, socket = std::move(socket), @@ -4406,28 +4424,28 @@ JamiAccount::fetchNewCommits(const std::string& peer, if (socket) { shared->addGitSocket(deviceId.toString(), conversationId, socket); - conversation->second - ->pull(deviceId.toString(), - [deviceId, - conversationId, - w, - announceMessages = std::move( - announceMessages)](bool ok, auto messages) { - auto shared = w.lock(); - if (!shared) - return; - if (!ok) { - JAMI_WARN("[Account %s] Could not fetch new commit " - "from %s for %s", - shared->getAccountID().c_str(), - deviceId.to_c_str(), - conversationId.c_str()); - shared->removeGitSocket(deviceId.toString(), - conversationId); - } - if (!messages.empty()) - announceMessages(messages); - }, commitId); + conversation->second->pull( + deviceId.toString(), + [deviceId, + conversationId, + w, + announceMessages = std::move(announceMessages)](bool ok, + auto messages) { + auto shared = w.lock(); + if (!shared) + return; + if (!ok) { + JAMI_WARN("[Account %s] Could not fetch new commit " + "from %s for %s", + shared->getAccountID().c_str(), + deviceId.to_c_str(), + conversationId.c_str()); + shared->removeGitSocket(deviceId.toString(), conversationId); + } + if (!messages.empty()) + announceMessages(messages); + }, + commitId); } else { JAMI_ERR("[Account %s] Couldn't open a new git channel with %s for " "conversation %s", @@ -4948,7 +4966,6 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket, // Save the git socket addGitSocket(deviceId.toString(), convId, socket); checkConversationsEvents(); - // TODO when do we remove the gitSocket? } else { lk.unlock(); socket->shutdown(); @@ -4971,7 +4988,8 @@ JamiAccount::cacheSyncConnection(std::shared_ptr<ChannelSocket>&& socket, std::lock_guard<std::mutex> lk(conversationsMtx_); auto itConv = conversations_.find(convId); if (itConv != conversations_.end() && !itConv->second->isRemoving()) { - emitSignal<DRing::ConversationSignal::ConversationRemoved>(accountID_, convId); + emitSignal<DRing::ConversationSignal::ConversationRemoved>(accountID_, + convId); itConv->second->setRemovingFlag(); } } @@ -5255,6 +5273,7 @@ JamiAccount::addCallHistoryMessage(const std::string& uri, uint64_t duration_ms) void JamiAccount::monitor() const { + std::lock_guard<std::mutex> lkCM(connManagerMtx_); if (connectionManager_) connectionManager_->monitor(); } diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index f9345749904ca6617d7b307724173fe2cc29dfc3..36d7b77f9e0afb7ee6f3d8fa05200d727d533391 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -26,6 +26,7 @@ #include <streambuf> #include <git2.h> #include <filesystem> +#include <msgpack.hpp> #include "manager.h" #include "jamidht/conversation.h" @@ -40,6 +41,16 @@ using namespace std::string_literals; using namespace DRing::Account; +struct ConvInfoTest +{ + std::string id {}; + time_t created {0}; + time_t removed {0}; + time_t erased {0}; + + MSGPACK_DEFINE_MAP(id, created, removed, erased) +}; + namespace jami { namespace test { @@ -50,9 +61,13 @@ public: static std::string name() { return "Conversation"; } void setUp(); void tearDown(); - void generateFakeVote(std::shared_ptr<JamiAccount> account, - const std::string& convId, - const std::string& votedUri); + void addVote(std::shared_ptr<JamiAccount> account, + const std::string& convId, + const std::string& votedUri, + const std::string& content); + void simulateRemoval(std::shared_ptr<JamiAccount> account, + const std::string& convId, + const std::string& votedUri); void generateFakeInvite(std::shared_ptr<JamiAccount> account, const std::string& convId, const std::string& uri); @@ -64,6 +79,10 @@ public: void commit(std::shared_ptr<JamiAccount> account, const std::string& convId, Json::Value& message); + std::string commitInRepo(const std::string& repoPath, + std::shared_ptr<JamiAccount> account, + const std::string& message); + std::string createFakeConversation(std::shared_ptr<JamiAccount> account); std::string aliceId; std::string bobId; @@ -78,6 +97,7 @@ private: void testRemoveConversationNoMember(); void testRemoveConversationWithMember(); void testAddMember(); + void testMemberAddedNoBadFile(); void testAddOfflineMemberThenConnects(); void testGetMembers(); void testSendMessage(); @@ -88,33 +108,31 @@ private: void testSendMessageToMultipleParticipants(); void testPingPongMessages(); void testRemoveMember(); - // void testBanDevice(); + void testMemberBanNoBadFile(); void testMemberTryToRemoveAdmin(); void testBannedMemberCannotSendMessage(); void testAddBannedMember(); void testMemberCannotBanOther(); void testCheckAdminFakeAVoteIsDetected(); + void testVoteNonEmpty(); void testAdminCannotKickTheirself(); - // TODO void testBannedDeviceCannotSendMessageButMemberCan(); - // TODO void testRevokedDeviceCannotSendMessage(); + void testCommitUnauthorizedUser(); + // LATER void testBanDevice(); + // LATER void testBannedDeviceCannotSendMessageButMemberCan(); + // LATER void testRevokedDeviceCannotSendMessage(); // LATER void test2AdminsCannotBanEachOthers(); // LATER void test2AdminsBanMembers(); // LATER void test2AdminsBanOtherAdmin(); // LATER void testAdminRemoveConversationShouldPromoteOther(); - // TODO2 testCommitUnauthorizedUser - // TODO2 testNoBadFileInInitialCommit + void testNoBadFileInInitialCommit(); void testPlainTextNoBadFile(); - // TODO2 testVoteFromNonAdmin void testVoteNoBadFile(); void testETooBigClone(); void testETooBigFetch(); - // TODO2 testVoteNonEmpty - // TODO2 testMemberAddedNoCertificate - // TODO2 void testMemberAddedNoBadFile(); - // TODO2 void testMemberJoinsNoBadFile(); - // TODO2 testMemberJoinsInviteRemoved - // TODO2 testMemberBanNoBadFile + void testMemberJoinsNoBadFile(); + void testMemberAddedNoCertificate(); + void testMemberJoinsInviteRemoved(); void testAddContact(); void testAddContactDeleteAndReAdd(); void testFailAddMemberInOneToOne(); @@ -134,6 +152,7 @@ private: CPPUNIT_TEST(testRemoveConversationNoMember); CPPUNIT_TEST(testRemoveConversationWithMember); CPPUNIT_TEST(testAddMember); + CPPUNIT_TEST(testMemberAddedNoBadFile); CPPUNIT_TEST(testAddOfflineMemberThenConnects); CPPUNIT_TEST(testGetMembers); CPPUNIT_TEST(testSendMessage); @@ -143,16 +162,23 @@ private: CPPUNIT_TEST(testSendMessageToMultipleParticipants); CPPUNIT_TEST(testPingPongMessages); CPPUNIT_TEST(testRemoveMember); + CPPUNIT_TEST(testMemberBanNoBadFile); CPPUNIT_TEST(testMemberTryToRemoveAdmin); CPPUNIT_TEST(testBannedMemberCannotSendMessage); CPPUNIT_TEST(testAddBannedMember); CPPUNIT_TEST(testMemberCannotBanOther); CPPUNIT_TEST(testCheckAdminFakeAVoteIsDetected); + CPPUNIT_TEST(testVoteNonEmpty); CPPUNIT_TEST(testAdminCannotKickTheirself); + CPPUNIT_TEST(testCommitUnauthorizedUser); + CPPUNIT_TEST(testNoBadFileInInitialCommit); CPPUNIT_TEST(testPlainTextNoBadFile); CPPUNIT_TEST(testVoteNoBadFile); CPPUNIT_TEST(testETooBigClone); CPPUNIT_TEST(testETooBigFetch); + CPPUNIT_TEST(testMemberJoinsNoBadFile); + CPPUNIT_TEST(testMemberAddedNoCertificate); + CPPUNIT_TEST(testMemberJoinsInviteRemoved); CPPUNIT_TEST(testAddContact); CPPUNIT_TEST(testAddContactDeleteAndReAdd); CPPUNIT_TEST(testFailAddMemberInOneToOne); @@ -307,7 +333,6 @@ void ConversationTest::testGetConversation() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto aliceDeviceId = aliceAccount->currentDeviceId(); auto uri = aliceAccount->getUsername(); auto convId = aliceAccount->startConversation(); @@ -320,7 +345,6 @@ void ConversationTest::testGetConversationsAfterRm() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto aliceDeviceId = aliceAccount->currentDeviceId(); auto uri = aliceAccount->getUsername(); std::mutex mtx; @@ -352,7 +376,6 @@ void ConversationTest::testRemoveInvalidConversation() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto aliceDeviceId = aliceAccount->currentDeviceId(); auto uri = aliceAccount->getUsername(); std::mutex mtx; @@ -384,7 +407,6 @@ void ConversationTest::testRemoveConversationNoMember() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto aliceDeviceId = aliceAccount->currentDeviceId(); auto uri = aliceAccount->getUsername(); std::mutex mtx; @@ -558,6 +580,56 @@ ConversationTest::testAddMember() CPPUNIT_ASSERT(fileutils::isFile(bobMember)); } +void +ConversationTest::testMemberAddedNoBadFile() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto convId = aliceAccount->startConversation(); + 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, errorDetected = false; + 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(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == bobId && conversationId == convId && code == 1) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + addFile(aliceAccount, convId, "BADFILE"); + generateFakeInvite(aliceAccount, convId, bobUri); + // Generate conv request + aliceAccount->sendTextMessage(bobUri, + {{"application/invite+json", + "{\"conversationId\":\"" + convId + "\"}"}}); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + errorDetected = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); + DRing::unregisterSignalHandlers(); +} + void ConversationTest::testAddOfflineMemberThenConnects() { @@ -595,11 +667,11 @@ ConversationTest::testAddOfflineMemberThenConnects() CPPUNIT_ASSERT(requestReceived); carlaAccount->acceptConversationRequest(convId); - cv.wait_for(lk, std::chrono::seconds(30)); - CPPUNIT_ASSERT(conversationReady); + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; }); auto clonedPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; CPPUNIT_ASSERT(fileutils::isDirectory(clonedPath)); + DRing::unregisterSignalHandlers(); } void @@ -671,6 +743,7 @@ ConversationTest::testGetMembers() CPPUNIT_ASSERT(members[0]["role"] == "admin"); CPPUNIT_ASSERT(members[1]["uri"] == bobUri); CPPUNIT_ASSERT(members[1]["role"] == "member"); + DRing::unregisterSignalHandlers(); } void @@ -730,7 +803,7 @@ ConversationTest::testSendMessage() // Wait that alice sees Bob cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; }); - aliceAccount->sendMessage(convId, std::string("hi")); + aliceAccount->sendMessage(convId, "hi"s); cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; }); DRing::unregisterSignalHandlers(); } @@ -760,10 +833,9 @@ ConversationTest::testSendMessageTriggerMessageReceived() DRing::registerSignalHandlers(confHandlers); auto convId = aliceAccount->startConversation(); - cv.wait_for(lk, std::chrono::seconds(30)); - CPPUNIT_ASSERT(conversationReady); + cv.wait_for(lk, std::chrono::seconds(30), [&] { return conversationReady; }); - aliceAccount->sendMessage(convId, std::string("hi")); + aliceAccount->sendMessage(convId, "hi"s); cv.wait_for(lk, std::chrono::seconds(30), [&] { return messageReceived == 1; }); CPPUNIT_ASSERT(messageReceived == 1); DRing::unregisterSignalHandlers(); @@ -793,7 +865,7 @@ ConversationTest::testMergeTwoDifferentHeads() confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( [&](const std::string& accountId, const std::string& conversationId, - std::map<std::string, std::string> message) { + std::map<std::string, std::string> /* message */) { if (accountId == carlaId && conversationId == convId) { carlaGotMessage = true; } @@ -819,9 +891,9 @@ ConversationTest::testMergeTwoDifferentHeads() ConversationRepository repo(carlaAccount, convId); repo.join(); - aliceAccount->sendMessage(convId, std::string("hi")); - aliceAccount->sendMessage(convId, std::string("sup")); - aliceAccount->sendMessage(convId, std::string("jami")); + aliceAccount->sendMessage(convId, "hi"s); + aliceAccount->sendMessage(convId, "sup"s); + aliceAccount->sendMessage(convId, "jami"s); // Start Carla, should merge and all messages should be there Manager::instance().sendRegister(carlaId, true); @@ -859,6 +931,7 @@ ConversationTest::testGetRequests() auto requests = bobAccount->getConversationRequests(); CPPUNIT_ASSERT(requests.size() == 1); CPPUNIT_ASSERT(requests.front()["id"] == convId); + DRing::unregisterSignalHandlers(); } void @@ -892,6 +965,7 @@ ConversationTest::testDeclineRequest() // Decline request auto requests = bobAccount->getConversationRequests(); CPPUNIT_ASSERT(requests.size() == 0); + DRing::unregisterSignalHandlers(); } void @@ -982,7 +1056,7 @@ ConversationTest::testSendMessageToMultipleParticipants() + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); - aliceAccount->sendMessage(convId, std::string("hi")); + aliceAccount->sendMessage(convId, "hi"s); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { return messageReceivedBob >= 1 && messageReceivedCarla >= 1; })); @@ -1045,19 +1119,19 @@ ConversationTest::testPingPongMessages() CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); messageBobReceived = 0; messageAliceReceived = 0; - aliceAccount->sendMessage(convId, std::string("ping")); + aliceAccount->sendMessage(convId, "ping"s); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1 && messageAliceReceived == 1; })); - bobAccount->sendMessage(convId, std::string("pong")); + bobAccount->sendMessage(convId, "pong"s); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 2 && messageAliceReceived == 2; })); - bobAccount->sendMessage(convId, std::string("ping")); + bobAccount->sendMessage(convId, "ping"s); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 3 && messageAliceReceived == 3; })); - aliceAccount->sendMessage(convId, std::string("pong")); + aliceAccount->sendMessage(convId, "pong"s); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 4 && messageAliceReceived == 4; })); @@ -1125,6 +1199,93 @@ ConversationTest::testRemoveMember() CPPUNIT_ASSERT(members.size() == 1); CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername()); CPPUNIT_ASSERT(members[0]["role"] == "admin"); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testMemberBanNoBadFile() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + auto convId = aliceAccount->startConversation(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + Manager::instance().sendRegister(carlaId, true); + + 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, memberMessageGenerated = false, + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; + 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 && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + } else if (accountId == bobId && conversationId == convId) { + messageBobReceived = true; + } + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == bobId && conversationId == convId && code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return requestReceived && memberMessageGenerated; + })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + requestReceived = false; + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return requestReceived && memberMessageGenerated; + })); + memberMessageGenerated = false; + messageBobReceived = false; + carlaAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return memberMessageGenerated && messageBobReceived; + })); + + memberMessageGenerated = false; + voteMessageGenerated = false; + addFile(aliceAccount, convId, "BADFILE"); + aliceAccount->removeConversationMember(convId, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); + DRing::unregisterSignalHandlers(); } /*void @@ -1290,6 +1451,7 @@ ConversationTest::testMemberTryToRemoveAdmin() bobAccount->removeConversationMember(convId, aliceUri); auto members = aliceAccount->getConversationMembers(convId); CPPUNIT_ASSERT(members.size() == 2 && !memberMessageGenerated); + DRing::unregisterSignalHandlers(); } void @@ -1357,9 +1519,10 @@ ConversationTest::testBannedMemberCannotSendMessage() // Now check that alice doesn't receive a message from Bob aliceMessageReceived = false; - bobAccount->sendMessage(convId, std::string("hi")); + bobAccount->sendMessage(convId, "hi"s); CPPUNIT_ASSERT( !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return aliceMessageReceived; })); + DRing::unregisterSignalHandlers(); } void @@ -1424,16 +1587,46 @@ ConversationTest::testAddBannedMember() // Then check that bobUri cannot be re-added CPPUNIT_ASSERT(!aliceAccount->addConversationMember(convId, bobUri)); + DRing::unregisterSignalHandlers(); } void -ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account, - const std::string& convId, - const std::string& votedUri) +ConversationTest::addVote(std::shared_ptr<JamiAccount> account, + const std::string& convId, + const std::string& votedUri, + const std::string& content) +{ + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + auto voteDirectory = repoPath + DIR_SEPARATOR_STR + "votes" + DIR_SEPARATOR_STR + "members"; + auto voteFile = voteDirectory + DIR_SEPARATOR_STR + votedUri; + if (!fileutils::recursive_mkdir(voteDirectory, 0700)) { + return; + } + + std::ofstream file(voteFile); + if (file.is_open()) { + file << content; + file.close(); + } + + Json::Value json; + json["uri"] = votedUri; + json["type"] = "vote"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + ConversationRepository cr(account->weak(), convId); + cr.commitMessage(Json::writeString(wbuilder, json)); +} + +void +ConversationTest::simulateRemoval(std::shared_ptr<JamiAccount> account, + const std::string& convId, + const std::string& votedUri) { auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; - // remove from member & add into banned without voting for the ban auto memberFile = repoPath + DIR_SEPARATOR_STR + "members" + DIR_SEPARATOR_STR + votedUri + ".crt"; auto bannedFile = repoPath + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "members" @@ -1447,7 +1640,7 @@ ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account, // git add -A git_index* index_ptr = nullptr; - git_strarray array = {0}; + git_strarray array = {nullptr, 0}; if (git_repository_index(&index_ptr, repo) < 0) return; GitIndex index {index_ptr, git_index_free}; @@ -1465,7 +1658,7 @@ ConversationTest::generateFakeVote(std::shared_ptr<JamiAccount> account, wbuilder["indentation"] = ""; cr.commitMessage(Json::writeString(wbuilder, json)); - account->sendMessage(convId, std::string("trigger the fake history to be pulled")); + account->sendMessage(convId, "trigger the fake history to be pulled"s); } void @@ -1489,7 +1682,7 @@ ConversationTest::generateFakeInvite(std::shared_ptr<JamiAccount> account, // git add -A git_index* index_ptr = nullptr; - git_strarray array = {0}; + git_strarray array = {nullptr, 0}; if (git_repository_index(&index_ptr, repo) < 0) return; GitIndex index {index_ptr, git_index_free}; @@ -1507,8 +1700,9 @@ ConversationTest::generateFakeInvite(std::shared_ptr<JamiAccount> account, wbuilder["indentation"] = ""; cr.commitMessage(Json::writeString(wbuilder, json)); - account->sendMessage(convId, std::string("trigger the fake history to be pulled")); + account->sendMessage(convId, "trigger the fake history to be pulled"s); } + void ConversationTest::addAll(std::shared_ptr<JamiAccount> account, const std::string& convId) { @@ -1522,7 +1716,7 @@ ConversationTest::addAll(std::shared_ptr<JamiAccount> account, const std::string // git add -A git_index* index_ptr = nullptr; - git_strarray array = {0}; + git_strarray array = {nullptr, 0}; if (git_repository_index(&index_ptr, repo) < 0) return; GitIndex index {index_ptr, git_index_free}; @@ -1543,6 +1737,265 @@ ConversationTest::commit(std::shared_ptr<JamiAccount> account, cr.commitMessage(Json::writeString(wbuilder, message)); } +std::string +ConversationTest::commitInRepo(const std::string& path, + std::shared_ptr<JamiAccount> account, + const std::string& msg) +{ + auto deviceId = std::string(account->currentDeviceId()); + auto name = account->getDisplayName(); + if (name.empty()) + name = deviceId; + + git_signature* sig_ptr = nullptr; + // Sign commit's buffer + if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) { + JAMI_ERR("Unable to create a commit signature."); + return {}; + } + GitSignature sig {sig_ptr, git_signature_free}; + + // Retrieve current index + git_index* index_ptr = nullptr; + git_repository* repo = nullptr; + // TODO share this repo with GitServer + if (git_repository_open(&repo, path.c_str()) != 0) { + JAMI_ERR("Could not open repository"); + return {}; + } + + if (git_repository_index(&index_ptr, repo) < 0) { + JAMI_ERR("Could not open repository index"); + return {}; + } + GitIndex index {index_ptr, git_index_free}; + + git_oid tree_id; + if (git_index_write_tree(&tree_id, index.get()) < 0) { + JAMI_ERR("Unable to write initial tree from index"); + return {}; + } + + git_tree* tree_ptr = nullptr; + if (git_tree_lookup(&tree_ptr, repo, &tree_id) < 0) { + JAMI_ERR("Could not look up initial tree"); + return {}; + } + GitTree tree = {tree_ptr, git_tree_free}; + + git_oid commit_id; + if (git_reference_name_to_id(&commit_id, repo, "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return {}; + } + + git_commit* head_ptr = nullptr; + if (git_commit_lookup(&head_ptr, repo, &commit_id) < 0) { + JAMI_ERR("Could not look up HEAD commit"); + return {}; + } + GitCommit head_commit {head_ptr, git_commit_free}; + + git_buf to_sign = {}; + const git_commit* head_ref[1] = {head_commit.get()}; + if (git_commit_create_buffer( + &to_sign, repo, sig.get(), sig.get(), nullptr, msg.c_str(), tree.get(), 1, &head_ref[0]) + < 0) { + JAMI_ERR("Could not create commit buffer"); + return {}; + } + + // git commit -S + auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size); + auto signed_buf = account->identity().first->sign(to_sign_vec); + std::string signed_str = base64::encode(signed_buf); + if (git_commit_create_with_signature(&commit_id, + repo, + to_sign.ptr, + signed_str.c_str(), + "signature") + < 0) { + JAMI_ERR("Could not sign commit"); + return {}; + } + + // Move commit to main branch + git_reference* ref_ptr = nullptr; + if (git_reference_create(&ref_ptr, repo, "refs/heads/main", &commit_id, true, nullptr) < 0) { + JAMI_WARN("Could not move commit to main"); + } + git_reference_free(ref_ptr); + git_repository_free(repo); + + auto commit_str = git_oid_tostr_s(&commit_id); + if (commit_str) { + JAMI_INFO("New message added with id: %s", commit_str); + return commit_str; + } + + return {}; +} + +std::string +ConversationTest::createFakeConversation(std::shared_ptr<JamiAccount> account) +{ + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + "tmp"; + + git_repository* repo_ptr = nullptr; + git_repository_init_options opts; + git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION); + opts.flags |= GIT_REPOSITORY_INIT_MKPATH; + opts.initial_head = "main"; + if (git_repository_init_ext(&repo_ptr, repoPath.c_str(), &opts) < 0) { + JAMI_ERR("Couldn't create a git repository in %s", repoPath.c_str()); + } + GitRepository repo {std::move(repo_ptr), git_repository_free}; + + // Add files + auto deviceId = std::string(account->currentDeviceId()); + + repoPath = git_repository_workdir(repo.get()); + std::string adminsPath = repoPath + "admins"; + std::string devicesPath = repoPath + "devices"; + std::string crlsPath = repoPath + "CRLs" + DIR_SEPARATOR_STR + deviceId; + + if (!fileutils::recursive_mkdir(adminsPath, 0700)) { + JAMI_ERR("Error when creating %s. Abort create conversations", adminsPath.c_str()); + } + + auto cert = account->identity().second; + auto deviceCert = cert->toString(false); + auto parentCert = cert->issuer; + if (!parentCert) { + JAMI_ERR("Parent cert is null!"); + } + + // /admins + std::string adminPath = adminsPath + DIR_SEPARATOR_STR + parentCert->getId().toString() + + ".crt"; + auto file = fileutils::ofstream(adminPath, std::ios::trunc | std::ios::binary); + if (!file.is_open()) { + JAMI_ERR("Could not write data to %s", adminPath.c_str()); + } + file << parentCert->toString(true); + file.close(); + + if (!fileutils::recursive_mkdir(devicesPath, 0700)) { + JAMI_ERR("Error when creating %s. Abort create conversations", devicesPath.c_str()); + } + + // /devices + std::string devicePath = devicesPath + DIR_SEPARATOR_STR + cert->getId().toString() + ".crt"; + file = fileutils::ofstream(devicePath, std::ios::trunc | std::ios::binary); + if (!file.is_open()) { + JAMI_ERR("Could not write data to %s", devicePath.c_str()); + } + file << deviceCert; + file.close(); + + if (!fileutils::recursive_mkdir(crlsPath, 0700)) { + JAMI_ERR("Error when creating %s. Abort create conversations", crlsPath.c_str()); + } + + std::string badFile = repoPath + DIR_SEPARATOR_STR + "BAD"; + file = fileutils::ofstream(badFile, std::ios::trunc | std::ios::binary); + + addAll(account, "tmp"); + + JAMI_INFO("Initial files added in %s", repoPath.c_str()); + + std::string name = account->getDisplayName(); + if (name.empty()) + name = deviceId; + + git_signature* sig_ptr = nullptr; + git_index* index_ptr = nullptr; + git_oid tree_id, commit_id; + git_tree* tree_ptr = nullptr; + git_buf to_sign = {}; + + // Sign commit's buffer + if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) { + JAMI_ERR("Unable to create a commit signature."); + } + GitSignature sig {sig_ptr, git_signature_free}; + + if (git_repository_index(&index_ptr, repo.get()) < 0) { + JAMI_ERR("Could not open repository index"); + } + GitIndex index {index_ptr, git_index_free}; + + if (git_index_write_tree(&tree_id, index.get()) < 0) { + JAMI_ERR("Unable to write initial tree from index"); + } + + if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) { + JAMI_ERR("Could not look up initial tree"); + } + GitTree tree = {tree_ptr, git_tree_free}; + + Json::Value json; + json["mode"] = 1; + json["type"] = "initial"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + + if (git_commit_create_buffer(&to_sign, + repo.get(), + sig.get(), + sig.get(), + nullptr, + Json::writeString(wbuilder, json).c_str(), + tree.get(), + 0, + nullptr) + < 0) { + JAMI_ERR("Could not create initial buffer"); + return {}; + } + + auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size); + auto signed_buf = account->identity().first->sign(to_sign_vec); + std::string signed_str = base64::encode(signed_buf); + + // git commit -S + if (git_commit_create_with_signature(&commit_id, + repo.get(), + to_sign.ptr, + signed_str.c_str(), + "signature") + < 0) { + JAMI_ERR("Could not sign initial commit"); + return {}; + } + + // Move commit to main branch + git_commit* commit = nullptr; + if (git_commit_lookup(&commit, repo.get(), &commit_id) == 0) { + git_reference* ref = nullptr; + git_branch_create(&ref, repo.get(), "main", commit, true); + git_commit_free(commit); + git_reference_free(ref); + } + + auto commit_str = git_oid_tostr_s(&commit_id); + + auto finalRepo = fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + commit_str; + std::rename(repoPath.c_str(), finalRepo.c_str()); + + file = std::ofstream(fileutils::get_data_dir() + DIR_SEPARATOR_STR + account->getAccountID() + + DIR_SEPARATOR_STR + "convInfo", + std::ios::trunc | std::ios::binary); + + std::vector<ConvInfoTest> test; + test.emplace_back(ConvInfoTest {commit_str, std::time(nullptr), 0, 0}); + msgpack::pack(file, test); + return commit_str; +} + void ConversationTest::addFile(std::shared_ptr<JamiAccount> account, const std::string& convId, @@ -1567,7 +2020,7 @@ ConversationTest::addFile(std::shared_ptr<JamiAccount> account, // git add -A git_index* index_ptr = nullptr; - git_strarray array = {0}; + git_strarray array = {nullptr, 0}; if (git_repository_index(&index_ptr, repo) < 0) return; GitIndex index {index_ptr, git_index_free}; @@ -1593,7 +2046,7 @@ ConversationTest::testMemberCannotBanOther() std::condition_variable cv; std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; bool conversationReady = false, requestReceived = false, memberMessageGenerated = false, - voteMessageGenerated = false, messageBobReceived = false; + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -1623,6 +2076,15 @@ ConversationTest::testMemberCannotBanOther() } cv.notify_one(); })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { @@ -1645,11 +2107,13 @@ ConversationTest::testMemberCannotBanOther() })); // Now Carla remove Bob as a member + errorDetected = false; messageBobReceived = false; - generateFakeVote(carlaAccount, convId, bobUri); - CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived; })); + // remove from member & add into banned without voting for the ban + simulateRemoval(carlaAccount, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); - aliceAccount->sendMessage(convId, std::string("hi")); + aliceAccount->sendMessage(convId, "hi"s); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived; })); } @@ -1671,7 +2135,7 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected() std::condition_variable cv; std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; bool conversationReady = false, requestReceived = false, memberMessageGenerated = false, - voteMessageGenerated = false, messageBobReceived = false; + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -1701,6 +2165,15 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected() } cv.notify_one(); })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { @@ -1723,19 +2196,189 @@ ConversationTest::testCheckAdminFakeAVoteIsDetected() })); // Now Alice remove Carla without a vote. Bob will not receive the message - messageBobReceived = false; - generateFakeVote(aliceAccount, convId, carlaUri); - CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived; })); + errorDetected = false; + simulateRemoval(aliceAccount, convId, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); } void -ConversationTest::testAdminCannotKickTheirself() +ConversationTest::testVoteNonEmpty() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); auto convId = aliceAccount->startConversation(); - std::mutex mtx; - std::unique_lock<std::mutex> lk {mtx}; + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + Manager::instance().sendRegister(carlaId, true); + + 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, memberMessageGenerated = false, + voteMessageGenerated = false, messageBobReceived = false, errorDetected = false; + 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 && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + } else if (accountId == bobId && conversationId == convId) { + messageBobReceived = true; + } + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return requestReceived && memberMessageGenerated; + })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + requestReceived = false; + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return requestReceived && memberMessageGenerated; + })); + memberMessageGenerated = false; + messageBobReceived = false; + carlaAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return memberMessageGenerated && messageBobReceived; + })); + + // Now Alice removes Carla with a non empty file + errorDetected = false; + addVote(aliceAccount, convId, carlaUri, "CONTENT"); + simulateRemoval(aliceAccount, convId, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); +} + +void +ConversationTest::testCommitUnauthorizedUser() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto bobUri = bobAccount->getUsername(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool errorDetected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + 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(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; })); + + // Assert that repository exists + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + // Wait that alice sees Bob + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; })); + + // Add commit from invalid user + Json::Value root; + root["type"] = "text/plain"; + root["body"] = "hi"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + auto message = Json::writeString(wbuilder, root); + commitInRepo(repoPath, carlaAccount, message); + + errorDetected = false; + bobAccount->sendMessage(convId, "hi"s); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testAdminCannotKickTheirself() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto convId = aliceAccount->startConversation(); + 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, memberMessageGenerated = false, @@ -1779,11 +2422,14 @@ ConversationTest::testAdminCannotKickTheirself() } void -ConversationTest::testPlainTextNoBadFile() +ConversationTest::testNoBadFileInInitialCommit() { auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); - auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); - auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + auto aliceUri = aliceAccount->getUsername(); + + auto convId = createFakeConversation(carlaAccount); std::mutex mtx; std::unique_lock<std::mutex> lk {mtx}; @@ -1791,7 +2437,8 @@ ConversationTest::testPlainTextNoBadFile() std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; auto messageBobReceived = 0, messageAliceReceived = 0; bool requestReceived = false; - bool conversationReady = false; + bool carlaConnected = false; + bool errorDetected = false; confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( [&](const std::string& accountId, const std::string& /* conversationId */, @@ -1811,6 +2458,74 @@ ConversationTest::testPlainTextNoBadFile() requestReceived = true; cv.notify_one(); })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; })); + JAMI_ERR("@@@@@@@@@@@22"); + CPPUNIT_ASSERT(carlaAccount->addConversationMember(convId, aliceUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + + aliceAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); +} + +void +ConversationTest::testPlainTextNoBadFile() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + auto messageBobReceived = 0; + bool memberMessageGenerated = false; + bool requestReceived = false; + bool conversationReady = false; + bool errorDetected = false; + std::string convId = aliceAccount->startConversation(); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == bobId) { + messageBobReceived += 1; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = 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) { @@ -1818,24 +2533,37 @@ ConversationTest::testPlainTextNoBadFile() cv.notify_one(); } })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); - auto convId = aliceAccount->startConversation(); - CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); - CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return requestReceived && memberMessageGenerated; + })); + memberMessageGenerated = false; bobAccount->acceptConversationRequest(convId); - cv.wait_for(lk, std::chrono::seconds(30)); - CPPUNIT_ASSERT(conversationReady); - // Wait that alice sees Bob - cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageAliceReceived == 1; }); + cv.wait_for(lk, std::chrono::seconds(30), [&] { + return conversationReady && memberMessageGenerated; + }); addFile(aliceAccount, convId, "BADFILE"); - aliceAccount->sendMessage(convId, std::string("hi")); + Json::Value root; + root["type"] = "text/plain"; + root["body"] = "hi"; + commit(aliceAccount, convId, root); + errorDetected = false; + aliceAccount->sendMessage(convId, "hi"s); // Check not received due to the unwanted file - CPPUNIT_ASSERT( - !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; })); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); DRing::unregisterSignalHandlers(); } @@ -1924,7 +2652,7 @@ ConversationTest::testVoteNoBadFile() })); messageCarlaReceived = false; - bobAccount->sendMessage(convId, std::string("final")); + bobAccount->sendMessage(convId, "final"s); CPPUNIT_ASSERT( cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageCarlaReceived; })); } @@ -1943,6 +2671,7 @@ ConversationTest::testETooBigClone() auto messageBobReceived = 0, messageAliceReceived = 0; bool requestReceived = false; bool conversationReady = false; + bool errorDetected = false; confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( [&](const std::string& accountId, const std::string& /* conversationId */, @@ -1969,6 +2698,15 @@ ConversationTest::testETooBigClone() cv.notify_one(); } })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); auto convId = aliceAccount->startConversation(); @@ -1987,8 +2725,9 @@ ConversationTest::testETooBigClone() CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + errorDetected = false; bobAccount->acceptConversationRequest(convId); - CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; })); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); DRing::unregisterSignalHandlers(); } @@ -2006,6 +2745,7 @@ ConversationTest::testETooBigFetch() auto messageBobReceived = 0, messageAliceReceived = 0; bool requestReceived = false; bool conversationReady = false; + bool errorDetected = false; confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( [&](const std::string& accountId, const std::string& /* conversationId */, @@ -2032,6 +2772,15 @@ ConversationTest::testETooBigFetch() cv.notify_one(); } })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); auto convId = aliceAccount->startConversation(); @@ -2049,6 +2798,7 @@ ConversationTest::testETooBigFetch() + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; std::ofstream bad(repoPath + DIR_SEPARATOR_STR + "BADFILE"); CPPUNIT_ASSERT(bad.is_open()); + errorDetected = false; for (int i = 0; i < 300 * 1024 * 1024; ++i) bad << "A"; bad.close(); @@ -2059,9 +2809,247 @@ ConversationTest::testETooBigFetch() json["type"] = "text/plain"; commit(aliceAccount, convId, json); - aliceAccount->sendMessage(convId, std::string("hi")); - CPPUNIT_ASSERT( - !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; })); + aliceAccount->sendMessage(convId, "hi"s); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testMemberJoinsNoBadFile() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + auto convId = aliceAccount->startConversation(); + + 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, errorDetected = false, carlaConnected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == carlaId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false)); + + // Cp conversations & convInfo + auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive); + auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + std::filesystem::copy(ciPathAlice, ciPathCarla); + + // Accept for alice and makes different heads + addFile(carlaAccount, convId, "BADFILE"); + ConversationRepository repo(carlaAccount, convId); + repo.join(); + + // Start Carla, should merge and all messages should be there + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; })); + + carlaAccount->sendMessage(convId, "hi"s); + errorDetected = false; + + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return errorDetected; })); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testMemberAddedNoCertificate() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + auto convId = aliceAccount->startConversation(); + + 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, errorDetected = false, carlaConnected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == carlaId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false)); + + // Cp conversations & convInfo + auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive); + auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + std::filesystem::copy(ciPathAlice, ciPathCarla); + + // Remove invite but do not add member certificate + std::string invitedPath = repoPathCarla + "invited"; + fileutils::remove(fileutils::getFullPath(invitedPath, carlaUri)); + + Json::Value json; + json["action"] = "join"; + json["uri"] = carlaUri; + json["type"] = "member"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + ConversationRepository cr(carlaAccount->weak(), convId); + cr.commitMessage(Json::writeString(wbuilder, json)); + + // Start Carla, should merge and all messages should be there + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; })); + + carlaAccount->sendMessage(convId, "hi"s); + errorDetected = false; + + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return errorDetected; })); + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testMemberJoinsInviteRemoved() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + aliceAccount->trackBuddyPresence(carlaUri, true); + auto convId = aliceAccount->startConversation(); + + 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, errorDetected = false, carlaConnected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == carlaId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") { + carlaConnected = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, carlaUri, false)); + + // Cp conversations & convInfo + auto repoPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + aliceAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + auto repoPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + + carlaAccount->getAccountID() + DIR_SEPARATOR_STR + "conversations"; + std::filesystem::copy(repoPathAlice, repoPathCarla, std::filesystem::copy_options::recursive); + auto ciPathAlice = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + auto ciPathCarla = fileutils::get_data_dir() + DIR_SEPARATOR_STR + carlaAccount->getAccountID() + + DIR_SEPARATOR_STR + "convInfo"; + std::filesystem::copy(ciPathAlice, ciPathCarla); + + // Let invited, but add /members + /devices + auto cert = carlaAccount->identity().second; + auto parentCert = cert->issuer; + auto uri = parentCert->getId().toString(); + std::string membersPath = repoPathCarla + "members" + DIR_SEPARATOR_STR; + std::string memberFile = membersPath + DIR_SEPARATOR_STR + carlaUri + ".crt"; + // Add members/uri.crt + fileutils::recursive_mkdir(membersPath, 0700); + auto file = fileutils::ofstream(memberFile, std::ios::trunc | std::ios::binary); + file << parentCert->toString(true); + file.close(); + + addAll(carlaAccount, convId); + Json::Value json; + json["action"] = "join"; + json["uri"] = carlaUri; + json["type"] = "member"; + commit(carlaAccount, convId, json); + + // Start Carla, should merge and all messages should be there + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return carlaConnected; })); + + carlaAccount->sendMessage(convId, "hi"s); + errorDetected = false; + + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&] { return errorDetected; })); DRing::unregisterSignalHandlers(); } @@ -2264,7 +3252,8 @@ ConversationTest::testUnknownModeDetected() 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, memberMessageGenerated = false; + bool conversationReady = false, requestReceived = false, memberMessageGenerated = false, + errorDetected = false; confHandlers.insert( DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( [&](const std::string& /*accountId*/, @@ -2289,11 +3278,21 @@ ConversationTest::testUnknownModeDetected() cv.notify_one(); } })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 1) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + errorDetected = false; bobAccount->acceptConversationRequest(convId); - CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return conversationReady; })); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); } void @@ -2418,7 +3417,6 @@ ConversationTest::testBanContact() memberMessageGenerated = false; bobAccount->removeContact(aliceUri, true); - cv.wait_for(lk, std::chrono::seconds(10)); CPPUNIT_ASSERT( !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + bobAccount->getAccountID() @@ -2440,7 +3438,7 @@ ConversationTest::testOneToOneFetchWithNewMemberRefused() std::condition_variable cv; std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; bool conversationReady = false, requestReceived = false, memberMessageGenerated = false, - messageBob = false; + messageBob = false, errorDetected = false; std::string convId = ""; confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>( [&](const std::string& account_id, @@ -2472,6 +3470,15 @@ ConversationTest::testOneToOneFetchWithNewMemberRefused() } cv.notify_one(); })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); DRing::registerSignalHandlers(confHandlers); aliceAccount->addContact(bobUri); aliceAccount->sendTrustRequest(bobUri, {}); @@ -2484,9 +3491,9 @@ ConversationTest::testOneToOneFetchWithNewMemberRefused() return conversationReady && memberMessageGenerated; })); - messageBob = false; + errorDetected = false; generateFakeInvite(aliceAccount, convId, carlaUri); - CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBob; })); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); } void