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