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 : #include "pluginsutils.h"
19 : #include "logger.h"
20 : #include "fileutils.h"
21 : #include "archiver.h"
22 : #include "string_utils.h"
23 :
24 : #include <msgpack.hpp>
25 :
26 : #include <fstream>
27 : #include <regex>
28 :
29 : #if defined(__APPLE__)
30 : #if (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
31 : #define ABI "iphone"
32 : #else
33 : #if defined(__x86_64__)
34 : #define ABI "x86_64-apple-Darwin"
35 : #else
36 : #define ABI "arm64-apple-Darwin"
37 : #endif
38 : #endif
39 : #elif defined(__arm__)
40 : #if defined(__ARM_ARCH_7A__)
41 : #define ABI "armeabi-v7a"
42 : #else
43 : #define ABI "armeabi"
44 : #endif
45 : #elif defined(__i386__)
46 : #if __ANDROID__
47 : #define ABI "x86"
48 : #else
49 : #define ABI "x86-linux-gnu"
50 : #endif
51 : #elif defined(__x86_64__)
52 : #if __ANDROID__
53 : #define ABI "x86_64"
54 : #else
55 : #define ABI "x86_64-linux-gnu"
56 : #endif
57 : #elif defined(__aarch64__)
58 : #define ABI "arm64-v8a"
59 : #elif defined(WIN32)
60 : #define ABI "x64-windows"
61 : #else
62 : #define ABI "unknown"
63 : #endif
64 :
65 : namespace jami {
66 : namespace PluginUtils {
67 :
68 : // DATA_REGEX is used to during the plugin jpl uncompressing
69 : const std::regex DATA_REGEX("^data" DIR_SEPARATOR_STR_ESC ".+");
70 : // SO_REGEX is used to find libraries during the plugin jpl uncompressing
71 : // lib/ABI/libplugin.SO
72 : static constexpr char SO_PATTERN[] = DIR_SEPARATOR_STR_ESC "(.*)" DIR_SEPARATOR_STR_ESC
73 : "([a-zA-Z0-9_]+.(dylib|so|dll|lib).*)";
74 : const std::regex SO_REGEX(SO_PATTERN);
75 :
76 : std::filesystem::path
77 25 : manifestPath(const std::filesystem::path& rootPath)
78 : {
79 25 : return rootPath / "manifest.json";
80 : }
81 :
82 : std::map<std::string, std::string>
83 0 : getPlatformInfo()
84 : {
85 0 : return {{"os", ABI}};
86 0 : }
87 :
88 : std::filesystem::path
89 10 : getRootPathFromSoPath(const std::filesystem::path& soPath)
90 : {
91 10 : return soPath.parent_path();
92 : }
93 :
94 : std::filesystem::path
95 8 : dataPath(const std::filesystem::path& pluginSoPath)
96 : {
97 16 : return getRootPathFromSoPath(pluginSoPath) / "data";
98 : }
99 :
100 : std::map<std::string, std::string>
101 52 : checkManifestJsonContentValidity(const Json::Value& root)
102 : {
103 52 : std::string name = root.get("name", "").asString();
104 52 : std::string id = root.get("id", name).asString();
105 52 : std::string description = root.get("description", "").asString();
106 52 : std::string version = root.get("version", "").asString();
107 52 : std::string iconPath = root.get("iconPath", "icon.png").asString();
108 52 : std::string background = root.get("backgroundPath", "background.jpg").asString();
109 52 : if (!name.empty() || !version.empty()) {
110 : return {
111 : {"id", id},
112 : {"name", name},
113 : {"description", description},
114 : {"version", version},
115 : {"iconPath", iconPath},
116 : {"backgroundPath", background},
117 468 : };
118 : } else {
119 0 : throw std::runtime_error("plugin manifest file: bad format");
120 : }
121 104 : }
122 :
123 : std::map<std::string, std::string>
124 0 : checkManifestValidity(std::istream& stream)
125 : {
126 0 : Json::Value root;
127 0 : Json::CharReaderBuilder rbuilder;
128 0 : rbuilder["collectComments"] = false;
129 0 : std::string errs;
130 :
131 0 : if (Json::parseFromStream(rbuilder, stream, &root, &errs)) {
132 0 : return checkManifestJsonContentValidity(root);
133 : } else {
134 0 : throw std::runtime_error("failed to parse the plugin manifest file");
135 : }
136 0 : }
137 :
138 : std::map<std::string, std::string>
139 52 : checkManifestValidity(const std::vector<uint8_t>& vec)
140 : {
141 52 : Json::Value root;
142 52 : std::unique_ptr<Json::CharReader> json_Reader(Json::CharReaderBuilder {}.newCharReader());
143 52 : std::string errs;
144 :
145 104 : bool ok = json_Reader->parse(reinterpret_cast<const char*>(vec.data()),
146 52 : reinterpret_cast<const char*>(vec.data() + vec.size()),
147 : &root,
148 : &errs);
149 :
150 52 : if (ok) {
151 104 : return checkManifestJsonContentValidity(root);
152 : } else {
153 0 : throw std::runtime_error("failed to parse the plugin manifest file");
154 : }
155 52 : }
156 :
157 : std::map<std::string, std::string>
158 25 : parseManifestFile(const std::filesystem::path& manifestFilePath, const std::string& rootPath)
159 : {
160 25 : std::lock_guard guard(dhtnet::fileutils::getFileLock(manifestFilePath));
161 25 : std::ifstream file(manifestFilePath);
162 25 : if (file) {
163 : try {
164 20 : const auto& traduction = parseManifestTranslation(rootPath, file);
165 20 : return checkManifestValidity(std::vector<uint8_t>(traduction.begin(), traduction.end()));
166 20 : } catch (const std::exception& e) {
167 0 : JAMI_ERROR("{}", e.what());
168 0 : }
169 : }
170 5 : return {};
171 25 : }
172 :
173 : std::string
174 20 : parseManifestTranslation(const std::string& rootPath, std::ifstream& manifestFile)
175 : {
176 20 : if (manifestFile) {
177 20 : std::stringstream buffer;
178 20 : buffer << manifestFile.rdbuf();
179 20 : std::string manifest = buffer.str();
180 20 : const auto& translation = getLocales(rootPath, getLanguage());
181 20 : std::regex pattern(R"(\{\{([^}]+)\}\})");
182 20 : std::smatch matches;
183 : // replace the pattern to the correct translation
184 60 : while (std::regex_search(manifest, matches, pattern)) {
185 20 : if (matches.size() == 2) {
186 20 : auto it = translation.find(matches[1].str());
187 20 : if (it == translation.end()) {
188 0 : manifest = std::regex_replace(manifest, pattern, "");
189 0 : continue;
190 : }
191 20 : manifest = std::regex_replace(manifest, pattern, it->second, std::regex_constants::format_first_only);
192 : }
193 : }
194 20 : return manifest;
195 20 : }
196 0 : return {};
197 : }
198 :
199 : bool
200 8 : checkPluginValidity(const std::filesystem::path& rootPath)
201 : {
202 8 : return !parseManifestFile(manifestPath(rootPath), rootPath.string()).empty();
203 : }
204 :
205 : std::map<std::string, std::string>
206 32 : readPluginManifestFromArchive(const std::string& jplPath)
207 : {
208 : try {
209 64 : return checkManifestValidity(archiver::readFileFromArchive(jplPath, "manifest.json"));
210 0 : } catch (const std::exception& e) {
211 0 : JAMI_ERROR("{}", e.what());
212 0 : }
213 0 : return {};
214 : }
215 :
216 : std::unique_ptr<dht::crypto::Certificate>
217 10 : readPluginCertificate(const std::string& rootPath, const std::string& pluginId)
218 : {
219 10 : std::string certPath = rootPath + DIR_SEPARATOR_CH + pluginId + ".crt";
220 : try {
221 10 : auto cert = fileutils::loadFile(certPath);
222 10 : return std::make_unique<dht::crypto::Certificate>(cert);
223 10 : } catch (const std::exception& e) {
224 0 : JAMI_ERROR("{}", e.what());
225 0 : }
226 0 : return {};
227 10 : }
228 :
229 : std::unique_ptr<dht::crypto::Certificate>
230 14 : readPluginCertificateFromArchive(const std::string& jplPath)
231 : {
232 : try {
233 14 : auto manifest = readPluginManifestFromArchive(jplPath);
234 14 : const std::string& name = manifest["id"];
235 :
236 14 : if (name.empty()) {
237 0 : return {};
238 : }
239 14 : return std::make_unique<dht::crypto::Certificate>(archiver::readFileFromArchive(jplPath, name + ".crt"));
240 14 : } catch (const std::exception& e) {
241 0 : JAMI_ERROR("{}", e.what());
242 0 : return {};
243 0 : }
244 : }
245 :
246 : std::map<std::string, std::vector<uint8_t>>
247 20 : readPluginSignatureFromArchive(const std::string& jplPath)
248 : {
249 : try {
250 21 : std::vector<uint8_t> vec = archiver::readFileFromArchive(jplPath, "signatures");
251 19 : msgpack::object_handle oh = msgpack::unpack(reinterpret_cast<const char*>(vec.data()),
252 38 : vec.size() * sizeof(uint8_t));
253 19 : msgpack::object obj = oh.get();
254 19 : return obj.as<std::map<std::string, std::vector<uint8_t>>>();
255 20 : } catch (const std::exception& e) {
256 4 : JAMI_ERROR("{}", e.what());
257 1 : return {};
258 1 : }
259 : }
260 :
261 : std::vector<uint8_t>
262 10 : readSignatureFileFromArchive(const std::string& jplPath)
263 : {
264 20 : return archiver::readFileFromArchive(jplPath, "signatures.sig");
265 : }
266 :
267 : std::pair<bool, std::string_view>
268 105 : uncompressJplFunction(std::string_view relativeFileName)
269 : {
270 105 : std::svmatch match;
271 : // manifest.json and files under data/ folder remains in the same structure
272 : // but libraries files are extracted from the folder that matches the running ABI to
273 : // the main installation path.
274 105 : if (std::regex_search(relativeFileName, match, SO_REGEX)) {
275 7 : if (std::svsub_match_view(match[1]) != ABI) {
276 0 : return std::make_pair(false, std::string_view {});
277 : } else {
278 7 : return std::make_pair(true, std::svsub_match_view(match[2]));
279 : }
280 : }
281 98 : return std::make_pair(true, relativeFileName);
282 105 : }
283 :
284 : std::string
285 133 : getLanguage()
286 : {
287 133 : std::string lang;
288 133 : if (auto* envLang = std::getenv("JAMI_LANG"))
289 132 : lang = envLang;
290 : else
291 4 : JAMI_LOG("Error getting JAMI_LANG env, attempting to get system language");
292 : // If language preference is empty, try to get from the system.
293 133 : if (lang.empty()) {
294 : #ifdef WIN32
295 : WCHAR localeBuffer[LOCALE_NAME_MAX_LENGTH];
296 : if (GetUserDefaultLocaleName(localeBuffer, LOCALE_NAME_MAX_LENGTH) != 0) {
297 : char utf8Buffer[LOCALE_NAME_MAX_LENGTH] {};
298 : WideCharToMultiByte(CP_UTF8,
299 : 0,
300 : localeBuffer,
301 : LOCALE_NAME_MAX_LENGTH,
302 : utf8Buffer,
303 : LOCALE_NAME_MAX_LENGTH,
304 : nullptr,
305 : nullptr);
306 :
307 : lang.append(utf8Buffer);
308 : string_replace(lang, "-", "_");
309 : }
310 : // Even though we default to the system variable in Windows, technically this
311 : // part of the code should not be reached because the client-qt must define that
312 : // variable and is unable to run the client and the daemon in diferent processes in Windows.
313 : #else
314 : // The same way described in the comment just above, Android should not reach this
315 : // part of the code given the client-android must define "JAMI_LANG" system variable.
316 : // And even if this part is reached, it should not work since std::locale is not
317 : // supported by the NDK.
318 :
319 : // LC_COLLATE is used to grab the locale for the case when the system user has set different
320 : // values for the preferred Language and Format.
321 1 : lang = setlocale(LC_COLLATE, "");
322 : // We set the environment to avoid checking from system everytime.
323 : // This is the case when running daemon and client in different processes
324 : // like with dbus.
325 1 : setenv("JAMI_LANG", lang.c_str(), 1);
326 : #endif // WIN32
327 : }
328 133 : return lang;
329 0 : }
330 :
331 : std::map<std::string, std::string>
332 133 : getLocales(const std::string& rootPath, const std::string& lang)
333 : {
334 133 : auto pluginName = rootPath.substr(rootPath.find_last_of(DIR_SEPARATOR_CH) + 1);
335 266 : auto basePath = fmt::format("{}/data/locale/{}", rootPath, pluginName + "_");
336 :
337 133 : std::map<std::string, std::string> locales = {};
338 :
339 : // Get language translations
340 133 : if (!lang.empty()) {
341 133 : locales = processLocaleFile(basePath + lang + ".json");
342 : }
343 :
344 : // Get default english values if no translations were found
345 133 : if (locales.empty()) {
346 41 : locales = processLocaleFile(basePath + "en.json");
347 : }
348 :
349 266 : return locales;
350 133 : }
351 :
352 : std::map<std::string, std::string>
353 174 : processLocaleFile(const std::string& preferenceLocaleFilePath)
354 : {
355 174 : std::error_code ec;
356 174 : if (!std::filesystem::is_regular_file(preferenceLocaleFilePath, ec)) {
357 41 : return {};
358 : }
359 133 : std::ifstream file(preferenceLocaleFilePath);
360 133 : Json::Value root;
361 133 : Json::CharReaderBuilder rbuilder;
362 399 : rbuilder["collectComments"] = false;
363 133 : std::string errs;
364 133 : std::map<std::string, std::string> locales {};
365 133 : if (file) {
366 : // Read the file to a json format
367 133 : if (Json::parseFromStream(rbuilder, file, &root, &errs)) {
368 133 : auto keys = root.getMemberNames();
369 1064 : for (const auto& key : keys) {
370 931 : locales[key] = root.get(key, "").asString();
371 : }
372 133 : }
373 : }
374 133 : return locales;
375 133 : }
376 : } // namespace PluginUtils
377 : } // namespace jami
|