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