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