From f12129986e444197716d4b3dc451937e1ee07c78 Mon Sep 17 00:00:00 2001 From: onebeastchris Date: Fri, 22 Dec 2023 14:39:47 +0100 Subject: [PATCH] More robust downloading/caching - better exception catching - proper error handling - caching of packs if size, etag, and last modified are the same --- .../geyser/network/UpstreamPacketHandler.java | 4 +- .../geyser/pack/url/GeyserUrlPackCodec.java | 2 +- .../registry/loader/ResourcePackLoader.java | 9 +-- .../org/geysermc/geyser/util/WebUtils.java | 65 ++++++++++++++++--- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 8459e9f16..8d8a44118 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -284,10 +284,10 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { // Ensure we don't a. spam console, and b. spam download/check requests if (!brokenResourcePacks.contains(packet.getPackId())) { brokenResourcePacks.add(packet.getPackId()); - GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded!" + + GeyserImpl.getInstance().getLogger().warning("Received a request for a remote pack that the client should have already downloaded! " + "Is the pack at the URL " + urlPackCodec.url() + " still available?"); // not actually interested in using the download, but this does all the checks we need - ResourcePackLoader.downloadPack(urlPackCodec.url()); + ResourcePackLoader.downloadPack(urlPackCodec.url(), true); } } diff --git a/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java index f1bb8eb3a..224ed4920 100644 --- a/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java +++ b/core/src/main/java/org/geysermc/geyser/pack/url/GeyserUrlPackCodec.java @@ -81,7 +81,7 @@ public class GeyserUrlPackCodec extends UrlPackCodec { public ResourcePack create() { if (this.fallback == null) { try { - final Path downloadedPack = ResourcePackLoader.downloadPack(url).whenComplete((pack, throwable) -> { + final Path downloadedPack = ResourcePackLoader.downloadPack(url, false).whenComplete((pack, throwable) -> { if (throwable != null) { GeyserImpl.getInstance().getLogger().error("Failed to download pack from " + url, throwable); if (GeyserImpl.getInstance().getConfig().isDebugMode()) { diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java index 6680c3825..e41ae7608 100644 --- a/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java +++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ResourcePackLoader.java @@ -200,7 +200,7 @@ public class ResourcePackLoader implements RegistryLoader loadRemotePacks() { + private Map loadRemotePacks() { final Path cachedCdnPacksDirectory = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); // Download CDN packs to get the pack uuid's @@ -231,11 +231,8 @@ public class ResourcePackLoader implements RegistryLoader downloadPack(String url) throws IllegalArgumentException { - - //TODO check if our cache pack is fine (size, url hash; head req) - - return WebUtils.checkUrlAndDownloadRemotePack(url).whenCompleteAsync((cachedPath, throwable) -> { + public static CompletableFuture<@Nullable Path> downloadPack(String url, boolean force) throws IllegalArgumentException { + return WebUtils.checkUrlAndDownloadRemotePack(url, force).whenCompleteAsync((cachedPath, throwable) -> { if (cachedPath == null) { // already warned about in WebUtils return; diff --git a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java index 487b9b198..46fc160af 100644 --- a/core/src/main/java/org/geysermc/geyser/util/WebUtils.java +++ b/core/src/main/java/org/geysermc/geyser/util/WebUtils.java @@ -34,6 +34,7 @@ import javax.naming.directory.InitialDirContext; import java.io.*; import java.net.HttpURLConnection; import java.net.MalformedURLException; +import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -41,11 +42,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; public class WebUtils { + private static final Path REMOTE_PACK_CACHE = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs"); + /** * Makes a web request to the given URL and returns the body as a string * @@ -103,13 +108,25 @@ public class WebUtils { * If it is, it will download the pack file and return a path to it * * @param url The URL to check + * @param force If true, the pack will be downloaded even if it is cached * @return Path to the downloaded pack file */ - public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url) { + public static CompletableFuture<@Nullable Path> checkUrlAndDownloadRemotePack(String url, boolean force) { return CompletableFuture.supplyAsync(() -> { try { HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); - con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().toString() + "/" + GeyserImpl.VERSION); + + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + con.setRequestProperty("User-Agent", "Geyser-" + GeyserImpl.getInstance().getPlatformType().platformName() + "/" + GeyserImpl.VERSION); + con.setInstanceFollowRedirects(false); // TODO verify + + int responseCode = con.getResponseCode(); + if (responseCode >= 400) { + GeyserImpl.getInstance().getLogger().error(String.format("Invalid response code from remote pack URL: %s (code: %d)", url, responseCode)); + return null; + } + int size = con.getContentLength(); String type = con.getContentType(); @@ -123,21 +140,49 @@ public class WebUtils { return null; } - InputStream in = con.getInputStream(); - Path fileLocation = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("remote_packs").resolve(url.hashCode() + ".zip"); - Files.copy(in, fileLocation, StandardCopyOption.REPLACE_EXISTING); + Path packLocation = REMOTE_PACK_CACHE.resolve(url.hashCode() + ".zip"); + Path packMetadata = packLocation.resolveSibling(url.hashCode() + ".metadata"); - if (Files.size(fileLocation) != size) { - GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(fileLocation) + " bytes, expected " + size + " bytes"); - Files.delete(fileLocation); + if (Files.exists(packLocation) && Files.exists(packMetadata)) { + try { + List metadataLines = Files.readAllLines(packMetadata, StandardCharsets.UTF_8); + int cachedSize = Integer.parseInt(metadataLines.get(0)); + String cachedEtag = metadataLines.get(1); + long cachedLastModified = Long.parseLong(metadataLines.get(2)); + + if (cachedSize == size && cachedEtag.equals(con.getHeaderField("ETag")) && cachedLastModified == con.getLastModified()) { + GeyserImpl.getInstance().getLogger().debug("Using cached pack for " + url); + return packLocation; + } + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to read cached pack metadata: " + e.getMessage()); + } + } + + InputStream in = con.getInputStream(); + Files.copy(in, packLocation, StandardCopyOption.REPLACE_EXISTING); + + if (Files.size(packLocation) != size) { + GeyserImpl.getInstance().getLogger().error("Downloaded pack has " + Files.size(packLocation) + " bytes, expected " + size + " bytes"); + Files.delete(packLocation); return null; } - return fileLocation; + try { + Files.write(packMetadata, Arrays.asList(String.valueOf(size), con.getHeaderField("ETag"), String.valueOf(con.getLastModified()))); + } catch (IOException e) { + GeyserImpl.getInstance().getLogger().error("Failed to write cached pack metadata: " + e.getMessage()); + } + + return packLocation; } catch (MalformedURLException e) { throw new IllegalArgumentException("Malformed URL: " + url); + } catch (SocketTimeoutException | ConnectException e) { + GeyserImpl.getInstance().getLogger().error("Unable to reach URL: " + url + " (" + e.getMessage() + ")"); + return null; } catch (IOException e) { - throw new RuntimeException("Unable to download and save remote resource pack from: " + url + ")"); + e.printStackTrace(); // TODO yeeeeeeeet + throw new RuntimeException("Unable to download and save remote resource pack from: " + url + " (" + e.getMessage() + ")"); } }); }