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