diff --git a/src/main/java/com/redhat/devtools/intellij/common/utils/DownloadHelper.java b/src/main/java/com/redhat/devtools/intellij/common/utils/DownloadHelper.java index 1ce360f..ad06a65 100644 --- a/src/main/java/com/redhat/devtools/intellij/common/utils/DownloadHelper.java +++ b/src/main/java/com/redhat/devtools/intellij/common/utils/DownloadHelper.java @@ -36,9 +36,12 @@ import java.io.OutputStream; import java.io.StringReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -96,19 +99,22 @@ public static DownloadHelper getInstance() { * "silentMode": true, //if the download needs to be started automatically without user input * "platforms": { * "win": { - * "url": "https://tool.com/tool/v1.0.0/odo-windows-amd64.exe.tar.gz", + * "url": "https://tool.com/tool/v1.0.0/tool-windows-amd64.exe.tar.gz", * "cmdFileName": "tool.exe", * "dlFileName": "tool-windows-amd64.exe.gz" + * "sha256": "123456789" * }, * "osx": { - * "url": "https://tool.com/tool/v1.0.0/odo-darwin-amd64.tar.gz", + * "url": "https://tool.com/tool/v1.0.0/tool-darwin-amd64.tar.gz", * "cmdFileName": "tool", * "dlFileName": "tool-darwin-amd64.gz" + * "sha256": "123456789" * }, * "lnx": { - * "url": "https://tool.com/tool/v1.0.0/odo-linux-amd64.tar.gz", - * "cmdFileName": "odo", - * "dlFileName": "odo-linux-amd64.gz" + * "url": "https://tool.com/tool/v1.0.0/tool-linux-amd64.tar.gz", + * "cmdFileName": "tool", + * "dlFileName": "tool-linux-amd64.gz" + * "sha256": "123456789" * } * } * } @@ -138,7 +144,7 @@ private CompletableFuture downloadIfRequiredAsyncInner(String tool Path path = Paths.get(tool.getBaseDir().replace("$HOME", CommonConstants.HOME_FOLDER), "cache", tool.getVersion(), command); final String cmd = path.toString(); if (!Files.exists(path)) { - downloadInBackground(toolName, platform, path, cmd, tool, version, result); + result = downloadInBackground(toolName, platform, path, cmd, tool, version, platform.getSha256()); } else { result.complete(new ToolInstance(cmd, false)); } @@ -158,21 +164,23 @@ private ToolsConfig.Platform getPlatformBasedOnOs(ToolsConfig.Tool tool) { } - private void downloadInBackground(String toolName, ToolsConfig.Platform platform, Path path, String cmd, ToolsConfig.Tool tool, String version, CompletableFuture result) { + private CompletableFuture downloadInBackground(String toolName, ToolsConfig.Platform platform, Path path, String cmd, ToolsConfig.Tool tool, String version, String checksum) { + CompletableFuture result = new CompletableFuture<>(); if (ApplicationManager.getApplication().isUnitTestMode()) { - downloadInBackgroundManager(toolName, platform, path, cmd, result); + downloadInBackgroundManager(toolName, platform, path, cmd, checksum, result); } else { ApplicationManager.getApplication().invokeLater(() -> { if (tool.isSilentMode() || isDownloadAllowed(toolName, version, tool.getVersion())) { - downloadInBackgroundManager(toolName, platform, path, cmd, result); + downloadInBackgroundManager(toolName, platform, path, cmd, checksum, result); } else { result.complete(new ToolInstance(platform.getCmdFileName(), false)); } }); } + return result; } - private void downloadInBackgroundManager(String toolName, ToolsConfig.Platform platform, Path path, String cmd, CompletableFuture result) { + private void downloadInBackgroundManager(String toolName, ToolsConfig.Platform platform, Path path, String cmd, String checksum, CompletableFuture result) { final Path dlFilePath = path.resolveSibling(platform.getDlFileName()); ProgressManager.getInstance().run(new Task.Backgroundable(null, "Downloading " + toolName, false) { @Override @@ -181,6 +189,9 @@ public void run(@NotNull ProgressIndicator progressIndicator) { HttpRequests.request(platform.getUrl().toString()).useProxy(true).connect(request -> { downloadFile(request.getInputStream(), dlFilePath, progressIndicator, request.getConnection().getContentLength()); uncompress(dlFilePath, path); + if (checksum != null && !verify(dlFilePath, checksum)){ + throw new IOException("Failed to verify checksum for " + platform.getDlFileName()); + } return cmd; }); } catch (IOException e) { @@ -266,15 +277,6 @@ private static void downloadFile(InputStream input, Path dlFileName, ProgressInd } } - private InputStream checkTar(InputStream stream) throws IOException { - TarArchiveInputStream tarStream = new TarArchiveInputStream(stream); - if (tarStream.getNextTarEntry() != null) { - return tarStream; - } else { - throw new IOException("No TAR entry found in " + stream); - } - } - private InputStream mapStream(String filename, InputStream input) { String extension; while (((extension = FilenameUtils.getExtension(filename)) != null) && MAPPERS.containsKey(extension)) { @@ -317,6 +319,16 @@ private void save(InputStream source, Path destination, long length) throws IOEx destination.toFile().setExecutable(true); } + private boolean verify(Path path, String checksum) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(Files.readAllBytes(path)); + return MessageDigest.isEqual(hash, checksum.getBytes(StandardCharsets.UTF_8)); + } catch (NoSuchAlgorithmException e) { + throw new IOException("Could not verify checksum for file " + path.toString(), e); + } + } + public static class ToolInstance { private final String command; private final boolean isDownloaded; diff --git a/src/main/java/com/redhat/devtools/intellij/common/utils/ToolsConfig.java b/src/main/java/com/redhat/devtools/intellij/common/utils/ToolsConfig.java index 18dfcbf..7ba14ec 100644 --- a/src/main/java/com/redhat/devtools/intellij/common/utils/ToolsConfig.java +++ b/src/main/java/com/redhat/devtools/intellij/common/utils/ToolsConfig.java @@ -34,42 +34,22 @@ public Map getPlatforms() { return platforms; } - public void setPlatforms(Map platforms) { - this.platforms = platforms; - } - public String getVersion() { return version; } - public void setVersion(String version) { - this.version = version; - } - public String getVersionCmd() { return versionCmd; } - public void setVersionCmd(String versionCmd) { - this.versionCmd = versionCmd; - } - public String getVersionExtractRegExp() { return versionExtractRegExp; } - public void setVersionExtractRegExp(String versionExtractRegExp) { - this.versionExtractRegExp = versionExtractRegExp; - } - public String getVersionMatchRegExpr() { return versionMatchRegExpr; } - public void setVersionMatchRegExpr(String versionMatchRegExpr) { - this.versionMatchRegExpr = versionMatchRegExpr; - } - public String getBaseDir() { return baseDir; } @@ -81,10 +61,6 @@ public void setBaseDir(String baseDir) { public boolean isSilentMode() { return silentMode; } - - public void setSilentMode(boolean silentMode) { - this.silentMode = silentMode; - } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -92,6 +68,7 @@ public static class Platform { private URL url; private String cmdFileName; private String dlFileName; + private String sha256; public URL getUrl() { return url; @@ -105,17 +82,14 @@ public String getCmdFileName() { return cmdFileName; } - public void setCmdFileName(String cmdFileName) { - this.cmdFileName = cmdFileName; - } - public String getDlFileName() { return dlFileName; } - public void setDlFileName(String dlFileName) { - this.dlFileName = dlFileName; + public String getSha256() { + return sha256; } + } private Map tools = new HashMap<>(); @@ -124,7 +98,4 @@ public Map getTools() { return tools; } - public void setTools(Map tools) { - this.tools = tools; - } } diff --git a/src/test/java/com/redhat/devtools/intellij/common/utils/DownloadHelperTest.java b/src/test/java/com/redhat/devtools/intellij/common/utils/DownloadHelperTest.java index fb06e1f..85fee6a 100644 --- a/src/test/java/com/redhat/devtools/intellij/common/utils/DownloadHelperTest.java +++ b/src/test/java/com/redhat/devtools/intellij/common/utils/DownloadHelperTest.java @@ -1,3 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ package com.redhat.devtools.intellij.common.utils; import com.intellij.openapi.ui.TestDialog; @@ -6,6 +16,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Paths; public class DownloadHelperTest extends LightPlatformTestCase { private TestDialog previous; @@ -39,11 +50,28 @@ public void testThatTarGZIsDownloaded() throws IOException { assertEquals(17, new File(toolInstance.getCommand()).length()); } - public void testThatPlainFileDownloaded() throws IOException{ + public void testThatPlainFileDownloaded() throws IOException { DownloadHelper.ToolInstance toolInstance = DownloadHelper.getInstance().downloadIfRequired("kn", DownloadHelperTest.class.getResource("/knative-test.json")); assertNotNull(toolInstance); assertNotNull(toolInstance.getCommand()); assertEquals("." + File.separatorChar + "cache" + File.separatorChar + "0.5.0" + File.separatorChar + "tkn", toolInstance.getCommand()); assertEquals(17, new File(toolInstance.getCommand()).length()); } + + public void testThatChecksumIsValidForDownloadedTool() throws IOException { + DownloadHelper.ToolInstance toolInstance = DownloadHelper.getInstance().downloadIfRequired("tkn", DownloadHelperTest.class.getResource("/tkn-test.json")); + assertNotNull(toolInstance); + assertNotNull(toolInstance.getCommand()); + FileUtils.deleteDirectory(Paths.get(toolInstance.getCommand()).toFile().getParentFile()); + } + + public void testThatChecksumIsInValidForDownloadedTool() { + try { + DownloadHelper.ToolInstance toolInstance = DownloadHelper.getInstance().downloadIfRequired("tkn", DownloadHelperTest.class.getResource("/tkn-test-invalid-checksum.json")); + FileUtils.deleteDirectory(Paths.get(toolInstance.getCommand()).toFile().getParentFile()); + fail("should raise exception"); + } catch (IOException e){ + assertTrue(e.getMessage().contains("Error while setting tool")); + } + } } diff --git a/src/test/java/com/redhat/devtools/intellij/common/utils/ToolsConfigTest.java b/src/test/java/com/redhat/devtools/intellij/common/utils/ToolsConfigTest.java index 9cd26f8..8b16e78 100644 --- a/src/test/java/com/redhat/devtools/intellij/common/utils/ToolsConfigTest.java +++ b/src/test/java/com/redhat/devtools/intellij/common/utils/ToolsConfigTest.java @@ -28,19 +28,19 @@ public static void init() throws IOException { } @Test - public void verifyThatConfigCanLoad() throws IOException { + public void verifyThatConfigCanLoad() { assertNotNull(config); } @Test - public void verifyThatConfigReturnsTools() throws IOException { + public void verifyThatConfigReturnsTools() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); } @Test - public void verifyThatConfigReturnsVersion() throws IOException { + public void verifyThatConfigReturnsVersion() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -48,7 +48,7 @@ public void verifyThatConfigReturnsVersion() throws IOException { } @Test - public void verifyThatConfigReturnsVersionCmd() throws IOException { + public void verifyThatConfigReturnsVersionCmd() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -56,7 +56,7 @@ public void verifyThatConfigReturnsVersionCmd() throws IOException { } @Test - public void verifyThatConfigReturnsVersionExtractRegExp() throws IOException { + public void verifyThatConfigReturnsVersionExtractRegExp() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -64,7 +64,7 @@ public void verifyThatConfigReturnsVersionExtractRegExp() throws IOException { } @Test - public void verifyThatConfigReturnsVersionMatchRegExp() throws IOException { + public void verifyThatConfigReturnsVersionMatchRegExp() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -72,7 +72,7 @@ public void verifyThatConfigReturnsVersionMatchRegExp() throws IOException { } @Test - public void verifyThatConfigReturnsBaseDir() throws IOException { + public void verifyThatConfigReturnsBaseDir() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -80,7 +80,7 @@ public void verifyThatConfigReturnsBaseDir() throws IOException { } @Test - public void verifyThatConfigReturnsPlatforms() throws IOException { + public void verifyThatConfigReturnsPlatforms() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -89,7 +89,7 @@ public void verifyThatConfigReturnsPlatforms() throws IOException { } @Test - public void verifyThatConfigReturnsWindowsPlatform() throws IOException { + public void verifyThatConfigReturnsWindowsPlatform() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -99,7 +99,7 @@ public void verifyThatConfigReturnsWindowsPlatform() throws IOException { } @Test - public void verifyThatConfigReturnsOSXPlatform() throws IOException { + public void verifyThatConfigReturnsOSXPlatform() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -109,7 +109,7 @@ public void verifyThatConfigReturnsOSXPlatform() throws IOException { } @Test - public void verifyThatConfigReturnsLinuxPlatform() throws IOException { + public void verifyThatConfigReturnsLinuxPlatform() { assertNotNull(config); ToolsConfig.Tool tool = config.getTools().get("tkn"); assertNotNull(tool); @@ -117,4 +117,15 @@ public void verifyThatConfigReturnsLinuxPlatform() throws IOException { assertFalse(tool.getPlatforms().isEmpty()); assertNotNull(tool.getPlatforms().get("lnx")); } + + @Test + public void verifyThatConfigReturnsCorrectChecksums() { + assertNotNull(config); + ToolsConfig.Tool tool = config.getTools().get("tkn"); + assertNotNull(tool); + assertNotNull(tool.getPlatforms()); + assertFalse(tool.getPlatforms().isEmpty()); + assertNotNull(tool.getPlatforms().get("win")); + assertEquals("50dfa941ccdbe63c112cb28af521f74f3d972cf06ba0092844a20197ddf31de5", tool.getPlatforms().get("win").getSha256()); + } } diff --git a/src/test/resources/tkn-test-invalid-checksum.json b/src/test/resources/tkn-test-invalid-checksum.json new file mode 100644 index 0000000..0027150 --- /dev/null +++ b/src/test/resources/tkn-test-invalid-checksum.json @@ -0,0 +1,33 @@ +{ + "tools": { + "tkn": { + "version": "0.5.0", + "versionCmd": "version", + "versionExtractRegExp": "Client version: (\\d+[\\.\\d+]*)\\s.*", + "versionMatchRegExpr": "0\\..*", + "baseDir": "$HOME/.tekton", + "silentMode": true, + "platforms": { + "win": { + "url": "https://github.com/tektoncd/cli/releases/download/v0.5.0/tkn_0.5.0_Windows_x86_64.zip", + "cmdFileName": "tkn.exe", + "dlFileName": "tkn_0.5.0_Windows_x86_64.zip", + "sha256": "74f3d972cf06ba0092844a20197ddf31de5" + }, + "osx": { + "url": "https://github.com/tektoncd/cli/releases/download/v0.5.0/tkn_0.5.0_Darwin_x86_64.tar.gz", + "cmdFileName": "tkn", + "dlFileName": "tkn_0.5.0_Darwin_x86_64.tar.gz", + "sha256": "0018c072d4df1365c7d2b01ebc29dd034745f78fb6ea1d5a28422a3ed784b42b" + }, + "lnx": { + "url": "https://github.com/tektoncd/cli/releases/download/v0.5.0/tkn_0.5.0_Linux_x86_64.tar.gz", + "cmdFileName": "tkn", + "dlFileName": "tkn_0.5.0_Linux_x86_64.tar.gz", + "sha256": "00000000000" + } + } + } + } +} + diff --git a/src/test/resources/tkn-test.json b/src/test/resources/tkn-test.json index 1f8bafe..2ba6df7 100644 --- a/src/test/resources/tkn-test.json +++ b/src/test/resources/tkn-test.json @@ -11,17 +11,20 @@ "win": { "url": "https://github.com/tektoncd/cli/releases/download/v0.5.0/tkn_0.5.0_Windows_x86_64.zip", "cmdFileName": "tkn.exe", - "dlFileName": "tkn_0.5.0_Windows_x86_64.zip" + "dlFileName": "tkn_0.5.0_Windows_x86_64.zip", + "sha256": "50dfa941ccdbe63c112cb28af521f74f3d972cf06ba0092844a20197ddf31de5" }, "osx": { "url": "https://github.com/tektoncd/cli/releases/download/v0.5.0/tkn_0.5.0_Darwin_x86_64.tar.gz", "cmdFileName": "tkn", - "dlFileName": "tkn_0.5.0_Darwin_x86_64.tar.gz" + "dlFileName": "tkn_0.5.0_Darwin_x86_64.tar.gz", + "sha256": "6818c072d4df1365c7d2b01ebc29dd034745f78fb6ea1d5a28422a3ed784b42b" }, "lnx": { "url": "https://github.com/tektoncd/cli/releases/download/v0.5.0/tkn_0.5.0_Linux_x86_64.tar.gz", "cmdFileName": "tkn", - "dlFileName": "tkn_0.5.0_Linux_x86_64.tar.gz" + "dlFileName": "tkn_0.5.0_Linux_x86_64.tar.gz", + "sha256": "24b9f16d28350eab730a04ba66f5adffbda9918b4895ac761f67e3310b0fecd9" } } }