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