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