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