Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

PluginFiles.java 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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.scanner.bootstrap;
  21. import com.google.common.annotations.VisibleForTesting;
  22. import java.io.BufferedInputStream;
  23. import java.io.File;
  24. import java.io.IOException;
  25. import java.io.InputStream;
  26. import java.net.HttpURLConnection;
  27. import java.nio.file.Files;
  28. import java.nio.file.Path;
  29. import java.nio.file.StandardCopyOption;
  30. import java.util.Optional;
  31. import org.apache.commons.codec.digest.DigestUtils;
  32. import org.apache.commons.io.FileUtils;
  33. import org.slf4j.Logger;
  34. import org.slf4j.LoggerFactory;
  35. import org.sonar.api.config.Configuration;
  36. import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
  37. import org.sonar.scanner.http.DefaultScannerWsClient;
  38. import org.sonarqube.ws.client.GetRequest;
  39. import org.sonarqube.ws.client.HttpException;
  40. import org.sonarqube.ws.client.WsResponse;
  41. import static java.lang.String.format;
  42. public class PluginFiles {
  43. private static final Logger LOGGER = LoggerFactory.getLogger(PluginFiles.class);
  44. private static final String MD5_HEADER = "Sonar-MD5";
  45. @VisibleForTesting
  46. static final String PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY = "sonar.plugins.download.timeout";
  47. private static final int PLUGINS_DOWNLOAD_TIMEOUT_DEFAULT = 300;
  48. private final DefaultScannerWsClient wsClient;
  49. private final Configuration configuration;
  50. private final Path cacheDir;
  51. private final Path tempDir;
  52. public PluginFiles(DefaultScannerWsClient wsClient, Configuration configuration, SonarUserHome sonarUserHome) {
  53. this.wsClient = wsClient;
  54. this.configuration = configuration;
  55. var home = sonarUserHome.getPath();
  56. this.cacheDir = mkdir(home.resolve("cache"), "user cache");
  57. this.tempDir = mkdir(home.resolve("_tmp"), "temp dir");
  58. LOGGER.info("User cache: {}", cacheDir);
  59. }
  60. public File createTempDir() {
  61. try {
  62. return Files.createTempDirectory(tempDir, "plugins").toFile();
  63. } catch (IOException e) {
  64. throw new IllegalStateException("Fail to create temp directory in " + tempDir, e);
  65. }
  66. }
  67. /**
  68. * Get the JAR file of specified plugin. If not present in user local cache,
  69. * then it's downloaded from server and added to cache.
  70. *
  71. * @return the file, or {@link Optional#empty()} if plugin not found (404 HTTP code)
  72. * @throws IllegalStateException if the plugin can't be downloaded (not 404 nor 2xx HTTP codes)
  73. * or can't be cached locally.
  74. */
  75. public Optional<File> get(InstalledPlugin plugin) {
  76. // Does not fail if another process tries to create the directory at the same time.
  77. Path jarInCache = jarInCache(plugin.key, plugin.hash);
  78. if (Files.isRegularFile(jarInCache)) {
  79. return Optional.of(jarInCache.toFile());
  80. }
  81. return download(plugin).map(Path::toFile);
  82. }
  83. private Optional<Path> download(InstalledPlugin plugin) {
  84. GetRequest request = new GetRequest("api/plugins/download")
  85. .setParam("plugin", plugin.key)
  86. .setTimeOutInMs(configuration.getInt(PLUGINS_DOWNLOAD_TIMEOUT_PROPERTY).orElse(PLUGINS_DOWNLOAD_TIMEOUT_DEFAULT) * 1000);
  87. Path downloadedFile = newTempFile();
  88. LOGGER.debug("Download plugin '{}' to '{}'", plugin.key, downloadedFile);
  89. try (WsResponse response = wsClient.call(request)) {
  90. Optional<String> expectedMd5 = response.header(MD5_HEADER);
  91. if (expectedMd5.isEmpty()) {
  92. throw new IllegalStateException(format(
  93. "Fail to download plugin [%s]. Request to %s did not return header %s", plugin.key, response.requestUrl(), MD5_HEADER));
  94. }
  95. downloadBinaryTo(plugin, downloadedFile, response);
  96. // verify integrity
  97. String effectiveTempMd5 = computeMd5(downloadedFile);
  98. if (!expectedMd5.get().equals(effectiveTempMd5)) {
  99. throw new IllegalStateException(format(
  100. "Fail to download plugin [%s]. File %s was expected to have checksum %s but had %s", plugin.key, downloadedFile, expectedMd5.get(), effectiveTempMd5));
  101. }
  102. // un-compress if needed
  103. String cacheMd5;
  104. Path tempJar;
  105. tempJar = downloadedFile;
  106. cacheMd5 = expectedMd5.get();
  107. // put in cache
  108. Path jarInCache = jarInCache(plugin.key, cacheMd5);
  109. mkdir(jarInCache.getParent());
  110. moveFile(tempJar, jarInCache);
  111. return Optional.of(jarInCache);
  112. } catch (HttpException e) {
  113. if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
  114. // Plugin was listed but not longer available. It has probably been
  115. // uninstalled.
  116. return Optional.empty();
  117. }
  118. // not 2xx nor 404
  119. throw new IllegalStateException(format("Fail to download plugin [%s]. Request to %s returned code %d.", plugin.key, e.url(), e.code()));
  120. }
  121. }
  122. private static void downloadBinaryTo(InstalledPlugin plugin, Path downloadedFile, WsResponse response) {
  123. try (InputStream stream = response.contentStream()) {
  124. FileUtils.copyInputStreamToFile(stream, downloadedFile.toFile());
  125. } catch (IOException e) {
  126. throw new IllegalStateException(format("Fail to download plugin [%s] into %s", plugin.key, downloadedFile), e);
  127. }
  128. }
  129. private Path jarInCache(String pluginKey, String hash) {
  130. Path hashDir = cacheDir.resolve(hash);
  131. Path file = hashDir.resolve(format("sonar-%s-plugin.jar", pluginKey));
  132. if (!file.getParent().equals(hashDir)) {
  133. // vulnerability - attempt to create a file outside the cache directory
  134. throw new IllegalStateException(format("Fail to download plugin [%s]. Key is not valid.", pluginKey));
  135. }
  136. return file;
  137. }
  138. private Path newTempFile() {
  139. try {
  140. return Files.createTempFile(tempDir, "fileCache", null);
  141. } catch (IOException e) {
  142. throw new IllegalStateException("Fail to create temp file in " + tempDir, e);
  143. }
  144. }
  145. private static String computeMd5(Path file) {
  146. try (InputStream fis = new BufferedInputStream(Files.newInputStream(file))) {
  147. return DigestUtils.md5Hex(fis);
  148. } catch (IOException e) {
  149. throw new IllegalStateException("Fail to compute md5 of " + file, e);
  150. }
  151. }
  152. private static void moveFile(Path sourceFile, Path targetFile) {
  153. try {
  154. Files.move(sourceFile, targetFile, StandardCopyOption.ATOMIC_MOVE);
  155. } catch (IOException e1) {
  156. // Check if the file was cached by another process during download
  157. if (!Files.exists(targetFile)) {
  158. LOGGER.warn("Unable to rename {} to {}", sourceFile, targetFile);
  159. LOGGER.warn("A copy/delete will be tempted but with no guarantee of atomicity");
  160. try {
  161. Files.move(sourceFile, targetFile);
  162. } catch (IOException e2) {
  163. throw new IllegalStateException("Fail to move " + sourceFile + " to " + targetFile, e2);
  164. }
  165. }
  166. }
  167. }
  168. private static void mkdir(Path dir) {
  169. try {
  170. Files.createDirectories(dir);
  171. } catch (IOException e) {
  172. throw new IllegalStateException("Fail to create cache directory: " + dir, e);
  173. }
  174. }
  175. private static Path mkdir(Path dir, String debugTitle) {
  176. if (!Files.isDirectory(dir)) {
  177. LOGGER.debug("Create : {}", dir);
  178. try {
  179. Files.createDirectories(dir);
  180. } catch (IOException e) {
  181. throw new IllegalStateException("Unable to create folder " + debugTitle + " at " + dir, e);
  182. }
  183. }
  184. return dir;
  185. }
  186. }