Line data Source code
1 : /*
2 : * Copyright (C) 2004-2025 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "conversationrepository.h"
19 :
20 : #include "account_const.h"
21 : #include "base64.h"
22 : #include "jamiaccount.h"
23 : #include "fileutils.h"
24 : #include "gittransport.h"
25 : #include "string_utils.h"
26 : #include "client/ring_signal.h"
27 : #include "vcard.h"
28 : #include "json_utils.h"
29 :
30 : #include <ctime>
31 : #include <fstream>
32 : #include <future>
33 : #include <json/json.h>
34 : #include <regex>
35 : #include <exception>
36 : #include <optional>
37 :
38 : using namespace std::string_view_literals;
39 : constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv;
40 : constexpr size_t MAX_FETCH_SIZE {256 * 1024 * 1024}; // 256Mb
41 :
42 : namespace jami {
43 :
44 : #ifdef LIBJAMI_TEST
45 : bool ConversationRepository::DISABLE_RESET = false;
46 : #endif
47 :
48 : static const std::regex regex_display_name("<|>");
49 :
50 : inline std::string_view
51 5802 : as_view(const git_blob* blob)
52 : {
53 5802 : return std::string_view(static_cast<const char*>(git_blob_rawcontent(blob)),
54 5802 : git_blob_rawsize(blob));
55 : }
56 : inline std::string_view
57 5802 : as_view(const GitObject& blob)
58 : {
59 5802 : return as_view(reinterpret_cast<git_blob*>(blob.get()));
60 : }
61 :
62 : class ConversationRepository::Impl
63 : {
64 : public:
65 311 : Impl(const std::shared_ptr<JamiAccount>& account, const std::string& id)
66 311 : : account_(account)
67 311 : , id_(id)
68 311 : , accountId_(account->getAccountID())
69 311 : , userId_(account->getUsername())
70 622 : , deviceId_(account->currentDeviceId())
71 : {
72 622 : conversationDataPath_ = fileutils::get_data_dir() / accountId_
73 933 : / "conversation_data" / id_;
74 311 : membersCache_ = conversationDataPath_ / "members";
75 311 : checkLocks();
76 311 : loadMembers();
77 311 : if (members_.empty()) {
78 306 : initMembers();
79 : }
80 311 : }
81 :
82 311 : void checkLocks()
83 : {
84 311 : auto repo = repository();
85 311 : if (!repo)
86 0 : throw std::logic_error("Invalid git repository");
87 :
88 311 : std::filesystem::path repoPath = git_repository_path(repo.get());
89 311 : std::error_code ec;
90 :
91 311 : auto indexPath = std::filesystem::path(repoPath / "index.lock");
92 311 : if (std::filesystem::exists(indexPath, ec)) {
93 0 : JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked, removing lock {}", accountId_, id_, indexPath);
94 0 : std::filesystem::remove(indexPath, ec);
95 0 : if (ec)
96 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
97 : accountId_,
98 : id_,
99 : indexPath,
100 : ec.message());
101 : }
102 :
103 622 : auto refPath = std::filesystem::path(repoPath / "refs" / "heads" / "main.lock");
104 311 : if (std::filesystem::exists(refPath)) {
105 0 : JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked, removing lock {}", accountId_, id_, refPath);
106 0 : std::filesystem::remove(refPath, ec);
107 0 : if (ec)
108 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
109 : accountId_,
110 : id_,
111 : refPath,
112 : ec.message());
113 : }
114 :
115 622 : auto remotePath = std::filesystem::path(repoPath / "refs" / "remotes");
116 1088 : for (const auto& fileIt : std::filesystem::directory_iterator(remotePath, ec)) {
117 155 : auto refPath = fileIt.path() / "main.lock";
118 155 : if (std::filesystem::exists(refPath, ec)) {
119 0 : JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked for remote {}, removing lock", accountId_, id_, fileIt.path().filename());
120 0 : std::filesystem::remove(refPath, ec);
121 0 : if (ec)
122 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
123 : accountId_,
124 : id_,
125 : refPath,
126 : ec.message());
127 : }
128 466 : }
129 :
130 311 : auto err = git_repository_state_cleanup(repo.get());
131 311 : if (err < 0) {
132 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to clean up the repository: {}",
133 : accountId_,
134 : id_,
135 : git_error_last()->message);
136 : }
137 311 : }
138 :
139 311 : void loadMembers()
140 : {
141 : try {
142 : // read file
143 617 : auto file = fileutils::loadFile(membersCache_);
144 : // load values
145 5 : msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
146 5 : std::lock_guard lk {membersMtx_};
147 5 : oh.get().convert(members_);
148 311 : } catch (const std::exception& e) {
149 306 : }
150 311 : }
151 : // Note: membersMtx_ needs to be locked when calling saveMembers
152 1293 : void saveMembers()
153 : {
154 1293 : std::ofstream file(membersCache_, std::ios::trunc | std::ios::binary);
155 1293 : msgpack::pack(file, members_);
156 :
157 1293 : if (onMembersChanged_) {
158 987 : std::set<std::string> memberUris;
159 13740 : for (const auto& member : members_) {
160 12753 : memberUris.emplace(member.uri);
161 : }
162 987 : onMembersChanged_(memberUris);
163 987 : }
164 1293 : }
165 :
166 : OnMembersChanged onMembersChanged_ {};
167 :
168 : // NOTE! We use temporary GitRepository to avoid keeping the file opened
169 : // TODO: check why git_remote_fetch() leaves pack-data opened
170 44279 : GitRepository repository() const
171 : {
172 88558 : auto path = fileutils::get_data_dir().string() + "/" + accountId_ + "/"
173 132837 : + "conversations" + "/" + id_;
174 44279 : git_repository* repo = nullptr;
175 44279 : auto err = git_repository_open(&repo, path.c_str());
176 44279 : if (err < 0) {
177 0 : JAMI_ERROR("Unable to open Git repository: {} ({})", path, git_error_last()->message);
178 0 : return {nullptr, git_repository_free};
179 : }
180 44279 : return {std::move(repo), git_repository_free};
181 44279 : }
182 :
183 386 : std::string getDisplayName() const
184 : {
185 386 : auto shared = account_.lock();
186 386 : if (!shared)
187 0 : return {};
188 386 : auto name = shared->getDisplayName();
189 386 : if (name.empty())
190 0 : name = deviceId_;
191 386 : return std::regex_replace(name, regex_display_name, "");
192 386 : }
193 :
194 : GitSignature signature();
195 : bool mergeFastforward(const git_oid* target_oid, int is_unborn);
196 : std::string createMergeCommit(git_index* index, const std::string& wanted_ref);
197 :
198 : bool validCommits(const std::vector<ConversationCommit>& commits) const;
199 : bool checkValidUserDiff(const std::string& userDevice,
200 : const std::string& commitId,
201 : const std::string& parentId) const;
202 : bool checkVote(const std::string& userDevice,
203 : const std::string& commitId,
204 : const std::string& parentId) const;
205 : bool checkEdit(const std::string& userDevice, const ConversationCommit& commit) const;
206 : bool isValidUserAtCommit(const std::string& userDevice, const std::string& commitId, const git_buf& sig, const git_buf& sig_data) const;
207 : bool checkInitialCommit(const std::string& userDevice,
208 : const std::string& commitId,
209 : const std::string& commitMsg) const;
210 : bool checkValidAdd(const std::string& userDevice,
211 : const std::string& uriMember,
212 : const std::string& commitid,
213 : const std::string& parentId) const;
214 : bool checkValidJoins(const std::string& userDevice,
215 : const std::string& uriMember,
216 : const std::string& commitid,
217 : const std::string& parentId) const;
218 : bool checkValidRemove(const std::string& userDevice,
219 : const std::string& uriMember,
220 : const std::string& commitid,
221 : const std::string& parentId) const;
222 : bool checkValidVoteResolution(const std::string& userDevice,
223 : const std::string& uriMember,
224 : const std::string& commitId,
225 : const std::string& parentId,
226 : const std::string& voteType) const;
227 : bool checkValidProfileUpdate(const std::string& userDevice,
228 : const std::string& commitid,
229 : const std::string& parentId) const;
230 :
231 : bool add(const std::string& path);
232 : void addUserDevice();
233 : void resetHard();
234 : // Verify that the device in the repository is still valid
235 : bool validateDevice();
236 : std::string commit(const std::string& msg, bool verifyDevice = true);
237 : std::string commitMessage(const std::string& msg, bool verifyDevice = true);
238 : ConversationMode mode() const;
239 :
240 : // NOTE! GitDiff needs to be deleted before repo
241 : GitDiff diff(git_repository* repo, const std::string& idNew, const std::string& idOld) const;
242 : std::string diffStats(const std::string& newId, const std::string& oldId) const;
243 : std::string diffStats(const GitDiff& diff) const;
244 :
245 : std::vector<ConversationCommit> behind(const std::string& from) const;
246 : void forEachCommit(PreConditionCb&& preCondition,
247 : std::function<void(ConversationCommit&&)>&& emplaceCb,
248 : PostConditionCb&& postCondition,
249 : const std::string& from = "",
250 : bool logIfNotFound = true) const;
251 : std::vector<ConversationCommit> log(const LogOptions& options) const;
252 :
253 : GitObject fileAtTree(const std::string& path, const GitTree& tree) const;
254 : GitObject memberCertificate(std::string_view memberUri, const GitTree& tree) const;
255 : // NOTE! GitDiff needs to be deleted before repo
256 : GitTree treeAtCommit(git_repository* repo, const std::string& commitId) const;
257 :
258 : std::vector<std::string> getInitialMembers() const;
259 :
260 : bool resolveBan(const std::string_view type, const std::string& uri);
261 : bool resolveUnban(const std::string_view type, const std::string& uri);
262 :
263 : std::weak_ptr<JamiAccount> account_;
264 : const std::string id_;
265 : const std::string accountId_;
266 : const std::string userId_;
267 : const std::string deviceId_;
268 : mutable std::optional<ConversationMode> mode_ {};
269 :
270 : // Members utils
271 : mutable std::mutex membersMtx_ {};
272 : std::vector<ConversationMember> members_ {};
273 :
274 1234 : std::vector<ConversationMember> members() const
275 : {
276 1234 : std::lock_guard lk(membersMtx_);
277 2468 : return members_;
278 1234 : }
279 :
280 : std::filesystem::path conversationDataPath_ {};
281 : std::filesystem::path membersCache_ {};
282 :
283 430 : std::map<std::string, std::vector<DeviceId>> devices(bool ignoreExpired = true) const
284 : {
285 430 : auto acc = account_.lock();
286 430 : auto repo = repository();
287 430 : if (!repo or !acc)
288 0 : return {};
289 430 : std::map<std::string, std::vector<DeviceId>> memberDevices;
290 430 : std::string deviceDir = fmt::format("{}devices/", git_repository_workdir(repo.get()));
291 430 : std::error_code ec;
292 2200 : for (const auto& fileIt : std::filesystem::directory_iterator(deviceDir, ec)) {
293 : try {
294 : auto cert = std::make_shared<dht::crypto::Certificate>(
295 1820 : fileutils::loadFile(fileIt.path()));
296 910 : if (!cert)
297 0 : continue;
298 910 : if (ignoreExpired && cert->getExpiration() < std::chrono::system_clock::now())
299 2 : continue;
300 908 : auto issuerUid = cert->getIssuerUID();
301 908 : if (!acc->certStore().getCertificate(issuerUid)) {
302 : // Check that parentCert
303 : auto memberFile = fmt::format("{}members/{}.crt",
304 0 : git_repository_workdir(repo.get()),
305 0 : issuerUid);
306 : auto adminFile = fmt::format("{}admins/{}.crt",
307 0 : git_repository_workdir(repo.get()),
308 0 : issuerUid);
309 : auto parentCert = std::make_shared<dht::crypto::Certificate>(
310 0 : dhtnet::fileutils::loadFile(
311 0 : std::filesystem::is_regular_file(memberFile, ec) ? memberFile : adminFile));
312 0 : if (parentCert
313 0 : && (ignoreExpired
314 0 : || parentCert->getExpiration() < std::chrono::system_clock::now()))
315 0 : acc->certStore().pinCertificate(
316 : parentCert, true); // Pin certificate to local store if not already done
317 0 : }
318 908 : if (!acc->certStore().getCertificate(cert->getPublicKey().getLongId().toString())) {
319 0 : acc->certStore()
320 0 : .pinCertificate(cert,
321 : true); // Pin certificate to local store if not already done
322 : }
323 908 : memberDevices[cert->getIssuerUID()].emplace_back(cert->getPublicKey().getLongId());
324 :
325 910 : } catch (const std::exception&) {
326 0 : }
327 430 : }
328 430 : return memberDevices;
329 430 : }
330 :
331 14925 : std::optional<ConversationCommit> getCommit(const std::string& commitId,
332 : bool logIfNotFound = true) const
333 : {
334 14925 : LogOptions options;
335 14925 : options.from = commitId;
336 14925 : options.nbOfCommits = 1;
337 14925 : options.logIfNotFound = logIfNotFound;
338 14925 : auto commits = log(options);
339 14925 : if (commits.empty())
340 2703 : return std::nullopt;
341 12222 : return std::move(commits[0]);
342 14925 : }
343 :
344 : bool resolveConflicts(git_index* index, const std::string& other_id);
345 :
346 2645 : std::set<std::string> memberUris(std::string_view filter,
347 : const std::set<MemberRole>& filteredRoles) const
348 : {
349 2645 : std::lock_guard lk(membersMtx_);
350 2645 : std::set<std::string> ret;
351 33210 : for (const auto& member : members_) {
352 30565 : if ((filteredRoles.find(member.role) != filteredRoles.end())
353 30565 : or (not filter.empty() and filter == member.uri))
354 1666 : continue;
355 28899 : ret.emplace(member.uri);
356 : }
357 5290 : return ret;
358 2645 : }
359 :
360 : void initMembers();
361 :
362 : std::optional<std::map<std::string, std::string>> convCommitToMap(
363 : const ConversationCommit& commit) const;
364 :
365 : // Permissions
366 : MemberRole updateProfilePermLvl_ {MemberRole::ADMIN};
367 :
368 : /**
369 : * Retrieve the user related to a device using the account's certificate store.
370 : * @note deviceToUri_ is used to cache result and avoid always loading the certificate
371 : */
372 33227 : std::string uriFromDevice(const std::string& deviceId, const std::string& commitId = "") const
373 : {
374 : // Check if we have the device in cache.
375 33227 : std::lock_guard lk(deviceToUriMtx_);
376 33227 : auto it = deviceToUri_.find(deviceId);
377 33227 : if (it != deviceToUri_.end())
378 32036 : return it->second;
379 :
380 1191 : auto acc = account_.lock();
381 1191 : if (!acc)
382 0 : return {};
383 :
384 1191 : auto cert = acc->certStore().getCertificate(deviceId);
385 1191 : if (!cert || !cert->issuer) {
386 43 : if (!commitId.empty()) {
387 43 : std::string uri = uriFromDeviceAtCommit(deviceId, commitId);
388 43 : if (!uri.empty()) {
389 42 : deviceToUri_.insert({deviceId, uri});
390 42 : return uri;
391 : }
392 43 : }
393 : // Not pinned, so load certificate from repo
394 1 : auto repo = repository();
395 1 : if (!repo)
396 0 : return {};
397 2 : auto deviceFile = std::filesystem::path(git_repository_workdir(repo.get())) / "devices"
398 3 : / fmt::format("{}.crt", deviceId);
399 1 : if (!std::filesystem::is_regular_file(deviceFile))
400 1 : return {};
401 : try {
402 0 : cert = std::make_shared<dht::crypto::Certificate>(fileutils::loadFile(deviceFile));
403 0 : } catch (const std::exception&) {
404 0 : JAMI_WARNING("Unable to load certificate from {}", deviceFile);
405 0 : }
406 0 : if (!cert)
407 0 : return {};
408 2 : }
409 1148 : auto issuerUid = cert->issuer ? cert->issuer->getId().toString() : cert->getIssuerUID();
410 1148 : if (issuerUid.empty())
411 0 : return {};
412 :
413 1148 : deviceToUri_.insert({deviceId, issuerUid});
414 1148 : return issuerUid;
415 33227 : }
416 : mutable std::mutex deviceToUriMtx_;
417 : mutable std::map<std::string, std::string> deviceToUri_;
418 :
419 : /**
420 : * Retrieve the user related to a device using certificate directly from the repository at a
421 : * specific commit.
422 : * @note Prefer uriFromDevice() if possible as it uses the cache.
423 : */
424 43 : std::string uriFromDeviceAtCommit(const std::string& deviceId, const std::string& commitId) const
425 : {
426 43 : auto repo = repository();
427 43 : if (!repo)
428 0 : return {};
429 43 : auto tree = treeAtCommit(repo.get(), commitId);
430 0 : auto deviceFile = fmt::format("devices/{}.crt", deviceId);
431 43 : auto blob_device = fileAtTree(deviceFile, tree);
432 43 : if (!blob_device) {
433 3 : JAMI_ERROR("{} announced but not found", deviceId);
434 1 : return {};
435 : }
436 42 : auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
437 42 : return deviceCert.getIssuerUID();
438 43 : }
439 :
440 : /**
441 : * Verify that a certificate modification is correct
442 : * @param certPath Where the certificate is saved (relative path)
443 : * @param userUri Account we want for this certificate
444 : * @param oldCert Previous certificate. getId() should return the same id as the new
445 : * certificate.
446 : * @note There is a few exception because JAMS certificates are buggy right now
447 : */
448 298 : bool verifyCertificate(std::string_view certContent,
449 : const std::string& userUri,
450 : std::string_view oldCert = ""sv) const
451 : {
452 298 : auto cert = dht::crypto::Certificate(certContent);
453 298 : auto isDeviceCertificate = cert.getId().toString() != userUri;
454 298 : auto issuerUid = cert.getIssuerUID();
455 298 : if (isDeviceCertificate && issuerUid.empty()) {
456 : // Err for Jams certificates
457 0 : JAMI_ERROR("Empty issuer for {}", cert.getId().toString());
458 : }
459 298 : if (!oldCert.empty()) {
460 2 : auto deviceCert = dht::crypto::Certificate(oldCert);
461 2 : if (isDeviceCertificate) {
462 1 : if (issuerUid != deviceCert.getIssuerUID()) {
463 : // NOTE: Here, because JAMS certificate can be incorrectly formatted, there is
464 : // just one valid possibility: passing from an empty issuer to
465 : // the valid issuer.
466 0 : if (issuerUid != userUri) {
467 0 : JAMI_ERROR("Device certificate with a bad issuer {}",
468 : cert.getId().toString());
469 0 : return false;
470 : }
471 : }
472 1 : } else if (cert.getId().toString() != userUri) {
473 0 : JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
474 0 : return false;
475 : }
476 2 : if (cert.getId() != deviceCert.getId()) {
477 0 : JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
478 0 : return false;
479 : }
480 2 : return true;
481 2 : }
482 :
483 : // If it's a device certificate, we need to verify that the issuer is not modified
484 296 : if (isDeviceCertificate) {
485 : // Check that issuer is the one we want.
486 : // NOTE: Still one case due to incorrectly formatted certificates from JAMS
487 149 : if (issuerUid != userUri && !issuerUid.empty()) {
488 0 : JAMI_ERROR("Device certificate with a bad issuer {}", cert.getId().toString());
489 0 : return false;
490 : }
491 147 : } else if (cert.getId().toString() != userUri) {
492 0 : JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
493 0 : return false;
494 : }
495 :
496 296 : return true;
497 298 : }
498 :
499 : std::mutex opMtx_; // Mutex for operations
500 : };
501 :
502 : /////////////////////////////////////////////////////////////////////////////////
503 :
504 : /**
505 : * Creates an empty repository
506 : * @param path Path of the new repository
507 : * @return The libgit2's managed repository
508 : */
509 : GitRepository
510 147 : create_empty_repository(const std::string& path)
511 : {
512 147 : git_repository* repo = nullptr;
513 : git_repository_init_options opts;
514 147 : git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
515 147 : opts.flags |= GIT_REPOSITORY_INIT_MKPATH;
516 147 : opts.initial_head = "main";
517 147 : if (git_repository_init_ext(&repo, path.c_str(), &opts) < 0) {
518 0 : JAMI_ERROR("Unable to create a git repository in {}", path);
519 : }
520 147 : return {std::move(repo), git_repository_free};
521 : }
522 :
523 : /**
524 : * Add all files to index
525 : * @param repo
526 : * @return if operation is successful
527 : */
528 : bool
529 290 : git_add_all(git_repository* repo)
530 : {
531 : // git add -A
532 290 : git_index* index_ptr = nullptr;
533 290 : if (git_repository_index(&index_ptr, repo) < 0) {
534 0 : JAMI_ERROR("Unable to open repository index");
535 0 : return false;
536 : }
537 290 : GitIndex index {index_ptr, git_index_free};
538 290 : git_strarray array {nullptr, 0};
539 290 : git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
540 290 : git_index_write(index.get());
541 290 : git_strarray_dispose(&array);
542 290 : return true;
543 290 : }
544 :
545 : /**
546 : * Adds initial files. This adds the certificate of the account in the /admins directory
547 : * the device's key in /devices and the CRLs in /CRLs.
548 : * @param repo The repository
549 : * @return if files were added successfully
550 : */
551 : bool
552 147 : add_initial_files(GitRepository& repo,
553 : const std::shared_ptr<JamiAccount>& account,
554 : ConversationMode mode,
555 : const std::string& otherMember = "")
556 : {
557 147 : auto deviceId = account->currentDeviceId();
558 147 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
559 147 : auto adminsPath = repoPath / "admins";
560 147 : auto devicesPath = repoPath / "devices";
561 147 : auto invitedPath = repoPath / "invited";
562 294 : auto crlsPath = repoPath / "CRLs" / deviceId;
563 :
564 147 : if (!dhtnet::fileutils::recursive_mkdir(adminsPath, 0700)) {
565 0 : JAMI_ERROR("Error when creating {}. Abort create conversations", adminsPath);
566 0 : return false;
567 : }
568 :
569 147 : auto cert = account->identity().second;
570 147 : auto deviceCert = cert->toString(false);
571 147 : auto parentCert = cert->issuer;
572 147 : if (!parentCert) {
573 0 : JAMI_ERROR("Parent cert is null");
574 0 : return false;
575 : }
576 :
577 : // /admins
578 441 : auto adminPath = adminsPath / fmt::format("{}.crt", parentCert->getId().toString());
579 147 : std::ofstream file(adminPath, std::ios::trunc | std::ios::binary);
580 147 : if (!file.is_open()) {
581 0 : JAMI_ERROR("Unable to write data to {}", adminPath);
582 0 : return false;
583 : }
584 147 : file << parentCert->toString(true);
585 147 : file.close();
586 :
587 147 : if (!dhtnet::fileutils::recursive_mkdir(devicesPath, 0700)) {
588 0 : JAMI_ERROR("Error when creating {}. Abort create conversations", devicesPath);
589 0 : return false;
590 : }
591 :
592 : // /devices
593 294 : auto devicePath = devicesPath / fmt::format("{}.crt", deviceId);
594 147 : file = std::ofstream(devicePath, std::ios::trunc | std::ios::binary);
595 147 : if (!file.is_open()) {
596 0 : JAMI_ERROR("Unable to write data to {}", devicePath);
597 0 : return false;
598 : }
599 147 : file << deviceCert;
600 147 : file.close();
601 :
602 147 : if (!dhtnet::fileutils::recursive_mkdir(crlsPath, 0700)) {
603 0 : JAMI_ERROR("Error when creating {}. Abort create conversations", crlsPath);
604 0 : return false;
605 : }
606 :
607 : // /CRLs
608 147 : for (const auto& crl : account->identity().second->getRevocationLists()) {
609 0 : if (!crl)
610 0 : continue;
611 0 : auto crlPath = crlsPath / deviceId / (dht::toHex(crl->getNumber()) + ".crl");
612 0 : std::ofstream file(crlPath, std::ios::trunc | std::ios::binary);
613 0 : if (!file.is_open()) {
614 0 : JAMI_ERROR("Unable to write data to {}", crlPath);
615 0 : return false;
616 : }
617 0 : file << crl->toString();
618 0 : file.close();
619 147 : }
620 :
621 : // /invited for one to one
622 147 : if (mode == ConversationMode::ONE_TO_ONE) {
623 56 : if (!dhtnet::fileutils::recursive_mkdir(invitedPath, 0700)) {
624 0 : JAMI_ERROR("Error when creating {}.", invitedPath);
625 0 : return false;
626 : }
627 56 : auto invitedMemberPath = invitedPath / otherMember;
628 56 : if (std::filesystem::is_regular_file(invitedMemberPath)) {
629 0 : JAMI_WARNING("Member {} already present", otherMember);
630 0 : return false;
631 : }
632 :
633 56 : std::ofstream file(invitedMemberPath, std::ios::trunc | std::ios::binary);
634 56 : if (!file.is_open()) {
635 0 : JAMI_ERROR("Unable to write data to {}", invitedMemberPath);
636 0 : return false;
637 : }
638 56 : }
639 :
640 147 : if (!git_add_all(repo.get())) {
641 0 : return false;
642 : }
643 :
644 441 : JAMI_LOG("Initial files added in {}", repoPath);
645 147 : return true;
646 147 : }
647 :
648 : /**
649 : * Sign and create the initial commit
650 : * @param repo The Git repository
651 : * @param account The account who signs
652 : * @param mode The mode
653 : * @param otherMember If one to one
654 : * @return The first commit hash or empty if failed
655 : */
656 : std::string
657 147 : initial_commit(GitRepository& repo,
658 : const std::shared_ptr<JamiAccount>& account,
659 : ConversationMode mode,
660 : const std::string& otherMember = "")
661 : {
662 147 : auto deviceId = std::string(account->currentDeviceId());
663 147 : auto name = account->getDisplayName();
664 147 : if (name.empty())
665 0 : name = deviceId;
666 147 : name = std::regex_replace(name, regex_display_name, "");
667 :
668 147 : git_signature* sig_ptr = nullptr;
669 147 : git_index* index_ptr = nullptr;
670 : git_oid tree_id, commit_id;
671 147 : git_tree* tree_ptr = nullptr;
672 :
673 : // Sign commit's buffer
674 147 : if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
675 0 : if (git_signature_new(&sig_ptr, deviceId.c_str(), deviceId.c_str(), std::time(nullptr), 0)
676 0 : < 0) {
677 0 : JAMI_ERROR("Unable to create a commit signature.");
678 0 : return {};
679 : }
680 : }
681 147 : GitSignature sig {sig_ptr, git_signature_free};
682 :
683 147 : if (git_repository_index(&index_ptr, repo.get()) < 0) {
684 0 : JAMI_ERROR("Unable to open the repository index");
685 0 : return {};
686 : }
687 147 : GitIndex index {index_ptr, git_index_free};
688 :
689 147 : if (git_index_write_tree(&tree_id, index.get()) < 0) {
690 0 : JAMI_ERROR("Unable to write initial tree from index");
691 0 : return {};
692 : }
693 :
694 147 : if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
695 0 : JAMI_ERROR("Unable to look up the initial tree");
696 0 : return {};
697 : }
698 147 : GitTree tree = {tree_ptr, git_tree_free};
699 :
700 147 : Json::Value json;
701 147 : json["mode"] = static_cast<int>(mode);
702 147 : if (mode == ConversationMode::ONE_TO_ONE) {
703 56 : json["invited"] = otherMember;
704 : }
705 147 : json["type"] = "initial";
706 :
707 147 : git_buf to_sign = {};
708 294 : if (git_commit_create_buffer(&to_sign,
709 : repo.get(),
710 147 : sig.get(),
711 147 : sig.get(),
712 : nullptr,
713 294 : json::toString(json).c_str(),
714 147 : tree.get(),
715 : 0,
716 : nullptr)
717 147 : < 0) {
718 0 : JAMI_ERROR("Unable to create initial buffer");
719 0 : return {};
720 : }
721 :
722 : std::string signed_str = base64::encode(
723 147 : account->identity().first->sign((const uint8_t*) to_sign.ptr, to_sign.size));
724 :
725 : // git commit -S
726 294 : if (git_commit_create_with_signature(&commit_id,
727 : repo.get(),
728 147 : to_sign.ptr,
729 : signed_str.c_str(),
730 : "signature")
731 147 : < 0) {
732 0 : git_buf_dispose(&to_sign);
733 0 : JAMI_ERROR("Unable to sign the initial commit");
734 0 : return {};
735 : }
736 147 : git_buf_dispose(&to_sign);
737 :
738 : // Move commit to main branch
739 147 : git_commit* commit = nullptr;
740 147 : if (git_commit_lookup(&commit, repo.get(), &commit_id) == 0) {
741 147 : git_reference* ref = nullptr;
742 147 : git_branch_create(&ref, repo.get(), "main", commit, true);
743 147 : git_commit_free(commit);
744 147 : git_reference_free(ref);
745 : }
746 :
747 147 : auto commit_str = git_oid_tostr_s(&commit_id);
748 147 : if (commit_str)
749 147 : return commit_str;
750 0 : return {};
751 147 : }
752 :
753 : //////////////////////////////////
754 :
755 : GitSignature
756 386 : ConversationRepository::Impl::signature()
757 : {
758 386 : auto name = getDisplayName();
759 386 : if (name.empty()) {
760 0 : JAMI_ERROR(
761 : "[Account {}] [Conversation {}] Unable to create a commit signature: no name set",
762 : accountId_,
763 : id_);
764 0 : return {nullptr, git_signature_free};
765 : }
766 :
767 386 : git_signature* sig_ptr = nullptr;
768 : // Sign commit's buffer
769 386 : if (git_signature_new(&sig_ptr, name.c_str(), deviceId_.c_str(), std::time(nullptr), 0) < 0) {
770 : // Maybe the display name is invalid (like " ") - try without
771 0 : int err = git_signature_new(&sig_ptr, deviceId_.c_str(), deviceId_.c_str(), std::time(nullptr), 0);
772 0 : if (err < 0) {
773 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to create a commit signature: {}",
774 : accountId_,
775 : id_,
776 : err);
777 0 : return {nullptr, git_signature_free};
778 : }
779 : }
780 386 : return {sig_ptr, git_signature_free};
781 386 : }
782 :
783 : std::string
784 9 : ConversationRepository::Impl::createMergeCommit(git_index* index, const std::string& wanted_ref)
785 : {
786 9 : if (!validateDevice()) {
787 0 : JAMI_ERROR("[Account {}] [Conversation {}] Invalid device. Not migrated?", accountId_, id_);
788 0 : return {};
789 : }
790 : // The merge will occur between current HEAD and wanted_ref
791 9 : git_reference* head_ref_ptr = nullptr;
792 9 : auto repo = repository();
793 9 : if (!repo || git_repository_head(&head_ref_ptr, repo.get()) < 0) {
794 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get HEAD reference", accountId_, id_);
795 0 : return {};
796 : }
797 9 : GitReference head_ref {head_ref_ptr, git_reference_free};
798 :
799 : // Maybe that's a ref, so DWIM it
800 9 : git_reference* merge_ref_ptr = nullptr;
801 9 : git_reference_dwim(&merge_ref_ptr, repo.get(), wanted_ref.c_str());
802 9 : GitReference merge_ref {merge_ref_ptr, git_reference_free};
803 :
804 9 : GitSignature sig {signature()};
805 :
806 : // Prepare a standard merge commit message
807 9 : const char* msg_target = nullptr;
808 9 : if (merge_ref) {
809 0 : git_branch_name(&msg_target, merge_ref.get());
810 : } else {
811 9 : msg_target = wanted_ref.c_str();
812 : }
813 :
814 9 : auto commitMsg = fmt::format("Merge {} '{}'", merge_ref ? "branch" : "commit", msg_target);
815 :
816 : // Set up our parent commits
817 36 : GitCommit parents[2] {{nullptr, git_commit_free}, {nullptr, git_commit_free}};
818 9 : git_commit* parent = nullptr;
819 9 : if (git_reference_peel((git_object**) &parent, head_ref.get(), GIT_OBJ_COMMIT) < 0) {
820 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to peel HEAD reference", accountId_, id_);
821 0 : return {};
822 : }
823 9 : parents[0] = {parent, git_commit_free};
824 : git_oid commit_id;
825 9 : if (git_oid_fromstr(&commit_id, wanted_ref.c_str()) < 0) {
826 0 : return {};
827 : }
828 9 : git_annotated_commit* annotated_ptr = nullptr;
829 9 : if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) {
830 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up commit {}", accountId_, id_, wanted_ref);
831 0 : return {};
832 : }
833 9 : GitAnnotatedCommit annotated {annotated_ptr, git_annotated_commit_free};
834 9 : if (git_commit_lookup(&parent, repo.get(), git_annotated_commit_id(annotated.get())) < 0) {
835 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up commit {}", accountId_, id_, wanted_ref);
836 0 : return {};
837 : }
838 9 : parents[1] = {parent, git_commit_free};
839 :
840 : // Prepare our commit tree
841 : git_oid tree_oid;
842 9 : git_tree* tree_ptr = nullptr;
843 9 : if (git_index_write_tree_to(&tree_oid, index, repo.get()) < 0) {
844 0 : const git_error* err = giterr_last();
845 0 : if (err)
846 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to write index: {}", accountId_, id_, err->message);
847 0 : return {};
848 : }
849 9 : if (git_tree_lookup(&tree_ptr, repo.get(), &tree_oid) < 0) {
850 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up tree", accountId_, id_);
851 0 : return {};
852 : }
853 9 : GitTree tree = {tree_ptr, git_tree_free};
854 :
855 : // Commit
856 9 : git_buf to_sign = {};
857 : // The last argument of git_commit_create_buffer is of type
858 : // 'const git_commit **' in all versions of libgit2 except 1.8.0,
859 : // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
860 : #if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 && \
861 : (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
862 9 : git_commit* const parents_ptr[2] {parents[0].get(), parents[1].get()};
863 : #else
864 : const git_commit* parents_ptr[2] {parents[0].get(), parents[1].get()};
865 : #endif
866 18 : if (git_commit_create_buffer(&to_sign,
867 : repo.get(),
868 9 : sig.get(),
869 9 : sig.get(),
870 : nullptr,
871 : commitMsg.c_str(),
872 9 : tree.get(),
873 : 2,
874 : &parents_ptr[0])
875 9 : < 0) {
876 0 : const git_error* err = giterr_last();
877 0 : if (err)
878 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to create commit buffer: {}", accountId_, id_, err->message);
879 0 : return {};
880 : }
881 :
882 9 : auto account = account_.lock();
883 9 : if (!account)
884 0 : return {};
885 : // git commit -S
886 9 : auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
887 9 : auto signed_buf = account->identity().first->sign(to_sign_vec);
888 9 : std::string signed_str = base64::encode(signed_buf);
889 : git_oid commit_oid;
890 18 : if (git_commit_create_with_signature(&commit_oid,
891 : repo.get(),
892 9 : to_sign.ptr,
893 : signed_str.c_str(),
894 : "signature")
895 9 : < 0) {
896 0 : git_buf_dispose(&to_sign);
897 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
898 0 : return {};
899 : }
900 9 : git_buf_dispose(&to_sign);
901 :
902 9 : auto commit_str = git_oid_tostr_s(&commit_oid);
903 9 : if (commit_str) {
904 27 : JAMI_LOG("[Account {}] [Conversation {}] New merge commit added with id: {}", accountId_, id_, commit_str);
905 : // Move commit to main branch
906 9 : git_reference* ref_ptr = nullptr;
907 9 : if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_oid, true, nullptr)
908 9 : < 0) {
909 0 : const git_error* err = giterr_last();
910 0 : if (err) {
911 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to move commit to main: {}", accountId_, id_, err->message);
912 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
913 0 : id_,
914 : ECOMMIT,
915 0 : err->message);
916 : }
917 0 : return {};
918 : }
919 9 : git_reference_free(ref_ptr);
920 : }
921 :
922 : // We're done merging. Clean up the repository state and index
923 9 : git_repository_state_cleanup(repo.get());
924 :
925 9 : git_object* target_ptr = nullptr;
926 9 : if (git_object_lookup(&target_ptr, repo.get(), &commit_oid, GIT_OBJ_COMMIT) != 0) {
927 0 : const git_error* err = giterr_last();
928 0 : if (err)
929 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to look up OID {}: {}", accountId_, id_, git_oid_tostr_s(&commit_oid), err->message);
930 0 : return {};
931 : }
932 9 : GitObject target {target_ptr, git_object_free};
933 :
934 9 : git_reset(repo.get(), target.get(), GIT_RESET_HARD, nullptr);
935 :
936 9 : return commit_str ? commit_str : "";
937 45 : }
938 :
939 : bool
940 846 : ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is_unborn)
941 : {
942 : // Initialize target
943 846 : git_reference* target_ref_ptr = nullptr;
944 846 : auto repo = repository();
945 846 : if (!repo) {
946 0 : JAMI_ERROR("[Account {}] [Conversation {}] No repository found", accountId_, id_);
947 0 : return false;
948 : }
949 846 : if (is_unborn) {
950 0 : git_reference* head_ref_ptr = nullptr;
951 : // HEAD reference is unborn, lookup manually so we don't try to resolve it
952 0 : if (git_reference_lookup(&head_ref_ptr, repo.get(), "HEAD") < 0) {
953 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to look up HEAD ref", accountId_, id_);
954 0 : return false;
955 : }
956 0 : GitReference head_ref {head_ref_ptr, git_reference_free};
957 :
958 : // Grab the reference HEAD should be pointing to
959 0 : const auto* symbolic_ref = git_reference_symbolic_target(head_ref.get());
960 :
961 : // Create our main reference on the target OID
962 0 : if (git_reference_create(&target_ref_ptr, repo.get(), symbolic_ref, target_oid, 0, nullptr)
963 0 : < 0) {
964 0 : const git_error* err = giterr_last();
965 0 : if (err)
966 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to create main reference: {}", accountId_, id_, err->message);
967 0 : return false;
968 : }
969 :
970 846 : } else if (git_repository_head(&target_ref_ptr, repo.get()) < 0) {
971 : // HEAD exists, just look up and resolve
972 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to get HEAD reference", accountId_, id_);
973 0 : return false;
974 : }
975 846 : GitReference target_ref {target_ref_ptr, git_reference_free};
976 :
977 : // Look up the target object
978 846 : git_object* target_ptr = nullptr;
979 846 : if (git_object_lookup(&target_ptr, repo.get(), target_oid, GIT_OBJ_COMMIT) != 0) {
980 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to look up OID {}", accountId_, id_, git_oid_tostr_s(target_oid));
981 0 : return false;
982 : }
983 846 : GitObject target {target_ptr, git_object_free};
984 :
985 : // Checkout the result so the workdir is in the expected state
986 : git_checkout_options ff_checkout_options;
987 846 : git_checkout_init_options(&ff_checkout_options, GIT_CHECKOUT_OPTIONS_VERSION);
988 846 : ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE;
989 846 : if (git_checkout_tree(repo.get(), target.get(), &ff_checkout_options) != 0) {
990 0 : if (auto err = git_error_last())
991 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to checkout HEAD reference: {}", accountId_, id_, err->message);
992 : else
993 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to checkout HEAD reference: unknown error", accountId_, id_);
994 0 : return false;
995 : }
996 :
997 : // Move the target reference to the target OID
998 : git_reference* new_target_ref;
999 846 : if (git_reference_set_target(&new_target_ref, target_ref.get(), target_oid, nullptr) < 0) {
1000 0 : JAMI_ERROR("[Account {}] [Conversation {}] failed to move HEAD reference", accountId_, id_);
1001 0 : return false;
1002 : }
1003 846 : git_reference_free(new_target_ref);
1004 :
1005 846 : return true;
1006 846 : }
1007 :
1008 : bool
1009 254 : ConversationRepository::Impl::add(const std::string& path)
1010 : {
1011 254 : auto repo = repository();
1012 254 : if (!repo)
1013 0 : return false;
1014 254 : git_index* index_ptr = nullptr;
1015 254 : if (git_repository_index(&index_ptr, repo.get()) < 0) {
1016 0 : JAMI_ERROR("Unable to open repository index");
1017 0 : return false;
1018 : }
1019 254 : GitIndex index {index_ptr, git_index_free};
1020 254 : if (git_index_add_bypath(index.get(), path.c_str()) != 0) {
1021 0 : const git_error* err = giterr_last();
1022 0 : if (err)
1023 0 : JAMI_ERROR("Error when adding file: {}", err->message);
1024 0 : return false;
1025 : }
1026 254 : return git_index_write(index.get()) == 0;
1027 254 : }
1028 :
1029 : bool
1030 133 : ConversationRepository::Impl::checkValidUserDiff(const std::string& userDevice,
1031 : const std::string& commitId,
1032 : const std::string& parentId) const
1033 : {
1034 : // Retrieve tree for recent commit
1035 133 : auto repo = repository();
1036 133 : if (!repo)
1037 0 : return false;
1038 : // Here, we check that a file device is modified or not.
1039 133 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1040 133 : if (changedFiles.size() == 0)
1041 129 : return true;
1042 :
1043 : // If a certificate is modified (in the changedFiles), it MUST be a certificate from the user
1044 : // Retrieve userUri
1045 4 : auto treeNew = treeAtCommit(repo.get(), commitId);
1046 4 : auto userUri = uriFromDevice(userDevice, commitId);
1047 4 : if (userUri.empty())
1048 0 : return false;
1049 :
1050 4 : std::string userDeviceFile = fmt::format("devices/{}.crt", userDevice);
1051 4 : std::string adminsFile = fmt::format("admins/{}.crt", userUri);
1052 0 : std::string membersFile = fmt::format("members/{}.crt", userUri);
1053 4 : auto treeOld = treeAtCommit(repo.get(), parentId);
1054 4 : if (not treeNew or not treeOld)
1055 0 : return false;
1056 8 : for (const auto& changedFile : changedFiles) {
1057 5 : if (changedFile == adminsFile || changedFile == membersFile) {
1058 : // In this case, we should verify it's not added (normal commit, not a member change)
1059 : // but only updated
1060 1 : auto oldFile = fileAtTree(changedFile, treeOld);
1061 1 : if (!oldFile) {
1062 0 : JAMI_ERROR("Invalid file modified: {}", changedFile);
1063 0 : return false;
1064 : }
1065 1 : auto newFile = fileAtTree(changedFile, treeNew);
1066 1 : if (!verifyCertificate(as_view(newFile), userUri, as_view(oldFile))) {
1067 0 : JAMI_ERROR("Invalid certificate {}", changedFile);
1068 0 : return false;
1069 : }
1070 5 : } else if (changedFile == userDeviceFile) {
1071 : // In this case, device is added or modified (certificate expiration)
1072 3 : auto oldFile = fileAtTree(changedFile, treeOld);
1073 3 : std::string_view oldCert;
1074 3 : if (oldFile)
1075 1 : oldCert = as_view(oldFile);
1076 3 : auto newFile = fileAtTree(changedFile, treeNew);
1077 3 : if (!verifyCertificate(as_view(newFile), userUri, oldCert)) {
1078 0 : JAMI_ERROR("Invalid certificate {}", changedFile);
1079 0 : return false;
1080 : }
1081 3 : } else {
1082 : // Invalid file detected
1083 3 : JAMI_ERROR("Invalid add file detected: {} {}", changedFile, (int) *mode_);
1084 1 : return false;
1085 : }
1086 : }
1087 :
1088 3 : return true;
1089 133 : }
1090 :
1091 : bool
1092 0 : ConversationRepository::Impl::checkEdit(const std::string& userDevice,
1093 : const ConversationCommit& commit) const
1094 : {
1095 0 : auto repo = repository();
1096 0 : if (!repo)
1097 0 : return false;
1098 0 : auto userUri = uriFromDevice(userDevice, commit.id);
1099 0 : if (userUri.empty())
1100 0 : return false;
1101 : // Check that edited commit is found, for the same author, and editable (plain/text)
1102 0 : auto commitMap = convCommitToMap(commit);
1103 0 : if (commitMap == std::nullopt) {
1104 0 : return false;
1105 : }
1106 0 : auto editedId = commitMap->at("edit");
1107 0 : auto editedCommit = getCommit(editedId);
1108 0 : if (editedCommit == std::nullopt) {
1109 0 : JAMI_ERROR("Commit {:s} not found", editedId);
1110 0 : return false;
1111 : }
1112 0 : auto editedCommitMap = convCommitToMap(*editedCommit);
1113 0 : if (editedCommitMap == std::nullopt or editedCommitMap->at("author").empty()
1114 0 : or editedCommitMap->at("author") != commitMap->at("author")
1115 0 : or commitMap->at("author") != userUri) {
1116 0 : JAMI_ERROR("Edited commit {:s} got a different author ({:s})", editedId, commit.id);
1117 0 : return false;
1118 : }
1119 0 : if (editedCommitMap->at("type") == "text/plain") {
1120 0 : return true;
1121 : }
1122 0 : if (editedCommitMap->at("type") == "application/data-transfer+json") {
1123 0 : if (editedCommitMap->find("tid") != editedCommitMap->end())
1124 0 : return true;
1125 : }
1126 0 : JAMI_ERROR("Edited commit {:s} is not valid!", editedId);
1127 0 : return false;
1128 0 : }
1129 :
1130 : bool
1131 6 : ConversationRepository::Impl::checkVote(const std::string& userDevice,
1132 : const std::string& commitId,
1133 : const std::string& parentId) const
1134 : {
1135 : // Check that maximum deviceFile and a vote is added
1136 6 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1137 6 : if (changedFiles.size() == 0) {
1138 0 : return true;
1139 6 : } else if (changedFiles.size() > 2) {
1140 0 : return false;
1141 : }
1142 : // If modified, it's the first commit of a device, we check
1143 : // that the file wasn't there previously. And the vote MUST be added
1144 6 : std::string deviceFile = "";
1145 6 : std::string votedFile = "";
1146 13 : for (const auto& changedFile : changedFiles) {
1147 : // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
1148 8 : if (changedFile == fmt::format("devices/{}.crt", userDevice)) {
1149 2 : deviceFile = changedFile;
1150 6 : } else if (changedFile.find("votes") == 0) {
1151 5 : votedFile = changedFile;
1152 : } else {
1153 : // Invalid file detected
1154 3 : JAMI_ERROR("Invalid vote file detected: {}", changedFile);
1155 1 : return false;
1156 : }
1157 : }
1158 :
1159 5 : if (votedFile.empty()) {
1160 0 : JAMI_WARNING("No vote detected for commit {}", commitId);
1161 0 : return false;
1162 : }
1163 :
1164 5 : auto repo = repository();
1165 5 : if (!repo)
1166 0 : return false;
1167 5 : auto treeNew = treeAtCommit(repo.get(), commitId);
1168 5 : auto treeOld = treeAtCommit(repo.get(), parentId);
1169 5 : if (not treeNew or not treeOld)
1170 0 : return false;
1171 :
1172 5 : auto userUri = uriFromDevice(userDevice, commitId);
1173 5 : if (userUri.empty())
1174 0 : return false;
1175 : // Check that voter is admin
1176 0 : auto adminFile = fmt::format("admins/{}.crt", userUri);
1177 :
1178 5 : if (!fileAtTree(adminFile, treeOld)) {
1179 0 : JAMI_ERROR("Vote from non admin: {}", userUri);
1180 0 : return false;
1181 : }
1182 :
1183 : // Check votedFile path
1184 : static const std::regex regex_votes(
1185 5 : "votes.(\\w+).(members|devices|admins|invited).(\\w+).(\\w+)");
1186 5 : std::svmatch base_match;
1187 5 : if (!std::regex_match(votedFile, base_match, regex_votes) or base_match.size() != 5) {
1188 0 : JAMI_WARNING("Invalid votes path: {}", votedFile);
1189 0 : return false;
1190 : }
1191 :
1192 5 : std::string_view matchedUri = svsub_match_view(base_match[4]);
1193 5 : if (matchedUri != userUri) {
1194 0 : JAMI_ERROR("Admin voted for other user: {:s} vs {:s}", userUri, matchedUri);
1195 0 : return false;
1196 : }
1197 5 : std::string_view votedUri = svsub_match_view(base_match[3]);
1198 5 : std::string_view type = svsub_match_view(base_match[2]);
1199 5 : std::string_view voteType = svsub_match_view(base_match[1]);
1200 5 : if (voteType != "ban" && voteType != "unban") {
1201 0 : JAMI_ERROR("Unrecognized vote {:s}", voteType);
1202 0 : return false;
1203 : }
1204 :
1205 : // Check that vote file is empty and wasn't modified
1206 5 : if (fileAtTree(votedFile, treeOld)) {
1207 0 : JAMI_ERROR("Invalid voted file modified: {:s}", votedFile);
1208 0 : return false;
1209 : }
1210 5 : auto vote = fileAtTree(votedFile, treeNew);
1211 5 : if (!vote) {
1212 0 : JAMI_ERROR("No vote file found for: {:s}", userUri);
1213 0 : return false;
1214 : }
1215 5 : auto voteContent = as_view(vote);
1216 5 : if (!voteContent.empty()) {
1217 0 : JAMI_ERROR("Vote file not empty: {:s}", votedFile);
1218 0 : return false;
1219 : }
1220 :
1221 : // Check that peer voted is only other device or other member
1222 5 : if (type != "devices") {
1223 5 : if (votedUri == userUri) {
1224 0 : JAMI_ERROR("Detected vote for self: {:s}", votedUri);
1225 0 : return false;
1226 : }
1227 5 : if (voteType == "ban") {
1228 : // file in members or admin or invited
1229 0 : auto invitedFile = fmt::format("invited/{}", votedUri);
1230 5 : if (!memberCertificate(votedUri, treeOld) && !fileAtTree(invitedFile, treeOld)) {
1231 0 : JAMI_ERROR("No member file found for vote: {:s}", votedUri);
1232 0 : return false;
1233 : }
1234 5 : }
1235 : } else {
1236 : // Check not current device
1237 0 : if (votedUri == userDevice) {
1238 0 : JAMI_ERROR("Detected vote for self: {:s}", votedUri);
1239 0 : return false;
1240 : }
1241 : // File in devices
1242 0 : deviceFile = fmt::format("devices/{}.crt", votedUri);
1243 0 : if (!fileAtTree(deviceFile, treeOld)) {
1244 0 : JAMI_ERROR("No device file found for vote: {:s}", votedUri);
1245 0 : return false;
1246 : }
1247 : }
1248 :
1249 5 : return true;
1250 6 : }
1251 :
1252 : bool
1253 723 : ConversationRepository::Impl::checkValidAdd(const std::string& userDevice,
1254 : const std::string& uriMember,
1255 : const std::string& commitId,
1256 : const std::string& parentId) const
1257 : {
1258 723 : auto repo = repository();
1259 723 : if (not repo)
1260 0 : return false;
1261 :
1262 : // std::string repoPath = git_repository_workdir(repo.get());
1263 723 : if (mode() == ConversationMode::ONE_TO_ONE) {
1264 1 : auto initialMembers = getInitialMembers();
1265 1 : auto it = std::find(initialMembers.begin(), initialMembers.end(), uriMember);
1266 1 : if (it == initialMembers.end()) {
1267 3 : JAMI_ERROR("Invalid add in one to one conversation: {}", uriMember);
1268 1 : return false;
1269 : }
1270 1 : }
1271 :
1272 722 : auto userUri = uriFromDevice(userDevice, commitId);
1273 722 : if (userUri.empty())
1274 0 : return false;
1275 :
1276 : // Check that only /invited/uri.crt is added & deviceFile & CRLs
1277 722 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1278 722 : if (changedFiles.size() == 0) {
1279 0 : return false;
1280 722 : } else if (changedFiles.size() > 3) {
1281 0 : return false;
1282 : }
1283 :
1284 : // Check that user added is not sender
1285 722 : if (userUri == uriMember) {
1286 0 : JAMI_ERROR("Member tried to add self: {}", userUri);
1287 0 : return false;
1288 : }
1289 :
1290 : // If modified, it's the first commit of a device, we check
1291 : // that the file wasn't there previously. And the member MUST be added
1292 : // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
1293 722 : std::string deviceFile = "";
1294 722 : std::string invitedFile = "";
1295 1444 : std::string crlFile = std::string("CRLs/") + userUri;
1296 1443 : for (const auto& changedFile : changedFiles) {
1297 722 : if (changedFile == std::string("devices/") + userDevice + ".crt") {
1298 0 : deviceFile = changedFile;
1299 722 : } else if (changedFile == std::string("invited/") + uriMember) {
1300 721 : invitedFile = changedFile;
1301 1 : } else if (changedFile == crlFile) {
1302 : // Nothing to do
1303 : } else {
1304 : // Invalid file detected
1305 3 : JAMI_ERROR("Invalid add file detected: {}", changedFile);
1306 1 : return false;
1307 : }
1308 : }
1309 :
1310 721 : auto treeOld = treeAtCommit(repo.get(), parentId);
1311 721 : if (not treeOld)
1312 0 : return false;
1313 721 : auto treeNew = treeAtCommit(repo.get(), commitId);
1314 721 : auto blob_invite = fileAtTree(invitedFile, treeNew);
1315 721 : if (!blob_invite) {
1316 0 : JAMI_ERROR("Invitation not found for commit {}", commitId);
1317 0 : return false;
1318 : }
1319 :
1320 721 : auto invitation = as_view(blob_invite);
1321 721 : if (!invitation.empty()) {
1322 0 : JAMI_ERROR("Invitation not empty for commit {}", commitId);
1323 0 : return false;
1324 : }
1325 :
1326 : // Check that user not in /banned
1327 1442 : std::string bannedFile = std::string("banned") + "/" + "members" + "/" + uriMember + ".crt";
1328 721 : if (fileAtTree(bannedFile, treeOld)) {
1329 0 : JAMI_ERROR("Tried to add banned member: {}", bannedFile);
1330 0 : return false;
1331 : }
1332 :
1333 721 : return true;
1334 723 : }
1335 :
1336 : bool
1337 751 : ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
1338 : const std::string& uriMember,
1339 : const std::string& commitId,
1340 : const std::string& parentId) const
1341 : {
1342 : // Check no other files changed
1343 1502 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1344 751 : auto invitedFile = fmt::format("invited/{}", uriMember);
1345 751 : auto membersFile = fmt::format("members/{}.crt", uriMember);
1346 0 : auto deviceFile = fmt::format("devices/{}.crt", userDevice);
1347 :
1348 2998 : for (auto& file : changedFiles) {
1349 2247 : if (file != invitedFile && file != membersFile && file != deviceFile) {
1350 0 : JAMI_ERROR("Unwanted file {} found", file);
1351 0 : return false;
1352 : }
1353 : }
1354 :
1355 : // Retrieve tree for commits
1356 751 : auto repo = repository();
1357 751 : assert(repo);
1358 751 : auto treeNew = treeAtCommit(repo.get(), commitId);
1359 751 : auto treeOld = treeAtCommit(repo.get(), parentId);
1360 751 : if (not treeNew or not treeOld)
1361 0 : return false;
1362 :
1363 : // Check /invited
1364 751 : if (fileAtTree(invitedFile, treeNew)) {
1365 6 : JAMI_ERROR("{} invited not removed", uriMember);
1366 2 : return false;
1367 : }
1368 749 : if (!fileAtTree(invitedFile, treeOld)) {
1369 3 : JAMI_ERROR("{} invited not found", uriMember);
1370 1 : return false;
1371 : }
1372 :
1373 : // Check /members added
1374 748 : if (!fileAtTree(membersFile, treeNew)) {
1375 0 : JAMI_ERROR("{} members not found", uriMember);
1376 0 : return false;
1377 : }
1378 748 : if (fileAtTree(membersFile, treeOld)) {
1379 0 : JAMI_ERROR("{} members found too soon", uriMember);
1380 0 : return false;
1381 : }
1382 :
1383 : // Check /devices added
1384 748 : if (!fileAtTree(deviceFile, treeNew)) {
1385 0 : JAMI_ERROR("{} devices not found", uriMember);
1386 0 : return false;
1387 : }
1388 :
1389 : // Check certificate
1390 748 : auto blob_device = fileAtTree(deviceFile, treeNew);
1391 748 : if (!blob_device) {
1392 0 : JAMI_ERROR("{} announced but not found", deviceFile);
1393 0 : return false;
1394 : }
1395 748 : auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
1396 748 : auto blob_member = fileAtTree(membersFile, treeNew);
1397 748 : if (!blob_member) {
1398 0 : JAMI_ERROR("{} announced but not found", userDevice);
1399 0 : return false;
1400 : }
1401 748 : auto memberCert = dht::crypto::Certificate(as_view(blob_member));
1402 1496 : if (memberCert.getId().toString() != deviceCert.getIssuerUID()
1403 1496 : || deviceCert.getIssuerUID() != uriMember) {
1404 0 : JAMI_ERROR("Incorrect device certificate {} for user {}", userDevice, uriMember);
1405 0 : return false;
1406 : }
1407 :
1408 748 : return true;
1409 751 : }
1410 :
1411 : bool
1412 1 : ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
1413 : const std::string& uriMember,
1414 : const std::string& commitId,
1415 : const std::string& parentId) const
1416 : {
1417 : // Retrieve tree for recent commit
1418 1 : auto repo = repository();
1419 1 : if (!repo)
1420 0 : return false;
1421 1 : auto treeOld = treeAtCommit(repo.get(), parentId);
1422 1 : if (not treeOld)
1423 0 : return false;
1424 :
1425 2 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1426 : // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
1427 1 : std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
1428 1 : std::string adminFile = fmt::format("admins/{}.crt", uriMember);
1429 1 : std::string memberFile = fmt::format("members/{}.crt", uriMember);
1430 1 : std::string crlFile = fmt::format("CRLs/{}", uriMember);
1431 0 : std::string invitedFile = fmt::format("invited/{}", uriMember);
1432 1 : std::vector<std::string> devicesRemoved;
1433 :
1434 : // Check that no weird file is added nor removed
1435 1 : static const std::regex regex_devices("devices.(\\w+)\\.crt");
1436 1 : std::smatch base_match;
1437 3 : for (const auto& f : changedFiles) {
1438 3 : if (f == deviceFile || f == adminFile || f == memberFile || f == crlFile
1439 3 : || f == invitedFile) {
1440 : // Ignore
1441 2 : continue;
1442 0 : } else if (std::regex_match(f, base_match, regex_devices)) {
1443 0 : if (base_match.size() == 2)
1444 0 : devicesRemoved.emplace_back(base_match[1]);
1445 : } else {
1446 0 : JAMI_ERROR("Unwanted changed file detected: {}", f);
1447 0 : return false;
1448 : }
1449 : }
1450 :
1451 : // Check that removed devices are for removed member (or directly uriMember)
1452 1 : for (const auto& deviceUri : devicesRemoved) {
1453 0 : deviceFile = fmt::format("devices/{}.crt", deviceUri);
1454 0 : auto blob_device = fileAtTree(deviceFile, treeOld);
1455 0 : if (!blob_device) {
1456 0 : JAMI_ERROR("Device not found added ({})", deviceFile);
1457 0 : return false;
1458 : }
1459 0 : auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
1460 0 : auto userUri = deviceCert.getIssuerUID();
1461 :
1462 0 : if (uriMember != userUri and uriMember != deviceUri /* If device is removed */) {
1463 0 : JAMI_ERROR("Device removed but not for removed user ({})", deviceFile);
1464 0 : return false;
1465 : }
1466 0 : }
1467 :
1468 1 : return true;
1469 1 : }
1470 :
1471 : bool
1472 8 : ConversationRepository::Impl::checkValidVoteResolution(const std::string& userDevice,
1473 : const std::string& uriMember,
1474 : const std::string& commitId,
1475 : const std::string& parentId,
1476 : const std::string& voteType) const
1477 : {
1478 : // Retrieve tree for recent commit
1479 8 : auto repo = repository();
1480 8 : if (!repo)
1481 0 : return false;
1482 8 : auto treeOld = treeAtCommit(repo.get(), parentId);
1483 8 : if (not treeOld)
1484 0 : return false;
1485 :
1486 16 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1487 : // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
1488 8 : std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
1489 8 : std::string adminFile = fmt::format("admins/{}.crt", uriMember);
1490 8 : std::string memberFile = fmt::format("members/{}.crt", uriMember);
1491 8 : std::string crlFile = fmt::format("CRLs/{}", uriMember);
1492 0 : std::string invitedFile = fmt::format("invited/{}", uriMember);
1493 8 : std::vector<std::string> voters;
1494 8 : std::vector<std::string> devicesRemoved;
1495 8 : std::vector<std::string> bannedFiles;
1496 : // Check that no weird file is added nor removed
1497 :
1498 8 : const std::regex regex_votes("votes." + voteType
1499 16 : + ".(members|devices|admins|invited).(\\w+).(\\w+)");
1500 8 : static const std::regex regex_devices("devices.(\\w+)\\.crt");
1501 8 : static const std::regex regex_banned("banned.(members|devices|admins).(\\w+)\\.crt");
1502 8 : static const std::regex regex_banned_invited("banned.(invited).(\\w+)");
1503 8 : std::smatch base_match;
1504 23 : for (const auto& f : changedFiles) {
1505 30 : if (f == deviceFile || f == adminFile || f == memberFile || f == crlFile
1506 30 : || f == invitedFile) {
1507 : // Ignore
1508 5 : continue;
1509 10 : } else if (std::regex_match(f, base_match, regex_votes)) {
1510 5 : if (base_match.size() != 4 or base_match[2] != uriMember) {
1511 0 : JAMI_ERROR("Invalid vote file detected: {}", f);
1512 0 : return false;
1513 : }
1514 5 : voters.emplace_back(base_match[3]);
1515 : // Check that votes were not added here
1516 5 : if (!fileAtTree(f, treeOld)) {
1517 0 : JAMI_ERROR("invalid vote added ({})", f);
1518 0 : return false;
1519 : }
1520 5 : } else if (std::regex_match(f, base_match, regex_devices)) {
1521 0 : if (base_match.size() == 2)
1522 0 : devicesRemoved.emplace_back(base_match[1]);
1523 5 : } else if (std::regex_match(f, base_match, regex_banned)
1524 5 : || std::regex_match(f, base_match, regex_banned_invited)) {
1525 5 : bannedFiles.emplace_back(f);
1526 5 : if (base_match.size() != 3 or base_match[2] != uriMember) {
1527 0 : JAMI_ERROR("Invalid banned file detected : {}", f);
1528 0 : return false;
1529 : }
1530 : } else {
1531 0 : JAMI_ERROR("Unwanted changed file detected: {}", f);
1532 0 : return false;
1533 : }
1534 : }
1535 :
1536 : // Check that removed devices are for removed member (or directly uriMember)
1537 8 : for (const auto& deviceUri : devicesRemoved) {
1538 0 : deviceFile = fmt::format("devices/{}.crt", deviceUri);
1539 0 : if (voteType == "ban") {
1540 : // If we ban a device, it should be there before
1541 0 : if (!fileAtTree(deviceFile, treeOld)) {
1542 0 : JAMI_ERROR("Device not found added ({})", deviceFile);
1543 0 : return false;
1544 : }
1545 0 : } else if (voteType == "unban") {
1546 : // If we unban a device, it should not be there before
1547 0 : if (fileAtTree(deviceFile, treeOld)) {
1548 0 : JAMI_ERROR("Device not found added ({})", deviceFile);
1549 0 : return false;
1550 : }
1551 : }
1552 0 : if (uriMember != uriFromDevice(deviceUri)
1553 0 : and uriMember != deviceUri /* If device is removed */) {
1554 0 : JAMI_ERROR("Device removed but not for removed user ({})", deviceFile);
1555 0 : return false;
1556 : }
1557 : }
1558 :
1559 8 : auto userUri = uriFromDevice(userDevice, commitId);
1560 8 : if (userUri.empty())
1561 0 : return false;
1562 :
1563 : // Check that voters are admins
1564 8 : adminFile = fmt::format("admins/{}.crt", userUri);
1565 8 : if (!fileAtTree(adminFile, treeOld)) {
1566 3 : JAMI_ERROR("admin file ({}) not found", adminFile);
1567 1 : return false;
1568 : }
1569 :
1570 : // If not for self check that vote is valid and not added
1571 7 : auto nbAdmins = 0;
1572 7 : auto nbVotes = 0;
1573 7 : std::string repoPath = git_repository_workdir(repo.get());
1574 14 : for (const auto& certificate : dhtnet::fileutils::readDirectory(repoPath + "admins")) {
1575 7 : if (certificate.find(".crt") == std::string::npos) {
1576 0 : JAMI_WARNING("Incorrect file found: {}", certificate);
1577 0 : continue;
1578 0 : }
1579 7 : nbAdmins += 1;
1580 14 : auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
1581 7 : if (std::find(voters.begin(), voters.end(), adminUri) != voters.end()) {
1582 5 : nbVotes += 1;
1583 : }
1584 14 : }
1585 :
1586 7 : if (nbAdmins == 0 or (static_cast<double>(nbVotes) / static_cast<double>(nbAdmins)) < .5) {
1587 6 : JAMI_ERROR("Incomplete vote detected (commit: {})", commitId);
1588 2 : return false;
1589 : }
1590 :
1591 : // If not for self check that member or device certificate is moved to banned/
1592 5 : return !bannedFiles.empty();
1593 8 : }
1594 :
1595 : bool
1596 3 : ConversationRepository::Impl::checkValidProfileUpdate(const std::string& userDevice,
1597 : const std::string& commitId,
1598 : const std::string& parentId) const
1599 : {
1600 : // Retrieve tree for recent commit
1601 3 : auto repo = repository();
1602 3 : if (!repo)
1603 0 : return false;
1604 3 : auto treeNew = treeAtCommit(repo.get(), commitId);
1605 3 : auto treeOld = treeAtCommit(repo.get(), parentId);
1606 3 : if (not treeNew or not treeOld)
1607 0 : return false;
1608 :
1609 3 : auto userUri = uriFromDevice(userDevice, commitId);
1610 3 : if (userUri.empty())
1611 0 : return false;
1612 :
1613 : // Check if profile is changed by an user with correct privilege
1614 3 : auto valid = false;
1615 3 : if (updateProfilePermLvl_ == MemberRole::ADMIN) {
1616 0 : std::string adminFile = fmt::format("admins/{}.crt", userUri);
1617 3 : auto adminCert = fileAtTree(adminFile, treeNew);
1618 3 : valid |= adminCert != nullptr;
1619 3 : }
1620 3 : if (updateProfilePermLvl_ >= MemberRole::MEMBER) {
1621 0 : std::string memberFile = fmt::format("members/{}.crt", userUri);
1622 0 : auto memberCert = fileAtTree(memberFile, treeNew);
1623 0 : valid |= memberCert != nullptr;
1624 0 : }
1625 :
1626 3 : if (!valid) {
1627 0 : JAMI_ERROR("Profile changed from unauthorized user: {} ({})", userDevice, userUri);
1628 0 : return false;
1629 : }
1630 :
1631 6 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
1632 : // Check that no weird file is added nor removed
1633 0 : std::string userDeviceFile = fmt::format("devices/{}.crt", userDevice);
1634 6 : for (const auto& f : changedFiles) {
1635 3 : if (f == "profile.vcf") {
1636 : // Ignore
1637 0 : } else if (f == userDeviceFile) {
1638 : // In this case, device is added or modified (certificate expiration)
1639 0 : auto oldFile = fileAtTree(f, treeOld);
1640 0 : std::string_view oldCert;
1641 0 : if (oldFile)
1642 0 : oldCert = as_view(oldFile);
1643 0 : auto newFile = fileAtTree(f, treeNew);
1644 0 : if (!verifyCertificate(as_view(newFile), userUri, oldCert)) {
1645 0 : JAMI_ERROR("Invalid certificate {}", f);
1646 0 : return false;
1647 : }
1648 0 : } else {
1649 0 : JAMI_ERROR("Unwanted changed file detected: {}", f);
1650 0 : return false;
1651 : }
1652 : }
1653 3 : return true;
1654 3 : }
1655 :
1656 : bool
1657 1620 : ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice,
1658 : const std::string& commitId,
1659 : const git_buf& sig,
1660 : const git_buf& sig_data) const
1661 : {
1662 1620 : auto acc = account_.lock();
1663 1620 : if (!acc)
1664 0 : return false;
1665 1620 : auto cert = acc->certStore().getCertificate(userDevice);
1666 1620 : auto hasPinnedCert = cert and cert->issuer;
1667 1620 : auto repo = repository();
1668 1620 : if (not repo)
1669 0 : return false;
1670 :
1671 : // Retrieve tree for commit
1672 1620 : auto tree = treeAtCommit(repo.get(), commitId);
1673 1620 : if (not tree)
1674 0 : return false;
1675 :
1676 : // Check that /devices/userDevice.crt exists
1677 0 : std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
1678 1620 : auto blob_device = fileAtTree(deviceFile, tree);
1679 1620 : if (!blob_device) {
1680 3 : JAMI_ERROR("{} announced but not found", deviceFile);
1681 1 : return false;
1682 : }
1683 1619 : auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
1684 1619 : auto userUri = deviceCert.getIssuerUID();
1685 1619 : if (userUri.empty()) {
1686 0 : JAMI_ERROR("{} got no issuer UID", deviceFile);
1687 0 : if (not hasPinnedCert) {
1688 0 : return false;
1689 : } else {
1690 : // HACK: JAMS device's certificate does not contains any issuer
1691 : // So, getIssuerUID() will be empty here, so there is no way
1692 : // to get the userURI from this certificate.
1693 : // Uses pinned certificate if one.
1694 0 : userUri = cert->issuer->getId().toString();
1695 : }
1696 : }
1697 :
1698 : // Check that /(members|admins)/userUri.crt exists
1699 1619 : auto blob_parent = memberCertificate(userUri, tree);
1700 1619 : if (not blob_parent) {
1701 0 : JAMI_ERROR("Certificate not found for {}", userUri);
1702 0 : return false;
1703 : }
1704 :
1705 : // Check that certificates were still valid
1706 1619 : auto parentCert = dht::crypto::Certificate(as_view(blob_parent));
1707 :
1708 : git_oid oid;
1709 1619 : git_commit* commit_ptr = nullptr;
1710 1619 : if (git_oid_fromstr(&oid, commitId.c_str()) < 0
1711 1619 : || git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
1712 0 : JAMI_WARNING("Failed to look up commit {}", commitId);
1713 0 : return false;
1714 : }
1715 1619 : GitCommit commit = {commit_ptr, git_commit_free};
1716 :
1717 1619 : auto commitTime = std::chrono::system_clock::from_time_t(git_commit_time(commit.get()));
1718 1619 : if (deviceCert.getExpiration() < commitTime) {
1719 0 : JAMI_ERROR("Certificate {} expired", deviceCert.getId().toString());
1720 0 : return false;
1721 : }
1722 1619 : if (parentCert.getExpiration() < commitTime) {
1723 0 : JAMI_ERROR("Certificate {} expired", parentCert.getId().toString());
1724 0 : return false;
1725 : }
1726 :
1727 : // Verify the signature (git verify-commit)
1728 1619 : auto pk = base64::decode(std::string_view(sig.ptr, sig.size));
1729 1619 : bool valid_signature = deviceCert.getPublicKey().checkSignature(reinterpret_cast<const uint8_t*>(
1730 1619 : sig_data.ptr),
1731 1619 : sig_data.size,
1732 1619 : pk.data(),
1733 : pk.size());
1734 :
1735 :
1736 1619 : if (!valid_signature) {
1737 3 : JAMI_WARNING("Commit {} not signed by device {}.", git_oid_tostr_s(&oid), userDevice);
1738 1 : return false;
1739 : }
1740 :
1741 1618 : auto res = parentCert.getId().toString() == userUri;
1742 1618 : if (res && not hasPinnedCert) {
1743 76 : acc->certStore().pinCertificate(std::move(deviceCert));
1744 76 : acc->certStore().pinCertificate(std::move(parentCert));
1745 : }
1746 1618 : return res;
1747 1620 : }
1748 :
1749 : bool
1750 147 : ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
1751 : const std::string& commitId,
1752 : const std::string& commitMsg) const
1753 : {
1754 147 : auto account = account_.lock();
1755 147 : auto repo = repository();
1756 147 : if (not account or not repo) {
1757 0 : JAMI_WARNING("Invalid repository detected");
1758 0 : return false;
1759 : }
1760 :
1761 147 : auto treeNew = treeAtCommit(repo.get(), commitId);
1762 147 : auto userUri = uriFromDevice(userDevice, commitId);
1763 147 : if (userUri.empty())
1764 0 : return false;
1765 :
1766 294 : auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, ""));
1767 : // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
1768 :
1769 : try {
1770 147 : mode();
1771 0 : } catch (...) {
1772 0 : JAMI_ERROR("Invalid mode detected for commit: {}", commitId);
1773 0 : return false;
1774 0 : }
1775 :
1776 147 : std::string invited = {};
1777 147 : if (mode_ == ConversationMode::ONE_TO_ONE) {
1778 45 : Json::Value cm;
1779 45 : if (json::parse(commitMsg, cm)) {
1780 45 : invited = cm["invited"].asString();
1781 : }
1782 45 : }
1783 :
1784 147 : auto hasDevice = false, hasAdmin = false;
1785 147 : std::string adminsFile = fmt::format("admins/{}.crt", userUri);
1786 147 : std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
1787 147 : std::string crlFile = fmt::format("CRLs/{}", userUri);
1788 0 : std::string invitedFile = fmt::format("invited/{}", invited);
1789 :
1790 : // Check that admin cert is added
1791 : // Check that device cert is added
1792 : // Check CRLs added
1793 : // Check that no other file is added
1794 : // Check if invited file present for one to one.
1795 486 : for (const auto& changedFile : changedFiles) {
1796 339 : if (changedFile == adminsFile) {
1797 147 : hasAdmin = true;
1798 147 : auto newFile = fileAtTree(changedFile, treeNew);
1799 147 : if (!verifyCertificate(as_view(newFile), userUri)) {
1800 0 : JAMI_ERROR("Invalid certificate found {}", changedFile);
1801 0 : return false;
1802 : }
1803 339 : } else if (changedFile == deviceFile) {
1804 147 : hasDevice = true;
1805 147 : auto newFile = fileAtTree(changedFile, treeNew);
1806 147 : if (!verifyCertificate(as_view(newFile), userUri)) {
1807 0 : JAMI_ERROR("Invalid certificate found {}", changedFile);
1808 0 : return false;
1809 : }
1810 192 : } else if (changedFile == crlFile || changedFile == invitedFile) {
1811 : // Nothing to do
1812 45 : continue;
1813 : } else {
1814 : // Invalid file detected
1815 0 : JAMI_ERROR("Invalid add file detected: {} {}", changedFile, (int) *mode_);
1816 0 : return false;
1817 : }
1818 : }
1819 :
1820 147 : return hasDevice && hasAdmin;
1821 147 : }
1822 :
1823 : bool
1824 390 : ConversationRepository::Impl::validateDevice()
1825 : {
1826 390 : auto repo = repository();
1827 390 : auto account = account_.lock();
1828 390 : if (!account || !repo) {
1829 0 : JAMI_WARNING("[Account {}] [Conversation {}] Invalid repository detected", accountId_, id_);
1830 0 : return false;
1831 : }
1832 390 : auto path = fmt::format("devices/{}.crt", deviceId_);
1833 390 : std::filesystem::path devicePath = git_repository_workdir(repo.get());
1834 390 : devicePath /= path;
1835 390 : if (!std::filesystem::is_regular_file(devicePath)) {
1836 0 : JAMI_WARNING("[Account {}] [Conversation {}] Unable to find file {}", accountId_, id_, devicePath);
1837 0 : return false;
1838 : }
1839 :
1840 390 : auto wrongDeviceFile = false;
1841 : try {
1842 780 : auto deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
1843 390 : wrongDeviceFile = !account->isValidAccountDevice(deviceCert);
1844 390 : } catch (const std::exception&) {
1845 0 : wrongDeviceFile = true;
1846 0 : }
1847 390 : if (wrongDeviceFile) {
1848 3 : JAMI_WARNING("[Account {}] [Conversation {}] Device certificate is no longer valid. Attempting to update certificate.", accountId_, id_);
1849 : // Replace certificate with current cert
1850 1 : auto cert = account->identity().second;
1851 1 : if (!cert || !account->isValidAccountDevice(*cert)) {
1852 0 : JAMI_ERROR("[Account {}] [Conversation {}] Current device's certificate is invalid. A migration is needed", accountId_, id_);
1853 0 : return false;
1854 : }
1855 1 : std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
1856 1 : if (!file.is_open()) {
1857 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to write data to {}", accountId_, id_, devicePath);
1858 0 : return false;
1859 : }
1860 1 : file << cert->toString(false);
1861 1 : file.close();
1862 1 : if (!add(path)) {
1863 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to add file {}", accountId_, id_, devicePath);
1864 0 : return false;
1865 : }
1866 1 : }
1867 :
1868 : // Check account cert (a new device can be added but account certifcate can be the old one!)
1869 390 : auto adminPath = fmt::format("admins/{}.crt", userId_);
1870 390 : auto memberPath = fmt::format("members/{}.crt", userId_);
1871 390 : std::filesystem::path parentPath = git_repository_workdir(repo.get());
1872 390 : std::filesystem::path relativeParentPath;
1873 390 : if (std::filesystem::is_regular_file(parentPath / adminPath))
1874 225 : relativeParentPath = adminPath;
1875 165 : else if (std::filesystem::is_regular_file(parentPath / memberPath))
1876 164 : relativeParentPath = memberPath;
1877 390 : parentPath /= relativeParentPath;
1878 390 : if (relativeParentPath.empty()) {
1879 3 : JAMI_ERROR("[Account {}] [Conversation {}] Invalid parent path (not in members or admins)", accountId_, id_);
1880 1 : return false;
1881 : }
1882 389 : wrongDeviceFile = false;
1883 : try {
1884 778 : auto parentCert = dht::crypto::Certificate(fileutils::loadFile(parentPath));
1885 389 : wrongDeviceFile = !account->isValidAccountDevice(parentCert);
1886 389 : } catch (const std::exception&) {
1887 0 : wrongDeviceFile = true;
1888 0 : }
1889 389 : if (wrongDeviceFile) {
1890 3 : JAMI_WARNING("[Account {}] [Conversation {}] Account certificate is no longer valid. Attempting to update certificate.", accountId_, id_);
1891 1 : auto cert = account->identity().second;
1892 1 : auto newCert = cert->issuer;
1893 1 : if (newCert && std::filesystem::is_regular_file(parentPath)) {
1894 1 : std::ofstream file(parentPath, std::ios::trunc | std::ios::binary);
1895 1 : if (!file.is_open()) {
1896 0 : JAMI_ERROR("Unable to write data to {}", path);
1897 0 : return false;
1898 : }
1899 1 : file << newCert->toString(true);
1900 1 : file.close();
1901 1 : if (!add(relativeParentPath.string())) {
1902 0 : JAMI_WARNING("Unable to add file {}", path);
1903 0 : return false;
1904 : }
1905 1 : }
1906 1 : }
1907 :
1908 389 : return true;
1909 390 : }
1910 :
1911 : std::string
1912 378 : ConversationRepository::Impl::commit(const std::string& msg, bool verifyDevice)
1913 : {
1914 378 : if (verifyDevice && !validateDevice()) {
1915 3 : JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Invalid device", accountId_, id_);
1916 1 : return {};
1917 : }
1918 377 : GitSignature sig = signature();
1919 377 : if (!sig) {
1920 0 : JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to generate signature", accountId_, id_);
1921 0 : return {};
1922 : }
1923 377 : auto account = account_.lock();
1924 :
1925 : // Retrieve current index
1926 377 : git_index* index_ptr = nullptr;
1927 377 : auto repo = repository();
1928 377 : if (!repo)
1929 0 : return {};
1930 377 : if (git_repository_index(&index_ptr, repo.get()) < 0) {
1931 0 : JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to open repository index", accountId_, id_);
1932 0 : return {};
1933 : }
1934 377 : GitIndex index {index_ptr, git_index_free};
1935 :
1936 : git_oid tree_id;
1937 377 : if (git_index_write_tree(&tree_id, index.get()) < 0) {
1938 0 : JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to write initial tree from index", accountId_, id_);
1939 0 : return {};
1940 : }
1941 :
1942 377 : git_tree* tree_ptr = nullptr;
1943 377 : if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
1944 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
1945 0 : return {};
1946 : }
1947 377 : GitTree tree = {tree_ptr, git_tree_free};
1948 :
1949 : git_oid commit_id;
1950 377 : if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
1951 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
1952 0 : return {};
1953 : }
1954 :
1955 377 : git_commit* head_ptr = nullptr;
1956 377 : if (git_commit_lookup(&head_ptr, repo.get(), &commit_id) < 0) {
1957 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", accountId_, id_);
1958 0 : return {};
1959 : }
1960 377 : GitCommit head_commit {head_ptr, git_commit_free};
1961 :
1962 377 : git_buf to_sign = {};
1963 : // The last argument of git_commit_create_buffer is of type
1964 : // 'const git_commit **' in all versions of libgit2 except 1.8.0,
1965 : // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
1966 : #if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 && \
1967 : (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
1968 377 : git_commit* const head_ref[1] = {head_commit.get()};
1969 : #else
1970 : const git_commit* head_ref[1] = {head_commit.get()};
1971 : #endif
1972 754 : if (git_commit_create_buffer(&to_sign,
1973 : repo.get(),
1974 377 : sig.get(),
1975 377 : sig.get(),
1976 : nullptr,
1977 : msg.c_str(),
1978 377 : tree.get(),
1979 : 1,
1980 : &head_ref[0])
1981 377 : < 0) {
1982 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to create commit buffer", accountId_, id_);
1983 0 : return {};
1984 : }
1985 :
1986 : // git commit -S
1987 377 : auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
1988 377 : auto signed_buf = account->identity().first->sign(to_sign_vec);
1989 377 : std::string signed_str = base64::encode(signed_buf);
1990 754 : if (git_commit_create_with_signature(&commit_id,
1991 : repo.get(),
1992 377 : to_sign.ptr,
1993 : signed_str.c_str(),
1994 : "signature")
1995 377 : < 0) {
1996 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
1997 0 : git_buf_dispose(&to_sign);
1998 0 : return {};
1999 : }
2000 377 : git_buf_dispose(&to_sign);
2001 :
2002 : // Move commit to main branch
2003 377 : git_reference* ref_ptr = nullptr;
2004 377 : if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr)
2005 377 : < 0) {
2006 0 : const git_error* err = giterr_last();
2007 0 : if (err) {
2008 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to move commit to main: {}", accountId_, id_, err->message);
2009 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2010 0 : id_,
2011 : ECOMMIT,
2012 0 : err->message);
2013 : }
2014 0 : return {};
2015 : }
2016 377 : git_reference_free(ref_ptr);
2017 :
2018 377 : auto commit_str = git_oid_tostr_s(&commit_id);
2019 377 : if (commit_str) {
2020 1131 : JAMI_LOG("[Account {}] [Conversation {}] New message added with id: {}", accountId_, id_, commit_str);
2021 : }
2022 377 : return commit_str ? commit_str : "";
2023 377 : }
2024 :
2025 : ConversationMode
2026 6420 : ConversationRepository::Impl::mode() const
2027 : {
2028 : // If already retrieved, return it, else get it from first commit
2029 6420 : if (mode_ != std::nullopt)
2030 6112 : return *mode_;
2031 :
2032 308 : LogOptions options;
2033 308 : options.from = id_;
2034 308 : options.nbOfCommits = 1;
2035 308 : auto lastMsg = log(options);
2036 308 : if (lastMsg.size() == 0) {
2037 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2038 0 : id_,
2039 : EINVALIDMODE,
2040 : "No initial commit");
2041 0 : throw std::logic_error("Unable to retrieve first commit");
2042 : }
2043 308 : auto commitMsg = lastMsg[0].commit_msg;
2044 :
2045 308 : Json::Value root;
2046 308 : if (!json::parse(commitMsg, root)) {
2047 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2048 0 : id_,
2049 : EINVALIDMODE,
2050 : "No initial commit");
2051 0 : throw std::logic_error("Unable to retrieve first commit");
2052 : }
2053 308 : if (!root.isMember("mode")) {
2054 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2055 0 : id_,
2056 : EINVALIDMODE,
2057 : "No mode detected");
2058 0 : throw std::logic_error("No mode detected for initial commit");
2059 : }
2060 308 : int mode = root["mode"].asInt();
2061 :
2062 308 : switch (mode) {
2063 103 : case 0:
2064 103 : mode_ = ConversationMode::ONE_TO_ONE;
2065 103 : break;
2066 0 : case 1:
2067 0 : mode_ = ConversationMode::ADMIN_INVITES_ONLY;
2068 0 : break;
2069 205 : case 2:
2070 205 : mode_ = ConversationMode::INVITES_ONLY;
2071 205 : break;
2072 0 : case 3:
2073 0 : mode_ = ConversationMode::PUBLIC;
2074 0 : break;
2075 0 : default:
2076 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2077 0 : id_,
2078 : EINVALIDMODE,
2079 : "Incorrect mode detected");
2080 0 : throw std::logic_error("Incorrect mode detected");
2081 : }
2082 308 : return *mode_;
2083 308 : }
2084 :
2085 : std::string
2086 2625 : ConversationRepository::Impl::diffStats(const std::string& newId, const std::string& oldId) const
2087 : {
2088 2625 : if (auto repo = repository()) {
2089 2625 : if (auto d = diff(repo.get(), newId, oldId))
2090 2625 : return diffStats(d);
2091 2625 : }
2092 0 : return {};
2093 : }
2094 :
2095 : GitDiff
2096 2625 : ConversationRepository::Impl::diff(git_repository* repo,
2097 : const std::string& idNew,
2098 : const std::string& idOld) const
2099 : {
2100 2625 : if (!repo) {
2101 0 : JAMI_ERROR("Unable to get reference for HEAD");
2102 0 : return {nullptr, git_diff_free};
2103 : }
2104 :
2105 : // Retrieve tree for commit new
2106 : git_oid oid;
2107 2625 : git_commit* commitNew = nullptr;
2108 2625 : if (idNew == "HEAD") {
2109 0 : if (git_reference_name_to_id(&oid, repo, "HEAD") < 0) {
2110 0 : JAMI_ERROR("Unable to get reference for HEAD");
2111 0 : return {nullptr, git_diff_free};
2112 : }
2113 :
2114 0 : if (git_commit_lookup(&commitNew, repo, &oid) < 0) {
2115 0 : JAMI_ERROR("Unable to look up HEAD commit");
2116 0 : return {nullptr, git_diff_free};
2117 : }
2118 : } else {
2119 2625 : if (git_oid_fromstr(&oid, idNew.c_str()) < 0
2120 2625 : || git_commit_lookup(&commitNew, repo, &oid) < 0) {
2121 0 : GitCommit new_commit = {commitNew, git_commit_free};
2122 0 : JAMI_WARNING("Failed to look up commit {}", idNew);
2123 0 : return {nullptr, git_diff_free};
2124 0 : }
2125 : }
2126 2625 : GitCommit new_commit = {commitNew, git_commit_free};
2127 :
2128 2625 : git_tree* tNew = nullptr;
2129 2625 : if (git_commit_tree(&tNew, new_commit.get()) < 0) {
2130 0 : JAMI_ERROR("Unable to look up initial tree");
2131 0 : return {nullptr, git_diff_free};
2132 : }
2133 2625 : GitTree treeNew = {tNew, git_tree_free};
2134 :
2135 2625 : git_diff* diff_ptr = nullptr;
2136 2625 : if (idOld.empty()) {
2137 148 : if (git_diff_tree_to_tree(&diff_ptr, repo, nullptr, treeNew.get(), {}) < 0) {
2138 0 : JAMI_ERROR("Unable to get diff to empty repository");
2139 0 : return {nullptr, git_diff_free};
2140 : }
2141 148 : return {diff_ptr, git_diff_free};
2142 : }
2143 :
2144 : // Retrieve tree for commit old
2145 2477 : git_commit* commitOld = nullptr;
2146 2477 : if (git_oid_fromstr(&oid, idOld.c_str()) < 0 || git_commit_lookup(&commitOld, repo, &oid) < 0) {
2147 0 : JAMI_WARNING("Failed to look up commit {}", idOld);
2148 0 : return {nullptr, git_diff_free};
2149 : }
2150 2477 : GitCommit old_commit {commitOld, git_commit_free};
2151 :
2152 2477 : git_tree* tOld = nullptr;
2153 2477 : if (git_commit_tree(&tOld, old_commit.get()) < 0) {
2154 0 : JAMI_ERROR("Unable to look up initial tree");
2155 0 : return {nullptr, git_diff_free};
2156 : }
2157 2477 : GitTree treeOld = {tOld, git_tree_free};
2158 :
2159 : // Calc diff
2160 2477 : if (git_diff_tree_to_tree(&diff_ptr, repo, treeOld.get(), treeNew.get(), {}) < 0) {
2161 0 : JAMI_ERROR("Unable to get diff between {} and {}", idOld, idNew);
2162 0 : return {nullptr, git_diff_free};
2163 : }
2164 2477 : return {diff_ptr, git_diff_free};
2165 2625 : }
2166 :
2167 : std::vector<ConversationCommit>
2168 1611 : ConversationRepository::Impl::behind(const std::string& from) const
2169 : {
2170 : git_oid oid_local, oid_head, oid_remote;
2171 1611 : auto repo = repository();
2172 1611 : if (!repo)
2173 0 : return {};
2174 1611 : if (git_reference_name_to_id(&oid_local, repo.get(), "HEAD") < 0) {
2175 0 : JAMI_ERROR("Unable to get reference for HEAD");
2176 0 : return {};
2177 : }
2178 1611 : oid_head = oid_local;
2179 1611 : std::string head = git_oid_tostr_s(&oid_head);
2180 1611 : if (git_oid_fromstr(&oid_remote, from.c_str()) < 0) {
2181 0 : JAMI_ERROR("Unable to get reference for commit {}", from);
2182 0 : return {};
2183 : }
2184 :
2185 : git_oidarray bases;
2186 1611 : if (git_merge_bases(&bases, repo.get(), &oid_local, &oid_remote) != 0) {
2187 0 : JAMI_ERROR("Unable to get any merge base for commit {} and {}", from, head);
2188 0 : return {};
2189 : }
2190 3185 : for (std::size_t i = 0; i < bases.count; ++i) {
2191 1611 : std::string oid = git_oid_tostr_s(&bases.ids[i]);
2192 1611 : if (oid != head) {
2193 37 : oid_local = bases.ids[i];
2194 37 : break;
2195 : }
2196 1611 : }
2197 1611 : git_oidarray_free(&bases);
2198 1611 : std::string to = git_oid_tostr_s(&oid_local);
2199 1611 : if (to == from)
2200 750 : return {};
2201 1722 : return log(LogOptions {from, to});
2202 1611 : }
2203 :
2204 : void
2205 19431 : ConversationRepository::Impl::forEachCommit(PreConditionCb&& preCondition,
2206 : std::function<void(ConversationCommit&&)>&& emplaceCb,
2207 : PostConditionCb&& postCondition,
2208 : const std::string& from,
2209 : bool logIfNotFound) const
2210 : {
2211 : git_oid oid, oidFrom, oidMerge;
2212 :
2213 : // NOTE! Start from head to get all merge possibilities and correct linearized parent.
2214 19431 : auto repo = repository();
2215 19431 : if (!repo or git_reference_name_to_id(&oid, repo.get(), "HEAD") < 0) {
2216 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
2217 0 : return;
2218 : }
2219 :
2220 19431 : if (from != "" && git_oid_fromstr(&oidFrom, from.c_str()) == 0) {
2221 17258 : auto isMergeBase = git_merge_base(&oidMerge, repo.get(), &oid, &oidFrom) == 0
2222 17258 : && git_oid_equal(&oidMerge, &oidFrom);
2223 17258 : if (!isMergeBase) {
2224 : // We're logging a non merged branch, so, take this one instead of HEAD
2225 5714 : oid = oidFrom;
2226 : }
2227 : }
2228 :
2229 19431 : git_revwalk* walker_ptr = nullptr;
2230 19431 : if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
2231 2705 : GitRevWalker walker {walker_ptr, git_revwalk_free};
2232 : // This fail can be permitted in the case we check if a commit exists before pulling (so can fail
2233 : // there). Only log if the fail is unwanted.
2234 2705 : if (logIfNotFound)
2235 4422 : JAMI_DEBUG("[Account {}] [Conversation {}] Unable to init revwalker", accountId_, id_);
2236 2705 : return;
2237 2705 : }
2238 :
2239 16726 : GitRevWalker walker {walker_ptr, git_revwalk_free};
2240 16726 : git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
2241 :
2242 39736 : for (auto idx = 0u; !git_revwalk_next(&oid, walker.get()); ++idx) {
2243 38651 : git_commit* commit_ptr = nullptr;
2244 38651 : std::string id = git_oid_tostr_s(&oid);
2245 38651 : if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
2246 0 : JAMI_WARNING("[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_, id);
2247 0 : break;
2248 : }
2249 38651 : GitCommit commit {commit_ptr, git_commit_free};
2250 :
2251 38651 : const git_signature* sig = git_commit_author(commit.get());
2252 38651 : GitAuthor author;
2253 38651 : author.name = sig->name;
2254 38651 : author.email = sig->email;
2255 :
2256 38651 : std::vector<std::string> parents;
2257 38651 : auto parentsCount = git_commit_parentcount(commit.get());
2258 75869 : for (unsigned int p = 0; p < parentsCount; ++p) {
2259 37218 : std::string parent {};
2260 37218 : const git_oid* pid = git_commit_parent_id(commit.get(), p);
2261 37218 : if (pid) {
2262 37218 : parent = git_oid_tostr_s(pid);
2263 37218 : parents.emplace_back(parent);
2264 : }
2265 37218 : }
2266 :
2267 38651 : auto result = preCondition(id, author, commit);
2268 38651 : if (result == CallbackResult::Skip)
2269 1035 : continue;
2270 37616 : else if (result == CallbackResult::Break)
2271 15380 : break;
2272 :
2273 22236 : ConversationCommit cc;
2274 22236 : cc.id = id;
2275 22236 : cc.commit_msg = git_commit_message(commit.get());
2276 22236 : cc.author = std::move(author);
2277 22236 : cc.parents = std::move(parents);
2278 22236 : git_buf signature = {}, signed_data = {};
2279 22236 : if (git_commit_extract_signature(&signature, &signed_data, repo.get(), &oid, "signature")
2280 22236 : < 0) {
2281 0 : JAMI_WARNING("[Account {}] [Conversation {}] Unable to extract signature for commit {}", accountId_, id_, id);
2282 : } else {
2283 44472 : cc.signature = base64::decode(
2284 66708 : std::string(signature.ptr, signature.ptr + signature.size));
2285 44472 : cc.signed_content = std::vector<uint8_t>(signed_data.ptr,
2286 22236 : signed_data.ptr + signed_data.size);
2287 : }
2288 22236 : git_buf_dispose(&signature);
2289 22236 : git_buf_dispose(&signed_data);
2290 22236 : cc.timestamp = git_commit_time(commit.get());
2291 :
2292 22236 : auto post = postCondition(id, cc.author, cc);
2293 22236 : emplaceCb(std::move(cc));
2294 :
2295 22236 : if (post)
2296 261 : break;
2297 88940 : }
2298 19431 : }
2299 :
2300 : std::vector<ConversationCommit>
2301 18057 : ConversationRepository::Impl::log(const LogOptions& options) const
2302 : {
2303 18057 : std::vector<ConversationCommit> commits {};
2304 18057 : auto startLogging = options.from == "";
2305 18057 : auto breakLogging = false;
2306 18057 : forEachCommit(
2307 31840 : [&](const auto& id, const auto& author, const auto& commit) {
2308 31840 : if (!commits.empty()) {
2309 : // Set linearized parent
2310 15463 : commits.rbegin()->linearized_parent = id;
2311 : }
2312 31840 : if (options.skipMerge && git_commit_parentcount(commit.get()) > 1) {
2313 0 : return CallbackResult::Skip;
2314 : }
2315 31840 : if ((options.nbOfCommits != 0 && commits.size() == options.nbOfCommits))
2316 13815 : return CallbackResult::Break; // Stop logging
2317 18025 : if (breakLogging)
2318 0 : return CallbackResult::Break; // Stop logging
2319 18025 : if (id == options.to) {
2320 861 : if (options.includeTo)
2321 0 : breakLogging = true; // For the next commit
2322 : else
2323 861 : return CallbackResult::Break; // Stop logging
2324 : }
2325 :
2326 17164 : if (!startLogging && options.from != "" && options.from == id)
2327 13549 : startLogging = true;
2328 17164 : if (!startLogging)
2329 1023 : return CallbackResult::Skip; // Start logging after this one
2330 :
2331 16141 : if (options.fastLog) {
2332 0 : if (options.authorUri != "") {
2333 0 : if (options.authorUri == uriFromDevice(author.email)) {
2334 0 : return CallbackResult::Break; // Found author, stop
2335 : }
2336 : }
2337 : // Used to only count commit
2338 0 : commits.emplace(commits.end(), ConversationCommit {});
2339 0 : return CallbackResult::Skip;
2340 : }
2341 :
2342 16141 : return CallbackResult::Ok; // Continue
2343 : },
2344 16141 : [&](auto&& cc) { commits.emplace(commits.end(), std::forward<decltype(cc)>(cc)); },
2345 16141 : [](auto, auto, auto) { return false; },
2346 18057 : options.from,
2347 18057 : options.logIfNotFound);
2348 36114 : return commits;
2349 0 : }
2350 :
2351 : GitObject
2352 11142 : ConversationRepository::Impl::fileAtTree(const std::string& path, const GitTree& tree) const
2353 : {
2354 11142 : git_object* blob_ptr = nullptr;
2355 22284 : if (git_object_lookup_bypath(&blob_ptr,
2356 11142 : reinterpret_cast<git_object*>(tree.get()),
2357 : path.c_str(),
2358 : GIT_OBJECT_BLOB)
2359 11142 : != 0) {
2360 3069 : return GitObject {nullptr, git_object_free};
2361 : }
2362 8073 : return GitObject {blob_ptr, git_object_free};
2363 : }
2364 :
2365 : GitObject
2366 1624 : ConversationRepository::Impl::memberCertificate(std::string_view memberUri,
2367 : const GitTree& tree) const
2368 : {
2369 1624 : auto blob = fileAtTree(fmt::format("members/{}.crt", memberUri), tree);
2370 1624 : if (not blob)
2371 837 : blob = fileAtTree(fmt::format("admins/{}.crt", memberUri), tree);
2372 1624 : return blob;
2373 0 : }
2374 :
2375 : GitTree
2376 4787 : ConversationRepository::Impl::treeAtCommit(git_repository* repo, const std::string& commitId) const
2377 : {
2378 : git_oid oid;
2379 4787 : git_commit* commit = nullptr;
2380 4787 : if (git_oid_fromstr(&oid, commitId.c_str()) < 0 || git_commit_lookup(&commit, repo, &oid) < 0) {
2381 0 : JAMI_WARNING("[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_, commitId);
2382 0 : return GitTree {nullptr, git_tree_free};
2383 : }
2384 4787 : GitCommit gc = {commit, git_commit_free};
2385 4787 : git_tree* tree = nullptr;
2386 4787 : if (git_commit_tree(&tree, gc.get()) < 0) {
2387 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
2388 0 : return GitTree {nullptr, git_tree_free};
2389 : }
2390 4787 : return GitTree {tree, git_tree_free};
2391 4787 : }
2392 :
2393 : std::vector<std::string>
2394 155 : ConversationRepository::Impl::getInitialMembers() const
2395 : {
2396 155 : auto acc = account_.lock();
2397 155 : if (!acc)
2398 0 : return {};
2399 155 : LogOptions options;
2400 155 : options.from = id_;
2401 155 : options.nbOfCommits = 1;
2402 155 : auto firstCommit = log(options);
2403 155 : if (firstCommit.size() == 0) {
2404 0 : return {};
2405 : }
2406 155 : auto commit = firstCommit[0];
2407 :
2408 155 : auto authorDevice = commit.author.email;
2409 155 : auto cert = acc->certStore().getCertificate(authorDevice);
2410 155 : if (!cert || !cert->issuer)
2411 0 : return {};
2412 155 : auto authorId = cert->issuer->getId().toString();
2413 155 : if (mode() == ConversationMode::ONE_TO_ONE) {
2414 155 : Json::Value root;
2415 155 : if (!json::parse(commit.commit_msg, root)) {
2416 0 : return {authorId};
2417 : }
2418 155 : if (root.isMember("invited") && root["invited"].asString() != authorId)
2419 462 : return {authorId, root["invited"].asString()};
2420 155 : }
2421 2 : return {authorId};
2422 155 : }
2423 :
2424 : bool
2425 1 : ConversationRepository::Impl::resolveConflicts(git_index* index, const std::string& other_id)
2426 : {
2427 1 : git_index_conflict_iterator* conflict_iterator = nullptr;
2428 1 : const git_index_entry* ancestor_out = nullptr;
2429 1 : const git_index_entry* our_out = nullptr;
2430 1 : const git_index_entry* their_out = nullptr;
2431 :
2432 1 : git_index_conflict_iterator_new(&conflict_iterator, index);
2433 1 : GitIndexConflictIterator ci {conflict_iterator, git_index_conflict_iterator_free};
2434 :
2435 : git_oid head_commit_id;
2436 1 : auto repo = repository();
2437 1 : if (!repo || git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) {
2438 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
2439 0 : return false;
2440 : }
2441 1 : auto commit_str = git_oid_tostr_s(&head_commit_id);
2442 1 : if (!commit_str)
2443 0 : return false;
2444 1 : auto useRemote = (other_id > commit_str); // Choose by commit version
2445 :
2446 : // NOTE: for now, only authorize conflicts on "profile.vcf"
2447 1 : std::vector<git_index_entry> new_entries;
2448 2 : while (git_index_conflict_next(&ancestor_out, &our_out, &their_out, ci.get()) != GIT_ITEROVER) {
2449 1 : if (ancestor_out && ancestor_out->path && our_out && our_out->path && their_out
2450 1 : && their_out->path) {
2451 1 : if (std::string_view(ancestor_out->path) == "profile.vcf"sv) {
2452 : // Checkout the wanted version. Copy the index_entry.
2453 1 : git_index_entry resolution = useRemote ? *their_out : *our_out;
2454 1 : resolution.flags &= GIT_INDEX_STAGE_NORMAL;
2455 1 : if (!(resolution.flags & GIT_IDXENTRY_VALID))
2456 1 : resolution.flags |= GIT_IDXENTRY_VALID;
2457 : // NOTE: do no git_index_add yet, wait for after full conflict checks
2458 1 : new_entries.push_back(resolution);
2459 1 : continue;
2460 1 : }
2461 0 : JAMI_ERROR("Conflict detected on a file that is not authorized: {}", ancestor_out->path);
2462 0 : return false;
2463 : }
2464 0 : return false;
2465 : }
2466 :
2467 2 : for (auto& entry : new_entries)
2468 1 : git_index_add(index, &entry);
2469 1 : git_index_conflict_cleanup(index);
2470 :
2471 : // Checkout and clean up
2472 : git_checkout_options opt;
2473 1 : git_checkout_options_init(&opt, GIT_CHECKOUT_OPTIONS_VERSION);
2474 1 : opt.checkout_strategy |= GIT_CHECKOUT_FORCE;
2475 1 : opt.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS;
2476 1 : if (other_id > commit_str)
2477 1 : opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS;
2478 : else
2479 0 : opt.checkout_strategy |= GIT_CHECKOUT_USE_OURS;
2480 :
2481 1 : if (git_checkout_index(repo.get(), index, &opt) < 0) {
2482 0 : const git_error* err = giterr_last();
2483 0 : if (err)
2484 0 : JAMI_ERROR("Unable to checkout index: {}", err->message);
2485 0 : return false;
2486 : }
2487 :
2488 1 : return true;
2489 1 : }
2490 :
2491 : void
2492 1044 : ConversationRepository::Impl::initMembers()
2493 : {
2494 : using std::filesystem::path;
2495 1044 : auto repo = repository();
2496 1044 : if (!repo)
2497 0 : throw std::logic_error("Invalid Git repository");
2498 :
2499 1044 : std::vector<std::string> uris;
2500 1044 : std::lock_guard lk(membersMtx_);
2501 1044 : members_.clear();
2502 1044 : path repoPath = git_repository_workdir(repo.get());
2503 :
2504 : static const std::vector<std::pair<MemberRole, path>> paths = {
2505 0 : {MemberRole::ADMIN, "admins"},
2506 0 : {MemberRole::MEMBER, "members"},
2507 0 : {MemberRole::INVITED, "invited"},
2508 26 : {MemberRole::BANNED, path("banned") / "members"},
2509 26 : {MemberRole::BANNED, path("banned") / "invited"}
2510 1135 : };
2511 :
2512 1044 : std::error_code ec;
2513 6264 : for (const auto& [role, p] : paths) {
2514 28116 : for (const auto& f : std::filesystem::directory_iterator(repoPath / p, ec)) {
2515 12456 : auto uri = f.path().stem().string();
2516 12456 : if (std::find(uris.begin(), uris.end(), uri) == uris.end()) {
2517 12452 : members_.emplace_back(ConversationMember {uri, role});
2518 12452 : uris.emplace_back(uri);
2519 : }
2520 17676 : }
2521 : }
2522 :
2523 1044 : if (mode() == ConversationMode::ONE_TO_ONE) {
2524 386 : for (const auto& member : getInitialMembers()) {
2525 257 : if (std::find(uris.begin(), uris.end(), member) == uris.end()) {
2526 : // If member is in the initial commit, but not in invited, this means that user left.
2527 0 : members_.emplace_back(ConversationMember {member, MemberRole::LEFT});
2528 : }
2529 129 : }
2530 : }
2531 1044 : saveMembers();
2532 1044 : }
2533 :
2534 : std::optional<std::map<std::string, std::string>>
2535 18865 : ConversationRepository::Impl::convCommitToMap(const ConversationCommit& commit) const
2536 : {
2537 18865 : auto authorId = uriFromDevice(commit.author.email, commit.id);
2538 18865 : if (authorId.empty()) {
2539 3 : JAMI_ERROR("[Account {}] [Conversation {}] Invalid author ID for commit {}", accountId_, id_, commit.id);
2540 1 : return std::nullopt;
2541 : }
2542 18864 : std::string parents;
2543 18864 : auto parentsSize = commit.parents.size();
2544 37166 : for (std::size_t i = 0; i < parentsSize; ++i) {
2545 18302 : parents += commit.parents[i];
2546 18302 : if (i != parentsSize - 1)
2547 25 : parents += ",";
2548 : }
2549 18864 : std::string type {};
2550 18864 : if (parentsSize > 1)
2551 25 : type = "merge";
2552 18864 : std::string body {};
2553 18864 : std::map<std::string, std::string> message;
2554 18864 : if (type.empty()) {
2555 18839 : Json::Value cm;
2556 18839 : if (json::parse(commit.commit_msg, cm)) {
2557 74012 : for (auto const& id : cm.getMemberNames()) {
2558 55173 : if (id == "type") {
2559 18839 : type = cm[id].asString();
2560 18839 : continue;
2561 : }
2562 36334 : message.insert({id, cm[id].asString()});
2563 18839 : }
2564 : }
2565 18839 : }
2566 18864 : if (type.empty()) {
2567 0 : return std::nullopt;
2568 18864 : } else if (type == "application/data-transfer+json") {
2569 : // Avoid the client to do the concatenation
2570 140 : auto tid = message["tid"];
2571 70 : if (not tid.empty()) {
2572 69 : auto extension = fileutils::getFileExtension(message["displayName"]);
2573 69 : if (!extension.empty())
2574 0 : message["fileId"] = fmt::format("{}_{}.{}", commit.id, tid, extension);
2575 : else
2576 138 : message["fileId"] = fmt::format("{}_{}", commit.id, tid);
2577 : } else {
2578 1 : message["fileId"] = "";
2579 : }
2580 70 : }
2581 18864 : message["id"] = commit.id;
2582 18864 : message["parents"] = parents;
2583 18864 : message["linearizedParent"] = commit.linearized_parent;
2584 18864 : message["author"] = authorId;
2585 18864 : message["type"] = type;
2586 18864 : message["timestamp"] = std::to_string(commit.timestamp);
2587 :
2588 18864 : return message;
2589 18865 : }
2590 :
2591 : std::string
2592 2625 : ConversationRepository::Impl::diffStats(const GitDiff& diff) const
2593 : {
2594 2625 : git_diff_stats* stats_ptr = nullptr;
2595 2625 : if (git_diff_get_stats(&stats_ptr, diff.get()) < 0) {
2596 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get diff stats", accountId_, id_);
2597 0 : return {};
2598 : }
2599 2625 : GitDiffStats stats = {stats_ptr, git_diff_stats_free};
2600 :
2601 2625 : git_diff_stats_format_t format = GIT_DIFF_STATS_FULL;
2602 2625 : git_buf statsBuf = {};
2603 2625 : if (git_diff_stats_to_buf(&statsBuf, stats.get(), format, 80) < 0) {
2604 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to format diff stats", accountId_, id_);
2605 0 : return {};
2606 : }
2607 :
2608 2625 : auto res = std::string(statsBuf.ptr, statsBuf.ptr + statsBuf.size);
2609 2625 : git_buf_dispose(&statsBuf);
2610 2625 : return res;
2611 2625 : }
2612 :
2613 : //////////////////////////////////
2614 :
2615 : std::unique_ptr<ConversationRepository>
2616 147 : ConversationRepository::createConversation(const std::shared_ptr<JamiAccount>& account,
2617 : ConversationMode mode,
2618 : const std::string& otherMember)
2619 : {
2620 : // Create temporary directory because we are unable to know the first hash for now
2621 147 : std::uniform_int_distribution<uint64_t> dist;
2622 294 : auto conversationsPath = fileutils::get_data_dir() / account->getAccountID() / "conversations";
2623 147 : dhtnet::fileutils::check_dir(conversationsPath);
2624 294 : auto tmpPath = conversationsPath / std::to_string(dist(account->rand));
2625 147 : if (std::filesystem::is_directory(tmpPath)) {
2626 0 : JAMI_ERROR("{} already exists. Abort create conversations", tmpPath);
2627 0 : return {};
2628 : }
2629 147 : if (!dhtnet::fileutils::recursive_mkdir(tmpPath, 0700)) {
2630 0 : JAMI_ERROR("An error occurred when creating {}. Abort create conversations.", tmpPath);
2631 0 : return {};
2632 : }
2633 147 : auto repo = create_empty_repository(tmpPath.string());
2634 147 : if (!repo) {
2635 0 : return {};
2636 : }
2637 :
2638 : // Add initial files
2639 147 : if (!add_initial_files(repo, account, mode, otherMember)) {
2640 0 : JAMI_ERROR("An error occurred while adding the initial files.");
2641 0 : dhtnet::fileutils::removeAll(tmpPath, true);
2642 0 : return {};
2643 : }
2644 :
2645 : // Commit changes
2646 147 : auto id = initial_commit(repo, account, mode, otherMember);
2647 147 : if (id.empty()) {
2648 0 : JAMI_ERROR("Unable to create initial commit in {}", tmpPath);
2649 0 : dhtnet::fileutils::removeAll(tmpPath, true);
2650 0 : return {};
2651 : }
2652 :
2653 : // Move to wanted directory
2654 147 : auto newPath = conversationsPath / id;
2655 147 : std::error_code ec;
2656 147 : std::filesystem::rename(tmpPath, newPath, ec);
2657 147 : if (ec) {
2658 0 : JAMI_ERROR("Unable to move {} in {}: {}", tmpPath, newPath, ec.message());
2659 0 : dhtnet::fileutils::removeAll(tmpPath, true);
2660 0 : return {};
2661 : }
2662 :
2663 441 : JAMI_LOG("New conversation initialized in {}", newPath);
2664 :
2665 147 : return std::make_unique<ConversationRepository>(account, id);
2666 147 : }
2667 :
2668 : std::unique_ptr<ConversationRepository>
2669 149 : ConversationRepository::cloneConversation(
2670 : const std::shared_ptr<JamiAccount>& account,
2671 : const std::string& deviceId,
2672 : const std::string& conversationId,
2673 : std::function<void(std::vector<ConversationCommit>)>&& checkCommitCb)
2674 : {
2675 : // Verify conversationId is not empty to avoid deleting the entire conversations directory
2676 149 : if (conversationId.empty()) {
2677 0 : JAMI_ERROR("[Account {}] Clone conversation with empty conversationId", account->getAccountID());
2678 0 : return nullptr;
2679 : }
2680 :
2681 298 : auto conversationsPath = fileutils::get_data_dir() / account->getAccountID() / "conversations";
2682 149 : dhtnet::fileutils::check_dir(conversationsPath);
2683 298 : auto path = conversationsPath / conversationId;
2684 0 : auto url = fmt::format("git://{}/{}", deviceId, conversationId);
2685 :
2686 : git_clone_options clone_options;
2687 149 : git_clone_options_init(&clone_options, GIT_CLONE_OPTIONS_VERSION);
2688 149 : git_fetch_options_init(&clone_options.fetch_opts, GIT_FETCH_OPTIONS_VERSION);
2689 149 : clone_options.fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats,
2690 : void*) {
2691 : // Uncomment to get advancment
2692 : // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects)
2693 : // JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects,
2694 : // stats->received_bytes/1024);
2695 : // If a pack is more than 256Mb, it's anormal.
2696 0 : if (stats->received_bytes > MAX_FETCH_SIZE) {
2697 0 : JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
2698 : stats->received_bytes,
2699 : stats->received_objects,
2700 : stats->total_objects);
2701 0 : return -1;
2702 : }
2703 0 : return 0;
2704 149 : };
2705 :
2706 149 : if (std::filesystem::is_directory(path)) {
2707 : // If a crash occurs during a previous clone, just in case
2708 0 : JAMI_WARNING("Removing existing directory {} (the dir exists and non empty)", path);
2709 0 : if (dhtnet::fileutils::removeAll(path, true) != 0)
2710 0 : return nullptr;
2711 : }
2712 :
2713 447 : JAMI_DEBUG("[Account {}] [Conversation {}] Start clone of {:s} to {}", account->getAccountID(), conversationId, url, path);
2714 149 : git_repository* rep = nullptr;
2715 149 : git_clone_options opts = GIT_CLONE_OPTIONS_INIT;
2716 149 : opts.fetch_opts.follow_redirects = GIT_REMOTE_REDIRECT_NONE;
2717 149 : if (auto err = git_clone(&rep, url.c_str(), path.string().c_str(), &opts)) {
2718 1 : if (const git_error* gerr = giterr_last())
2719 3 : JAMI_ERROR("[Account {}] [Conversation {}] Error when retrieving remote conversation: {:s} {}", account->getAccountID(), conversationId, gerr->message, path);
2720 : else
2721 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unknown error {:d} when retrieving remote conversation", account->getAccountID(), conversationId, err);
2722 1 : return nullptr;
2723 : }
2724 148 : git_repository_free(rep);
2725 148 : auto repo = std::make_unique<ConversationRepository>(account, conversationId);
2726 148 : repo->pinCertificates(true); // need to load certificates to validate unknown members
2727 148 : if (!repo->validClone(std::move(checkCommitCb))) {
2728 1 : repo->erase();
2729 3 : JAMI_ERROR("[Account {}] [Conversation {}] An error occurred while validating remote conversation.", account->getAccountID(), conversationId);
2730 1 : return nullptr;
2731 : }
2732 441 : JAMI_LOG("[Account {}] [Conversation {}] New conversation cloned in {}", account->getAccountID(), conversationId, path);
2733 147 : return repo;
2734 149 : }
2735 :
2736 : bool
2737 1760 : ConversationRepository::Impl::validCommits(
2738 : const std::vector<ConversationCommit>& commitsToValidate) const
2739 : {
2740 1760 : auto repo = repository();
2741 :
2742 3524 : for (const auto& commit : commitsToValidate) {
2743 1775 : auto userDevice = commit.author.email;
2744 1775 : auto validUserAtCommit = commit.id;
2745 :
2746 : git_oid oid;
2747 1775 : git_commit* commit_ptr = nullptr;
2748 1775 : if (git_oid_fromstr(&oid, validUserAtCommit.c_str()) < 0
2749 1775 : || git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
2750 0 : JAMI_WARNING("Failed to look up commit {}", validUserAtCommit.c_str());
2751 : }
2752 : struct GitBufDeleter
2753 : {
2754 3550 : void operator()(git_buf* b) const { git_buf_dispose(b); }
2755 : };
2756 1775 : std::unique_ptr<git_buf, GitBufDeleter> sig(new git_buf {});
2757 1775 : std::unique_ptr<git_buf, GitBufDeleter> sig_data(new git_buf {});
2758 :
2759 : // Extract the signature block and signature content from the commit
2760 1775 : int sig_extract_res = git_commit_extract_signature(sig.get(),
2761 : sig_data.get(),
2762 : repo.get(),
2763 : &oid,
2764 : "signature");
2765 : // Verify that the extraction was successful
2766 1775 : if (sig_extract_res != 0) {
2767 0 : switch (sig_extract_res) {
2768 0 : case GIT_ERROR_INVALID:
2769 0 : JAMI_ERROR("Error, the commit ID ({}) does not correspond to a commit.",
2770 : validUserAtCommit.c_str());
2771 0 : break;
2772 0 : case GIT_ERROR_OBJECT:
2773 0 : JAMI_ERROR("Error, the commit ID ({}) does not have a signature.",
2774 : validUserAtCommit.c_str());
2775 0 : break;
2776 0 : default:
2777 0 : JAMI_ERROR("An unknown error occurred while extracting signature for commit ID {}.",
2778 : validUserAtCommit.c_str());
2779 0 : break;
2780 : return false;
2781 : }
2782 : }
2783 :
2784 1775 : if (commit.parents.size() == 0) {
2785 147 : if (!checkInitialCommit(userDevice, commit.id, commit.commit_msg)) {
2786 0 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed initial commit {}. Please "
2787 : "ensure that you are using the latest "
2788 : "version of Jami, or that one of your contacts is not performing any "
2789 : "unwanted actions.",
2790 : accountId_,
2791 : id_,
2792 : commit.id);
2793 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2794 0 : accountId_, id_, EVALIDFETCH, "Malformed initial commit");
2795 0 : return false;
2796 : }
2797 1628 : } else if (commit.parents.size() == 1) {
2798 1625 : std::string type = {}, editId = {};
2799 1625 : Json::Value cm;
2800 1625 : if (json::parse(commit.commit_msg, cm)) {
2801 1625 : type = cm["type"].asString();
2802 1625 : editId = cm["edit"].asString();
2803 : } else {
2804 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2805 0 : id_,
2806 : EVALIDFETCH,
2807 : "Malformed commit");
2808 0 : return false;
2809 : }
2810 :
2811 1625 : if (type == "vote") {
2812 : // Check that vote is valid
2813 6 : if (!checkVote(userDevice, commit.id, commit.parents[0])) {
2814 3 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed vote commit {}. Please "
2815 : "ensure that you are using the latest "
2816 : "version of Jami, or that one of your contacts is not performing "
2817 : "any unwanted actions.",
2818 : accountId_,
2819 : id_,
2820 : commit.id);
2821 :
2822 1 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2823 1 : id_,
2824 : EVALIDFETCH,
2825 : "Malformed vote");
2826 1 : return false;
2827 : }
2828 1619 : } else if (type == "member") {
2829 1483 : std::string action = cm["action"].asString();
2830 1483 : std::string uriMember = cm["uri"].asString();
2831 1483 : if (action == "add") {
2832 723 : if (!checkValidAdd(userDevice, uriMember, commit.id, commit.parents[0])) {
2833 6 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed add commit {}. "
2834 : "Please ensure that you are using the latest "
2835 : "version of Jami, or that one of your contacts is not "
2836 : "performing any unwanted actions.",
2837 : accountId_,
2838 : id_,
2839 : commit.id);
2840 :
2841 2 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2842 2 : accountId_, id_, EVALIDFETCH, "Malformed add member commit");
2843 2 : return false;
2844 : }
2845 760 : } else if (action == "join") {
2846 751 : if (!checkValidJoins(userDevice, uriMember, commit.id, commit.parents[0])) {
2847 9 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed joins commit {}. "
2848 : "Please ensure that you are using the latest "
2849 : "version of Jami, or that one of your contacts is not "
2850 : "performing any unwanted actions.",
2851 : accountId_,
2852 : id_,
2853 : commit.id);
2854 :
2855 3 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2856 3 : accountId_, id_, EVALIDFETCH, "Malformed join member commit");
2857 3 : return false;
2858 : }
2859 9 : } else if (action == "remove") {
2860 : // In this case, we remove the user. So if self, the user will not be
2861 : // valid for this commit. Check previous commit
2862 1 : validUserAtCommit = commit.parents[0];
2863 1 : if (!checkValidRemove(userDevice, uriMember, commit.id, commit.parents[0])) {
2864 0 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed removes commit {}. "
2865 : "Please ensure that you are using the latest "
2866 : "version of Jami, or that one of your contacts is not "
2867 : "performing any unwanted actions.",
2868 : accountId_,
2869 : id_,
2870 : commit.id);
2871 :
2872 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2873 0 : accountId_, id_, EVALIDFETCH, "Malformed remove member commit");
2874 0 : return false;
2875 : }
2876 8 : } else if (action == "ban" || action == "unban") {
2877 : // Note device.size() == "member".size()
2878 8 : if (!checkValidVoteResolution(userDevice,
2879 : uriMember,
2880 8 : commit.id,
2881 8 : commit.parents[0],
2882 : action)) {
2883 9 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed removes commit {}. "
2884 : "Please ensure that you are using the latest "
2885 : "version of Jami, or that one of your contacts is not "
2886 : "performing any unwanted actions.",
2887 : accountId_,
2888 : id_,
2889 : commit.id);
2890 :
2891 3 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2892 3 : accountId_, id_, EVALIDFETCH, "Malformed ban member commit");
2893 3 : return false;
2894 : }
2895 : } else {
2896 0 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed member commit {} with "
2897 : "action {}. Please ensure that you are using the latest "
2898 : "version of Jami, or that one of your contacts is not performing "
2899 : "any unwanted actions.",
2900 : accountId_,
2901 : id_,
2902 : commit.id,
2903 : action);
2904 :
2905 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2906 0 : accountId_, id_, EVALIDFETCH, "Malformed member commit");
2907 0 : return false;
2908 : }
2909 1627 : } else if (type == "application/update-profile") {
2910 3 : if (!checkValidProfileUpdate(userDevice, commit.id, commit.parents[0])) {
2911 0 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed profile updates commit "
2912 : "{}. Please ensure that you are using the latest "
2913 : "version of Jami, or that one of your contacts is not performing "
2914 : "any unwanted actions.",
2915 : accountId_,
2916 : id_,
2917 : commit.id);
2918 :
2919 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2920 0 : accountId_, id_, EVALIDFETCH, "Malformed profile updates commit");
2921 0 : return false;
2922 : }
2923 133 : } else if (type == "application/edited-message" || !editId.empty()) {
2924 0 : if (!checkEdit(userDevice, commit)) {
2925 0 : JAMI_ERROR("Commit {:s} malformed", commit.id);
2926 :
2927 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2928 0 : accountId_, id_, EVALIDFETCH, "Malformed edit commit");
2929 0 : return false;
2930 : }
2931 : } else {
2932 : // Note: accept all mimetype here, as we can have new mimetypes
2933 : // Just avoid to add weird files
2934 : // Check that no weird file is added outside device cert nor removed
2935 133 : if (!checkValidUserDiff(userDevice, commit.id, commit.parents[0])) {
2936 3 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed {} commit {}. Please "
2937 : "ensure that you are using the latest "
2938 : "version of Jami, or that one of your contacts is not performing "
2939 : "any unwanted actions.",
2940 : accountId_,
2941 : id_,
2942 : type,
2943 : commit.id);
2944 :
2945 1 : emitSignal<libjami::ConversationSignal::OnConversationError>(
2946 1 : accountId_, id_, EVALIDFETCH, "Malformed commit");
2947 1 : return false;
2948 : }
2949 : }
2950 : // For all commits, check that the user is valid.
2951 : // So, the user certificate MUST be in /members or /admins
2952 : // and device cert MUST be in /devices
2953 1615 : if (!isValidUserAtCommit(userDevice, validUserAtCommit, *sig, *sig_data)) {
2954 3 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed commit {}.Please ensure "
2955 : "that you are using the latest "
2956 : "version of Jami, or that one of your contacts is not performing any "
2957 : "unwanted actions. {}",
2958 : accountId_,
2959 : id_,
2960 : validUserAtCommit,
2961 : commit.commit_msg);
2962 1 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2963 1 : id_,
2964 : EVALIDFETCH,
2965 : "Malformed commit");
2966 1 : return false;
2967 : }
2968 1647 : } else {
2969 : // For all commits, check that the user is valid.
2970 : // So, the user certificate MUST be in /members or /admins
2971 : // and device cert MUST be in /devices
2972 3 : if (!isValidUserAtCommit(userDevice, validUserAtCommit, *sig, *sig_data)) {
2973 0 : JAMI_WARNING("[Account {}] [Conversation {}] Malformed commit {}.Please ensure "
2974 : "that you are using the latest "
2975 : "version of Jami, or that one of your contacts is not performing any "
2976 : "unwanted actions. {}",
2977 : accountId_,
2978 : id_,
2979 : validUserAtCommit,
2980 : commit.commit_msg);
2981 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
2982 0 : id_,
2983 : EVALIDFETCH,
2984 : "Malformed commit");
2985 0 : return false;
2986 : }
2987 : }
2988 5292 : JAMI_DEBUG("[Account {}] [Conversation {}] Validate commit {}", accountId_, id_, commit.id);
2989 1808 : }
2990 1749 : return true;
2991 1760 : }
2992 :
2993 : /////////////////////////////////////////////////////////////////////////////////
2994 :
2995 311 : ConversationRepository::ConversationRepository(const std::shared_ptr<JamiAccount>& account,
2996 311 : const std::string& id)
2997 311 : : pimpl_ {new Impl {account, id}}
2998 311 : {}
2999 :
3000 310 : ConversationRepository::~ConversationRepository() = default;
3001 :
3002 : const std::string&
3003 23659 : ConversationRepository::id() const
3004 : {
3005 23659 : return pimpl_->id_;
3006 : }
3007 :
3008 : std::string
3009 107 : ConversationRepository::addMember(const std::string& uri)
3010 : {
3011 107 : std::lock_guard lkOp(pimpl_->opMtx_);
3012 107 : pimpl_->resetHard();
3013 107 : auto repo = pimpl_->repository();
3014 107 : if (not repo)
3015 0 : return {};
3016 :
3017 : // First, we need to add the member file to the repository if not present
3018 107 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3019 :
3020 107 : std::filesystem::path invitedPath = repoPath / "invited";
3021 107 : if (!dhtnet::fileutils::recursive_mkdir(invitedPath, 0700)) {
3022 0 : JAMI_ERROR("Error when creating {}.", invitedPath);
3023 0 : return {};
3024 : }
3025 107 : std::filesystem::path devicePath = invitedPath / uri;
3026 107 : if (std::filesystem::is_regular_file(devicePath)) {
3027 0 : JAMI_WARNING("Member {} already present!", uri);
3028 0 : return {};
3029 : }
3030 :
3031 107 : std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
3032 107 : if (!file.is_open()) {
3033 0 : JAMI_ERROR("Unable to write data to {}", devicePath);
3034 0 : return {};
3035 : }
3036 107 : std::string path = "invited/" + uri;
3037 107 : if (!pimpl_->add(path))
3038 0 : return {};
3039 :
3040 : {
3041 107 : std::lock_guard lk(pimpl_->membersMtx_);
3042 107 : pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::INVITED});
3043 107 : pimpl_->saveMembers();
3044 107 : }
3045 :
3046 107 : Json::Value json;
3047 107 : json["action"] = "add";
3048 107 : json["uri"] = uri;
3049 107 : json["type"] = "member";
3050 214 : return pimpl_->commit(json::toString(json));
3051 107 : }
3052 :
3053 : void
3054 292 : ConversationRepository::onMembersChanged(OnMembersChanged&& cb)
3055 : {
3056 292 : pimpl_->onMembersChanged_ = std::move(cb);
3057 292 : }
3058 :
3059 : std::string
3060 0 : ConversationRepository::amend(const std::string& id, const std::string& msg)
3061 : {
3062 0 : GitSignature sig = pimpl_->signature();
3063 0 : if (!sig)
3064 0 : return {};
3065 :
3066 : git_oid tree_id, commit_id;
3067 0 : git_commit* commit_ptr = nullptr;
3068 0 : auto repo = pimpl_->repository();
3069 0 : if (!repo || git_oid_fromstr(&tree_id, id.c_str()) < 0
3070 0 : || git_commit_lookup(&commit_ptr, repo.get(), &tree_id) < 0) {
3071 0 : GitCommit commit {commit_ptr, git_commit_free};
3072 0 : JAMI_WARNING("Failed to look up commit {}", id);
3073 0 : return {};
3074 0 : }
3075 0 : GitCommit commit {commit_ptr, git_commit_free};
3076 :
3077 0 : if (git_commit_amend(
3078 0 : &commit_id, commit.get(), nullptr, sig.get(), sig.get(), nullptr, msg.c_str(), nullptr)
3079 0 : < 0) {
3080 0 : const git_error* err = giterr_last();
3081 0 : if (err)
3082 0 : JAMI_ERROR("Unable to amend commit: {}", err->message);
3083 0 : return {};
3084 : }
3085 :
3086 : // Move commit to main branch
3087 0 : git_reference* ref_ptr = nullptr;
3088 0 : if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr)
3089 0 : < 0) {
3090 0 : const git_error* err = giterr_last();
3091 0 : if (err) {
3092 0 : JAMI_ERROR("Unable to move commit to main: {}", err->message);
3093 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(pimpl_->accountId_,
3094 0 : pimpl_->id_,
3095 : ECOMMIT,
3096 0 : err->message);
3097 : }
3098 0 : return {};
3099 : }
3100 0 : git_reference_free(ref_ptr);
3101 :
3102 0 : auto commit_str = git_oid_tostr_s(&commit_id);
3103 0 : if (commit_str) {
3104 0 : JAMI_DEBUG("Commit {} amended (new ID: {})", id, commit_str);
3105 0 : return commit_str;
3106 : }
3107 0 : return {};
3108 0 : }
3109 :
3110 : bool
3111 1652 : ConversationRepository::fetch(const std::string& remoteDeviceId)
3112 : {
3113 1652 : std::lock_guard lkOp(pimpl_->opMtx_);
3114 1652 : pimpl_->resetHard();
3115 : // Fetch distant repository
3116 1652 : git_remote* remote_ptr = nullptr;
3117 : git_fetch_options fetch_opts;
3118 1652 : git_fetch_options_init(&fetch_opts, GIT_FETCH_OPTIONS_VERSION);
3119 1652 : fetch_opts.follow_redirects = GIT_REMOTE_REDIRECT_NONE;
3120 :
3121 1652 : LogOptions options;
3122 1652 : options.nbOfCommits = 1;
3123 1652 : auto lastMsg = log(options);
3124 1652 : if (lastMsg.size() == 0)
3125 0 : return false;
3126 1652 : auto lastCommit = lastMsg[0].id;
3127 :
3128 : // Assert that repository exists
3129 1652 : auto repo = pimpl_->repository();
3130 1652 : if (!repo)
3131 0 : return false;
3132 1652 : auto res = git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str());
3133 1652 : if (res != 0) {
3134 669 : if (res != GIT_ENOTFOUND) {
3135 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up for remote {}", pimpl_->accountId_, pimpl_->id_, remoteDeviceId);
3136 0 : return false;
3137 : }
3138 669 : std::string channelName = fmt::format("git://{}/{}", remoteDeviceId, pimpl_->id_);
3139 669 : if (git_remote_create(&remote_ptr, repo.get(), remoteDeviceId.c_str(), channelName.c_str())
3140 669 : < 0) {
3141 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to create remote for repository", pimpl_->accountId_, pimpl_->id_);
3142 0 : return false;
3143 : }
3144 669 : }
3145 1652 : GitRemote remote {remote_ptr, git_remote_free};
3146 :
3147 32814 : fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void*) {
3148 : // Uncomment to get advancment
3149 : // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects)
3150 : // JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects,
3151 : // stats->received_bytes/1024);
3152 : // If a pack is more than 256Mb, it's anormal.
3153 31162 : if (stats->received_bytes > MAX_FETCH_SIZE) {
3154 0 : JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
3155 : stats->received_bytes,
3156 : stats->received_objects,
3157 : stats->total_objects);
3158 0 : return -1;
3159 : }
3160 31162 : return 0;
3161 1652 : };
3162 1652 : if (git_remote_fetch(remote.get(), nullptr, &fetch_opts, "fetch") < 0) {
3163 41 : const git_error* err = giterr_last();
3164 41 : if (err) {
3165 123 : JAMI_WARNING("[Account {}] [Conversation {}] Unable to fetch remote repository: {:s}",
3166 : pimpl_->accountId_,
3167 : pimpl_->id_,
3168 : err->message);
3169 : }
3170 41 : return false;
3171 : }
3172 :
3173 1611 : return true;
3174 1652 : }
3175 :
3176 : std::string
3177 3222 : ConversationRepository::remoteHead(const std::string& remoteDeviceId,
3178 : const std::string& branch) const
3179 : {
3180 3222 : git_remote* remote_ptr = nullptr;
3181 3222 : auto repo = pimpl_->repository();
3182 3222 : if (!repo || git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str()) < 0) {
3183 0 : JAMI_WARNING("No remote found with ID: {}", remoteDeviceId);
3184 0 : return {};
3185 : }
3186 3222 : GitRemote remote {remote_ptr, git_remote_free};
3187 :
3188 3222 : git_reference* head_ref_ptr = nullptr;
3189 6444 : std::string remoteHead = "refs/remotes/" + remoteDeviceId + "/" + branch;
3190 : git_oid commit_id;
3191 3222 : if (git_reference_name_to_id(&commit_id, repo.get(), remoteHead.c_str()) < 0) {
3192 0 : const git_error* err = giterr_last();
3193 0 : if (err)
3194 0 : JAMI_ERROR("failed to look up {} ref: {}", remoteHead, err->message);
3195 0 : return {};
3196 : }
3197 3222 : GitReference head_ref {head_ref_ptr, git_reference_free};
3198 :
3199 3222 : auto commit_str = git_oid_tostr_s(&commit_id);
3200 3222 : if (!commit_str)
3201 0 : return {};
3202 3222 : return commit_str;
3203 3222 : }
3204 :
3205 : void
3206 266 : ConversationRepository::Impl::addUserDevice()
3207 : {
3208 266 : auto account = account_.lock();
3209 266 : if (!account)
3210 0 : return;
3211 :
3212 : // First, we need to add device file to the repository if not present
3213 266 : auto repo = repository();
3214 266 : if (!repo)
3215 0 : return;
3216 : // NOTE: libgit2 uses / for files
3217 266 : std::string path = fmt::format("devices/{}.crt", deviceId_);
3218 266 : std::filesystem::path devicePath = git_repository_workdir(repo.get()) + path;
3219 266 : if (!std::filesystem::is_regular_file(devicePath)) {
3220 129 : std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
3221 129 : if (!file.is_open()) {
3222 0 : JAMI_ERROR("Unable to write data to {}", devicePath);
3223 0 : return;
3224 : }
3225 129 : auto cert = account->identity().second;
3226 129 : auto deviceCert = cert->toString(false);
3227 129 : file << deviceCert;
3228 129 : file.close();
3229 :
3230 129 : if (!add(path))
3231 0 : JAMI_WARNING("Unable to add file {}", devicePath);
3232 129 : }
3233 266 : }
3234 :
3235 : void
3236 2908 : ConversationRepository::Impl::resetHard()
3237 : {
3238 : #ifdef LIBJAMI_TEST
3239 2908 : if (DISABLE_RESET)
3240 255 : return;
3241 : #endif
3242 2653 : auto repo = repository();
3243 2653 : if (!repo)
3244 0 : return;
3245 2653 : git_object *head_commit_obj = nullptr;
3246 2653 : auto error = git_revparse_single(&head_commit_obj, repo.get(), "HEAD");
3247 2653 : if (error < 0) {
3248 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get HEAD commit: {}", accountId_, id_, error);
3249 0 : return;
3250 : }
3251 2653 : GitObject target {head_commit_obj, git_object_free};
3252 2653 : git_reset(repo.get(), head_commit_obj, GIT_RESET_HARD, nullptr);
3253 2653 : }
3254 :
3255 : std::string
3256 113 : ConversationRepository::commitMessage(const std::string& msg, bool verifyDevice)
3257 : {
3258 113 : std::lock_guard lkOp(pimpl_->opMtx_);
3259 113 : pimpl_->resetHard();
3260 226 : return pimpl_->commitMessage(msg, verifyDevice);
3261 113 : }
3262 :
3263 : std::string
3264 266 : ConversationRepository::Impl::commitMessage(const std::string& msg, bool verifyDevice)
3265 : {
3266 266 : addUserDevice();
3267 266 : return commit(msg, verifyDevice);
3268 : }
3269 :
3270 : std::vector<std::string>
3271 0 : ConversationRepository::commitMessages(const std::vector<std::string>& msgs)
3272 : {
3273 0 : pimpl_->addUserDevice();
3274 0 : std::vector<std::string> ret;
3275 0 : ret.reserve(msgs.size());
3276 0 : for (const auto& msg : msgs)
3277 0 : ret.emplace_back(pimpl_->commit(msg));
3278 0 : return ret;
3279 0 : }
3280 :
3281 : std::vector<ConversationCommit>
3282 1808 : ConversationRepository::log(const LogOptions& options) const
3283 : {
3284 1808 : return pimpl_->log(options);
3285 : }
3286 :
3287 : void
3288 1374 : ConversationRepository::log(PreConditionCb&& preCondition,
3289 : std::function<void(ConversationCommit&&)>&& emplaceCb,
3290 : PostConditionCb&& postCondition,
3291 : const std::string& from,
3292 : bool logIfNotFound) const
3293 : {
3294 2748 : pimpl_->forEachCommit(std::move(preCondition),
3295 1374 : std::move(emplaceCb),
3296 1374 : std::move(postCondition),
3297 : from,
3298 : logIfNotFound);
3299 1374 : }
3300 :
3301 : std::optional<ConversationCommit>
3302 14925 : ConversationRepository::getCommit(const std::string& commitId, bool logIfNotFound) const
3303 : {
3304 14925 : return pimpl_->getCommit(commitId, logIfNotFound);
3305 : }
3306 :
3307 : std::pair<bool, std::string>
3308 855 : ConversationRepository::merge(const std::string& merge_id, bool force)
3309 : {
3310 855 : std::lock_guard lkOp(pimpl_->opMtx_);
3311 855 : pimpl_->resetHard();
3312 : // First, the repository must be in a clean state
3313 855 : auto repo = pimpl_->repository();
3314 855 : if (!repo) {
3315 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to merge without repo", pimpl_->accountId_, pimpl_->id_);
3316 0 : return {false, ""};
3317 : }
3318 855 : int state = git_repository_state(repo.get());
3319 855 : if (state != GIT_REPOSITORY_STATE_NONE) {
3320 0 : pimpl_->resetHard();
3321 0 : int state = git_repository_state(repo.get());
3322 0 : if (state != GIT_REPOSITORY_STATE_NONE) {
3323 0 : JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: repository is in unexpected state {}", pimpl_->accountId_, pimpl_->id_, state);
3324 0 : return {false, ""};
3325 : }
3326 : }
3327 : // Checkout main (to do a `git_merge branch`)
3328 855 : if (git_repository_set_head(repo.get(), "refs/heads/main") < 0) {
3329 0 : JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to checkout main branch", pimpl_->accountId_, pimpl_->id_);
3330 0 : return {false, ""};
3331 : }
3332 :
3333 : // Then check that merge_id exists
3334 : git_oid commit_id;
3335 855 : if (git_oid_fromstr(&commit_id, merge_id.c_str()) < 0) {
3336 0 : JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to look up commit {}", pimpl_->accountId_, pimpl_->id_, merge_id);
3337 0 : return {false, ""};
3338 : }
3339 855 : git_annotated_commit* annotated_ptr = nullptr;
3340 855 : if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) {
3341 0 : JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to look up commit {}", pimpl_->accountId_, pimpl_->id_, merge_id);
3342 0 : return {false, ""};
3343 : }
3344 855 : GitAnnotatedCommit annotated {annotated_ptr, git_annotated_commit_free};
3345 :
3346 : // Now, we can analyze the type of merge required
3347 : git_merge_analysis_t analysis;
3348 : git_merge_preference_t preference;
3349 855 : const git_annotated_commit* const_annotated = annotated.get();
3350 855 : if (git_merge_analysis(&analysis, &preference, repo.get(), &const_annotated, 1) < 0) {
3351 0 : JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: repository analysis failed", pimpl_->accountId_, pimpl_->id_);
3352 0 : return {false, ""};
3353 : }
3354 :
3355 : // Handle easy merges
3356 855 : if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
3357 0 : JAMI_LOG("Already up-to-date");
3358 0 : return {true, ""};
3359 855 : } else if (analysis & GIT_MERGE_ANALYSIS_UNBORN
3360 855 : || (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD
3361 846 : && !(preference & GIT_MERGE_PREFERENCE_NO_FASTFORWARD))) {
3362 846 : if (analysis & GIT_MERGE_ANALYSIS_UNBORN)
3363 0 : JAMI_LOG("[Account {}] [Conversation {}] Merge analysis result: Unborn", pimpl_->accountId_, pimpl_->id_);
3364 : else
3365 2538 : JAMI_LOG("[Account {}] [Conversation {}] Merge analysis result: Fast-forward", pimpl_->accountId_, pimpl_->id_);
3366 846 : const auto* target_oid = git_annotated_commit_id(annotated.get());
3367 :
3368 846 : if (!pimpl_->mergeFastforward(target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN))) {
3369 0 : const git_error* err = giterr_last();
3370 0 : if (err)
3371 0 : JAMI_ERROR("[Account {}] [Conversation {}] Fast forward merge failed: {}", pimpl_->accountId_, pimpl_->id_, err->message);
3372 0 : return {false, ""};
3373 : }
3374 846 : return {true, ""}; // fast forward so no commit generated;
3375 : }
3376 :
3377 9 : if (!pimpl_->validateDevice() && !force) {
3378 0 : JAMI_ERROR("[Account {}] [Conversation {}] Invalid device. Not migrated?", pimpl_->accountId_, pimpl_->id_);
3379 0 : return {false, ""};
3380 : }
3381 :
3382 : // Else we want to check for conflicts
3383 : git_oid head_commit_id;
3384 9 : if (git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) {
3385 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", pimpl_->accountId_, pimpl_->id_);
3386 0 : return {false, ""};
3387 : }
3388 :
3389 9 : git_commit* head_ptr = nullptr;
3390 9 : if (git_commit_lookup(&head_ptr, repo.get(), &head_commit_id) < 0) {
3391 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
3392 0 : return {false, ""};
3393 : }
3394 9 : GitCommit head_commit {head_ptr, git_commit_free};
3395 :
3396 9 : git_commit* other__ptr = nullptr;
3397 9 : if (git_commit_lookup(&other__ptr, repo.get(), &commit_id) < 0) {
3398 0 : JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
3399 0 : return {false, ""};
3400 : }
3401 9 : GitCommit other_commit {other__ptr, git_commit_free};
3402 :
3403 : git_merge_options merge_opts;
3404 9 : git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION);
3405 9 : merge_opts.recursion_limit = 2;
3406 9 : git_index* index_ptr = nullptr;
3407 9 : if (git_merge_commits(&index_ptr, repo.get(), head_commit.get(), other_commit.get(), &merge_opts)
3408 9 : < 0) {
3409 0 : const git_error* err = giterr_last();
3410 0 : if (err)
3411 0 : JAMI_ERROR("[Account {}] [Conversation {}] Git merge failed: {}", pimpl_->accountId_, pimpl_->id_, err->message);
3412 0 : return {false, ""};
3413 : }
3414 9 : GitIndex index {index_ptr, git_index_free};
3415 9 : if (git_index_has_conflicts(index.get())) {
3416 3 : JAMI_LOG("Some conflicts were detected during the merge operations. Resolution phase.");
3417 1 : if (!pimpl_->resolveConflicts(index.get(), merge_id) or !git_add_all(repo.get())) {
3418 0 : JAMI_ERROR("Merge operation aborted; Unable to automatically resolve conflicts");
3419 0 : return {false, ""};
3420 : }
3421 : }
3422 9 : auto result = pimpl_->createMergeCommit(index.get(), merge_id);
3423 27 : JAMI_LOG("Merge done between {} and main", merge_id);
3424 :
3425 9 : return {!result.empty(), result};
3426 855 : }
3427 :
3428 : std::string
3429 8 : ConversationRepository::mergeBase(const std::string& from, const std::string& to) const
3430 : {
3431 8 : if (auto repo = pimpl_->repository()) {
3432 : git_oid oid, oidFrom, oidMerge;
3433 8 : git_oid_fromstr(&oidFrom, from.c_str());
3434 8 : git_oid_fromstr(&oid, to.c_str());
3435 8 : git_merge_base(&oidMerge, repo.get(), &oid, &oidFrom);
3436 8 : if (auto* commit_str = git_oid_tostr_s(&oidMerge))
3437 8 : return commit_str;
3438 8 : }
3439 0 : return {};
3440 : }
3441 :
3442 : std::string
3443 854 : ConversationRepository::diffStats(const std::string& newId, const std::string& oldId) const
3444 : {
3445 854 : return pimpl_->diffStats(newId, oldId);
3446 : }
3447 :
3448 : std::vector<std::string>
3449 2625 : ConversationRepository::changedFiles(std::string_view diffStats)
3450 : {
3451 2625 : static const std::regex re(" +\\| +[0-9]+.*");
3452 2625 : std::vector<std::string> changedFiles;
3453 2625 : std::string_view line;
3454 10204 : while (jami::getline(diffStats, line)) {
3455 7579 : std::svmatch match;
3456 7579 : if (!std::regex_search(line, match, re) && match.size() == 0)
3457 2625 : continue;
3458 4954 : changedFiles.emplace_back(std::regex_replace(std::string {line}, re, "").substr(1));
3459 7579 : }
3460 5250 : return changedFiles;
3461 0 : }
3462 :
3463 : std::string
3464 147 : ConversationRepository::join()
3465 : {
3466 147 : std::lock_guard lkOp(pimpl_->opMtx_);
3467 147 : pimpl_->resetHard();
3468 : // Check that not already member
3469 147 : auto repo = pimpl_->repository();
3470 147 : if (!repo)
3471 0 : return {};
3472 147 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3473 147 : auto account = pimpl_->account_.lock();
3474 147 : if (!account)
3475 0 : return {};
3476 147 : auto cert = account->identity().second;
3477 147 : auto parentCert = cert->issuer;
3478 147 : if (!parentCert) {
3479 0 : JAMI_ERROR("Parent cert is null!");
3480 0 : return {};
3481 : }
3482 147 : auto uri = parentCert->getId().toString();
3483 147 : auto membersPath = repoPath / "members";
3484 294 : auto memberFile = membersPath / (uri + ".crt");
3485 294 : auto adminsPath = repoPath / "admins" / (uri + ".crt");
3486 147 : if (std::filesystem::is_regular_file(memberFile)
3487 147 : or std::filesystem::is_regular_file(adminsPath)) {
3488 : // Already member, nothing to commit
3489 22 : return {};
3490 : }
3491 : // Remove invited/uri.crt
3492 125 : auto invitedPath = repoPath / "invited";
3493 125 : dhtnet::fileutils::remove(fileutils::getFullPath(invitedPath, uri));
3494 : // Add members/uri.crt
3495 125 : if (!dhtnet::fileutils::recursive_mkdir(membersPath, 0700)) {
3496 0 : JAMI_ERROR("Error when creating {}. Abort create conversations", membersPath);
3497 0 : return {};
3498 : }
3499 125 : std::ofstream file(memberFile, std::ios::trunc | std::ios::binary);
3500 125 : if (!file.is_open()) {
3501 0 : JAMI_ERROR("Unable to write data to {}", memberFile);
3502 0 : return {};
3503 : }
3504 125 : file << parentCert->toString(true);
3505 125 : file.close();
3506 : // git add -A
3507 125 : if (!git_add_all(repo.get())) {
3508 0 : return {};
3509 : }
3510 125 : Json::Value json;
3511 125 : json["action"] = "join";
3512 125 : json["uri"] = uri;
3513 125 : json["type"] = "member";
3514 :
3515 : {
3516 125 : std::lock_guard lk(pimpl_->membersMtx_);
3517 125 : auto updated = false;
3518 :
3519 564 : for (auto& member : pimpl_->members_) {
3520 564 : if (member.uri == uri) {
3521 125 : updated = true;
3522 125 : member.role = MemberRole::MEMBER;
3523 125 : break;
3524 : }
3525 : }
3526 125 : if (!updated)
3527 0 : pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::MEMBER});
3528 125 : pimpl_->saveMembers();
3529 125 : }
3530 :
3531 250 : return pimpl_->commitMessage(json::toString(json));
3532 147 : }
3533 :
3534 : std::string
3535 5 : ConversationRepository::leave()
3536 : {
3537 5 : std::lock_guard lkOp(pimpl_->opMtx_);
3538 5 : pimpl_->resetHard();
3539 : // TODO: simplify
3540 5 : auto account = pimpl_->account_.lock();
3541 5 : auto repo = pimpl_->repository();
3542 5 : if (!account || !repo)
3543 0 : return {};
3544 :
3545 : // Remove related files
3546 5 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3547 5 : auto crt = fmt::format("{}.crt", pimpl_->userId_);
3548 10 : auto adminFile = repoPath / "admins" / crt;
3549 10 : auto memberFile = repoPath / "members" / crt;
3550 5 : auto crlsPath = repoPath / "CRLs";
3551 5 : std::error_code ec;
3552 :
3553 5 : if (std::filesystem::is_regular_file(adminFile, ec)) {
3554 5 : std::filesystem::remove(adminFile, ec);
3555 : }
3556 :
3557 5 : if (std::filesystem::is_regular_file(memberFile, ec)) {
3558 0 : std::filesystem::remove(memberFile, ec);
3559 : }
3560 :
3561 : // /CRLs
3562 5 : for (const auto& crl : account->identity().second->getRevocationLists()) {
3563 0 : if (!crl)
3564 0 : continue;
3565 0 : auto crlPath = crlsPath / pimpl_->deviceId_ / fmt::format("{}.crl", dht::toHex(crl->getNumber()));
3566 0 : if (std::filesystem::is_regular_file(crlPath, ec)) {
3567 0 : std::filesystem::remove(crlPath, ec);
3568 : }
3569 5 : }
3570 :
3571 : // Devices
3572 21 : for (const auto& certificate : std::filesystem::directory_iterator(repoPath / "devices", ec)) {
3573 6 : if (certificate.is_regular_file(ec)) {
3574 : try {
3575 12 : crypto::Certificate cert(fileutils::loadFile(certificate.path()));
3576 6 : if (cert.getIssuerUID() == pimpl_->userId_)
3577 5 : std::filesystem::remove(certificate.path(), ec);
3578 6 : } catch (...) {
3579 0 : continue;
3580 0 : }
3581 : }
3582 5 : }
3583 :
3584 5 : if (!git_add_all(repo.get())) {
3585 0 : return {};
3586 : }
3587 :
3588 5 : Json::Value json;
3589 5 : json["action"] = "remove";
3590 5 : json["uri"] = pimpl_->userId_;
3591 5 : json["type"] = "member";
3592 :
3593 : {
3594 5 : std::lock_guard lk(pimpl_->membersMtx_);
3595 10 : pimpl_->members_.erase(std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](auto& member) {
3596 7 : return member.uri == pimpl_->userId_;
3597 5 : }), pimpl_->members_.end());
3598 5 : pimpl_->saveMembers();
3599 5 : }
3600 :
3601 10 : return pimpl_->commit(json::toString(json), false);
3602 5 : }
3603 :
3604 : void
3605 16 : ConversationRepository::erase()
3606 : {
3607 : // First, we need to add the member file to the repository if not present
3608 16 : if (auto repo = pimpl_->repository()) {
3609 16 : std::string repoPath = git_repository_workdir(repo.get());
3610 48 : JAMI_LOG("Erasing {}", repoPath);
3611 16 : dhtnet::fileutils::removeAll(repoPath, true);
3612 32 : }
3613 16 : }
3614 :
3615 : ConversationMode
3616 4351 : ConversationRepository::mode() const
3617 : {
3618 4351 : return pimpl_->mode();
3619 : }
3620 :
3621 : std::string
3622 12 : ConversationRepository::voteKick(const std::string& uri, const std::string& type)
3623 : {
3624 12 : std::lock_guard lkOp(pimpl_->opMtx_);
3625 12 : pimpl_->resetHard();
3626 12 : auto repo = pimpl_->repository();
3627 12 : auto account = pimpl_->account_.lock();
3628 12 : if (!account || !repo)
3629 0 : return {};
3630 12 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3631 12 : auto cert = account->identity().second;
3632 12 : if (!cert || !cert->issuer)
3633 0 : return {};
3634 12 : auto adminUri = cert->issuer->getId().toString();
3635 12 : if (adminUri == uri) {
3636 3 : JAMI_WARNING("Admin tried to ban theirself");
3637 1 : return {};
3638 : }
3639 :
3640 22 : auto oldFile = repoPath / type / (uri + (type != "invited" ? ".crt" : ""));
3641 11 : if (!std::filesystem::is_regular_file(oldFile)) {
3642 0 : JAMI_WARNING("Didn't found file for {} with type {}", uri, type);
3643 0 : return {};
3644 : }
3645 :
3646 0 : auto relativeVotePath = fmt::format("votes/ban/{}/{}", type, uri);
3647 11 : auto voteDirectory = repoPath / relativeVotePath;
3648 11 : if (!dhtnet::fileutils::recursive_mkdir(voteDirectory, 0700)) {
3649 0 : JAMI_ERROR("Error when creating {}. Abort vote", voteDirectory);
3650 0 : return {};
3651 : }
3652 11 : auto votePath = fileutils::getFullPath(voteDirectory, adminUri);
3653 11 : std::ofstream voteFile(votePath, std::ios::trunc | std::ios::binary);
3654 11 : if (!voteFile.is_open()) {
3655 0 : JAMI_ERROR("Unable to write data to {}", votePath);
3656 0 : return {};
3657 : }
3658 11 : voteFile.close();
3659 :
3660 0 : auto toAdd = fmt::format("{}/{}", relativeVotePath, adminUri);
3661 11 : if (!pimpl_->add(toAdd))
3662 0 : return {};
3663 :
3664 11 : Json::Value json;
3665 11 : json["uri"] = uri;
3666 11 : json["type"] = "vote";
3667 22 : return pimpl_->commitMessage(json::toString(json));
3668 12 : }
3669 :
3670 : std::string
3671 1 : ConversationRepository::voteUnban(const std::string& uri, const std::string_view type)
3672 : {
3673 1 : std::lock_guard lkOp(pimpl_->opMtx_);
3674 1 : pimpl_->resetHard();
3675 1 : auto repo = pimpl_->repository();
3676 1 : auto account = pimpl_->account_.lock();
3677 1 : if (!account || !repo)
3678 0 : return {};
3679 1 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3680 1 : auto cert = account->identity().second;
3681 1 : if (!cert || !cert->issuer)
3682 0 : return {};
3683 1 : auto adminUri = cert->issuer->getId().toString();
3684 :
3685 0 : auto relativeVotePath = fmt::format("votes/unban/{}/{}", type, uri);
3686 1 : auto voteDirectory = repoPath / relativeVotePath;
3687 1 : if (!dhtnet::fileutils::recursive_mkdir(voteDirectory, 0700)) {
3688 0 : JAMI_ERROR("Error when creating {}. Abort vote", voteDirectory);
3689 0 : return {};
3690 : }
3691 1 : auto votePath = voteDirectory / adminUri;
3692 1 : std::ofstream voteFile(votePath, std::ios::trunc | std::ios::binary);
3693 1 : if (!voteFile.is_open()) {
3694 0 : JAMI_ERROR("Unable to write data to {}", votePath);
3695 0 : return {};
3696 : }
3697 1 : voteFile.close();
3698 :
3699 2 : auto toAdd = fileutils::getFullPath(relativeVotePath, adminUri).string();
3700 1 : if (!pimpl_->add(toAdd.c_str()))
3701 0 : return {};
3702 :
3703 1 : Json::Value json;
3704 1 : json["uri"] = uri;
3705 1 : json["type"] = "vote";
3706 2 : return pimpl_->commitMessage(json::toString(json));
3707 1 : }
3708 :
3709 : bool
3710 11 : ConversationRepository::Impl::resolveBan(const std::string_view type, const std::string& uri)
3711 : {
3712 11 : auto repo = repository();
3713 11 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3714 11 : auto bannedPath = repoPath / "banned";
3715 11 : auto devicesPath = repoPath / "devices";
3716 : // Move from device or members file into banned
3717 11 : auto crtStr = uri + (type != "invited" ? ".crt" : "");
3718 22 : auto originFilePath = repoPath / type / crtStr;
3719 :
3720 11 : auto destPath = bannedPath / type;
3721 11 : auto destFilePath = destPath / crtStr;
3722 11 : if (!dhtnet::fileutils::recursive_mkdir(destPath, 0700)) {
3723 0 : JAMI_ERROR("An error occurred while creating the {} directory. Abort resolving vote.", destPath);
3724 0 : return false;
3725 : }
3726 :
3727 11 : std::error_code ec;
3728 11 : std::filesystem::rename(originFilePath, destFilePath, ec);
3729 11 : if (ec) {
3730 0 : JAMI_ERROR("An error occurred while moving the {} origin file path to the {} destination "
3731 : "file path. Abort resolving vote.",
3732 : originFilePath,
3733 : destFilePath);
3734 0 : return false;
3735 : }
3736 :
3737 : // If members, remove related devices and mark as banned
3738 11 : if (type != "devices") {
3739 11 : std::error_code ec;
3740 58 : for (const auto& certificate : std::filesystem::directory_iterator(devicesPath, ec)) {
3741 25 : auto certPath = certificate.path();
3742 : try {
3743 50 : crypto::Certificate cert(fileutils::loadFile(certPath));
3744 25 : if (auto issuer = cert.issuer)
3745 0 : if (issuer->getPublicKey().getId().to_view() == uri)
3746 25 : dhtnet::fileutils::remove(certPath, true);
3747 25 : } catch (...) {
3748 0 : continue;
3749 0 : }
3750 36 : }
3751 11 : std::lock_guard lk(membersMtx_);
3752 11 : auto updated = false;
3753 :
3754 26 : for (auto& member : members_) {
3755 26 : if (member.uri == uri) {
3756 11 : updated = true;
3757 11 : member.role = MemberRole::BANNED;
3758 11 : break;
3759 : }
3760 : }
3761 11 : if (!updated)
3762 0 : members_.emplace_back(ConversationMember {uri, MemberRole::BANNED});
3763 11 : saveMembers();
3764 11 : }
3765 11 : return true;
3766 11 : }
3767 :
3768 : bool
3769 1 : ConversationRepository::Impl::resolveUnban(const std::string_view type, const std::string& uri)
3770 : {
3771 1 : auto repo = repository();
3772 1 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3773 1 : auto bannedPath = repoPath / "banned";
3774 1 : auto crtStr = uri + (type != "invited" ? ".crt" : "");
3775 2 : auto originFilePath = bannedPath / type / crtStr;
3776 1 : auto destPath = repoPath / type;
3777 1 : auto destFilePath = destPath / crtStr;
3778 1 : if (!dhtnet::fileutils::recursive_mkdir(destPath, 0700)) {
3779 0 : JAMI_ERROR("An error occurred while creating the {} destination path. Abort resolving vote.", destPath);
3780 0 : return false;
3781 : }
3782 1 : std::error_code ec;
3783 1 : std::filesystem::rename(originFilePath, destFilePath, ec);
3784 1 : if (ec) {
3785 0 : JAMI_ERROR("Error when moving {} to {}. Abort resolving vote.", originFilePath, destFilePath);
3786 0 : return false;
3787 : }
3788 :
3789 1 : std::lock_guard lk(membersMtx_);
3790 1 : auto updated = false;
3791 :
3792 1 : auto role = MemberRole::MEMBER;
3793 1 : if (type == "invited")
3794 0 : role = MemberRole::INVITED;
3795 1 : else if (type == "admins")
3796 0 : role = MemberRole::ADMIN;
3797 :
3798 2 : for (auto& member : members_) {
3799 2 : if (member.uri == uri) {
3800 1 : updated = true;
3801 1 : member.role = role;
3802 1 : break;
3803 : }
3804 : }
3805 1 : if (!updated)
3806 0 : members_.emplace_back(ConversationMember {uri, role});
3807 1 : saveMembers();
3808 1 : return true;
3809 1 : }
3810 :
3811 : std::string
3812 12 : ConversationRepository::resolveVote(const std::string& uri,
3813 : const std::string_view type,
3814 : const std::string& voteType)
3815 : {
3816 12 : std::lock_guard lkOp(pimpl_->opMtx_);
3817 12 : pimpl_->resetHard();
3818 : // Count ratio admin/votes
3819 12 : auto nbAdmins = 0, nbVotes = 0;
3820 : // For each admin, check if voted
3821 12 : auto repo = pimpl_->repository();
3822 12 : if (!repo)
3823 0 : return {};
3824 12 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
3825 12 : auto adminsPath = repoPath / "admins";
3826 24 : auto voteDirectory = repoPath / "votes" / voteType / type / uri;
3827 24 : for (const auto& certificate : dhtnet::fileutils::readDirectory(adminsPath)) {
3828 12 : if (certificate.find(".crt") == std::string::npos) {
3829 0 : JAMI_WARNING("Incorrect file found: {}/{}", adminsPath, certificate);
3830 0 : continue;
3831 0 : }
3832 24 : auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
3833 12 : nbAdmins += 1;
3834 12 : if (std::filesystem::is_regular_file(fileutils::getFullPath(voteDirectory, adminUri)))
3835 12 : nbVotes += 1;
3836 24 : }
3837 :
3838 12 : if (nbAdmins > 0 && (static_cast<double>(nbVotes) / static_cast<double>(nbAdmins)) > .5) {
3839 36 : JAMI_WARNING("More than half of the admins voted to ban {}, applying the ban.", uri);
3840 :
3841 : // Remove vote directory
3842 12 : dhtnet::fileutils::removeAll(voteDirectory, true);
3843 :
3844 12 : if (voteType == "ban") {
3845 11 : if (!pimpl_->resolveBan(type, uri))
3846 0 : return {};
3847 1 : } else if (voteType == "unban") {
3848 1 : if (!pimpl_->resolveUnban(type, uri))
3849 0 : return {};
3850 : }
3851 :
3852 : // Commit
3853 12 : if (!git_add_all(repo.get()))
3854 0 : return {};
3855 :
3856 12 : Json::Value json;
3857 12 : json["action"] = voteType;
3858 12 : json["uri"] = uri;
3859 12 : json["type"] = "member";
3860 24 : return pimpl_->commitMessage(json::toString(json));
3861 12 : }
3862 :
3863 : // If vote nok
3864 0 : return {};
3865 12 : }
3866 :
3867 : std::pair<std::vector<ConversationCommit>, bool>
3868 1611 : ConversationRepository::validFetch(const std::string& remoteDevice) const
3869 : {
3870 3222 : auto newCommit = remoteHead(remoteDevice);
3871 1611 : if (not pimpl_ or newCommit.empty())
3872 0 : return {{}, false};
3873 1611 : auto commitsToValidate = pimpl_->behind(newCommit);
3874 1611 : std::reverse(std::begin(commitsToValidate), std::end(commitsToValidate));
3875 1611 : auto isValid = pimpl_->validCommits(commitsToValidate);
3876 1611 : if (isValid)
3877 1602 : return {commitsToValidate, false};
3878 9 : return {{}, true};
3879 1611 : }
3880 :
3881 : bool
3882 148 : ConversationRepository::validClone(
3883 : std::function<void(std::vector<ConversationCommit>)>&& checkCommitCb) const
3884 : {
3885 148 : auto commits = log({});
3886 148 : auto res = pimpl_->validCommits(commits);
3887 148 : if (!res)
3888 1 : return false;
3889 147 : if (checkCommitCb)
3890 147 : checkCommitCb(std::move(commits));
3891 147 : return true;
3892 148 : }
3893 :
3894 : bool
3895 2 : ConversationRepository::isValidUserAtCommit(const std::string& userDevice,
3896 : const std::string& commitId,
3897 : const git_buf& sig,
3898 : const git_buf& sig_data) const
3899 : {
3900 2 : return pimpl_->isValidUserAtCommit(userDevice, commitId, sig, sig_data);
3901 : }
3902 :
3903 : bool
3904 1 : ConversationRepository::validCommits(const std::vector<ConversationCommit>& commitsToValidate) const
3905 : {
3906 1 : return pimpl_->validCommits(commitsToValidate);
3907 : }
3908 :
3909 : void
3910 759 : ConversationRepository::removeBranchWith(const std::string& remoteDevice)
3911 : {
3912 759 : git_remote* remote_ptr = nullptr;
3913 759 : auto repo = pimpl_->repository();
3914 759 : if (!repo || git_remote_lookup(&remote_ptr, repo.get(), remoteDevice.c_str()) < 0) {
3915 0 : JAMI_WARNING("No remote found with id: {}", remoteDevice);
3916 0 : return;
3917 : }
3918 759 : GitRemote remote {remote_ptr, git_remote_free};
3919 :
3920 759 : git_remote_prune(remote.get(), nullptr);
3921 759 : }
3922 :
3923 : std::vector<std::string>
3924 25 : ConversationRepository::getInitialMembers() const
3925 : {
3926 25 : return pimpl_->getInitialMembers();
3927 : }
3928 :
3929 : std::vector<ConversationMember>
3930 1234 : ConversationRepository::members() const
3931 : {
3932 1234 : return pimpl_->members();
3933 : }
3934 :
3935 : std::set<std::string>
3936 2645 : ConversationRepository::memberUris(std::string_view filter,
3937 : const std::set<MemberRole>& filteredRoles) const
3938 : {
3939 2645 : return pimpl_->memberUris(filter, filteredRoles);
3940 : }
3941 :
3942 : std::map<std::string, std::vector<DeviceId>>
3943 430 : ConversationRepository::devices(bool ignoreExpired) const
3944 : {
3945 430 : return pimpl_->devices(ignoreExpired);
3946 : }
3947 :
3948 : void
3949 738 : ConversationRepository::refreshMembers() const
3950 : {
3951 : try {
3952 738 : pimpl_->initMembers();
3953 0 : } catch (...) {
3954 0 : }
3955 738 : }
3956 :
3957 : void
3958 148 : ConversationRepository::pinCertificates(bool blocking)
3959 : {
3960 148 : auto acc = pimpl_->account_.lock();
3961 148 : auto repo = pimpl_->repository();
3962 148 : if (!repo or !acc)
3963 0 : return;
3964 :
3965 148 : std::string repoPath = git_repository_workdir(repo.get());
3966 : std::vector<std::string> paths = {repoPath + "admins",
3967 : repoPath + "members",
3968 740 : repoPath + "devices"};
3969 :
3970 592 : for (const auto& path : paths) {
3971 444 : if (blocking) {
3972 444 : std::promise<bool> p;
3973 444 : std::future<bool> f = p.get_future();
3974 888 : acc->certStore().pinCertificatePath(path, [&](auto /* certs */) { p.set_value(true); });
3975 444 : f.wait();
3976 444 : } else {
3977 0 : acc->certStore().pinCertificatePath(path, {});
3978 : }
3979 : }
3980 148 : }
3981 :
3982 : std::string
3983 13473 : ConversationRepository::uriFromDevice(const std::string& deviceId) const
3984 : {
3985 13473 : return pimpl_->uriFromDevice(deviceId);
3986 : }
3987 :
3988 : std::string
3989 4 : ConversationRepository::updateInfos(const std::map<std::string, std::string>& profile)
3990 : {
3991 4 : std::lock_guard lkOp(pimpl_->opMtx_);
3992 4 : pimpl_->resetHard();
3993 4 : auto valid = false;
3994 : {
3995 4 : std::lock_guard lk(pimpl_->membersMtx_);
3996 4 : for (const auto& member : pimpl_->members_) {
3997 4 : if (member.uri == pimpl_->userId_) {
3998 4 : valid = member.role <= pimpl_->updateProfilePermLvl_;
3999 4 : break;
4000 : }
4001 : }
4002 4 : }
4003 4 : if (!valid) {
4004 0 : JAMI_ERROR("Insufficient permission to update information.");
4005 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
4006 0 : pimpl_->accountId_,
4007 0 : pimpl_->id_,
4008 : EUNAUTHORIZED,
4009 : "Insufficient permission to update information.");
4010 0 : return {};
4011 : }
4012 :
4013 4 : auto infosMap = infos();
4014 11 : for (const auto& [k, v] : profile) {
4015 7 : infosMap[k] = v;
4016 : }
4017 4 : auto repo = pimpl_->repository();
4018 4 : if (!repo)
4019 0 : return {};
4020 4 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
4021 4 : auto profilePath = repoPath / "profile.vcf";
4022 4 : std::ofstream file(profilePath, std::ios::trunc | std::ios::binary);
4023 4 : if (!file.is_open()) {
4024 0 : JAMI_ERROR("Unable to write data to {}", profilePath);
4025 0 : return {};
4026 : }
4027 :
4028 16 : auto addKey = [&](auto property, auto key) {
4029 16 : auto it = infosMap.find(std::string(key));
4030 16 : if (it != infosMap.end()) {
4031 7 : file << property;
4032 7 : file << ":";
4033 7 : file << it->second;
4034 7 : file << vCard::Delimiter::END_LINE_TOKEN;
4035 : }
4036 16 : };
4037 :
4038 4 : file << vCard::Delimiter::BEGIN_TOKEN;
4039 4 : file << vCard::Delimiter::END_LINE_TOKEN;
4040 4 : file << vCard::Property::VCARD_VERSION;
4041 4 : file << ":2.1";
4042 4 : file << vCard::Delimiter::END_LINE_TOKEN;
4043 4 : addKey(vCard::Property::FORMATTED_NAME, vCard::Value::TITLE);
4044 4 : addKey(vCard::Property::DESCRIPTION, vCard::Value::DESCRIPTION);
4045 4 : file << vCard::Property::PHOTO;
4046 4 : file << vCard::Delimiter::SEPARATOR_TOKEN;
4047 4 : file << vCard::Property::BASE64;
4048 4 : auto avatarIt = infosMap.find(std::string(vCard::Value::AVATAR));
4049 4 : if (avatarIt != infosMap.end()) {
4050 : // TODO: type=png? store another way?
4051 0 : file << ":";
4052 0 : file << avatarIt->second;
4053 : }
4054 4 : file << vCard::Delimiter::END_LINE_TOKEN;
4055 4 : addKey(vCard::Property::RDV_ACCOUNT, vCard::Value::RDV_ACCOUNT);
4056 4 : file << vCard::Delimiter::END_LINE_TOKEN;
4057 4 : addKey(vCard::Property::RDV_DEVICE, vCard::Value::RDV_DEVICE);
4058 4 : file << vCard::Delimiter::END_LINE_TOKEN;
4059 4 : file << vCard::Delimiter::END_TOKEN;
4060 4 : file.close();
4061 :
4062 4 : if (!pimpl_->add("profile.vcf"))
4063 0 : return {};
4064 4 : Json::Value json;
4065 4 : json["type"] = "application/update-profile";
4066 8 : return pimpl_->commitMessage(json::toString(json));
4067 4 : }
4068 :
4069 : std::map<std::string, std::string>
4070 265 : ConversationRepository::infos() const
4071 : {
4072 265 : if (auto repo = pimpl_->repository()) {
4073 : try {
4074 265 : std::filesystem::path repoPath = git_repository_workdir(repo.get());
4075 265 : auto profilePath = repoPath / "profile.vcf";
4076 265 : std::map<std::string, std::string> result;
4077 265 : std::error_code ec;
4078 265 : if (std::filesystem::is_regular_file(profilePath, ec)) {
4079 14 : auto content = fileutils::loadFile(profilePath);
4080 28 : result = ConversationRepository::infosFromVCard(vCard::utils::toMap(
4081 28 : std::string_view {(const char*) content.data(), content.size()}));
4082 14 : }
4083 265 : result["mode"] = std::to_string(static_cast<int>(mode()));
4084 265 : return result;
4085 265 : } catch (...) {
4086 0 : }
4087 265 : }
4088 0 : return {};
4089 : }
4090 :
4091 : std::map<std::string, std::string>
4092 104 : ConversationRepository::infosFromVCard(std::map<std::string, std::string>&& details)
4093 : {
4094 104 : std::map<std::string, std::string> result;
4095 171 : for (auto&& [k, v] : details) {
4096 67 : if (k == vCard::Property::FORMATTED_NAME) {
4097 3 : result["title"] = std::move(v);
4098 64 : } else if (k == vCard::Property::DESCRIPTION) {
4099 0 : result["description"] = std::move(v);
4100 64 : } else if (k.find(vCard::Property::PHOTO) == 0) {
4101 0 : result["avatar"] = std::move(v);
4102 64 : } else if (k.find(vCard::Property::RDV_ACCOUNT) == 0) {
4103 11 : result["rdvAccount"] = std::move(v);
4104 53 : } else if (k.find(vCard::Property::RDV_DEVICE) == 0) {
4105 11 : result["rdvDevice"] = std::move(v);
4106 : }
4107 : }
4108 104 : return result;
4109 0 : }
4110 :
4111 : std::string
4112 1611 : ConversationRepository::getHead() const
4113 : {
4114 1611 : if (auto repo = pimpl_->repository()) {
4115 : git_oid commit_id;
4116 1611 : if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
4117 0 : JAMI_ERROR("Unable to get reference for HEAD");
4118 1611 : return {};
4119 : }
4120 1611 : if (auto commit_str = git_oid_tostr_s(&commit_id))
4121 1611 : return commit_str;
4122 1611 : }
4123 0 : return {};
4124 : }
4125 :
4126 : std::optional<std::map<std::string, std::string>>
4127 16836 : ConversationRepository::convCommitToMap(const ConversationCommit& commit) const
4128 : {
4129 16836 : return pimpl_->convCommitToMap(commit);
4130 : }
4131 :
4132 : std::vector<std::map<std::string, std::string>>
4133 1228 : ConversationRepository::convCommitsToMap(const std::vector<ConversationCommit>& commits) const
4134 : {
4135 1228 : std::vector<std::map<std::string, std::string>> result = {};
4136 1228 : result.reserve(commits.size());
4137 3257 : for (const auto& commit : commits) {
4138 2029 : auto message = pimpl_->convCommitToMap(commit);
4139 2029 : if (message == std::nullopt)
4140 0 : continue;
4141 2029 : result.emplace_back(*message);
4142 2029 : }
4143 1228 : return result;
4144 0 : }
4145 :
4146 : } // namespace jami
|