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