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