You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PluginJarLoader.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (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 GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.plugins;
  21. import com.google.common.base.Strings;
  22. import java.io.File;
  23. import java.io.IOException;
  24. import java.nio.file.Path;
  25. import java.util.Collection;
  26. import java.util.Collections;
  27. import java.util.HashMap;
  28. import java.util.LinkedHashMap;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Set;
  32. import java.util.function.Function;
  33. import java.util.stream.Collectors;
  34. import javax.inject.Inject;
  35. import org.apache.commons.io.FileUtils;
  36. import org.sonar.api.utils.MessageException;
  37. import org.sonar.api.utils.log.Logger;
  38. import org.sonar.api.utils.log.Loggers;
  39. import org.sonar.core.platform.PluginInfo;
  40. import org.sonar.core.platform.SonarQubeVersion;
  41. import org.sonar.server.platform.ServerFileSystem;
  42. import static java.lang.String.format;
  43. import static org.apache.commons.io.FileUtils.moveFile;
  44. import static org.sonar.core.util.FileUtils.deleteQuietly;
  45. import static org.sonar.server.log.ServerProcessLogging.STARTUP_LOGGER_NAME;
  46. import static org.sonar.server.plugins.PluginType.BUNDLED;
  47. import static org.sonar.server.plugins.PluginType.EXTERNAL;
  48. public class PluginJarLoader {
  49. private static final Logger LOG = Loggers.get(PluginJarLoader.class);
  50. // List of plugins that are silently removed if installed
  51. private static final Set<String> DEFAULT_BLACKLISTED_PLUGINS = Set.of("scmactivity", "issuesreport", "genericcoverage");
  52. // List of plugins that should prevent the server to finish its startup
  53. private static final Set<String> FORBIDDEN_INCOMPATIBLE_PLUGINS = Set.of(
  54. "sqale", "report", "views", "authgithub", "authgitlab", "authbitbucket", "authsaml", "ldap", "scmgit", "scmsvn");
  55. private static final String LOAD_ERROR_GENERIC_MESSAGE = "Startup failed: Plugins can't be loaded. See web logs for more information";
  56. private final ServerFileSystem fs;
  57. private final SonarQubeVersion sonarQubeVersion;
  58. private final Set<String> blacklistedPluginKeys;
  59. @Inject
  60. public PluginJarLoader(ServerFileSystem fs, SonarQubeVersion sonarQubeVersion) {
  61. this(fs, sonarQubeVersion, DEFAULT_BLACKLISTED_PLUGINS);
  62. }
  63. PluginJarLoader(ServerFileSystem fs, SonarQubeVersion sonarQubeVersion, Set<String> blacklistedPluginKeys) {
  64. this.fs = fs;
  65. this.sonarQubeVersion = sonarQubeVersion;
  66. this.blacklistedPluginKeys = blacklistedPluginKeys;
  67. }
  68. /**
  69. * Load the plugins that are located in lib/extensions and extensions/plugins. Blacklisted plugins are deleted.
  70. */
  71. public Collection<ServerPluginInfo> loadPlugins() {
  72. Map<String, ServerPluginInfo> bundledPluginsByKey = new LinkedHashMap<>();
  73. for (ServerPluginInfo bundled : getBundledPluginsMetadata()) {
  74. failIfContains(bundledPluginsByKey, bundled,
  75. plugin -> MessageException.of(format("Found two versions of the plugin %s [%s] in the directory %s. Please remove one of %s or %s.",
  76. bundled.getName(), bundled.getKey(), getRelativeDir(fs.getInstalledBundledPluginsDir()), bundled.getNonNullJarFile().getName(), plugin.getNonNullJarFile().getName())));
  77. bundledPluginsByKey.put(bundled.getKey(), bundled);
  78. }
  79. Map<String, ServerPluginInfo> externalPluginsByKey = new LinkedHashMap<>();
  80. for (ServerPluginInfo external : getExternalPluginsMetadata()) {
  81. failIfContains(bundledPluginsByKey, external,
  82. plugin -> MessageException.of(format("Found a plugin '%s' in the directory '%s' with the same key [%s] as a built-in feature '%s'. Please remove '%s'.",
  83. external.getName(), getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getKey(), plugin.getName(),
  84. new File(getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getNonNullJarFile().getName()))));
  85. failIfContains(externalPluginsByKey, external,
  86. plugin -> MessageException.of(format("Found two versions of the plugin '%s' [%s] in the directory '%s'. Please remove %s or %s.", external.getName(), external.getKey(),
  87. getRelativeDir(fs.getInstalledExternalPluginsDir()), external.getNonNullJarFile().getName(), plugin.getNonNullJarFile().getName())));
  88. externalPluginsByKey.put(external.getKey(), external);
  89. }
  90. for (PluginInfo downloaded : getDownloadedPluginsMetadata()) {
  91. failIfContains(bundledPluginsByKey, downloaded,
  92. plugin -> MessageException.of(format("Fail to update plugin: %s. Built-in feature with same key already exists: %s. Move or delete plugin from %s directory",
  93. plugin.getName(), plugin.getKey(), getRelativeDir(fs.getDownloadedPluginsDir()))));
  94. ServerPluginInfo installedPlugin;
  95. if (externalPluginsByKey.containsKey(downloaded.getKey())) {
  96. deleteQuietly(externalPluginsByKey.get(downloaded.getKey()).getNonNullJarFile());
  97. installedPlugin = moveDownloadedPluginToExtensions(downloaded);
  98. LOG.info("Plugin {} [{}] updated to version {}", installedPlugin.getName(), installedPlugin.getKey(), installedPlugin.getVersion());
  99. } else {
  100. installedPlugin = moveDownloadedPluginToExtensions(downloaded);
  101. LOG.info("Plugin {} [{}] installed", installedPlugin.getName(), installedPlugin.getKey());
  102. }
  103. externalPluginsByKey.put(downloaded.getKey(), installedPlugin);
  104. }
  105. Map<String, ServerPluginInfo> plugins = new HashMap<>(externalPluginsByKey.size() + bundledPluginsByKey.size());
  106. plugins.putAll(externalPluginsByKey);
  107. plugins.putAll(bundledPluginsByKey);
  108. PluginRequirementsValidator.unloadIncompatiblePlugins(plugins);
  109. return plugins.values();
  110. }
  111. private static String getRelativeDir(File dir) {
  112. Path parent = dir.toPath().getParent().getParent();
  113. return parent.relativize(dir.toPath()).toString();
  114. }
  115. private static void failIfContains(Map<String, ? extends PluginInfo> map, PluginInfo value, Function<PluginInfo, RuntimeException> msg) {
  116. PluginInfo pluginInfo = map.get(value.getKey());
  117. if (pluginInfo != null) {
  118. RuntimeException exception = msg.apply(pluginInfo);
  119. logGenericPluginLoadErrorLog();
  120. throw exception;
  121. }
  122. }
  123. private static void logGenericPluginLoadErrorLog() {
  124. Logger logger = Loggers.get(STARTUP_LOGGER_NAME);
  125. logger.error(LOAD_ERROR_GENERIC_MESSAGE);
  126. }
  127. private List<ServerPluginInfo> getBundledPluginsMetadata() {
  128. return loadPluginsFromDir(fs.getInstalledBundledPluginsDir(), jar -> ServerPluginInfo.create(jar, BUNDLED));
  129. }
  130. private List<ServerPluginInfo> getExternalPluginsMetadata() {
  131. return loadPluginsFromDir(fs.getInstalledExternalPluginsDir(), jar -> ServerPluginInfo.create(jar, EXTERNAL));
  132. }
  133. private List<PluginInfo> getDownloadedPluginsMetadata() {
  134. return loadPluginsFromDir(fs.getDownloadedPluginsDir(), PluginInfo::create);
  135. }
  136. private ServerPluginInfo moveDownloadedPluginToExtensions(PluginInfo pluginInfo) {
  137. File destDir = fs.getInstalledExternalPluginsDir();
  138. File destFile = new File(destDir, pluginInfo.getNonNullJarFile().getName());
  139. if (destFile.exists()) {
  140. deleteQuietly(destFile);
  141. }
  142. movePlugin(pluginInfo.getNonNullJarFile(), destFile);
  143. return ServerPluginInfo.create(destFile, EXTERNAL);
  144. }
  145. private static void movePlugin(File sourcePluginFile, File destPluginFile) {
  146. try {
  147. moveFile(sourcePluginFile, destPluginFile);
  148. } catch (IOException e) {
  149. throw new IllegalStateException(format("Fail to move plugin: %s to %s", sourcePluginFile.getAbsolutePath(), destPluginFile.getAbsolutePath()), e);
  150. }
  151. }
  152. private <T extends PluginInfo> List<T> loadPluginsFromDir(File pluginsDir, Function<File, T> toPluginInfo) {
  153. List<T> list = listJarFiles(pluginsDir).stream()
  154. .map(toPluginInfo)
  155. .filter(this::checkPluginInfo)
  156. .collect(Collectors.toList());
  157. failIfContainsIncompatiblePlugins(list);
  158. return list;
  159. }
  160. private static void failIfContainsIncompatiblePlugins(List<? extends PluginInfo> plugins) {
  161. List<String> incompatiblePlugins = plugins.stream()
  162. .filter(p -> FORBIDDEN_INCOMPATIBLE_PLUGINS.contains(p.getKey()))
  163. .map(p -> "'" + p.getKey() + "'")
  164. .sorted()
  165. .collect(Collectors.toList());
  166. if (!incompatiblePlugins.isEmpty()) {
  167. logGenericPluginLoadErrorLog();
  168. throw MessageException.of(String.format("The following %s no longer compatible with this version of SonarQube: %s",
  169. incompatiblePlugins.size() > 1 ? "plugins are" : "plugin is", String.join(", ", incompatiblePlugins)));
  170. }
  171. }
  172. private boolean checkPluginInfo(PluginInfo info) {
  173. String pluginKey = info.getKey();
  174. if (blacklistedPluginKeys.contains(pluginKey)) {
  175. LOG.warn("Plugin {} [{}] is blacklisted and is being uninstalled", info.getName(), pluginKey);
  176. deleteQuietly(info.getNonNullJarFile());
  177. return false;
  178. }
  179. if (Strings.isNullOrEmpty(info.getMainClass()) && Strings.isNullOrEmpty(info.getBasePlugin())) {
  180. LOG.warn("Plugin {} [{}] is ignored because entry point class is not defined", info.getName(), info.getKey());
  181. return false;
  182. }
  183. if (!info.isCompatibleWith(sonarQubeVersion.get().toString())) {
  184. throw MessageException.of(format("Plugin %s [%s] requires at least SonarQube %s", info.getName(), info.getKey(), info.getMinimalSqVersion()));
  185. }
  186. return true;
  187. }
  188. private static Collection<File> listJarFiles(File dir) {
  189. if (dir.exists()) {
  190. return FileUtils.listFiles(dir, new String[] {"jar"}, false);
  191. }
  192. return Collections.emptyList();
  193. }
  194. }