From 3a7e77e9cfcd18471a01e7ffc8ffeb1e8474acc9 Mon Sep 17 00:00:00 2001 From: Tim203 Date: Fri, 24 Mar 2023 00:27:13 +0100 Subject: [PATCH] Initial work on loading libraries on runtime --- .../floodgate.dependency-hash.gradle.kts | 93 ++++++++++ core/build.gradle.kts | 1 + .../floodgate/core/FloodgatePlatform.java | 20 +++ .../floodgate/core/library/Library.java | 159 ++++++++++++++++++ .../core/library/LibraryManager.java | 72 ++++++++ .../floodgate/core/library/Repository.java | 74 ++++++++ .../classloader/LibraryClassLoader.java | 64 +++++++ .../core/library/info/DependencyInfo.java | 38 +++++ .../library/info/DependencyInfoLoader.java | 58 +++++++ .../floodgate/core/util/HttpClient.java | 27 ++- .../geysermc/floodgate/core/util/Utils.java | 43 +---- core/src/main/resources/dependencyInfo.txt | 15 ++ .../floodgate/core/util/Constants.java | 1 + 13 files changed, 622 insertions(+), 43 deletions(-) create mode 100644 build-logic/src/main/kotlin/floodgate.dependency-hash.gradle.kts create mode 100644 core/src/main/java/org/geysermc/floodgate/core/library/Library.java create mode 100644 core/src/main/java/org/geysermc/floodgate/core/library/LibraryManager.java create mode 100644 core/src/main/java/org/geysermc/floodgate/core/library/Repository.java create mode 100644 core/src/main/java/org/geysermc/floodgate/core/library/classloader/LibraryClassLoader.java create mode 100644 core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfo.java create mode 100644 core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfoLoader.java create mode 100644 core/src/main/resources/dependencyInfo.txt diff --git a/build-logic/src/main/kotlin/floodgate.dependency-hash.gradle.kts b/build-logic/src/main/kotlin/floodgate.dependency-hash.gradle.kts new file mode 100644 index 00000000..700e37ee --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate.dependency-hash.gradle.kts @@ -0,0 +1,93 @@ +import java.nio.file.Files +import java.nio.file.Path +import java.security.MessageDigest +import java.util.* + +plugins { + `java-library` +} + +tasks.register("listDependencyInfo") { + doLast { + listDependencies().forEach { + println(it) + } + } +} + +tasks.register("writeDependencyInfo") { + doLast { + Files.writeString( + dependenciesInfoFile(), + dependenciesToString(listDependencies()) + ) + } +} + +tasks.register("checkDependencyInfoFile") { + doLast { + val asString = dependenciesToString(listDependencies()) + if (!asString.contentEquals(Files.readString(dependenciesInfoFile()))) { + throw IllegalStateException("The dependency hashes file is outdated!") + } + println("The dependency hashes file is up-to-date") + } +} + +tasks.named("build") { + dependsOn("checkDependencyHashesFile") +} + +fun dependenciesToString(dependencies: Collection): String { + val builder = StringBuilder() + dependencies.forEach { + if (builder.length > 1) { + builder.append("\n") + } + builder.append(it) + } + return builder.toString() +} + +fun dependenciesInfoFile(): Path { + return project.projectDir.toPath().resolve("src/main/resources/dependencyInfo.txt") +} + +fun listDependencies(): Collection { + return listConfigurationDependencies(configurations.default.get()) +} + +fun listConfigurationDependencies(configuration: Configuration): Collection { + val deps = configuration.resolvedConfiguration.firstLevelModuleDependencies + .associateBy({"${it.moduleGroup}:${it.moduleName}"}, {it}) + + return configuration.resolvedConfiguration.resolvedArtifacts + .mapNotNull { + val id = it.id.componentIdentifier + if (id is ModuleComponentIdentifier && deps.containsKey("${id.group}:${id.module}")) { + convertDependency(id, it.file) + } else { + null + } + } +} + +fun convertDependency(id: ModuleComponentIdentifier, file: File): Dependency { + return Dependency(id.group, id.module, id.version, calculateSha256(file)) +} + +fun calculateSha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + return Base64.getEncoder().encodeToString(digest.digest(file.readBytes())) +} + +data class Dependency( + val groupId: String, + val artifactId: String, + val version: String, + val sha256: String +) { + override fun toString(): String { + return arrayOf(groupId, artifactId, version, sha256).joinToString(":") + } +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0ae742e4..6541274f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("floodgate.generate-templates") + id("floodgate.dependency-hash") id("io.micronaut.library") version "3.7.4" } diff --git a/core/src/main/java/org/geysermc/floodgate/core/FloodgatePlatform.java b/core/src/main/java/org/geysermc/floodgate/core/FloodgatePlatform.java index 4001935e..2f246332 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/FloodgatePlatform.java +++ b/core/src/main/java/org/geysermc/floodgate/core/FloodgatePlatform.java @@ -26,6 +26,7 @@ package org.geysermc.floodgate.core; import io.micronaut.context.ApplicationContext; +import java.nio.file.Paths; import java.util.Map; import java.util.UUID; import org.geysermc.floodgate.api.FloodgateApi; @@ -42,6 +43,10 @@ import org.geysermc.floodgate.core.database.entity.LinkedPlayer; import org.geysermc.floodgate.core.event.EventBus; import org.geysermc.floodgate.core.event.lifecycle.PostEnableEvent; import org.geysermc.floodgate.core.event.lifecycle.ShutdownEvent; +import org.geysermc.floodgate.core.library.Library; +import org.geysermc.floodgate.core.library.LibraryManager; +import org.geysermc.floodgate.core.library.Repository; +import org.geysermc.floodgate.core.library.info.DependencyInfoLoader; import org.geysermc.floodgate.core.util.EagerSingleton; public abstract class FloodgatePlatform { @@ -56,6 +61,21 @@ public abstract class FloodgatePlatform { public void load() { long startTime = System.currentTimeMillis(); + var infoLoader = DependencyInfoLoader.load( + getClass().getClassLoader().getResource("dependencyInfo.txt") + ); + + new LibraryManager(ClassLoader.getSystemClassLoader().getParent(), Paths.get("./libs")) + .addLibrary( + Library.builder(infoLoader) + .id("guava") + .repository(Repository.MAVEN_CENTRAL) + .groupId("com.google.guava") + .artifactId("guava") + .build() + ) + .apply(); + //noinspection unchecked context = ApplicationContext.builder() .properties(Map.of( diff --git a/core/src/main/java/org/geysermc/floodgate/core/library/Library.java b/core/src/main/java/org/geysermc/floodgate/core/library/Library.java new file mode 100644 index 00000000..1c613d15 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/library/Library.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.core.library; + +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.core.library.info.DependencyInfo; +import org.geysermc.floodgate.core.library.info.DependencyInfoLoader; +import org.geysermc.floodgate.core.util.Utils; + +public record Library( + @NonNull String id, + @NonNull Repository repository, + @NonNull String groupId, + @NonNull String artifactId, + @NonNull String version, + byte[] sha256 +) { + public Library { + Objects.requireNonNull(id); + Objects.requireNonNull(repository); + Objects.requireNonNull(groupId); + Objects.requireNonNull(artifactId); + Objects.requireNonNull(version); + } + + public String path() { + return "%s/%s/%s/%s-%s.jar".formatted( + groupId.replace('.', '/'), + artifactId, + version, + artifactId, + version + ); + } + + public static LibraryBuilder builder() { + return builder(null); + } + + public static LibraryBuilder builder(DependencyInfoLoader info) { + return new LibraryBuilder(info); + } + + public Path filePath() { + return Path.of(id(), id() + "-" + version() + ".jar"); + } + + public void validateChecksum(byte[] data) { + byte[] hash; + try { + hash = MessageDigest.getInstance("SHA-256").digest(data); + } catch (NoSuchAlgorithmException exception) { + throw new RuntimeException(exception); + } + + if (Arrays.equals(hash, sha256)) { + return; + } + + throw new IllegalStateException(String.format( + "Downloaded library hash (%s) didn't match expected hash (%s)!", + Utils.base64Encode(data), + Utils.base64Encode(sha256()) + )); + } + + public static class LibraryBuilder { + private final DependencyInfoLoader dependencyInfoLoader; + + private String id; + private Repository repository; + private String groupId; + private String artifactId; + + private String version; + private byte[] sha256; + + LibraryBuilder(DependencyInfoLoader info) { + this.dependencyInfoLoader = info; + } + + public LibraryBuilder id(String id) { + this.id = id; + return this; + } + + public LibraryBuilder repository(@NonNull Repository repository) { + this.repository = Objects.requireNonNull(repository); + return this; + } + + public LibraryBuilder groupId(String groupId) { + this.groupId = groupId; + return this; + } + + public LibraryBuilder artifactId(String artifactId) { + this.artifactId = artifactId; + return this; + } + + public LibraryBuilder version(String version) { + this.version = version; + return this; + } + + public LibraryBuilder sha256(byte[] sha256) { + this.sha256 = sha256; + return this; + } + + public LibraryBuilder sha256(String sha256) { + return sha256(Base64.getDecoder().decode(sha256)); + } + + public Library build() { + if (version == null || sha256 == null) { + if (dependencyInfoLoader == null) { + throw new IllegalStateException( + "Provide a version and hash or provide a DependencyInfoLoader" + ); + } + DependencyInfo dependencyInfo = dependencyInfoLoader.byCombinedId(groupId, artifactId); + version = dependencyInfo.version(); + sha256 = dependencyInfo.sha256(); + } + return new Library(id, repository, groupId, artifactId, version, sha256); + } + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/library/LibraryManager.java b/core/src/main/java/org/geysermc/floodgate/core/library/LibraryManager.java new file mode 100644 index 00000000..7c0245d4 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/library/LibraryManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.core.library; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.geysermc.floodgate.core.library.classloader.LibraryClassLoader; + +// Credits for the idea of downloading dependencies on runtime go to LuckPerms +public final class LibraryManager { + private final LibraryClassLoader classLoader; + private final Path cacheDirectory; + + private final Set toApply = new HashSet<>(); + + public LibraryManager(ClassLoader parent, Path cacheDirectory) { + this.classLoader = new LibraryClassLoader(parent); + this.cacheDirectory = cacheDirectory; + } + + public LibraryManager addLibrary(Library library) { + toApply.add(library); + return this; + } + + public void apply() { + CompletableFuture.allOf( + toApply.stream() + .map(library -> CompletableFuture.runAsync(() -> loadLibrary(library))) + .toArray(CompletableFuture[]::new) + ).join(); + } + + private void loadLibrary(Library library) { + var libPath = cacheDirectory.resolve(library.filePath()); + if (classLoader.isLoaded(libPath)) { + return; + } + + if (!Files.exists(libPath)) { + library.repository().downloadTo(library, libPath); + } + + classLoader.addPath(libPath); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/library/Repository.java b/core/src/main/java/org/geysermc/floodgate/core/library/Repository.java new file mode 100644 index 00000000..f9aa2bbb --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/library/Repository.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.core.library; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.geysermc.floodgate.core.util.HttpClient; +import org.geysermc.floodgate.core.util.HttpClient.HttpResponse; + +public enum Repository { + MAVEN_CENTRAL("https://repo1.maven.org/maven2/"); + + private final String baseUrl; + + Repository(String baseUrl) { + this.baseUrl = baseUrl; + } + + private byte[] justDownload(Library library) { + try { + HttpResponse response = new HttpClient().getRawData(baseUrl + library.path()); + + if (!response.isCodeOk()) { + throw new RuntimeException(String.format( + "Got an invalid response code (%s) while downloading library %s", + response.getHttpCode(), library.id() + )); + } + + return response.getResponse(); + } catch (IOException exception) { + throw new IllegalStateException("Failed to download library " + library.id(), exception); + } + } + + public byte[] download(Library library) { + byte[] data = justDownload(library); + library.validateChecksum(data); + return data; + } + + public void downloadTo(Library library, Path location) { + try { + Files.createDirectories(location.getParent()); + Files.write(location, download(library)); + } catch (IOException exception) { + throw new IllegalStateException("Failed to save library!", exception); + } + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/library/classloader/LibraryClassLoader.java b/core/src/main/java/org/geysermc/floodgate/core/library/classloader/LibraryClassLoader.java new file mode 100644 index 00000000..3a4df109 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/library/classloader/LibraryClassLoader.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.core.library.classloader; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class LibraryClassLoader extends URLClassLoader { + private final Set loadedPaths = new HashSet<>(); + + public LibraryClassLoader(ClassLoader parent) { + super(new URL[0], parent); + } + + @Override + public void addURL(URL url) { + super.addURL(url); + } + + public void addPath(Path path) { + try { + addURL(path.toUri().toURL()); + loadedPaths.add(path); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + public Set loadedPaths() { + return Collections.unmodifiableSet(loadedPaths); + } + + public boolean isLoaded(Path path) { + return loadedPaths.contains(path); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfo.java b/core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfo.java new file mode 100644 index 00000000..d13869d8 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfo.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.core.library.info; + +import java.util.Base64; + +public record DependencyInfo(String groupId, String artifactId, String version, byte[] sha256) { + public static DependencyInfo fromString(String string) { + var split = string.split(":"); + if (split.length != 4) { + throw new IllegalArgumentException("Invalid LibraryInfo string: " + string); + } + return new DependencyInfo(split[0], split[1], split[2], Base64.getDecoder().decode(split[3])); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfoLoader.java b/core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfoLoader.java new file mode 100644 index 00000000..537d89b3 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/core/library/info/DependencyInfoLoader.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.core.library.info; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +public final class DependencyInfoLoader { + private final Map infoMap = new HashMap<>(); + + private DependencyInfoLoader() {} + + public static DependencyInfoLoader load(URL infoUrl) { + var loader = new DependencyInfoLoader(); + + try (var reader = new BufferedReader(new InputStreamReader(infoUrl.openStream()))) { + reader.lines().forEach(line -> { + var info = DependencyInfo.fromString(line); + loader.infoMap.put(info.groupId() + ":" + info.artifactId(), info); + }); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + + return loader; + } + + public DependencyInfo byCombinedId(String groupId, String artifactId) { + return infoMap.get(groupId + ":" + artifactId); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/HttpClient.java b/core/src/main/java/org/geysermc/floodgate/core/util/HttpClient.java index b19d4ef9..126086a6 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/util/HttpClient.java +++ b/core/src/main/java/org/geysermc/floodgate/core/util/HttpClient.java @@ -30,9 +30,12 @@ import com.google.gson.JsonObject; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; import java.net.URL; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -46,8 +49,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; @SuppressWarnings({"PMD.CloseResource", "PMD.PreserveStackTrace"}) @Singleton public class HttpClient { - private static final String USER_AGENT = "GeyserMC/Floodgate"; - private final Gson gson = new Gson(); @Inject @Named("commonPool") @@ -86,7 +87,7 @@ public class HttpClient { try { connection.setRequestMethod("GET"); connection.setUseCaches(false); - connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("User-Agent", Constants.USER_AGENT); connection.setConnectTimeout(3000); connection.setReadTimeout(5000); } catch (Exception exception) { @@ -96,6 +97,26 @@ public class HttpClient { return connection; } + @NonNull + public HttpResponse getRawData(String urlString) throws IOException { + HttpURLConnection connection = request(urlString); + + try (InputStream inputStream = connection.getInputStream()) { + int responseCode = connection.getResponseCode(); + + byte[] buffer = new byte[8196]; + int len; + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + while ((len = inputStream.read(buffer, 0, buffer.length)) != -1) { + outputStream.write(buffer, 0, len); + } + return new HttpResponse<>(responseCode, outputStream.toByteArray()); + } + } catch (SocketTimeoutException | NullPointerException exception) { + return new HttpResponse<>(-1, null); + } + } + @NonNull private HttpResponse readResponse(HttpURLConnection connection, Class clazz) { InputStreamReader streamReader = createReader(connection); diff --git a/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java b/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java index b5e3707c..0d8dcf34 100644 --- a/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java +++ b/core/src/main/java/org/geysermc/floodgate/core/util/Utils.java @@ -28,21 +28,18 @@ package org.geysermc.floodgate.core.util; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.StringWriter; -import java.lang.annotation.Annotation; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Locale; import java.util.Properties; -import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class Utils { private static final Pattern NON_UNIQUE_PREFIX = Pattern.compile("^\\w{0,16}$"); @@ -138,41 +135,7 @@ public class Utils { return future; } - /** - * Returns a set of all the classes that are annotated by a given annotation. - * Keep in mind that these are from a set of generated annotations generated - * at compile time by the annotation processor, meaning that arbitrary annotations - * cannot be passed into this method and expected to get a set of classes back. - * - * @param annotationClass the annotation class - * @return a set of all the classes annotated by the given annotation - */ - public static Set> getGeneratedClassesForAnnotation(Class annotationClass) { - return getGeneratedClassesForAnnotation(annotationClass.getName()); - } - - /** - * Returns a set of all the classes that are annotated by a given annotation. - * Keep in mind that these are from a set of generated annotations generated - * at compile time by the annotation processor, meaning that arbitrary annotations - * cannot be passed into this method and expected to have a set of classes - * returned back. - * - * @param input the fully qualified name of the annotation - * @return a set of all the classes annotated by the given annotation - */ - public static Set> getGeneratedClassesForAnnotation(String input) { - try (InputStream annotatedClass = Utils.class.getClassLoader().getResourceAsStream(input); - BufferedReader reader = new BufferedReader(new InputStreamReader(annotatedClass))) { - return reader.lines().map(className -> { - try { - return Class.forName(className); - } catch (ClassNotFoundException ex) { - throw new RuntimeException("Failed to find class for annotation " + input, ex); - } - }).collect(Collectors.toSet()); - } catch (IOException e) { - throw new RuntimeException(e); - } + public static String base64Encode(byte[] data) { + return Base64.getEncoder().encodeToString(data); } } diff --git a/core/src/main/resources/dependencyInfo.txt b/core/src/main/resources/dependencyInfo.txt new file mode 100644 index 00000000..62a7f400 --- /dev/null +++ b/core/src/main/resources/dependencyInfo.txt @@ -0,0 +1,15 @@ +org.geysermc.configutils:configutils:1.0-SNAPSHOT:Xyh6oCOT1RwP5hw1gRM77M0mOJ86nNPf4J9Hs9PL4Ek= +com.google.inject:guice:5.1.0:QTDlC/rEgJnIYPDZA7kYYMgaJJyQ84JF+P7Vj8gXvCY= +com.nukkitx.fastutil:fastutil-short-object-maps:8.5.3:nTF5o2PCLMeBm8gaPeU4Q41FG2CNpmC9PxeJVHZot+U= +com.nukkitx.fastutil:fastutil-int-object-maps:8.5.3:Z0iKxf9OmoIcQtmqV9Lo9Z39XSpBXKjL8tavAdVCKBY= +org.java-websocket:Java-WebSocket:1.5.2:/4adGYqNxdAJZzkuGHrtqTuDO3zdO8zvs9f7WDQJi4U= +cloud.commandframework:cloud-core:1.5.0:+hDJlLwU84P6sf0gkDq+xStIqAHhMXUCI+Wb8VsD1Zk= +io.micronaut.sql:micronaut-jdbc-hikari:4.7.2:w2N1dH4vY/QjjQGsViMOwgVYxAnZNGgMm1doCa19wOA= +io.micronaut.data:micronaut-data-hibernate-jpa:3.9.6:kFYugtQeXfLjqUCWrf3MFFDoUdYhXrPQmSh0pu2Zxzo= +io.micronaut:micronaut-context:3.8.7:ir020Tq3UavnPZhXOUNHCl/XdGcVrOzhM1lU5NT6YEc= +io.micronaut:micronaut-inject-java:3.8.7:V7Dxe3ctZ8+c7ZUDQ3Cwtsqc6S7l2SE8+qTVhvhTHQs= +io.micronaut:micronaut-inject:3.8.7:6C+sHfQBffYWV/LnD+Vc5WhqRbkWZIgJXqxThca2tXE= +org.yaml:snakeyaml:2.0:iAydiW5LdKBsVJwVyklkUBZdaQn6FdfmYr7o9qZtevo= +org.bstats:bstats-base:3.0.1:vdyqrd84GBpK0MnfDnSXXMTauyzL9nkMSfAiMOrVU2Q= +com.google.guava:guava:31.1-jre:pC7cnKt5Ljn+ObuU8/ymVe0Vf/h6iveOHWulsHxKAKs= +com.h2database:h2:1.4.200:OtmsS2qunNnTrBxEdGXh7QYBm4UbiT3WqNdt222FvKY= \ No newline at end of file diff --git a/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java b/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java index b0fdf69b..111e5252 100644 --- a/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java +++ b/core/src/main/templates/org/geysermc/floodgate/core/util/Constants.java @@ -39,6 +39,7 @@ public final class Constants { public static final String DATABASE_NAME_FORMAT = "^floodgate-[a-zA-Z0-9_]{0,16}-database.jar$"; + public static final String USER_AGENT = "GeyserMC/Floodgate"; private static final String API_BASE_URL = "s://api.geysermc.org"; public static final String HEALTH_URL = "http" + API_BASE_URL + "/health";