Line data Source code
1 : /*
2 : * Copyright (C) 2020-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 "jamipluginmanager.h"
22 : #include "pluginsutils.h"
23 : #include "fileutils.h"
24 : #include "archiver.h"
25 : #include "logger.h"
26 : #include "manager.h"
27 : #include "jami/plugin_manager_interface.h"
28 : #include "store_ca_crt.cpp"
29 :
30 : #include <fstream>
31 : #include <stdexcept>
32 : #include <msgpack.hpp>
33 :
34 : #define FAILURE -1
35 : #define SUCCESS 0
36 : #define PLUGIN_ALREADY_INSTALLED 100 /* Plugin already installed with the same version */
37 : #define PLUGIN_OLD_VERSION 200 /* Plugin already installed with a newer version */
38 : #define SIGNATURE_VERIFICATION_FAILED 300
39 : #define CERTIFICATE_VERIFICATION_FAILED 400
40 : #define INVALID_PLUGIN 500
41 :
42 :
43 : #ifdef WIN32
44 : #define LIB_TYPE ".dll"
45 : #define LIB_PREFIX ""
46 : #else
47 : #ifdef __APPLE__
48 : #define LIB_TYPE ".dylib"
49 : #define LIB_PREFIX "lib"
50 : #else
51 : #define LIB_TYPE ".so"
52 : #define LIB_PREFIX "lib"
53 : #endif
54 : #endif
55 :
56 : namespace jami {
57 :
58 33 : JamiPluginManager::JamiPluginManager()
59 33 : : callsm_ {pm_}
60 33 : , chatsm_ {pm_}
61 33 : , webviewsm_ {pm_}
62 66 : , preferencesm_ {pm_}
63 : {
64 33 : registerServices();
65 33 : }
66 :
67 : std::string
68 10 : JamiPluginManager::getPluginAuthor(const std::string& rootPath, const std::string& pluginId)
69 : {
70 10 : auto cert = PluginUtils::readPluginCertificate(rootPath, pluginId);
71 10 : if (!cert) {
72 0 : JAMI_ERROR("Could not read plugin certificate");
73 0 : return {};
74 : }
75 10 : return cert->getIssuerName();
76 10 : }
77 :
78 : std::map<std::string, std::string>
79 19 : JamiPluginManager::getPluginDetails(const std::string& rootPath, bool reset)
80 : {
81 19 : auto detailsIt = pluginDetailsMap_.find(rootPath);
82 19 : if (detailsIt != pluginDetailsMap_.end()) {
83 12 : if (!reset)
84 9 : return detailsIt->second;
85 3 : pluginDetailsMap_.erase(detailsIt);
86 : }
87 :
88 : std::map<std::string, std::string> details = PluginUtils::parseManifestFile(
89 20 : PluginUtils::manifestPath(rootPath), rootPath);
90 10 : if (!details.empty()) {
91 10 : auto itIcon = details.find("iconPath");
92 10 : itIcon->second.insert(0, rootPath + DIR_SEPARATOR_CH + "data" + DIR_SEPARATOR_CH);
93 :
94 10 : auto itImage = details.find("backgroundPath");
95 10 : itImage->second.insert(0, rootPath + DIR_SEPARATOR_CH + "data" + DIR_SEPARATOR_CH);
96 :
97 10 : details["soPath"] = rootPath + DIR_SEPARATOR_CH + LIB_PREFIX + details["id"] + LIB_TYPE;
98 10 : details["author"] = getPluginAuthor(rootPath, details["id"]);
99 10 : detailsIt = pluginDetailsMap_.emplace(rootPath, std::move(details)).first;
100 10 : return detailsIt->second;
101 : }
102 0 : return {};
103 10 : }
104 :
105 : std::vector<std::string>
106 2 : JamiPluginManager::getInstalledPlugins()
107 : {
108 : // Gets all plugins in standard path
109 2 : auto pluginsPath = fileutils::get_data_dir() / "plugins";
110 2 : std::vector<std::string> pluginsPaths;
111 2 : std::error_code ec;
112 7 : for (const auto& entry : std::filesystem::directory_iterator(pluginsPath, ec)) {
113 1 : const auto& p = entry.path();
114 1 : if (PluginUtils::checkPluginValidity(p))
115 1 : pluginsPaths.emplace_back(p.string());
116 2 : }
117 :
118 : // Gets plugins installed in non standard path
119 2 : std::vector<std::string> nonStandardInstalls = jami::Manager::instance()
120 2 : .pluginPreferences.getInstalledPlugins();
121 2 : for (auto& path : nonStandardInstalls) {
122 0 : if (PluginUtils::checkPluginValidity(path))
123 0 : pluginsPaths.emplace_back(path);
124 : }
125 :
126 4 : return pluginsPaths;
127 2 : }
128 :
129 : bool
130 0 : JamiPluginManager::checkPluginCertificatePublicKey(const std::string& oldJplPath, const std::string& newJplPath)
131 : {
132 0 : std::map<std::string, std::string> oldDetails = PluginUtils::parseManifestFile(PluginUtils::manifestPath(oldJplPath), oldJplPath);
133 0 : if (
134 0 : oldDetails.empty() ||
135 0 : !std::filesystem::is_regular_file(oldJplPath + DIR_SEPARATOR_CH + oldDetails["id"] + ".crt") ||
136 0 : !std::filesystem::is_regular_file(newJplPath)
137 : )
138 0 : return false;
139 : try {
140 0 : auto oldCert = PluginUtils::readPluginCertificate(oldJplPath, oldDetails["id"]);
141 0 : auto newCert = PluginUtils::readPluginCertificateFromArchive(newJplPath);
142 0 : if (!oldCert || !newCert) {
143 0 : return false;
144 : }
145 0 : return oldCert->getPublicKey() == newCert->getPublicKey();
146 0 : } catch (const std::exception& e) {
147 0 : JAMI_ERR() << e.what();
148 0 : return false;
149 0 : }
150 : return true;
151 0 : }
152 :
153 : bool
154 12 : JamiPluginManager::checkPluginCertificateValidity(dht::crypto::Certificate* cert)
155 : {
156 12 : trust_.add(crypto::Certificate(store_ca_crt, sizeof(store_ca_crt)));
157 12 : return cert && *cert && trust_.verify(*cert);
158 : }
159 :
160 : std::map<std::string, std::string>
161 0 : JamiPluginManager::getPlatformInfo()
162 : {
163 0 : return PluginUtils::getPlatformInfo();
164 : }
165 :
166 : bool
167 12 : JamiPluginManager::checkPluginSignatureFile(const std::string& jplPath)
168 : {
169 : // check if the file exists
170 12 : if (!std::filesystem::is_regular_file(jplPath)){
171 1 : return false;
172 : }
173 : try {
174 11 : auto signatures = PluginUtils::readPluginSignatureFromArchive(jplPath);
175 11 : auto manifest = PluginUtils::readPluginManifestFromArchive(jplPath);
176 11 : const std::string& name = manifest["id"];
177 11 : auto filesPath = archiver::listFilesFromArchive(jplPath);
178 120 : for (const auto& file : filesPath) {
179 : // we skip the signatures and signatures.sig file
180 111 : if (file == "signatures" || file == "signatures.sig")
181 20 : continue;
182 : // we also skip the plugin certificate
183 91 : if (file == name + ".crt")
184 9 : continue;
185 :
186 82 : if (signatures.count(file) == 0) {
187 2 : return false;
188 : }
189 : }
190 15 : } catch (const std::exception& e) {
191 0 : return false;
192 0 : }
193 9 : return true;
194 : }
195 :
196 : bool
197 11 : JamiPluginManager::checkPluginSignatureValidity(const std::string& jplPath, dht::crypto::Certificate* cert)
198 : {
199 11 : if (!std::filesystem::is_regular_file(jplPath))
200 0 : return false;
201 : try{
202 11 : const auto& pk = cert->getPublicKey();
203 20 : auto signaturesData = archiver::readFileFromArchive(jplPath, "signatures");
204 10 : auto signatureFile = PluginUtils::readSignatureFileFromArchive(jplPath);
205 10 : if (!pk.checkSignature(signaturesData, signatureFile))
206 0 : return false;
207 10 : auto signatures = PluginUtils::readPluginSignatureFromArchive(jplPath);
208 89 : for (const auto& signature : signatures) {
209 80 : auto file = archiver::readFileFromArchive(jplPath, signature.first);
210 80 : if (!pk.checkSignature(file, signature.second)){
211 3 : JAMI_ERROR("{} not correctly signed", signature.first);
212 1 : return false;
213 : }
214 80 : }
215 13 : } catch (const std::exception& e) {
216 1 : return false;
217 1 : }
218 :
219 9 : return true;
220 : }
221 :
222 : bool
223 9 : JamiPluginManager::checkPluginSignature(const std::string& jplPath, dht::crypto::Certificate* cert)
224 : {
225 9 : if (!std::filesystem::is_regular_file(jplPath) || !cert || !*cert)
226 1 : return false;
227 : try {
228 16 : return checkPluginSignatureValidity(jplPath, cert) &&
229 16 : checkPluginSignatureFile(jplPath);
230 0 : } catch (const std::exception& e) {
231 0 : return false;
232 0 : }
233 : }
234 :
235 : std::unique_ptr<dht::crypto::Certificate>
236 13 : JamiPluginManager::checkPluginCertificate(const std::string& jplPath, bool force)
237 : {
238 13 : if (!std::filesystem::is_regular_file(jplPath))
239 1 : return {};
240 : try {
241 12 : auto cert = PluginUtils::readPluginCertificateFromArchive(jplPath);
242 12 : if(checkPluginCertificateValidity(cert.get()) || force){
243 10 : return cert;
244 : }
245 2 : return {};
246 12 : } catch (const std::exception& e) {
247 0 : return {};
248 0 : }
249 : }
250 :
251 : int
252 7 : JamiPluginManager::installPlugin(const std::string& jplPath, bool force)
253 : {
254 7 : int r {SUCCESS};
255 7 : if (std::filesystem::is_regular_file(jplPath)) {
256 : try {
257 7 : auto manifestMap = PluginUtils::readPluginManifestFromArchive(jplPath);
258 7 : const std::string& name = manifestMap["id"];
259 7 : if (name.empty())
260 0 : return INVALID_PLUGIN;
261 7 : auto cert = checkPluginCertificate(jplPath, force);
262 7 : if (!cert)
263 0 : return CERTIFICATE_VERIFICATION_FAILED;
264 7 : if(!checkPluginSignature(jplPath, cert.get()))
265 0 : return SIGNATURE_VERIFICATION_FAILED;
266 7 : const std::string& version = manifestMap["version"];
267 14 : auto destinationDir = (fileutils::get_data_dir() / "plugins" / name).string();
268 : // Find if there is an existing version of this plugin
269 : const auto alreadyInstalledManifestMap = PluginUtils::parseManifestFile(
270 14 : PluginUtils::manifestPath(destinationDir), destinationDir);
271 :
272 7 : if (!alreadyInstalledManifestMap.empty()) {
273 1 : if (force) {
274 1 : r = uninstallPlugin(destinationDir);
275 1 : if (r == SUCCESS) {
276 1 : archiver::uncompressArchive(jplPath,
277 : destinationDir,
278 : PluginUtils::uncompressJplFunction);
279 : }
280 : } else {
281 0 : std::string installedVersion = alreadyInstalledManifestMap.at("version");
282 0 : if (version > installedVersion) {
283 0 : if(!checkPluginCertificatePublicKey(destinationDir, jplPath))
284 0 : return CERTIFICATE_VERIFICATION_FAILED;
285 0 : r = uninstallPlugin(destinationDir);
286 0 : if (r == SUCCESS) {
287 0 : archiver::uncompressArchive(jplPath,
288 : destinationDir,
289 : PluginUtils::uncompressJplFunction);
290 : }
291 0 : } else if (version == installedVersion) {
292 0 : r = PLUGIN_ALREADY_INSTALLED;
293 : } else {
294 0 : r = PLUGIN_OLD_VERSION;
295 : }
296 0 : }
297 : } else {
298 6 : archiver::uncompressArchive(jplPath,
299 : destinationDir,
300 : PluginUtils::uncompressJplFunction);
301 : }
302 7 : if (!libjami::getPluginsEnabled()) {
303 0 : libjami::setPluginsEnabled(true);
304 0 : Manager::instance().saveConfig();
305 0 : loadPlugins();
306 0 : return r;
307 : }
308 7 : libjami::loadPlugin(destinationDir);
309 7 : } catch (const std::exception& e) {
310 0 : JAMI_ERR() << e.what();
311 0 : }
312 : }
313 7 : return r;
314 : }
315 :
316 : int
317 7 : JamiPluginManager::uninstallPlugin(const std::string& rootPath)
318 : {
319 7 : std::error_code ec;
320 7 : if (PluginUtils::checkPluginValidity(rootPath)) {
321 7 : auto detailsIt = pluginDetailsMap_.find(rootPath);
322 7 : if (detailsIt != pluginDetailsMap_.end()) {
323 7 : bool loaded = pm_.checkLoadedPlugin(rootPath);
324 7 : if (loaded) {
325 5 : JAMI_INFO() << "PLUGIN: unloading before uninstall.";
326 5 : bool status = libjami::unloadPlugin(rootPath);
327 5 : if (!status) {
328 0 : JAMI_INFO() << "PLUGIN: could not unload, not performing uninstall.";
329 0 : return FAILURE;
330 : }
331 : }
332 21 : for (const auto& accId : jami::Manager::instance().getAccountList())
333 42 : std::filesystem::remove_all(fileutils::get_data_dir() / accId
334 63 : / "plugins" / detailsIt->second.at("id"), ec);
335 7 : pluginDetailsMap_.erase(detailsIt);
336 : }
337 7 : return std::filesystem::remove_all(rootPath, ec) ? SUCCESS : FAILURE;
338 : } else {
339 0 : JAMI_INFO() << "PLUGIN: not installed.";
340 0 : return FAILURE;
341 : }
342 : }
343 :
344 : bool
345 8 : JamiPluginManager::loadPlugin(const std::string& rootPath)
346 : {
347 : #ifdef ENABLE_PLUGIN
348 : try {
349 8 : bool status = pm_.load(getPluginDetails(rootPath).at("soPath"));
350 8 : JAMI_INFO() << "PLUGIN: load status - " << status;
351 :
352 8 : return status;
353 :
354 0 : } catch (const std::exception& e) {
355 0 : JAMI_ERR() << e.what();
356 0 : return false;
357 0 : }
358 : #endif
359 : return false;
360 : }
361 :
362 : bool
363 10 : JamiPluginManager::loadPlugins()
364 : {
365 : #ifdef ENABLE_PLUGIN
366 10 : bool status = true;
367 10 : auto loadedPlugins = jami::Manager::instance().pluginPreferences.getLoadedPlugins();
368 11 : for (const auto& pluginPath : loadedPlugins) {
369 1 : status &= loadPlugin(pluginPath);
370 : }
371 10 : return status;
372 : #endif
373 : return false;
374 10 : }
375 :
376 : bool
377 7 : JamiPluginManager::unloadPlugin(const std::string& rootPath)
378 : {
379 : #ifdef ENABLE_PLUGIN
380 : try {
381 7 : bool status = pm_.unload(getPluginDetails(rootPath).at("soPath"));
382 7 : JAMI_INFO() << "PLUGIN: unload status - " << status;
383 :
384 7 : return status;
385 0 : } catch (const std::exception& e) {
386 0 : JAMI_ERR() << e.what();
387 0 : return false;
388 0 : }
389 : #endif
390 : return false;
391 : }
392 :
393 : std::vector<std::string>
394 4 : JamiPluginManager::getLoadedPlugins() const
395 : {
396 4 : std::vector<std::string> loadedSoPlugins = pm_.getLoadedPlugins();
397 4 : std::vector<std::string> loadedPlugins {};
398 4 : loadedPlugins.reserve(loadedSoPlugins.size());
399 4 : std::transform(loadedSoPlugins.begin(),
400 : loadedSoPlugins.end(),
401 : std::back_inserter(loadedPlugins),
402 2 : [](const std::string& soPath) {
403 4 : return PluginUtils::getRootPathFromSoPath(soPath).string();
404 : });
405 8 : return loadedPlugins;
406 4 : }
407 :
408 : std::vector<std::map<std::string, std::string>>
409 2 : JamiPluginManager::getPluginPreferences(const std::string& rootPath, const std::string& accountId)
410 : {
411 2 : return PluginPreferencesUtils::getPreferences(rootPath, accountId);
412 : }
413 :
414 : bool
415 2 : JamiPluginManager::setPluginPreference(const std::filesystem::path& rootPath,
416 : const std::string& accountId,
417 : const std::string& key,
418 : const std::string& value)
419 : {
420 2 : std::string acc = accountId;
421 :
422 : // If we try to change a preference value linked to an account
423 : // but that preference is global, we must ignore accountId and
424 : // change the preference for every account
425 2 : if (!accountId.empty()) {
426 : // Get global preferences
427 2 : auto preferences = PluginPreferencesUtils::getPreferences(rootPath, "");
428 : // Check if the preference we want to change is global
429 1 : auto it = std::find_if(preferences.cbegin(),
430 : preferences.cend(),
431 1 : [key](const std::map<std::string, std::string>& preference) {
432 1 : return preference.at("key") == key;
433 : });
434 : // Ignore accountId if global preference
435 1 : if (it != preferences.cend())
436 0 : acc.clear();
437 1 : }
438 :
439 : std::map<std::string, std::string> pluginUserPreferencesMap
440 2 : = PluginPreferencesUtils::getUserPreferencesValuesMap(rootPath, acc);
441 : std::map<std::string, std::string> pluginPreferencesMap
442 2 : = PluginPreferencesUtils::getPreferencesValuesMap(rootPath, acc);
443 :
444 : // If any plugin handler is active we may have to reload it
445 2 : bool force {pm_.checkLoadedPlugin(rootPath.string())};
446 :
447 : // We check if the preference is modified without having to reload plugin
448 2 : force &= preferencesm_.setPreference(key, value, rootPath.string(), acc);
449 2 : force &= callsm_.setPreference(key, value, rootPath.string());
450 2 : force &= chatsm_.setPreference(key, value, rootPath.string());
451 :
452 2 : if (force)
453 0 : unloadPlugin(rootPath.string());
454 :
455 : // Save preferences.msgpack with modified preferences values
456 2 : auto find = pluginPreferencesMap.find(key);
457 2 : if (find != pluginPreferencesMap.end()) {
458 2 : pluginUserPreferencesMap[key] = value;
459 : auto preferencesValuesFilePath
460 2 : = PluginPreferencesUtils::valuesFilePath(rootPath, acc);
461 2 : std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
462 2 : std::ofstream fs(preferencesValuesFilePath, std::ios::binary);
463 2 : if (!fs.good()) {
464 0 : if (force) {
465 0 : loadPlugin(rootPath.string());
466 : }
467 0 : return false;
468 : }
469 : try {
470 2 : msgpack::pack(fs, pluginUserPreferencesMap);
471 0 : } catch (const std::exception& e) {
472 0 : JAMI_ERR() << e.what();
473 0 : if (force) {
474 0 : loadPlugin(rootPath.string());
475 : }
476 0 : return false;
477 0 : }
478 2 : }
479 2 : if (force) {
480 0 : loadPlugin(rootPath.string());
481 : }
482 2 : return true;
483 2 : }
484 :
485 : std::map<std::string, std::string>
486 15 : JamiPluginManager::getPluginPreferencesValuesMap(const std::string& rootPath,
487 : const std::string& accountId)
488 : {
489 15 : return PluginPreferencesUtils::getPreferencesValuesMap(rootPath, accountId);
490 : }
491 :
492 : bool
493 3 : JamiPluginManager::resetPluginPreferencesValuesMap(const std::string& rootPath,
494 : const std::string& accountId)
495 : {
496 3 : bool acc {accountId.empty()};
497 3 : bool loaded {pm_.checkLoadedPlugin(rootPath)};
498 3 : if (loaded && acc)
499 0 : unloadPlugin(rootPath);
500 3 : auto status = PluginPreferencesUtils::resetPreferencesValuesMap(rootPath, accountId);
501 3 : preferencesm_.resetPreferences(rootPath, accountId);
502 3 : if (loaded && acc) {
503 0 : loadPlugin(rootPath);
504 : }
505 3 : return status;
506 : }
507 :
508 : void
509 33 : JamiPluginManager::registerServices()
510 : {
511 : // Register getPluginPreferences so that plugin's can receive it's preferences
512 33 : pm_.registerService("getPluginPreferences", [](const DLPlugin* plugin, void* data) {
513 0 : auto ppp = static_cast<std::map<std::string, std::string>*>(data);
514 0 : *ppp = PluginPreferencesUtils::getPreferencesValuesMap(
515 0 : PluginUtils::getRootPathFromSoPath(plugin->getPath()));
516 0 : return SUCCESS;
517 : });
518 :
519 : // Register getPluginDataPath so that plugin's can receive the path to it's data folder
520 33 : pm_.registerService("getPluginDataPath", [](const DLPlugin* plugin, void* data) {
521 8 : auto dataPath = static_cast<std::string*>(data);
522 8 : dataPath->assign(PluginUtils::dataPath(plugin->getPath()).string());
523 8 : return SUCCESS;
524 : });
525 :
526 : // getPluginAccPreferences is a service that allows plugins to load saved per account preferences.
527 0 : auto getPluginAccPreferences = [](const DLPlugin* plugin, void* data) {
528 0 : const auto path = PluginUtils::getRootPathFromSoPath(plugin->getPath());
529 0 : auto preferencesPtr {(static_cast<PreferencesMap*>(data))};
530 0 : if (!preferencesPtr)
531 0 : return FAILURE;
532 :
533 0 : preferencesPtr->emplace("default",
534 0 : PluginPreferencesUtils::getPreferencesValuesMap(path, "default"));
535 :
536 0 : for (const auto& accId : jami::Manager::instance().getAccountList())
537 0 : preferencesPtr->emplace(accId,
538 0 : PluginPreferencesUtils::getPreferencesValuesMap(path, accId));
539 0 : return SUCCESS;
540 0 : };
541 :
542 33 : pm_.registerService("getPluginAccPreferences", getPluginAccPreferences);
543 33 : }
544 :
545 : void
546 1 : JamiPluginManager::addPluginAuthority(const dht::crypto::Certificate& cert)
547 : {
548 1 : trust_.add(cert);
549 1 : }
550 : } // namespace jami
|