Skip to content

Commit

Permalink
feat: check checksums when downloading tools from json (#230)
Browse files Browse the repository at this point in the history
* feat: check checksums when downloading tools from json

Signed-off-by: Stephane Bouchet <[email protected]>

* fix after reviews

Signed-off-by: Stephane Bouchet <[email protected]>

---------

Signed-off-by: Stephane Bouchet <[email protected]>
  • Loading branch information
sbouchet authored Jun 21, 2024
1 parent 1ecbc93 commit 43e00da
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"
* }
* }
* }
Expand Down Expand Up @@ -138,7 +144,7 @@ private CompletableFuture<ToolInstance> 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));
}
Expand All @@ -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<ToolInstance> result) {
private CompletableFuture<ToolInstance> downloadInBackground(String toolName, ToolsConfig.Platform platform, Path path, String cmd, ToolsConfig.Tool tool, String version, String checksum) {
CompletableFuture<ToolInstance> 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<ToolInstance> result) {
private void downloadInBackgroundManager(String toolName, ToolsConfig.Platform platform, Path path, String cmd, String checksum, CompletableFuture<ToolInstance> result) {
final Path dlFilePath = path.resolveSibling(platform.getDlFileName());
ProgressManager.getInstance().run(new Task.Backgroundable(null, "Downloading " + toolName, false) {
@Override
Expand All @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,22 @@ public Map<String, Platform> getPlatforms() {
return platforms;
}

public void setPlatforms(Map<String, Platform> 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;
}
Expand All @@ -81,17 +61,14 @@ public void setBaseDir(String baseDir) {
public boolean isSilentMode() {
return silentMode;
}

public void setSilentMode(boolean silentMode) {
this.silentMode = silentMode;
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
public static class Platform {
private URL url;
private String cmdFileName;
private String dlFileName;
private String sha256;

public URL getUrl() {
return url;
Expand All @@ -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<String, Tool> tools = new HashMap<>();
Expand All @@ -124,7 +98,4 @@ public Map<String, Tool> getTools() {
return tools;
}

public void setTools(Map<String, Tool> tools) {
this.tools = tools;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,59 +28,59 @@ 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);
assertEquals("0.5.0", tool.getVersion());
}

@Test
public void verifyThatConfigReturnsVersionCmd() throws IOException {
public void verifyThatConfigReturnsVersionCmd() {
assertNotNull(config);
ToolsConfig.Tool tool = config.getTools().get("tkn");
assertNotNull(tool);
assertEquals("version", tool.getVersionCmd());
}

@Test
public void verifyThatConfigReturnsVersionExtractRegExp() throws IOException {
public void verifyThatConfigReturnsVersionExtractRegExp() {
assertNotNull(config);
ToolsConfig.Tool tool = config.getTools().get("tkn");
assertNotNull(tool);
assertEquals("Client version: (\\d+[\\.\\d+]*)\\s.*", tool.getVersionExtractRegExp());
}

@Test
public void verifyThatConfigReturnsVersionMatchRegExp() throws IOException {
public void verifyThatConfigReturnsVersionMatchRegExp() {
assertNotNull(config);
ToolsConfig.Tool tool = config.getTools().get("tkn");
assertNotNull(tool);
assertEquals("0\\..*", tool.getVersionMatchRegExpr());
}

@Test
public void verifyThatConfigReturnsBaseDir() throws IOException {
public void verifyThatConfigReturnsBaseDir() {
assertNotNull(config);
ToolsConfig.Tool tool = config.getTools().get("tkn");
assertNotNull(tool);
assertEquals("$HOME/.tekton", tool.getBaseDir());
}

@Test
public void verifyThatConfigReturnsPlatforms() throws IOException {
public void verifyThatConfigReturnsPlatforms() {
assertNotNull(config);
ToolsConfig.Tool tool = config.getTools().get("tkn");
assertNotNull(tool);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -109,12 +109,23 @@ 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);
assertNotNull(tool.getPlatforms());
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());
}
}
Loading

0 comments on commit 43e00da

Please sign in to comment.