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.

PluginFiles.java 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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.scanner.bootstrap;
  21. import java.io.BufferedInputStream;
  22. import java.io.BufferedOutputStream;
  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.util.Objects;
  29. import java.util.Optional;
  30. import java.util.jar.JarOutputStream;
  31. import java.util.jar.Pack200;
  32. import java.util.stream.Stream;
  33. import java.util.zip.GZIPInputStream;
  34. import org.apache.commons.codec.digest.DigestUtils;
  35. import org.apache.commons.io.FileUtils;
  36. import org.sonar.api.config.Configuration;
  37. import org.sonar.api.utils.log.Logger;
  38. import org.sonar.api.utils.log.Loggers;
  39. import org.sonar.scanner.bootstrap.ScannerPluginInstaller.InstalledPlugin;
  40. import org.sonarqube.ws.client.GetRequest;
  41. import org.sonarqube.ws.client.HttpException;
  42. import org.sonarqube.ws.client.WsResponse;
  43. import static java.lang.String.format;
  44. public class PluginFiles {
  45. private static final Logger LOGGER = Loggers.get(PluginFiles.class);
  46. private static final String MD5_HEADER = "Sonar-MD5";
  47. private static final String COMPRESSION_HEADER = "Sonar-Compression";
  48. private static final String PACK200 = "pack200";
  49. private static final String UNCOMPRESSED_MD5_HEADER = "Sonar-UncompressedMD5";
  50. private final DefaultScannerWsClient wsClient;
  51. private final File cacheDir;
  52. private final File tempDir;
  53. public PluginFiles(DefaultScannerWsClient wsClient, Configuration configuration) {
  54. this.wsClient = wsClient;
  55. File home = locateHomeDir(configuration);
  56. this.cacheDir = mkdir(new File(home, "cache"), "user cache");
  57. this.tempDir = mkdir(new File(home, "_tmp"), "temp dir");
  58. LOGGER.info("User cache: {}", cacheDir.getAbsolutePath());
  59. }
  60. public File createTempDir() {
  61. try {
  62. return Files.createTempDirectory(tempDir.toPath(), "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. File jarInCache = jarInCache(plugin.key, plugin.hash);
  78. if (jarInCache.exists() && jarInCache.isFile()) {
  79. return Optional.of(jarInCache);
  80. }
  81. return download(plugin);
  82. }
  83. private Optional<File> download(InstalledPlugin plugin) {
  84. GetRequest request = new GetRequest("api/plugins/download")
  85. .setParam("plugin", plugin.key)
  86. .setTimeOutInMs(5 * 60_000);
  87. try {
  88. Class.forName("java.util.jar.Pack200");
  89. request.setParam("acceptCompressions", PACK200);
  90. } catch (ClassNotFoundException e) {
  91. // ignore and don't use any compression
  92. }
  93. File downloadedFile = newTempFile();
  94. LOGGER.debug("Download plugin '{}' to '{}'", plugin.key, downloadedFile);
  95. try (WsResponse response = wsClient.call(request)) {
  96. Optional<String> expectedMd5 = response.header(MD5_HEADER);
  97. if (!expectedMd5.isPresent()) {
  98. throw new IllegalStateException(format(
  99. "Fail to download plugin [%s]. Request to %s did not return header %s", plugin.key, response.requestUrl(), MD5_HEADER));
  100. }
  101. downloadBinaryTo(plugin, downloadedFile, response);
  102. // verify integrity
  103. String effectiveTempMd5 = computeMd5(downloadedFile);
  104. if (!expectedMd5.get().equals(effectiveTempMd5)) {
  105. throw new IllegalStateException(format(
  106. "Fail to download plugin [%s]. File %s was expected to have checksum %s but had %s", plugin.key, downloadedFile, expectedMd5.get(), effectiveTempMd5));
  107. }
  108. // un-compress if needed
  109. String cacheMd5;
  110. File tempJar;
  111. Optional<String> compression = response.header(COMPRESSION_HEADER);
  112. if (compression.isPresent() && PACK200.equals(compression.get())) {
  113. tempJar = unpack200(plugin.key, downloadedFile);
  114. cacheMd5 = response.header(UNCOMPRESSED_MD5_HEADER).orElseThrow(() -> new IllegalStateException(format(
  115. "Fail to download plugin [%s]. Request to %s did not return header %s.", plugin.key, response.requestUrl(), UNCOMPRESSED_MD5_HEADER)));
  116. } else {
  117. tempJar = downloadedFile;
  118. cacheMd5 = expectedMd5.get();
  119. }
  120. // put in cache
  121. File jarInCache = jarInCache(plugin.key, cacheMd5);
  122. mkdir(jarInCache.getParentFile());
  123. moveFile(tempJar, jarInCache);
  124. return Optional.of(jarInCache);
  125. } catch (HttpException e) {
  126. if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
  127. // Plugin was listed but not longer available. It has probably been
  128. // uninstalled.
  129. return Optional.empty();
  130. }
  131. // not 2xx nor 404
  132. throw new IllegalStateException(format("Fail to download plugin [%s]. Request to %s returned code %d.", plugin.key, e.url(), e.code()));
  133. }
  134. }
  135. private static void downloadBinaryTo(InstalledPlugin plugin, File downloadedFile, WsResponse response) {
  136. try (InputStream stream = response.contentStream()) {
  137. FileUtils.copyInputStreamToFile(stream, downloadedFile);
  138. } catch (IOException e) {
  139. throw new IllegalStateException(format("Fail to download plugin [%s] into %s", plugin.key, downloadedFile), e);
  140. }
  141. }
  142. private File jarInCache(String pluginKey, String hash) {
  143. File hashDir = new File(cacheDir, hash);
  144. File file = new File(hashDir, format("sonar-%s-plugin.jar", pluginKey));
  145. if (!file.getParentFile().toPath().equals(hashDir.toPath())) {
  146. // vulnerability - attempt to create a file outside the cache directory
  147. throw new IllegalStateException(format("Fail to download plugin [%s]. Key is not valid.", pluginKey));
  148. }
  149. return file;
  150. }
  151. private File newTempFile() {
  152. try {
  153. return File.createTempFile("fileCache", null, tempDir);
  154. } catch (IOException e) {
  155. throw new IllegalStateException("Fail to create temp file in " + tempDir, e);
  156. }
  157. }
  158. private File unpack200(String pluginKey, File compressedFile) {
  159. LOGGER.debug("Unpacking plugin {}", pluginKey);
  160. File jar = newTempFile();
  161. try (InputStream input = new GZIPInputStream(new BufferedInputStream(FileUtils.openInputStream(compressedFile)));
  162. JarOutputStream output = new JarOutputStream(new BufferedOutputStream(FileUtils.openOutputStream(jar)))) {
  163. Pack200.newUnpacker().unpack(input, output);
  164. } catch (IOException e) {
  165. throw new IllegalStateException(format("Fail to download plugin [%s]. Pack200 error.", pluginKey), e);
  166. }
  167. return jar;
  168. }
  169. private static String computeMd5(File file) {
  170. try (InputStream fis = new BufferedInputStream(FileUtils.openInputStream(file))) {
  171. return DigestUtils.md5Hex(fis);
  172. } catch (IOException e) {
  173. throw new IllegalStateException("Fail to compute md5 of " + file, e);
  174. }
  175. }
  176. private static void moveFile(File sourceFile, File targetFile) {
  177. boolean rename = sourceFile.renameTo(targetFile);
  178. // Check if the file was cached by another process during download
  179. if (!rename && !targetFile.exists()) {
  180. LOGGER.warn("Unable to rename {} to {}", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
  181. LOGGER.warn("A copy/delete will be tempted but with no guarantee of atomicity");
  182. try {
  183. Files.move(sourceFile.toPath(), targetFile.toPath());
  184. } catch (IOException e) {
  185. throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
  186. }
  187. }
  188. }
  189. private static void mkdir(File dir) {
  190. try {
  191. Files.createDirectories(dir.toPath());
  192. } catch (IOException e) {
  193. throw new IllegalStateException("Fail to create cache directory: " + dir, e);
  194. }
  195. }
  196. private static File mkdir(File dir, String debugTitle) {
  197. if (!dir.isDirectory() || !dir.exists()) {
  198. LOGGER.debug("Create : {}", dir.getAbsolutePath());
  199. try {
  200. Files.createDirectories(dir.toPath());
  201. } catch (IOException e) {
  202. throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
  203. }
  204. }
  205. return dir;
  206. }
  207. private static File locateHomeDir(Configuration configuration) {
  208. return Stream.of(
  209. configuration.get("sonar.userHome").orElse(null),
  210. System.getenv("SONAR_USER_HOME"),
  211. System.getProperty("user.home") + File.separator + ".sonar")
  212. .filter(Objects::nonNull)
  213. .findFirst()
  214. .map(File::new)
  215. .get();
  216. }
  217. }