Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : #ifdef HAVE_CONFIG_H
19 : #include "config.h"
20 : #endif
21 :
22 : #include "fileutils.h"
23 : #include "logger.h"
24 : #include "archiver.h"
25 : #include "compiler_intrinsics.h"
26 : #include "base64.h"
27 : #include "string_utils.h"
28 :
29 : #include <opendht/crypto.h>
30 :
31 : #ifdef __APPLE__
32 : #include <TargetConditionals.h>
33 : #endif
34 :
35 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
36 : #include "client/jami_signal.h"
37 : #endif
38 :
39 : #ifdef _WIN32
40 : #include <windows.h>
41 : #include "string_utils.h"
42 : #endif
43 :
44 : #include <sys/types.h>
45 : #include <sys/stat.h>
46 :
47 : #ifndef _MSC_VER
48 : #include <libgen.h>
49 : #endif
50 :
51 : #ifdef _MSC_VER
52 : #include "windirent.h"
53 : #else
54 : #include <dirent.h>
55 : #endif
56 :
57 : #include <signal.h>
58 : #include <unistd.h>
59 : #include <fcntl.h>
60 : #ifndef _WIN32
61 : #include <pwd.h>
62 : #else
63 : #include <shlobj.h>
64 : #define NAME_MAX 255
65 : #endif
66 : #if !defined __ANDROID__ && !defined _WIN32
67 : #include <wordexp.h>
68 : #endif
69 :
70 : #include <nettle/sha3.h>
71 : #include <nettle/version.h>
72 :
73 : #include <sstream>
74 : #include <fstream>
75 : #include <iostream>
76 : #include <stdexcept>
77 : #include <limits>
78 : #include <array>
79 :
80 : #include <cstdlib>
81 : #include <cstring>
82 : #include <cerrno>
83 : #include <cstddef>
84 :
85 : #include <pj/ctype.h>
86 : #include <pjlib-util/md5.h>
87 :
88 : #ifndef _MSC_VER
89 : #define PROTECTED_GETENV(str) \
90 : ({ \
91 : char* envvar_ = getenv((str)); \
92 : envvar_ ? envvar_ : ""; \
93 : })
94 :
95 : #define XDG_DATA_HOME (PROTECTED_GETENV("XDG_DATA_HOME"))
96 : #define XDG_CONFIG_HOME (PROTECTED_GETENV("XDG_CONFIG_HOME"))
97 : #define XDG_CACHE_HOME (PROTECTED_GETENV("XDG_CACHE_HOME"))
98 : #else
99 : const wchar_t*
100 : winGetEnv(const wchar_t* name)
101 : {
102 : const DWORD buffSize = 65535;
103 : static wchar_t buffer[buffSize];
104 : if (GetEnvironmentVariable(name, buffer, buffSize)) {
105 : return buffer;
106 : } else {
107 : return L"";
108 : }
109 : }
110 :
111 : #define PROTECTED_GETENV(str) winGetEnv(str)
112 :
113 : #define JAMI_DATA_HOME PROTECTED_GETENV(L"JAMI_DATA_HOME")
114 : #define JAMI_CONFIG_HOME PROTECTED_GETENV(L"JAMI_CONFIG_HOME")
115 : #define JAMI_CACHE_HOME PROTECTED_GETENV(L"JAMI_CACHE_HOME")
116 : #endif
117 :
118 : #define PIDFILE ".ring.pid"
119 : #define ERASE_BLOCK 4096
120 :
121 : namespace jami {
122 : namespace fileutils {
123 :
124 : static std::filesystem::path resource_dir_path;
125 :
126 : void
127 0 : set_resource_dir_path(const std::filesystem::path& resourceDirPath)
128 : {
129 0 : resource_dir_path = resourceDirPath;
130 0 : }
131 :
132 : const std::filesystem::path&
133 965 : get_resource_dir_path()
134 : {
135 965 : static const std::filesystem::path jami_default_data_dir(JAMI_DATADIR);
136 965 : return resource_dir_path.empty() ? jami_default_data_dir : resource_dir_path;
137 : }
138 :
139 : std::string
140 5 : expand_path(const std::string& path)
141 : {
142 : #if defined __ANDROID__ || defined _MSC_VER || defined WIN32 || defined __APPLE__
143 : JAMI_ERROR("Path expansion not implemented, returning original");
144 : return path;
145 : #else
146 :
147 5 : std::string result;
148 :
149 : wordexp_t p;
150 5 : int ret = wordexp(path.c_str(), &p, 0);
151 :
152 5 : switch (ret) {
153 0 : case WRDE_BADCHAR:
154 0 : JAMI_ERROR("Illegal occurrence of newline or one of |, &, ;, <, >, (, ), {{, }}.");
155 0 : return result;
156 0 : case WRDE_BADVAL:
157 0 : JAMI_ERROR("An undefined shell variable was referenced");
158 0 : return result;
159 0 : case WRDE_CMDSUB:
160 0 : JAMI_ERROR("Command substitution occurred");
161 0 : return result;
162 0 : case WRDE_SYNTAX:
163 0 : JAMI_ERROR("Shell syntax error");
164 0 : return result;
165 0 : case WRDE_NOSPACE:
166 0 : JAMI_ERROR("Out of memory.");
167 : // This is the only error where we must call wordfree
168 0 : break;
169 5 : default:
170 5 : if (p.we_wordc > 0)
171 10 : result = std::string(p.we_wordv[0]);
172 5 : break;
173 : }
174 :
175 5 : wordfree(&p);
176 :
177 5 : return result;
178 : #endif
179 0 : }
180 :
181 : bool
182 7 : isDirectoryWritable(const std::string& directory)
183 : {
184 7 : return accessFile(directory, W_OK) == 0;
185 : }
186 :
187 : bool
188 2 : createSymlink(const std::filesystem::path& linkFile, const std::filesystem::path& target)
189 : {
190 2 : std::error_code ec;
191 2 : std::filesystem::create_symlink(target, linkFile, ec);
192 2 : if (ec) {
193 0 : JAMI_WARNING("Unable to create soft link from {} to {}: {}", linkFile, target, ec.message());
194 0 : return false;
195 : } else {
196 2 : JAMI_LOG("Created soft link from {} to {}", linkFile, target);
197 : }
198 2 : return true;
199 : }
200 :
201 : bool
202 0 : createHardlink(const std::filesystem::path& linkFile, const std::filesystem::path& target)
203 : {
204 0 : std::error_code ec;
205 0 : std::filesystem::create_hard_link(target, linkFile, ec);
206 0 : if (ec) {
207 0 : JAMI_WARNING("Unable to create hard link from {} to {}: {}", linkFile, target, ec.message());
208 0 : return false;
209 : } else {
210 0 : JAMI_LOG("Created hard link from {} to {}", linkFile, target);
211 : }
212 0 : return true;
213 : }
214 :
215 : bool
216 2 : createFileLink(const std::filesystem::path& linkFile, const std::filesystem::path& target, bool hard)
217 : {
218 2 : if (linkFile == target)
219 0 : return true;
220 2 : std::error_code ec;
221 : // Use symlink_status() because exists() could return false for broken symlinks
222 2 : auto status = std::filesystem::symlink_status(linkFile, ec);
223 2 : if (status.type() != std::filesystem::file_type::not_found) {
224 2 : if (status.type() == std::filesystem::file_type::symlink
225 1 : && std::filesystem::read_symlink(linkFile, ec) == target) {
226 0 : JAMI_DEBUG("createFileLink: {} symlink already points to target {}", linkFile, target);
227 0 : return true;
228 : }
229 : // Remove any existing file or symlink before creating a new one, as create_symlink()
230 : // will fail with "File exists" error if the linkFile path already exists.
231 1 : if (status.type() == std::filesystem::file_type::regular
232 1 : || status.type() == std::filesystem::file_type::symlink) {
233 1 : std::filesystem::remove(linkFile, ec);
234 : }
235 : }
236 :
237 : // Try to create a hard link if requested; fall back to symlink on failure
238 2 : if (not hard or not createHardlink(linkFile, target))
239 2 : return createSymlink(linkFile, target);
240 0 : return true;
241 : }
242 :
243 : std::string_view
244 98 : getFileExtension(std::string_view filename)
245 : {
246 98 : std::string_view result;
247 98 : auto sep = filename.find_last_of('.');
248 98 : if (sep != std::string_view::npos && sep != filename.size() - 1)
249 13 : result = filename.substr(sep + 1);
250 194 : if (result.size() > MAX_EXTENSION_SIZE || result.find('/') != std::string_view::npos
251 194 : || result.find('\\') != std::string_view::npos)
252 2 : return {};
253 96 : return result;
254 : }
255 :
256 : bool
257 10877 : isPathRelative(const std::filesystem::path& path)
258 : {
259 10877 : return not path.empty() and path.is_relative();
260 : }
261 :
262 : std::string
263 12139 : getCleanPath(const std::string& base, const std::string& path)
264 : {
265 12139 : if (base.empty() or path.size() < base.size())
266 12132 : return path;
267 7 : auto base_sep = base + DIR_SEPARATOR_STR;
268 7 : if (path.compare(0, base_sep.size(), base_sep) == 0)
269 7 : return path.substr(base_sep.size());
270 : else
271 0 : return path;
272 7 : }
273 :
274 : std::filesystem::path
275 18458 : getFullPath(const std::filesystem::path& base, const std::filesystem::path& path)
276 : {
277 18458 : bool isRelative {not base.empty() and isPathRelative(path)};
278 18458 : return isRelative ? base / path : path;
279 : }
280 :
281 : std::vector<uint8_t>
282 9413 : loadFile(const std::filesystem::path& path, const std::filesystem::path& default_dir)
283 : {
284 9413 : return dhtnet::fileutils::loadFile(getFullPath(default_dir, path));
285 : }
286 :
287 : std::string
288 10 : loadTextFile(const std::filesystem::path& path, const std::filesystem::path& default_dir)
289 : {
290 10 : std::string buffer;
291 10 : auto fullPath = getFullPath(default_dir, path);
292 :
293 : // Open with explicit share mode to allow reading even if file is opened elsewhere
294 : #ifdef _WIN32
295 : std::ifstream file(fullPath, std::ios::in | std::ios::binary, _SH_DENYNO);
296 : #else
297 10 : std::ifstream file(fullPath);
298 : #endif
299 :
300 10 : if (!file)
301 3 : throw std::runtime_error("Unable to read file: " + path.string());
302 :
303 7 : file.seekg(0, std::ios::end);
304 7 : auto size = file.tellg();
305 7 : if (size > std::numeric_limits<unsigned>::max())
306 0 : throw std::runtime_error("File is too big: " + path.string());
307 7 : buffer.resize(size);
308 7 : file.seekg(0, std::ios::beg);
309 7 : if (!file.read((char*) buffer.data(), size))
310 0 : throw std::runtime_error("Unable to load file: " + path.string());
311 14 : return buffer;
312 16 : }
313 :
314 : void
315 1867 : saveFile(const std::filesystem::path& path, const uint8_t* data, size_t data_size, mode_t UNUSED mode)
316 : {
317 1867 : std::ofstream file(path, std::ios::trunc | std::ios::binary);
318 1867 : if (!file.is_open()) {
319 0 : JAMI_ERROR("Unable to write data to {}", path);
320 0 : return;
321 : }
322 1867 : file.write((char*) data, data_size);
323 : #ifndef _WIN32
324 1867 : file.close();
325 1867 : if (chmod(path.c_str(), mode) < 0)
326 0 : JAMI_WARNING("fileutils::saveFile(): chmod() failed on {}, {}", path, strerror(errno));
327 : #endif
328 1867 : }
329 :
330 : std::vector<uint8_t>
331 0 : loadCacheFile(const std::filesystem::path& path, std::chrono::system_clock::duration maxAge)
332 : {
333 : // last_write_time throws exception if file doesn't exist
334 0 : std::error_code ec;
335 0 : auto writeTime = std::filesystem::last_write_time(path, ec);
336 0 : if (ec)
337 0 : throw std::runtime_error("unable to get last write time of file");
338 0 : auto now = decltype(writeTime)::clock::now();
339 0 : if (now - writeTime > maxAge)
340 0 : throw std::runtime_error("file too old " + dht::print_time_relative(now, writeTime));
341 :
342 0 : JAMI_LOG("Loading cache file '{}'", path);
343 0 : return dhtnet::fileutils::loadFile(path);
344 : }
345 :
346 : std::string
347 0 : loadCacheTextFile(const std::filesystem::path& path, std::chrono::system_clock::duration maxAge)
348 : {
349 : // last_write_time throws exception if file doesn't exist
350 0 : std::error_code ec;
351 0 : auto writeTime = std::filesystem::last_write_time(path, ec);
352 0 : if (ec)
353 0 : throw std::runtime_error("unable to get last write time of file");
354 0 : auto now = decltype(writeTime)::clock::now();
355 0 : if (now - writeTime > maxAge)
356 0 : throw std::runtime_error("file too old " + dht::print_time_relative(now, writeTime));
357 :
358 0 : JAMI_LOG("Loading cache file '{}'", path);
359 0 : return loadTextFile(path);
360 : }
361 :
362 : ArchiveStorageData
363 110 : readArchive(const std::filesystem::path& path, std::string_view scheme, const std::string& pwd)
364 : {
365 110 : JAMI_LOG("Reading archive from {} with scheme '{}'", path, scheme);
366 :
367 206 : auto isUnencryptedGzip = [](const std::vector<uint8_t>& data) {
368 : // NOTE: some webserver modify gzip files and this can end with a gunzip in a gunzip
369 : // file. So, to make the readArchive more robust, we can support this case by detecting
370 : // gzip header via 1f8b 08
371 : // We don't need to support more than 2 level, else somebody may be able to send
372 : // gunzip in loops and abuse.
373 206 : return data.size() > 3 && data[0] == 0x1f && data[1] == 0x8b && data[2] == 0x08;
374 : };
375 :
376 108 : auto decompress = [](std::vector<uint8_t>& data) {
377 : try {
378 108 : data = archiver::decompress(data);
379 0 : } catch (const std::exception& e) {
380 0 : JAMI_ERROR("Error decrypting archive: {}", e.what());
381 0 : throw e;
382 0 : }
383 108 : };
384 :
385 110 : std::vector<uint8_t> fileContent;
386 :
387 : // Read file
388 : try {
389 110 : fileContent = dhtnet::fileutils::loadFile(path);
390 0 : } catch (const std::exception& e) {
391 0 : JAMI_ERROR("Error loading archive: {}", e.what());
392 0 : throw;
393 0 : }
394 :
395 110 : if (isUnencryptedGzip(fileContent)) {
396 97 : if (!pwd.empty())
397 2 : JAMI_WARNING("A gunzip in a gunzip is detected. A webserver may have a bad config");
398 97 : decompress(fileContent);
399 : }
400 :
401 110 : ArchiveStorageData ret;
402 : // ret.data = {fileContent.data(), fileContent.data()+fileContent.size()};
403 :
404 110 : if (!pwd.empty()) {
405 : // Decrypt
406 14 : if (scheme == ARCHIVE_AUTH_SCHEME_KEY) {
407 : try {
408 0 : ret.salt = dht::crypto::aesGetSalt(fileContent);
409 0 : fileContent = dht::crypto::aesDecrypt(dht::crypto::aesGetEncrypted(fileContent), base64::decode(pwd));
410 0 : } catch (const std::exception& e) {
411 0 : JAMI_ERROR("Error decrypting archive: {}", e.what());
412 0 : throw;
413 0 : }
414 14 : } else if (scheme == ARCHIVE_AUTH_SCHEME_PASSWORD) {
415 : try {
416 14 : ret.salt = dht::crypto::aesGetSalt(fileContent);
417 14 : fileContent = dht::crypto::aesDecrypt(fileContent, pwd);
418 4 : } catch (const std::exception& e) {
419 4 : JAMI_ERROR("Error decrypting archive: {}", e.what());
420 4 : throw;
421 4 : }
422 : }
423 10 : decompress(fileContent);
424 96 : } else if (isUnencryptedGzip(fileContent)) {
425 1 : JAMI_WARNING("A gunzip in a gunzip is detected. A webserver may have a bad config");
426 1 : decompress(fileContent);
427 : }
428 212 : ret.data = {fileContent.data(), fileContent.data() + fileContent.size()};
429 212 : return ret;
430 114 : }
431 :
432 : bool
433 970 : writeArchive(const std::string& archive_str,
434 : const std::filesystem::path& path,
435 : std::string_view scheme,
436 : const std::string& password,
437 : const std::vector<uint8_t>& password_salt)
438 : {
439 970 : JAMI_LOG("Writing archive to {} using scheme '{}'", path, scheme);
440 :
441 970 : if (scheme == ARCHIVE_AUTH_SCHEME_KEY) {
442 : // Encrypt using provided key
443 : try {
444 0 : auto key = base64::decode(password);
445 0 : auto newArchive = dht::crypto::aesEncrypt(archiver::compress(archive_str), key);
446 0 : saveFile(path, dht::crypto::aesBuildEncrypted(newArchive, password_salt));
447 0 : } catch (const std::runtime_error& ex) {
448 0 : JAMI_ERROR("Export failed: {}", ex.what());
449 0 : return false;
450 0 : }
451 970 : } else if (scheme == ARCHIVE_AUTH_SCHEME_PASSWORD and not password.empty()) {
452 : // Encrypt using provided password
453 : try {
454 18 : saveFile(path, dht::crypto::aesEncrypt(archiver::compress(archive_str), password, password_salt));
455 0 : } catch (const std::runtime_error& ex) {
456 0 : JAMI_ERROR("Export failed: {}", ex.what());
457 0 : return false;
458 0 : }
459 952 : } else if (scheme == ARCHIVE_AUTH_SCHEME_NONE || (scheme == ARCHIVE_AUTH_SCHEME_PASSWORD && password.empty())) {
460 952 : JAMI_WARNING("Unsecured archiving (no password)");
461 952 : archiver::compressGzip(archive_str, path.string());
462 : } else {
463 0 : JAMI_ERROR("Unsupported scheme: {}", scheme);
464 0 : return false;
465 : }
466 970 : return true;
467 : }
468 :
469 : std::filesystem::path
470 80 : get_cache_dir([[maybe_unused]] const char* pkg)
471 : {
472 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
473 : std::vector<std::string> paths;
474 : paths.reserve(1);
475 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("cache", &paths);
476 : if (not paths.empty())
477 : return paths[0];
478 : return {};
479 : #elif defined(__APPLE__)
480 : return get_home_dir() / "Library" / "Caches" / pkg;
481 : #else
482 : #ifdef _WIN32
483 : const std::wstring cache_home(JAMI_CACHE_HOME);
484 : if (not cache_home.empty())
485 : return jami::to_string(cache_home);
486 : #else
487 80 : const std::string cache_home(XDG_CACHE_HOME);
488 80 : if (not cache_home.empty())
489 0 : return cache_home;
490 : #endif
491 160 : return get_home_dir() / ".cache" / pkg;
492 : #endif
493 80 : }
494 :
495 : const std::filesystem::path&
496 3900 : get_cache_dir()
497 : {
498 3900 : static const std::filesystem::path cache_dir = get_cache_dir(PACKAGE);
499 3900 : return cache_dir;
500 : }
501 :
502 : std::filesystem::path
503 42 : get_home_dir_impl()
504 : {
505 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
506 : std::vector<std::string> paths;
507 : paths.reserve(1);
508 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("files", &paths);
509 : if (not paths.empty())
510 : return paths[0];
511 : return {};
512 : #elif defined _WIN32
513 : TCHAR path[MAX_PATH];
514 : if (SUCCEEDED(SHGetFolderPath(nullptr, CSIDL_PROFILE, nullptr, 0, path))) {
515 : return jami::to_string(path);
516 : }
517 : return {};
518 : #else
519 :
520 : // 1) try getting user's home directory from the environment
521 42 : std::string home(PROTECTED_GETENV("HOME"));
522 42 : if (not home.empty())
523 42 : return home;
524 :
525 : // 2) try getting it from getpwuid_r (i.e. /etc/passwd)
526 0 : const long max = sysconf(_SC_GETPW_R_SIZE_MAX);
527 0 : if (max != -1) {
528 0 : char buf[max];
529 : struct passwd pwbuf, *pw;
530 0 : if (getpwuid_r(getuid(), &pwbuf, buf, sizeof(buf), &pw) == 0 and pw != NULL)
531 0 : return pw->pw_dir;
532 0 : }
533 :
534 0 : return {};
535 : #endif
536 42 : }
537 :
538 : const std::filesystem::path&
539 244 : get_home_dir()
540 : {
541 244 : static const std::filesystem::path home_dir = get_home_dir_impl();
542 244 : return home_dir;
543 : }
544 :
545 : std::filesystem::path
546 81 : get_data_dir([[maybe_unused]] const char* pkg)
547 : {
548 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
549 : std::vector<std::string> paths;
550 : paths.reserve(1);
551 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("files", &paths);
552 : if (not paths.empty())
553 : return paths[0];
554 : return {};
555 : #elif defined(__APPLE__)
556 : return get_home_dir() / "Library" / "Application Support" / pkg;
557 : #elif defined(_WIN32)
558 : std::wstring data_home(JAMI_DATA_HOME);
559 : if (not data_home.empty())
560 : return std::filesystem::path(data_home) / pkg;
561 :
562 : if (!strcmp(pkg, "ring")) {
563 : return get_home_dir() / ".local" / "share" / pkg;
564 : } else {
565 : return get_home_dir() / "AppData" / "Local" / pkg;
566 : }
567 : #else
568 81 : std::string_view data_home(XDG_DATA_HOME);
569 81 : if (not data_home.empty())
570 0 : return std::filesystem::path(data_home) / pkg;
571 : // "If $XDG_DATA_HOME is either not set or empty, a default equal to
572 : // $HOME/.local/share should be used."
573 162 : return get_home_dir() / ".local" / "share" / pkg;
574 : #endif
575 : }
576 :
577 : const std::filesystem::path&
578 65351 : get_data_dir()
579 : {
580 65351 : static const std::filesystem::path data_dir = get_data_dir(PACKAGE);
581 65353 : return data_dir;
582 : }
583 :
584 : std::filesystem::path
585 81 : get_config_dir([[maybe_unused]] const char* pkg)
586 : {
587 81 : std::filesystem::path configdir;
588 : #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
589 : std::vector<std::string> paths;
590 : emitSignal<libjami::ConfigurationSignal::GetAppDataPath>("config", &paths);
591 : if (not paths.empty())
592 : configdir = std::filesystem::path(paths[0]);
593 : #elif defined(__APPLE__)
594 : configdir = fileutils::get_home_dir() / "Library" / "Application Support" / pkg;
595 : #elif defined(_WIN32)
596 : std::wstring xdg_env(JAMI_CONFIG_HOME);
597 : if (not xdg_env.empty()) {
598 : configdir = std::filesystem::path(xdg_env) / pkg;
599 : } else if (!strcmp(pkg, "ring")) {
600 : configdir = fileutils::get_home_dir() / ".config" / pkg;
601 : } else {
602 : configdir = fileutils::get_home_dir() / "AppData" / "Local" / pkg;
603 : }
604 : #else
605 81 : std::string xdg_env(XDG_CONFIG_HOME);
606 81 : if (not xdg_env.empty())
607 0 : configdir = std::filesystem::path(xdg_env) / pkg;
608 : else
609 81 : configdir = fileutils::get_home_dir() / ".config" / pkg;
610 : #endif
611 81 : if (!dhtnet::fileutils::recursive_mkdir(configdir, 0700)) {
612 : // If directory creation failed
613 0 : if (errno != EEXIST)
614 0 : JAMI_LOG("Unable to create directory: {}!", configdir);
615 : }
616 162 : return configdir;
617 81 : }
618 :
619 : const std::filesystem::path&
620 297 : get_config_dir()
621 : {
622 297 : static const std::filesystem::path config_dir = get_config_dir(PACKAGE);
623 297 : return config_dir;
624 : }
625 :
626 : #ifdef _WIN32
627 : bool
628 : eraseFile_win32(const std::string& path, bool dosync)
629 : {
630 : // Note: from
631 : // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-deletefilea#remarks To
632 : // delete a read-only file, first you must remove the read-only attribute.
633 : SetFileAttributesA(path.c_str(), GetFileAttributesA(path.c_str()) & ~FILE_ATTRIBUTE_READONLY);
634 : HANDLE h = CreateFileA(path.c_str(), GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
635 : if (h == INVALID_HANDLE_VALUE) {
636 : JAMI_WARNING("Unable to open file {} for erasing.", path);
637 : return false;
638 : }
639 :
640 : LARGE_INTEGER size;
641 : if (!GetFileSizeEx(h, &size)) {
642 : JAMI_WARNING("Unable to erase file {}: GetFileSizeEx() failed.", path);
643 : CloseHandle(h);
644 : return false;
645 : }
646 : if (size.QuadPart == 0) {
647 : CloseHandle(h);
648 : return false;
649 : }
650 :
651 : uint64_t size_blocks = size.QuadPart / ERASE_BLOCK;
652 : if (size.QuadPart % ERASE_BLOCK)
653 : size_blocks++;
654 :
655 : char* buffer;
656 : try {
657 : buffer = new char[ERASE_BLOCK];
658 : } catch (std::bad_alloc& ba) {
659 : JAMI_WARNING("Unable to allocate buffer for erasing {}.", path);
660 : CloseHandle(h);
661 : return false;
662 : }
663 : memset(buffer, 0x00, ERASE_BLOCK);
664 :
665 : OVERLAPPED ovlp;
666 : if (size.QuadPart < (1024 - 42)) { // a small file can be stored in the MFT record
667 : ovlp.Offset = 0;
668 : ovlp.OffsetHigh = 0;
669 : WriteFile(h, buffer, (DWORD) size.QuadPart, 0, &ovlp);
670 : FlushFileBuffers(h);
671 : }
672 : for (uint64_t i = 0; i < size_blocks; i++) {
673 : uint64_t offset = i * ERASE_BLOCK;
674 : ovlp.Offset = offset & 0x00000000FFFFFFFF;
675 : ovlp.OffsetHigh = offset >> 32;
676 : WriteFile(h, buffer, ERASE_BLOCK, 0, &ovlp);
677 : }
678 :
679 : delete[] buffer;
680 :
681 : if (dosync)
682 : FlushFileBuffers(h);
683 :
684 : CloseHandle(h);
685 : return true;
686 : }
687 :
688 : #else
689 :
690 : bool
691 0 : eraseFile_posix(const std::string& path, bool dosync)
692 : {
693 : struct stat st;
694 0 : if (stat(path.c_str(), &st) == -1) {
695 0 : JAMI_WARNING("Unable to erase file {}: fstat() failed.", path);
696 0 : return false;
697 : }
698 : // Remove read-only flag if possible
699 0 : chmod(path.c_str(), st.st_mode | (S_IWGRP + S_IWUSR));
700 :
701 0 : int fd = open(path.c_str(), O_WRONLY);
702 0 : if (fd == -1) {
703 0 : JAMI_WARNING("Unable to open file {} for erasing.", path);
704 0 : return false;
705 : }
706 :
707 0 : if (st.st_size == 0) {
708 0 : close(fd);
709 0 : return false;
710 : }
711 :
712 0 : lseek(fd, 0, SEEK_SET);
713 :
714 : std::array<char, ERASE_BLOCK> buffer;
715 0 : buffer.fill(0);
716 0 : decltype(st.st_size) written(0);
717 0 : while (written < st.st_size) {
718 0 : auto ret = write(fd, buffer.data(), buffer.size());
719 0 : if (ret < 0) {
720 0 : JAMI_WARNING("Error while overriding file with zeros.");
721 0 : break;
722 : } else
723 0 : written += ret;
724 : }
725 :
726 0 : if (dosync)
727 0 : fsync(fd);
728 :
729 0 : close(fd);
730 0 : return written >= st.st_size;
731 : }
732 : #endif
733 :
734 : bool
735 0 : eraseFile(const std::string& path, bool dosync)
736 : {
737 : #ifdef _WIN32
738 : return eraseFile_win32(path, dosync);
739 : #else
740 0 : return eraseFile_posix(path, dosync);
741 : #endif
742 : }
743 :
744 : int
745 0 : remove(const std::filesystem::path& path, bool erase)
746 : {
747 0 : if (erase and dhtnet::fileutils::isFile(path, false) and !dhtnet::fileutils::hasHardLink(path))
748 0 : eraseFile(path.string(), true);
749 :
750 : #ifdef _WIN32
751 : // use Win32 api since std::remove will not unlink directory in use
752 : if (std::filesystem::is_directory(path))
753 : return !RemoveDirectory(path.c_str());
754 : #endif
755 :
756 0 : return std::remove(path.string().c_str());
757 : }
758 :
759 : std::string
760 63 : sha3File(const std::filesystem::path& path)
761 : {
762 : sha3_512_ctx ctx;
763 63 : sha3_512_init(&ctx);
764 :
765 : try {
766 63 : if (not std::filesystem::is_regular_file(path)) {
767 2 : JAMI_ERROR("Unable to compute sha3sum of {}: not a regular file", path);
768 2 : return {};
769 : }
770 61 : std::ifstream file(path, std::ios::binary | std::ios::in);
771 61 : if (!file) {
772 0 : JAMI_ERROR("Unable to compute sha3sum of {}: failed to open file", path);
773 0 : return {};
774 : }
775 61 : constexpr size_t BUFFER_SIZE = 64 * 1024ul;
776 61 : std::vector<char> buffer(BUFFER_SIZE);
777 954 : while (file) {
778 895 : file.read(buffer.data(), BUFFER_SIZE);
779 895 : const auto bytesRead = file.gcount();
780 895 : if (bytesRead == 0)
781 2 : break;
782 893 : sha3_512_update(&ctx, static_cast<size_t>(bytesRead), (const uint8_t*) buffer.data());
783 : }
784 61 : } catch (const std::exception& e) {
785 0 : JAMI_ERROR("Unable to compute sha3sum of {}: {}", path, e.what());
786 0 : return {};
787 0 : }
788 :
789 : unsigned char digest[SHA3_512_DIGEST_SIZE];
790 : #if NETTLE_VERSION_MAJOR >= 4
791 : sha3_512_digest(&ctx, digest);
792 : #else
793 61 : sha3_512_digest(&ctx, SHA3_512_DIGEST_SIZE, digest);
794 : #endif
795 61 : return dht::toHex(digest, SHA3_512_DIGEST_SIZE);
796 : }
797 :
798 : std::string
799 6 : sha3sum(const std::vector<uint8_t>& buffer)
800 : {
801 : sha3_512_ctx ctx;
802 6 : sha3_512_init(&ctx);
803 6 : sha3_512_update(&ctx, buffer.size(), buffer.data());
804 : unsigned char digest[SHA3_512_DIGEST_SIZE];
805 : #if NETTLE_VERSION_MAJOR >= 4
806 : sha3_512_digest(&ctx, digest);
807 : #else
808 6 : sha3_512_digest(&ctx, SHA3_512_DIGEST_SIZE, digest);
809 : #endif
810 12 : return dht::toHex(digest, SHA3_512_DIGEST_SIZE);
811 : }
812 :
813 : int
814 7 : accessFile(const std::string& file, int mode)
815 : {
816 : #ifdef _WIN32
817 : return _waccess(jami::to_wstring(file).c_str(), mode);
818 : #else
819 7 : return access(file.c_str(), mode);
820 : #endif
821 : }
822 :
823 : uint64_t
824 18 : lastWriteTimeInSeconds(const std::filesystem::path& filePath)
825 : {
826 18 : std::error_code ec;
827 18 : auto lastWrite = std::filesystem::last_write_time(filePath, ec);
828 18 : if (ec) {
829 2 : JAMI_WARNING("Unable to get last write time of {}: {}", filePath, ec.message());
830 2 : return 0;
831 : }
832 16 : return std::chrono::duration_cast<std::chrono::seconds>(lastWrite.time_since_epoch()).count();
833 : }
834 :
835 : std::string
836 40 : getOrCreateLocalDeviceId()
837 : {
838 40 : const auto& localDir = get_data_dir(); // ~/.local/share/jami/
839 40 : auto fullIdPath = localDir / "local_device_id";
840 40 : std::string localDeviceId;
841 :
842 40 : if (std::filesystem::exists(fullIdPath)) {
843 : // Get the id inside the file
844 0 : std::ifstream inStream(fullIdPath);
845 0 : std::getline(inStream, localDeviceId);
846 0 : inStream.close();
847 0 : if (!localDeviceId.empty())
848 0 : return localDeviceId;
849 0 : }
850 :
851 : // Generate a random hex string
852 : {
853 40 : std::random_device randomDevice;
854 40 : localDeviceId = to_hex_string(std::uniform_int_distribution<uint64_t>()(randomDevice));
855 40 : }
856 :
857 : // Create a new file and write the id in it
858 40 : std::error_code ec;
859 40 : std::filesystem::create_directories(localDir, ec);
860 40 : std::ofstream outStream(fullIdPath);
861 40 : if (outStream) {
862 40 : outStream << localDeviceId << '\n';
863 40 : outStream.close();
864 : } else {
865 0 : JAMI_ERROR("Unable to create local device id file: {}", fullIdPath);
866 : }
867 40 : return localDeviceId;
868 40 : }
869 :
870 : } // namespace fileutils
871 : } // namespace jami
|