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