From fb5b89a3f25422e7b8bb7830eb947f66b3987d5b Mon Sep 17 00:00:00 2001 From: comp500 Date: Tue, 18 Feb 2020 16:48:29 +0000 Subject: [PATCH] Improve downloading: save to .tmp then rename, check sha1 hash of maven jars --- .../meta/FabricAutoconfHandler.java | 4 +-- .../jumploader/meta/MinecraftDownloadApi.java | 8 +++--- .../infra/jumploader/resources/MavenJar.java | 25 ++++++++++++++++++- .../jumploader/resources/MinecraftJar.java | 4 +-- .../jumploader/resources/ResolvableJar.java | 10 +++++++- .../jumploader/util/InvalidHashException.java | 11 ++++++++ .../{RequestJson.java => RequestUtils.java} | 20 +++++++++++++-- .../util/SHA1HashingInputStream.java | 8 ------ 8 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 src/main/java/link/infra/jumploader/util/InvalidHashException.java rename src/main/java/link/infra/jumploader/util/{RequestJson.java => RequestUtils.java} (67%) diff --git a/src/main/java/link/infra/jumploader/meta/FabricAutoconfHandler.java b/src/main/java/link/infra/jumploader/meta/FabricAutoconfHandler.java index a8dc122..e4ac101 100644 --- a/src/main/java/link/infra/jumploader/meta/FabricAutoconfHandler.java +++ b/src/main/java/link/infra/jumploader/meta/FabricAutoconfHandler.java @@ -8,7 +8,7 @@ import link.infra.jumploader.resources.MavenJar; import link.infra.jumploader.resources.MinecraftJar; import link.infra.jumploader.resources.ParsedArguments; -import link.infra.jumploader.util.RequestJson; +import link.infra.jumploader.util.RequestUtils; import java.io.IOException; import java.net.URI; @@ -34,7 +34,7 @@ public void updateConfig(ConfigFile configFile, ParsedArguments args, Environmen String side = configFile.autoconfig.side != null ? configFile.autoconfig.side : args.inferredSide; try { URL loaderJsonUrl = new URI("https", "meta.fabricmc.net", "/v2/versions/loader/" + args.mcVersion, null).toURL(); - JsonArray manifestData = RequestJson.getJson(loaderJsonUrl).getAsJsonArray(); + JsonArray manifestData = RequestUtils.getJson(loaderJsonUrl).getAsJsonArray(); if (manifestData.size() == 0) { throw new RuntimeException("Failed to update configuration: no Fabric versions available!"); } diff --git a/src/main/java/link/infra/jumploader/meta/MinecraftDownloadApi.java b/src/main/java/link/infra/jumploader/meta/MinecraftDownloadApi.java index b327fb1..7764bcf 100644 --- a/src/main/java/link/infra/jumploader/meta/MinecraftDownloadApi.java +++ b/src/main/java/link/infra/jumploader/meta/MinecraftDownloadApi.java @@ -4,7 +4,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import link.infra.jumploader.util.RequestJson; +import link.infra.jumploader.util.RequestUtils; import java.io.IOException; import java.net.URL; @@ -21,14 +21,14 @@ private LoginValidationException() { public static void validate(String accessToken) throws IOException { JsonObject req = new JsonObject(); req.addProperty("accessToken", accessToken); - int resCode = RequestJson.postJsonForResCode(new URL("https://authserver.mojang.com/validate"), req); + int resCode = RequestUtils.postJsonForResCode(new URL("https://authserver.mojang.com/validate"), req); if (resCode != 204 && resCode != 200) { throw new LoginValidationException(); } } public static URL retrieveVersionMetaUrl(String minecraftVersion) throws IOException { - JsonObject manifestData = RequestJson.getJson(new URL("https://launchermeta.mojang.com/mc/game/version_manifest.json")).getAsJsonObject(); + JsonObject manifestData = RequestUtils.getJson(new URL("https://launchermeta.mojang.com/mc/game/version_manifest.json")).getAsJsonObject(); JsonArray versions = manifestData.getAsJsonArray("versions"); for (JsonElement version : versions) { JsonObject versionObj = version.getAsJsonObject(); @@ -47,7 +47,7 @@ private DownloadDetails() {} } public static DownloadDetails retrieveDownloadDetails(URL versionMetaUrl, String downloadType) throws IOException { - JsonObject manifestData = RequestJson.getJson(versionMetaUrl).getAsJsonObject(); + JsonObject manifestData = RequestUtils.getJson(versionMetaUrl).getAsJsonObject(); JsonObject downloads = manifestData.getAsJsonObject("downloads"); JsonObject download = downloads.getAsJsonObject(downloadType); Gson gson = new Gson(); diff --git a/src/main/java/link/infra/jumploader/resources/MavenJar.java b/src/main/java/link/infra/jumploader/resources/MavenJar.java index 0fe8ced..989699a 100644 --- a/src/main/java/link/infra/jumploader/resources/MavenJar.java +++ b/src/main/java/link/infra/jumploader/resources/MavenJar.java @@ -1,9 +1,15 @@ package link.infra.jumploader.resources; import link.infra.jumploader.DownloadWorkerManager; +import link.infra.jumploader.util.InvalidHashException; +import link.infra.jumploader.util.RequestUtils; +import link.infra.jumploader.util.SHA1HashingInputStream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -14,6 +20,8 @@ public class MavenJar extends ResolvableJar { public String mavenPath; public String repoUrl; + private static final Logger LOGGER = LogManager.getLogger(); + public MavenJar(EnvironmentDiscoverer.JarStorageLocation jarStorage) { super(jarStorage); } @@ -33,11 +41,26 @@ public URL resolveLocal() throws FileNotFoundException { throw new FileNotFoundException(); } + private static URL getSha1Url(URL downloadUrl) throws MalformedURLException { + return new URL(downloadUrl.getProtocol(), downloadUrl.getHost(), downloadUrl.getPort(), downloadUrl.getFile() + ".sha1"); + } + @Override public URL resolveRemote(DownloadWorkerManager.TaskStatus status, ParsedArguments args) throws URISyntaxException, IOException { URL downloadUrl = resolveMavenPath(new URI(repoUrl), mavenPath).toURL(); Path jarPath = jarStorage.getMavenJar(mavenPath); - downloadFile(status, downloadUrl, jarPath, is -> is); + + String sha1Hash = RequestUtils.getString(getSha1Url(downloadUrl)); + + try { + downloadFile(status, downloadUrl, jarPath, SHA1HashingInputStream.transformer(sha1Hash)); + } catch (InvalidHashException e) { + // TODO: better UI for this? + LOGGER.error("Maven JAR hash mismatch for " + downloadUrl); + LOGGER.error("Expected: " + sha1Hash); + LOGGER.error("Found: " + e.hashFound); + throw new RuntimeException("Failed to download Maven JAR!"); + } return pathToURL(jarPath); } diff --git a/src/main/java/link/infra/jumploader/resources/MinecraftJar.java b/src/main/java/link/infra/jumploader/resources/MinecraftJar.java index 33ed609..b2e3d77 100644 --- a/src/main/java/link/infra/jumploader/resources/MinecraftJar.java +++ b/src/main/java/link/infra/jumploader/resources/MinecraftJar.java @@ -2,6 +2,7 @@ import link.infra.jumploader.DownloadWorkerManager; import link.infra.jumploader.meta.MinecraftDownloadApi; +import link.infra.jumploader.util.InvalidHashException; import link.infra.jumploader.util.SHA1HashingInputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,12 +50,11 @@ public URL resolveRemote(DownloadWorkerManager.TaskStatus status, ParsedArgument Path gameJarPath = jarStorage.getGameJar(gameVersion, downloadType); try { downloadFile(status, details.url, gameJarPath, SHA1HashingInputStream.transformer(details.sha1)); - } catch (SHA1HashingInputStream.InvalidHashException e) { + } catch (InvalidHashException e) { // TODO: better UI for this? LOGGER.error("Minecraft JAR hash mismatch for " + details.url); LOGGER.error("Expected: " + details.sha1); LOGGER.error("Found: " + e.hashFound); - Files.deleteIfExists(gameJarPath); throw new RuntimeException("Failed to download Minecraft JAR!"); } return pathToURL(gameJarPath); diff --git a/src/main/java/link/infra/jumploader/resources/ResolvableJar.java b/src/main/java/link/infra/jumploader/resources/ResolvableJar.java index 1748679..1d95157 100644 --- a/src/main/java/link/infra/jumploader/resources/ResolvableJar.java +++ b/src/main/java/link/infra/jumploader/resources/ResolvableJar.java @@ -2,6 +2,7 @@ import link.infra.jumploader.DownloadWorkerManager; import link.infra.jumploader.Jumploader; +import link.infra.jumploader.util.InvalidHashException; import javax.annotation.Nonnull; import java.io.FileNotFoundException; @@ -14,6 +15,7 @@ import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.function.Function; public abstract class ResolvableJar { @@ -108,9 +110,15 @@ protected static void downloadFile(DownloadWorkerManager.TaskStatus status, URL conn.setRequestProperty("Accept", "application/octet-stream"); int contentLength = conn.getContentLength(); + Path destPathTemp = destPath.resolveSibling(destPath.getFileName() + ".tmp"); try (InputStream res = bytesTransformer.apply(conn.getInputStream()); BytesReportingInputStream bris = new BytesReportingInputStream(res, status, contentLength)) { - Files.copy(bris, destPath); + Files.copy(bris, destPathTemp, StandardCopyOption.REPLACE_EXISTING); + Files.move(destPathTemp, destPath); + } catch (InvalidHashException e) { + Files.deleteIfExists(destPath); + Files.deleteIfExists(destPathTemp); + throw e; } } diff --git a/src/main/java/link/infra/jumploader/util/InvalidHashException.java b/src/main/java/link/infra/jumploader/util/InvalidHashException.java new file mode 100644 index 0000000..4cfc465 --- /dev/null +++ b/src/main/java/link/infra/jumploader/util/InvalidHashException.java @@ -0,0 +1,11 @@ +package link.infra.jumploader.util; + +import java.io.IOException; + +public class InvalidHashException extends IOException { + public final String hashFound; + + public InvalidHashException(String hashFound) { + this.hashFound = hashFound; + } +} diff --git a/src/main/java/link/infra/jumploader/util/RequestJson.java b/src/main/java/link/infra/jumploader/util/RequestUtils.java similarity index 67% rename from src/main/java/link/infra/jumploader/util/RequestJson.java rename to src/main/java/link/infra/jumploader/util/RequestUtils.java index a81d8f9..34e3dc5 100644 --- a/src/main/java/link/infra/jumploader/util/RequestJson.java +++ b/src/main/java/link/infra/jumploader/util/RequestUtils.java @@ -9,9 +9,10 @@ import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.StandardCharsets; -public class RequestJson { - private RequestJson() {} +public class RequestUtils { + private RequestUtils() {} public static JsonElement getJson(URL requestUrl) throws IOException { URLConnection conn = requestUrl.openConnection(); @@ -39,4 +40,19 @@ public static int postJsonForResCode(URL requestUrl, JsonElement requestData) th return conn.getResponseCode(); } + public static String getString(URL requestUrl) throws IOException { + URLConnection conn = requestUrl.openConnection(); + conn.setRequestProperty("User-Agent", Jumploader.USER_AGENT); + conn.setRequestProperty("Accept", "text/plain"); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + try (InputStream res = conn.getInputStream()) { + int n; + while ((n = res.read(buffer, 0, 1024)) != -1) { + baos.write(buffer, 0, n); + } + } + return baos.toString(StandardCharsets.UTF_8.name()); + } } diff --git a/src/main/java/link/infra/jumploader/util/SHA1HashingInputStream.java b/src/main/java/link/infra/jumploader/util/SHA1HashingInputStream.java index 86591ab..e1ef7ff 100644 --- a/src/main/java/link/infra/jumploader/util/SHA1HashingInputStream.java +++ b/src/main/java/link/infra/jumploader/util/SHA1HashingInputStream.java @@ -16,14 +16,6 @@ public class SHA1HashingInputStream extends FilterInputStream { private final byte[] compareToHash; private final MessageDigest digest; - public static class InvalidHashException extends IOException { - public final String hashFound; - - public InvalidHashException(String hashFound) { - this.hashFound = hashFound; - } - } - public static Function transformer(String compareToHash) { return inputStream -> new SHA1HashingInputStream(inputStream, compareToHash); }